support listening on unix sockets

This commit is contained in:
ed 2024-08-12 21:58:02 +00:00
parent 2d6eb63fce
commit ee9aad82dd
9 changed files with 121 additions and 26 deletions

View file

@ -196,7 +196,7 @@ firewall-cmd --reload
also see [comparison to similar software](./docs/versus.md) also see [comparison to similar software](./docs/versus.md)
* backend stuff * backend stuff
* ☑ IPv6 * ☑ IPv6 + unix-sockets
* ☑ [multiprocessing](#performance) (actual multithreading) * ☑ [multiprocessing](#performance) (actual multithreading)
* ☑ volumes (mountpoints) * ☑ volumes (mountpoints)
* ☑ [accounts](#accounts-and-volumes) * ☑ [accounts](#accounts-and-volumes)
@ -1459,6 +1459,8 @@ some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatical
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now * **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2 * depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
for improved security (and a tiny performance boost) consider listening on a unix-socket with `-i /tmp/party.sock` instead of `-i 127.0.0.1`
example webserver configs: example webserver configs:
* [nginx config](contrib/nginx/copyparty.conf) -- entire domain/subdomain * [nginx config](contrib/nginx/copyparty.conf) -- entire domain/subdomain
@ -1898,6 +1900,7 @@ some notes on hardening
* cors doesn't work right otherwise * cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml` * if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
* this returns html documents as plaintext, and also disables markdown rendering * this returns html documents as plaintext, and also disables markdown rendering
* when running behind a reverse-proxy, listen on a unix-socket with `-i /tmp/party.sock` instead of `-i 127.0.0.1` for tighter access control (plus you get a tiny performance boost for free)
safety profiles: safety profiles:

View file

@ -969,8 +969,8 @@ def add_upload(ap):
def add_network(ap): def add_network(ap):
ap2 = ap.add_argument_group('network options') ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.), default: all IPv4 and IPv6") ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.) and/or [\033[32munix:/tmp/a.sock\033[0m], default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range); ignored for unix-sockets")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
@ -1394,6 +1394,7 @@ def add_debug(ap):
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead") ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests") ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks") ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")

View file

@ -586,9 +586,15 @@ class Ftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x]
if self.args.ftp4: if self.args.ftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]
if not ips:
lgr.fatal("cannot start ftp-server; no compatible IPs in -i")
return
ips = list(ODict.fromkeys(ips)) # dedup ips = list(ODict.fromkeys(ips)) # dedup
ioloop = IOLoop() ioloop = IOLoop()

View file

@ -13,6 +13,7 @@ import json
import os import os
import random import random
import re import re
import socket
import stat import stat
import string import string
import threading # typechk import threading # typechk
@ -314,8 +315,11 @@ class HttpCli(object):
) )
self.host = self.headers.get("host") or "" self.host = self.headers.get("host") or ""
if not self.host: if not self.host:
zs = "%s:%s" % self.s.getsockname()[:2] if self.s.family == socket.AF_UNIX:
self.host = zs[7:] if zs.startswith("::ffff:") else zs self.host = self.args.name
else:
zs = "%s:%s" % self.s.getsockname()[:2]
self.host = zs[7:] if zs.startswith("::ffff:") else zs
trusted_xff = False trusted_xff = False
n = self.args.rproxy n = self.args.rproxy

View file

@ -243,15 +243,24 @@ class HttpSrv(object):
return return
def listen(self, sck: socket.socket, nlisteners: int) -> None: def listen(self, sck: socket.socket, nlisteners: int) -> None:
tcp = sck.family != socket.AF_UNIX
if self.args.j != 1: if self.args.j != 1:
# lost in the pickle; redefine # lost in the pickle; redefine
if not ANYWIN or self.args.reuseaddr: if not ANYWIN or self.args.reuseaddr:
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if tcp:
sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sck.settimeout(None) # < does not inherit, ^ opts above do sck.settimeout(None) # < does not inherit, ^ opts above do
ip, port = sck.getsockname()[:2] if tcp:
ip, port = sck.getsockname()[:2]
else:
ip = re.sub(r"\.[0-9]+$", "", sck.getsockname().split("/")[-1])
port = 0
self.srvs.append(sck) self.srvs.append(sck)
self.bound.add((ip, port)) self.bound.add((ip, port))
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners) self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
@ -263,10 +272,19 @@ class HttpSrv(object):
def thr_listen(self, srv_sck: socket.socket) -> None: def thr_listen(self, srv_sck: socket.socket) -> None:
"""listens on a shared tcp server""" """listens on a shared tcp server"""
ip, port = srv_sck.getsockname()[:2]
fno = srv_sck.fileno() fno = srv_sck.fileno()
hip = "[{}]".format(ip) if ":" in ip else ip if srv_sck.family == socket.AF_UNIX:
msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid()) ip = re.sub(r"\.[0-9]+$", "", srv_sck.getsockname())
msg = "subscribed @ %s f%d p%d" % (ip, fno, os.getpid())
ip = ip.split("/")[-1]
port = 0
tcp = False
else:
tcp = True
ip, port = srv_sck.getsockname()[:2]
hip = "[%s]" % (ip,) if ":" in ip else ip
msg = "subscribed @ %s:%d f%d p%d" % (hip, port, fno, os.getpid())
self.log(self.name, msg) self.log(self.name, msg)
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",)) Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
@ -338,11 +356,13 @@ class HttpSrv(object):
try: try:
sck, saddr = srv_sck.accept() sck, saddr = srv_sck.accept()
cip = unicode(saddr[0]) if tcp:
if cip.startswith("::ffff:"): cip = unicode(saddr[0])
cip = cip[7:] if cip.startswith("::ffff:"):
cip = cip[7:]
addr = (cip, saddr[1]) addr = (cip, saddr[1])
else:
addr = (ip, sck.fileno())
except (OSError, socket.error) as ex: except (OSError, socket.error) as ex:
if self.stopping: if self.stopping:
break break

View file

@ -17,14 +17,16 @@ from .util import (
E_UNREACH, E_UNREACH,
HAVE_IPV6, HAVE_IPV6,
IP6ALL, IP6ALL,
VF_CAREFUL,
Netdev, Netdev,
atomic_move,
min_ex, min_ex,
sunpack, sunpack,
termsize, termsize,
) )
if True: if True:
from typing import Generator from typing import Generator, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -217,14 +219,29 @@ class TcpSrv(object):
if self.args.qr or self.args.qrs: if self.args.qr or self.args.qrs:
self.qr = self._qr(qr1, qr2) self.qr = self._qr(qr1, qr2)
def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
self.log("tcpsrv", msg, c)
def _listen(self, ip: str, port: int) -> None: def _listen(self, ip: str, port: int) -> None:
ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET if "unix:" in ip:
tcp = False
ipv = socket.AF_UNIX
ip = ip.split("unix:")[1]
elif ":" in ip:
tcp = True
ipv = socket.AF_INET6
else:
tcp = True
ipv = socket.AF_INET
srv = socket.socket(ipv, socket.SOCK_STREAM) srv = socket.socket(ipv, socket.SOCK_STREAM)
if not ANYWIN or self.args.reuseaddr: if not ANYWIN or self.args.reuseaddr:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if tcp:
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
srv.settimeout(None) # < does not inherit, ^ opts above do srv.settimeout(None) # < does not inherit, ^ opts above do
try: try:
@ -236,8 +253,19 @@ class TcpSrv(object):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1) srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
try: try:
srv.bind((ip, port)) if tcp:
sport = srv.getsockname()[1] srv.bind((ip, port))
else:
if ANYWIN or self.args.rm_sck:
if os.path.exists(ip):
os.unlink(ip)
srv.bind(ip)
else:
tf = "%s.%d" % (ip, os.getpid())
srv.bind(tf)
atomic_move(self.nlog, tf, ip, VF_CAREFUL)
sport = srv.getsockname()[1] if tcp else port
if port != sport: if port != sport:
# linux 6.0.16 lets you bind a port which is in use # linux 6.0.16 lets you bind a port which is in use
# except it just gives you a random port instead # except it just gives you a random port instead
@ -249,12 +277,23 @@ class TcpSrv(object):
except: except:
pass pass
e = ""
if ex.errno in E_ADDR_IN_USE: if ex.errno in E_ADDR_IN_USE:
e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip) e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip)
if not tcp:
e = "\033[1;31munix-socket {} is busy\033[0m".format(ip)
elif ex.errno in E_ADDR_NOT_AVAIL: elif ex.errno in E_ADDR_NOT_AVAIL:
e = "\033[1;31minterface {} does not exist\033[0m".format(ip) e = "\033[1;31minterface {} does not exist\033[0m".format(ip)
else:
if not e:
if not tcp:
t = "\n\n\n NOTE: this crash may be due to a unix-socket bug; try --rm-sck\n"
self.log("tcpsrv", t, 2)
raise raise
if not tcp and not self.args.rm_sck:
e += "; maybe this is a bug? try --rm-sck"
raise Exception(e) raise Exception(e)
def run(self) -> None: def run(self) -> None:
@ -262,7 +301,14 @@ class TcpSrv(object):
bound: list[tuple[str, int]] = [] bound: list[tuple[str, int]] = []
srvs: list[socket.socket] = [] srvs: list[socket.socket] = []
for srv in self.srv: for srv in self.srv:
ip, port = srv.getsockname()[:2] if srv.family == socket.AF_UNIX:
tcp = False
ip = re.sub(r"\.[0-9]+$", "", srv.getsockname())
port = 0
else:
tcp = True
ip, port = srv.getsockname()[:2]
if ip == IP6ALL: if ip == IP6ALL:
ip = "::" # jython ip = "::" # jython
@ -294,8 +340,12 @@ class TcpSrv(object):
bound.append((ip, port)) bound.append((ip, port))
srvs.append(srv) srvs.append(srv)
fno = srv.fileno() fno = srv.fileno()
hip = "[{}]".format(ip) if ":" in ip else ip if tcp:
msg = "listening @ {}:{} f{} p{}".format(hip, port, fno, os.getpid()) hip = "[{}]".format(ip) if ":" in ip else ip
msg = "listening @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
else:
msg = "listening @ {} f{} p{}".format(ip, fno, os.getpid())
self.log("tcpsrv", msg) self.log("tcpsrv", msg)
if self.args.q: if self.args.q:
print(msg) print(msg)
@ -348,6 +398,8 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if "unix:" not in x]
nics = get_adapters(True) nics = get_adapters(True)
eps: dict[str, Netdev] = {} eps: dict[str, Netdev] = {}
for nic in nics: for nic in nics:

