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 a9b30d8..0000000 Binary files a/__pycache__/svc-proxycpython-311.pyc and /dev/null differ 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))