From 3862aae27e3b52851c8cff133d356c114f6ec551 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 5 Oct 2026 14:42:36 +0200 Subject: [PATCH] =?UTF-8?q?proxychains-Konfiguration=20vorab=20pr=C3=BCfen?= =?UTF-8?q?,=20alte=20Versionen=20unterst=C3=BCtzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxychains-ng 4.12/4.14 (Debian 11, Ubuntu 20.04) kennt keine IPv6- localnet-Einträge und bricht beim Dienststart mit "localnet netmask error" ab, wodurch z.B. Kerio Connect nicht mehr startete. - jede erzeugte Konfiguration wird vor dem Installieren von der installierten Bibliothek eingelesen; bei Ablehnung wird nichts installiert (Warnung bzw. Fehler bei 'set') - IPv6-Fähigkeit der Bibliothek wird erkannt; ohne sie werden die IPv6-Ausnahmen weggelassen und deutlich gewarnt (auch in show/test) - selbst kompilierte Bibliothek unter /usr/local/lib wird bevorzugt - README: Abschnitt zu alten Versionen und Bauanleitung für 4.17 Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 37 ++++++++++++++- svc-proxy | 135 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 150 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3803dde..001cd5b 100644 --- a/README.md +++ b/README.md @@ -96,16 +96,49 @@ PROXYCHAINS_CONF_FILE="/etc/svc-proxy/proxychains/.conf" PROXYCHAINS_QUIET_MODE="1" ``` +**Sicherheitsprüfung:** proxychains liest seine Konfiguration schon beim Start +des Dienstes. Ein Fehler darin (z.B. `localnet netmask error`) bricht den Dienst ab. +Deshalb lässt svc-proxy jede erzeugte Konfiguration vorher von der installierten +Bibliothek einlesen. Lehnt sie die Konfiguration ab, wird für den Dienst nichts +installiert und eine Warnung ausgegeben. Der Dienst startet dann normal ohne Proxy. + **Direkt (ohne Proxy) verbunden werden immer:** -- localhost (`127.0.0.0/8`, `::1`) -- private Netze: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `fc00::/7`, `fe80::/10` +- localhost (`127.0.0.0/8`, `::1`\*) +- private Netze: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `fc00::/7`\*, `fe80::/10`\* - DNS (Port 53, jedes Ziel), damit z.B. MX-Abfragen normal funktionieren - alle **IP-Adressen und Netze** aus der No-Proxy-Liste (z.B. `192.0.2.0/24`). Hostnamen/Domains aus No-Proxy kann proxychains nicht berücksichtigen. Hinweise: +\* IPv6-Ausnahmen nur mit proxychains-ng **ab 4.16**, siehe nächster Abschnitt. + +#### Ältere proxychains-Versionen (Debian 11, Ubuntu 20.04) + +proxychains-ng **4.12/4.14** kennt keine IPv6-Ausnahmen. Mit einer solchen Zeile +bricht es mit `localnet netmask error` ab, und Verbindungen zu `::1` schickt es +über den Proxy. svc-proxy erkennt das: Die IPv6-Zeilen werden weggelassen, und +es gibt eine Warnung (auch in `show` und `test`). IPv4 funktioniert normal. Spricht +der Dienst intern aber über `::1`/localhost via IPv6, schlagen diese Verbindungen fehl. + +Lösung: eine aktuelle Version selbst bauen. svc-proxy bevorzugt automatisch die +Bibliothek unter `/usr/local/lib`: + +```bash +sudo apt install build-essential git +git clone https://github.com/rofl0r/proxychains-ng.git +cd proxychains-ng +git checkout v4.17 +./configure --prefix=/usr/local --sysconfdir=/etc +make +sudo make install # -> /usr/local/lib/libproxychains4.so +sudo svc-proxy apply # Konfigurationen neu erzeugen +sudo svc-proxy show kerio-connect # zeigt Bibliothek und "IPv6-Ausnahmen: ja" +``` + +Weitere Hinweise: + - Proxy-Typen: `http` (CONNECT), `socks4`, `socks5`. `socks5h` wird wie `socks5` behandelt. Einen `https`-Proxy kann proxychains nicht. - proxychains braucht für den Proxy eine **IPv4-Adresse**. Ein Hostname wird beim diff --git a/svc-proxy b/svc-proxy index dbe8d7c..198dcef 100755 --- a/svc-proxy +++ b/svc-proxy @@ -16,12 +16,14 @@ import locale import os import pwd import re +import shutil import socket import ssl import struct import subprocess import sys import tempfile +import textwrap import time from glob import glob as glob_paths from urllib.parse import quote, unquote, urlsplit @@ -54,23 +56,28 @@ MODE_LABELS = { } PROXYCHAINS_TYPES = {"http": "http", "socks4": "socks4", "socks5": "socks5", "socks5h": "socks5"} PROXYCHAINS_LIB_GLOBS = [ + # selbst kompilierte (meist neuere) Version zuerst + "/usr/local/lib/libproxychains4.so", + "/usr/local/lib*/libproxychains4.so", "/usr/lib/*/libproxychains.so.4", "/usr/lib/libproxychains.so.4", "/usr/lib*/libproxychains4.so", "/usr/lib*/proxychains-ng/libproxychains4.so", - "/usr/local/lib/libproxychains4.so", ] # Ziele, die im proxychains-Modus immer direkt (ohne Proxy) erreicht werden -PROXYCHAINS_LOCALNETS = [ +PROXYCHAINS_LOCALNETS_V4 = [ "127.0.0.0/255.0.0.0", - "::1/128", "10.0.0.0/255.0.0.0", "172.16.0.0/255.240.0.0", "192.168.0.0/255.255.0.0", "169.254.0.0/255.255.0.0", + "0.0.0.0:53/0.0.0.0", +] +# IPv6-Ausnahmen versteht proxychains-ng erst ab 4.16 (ältere brechen mit "localnet netmask error" ab) +PROXYCHAINS_LOCALNETS_V6 = [ + "::1/128", "fc00::/7", "fe80::/10", - "0.0.0.0:53/0.0.0.0", "[::]:53/0", ] @@ -117,6 +124,11 @@ Proxy laufen lässt. Pro Dienst gibt es zwei Modi: 192.168/16, fc00::/7, Link-Local), DNS (Port 53) und alle IP-/Netz- Einträge aus der No-Proxy-Liste. Benötigt: apt install proxychains4 Proxy-Typen: http, socks4, socks5 (socks5h wird wie socks5 behandelt). + Jede erzeugte Konfiguration wird vorab mit der installierten Bibliothek + geprüft; lehnt sie sie ab, wird nichts installiert. + IPv6-Ausnahmen (::1 usw.) kann proxychains erst ab Version 4.16 + (Debian 11/Ubuntu 20.04 haben 4.14) – dann gibt es eine Warnung. + Eine selbst kompilierte Version unter /usr/local/lib wird bevorzugt. AUFRUF {prog} ncurses-Menü starten (wie '{prog} tui') @@ -417,16 +429,61 @@ def resolve(svc, glob): return eff, src +_PC_CHECK_CACHE = {} + + +def proxychains_check_conf(lib, conf_text): + """Lässt die Bibliothek eine Konfiguration einlesen, so wie beim Dienststart + (proxychains liest sie schon beim Laden). Liefert None oder die Fehlermeldung.""" + key = (lib, conf_text) + if key in _PC_CHECK_CACHE: + return _PC_CHECK_CACHE[key] + fd, path = tempfile.mkstemp(prefix="svc-proxy-check-", suffix=".conf") + try: + with os.fdopen(fd, "w") as f: + f.write(conf_text) + env = dict(os.environ, LD_PRELOAD=lib, PROXYCHAINS_CONF_FILE=path, PROXYCHAINS_QUIET_MODE="1") + try: + r = subprocess.run([shutil.which("true") or "/bin/true"], env=env, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True, timeout=15) + err = r.stderr.strip() + if r.returncode != 0: + result = err or "Exit-Code %d" % r.returncode + elif "preload" in err or "ld.so" in err: + result = err # Bibliothek wurde gar nicht geladen (falsche Architektur o.ä.) + else: + result = None + except (OSError, subprocess.TimeoutExpired) as e: + result = "Prüfung nicht möglich: %s" % e + finally: + os.unlink(path) + _PC_CHECK_CACHE[key] = result + return result + + +def proxychains_ipv6_ok(lib): + test = "strict_chain\nquiet_mode\nlocalnet ::1/128\n[ProxyList]\nsocks5 127.0.0.1 1080\n" + return proxychains_check_conf(lib, test) is None + + def find_proxychains_lib(): + """Pfad der proxychains-Bibliothek; bevorzugt eine Version mit IPv6-Ausnahmen.""" override = os.environ.get("SVC_PROXY_PROXYCHAINS_LIB") if override: return override + found = [] for pattern in PROXYCHAINS_LIB_GLOBS: for path in sorted(glob_paths(pattern)): - if os.path.isfile(path): - return path - raise ProxyError("proxychains ist nicht installiert (Debian/Ubuntu: apt install proxychains4, " - "RHEL/Fedora: dnf install proxychains-ng)") + real = os.path.realpath(path) + if os.path.isfile(real) and real not in [os.path.realpath(f) for f in found]: + found.append(path) + if not found: + raise ProxyError("proxychains ist nicht installiert (Debian/Ubuntu: apt install proxychains4, " + "RHEL/Fedora: dnf install proxychains-ng)") + for path in found: + if proxychains_ipv6_ok(path): + return path + return found[0] def proxy_ipv4(host): @@ -447,9 +504,9 @@ def proxy_ipv4(host): return infos[0][4][0] -def proxychains_localnets(no_proxy): +def proxychains_localnets(no_proxy, ipv6=True): """Standard-Ausnahmen plus alle IP-/Netz-Einträge aus der No-Proxy-Liste.""" - nets = list(PROXYCHAINS_LOCALNETS) + nets = list(PROXYCHAINS_LOCALNETS_V4) + (list(PROXYCHAINS_LOCALNETS_V6) if ipv6 else []) for entry in no_proxy.split(","): entry = entry.strip().strip("[]") try: @@ -458,8 +515,10 @@ def proxychains_localnets(no_proxy): continue # Hostnamen/Domains kann proxychains nicht ausnehmen if net.version == 4: item = "%s/%s" % (net.network_address, net.netmask) - else: + elif ipv6: item = "%s/%d" % (net.network_address, net.prefixlen) + else: + continue if item not in nets: nets.append(item) return nets @@ -468,8 +527,8 @@ def proxychains_localnets(no_proxy): def check_mode(eff): """Prüft vor dem Speichern, ob der gewählte Modus nutzbar ist.""" if eff["mode"] == "proxychains": - find_proxychains_lib() - proxy_ipv4(eff["host"]) + # rendert und prüft die Konfiguration mit der installierten Bibliothek + Config(DEFAULT_CONFIG_DIR, DEFAULT_SYSTEMD_DIR).build_proxychains("check", eff) def service_user(unit): @@ -616,7 +675,26 @@ class Config: lines.append('NO_PROXY="%s"' % eff["no_proxy"]) return "\n".join(lines) + "\n" - def render_proxychains(self, unit, eff): + def build_proxychains(self, unit, eff): + """Erzeugt die proxychains-Konfiguration und lässt sie von der installierten + Bibliothek prüfen. Liefert (text, warnungen); wirft ProxyError, wenn die + Bibliothek sie ablehnt – dann wird nichts installiert.""" + lib = find_proxychains_lib() + ipv6 = proxychains_ipv6_ok(lib) + text = self.render_proxychains(unit, eff, ipv6) + err = proxychains_check_conf(lib, text) + if err: + raise ProxyError("proxychains (%s) lehnt die erzeugte Konfiguration ab: %s" % (lib, err)) + warnings = [] + if not ipv6: + warnings.append( + "%s: Die installierte proxychains-Version (%s) kann keine IPv6-Ausnahmen " + "(erst ab 4.16). IPv6-Verbindungen – auch zu ::1/localhost – laufen über den Proxy " + "und schlagen dann fehl. Empfehlung: neuere proxychains-ng installieren (siehe README)." + % (unit, lib)) + return text, warnings + + def render_proxychains(self, unit, eff, ipv6=True): entry = "%s %s %s" % (PROXYCHAINS_TYPES[eff["type"]], proxy_ipv4(eff["host"]), eff["port"]) if eff["user"]: entry += " %s %s" % (eff["user"], eff["password"]) @@ -631,7 +709,9 @@ class Config: "", "# Diese Ziele werden direkt (ohne Proxy) verbunden:", ] - lines += ["localnet " + n for n in proxychains_localnets(eff["no_proxy"])] + if not ipv6: + lines.append("# ACHTUNG: diese proxychains-Version kennt keine IPv6-Ausnahmen (erst ab 4.16).") + lines += ["localnet " + n for n in proxychains_localnets(eff["no_proxy"], ipv6)] lines += ["", "[ProxyList]", entry] return "\n".join(lines) + "\n" @@ -640,9 +720,10 @@ class Config: self.ensure_config_dir() os.makedirs(self.pc_dir, mode=0o755, exist_ok=True) os.chmod(self.pc_dir, 0o755) + text, warnings = self.build_proxychains(unit, eff) path = self.pc_file(unit) secret = bool(eff["user"]) - changed = write_if_different(path, self.render_proxychains(unit, eff), 0o600 if secret else 0o644) + changed = write_if_different(path, text, 0o600 if secret else 0o644) os.chmod(path, 0o600 if secret else 0o644) warning = None # Die Bibliothek liest die Datei im Dienst-Prozess, also mit dessen Benutzer. @@ -661,7 +742,9 @@ class Config: st = os.stat(path) if (st.st_uid, st.st_gid) != (uid, gid): os.chown(path, uid, gid) - return changed, warning + if warning: + warnings.append(warning) + return changed, "\n".join(warnings) or None def render_dropin(self, unit): return ( @@ -1094,11 +1177,14 @@ def run_proxy_test(eff, targets, cfg=None, unit=None, timeout=TEST_TIMEOUT): if eff["mode"] == "proxychains": try: - conf = (cfg or Config(DEFAULT_CONFIG_DIR, DEFAULT_SYSTEMD_DIR)).render_proxychains(unit or "test", eff) + conf, warnings = (cfg or Config(DEFAULT_CONFIG_DIR, DEFAULT_SYSTEMD_DIR)).build_proxychains( + unit or "test", eff) except ProxyError as e: - results.append(("fail", "proxychains-Test: %s" % e)) + results.append(("fail", "proxychains: %s" % e)) else: results.append(("info", "Test über proxychains (so verbindet der Dienst):")) + for w in warnings: + results.append(("fail", "proxychains: " + w.split(": ", 1)[-1])) results += proxychains_selftest(conf, targets, timeout) return results @@ -1381,7 +1467,7 @@ class TUI: self.message("Fehler", [str(e)], error=True) return if warnings: - self.message("Hinweis", list(warnings), error=True) + self.message("Hinweis", [l for w in warnings for l in w.split("\n")], error=True) def edit_service(self, unit): svc = self.cfg.services.get(unit) @@ -1469,6 +1555,9 @@ class TUI: return win, bh, bw def message(self, title, lines, error=False): + _, w = self.scr.getmaxyx() + width = max(20, min(w - 8, 100)) + lines = [part for l in lines for part in (textwrap.wrap(l, width) or [""])] win, bh, bw = self.box(title, lines) for i, l in enumerate(lines[:bh - 4]): put(win, 1 + i, 2, l, self.c_err if error else 0, bw - 4) @@ -2097,6 +2186,12 @@ def cmd_show(cfg, args): print("Env-Datei: %s" % cfg.env_file(unit)) if eff["mode"] == "proxychains": print("proxychains-Konfiguration: %s" % cfg.pc_file(unit)) + try: + lib = find_proxychains_lib() + print("proxychains-Bibliothek: %s (IPv6-Ausnahmen: %s)" + % (lib, "ja" if proxychains_ipv6_ok(lib) else "NEIN – Version zu alt, siehe README")) + except ProxyError as e: + print("proxychains-Bibliothek: FEHLER: %s" % e) return 0