Modus proxychains pro Dienst hinzufügen

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) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-10-05 14:11:32 +02:00
co-authored by Claude Opus 5.5
parent 2d786a0270
commit 3968c1a7b8
4 changed files with 375 additions and 39 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+101 -5
View File
@@ -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/<dienst>.d/50-svc-proxy.conf` | Drop-in mit `EnvironmentFile=` | 0644 |
| `/etc/svc-proxy/env/<dienst>.env` | die eigentlichen Proxy-Variablen | 0600 |
Im Modus `proxychains` kommt eine dritte Datei dazu:
| Datei | Inhalt | Rechte |
|-------|--------|--------|
| `/etc/svc-proxy/proxychains/<dienst>.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/<dienst>.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
Binary file not shown.
+272 -34
View File
@@ -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))