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:
co-authored by
Claude Opus 5.5
parent
2d786a0270
commit
3968c1a7b8
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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.
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user