add native ipv6 support

This commit is contained in:
ed 2022-11-06 16:48:05 +00:00
parent e442cb677a
commit c72753c5da
6 changed files with 119 additions and 34 deletions

View file

@ -144,6 +144,7 @@ recommended additional steps on debian which enable audio metadata and thumbnai
## features
* backend stuff
* ☑ IPv6
* ☑ [multiprocessing](#performance) (actual multithreading)
* ☑ volumes (mountpoints)
* ☑ [accounts](#accounts-and-volumes)
@ -223,6 +224,9 @@ browser-specific:
* Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now*
* Desktop-Firefox: may stop you from deleting files you've uploaded until you visit `about:memory` and click `Minimize memory usage`
server-os-specific:
* RHEL8 / Rocky8: you can run copyparty using `/usr/libexec/platform-python`
# bugs
@ -756,7 +760,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
and some minor issues,
* clients only see the first ~400 files in big folders; [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433)
* hot-reload of server config (`/?reload=cfg`) only works for volumes, not account passwords
* listens on the first `-i` interface only (default = 0.0.0.0 = all)
* listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all)
* login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work
* win10 onwards does not allow connecting anonymously / without accounts
* on windows, creating a new file through rightclick --> new --> textfile throws an error due to impacket limitations -- hit OK and F5 to get your file

View file

@ -613,7 +613,7 @@ def run_argparse(
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.), default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd), [\033[32m2\033[0m]=cloudflare, [\033[32m3\033[0m]=nginx, [\033[32m-1\033[0m]=closest proxy")
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")

View file

@ -1366,7 +1366,7 @@ class HttpCli(object):
url = "{}://{}/{}".format(
"https" if self.is_https else "http",
self.headers.get("host") or "{}:{}".format(*list(self.s.getsockname())),
self.headers.get("host") or "{}:{}".format(*list(self.s.getsockname()[:2])),
vpath + vsuf,
)
@ -1386,7 +1386,7 @@ class HttpCli(object):
else:
t = "{}\n{}\n{}\n{}\n".format(post_sz, sha_b64, sha_hex[:56], url)
h = {"Location": url} if is_put else {}
h = {"Location": url} if is_put and url else {}
self.reply(t.encode("utf-8"), 201, headers=h)
return True
@ -2037,7 +2037,7 @@ class HttpCli(object):
"url": "{}://{}/{}".format(
"https" if self.is_https else "http",
self.headers.get("host")
or "{}:{}".format(*list(self.s.getsockname())),
or "{}:{}".format(*list(self.s.getsockname()[:2])),
rel_url,
),
"sha512": sha_hex[:56],

View file

@ -174,7 +174,7 @@ class HttpSrv(object):
sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sck.settimeout(None) # < does not inherit, ^ does
ip, port = sck.getsockname()
ip, port = sck.getsockname()[:2]
self.srvs.append(sck)
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
Daemon(
@ -185,9 +185,10 @@ class HttpSrv(object):
def thr_listen(self, srv_sck: socket.socket) -> None:
"""listens on a shared tcp server"""
ip, port = srv_sck.getsockname()
ip, port = srv_sck.getsockname()[:2]
fno = srv_sck.fileno()
msg = "subscribed @ {}:{} f{} p{}".format(ip, port, fno, os.getpid())
hip = "[{}]".format(ip) if ":" in ip else ip
msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
self.log(self.name, msg)
def fun() -> None:
@ -261,7 +262,12 @@ class HttpSrv(object):
self.log(self.name, "|%sC-acc1" % ("-" * 2,), c="90")
try:
sck, addr = srv_sck.accept()
sck, saddr = srv_sck.accept()
cip, cport = saddr[:2]
if cip.startswith("::ffff:"):
cip = cip[7:]
addr = (cip, cport)
except (OSError, socket.error) as ex:
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
time.sleep(0.02)

View file

@ -75,7 +75,11 @@ class SMB(object):
smbserver.isInFileJail = self._is_in_file_jail
self._disarm()
ip = self.args.i[0]
ip = next((x for x in self.args.i if ":" not in x), None)
if not ip:
self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3)
ip = "0.0.0.0"
port = int(self.args.smb_port)
srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port)

View file

@ -14,6 +14,7 @@ from .util import (
E_ADDR_NOT_AVAIL,
E_UNREACH,
chkcmd,
min_ex,
sunpack,
termsize,
)
@ -43,25 +44,57 @@ class TcpSrv(object):
self.srv: list[socket.socket] = []
self.nsrv = 0
self.qr = ""
pad = False
ok: dict[str, list[int]] = {}
for ip in self.args.i:
ok[ip] = []
if ip == "::":
if socket.has_ipv6:
ips = ["::", "0.0.0.0"]
dual = True
else:
ips = ["0.0.0.0"]
dual = False
else:
ips = [ip]
dual = False
for ipa in ips:
ok[ipa] = []
for port in self.args.p:
self.nsrv += 1
successful_binds = 0
try:
self._listen(ip, port)
ok[ip].append(port)
for ipa in ips:
try:
self._listen(ipa, port)
ok[ipa].append(port)
successful_binds += 1
except:
if dual and ":" in ipa:
t = "listen on IPv6 [{}] failed; trying IPv4 {}...\n{}"
self.log("tcpsrv", t.format(ipa, ips[1], min_ex()), 3)
pad = True
continue
# binding 0.0.0.0 after :: fails on dualstack
# but is necessary on non-dualstakc
if successful_binds:
continue
raise
except Exception as ex:
if self.args.ign_ebind or self.args.ign_ebind_all:
t = "could not listen on {}:{}: {}"
self.log("tcpsrv", t.format(ip, port, ex), c=3)
pad = True
else:
raise
if not self.srv and not self.args.ign_ebind_all:
raise Exception("could not listen on any of the given interfaces")
if self.nsrv != len(self.srv):
if pad:
self.log("tcpsrv", "")
ip = "127.0.0.1"
@ -81,7 +114,11 @@ class TcpSrv(object):
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
for port in sorted(self.args.p):
if port not in ok.get(ip, ok.get("0.0.0.0", [])):
if (
port not in ok.get(ip, [])
and port not in ok.get("::", [])
and port not in ok.get("0.0.0.0", [])
):
continue
proto = " http"
@ -90,7 +127,8 @@ class TcpSrv(object):
elif self.args.https_only or port == 443:
proto = "https"
msgs.append(t.format(proto, ip, port, desc))
hip = "[{}]".format(ip) if ":" in ip else ip
msgs.append(t.format(proto, hip, port, desc))
is_ext = "external" in unicode(desc)
qrt = qr1 if is_ext else qr2
@ -125,18 +163,20 @@ class TcpSrv(object):
title_tab[tk] = {tv: 1}
if msgs:
msgs[-1] += "\n"
for t in msgs:
self.log("tcpsrv", t)
if self.args.wintitle:
self._set_wintitle(title_tab)
else:
print("\n", end="")
if self.args.qr or self.args.qrs:
self.qr = self._qr(qr1, qr2)
def _listen(self, ip: str, port: int) -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET
srv = socket.socket(ipv, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
srv.settimeout(None) # < does not inherit, ^ does
@ -153,17 +193,40 @@ class TcpSrv(object):
raise Exception(e)
def run(self) -> None:
all_eps = [x.getsockname()[:2] for x in self.srv]
bound = []
srvs = []
for srv in self.srv:
srv.listen(self.args.nc)
ip, port = srv.getsockname()
ip, port = srv.getsockname()[:2]
try:
srv.listen(self.args.nc)
except:
if ip == "0.0.0.0" and ("::", port) in bound:
# dualstack
srv.close()
continue
if ip == "::" and ("0.0.0.0", port) in all_eps:
# no ipv6
srv.close()
continue
raise
bound.append((ip, port))
srvs.append(srv)
fno = srv.fileno()
msg = "listening @ {}:{} f{} p{}".format(ip, port, fno, os.getpid())
hip = "[{}]".format(ip) if ":" in ip else ip
msg = "listening @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
self.log("tcpsrv", msg)
if self.args.q:
print(msg)
self.hub.broker.say("listen", srv)
self.srv = srvs
self.nsrv = len(srvs)
def shutdown(self) -> None:
self.stopping = True
try:
@ -209,19 +272,23 @@ class TcpSrv(object):
except:
return self.ips_linux_ifconfig()
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
ri = re.compile(r"^\s*[0-9]+\s*:.*")
r = re.compile(r"^\s+inet6? ([^ ]+)/")
ri = re.compile(r"^[0-9]+: ([^:]+): ")
dev = ""
up = False
eps: dict[str, str] = {}
for ln in txt.split("\n"):
if ri.match(ln):
m = ri.match(ln)
if m:
dev = m.group(1)
up = "UP" in re.split("[>,< ]", ln)
try:
ip, dev = r.match(ln.rstrip()).groups() # type: ignore
eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN")
except:
pass
m = r.match(ln.rstrip())
if not m or not dev or " scope link" in ln:
continue
ip = m.group(1)
eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN")
return eps
@ -314,7 +381,7 @@ class TcpSrv(object):
else:
eps = self.ips_linux()
if "0.0.0.0" not in listen_ips:
if "0.0.0.0" not in listen_ips and "::" not in listen_ips:
eps = {k: v for k, v in eps.items() if k in listen_ips}
try:
@ -323,14 +390,15 @@ class TcpSrv(object):
if not ext_ips:
raise Exception()
except:
ext_ips = [self._defroute()]
rt = self._defroute()
ext_ips = [rt] if rt else []
for lip in listen_ips:
if not ext_ips or lip not in ["0.0.0.0"] + ext_ips:
if not ext_ips or lip not in ["0.0.0.0", "::"] + ext_ips:
continue
desc = "\033[32mexternal"
ips = ext_ips if lip == "0.0.0.0" else [lip]
ips = ext_ips if lip in ["0.0.0.0", "::"] else [lip]
for ip in ips:
try:
if "external" not in eps[ip]:
@ -422,6 +490,9 @@ class TcpSrv(object):
if not ip:
return ""
if ":" in ip:
ip = "[{}]".format(ip)
if self.args.http_only:
https = ""
elif self.args.https_only: