From 3968c1a7b8a13d8c9da4d58dc9b0c436c8fb595d Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 5 Oct 2026 14:11:32 +0200 Subject: [PATCH] =?UTF-8?q?Modus=20proxychains=20pro=20Dienst=20hinzuf?= =?UTF-8?q?=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neben den Proxy-Variablen (env) kann pro Dienst jetzt der Modus proxychains gewählt werden: proxychains-ng wird per LD_PRELOAD geladen und leitet alle ausgehenden TCP-Verbindungen über den Proxy, auch bei Programmen ohne Proxy-Unterstützung (z.B. Kerio Connect SMTP/POP3). - eigene proxychains-Konfiguration je Dienst unter /etc/svc-proxy/proxychains/ - localhost, private Netze, DNS und IP-Einträge aus No-Proxy gehen direkt - Proxy-Hostname wird zu IPv4 aufgelöst, Kennwort-Datei an User= des Dienstes - Modus-Auswahl im ncurses-Formular, --mode für set, Anzeige in list/show - README und --help ergänzt - __pycache__ aus dem Repo entfernt, .gitignore angelegt Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 2 + README.md | 106 +++++++++- __pycache__/svc-proxycpython-311.pyc | Bin 90928 -> 0 bytes svc-proxy | 306 ++++++++++++++++++++++++--- 4 files changed, 375 insertions(+), 39 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/svc-proxycpython-311.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 65ee86f..a3d271d 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,20 @@ aufbaut. Bedienen lässt es sich auf zwei Wegen: - **ncurses-Menü**: listet alle Dienste auf. Man wählt einen aus und trägt die Daten bequem ein. - **Kommandozeile**: für Skripte, Ansible, Provisionierung usw. +Pro Dienst wählt man den **Modus**: + +| Modus | Wirkung | Geeignet für | +|---|---|---| +| `env` – Proxy-Variablen (Standard) | setzt `http_proxy`, `https_proxy`, … | Programme, die diese Variablen beachten (curl, apt, Docker, Python, Go, …) | +| `proxychains` | leitet **alle** ausgehenden TCP-Verbindungen des Dienstes über den Proxy | Programme ohne Proxy-Unterstützung, z.B. **Kerio Connect** (SMTP, POP3, IMAP) | + Es gibt einen **globalen Proxy**. Alle Felder, die man bei einem Dienst leer lässt, übernehmen den globalen Wert. Explizit gesetzte Werte beim Dienst gelten vorrangig. Benutzername und Kennwort sind optional; ohne sie wird der Proxy ohne Anmeldung genutzt. Abhängigkeiten: nur `python3` (≥ 3.6) mit den Standardmodulen. `curses` ist bei -Debian/Ubuntu/RHEL im Python-Paket enthalten. +Debian/Ubuntu/RHEL im Python-Paket enthalten. Für den Modus `proxychains` zusätzlich +`proxychains4` (Debian/Ubuntu) bzw. `proxychains-ng` (RHEL/Fedora). --- @@ -19,6 +27,7 @@ Debian/Ubuntu/RHEL im Python-Paket enthalten. ```bash sudo install -m 0755 svc-proxy /usr/local/sbin/svc-proxy +sudo apt install proxychains4 # nur für den Modus proxychains nötig ``` Danach `sudo svc-proxy` (Menü) oder `svc-proxy --help` (Hilfe). @@ -34,10 +43,18 @@ Für jeden konfigurierten Dienst werden zwei Dateien erzeugt: | `/etc/systemd/system/.d/50-svc-proxy.conf` | Drop-in mit `EnvironmentFile=` | 0644 | | `/etc/svc-proxy/env/.env` | die eigentlichen Proxy-Variablen | 0600 | +Im Modus `proxychains` kommt eine dritte Datei dazu: + +| Datei | Inhalt | Rechte | +|-------|--------|--------| +| `/etc/svc-proxy/proxychains/.conf` | proxychains-Konfiguration | 0644, mit Kennwort 0600 | + Die Konfiguration selbst (global + alle Dienste) liegt in `/etc/svc-proxy/config.json` (0600). Die Drop-ins und Env-Dateien werden daraus generiert und sollten nicht von Hand bearbeitet werden. +### Modus `env` (Proxy-Variablen) + Gesetzt werden die Variablen ``` @@ -63,7 +80,66 @@ Programm *kann* den Proxy umgehen. SOCKS-Proxys (`socks5://`, `socks5h://`) werden ebenfalls in alle Variablen geschrieben. Das funktioniert z.B. mit curl und Go. Manche Programme verstehen in -`http_proxy` aber nur `http://`. +`http_proxy` aber nur `http://`. Für solche Fälle gibt es den Modus `proxychains`. + +### Modus `proxychains` (alle TCP-Verbindungen) + +[proxychains-ng](https://github.com/rofl0r/proxychains-ng) wird per `LD_PRELOAD` +in den Dienst geladen. Die Bibliothek fängt jeden `connect()`-Aufruf ab und baut +die Verbindung stattdessen über den Proxy auf. Das Programm selbst muss nichts von +einem Proxy wissen. Das ist dasselbe wie `proxychains4 programm`, nur fest im +Dienst eingetragen. Die Env-Datei enthält dann: + +``` +LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libproxychains.so.4" +PROXYCHAINS_CONF_FILE="/etc/svc-proxy/proxychains/.conf" +PROXYCHAINS_QUIET_MODE="1" +``` + +**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` +- 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: + +- 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 + Speichern aufgelöst und die IP fest eingetragen. Ändert sich die IP des Proxys, + einmal `svc-proxy apply` ausführen. +- Benutzer/Kennwort dürfen hier keine Leerzeichen enthalten. +- Läuft der Dienst unter einem eigenen Benutzer (`User=`), bekommt dieser die + Datei mit dem Kennwort zugewiesen. Bei `DynamicUser=yes` geht das nicht, dann + einen Proxy ohne Anmeldung verwenden. +- Nur TCP wird umgeleitet, UDP nicht. Eingehende Verbindungen (z.B. MX-Empfang) + sind nicht betroffen. +- Statisch gelinkte Programme (z.B. viele Go-Binaries) ignorieren `LD_PRELOAD`. + Für die gibt es meist den Modus `env`. Prüfen mit `ldd /pfad/zum/programm`. +- Der Modus wird **nicht** vom globalen Proxy geerbt, er gilt immer nur für den + jeweiligen Dienst (Standard: `env`). + +### Beispiel: Kerio Connect + +Kerio beachtet keine Proxy-Variablen, und der Proxy in den Kerio-Einstellungen +gilt nur für Updates und Antivirus. Mit proxychains laufen auch SMTP-Versand und +POP3/IMAP-Abruf über den Proxy: + +```bash +sudo apt install proxychains4 +ldd /opt/kerio/mailserver/mailserver | grep libc # muss dynamisch gelinkt sein +sudo svc-proxy global --proxy socks5://192.168.1.50:1080 +sudo svc-proxy set kerio-connect --mode proxychains --restart +``` + +Beim Mailversand über einen Proxy gilt: Empfänger sehen die IP des Proxys. Diese +muss im SPF-Eintrag stehen und einen passenden Reverse-DNS haben, und der Proxy +muss ausgehend Port 25 erlauben. Alternativ in Kerio einen SMTP-Relay (Smarthost, +Port 587 mit Anmeldung) eintragen. Die Verbindung dorthin läuft dann ebenfalls +über den Proxy. --- @@ -108,7 +184,7 @@ sudo svc-proxy ``` Markierungen: **G** = nutzt globalen Proxy, **E** = eigene Werte, -**\*** = in dieser Sitzung geändert (Neustart noch offen). +**\*** = in dieser Sitzung geändert (Neustart noch offen), **[proxychains]** = Modus proxychains. ### Tasten in der Liste @@ -130,7 +206,7 @@ Markierungen: **G** = nutzt globalen Proxy, **E** = eigene Werte, | Taste | Funktion | |---|---| | Tab / ↑ ↓ | Feld wechseln | -| ← → / Leertaste | Proxy-Typ wählen (beim Dienst gibt es zusätzlich „global“) | +| ← → / Leertaste | Modus bzw. Proxy-Typ wählen (beim Typ gibt es zusätzlich „global“) | | Enter | nächstes Feld bzw. Schaltfläche auslösen | | F2 | Speichern | | Strg+U | Feld leeren | @@ -174,6 +250,7 @@ svc-proxy [BEFEHL] [OPTIONEN] | `--password PW` | Kennwort (in `ps` sichtbar!) | | `--password-stdin` | Kennwort aus der Standardeingabe lesen (empfohlen) | | `--no-proxy LISTE` | Ausnahmen, kommagetrennt | +| `--mode MODUS` | nur bei `set`: `env` (Standard) oder `proxychains` | - Bei `set` und `global` werden **nur die angegebenen Felder** geändert, der Rest bleibt. - Leerer Wert leert ein Feld: `--user ""`. @@ -202,6 +279,9 @@ Gestoppte Dienste werden nicht gestartet. | `--systemd-dir DIR` | anderes Ziel für Drop-ins (Standard `/etc/systemd/system`) | | `-V`, `--version` | Version | +Umgebungsvariable `SVC_PROXY_PROXYCHAINS_LIB`: Pfad zur proxychains-Bibliothek, +falls sie nicht automatisch gefunden wird. + Mit `--config-dir`/`--systemd-dir` läuft das Skript auch ohne root. Das ist praktisch zum Testen, z.B. zusammen mit `--no-reload`. @@ -234,6 +314,16 @@ sudo svc-proxy set myapp --proxy socks5h://10.1.1.1:1080 # keine Anmeldung (eigener Host ohne Benutzer) sudo svc-proxy set backup --host proxy2.firma.local +# Kerio Connect: alle Mail-Verbindungen über den (globalen) SOCKS5-Proxy +sudo svc-proxy set kerio-connect --mode proxychains --restart + +# Eigener Proxy mit Anmeldung im proxychains-Modus, Netz 192.0.2.0/24 direkt +echo 'geheim' | sudo svc-proxy set kerio-connect --mode proxychains \ + --proxy socks5://192.0.2.10:1080 --user max --password-stdin --no-proxy 192.0.2.0/24 + +# Zurück zu den Proxy-Variablen +sudo svc-proxy set kerio-connect --mode env + # Nur den Port ändern sudo svc-proxy set myapp --port 1081 --no-restart @@ -256,7 +346,8 @@ Beispielausgabe von `show`: ``` Dienst: ssh.service Status: active -Modus: eigene Werte (leere Felder = global) +Herkunft: eigene Werte (leere Felder = global) +Modus: Proxy-Variablen Proxy-Typ http (global) Host other.proxy (dienst) Port 3128 (global) @@ -285,6 +376,11 @@ fehlgeschlagener Neustart). ```bash systemctl cat docker.service # zeigt das Drop-in sudo cat /proc/$(systemctl show -p MainPID --value docker)/environ | tr '\0' '\n' | grep -i proxy + +# proxychains-Modus: ist die Bibliothek im Prozess geladen? +sudo grep proxychains /proc/$(systemctl show -p MainPID --value kerio-connect)/maps | head -1 +# laufende Verbindungen des Dienstes: Ziel sollte der Proxy sein +sudo ss -tnp | grep mailserver ``` ## Deinstallation diff --git a/__pycache__/svc-proxycpython-311.pyc b/__pycache__/svc-proxycpython-311.pyc deleted file mode 100644 index a9b30d8595d1b7be3c3ee55ee5e2f1247b28ace4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90928 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­6uF!$8KG#O);DNUX!k&ETu;z+ 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 diff --git a/svc-proxy b/svc-proxy index 9141a37..495a96e 100755 --- a/svc-proxy +++ b/svc-proxy @@ -9,16 +9,20 @@ Ausführliche Hilfe: svc-proxy --help import argparse import curses +import ipaddress import json import locale import os +import pwd import re +import socket import subprocess import sys import tempfile +from glob import glob as glob_paths from urllib.parse import quote, unquote, urlsplit -VERSION = "1.0.0" +VERSION = "1.1.0" PROG = "svc-proxy" DEFAULT_CONFIG_DIR = "/etc/svc-proxy" @@ -38,6 +42,34 @@ FIELD_LABELS = { } PROXY_ENV_VARS = ["http_proxy", "https_proxy", "ftp_proxy", "all_proxy"] +# Modus pro Dienst: Proxy-Variablen oder proxychains (LD_PRELOAD, alle TCP-Verbindungen) +MODES = ["env", "proxychains"] +MODE_LABELS = { + "env": "Proxy-Variablen", + "proxychains": "proxychains (alle TCP-Verbindungen)", +} +PROXYCHAINS_TYPES = {"http": "http", "socks4": "socks4", "socks5": "socks5", "socks5h": "socks5"} +PROXYCHAINS_LIB_GLOBS = [ + "/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 = [ + "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", + "fc00::/7", + "fe80::/10", + "0.0.0.0:53/0.0.0.0", + "[::]:53/0", +] + UNIT_RE = re.compile(r"^[A-Za-z0-9:_.@\\-]+$") HOST_RE = re.compile(r"^[A-Za-z0-9._:-]+$") NO_PROXY_RE = re.compile(r"^[A-Za-z0-9.,:*/_\[\]-]*$") @@ -45,10 +77,19 @@ NO_PROXY_RE = re.compile(r"^[A-Za-z0-9.,:*/_\[\]-]*$") HELP_TEXT = """\ {prog} {version} – Proxy für systemd-Dienste setzen -Hängt an beliebige systemd-Dienste ein Drop-in an, das die Umgebungsvariablen -http_proxy, https_proxy, ftp_proxy, all_proxy und no_proxy (jeweils auch in -GROSSBUCHSTABEN) setzt. Alle Programme, die diese Variablen beachten (curl, -wget, apt, Python, Go, Node, ...), laufen dann über den angegebenen Proxy. +Hängt an beliebige systemd-Dienste ein Drop-in an, das den Dienst über einen +Proxy laufen lässt. Pro Dienst gibt es zwei Modi: + + env (Standard) Setzt http_proxy, https_proxy, ftp_proxy, all_proxy und + no_proxy (auch in GROSSBUCHSTABEN). Wirkt bei allen Programmen, die + diese Variablen beachten (curl, wget, apt, Python, Go, Docker, ...). + proxychains Lädt proxychains-ng per LD_PRELOAD in den Dienst. Dadurch laufen ALLE + ausgehenden TCP-Verbindungen über den Proxy, auch bei Programmen, die + keine Proxy-Variablen kennen (z.B. Kerio Connect: SMTP, POP3, IMAP). + Direkt (ohne Proxy) bleiben: localhost, private Netze (10/8, 172.16/12, + 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). AUFRUF {prog} ncurses-Menü starten (wie '{prog} tui') @@ -79,6 +120,8 @@ FELDER (für 'global' und 'set') --password KENNWORT Kennwort (Achtung: in der Prozessliste sichtbar!) --password-stdin Kennwort von der Standardeingabe lesen (empfohlen) --no-proxy LISTE Kommagetrennte Ausnahmen, z.B. localhost,10.0.0.0/8,.intern + --mode MODUS (nur 'set') env | proxychains (Standard: env, wird nicht + vom globalen Proxy geerbt) Ein Feld mit leerem Wert (z.B. --user "") wird geleert. Bei 'set' werden nur die angegebenen Felder geändert, die übrigen bleiben erhalten. @@ -131,6 +174,17 @@ BEISPIELE # Eigener SOCKS-Proxy ohne Anmeldung für einen Dienst sudo {prog} set myapp --proxy socks5h://10.1.1.1:1080 + # Kerio Connect: alle Mailverbindungen über SOCKS5, Proxy-Daten vom globalen Proxy + sudo apt install proxychains4 + sudo {prog} set kerio-connect --mode proxychains --restart + + # Kerio mit eigenem SOCKS5-Proxy inkl. Anmeldung, LAN-Netz 192.0.2.0/24 direkt + sudo {prog} set kerio-connect --mode proxychains --proxy socks5://192.0.2.10:1080 \\ + --user max --password-stdin --no-proxy 192.0.2.0/24 + + # Zurück zu den Proxy-Variablen + sudo {prog} set kerio-connect --mode env + # Nur den Port eines Dienstes ändern (Rest bleibt) sudo {prog} set myapp --port 1081 --no-restart @@ -147,6 +201,7 @@ BEISPIELE DATEIEN {cfg}/config.json Konfiguration (global + Dienste, 0600) {cfg}/env/DIENST.env erzeugte Umgebungsdatei (0600) + {cfg}/proxychains/DIENST.conf proxychains-Konfiguration (Modus proxychains) {sysd}/DIENST.d/{dropin} erzeugtes Drop-in Dienstnamen ohne Endung erhalten automatisch '.service'. @@ -163,7 +218,9 @@ class ProxyError(Exception): # -------------------------------------------------------------------------- def empty_settings(): - return {k: "" for k in FIELDS} + s = {k: "" for k in FIELDS} + s["mode"] = "" # nur bei Diensten benutzt, wird nicht vererbt + return s def normalize_unit(name): @@ -245,7 +302,13 @@ def validate_settings(raw, is_global): if not NO_PROXY_RE.match(s["no_proxy"]): raise ProxyError("Ungültige No-Proxy-Liste: %s" % s["no_proxy"]) + s["mode"] = (raw.get("mode") or "").strip().lower() + if s["mode"] and s["mode"] not in MODES: + raise ProxyError("Unbekannter Modus '%s' (erlaubt: %s)" % (s["mode"], ", ".join(MODES))) + if is_global: + if s["mode"]: + raise ProxyError("Den Modus gibt es nur pro Dienst, nicht global") if any(s[k] for k in ("host", "port", "user", "password")): if not s["host"]: raise ProxyError("Globaler Proxy: Host fehlt") @@ -283,13 +346,85 @@ def resolve(svc, glob): eff["user"], eff["password"] = glob["user"], glob["password"] src["user"] = src["password"] = "global" + eff["mode"] = svc.get("mode") or "env" + src["mode"] = "dienst" if svc.get("mode") else "standard" + if not eff["host"]: raise ProxyError("kein Proxy-Host (weder beim Dienst noch global gesetzt)") if not eff["port"]: raise ProxyError("kein Proxy-Port (weder beim Dienst noch global gesetzt)") + if eff["mode"] == "proxychains": + if eff["type"] not in PROXYCHAINS_TYPES: + raise ProxyError("proxychains kann keinen '%s'-Proxy (nur http, socks4, socks5)" % eff["type"]) + if re.search(r"\s", eff["user"] + eff["password"]): + raise ProxyError("proxychains: Benutzer/Kennwort dürfen keine Leerzeichen enthalten") return eff, src +def find_proxychains_lib(): + override = os.environ.get("SVC_PROXY_PROXYCHAINS_LIB") + if override: + return override + 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)") + + +def proxy_ipv4(host): + """proxychains akzeptiert in der ProxyList nur IPv4-Adressen.""" + try: + addr = ipaddress.ip_address(host) + except ValueError: + addr = None + if addr is not None: + if addr.version != 4: + raise ProxyError("proxychains braucht eine IPv4-Adresse für den Proxy, nicht %s" % host) + return str(addr) + try: + infos = socket.getaddrinfo(host, None, socket.AF_INET, socket.SOCK_STREAM) + except socket.gaierror as e: + raise ProxyError("Proxy-Host %s nicht auflösbar (%s) – proxychains braucht eine IPv4-Adresse" + % (host, e.strerror)) + return infos[0][4][0] + + +def proxychains_localnets(no_proxy): + """Standard-Ausnahmen plus alle IP-/Netz-Einträge aus der No-Proxy-Liste.""" + nets = list(PROXYCHAINS_LOCALNETS) + for entry in no_proxy.split(","): + entry = entry.strip().strip("[]") + try: + net = ipaddress.ip_network(entry, strict=False) + except ValueError: + continue # Hostnamen/Domains kann proxychains nicht ausnehmen + if net.version == 4: + item = "%s/%s" % (net.network_address, net.netmask) + else: + item = "%s/%d" % (net.network_address, net.prefixlen) + if item not in nets: + nets.append(item) + return nets + + +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"]) + + +def service_user(unit): + """User= des Dienstes (leer = root).""" + rc, out, _ = run_cmd(["systemctl", "show", "-p", "User", "-p", "DynamicUser", unit]) + if rc != 0: + return "", False + vals = dict(line.split("=", 1) for line in out.splitlines() if "=" in line) + return vals.get("User", ""), vals.get("DynamicUser", "no") == "yes" + + def proxy_url(eff, mask=False): auth = "" if eff.get("user"): @@ -330,6 +465,10 @@ def is_inheriting(svc): return not any(svc[k] for k in FIELDS) +def mode_suffix(eff): + return " [proxychains]" if eff["mode"] == "proxychains" else "" + + # -------------------------------------------------------------------------- # Konfiguration und Erzeugen der Dateien # -------------------------------------------------------------------------- @@ -339,13 +478,21 @@ class Config: self.config_dir = config_dir self.config_file = os.path.join(config_dir, "config.json") self.env_dir = os.path.join(config_dir, "env") + self.pc_dir = os.path.join(config_dir, "proxychains") self.systemd_dir = systemd_dir self.global_ = empty_settings() self.services = {} # Pfade + @staticmethod + def safe_name(unit): + return re.sub(r"[^A-Za-z0-9:_.@-]", "_", unit) + def env_file(self, unit): - return os.path.join(self.env_dir, re.sub(r"[^A-Za-z0-9:_.@-]", "_", unit) + ".env") + return os.path.join(self.env_dir, self.safe_name(unit) + ".env") + + def pc_file(self, unit): + return os.path.join(self.pc_dir, self.safe_name(unit) + ".conf") def dropin_dir(self, unit): return os.path.join(self.systemd_dir, unit + ".d") @@ -365,35 +512,46 @@ class Config: except (OSError, ValueError) as e: raise ProxyError("Konfiguration %s nicht lesbar: %s" % (self.config_file, e)) - def clean(d): + def clean(d, keys): s = empty_settings() - for k in FIELDS: + for k in keys: if isinstance(d, dict) and isinstance(d.get(k), str): s[k] = d[k] return s - self.global_ = clean(data.get("global")) + self.global_ = clean(data.get("global"), FIELDS) self.services = {} for unit, s in (data.get("services") or {}).items(): if UNIT_RE.match(unit): - self.services[unit] = clean(s) + self.services[unit] = clean(s, FIELDS + ["mode"]) + + def ensure_config_dir(self): + # 0711: Dienste mit eigenem Benutzer müssen ihre proxychains-Datei erreichen, + # dürfen das Verzeichnis aber nicht auflisten. config.json und env/ bleiben 0600/0700. + os.makedirs(self.config_dir, mode=0o711, exist_ok=True) + os.chmod(self.config_dir, 0o711) def save(self): - os.makedirs(self.config_dir, mode=0o700, exist_ok=True) + self.ensure_config_dir() data = { "version": 1, - "global": self.global_, + "global": {k: self.global_[k] for k in FIELDS}, "services": {u: self.services[u] for u in sorted(self.services)}, } atomic_write(self.config_file, json.dumps(data, indent=2, ensure_ascii=False) + "\n", 0o600) # Drop-ins def render_env(self, unit, eff): - url = proxy_url(eff) lines = [ "# Erzeugt von %s fuer %s - Aenderungen werden ueberschrieben." % (PROG, unit), "# Bearbeiten mit: %s set %s ..." % (PROG, unit), ] + if eff["mode"] == "proxychains": + lines.append('LD_PRELOAD="%s"' % find_proxychains_lib()) + lines.append('PROXYCHAINS_CONF_FILE="%s"' % self.pc_file(unit)) + lines.append('PROXYCHAINS_QUIET_MODE="1"') + return "\n".join(lines) + "\n" + url = proxy_url(eff) for var in PROXY_ENV_VARS: lines.append('%s="%s"' % (var, url)) lines.append('%s="%s"' % (var.upper(), url)) @@ -402,6 +560,53 @@ class Config: lines.append('NO_PROXY="%s"' % eff["no_proxy"]) return "\n".join(lines) + "\n" + def render_proxychains(self, unit, eff): + entry = "%s %s %s" % (PROXYCHAINS_TYPES[eff["type"]], proxy_ipv4(eff["host"]), eff["port"]) + if eff["user"]: + entry += " %s %s" % (eff["user"], eff["password"]) + lines = [ + "# Erzeugt von %s fuer %s - Aenderungen werden ueberschrieben." % (PROG, unit), + "# Proxy: %s (%s)" % (proxy_url(eff, mask=True), eff["host"]), + "strict_chain", + "quiet_mode", + "tcp_read_time_out 15000", + "tcp_connect_time_out 8000", + "# Kein proxy_dns: DNS wird normal aufgeloest (wichtig z.B. fuer MX-Abfragen).", + "", + "# Diese Ziele werden direkt (ohne Proxy) verbunden:", + ] + lines += ["localnet " + n for n in proxychains_localnets(eff["no_proxy"])] + lines += ["", "[ProxyList]", entry] + return "\n".join(lines) + "\n" + + def write_proxychains(self, unit, eff): + """Schreibt die proxychains-Datei. Liefert (geändert, warnung).""" + self.ensure_config_dir() + os.makedirs(self.pc_dir, mode=0o755, exist_ok=True) + os.chmod(self.pc_dir, 0o755) + path = self.pc_file(unit) + secret = bool(eff["user"]) + changed = write_if_different(path, self.render_proxychains(unit, eff), 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. + user, dynamic = service_user(unit) + if secret and os.geteuid() == 0: + uid = gid = 0 + if dynamic: + warning = ("%s: nutzt DynamicUser – die proxychains-Datei mit Kennwort ist für den " + "Dienst nicht lesbar. Proxy ohne Anmeldung verwenden." % unit) + elif user and user != "root": + try: + pw = pwd.getpwnam(user) + uid, gid = pw.pw_uid, pw.pw_gid + except KeyError: + warning = "%s: Benutzer '%s' unbekannt – proxychains-Datei evtl. nicht lesbar" % (unit, user) + st = os.stat(path) + if (st.st_uid, st.st_gid) != (uid, gid): + os.chown(path, uid, gid) + return changed, warning + def render_dropin(self, unit): return ( "# Erzeugt von %s - nicht manuell bearbeiten.\n" @@ -413,6 +618,7 @@ class Config: def remove_unit_files(self, unit): changed = remove_file(self.dropin_file(unit)) changed |= remove_file(self.env_file(unit)) + changed |= remove_file(self.pc_file(unit)) try: os.rmdir(self.dropin_dir(unit)) except OSError: @@ -424,14 +630,22 @@ class Config: svc = self.services.get(unit) if svc is None: return self.remove_unit_files(unit), None + warning = None try: eff, _ = resolve(svc, self.global_) + changed = False + if eff["mode"] == "proxychains": + changed, warning = self.write_proxychains(unit, eff) + else: + changed = remove_file(self.pc_file(unit)) + env = self.render_env(unit, eff) except ProxyError as e: return self.remove_unit_files(unit), "%s: %s – Proxy für diesen Dienst deaktiviert" % (unit, e) + self.ensure_config_dir() os.makedirs(self.env_dir, mode=0o700, exist_ok=True) - changed = write_if_different(self.env_file(unit), self.render_env(unit, eff), 0o600) + changed |= write_if_different(self.env_file(unit), env, 0o600) changed |= write_if_different(self.dropin_file(unit), self.render_dropin(unit), 0o644) - return changed, None + return changed, warning def managed_dropins(self): """Units, für die ein Drop-in von svc-proxy existiert.""" @@ -549,12 +763,14 @@ def is_backspace(k): class FormField: - def __init__(self, key, label, value, kind="text", choices=None, secret=False, placeholder=""): + def __init__(self, key, label, value, kind="text", choices=None, secret=False, placeholder="", + display=None): self.key = key self.label = label self.kind = kind # text | choice | button self.value = value self.choices = choices or [] + self.display = display or {} self.secret = secret self.placeholder = placeholder self.pos = len(value) @@ -659,8 +875,8 @@ class TUI: except ProxyError as e: return "FEHLER: " + str(e), self.c_err if is_inheriting(svc): - return "global → " + proxy_url(eff, mask=True), self.c_global - return "eigen → " + proxy_url(eff, mask=True), self.c_own + return "global → " + proxy_url(eff, mask=True) + mode_suffix(eff), self.c_global + return "eigen → " + proxy_url(eff, mask=True) + mode_suffix(eff), self.c_own # ---------- Hauptliste ---------- def draw_main(self): @@ -959,6 +1175,11 @@ class TUI: "Benutzer/Kennwort werden nur geerbt, wenn auch der Host global ist.", "Eigener Host ohne Benutzer = Proxy ohne Anmeldung.", "", + "Modus (pro Dienst):", + " Proxy-Variablen setzt http_proxy & Co. – nur für Programme, die sie beachten", + " proxychains leitet ALLE TCP-Verbindungen des Dienstes über den Proxy", + " (z.B. Kerio SMTP/POP3). Lokale/private Netze und DNS gehen direkt.", + "", "Tasten: Enter bearbeiten, g global, Entf/d entfernen, / filtern,", "c nur konfigurierte, r Dienst neu starten, F5 Liste neu laden, q Ende.", "Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt.", @@ -983,7 +1204,11 @@ class TUI: else: type_choices = [""] + PROXY_TYPES tval = settings["type"] - fields = [FormField("type", "Proxy-Typ", tval, kind="choice", choices=type_choices)] + fields = [] + if not is_global: + fields.append(FormField("mode", "Modus", settings["mode"] or "env", kind="choice", + choices=MODES, display=MODE_LABELS)) + fields.append(FormField("type", "Proxy-Typ", tval, kind="choice", choices=type_choices)) for key in ("host", "port", "user", "password", "no_proxy"): fields.append(FormField(key, FIELD_LABELS[key], settings[key], secret=(key == "password"), placeholder=ph(key))) @@ -993,7 +1218,7 @@ class TUI: buttons.append(FormField("remove", "Proxy entfernen" if not is_global else "Global löschen", "", kind="button")) items = fields + buttons - cur = 1 # Host + cur = [f.key for f in fields].index("host") error = "" def collect(): @@ -1016,13 +1241,17 @@ class TUI: input_x = 2 + label_w + 3 input_w = max(10, min(w - input_x - 2, 60)) cursor_pos = None + step = 2 if h >= 4 + len(fields) * 2 + 9 else 1 for i, f in enumerate(fields): - y = 4 + i * 2 + y = 4 + i * step focused = (i == cur) put(scr, y, 2, f.label.rjust(label_w), curses.A_BOLD if focused else 0) put(scr, y, 2 + label_w, " : ") if f.kind == "choice": - shown = f.value if f.value else "(global: %s)" % (g["type"] or "http") + if f.display: + shown = f.display.get(f.value, f.value) + else: + shown = f.value if f.value else "(global: %s)" % (g["type"] or "http") text = "◀ %s ▶" % shown put(scr, y, input_x, text.ljust(input_w), self.c_sel if focused else curses.A_UNDERLINE) else: @@ -1039,7 +1268,7 @@ class TUI: if focused: cursor_pos = (y, input_x + f.pos - f.off) - by = 4 + len(fields) * 2 + by = 4 + len(fields) * step bx = input_x for j, b in enumerate(buttons): focused = (len(fields) + j == cur) @@ -1055,7 +1284,7 @@ class TUI: preview = proxy_url(cand, mask=True) if cand["host"] else "(kein globaler Proxy)" else: eff, _ = resolve(cand, g) - preview = proxy_url(eff, mask=True) + preview = proxy_url(eff, mask=True) + mode_suffix(eff) if eff["no_proxy"]: preview += " No-Proxy: " + eff["no_proxy"] put(scr, py, 2, "Ergebnis: ", curses.A_BOLD) @@ -1066,12 +1295,15 @@ class TUI: if error: put(scr, py + 1, 2, error, self.c_err) if not is_global: - put(scr, py + 3, 2, "Leere Felder = globaler Wert. Benutzer/Kennwort werden nur geerbt,", self.c_dim) - put(scr, py + 4, 2, "wenn auch der Host leer ist; eigener Host ohne Benutzer = ohne Anmeldung.", self.c_dim) + put(scr, py + 2, 2, "Leere Felder = globaler Wert. Benutzer/Kennwort werden nur geerbt,", self.c_dim) + put(scr, py + 3, 2, "wenn auch der Host leer ist; eigener Host ohne Benutzer = ohne Anmeldung.", self.c_dim) + if fields[0].value == "proxychains": + put(scr, py + 4, 2, "proxychains: alle TCP-Verbindungen über den Proxy; direkt bleiben localhost,", self.c_dim) + put(scr, py + 5, 2, "private Netze, DNS (Port 53) und IP-/Netz-Einträge aus No-Proxy.", self.c_dim) else: put(scr, py + 3, 2, "Benutzer/Kennwort leer = Proxy ohne Anmeldung.", self.c_dim) - self.key_bar("Tab/↑↓ Feld ←→ Typ Enter weiter F2 Speichern Strg+U Feld leeren Esc Abbrechen") + self.key_bar("Tab/↑↓ Feld ←→ Auswahl Enter weiter F2 Speichern Strg+U Feld leeren Esc Abbrechen") if cursor_pos and items[cur].kind == "text": self.cursor(True) try: @@ -1148,7 +1380,8 @@ class TUI: try: cand = validate_settings(collect(), is_global) if not is_global: - resolve(cand, g) + eff, _ = resolve(cand, g) + check_mode(eff) except ProxyError as e: error = "Nicht gespeichert: %s" % e continue @@ -1210,7 +1443,7 @@ def fields_from_args(args): vals = {} if args.proxy is not None: vals.update(parse_proxy_url(args.proxy)) - for key in ("type", "host", "port", "user", "password", "no_proxy"): + for key in ("type", "host", "port", "user", "password", "no_proxy", "mode"): v = getattr(args, key) if v is not None: vals[key] = v @@ -1248,11 +1481,12 @@ def cmd_set(cfg, args): svc = validate_settings(base, is_global=False) try: eff, _ = resolve(svc, cfg.global_) + check_mode(eff) except ProxyError as e: raise ProxyError("%s: %s" % (unit, e)) staged[unit] = svc - print("%s -> %s%s" % (unit, proxy_url(eff, mask=True), - " (global)" if is_inheriting(svc) else "")) + print("%s -> %s%s%s" % (unit, proxy_url(eff, mask=True), + " (global)" if is_inheriting(svc) else "", mode_suffix(eff))) cfg.services.update(staged) cfg.save() changed, warnings = set(), [] @@ -1318,7 +1552,7 @@ def cmd_list(cfg, args): mark = "G" if is_inheriting(svc) else "E" try: eff, _ = resolve(svc, cfg.global_) - text = proxy_url(eff, mask=True) + text = proxy_url(eff, mask=True) + mode_suffix(eff) except ProxyError as e: text = "FEHLER: %s" % e print(" %s %-*s %-10s %s" % (mark, width, name, state, text)) @@ -1347,12 +1581,13 @@ def cmd_show(cfg, args): if svc is None: print("Proxy: keiner") return 0 - print("Modus: %s" % ("nutzt globalen Proxy" if is_inheriting(svc) else "eigene Werte (leere Felder = global)")) + print("Herkunft: %s" % ("nutzt globalen Proxy" if is_inheriting(svc) else "eigene Werte (leere Felder = global)")) try: eff, src = resolve(svc, cfg.global_) except ProxyError as e: print("FEHLER: %s" % e) return 1 + print("Modus: %s" % MODE_LABELS[eff["mode"]]) for key in FIELDS: val = eff[key] if key == "password" and val: @@ -1361,6 +1596,8 @@ def cmd_show(cfg, args): print("Proxy-URL: %s" % proxy_url(eff, mask=True)) print("Drop-in: %s" % cfg.dropin_file(unit)) print("Env-Datei: %s" % cfg.env_file(unit)) + if eff["mode"] == "proxychains": + print("proxychains-Konfiguration: %s" % cfg.pc_file(unit)) return 0 @@ -1387,6 +1624,7 @@ def build_parser(): fields.add_argument("--password") fields.add_argument("--password-stdin", action="store_true") fields.add_argument("--no-proxy", dest="no_proxy") + fields.add_argument("--mode", type=str.lower, choices=MODES) parser = argparse.ArgumentParser(prog=PROG, add_help=False, parents=[common(False)]) parser.add_argument("-V", "--version", action="version", version="%s %s" % (PROG, VERSION))