View file

@ -166,9 +166,16 @@ class Tftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x]
if self.args.tftp4: if self.args.tftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]
if not ips:
t = "cannot start tftp-server; no compatible IPs in -i"
self.nlog(t, 1)
return
ips = list(ODict.fromkeys(ips)) # dedup ips = list(ODict.fromkeys(ips)) # dedup
for ip in ips: for ip in ips:

View file

@ -221,7 +221,7 @@ symbol legend,
| serve sftp (ssh) | | | | | | █ | | | | | | █ | █ | | serve sftp (ssh) | | | | | | █ | | | | | | █ | █ |
| serve smb/cifs | | | | | | █ | | | | | | | | | serve smb/cifs | | | | | | █ | | | | | | | |
| serve dlna | | | | | | █ | | | | | | | | | serve dlna | | | | | | █ | | | | | | | |
| listen on unix-socket | | | | █ | █ | | █ | █ | █ | █ | █ | █ | | | listen on unix-socket | | | | █ | █ | | █ | █ | █ | █ | █ | █ | |
| zeroconf | █ | | | | | | | | | | | | █ | | zeroconf | █ | | | | | | | | | | | | █ |
| supports netscape 4 | | | | | | █ | | | | | • | | | | supports netscape 4 | | | | | | █ | | | | | • | | |
| ...internet explorer 6 | | █ | | █ | | █ | | | | | • | | | | ...internet explorer 6 | | █ | | █ | | █ | | | | | • | | |

View file

@ -6,6 +6,7 @@ import os
import platform import platform
import re import re
import shutil import shutil
import socket
import subprocess as sp import subprocess as sp
import sys import sys
import tempfile import tempfile
@ -124,7 +125,7 @@ class Cfg(Namespace):
ex = "dotpart dotsrch hook_v no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip" ex = "dotpart dotsrch hook_v no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()}) ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua" ex = "ah_cli ah_gen css_browser hist js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "hash_mt srch_time u2abort u2j u2sz" ex = "hash_mt srch_time u2abort u2j u2sz"
@ -200,6 +201,7 @@ class VSock(object):
def __init__(self, buf): def __init__(self, buf):
self._query = buf self._query = buf
self._reply = b"" self._reply = b""
self.family = socket.AF_INET
self.sendall = self.send self.sendall = self.send
def recv(self, sz): def recv(self, sz):