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. - **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. - **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, 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. übernehmen den globalen Wert. Explizit gesetzte Werte beim Dienst gelten vorrangig.
Benutzername und Kennwort sind optional; ohne sie wird der Proxy ohne Anmeldung genutzt. 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 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 ```bash
sudo install -m 0755 svc-proxy /usr/local/sbin/svc-proxy 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). 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/systemd/system/<dienst>.d/50-svc-proxy.conf` | Drop-in mit `EnvironmentFile=` | 0644 |
| `/etc/svc-proxy/env/<dienst>.env` | die eigentlichen Proxy-Variablen | 0600 | | `/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 Die Konfiguration selbst (global + alle Dienste) liegt in
`/etc/svc-proxy/config.json` (0600). Die Drop-ins und Env-Dateien werden daraus `/etc/svc-proxy/config.json` (0600). Die Drop-ins und Env-Dateien werden daraus
generiert und sollten nicht von Hand bearbeitet werden. generiert und sollten nicht von Hand bearbeitet werden.
### Modus `env` (Proxy-Variablen)
Gesetzt werden die Variablen Gesetzt werden die Variablen
``` ```
@@ -63,7 +80,66 @@ Programm *kann* den Proxy umgehen.
SOCKS-Proxys (`socks5://`, `socks5h://`) werden ebenfalls in alle Variablen SOCKS-Proxys (`socks5://`, `socks5h://`) werden ebenfalls in alle Variablen
geschrieben. Das funktioniert z.B. mit curl und Go. Manche Programme verstehen in 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, 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 ### Tasten in der Liste
@@ -130,7 +206,7 @@ Markierungen: **G** = nutzt globalen Proxy, **E** = eigene Werte,
| Taste | Funktion | | Taste | Funktion |
|---|---| |---|---|
| Tab / ↑ ↓ | Feld wechseln | | 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 | | Enter | nächstes Feld bzw. Schaltfläche auslösen |
| F2 | Speichern | | F2 | Speichern |
| Strg+U | Feld leeren | | Strg+U | Feld leeren |
@@ -174,6 +250,7 @@ svc-proxy [BEFEHL] [OPTIONEN]
| `--password PW` | Kennwort (in `ps` sichtbar!) | | `--password PW` | Kennwort (in `ps` sichtbar!) |
| `--password-stdin` | Kennwort aus der Standardeingabe lesen (empfohlen) | | `--password-stdin` | Kennwort aus der Standardeingabe lesen (empfohlen) |
| `--no-proxy LISTE` | Ausnahmen, kommagetrennt | | `--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. - Bei `set` und `global` werden **nur die angegebenen Felder** geändert, der Rest bleibt.
- Leerer Wert leert ein Feld: `--user ""`. - 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`) | | `--systemd-dir DIR` | anderes Ziel für Drop-ins (Standard `/etc/systemd/system`) |
| `-V`, `--version` | Version | | `-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 Mit `--config-dir`/`--systemd-dir` läuft das Skript auch ohne root. Das ist
praktisch zum Testen, z.B. zusammen mit `--no-reload`. 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) # keine Anmeldung (eigener Host ohne Benutzer)
sudo svc-proxy set backup --host proxy2.firma.local 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 # Nur den Port ändern
sudo svc-proxy set myapp --port 1081 --no-restart sudo svc-proxy set myapp --port 1081 --no-restart
@@ -256,7 +346,8 @@ Beispielausgabe von `show`:
``` ```
Dienst: ssh.service Dienst: ssh.service
Status: active Status: active
Modus: eigene Werte (leere Felder = global) Herkunft: eigene Werte (leere Felder = global)
Modus: Proxy-Variablen
Proxy-Typ http (global) Proxy-Typ http (global)
Host other.proxy (dienst) Host other.proxy (dienst)
Port 3128 (global) Port 3128 (global)
@@ -285,6 +376,11 @@ fehlgeschlagener Neustart).
```bash ```bash
systemctl cat docker.service # zeigt das Drop-in systemctl cat docker.service # zeigt das Drop-in
sudo cat /proc/$(systemctl show -p MainPID --value docker)/environ | tr '\0' '\n' | grep -i proxy 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 ## Deinstallation
Binary file not shown.
+272 -34
View File
@@ -9,16 +9,20 @@ Ausführliche Hilfe: svc-proxy --help
import argparse import argparse
import curses import curses
import ipaddress
import json import json
import locale import locale
import os import os
import pwd
import re import re
import socket
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from glob import glob as glob_paths
from urllib.parse import quote, unquote, urlsplit from urllib.parse import quote, unquote, urlsplit
VERSION = "1.0.0" VERSION = "1.1.0"
PROG = "svc-proxy" PROG = "svc-proxy"
DEFAULT_CONFIG_DIR = "/etc/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"] 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:_.@\\-]+$") UNIT_RE = re.compile(r"^[A-Za-z0-9:_.@\\-]+$")
HOST_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.,:*/_\[\]-]*$") 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 = """\ HELP_TEXT = """\
{prog} {version} – Proxy für systemd-Dienste setzen {prog} {version} – Proxy für systemd-Dienste setzen
Hängt an beliebige systemd-Dienste ein Drop-in an, das die Umgebungsvariablen Hängt an beliebige systemd-Dienste ein Drop-in an, das den Dienst über einen
http_proxy, https_proxy, ftp_proxy, all_proxy und no_proxy (jeweils auch in Proxy laufen lässt. Pro Dienst gibt es zwei Modi:
GROSSBUCHSTABEN) setzt. Alle Programme, die diese Variablen beachten (curl,
wget, apt, Python, Go, Node, ...), laufen dann über den angegebenen Proxy. 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 AUFRUF
{prog} ncurses-Menü starten (wie '{prog} tui') {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 KENNWORT Kennwort (Achtung: in der Prozessliste sichtbar!)
--password-stdin Kennwort von der Standardeingabe lesen (empfohlen) --password-stdin Kennwort von der Standardeingabe lesen (empfohlen)
--no-proxy LISTE Kommagetrennte Ausnahmen, z.B. localhost,10.0.0.0/8,.intern --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 Ein Feld mit leerem Wert (z.B. --user "") wird geleert. Bei 'set' werden nur
die angegebenen Felder geändert, die übrigen bleiben erhalten. die angegebenen Felder geändert, die übrigen bleiben erhalten.
@@ -131,6 +174,17 @@ BEISPIELE
# Eigener SOCKS-Proxy ohne Anmeldung für einen Dienst # Eigener SOCKS-Proxy ohne Anmeldung für einen Dienst
sudo {prog} set myapp --proxy socks5h://10.1.1.1:1080 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) # Nur den Port eines Dienstes ändern (Rest bleibt)
sudo {prog} set myapp --port 1081 --no-restart sudo {prog} set myapp --port 1081 --no-restart
@@ -147,6 +201,7 @@ BEISPIELE
DATEIEN DATEIEN
{cfg}/config.json Konfiguration (global + Dienste, 0600) {cfg}/config.json Konfiguration (global + Dienste, 0600)
{cfg}/env/DIENST.env erzeugte Umgebungsdatei (0600) {cfg}/env/DIENST.env erzeugte Umgebungsdatei (0600)
{cfg}/proxychains/DIENST.conf proxychains-Konfiguration (Modus proxychains)
{sysd}/DIENST.d/{dropin} erzeugtes Drop-in {sysd}/DIENST.d/{dropin} erzeugtes Drop-in
Dienstnamen ohne Endung erhalten automatisch '.service'. Dienstnamen ohne Endung erhalten automatisch '.service'.
@@ -163,7 +218,9 @@ class ProxyError(Exception):
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def empty_settings(): 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): def normalize_unit(name):
@@ -245,7 +302,13 @@ def validate_settings(raw, is_global):
if not NO_PROXY_RE.match(s["no_proxy"]): if not NO_PROXY_RE.match(s["no_proxy"]):
raise ProxyError("Ungültige No-Proxy-Liste: %s" % 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 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 any(s[k] for k in ("host", "port", "user", "password")):
if not s["host"]: if not s["host"]:
raise ProxyError("Globaler Proxy: Host fehlt") raise ProxyError("Globaler Proxy: Host fehlt")
@@ -283,13 +346,85 @@ def resolve(svc, glob):
eff["user"], eff["password"] = glob["user"], glob["password"] eff["user"], eff["password"] = glob["user"], glob["password"]
src["user"] = src["password"] = "global" 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"]: if not eff["host"]:
raise ProxyError("kein Proxy-Host (weder beim Dienst noch global gesetzt)") raise ProxyError("kein Proxy-Host (weder beim Dienst noch global gesetzt)")
if not eff["port"]: if not eff["port"]:
raise ProxyError("kein Proxy-Port (weder beim Dienst noch global gesetzt)") 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 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): def proxy_url(eff, mask=False):
auth = "" auth = ""
if eff.get("user"): if eff.get("user"):
@@ -330,6 +465,10 @@ def is_inheriting(svc):
return not any(svc[k] for k in FIELDS) 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 # Konfiguration und Erzeugen der Dateien
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -339,13 +478,21 @@ class Config:
self.config_dir = config_dir self.config_dir = config_dir
self.config_file = os.path.join(config_dir, "config.json") self.config_file = os.path.join(config_dir, "config.json")
self.env_dir = os.path.join(config_dir, "env") self.env_dir = os.path.join(config_dir, "env")
self.pc_dir = os.path.join(config_dir, "proxychains")
self.systemd_dir = systemd_dir self.systemd_dir = systemd_dir
self.global_ = empty_settings() self.global_ = empty_settings()
self.services = {} self.services = {}
# Pfade # Pfade
@staticmethod
def safe_name(unit):
return re.sub(r"[^A-Za-z0-9:_.@-]", "_", unit)
def env_file(self, 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): def dropin_dir(self, unit):
return os.path.join(self.systemd_dir, unit + ".d") return os.path.join(self.systemd_dir, unit + ".d")
@@ -365,35 +512,46 @@ class Config:
except (OSError, ValueError) as e: except (OSError, ValueError) as e:
raise ProxyError("Konfiguration %s nicht lesbar: %s" % (self.config_file, e)) raise ProxyError("Konfiguration %s nicht lesbar: %s" % (self.config_file, e))
def clean(d): def clean(d, keys):
s = empty_settings() s = empty_settings()
for k in FIELDS: for k in keys:
if isinstance(d, dict) and isinstance(d.get(k), str): if isinstance(d, dict) and isinstance(d.get(k), str):
s[k] = d[k] s[k] = d[k]
return s return s
self.global_ = clean(data.get("global")) self.global_ = clean(data.get("global"), FIELDS)
self.services = {} self.services = {}
for unit, s in (data.get("services") or {}).items(): for unit, s in (data.get("services") or {}).items():
if UNIT_RE.match(unit): 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): def save(self):
os.makedirs(self.config_dir, mode=0o700, exist_ok=True) self.ensure_config_dir()
data = { data = {
"version": 1, "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)}, "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) atomic_write(self.config_file, json.dumps(data, indent=2, ensure_ascii=False) + "\n", 0o600)
# Drop-ins # Drop-ins
def render_env(self, unit, eff): def render_env(self, unit, eff):
url = proxy_url(eff)
lines = [ lines = [
"# Erzeugt von %s fuer %s - Aenderungen werden ueberschrieben." % (PROG, unit), "# Erzeugt von %s fuer %s - Aenderungen werden ueberschrieben." % (PROG, unit),
"# Bearbeiten mit: %s set %s ..." % (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: for var in PROXY_ENV_VARS:
lines.append('%s="%s"' % (var, url)) lines.append('%s="%s"' % (var, url))
lines.append('%s="%s"' % (var.upper(), url)) lines.append('%s="%s"' % (var.upper(), url))
@@ -402,6 +560,53 @@ class Config:
lines.append('NO_PROXY="%s"' % eff["no_proxy"]) lines.append('NO_PROXY="%s"' % eff["no_proxy"])
return "\n".join(lines) + "\n" 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): def render_dropin(self, unit):
return ( return (
"# Erzeugt von %s - nicht manuell bearbeiten.\n" "# Erzeugt von %s - nicht manuell bearbeiten.\n"
@@ -413,6 +618,7 @@ class Config:
def remove_unit_files(self, unit): def remove_unit_files(self, unit):
changed = remove_file(self.dropin_file(unit)) changed = remove_file(self.dropin_file(unit))
changed |= remove_file(self.env_file(unit)) changed |= remove_file(self.env_file(unit))
changed |= remove_file(self.pc_file(unit))
try: try:
os.rmdir(self.dropin_dir(unit)) os.rmdir(self.dropin_dir(unit))
except OSError: except OSError:
@@ -424,14 +630,22 @@ class Config:
svc = self.services.get(unit) svc = self.services.get(unit)
if svc is None: if svc is None:
return self.remove_unit_files(unit), None return self.remove_unit_files(unit), None
warning = None
try: try:
eff, _ = resolve(svc, self.global_) 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: except ProxyError as e:
return self.remove_unit_files(unit), "%s: %s – Proxy für diesen Dienst deaktiviert" % (unit, 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) 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) changed |= write_if_different(self.dropin_file(unit), self.render_dropin(unit), 0o644)
return changed, None return changed, warning
def managed_dropins(self): def managed_dropins(self):
"""Units, für die ein Drop-in von svc-proxy existiert.""" """Units, für die ein Drop-in von svc-proxy existiert."""
@@ -549,12 +763,14 @@ def is_backspace(k):
class FormField: 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.key = key
self.label = label self.label = label
self.kind = kind # text | choice | button self.kind = kind # text | choice | button
self.value = value self.value = value
self.choices = choices or [] self.choices = choices or []
self.display = display or {}
self.secret = secret self.secret = secret
self.placeholder = placeholder self.placeholder = placeholder
self.pos = len(value) self.pos = len(value)
@@ -659,8 +875,8 @@ class TUI:
except ProxyError as e: except ProxyError as e:
return "FEHLER: " + str(e), self.c_err return "FEHLER: " + str(e), self.c_err
if is_inheriting(svc): if is_inheriting(svc):
return "global → " + proxy_url(eff, mask=True), self.c_global return "global → " + proxy_url(eff, mask=True) + mode_suffix(eff), self.c_global
return "eigen → " + proxy_url(eff, mask=True), self.c_own return "eigen → " + proxy_url(eff, mask=True) + mode_suffix(eff), self.c_own
# ---------- Hauptliste ---------- # ---------- Hauptliste ----------
def draw_main(self): def draw_main(self):
@@ -959,6 +1175,11 @@ class TUI:
"Benutzer/Kennwort werden nur geerbt, wenn auch der Host global ist.", "Benutzer/Kennwort werden nur geerbt, wenn auch der Host global ist.",
"Eigener Host ohne Benutzer = Proxy ohne Anmeldung.", "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,", "Tasten: Enter bearbeiten, g global, Entf/d entfernen, / filtern,",
"c nur konfigurierte, r Dienst neu starten, F5 Liste neu laden, q Ende.", "c nur konfigurierte, r Dienst neu starten, F5 Liste neu laden, q Ende.",
"Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt.", "Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt.",
@@ -983,7 +1204,11 @@ class TUI:
else: else:
type_choices = [""] + PROXY_TYPES type_choices = [""] + PROXY_TYPES
tval = settings["type"] 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"): for key in ("host", "port", "user", "password", "no_proxy"):
fields.append(FormField(key, FIELD_LABELS[key], settings[key], fields.append(FormField(key, FIELD_LABELS[key], settings[key],
secret=(key == "password"), placeholder=ph(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", buttons.append(FormField("remove", "Proxy entfernen" if not is_global else "Global löschen",
"", kind="button")) "", kind="button"))
items = fields + buttons items = fields + buttons
cur = 1 # Host cur = [f.key for f in fields].index("host")
error = "" error = ""
def collect(): def collect():
@@ -1016,13 +1241,17 @@ class TUI:
input_x = 2 + label_w + 3 input_x = 2 + label_w + 3
input_w = max(10, min(w - input_x - 2, 60)) input_w = max(10, min(w - input_x - 2, 60))
cursor_pos = None cursor_pos = None
step = 2 if h >= 4 + len(fields) * 2 + 9 else 1
for i, f in enumerate(fields): for i, f in enumerate(fields):
y = 4 + i * 2 y = 4 + i * step
focused = (i == cur) focused = (i == cur)
put(scr, y, 2, f.label.rjust(label_w), curses.A_BOLD if focused else 0) put(scr, y, 2, f.label.rjust(label_w), curses.A_BOLD if focused else 0)
put(scr, y, 2 + label_w, " : ") put(scr, y, 2 + label_w, " : ")
if f.kind == "choice": 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 text = "◀ %s ▶" % shown
put(scr, y, input_x, text.ljust(input_w), self.c_sel if focused else curses.A_UNDERLINE) put(scr, y, input_x, text.ljust(input_w), self.c_sel if focused else curses.A_UNDERLINE)
else: else:
@@ -1039,7 +1268,7 @@ class TUI:
if focused: if focused:
cursor_pos = (y, input_x + f.pos - f.off) cursor_pos = (y, input_x + f.pos - f.off)
by = 4 + len(fields) * 2 by = 4 + len(fields) * step
bx = input_x bx = input_x
for j, b in enumerate(buttons): for j, b in enumerate(buttons):
focused = (len(fields) + j == cur) focused = (len(fields) + j == cur)
@@ -1055,7 +1284,7 @@ class TUI:
preview = proxy_url(cand, mask=True) if cand["host"] else "(kein globaler Proxy)" preview = proxy_url(cand, mask=True) if cand["host"] else "(kein globaler Proxy)"
else: else:
eff, _ = resolve(cand, g) eff, _ = resolve(cand, g)
preview = proxy_url(eff, mask=True) preview = proxy_url(eff, mask=True) + mode_suffix(eff)
if eff["no_proxy"]: if eff["no_proxy"]:
preview += " No-Proxy: " + eff["no_proxy"] preview += " No-Proxy: " + eff["no_proxy"]
put(scr, py, 2, "Ergebnis: ", curses.A_BOLD) put(scr, py, 2, "Ergebnis: ", curses.A_BOLD)
@@ -1066,12 +1295,15 @@ class TUI:
if error: if error:
put(scr, py + 1, 2, error, self.c_err) put(scr, py + 1, 2, error, self.c_err)
if not is_global: if not is_global:
put(scr, py + 3, 2, "Leere Felder = globaler Wert. Benutzer/Kennwort werden nur geerbt,", self.c_dim) put(scr, py + 2, 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 + 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: else:
put(scr, py + 3, 2, "Benutzer/Kennwort leer = Proxy ohne Anmeldung.", self.c_dim) 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": if cursor_pos and items[cur].kind == "text":
self.cursor(True) self.cursor(True)
try: try:
@@ -1148,7 +1380,8 @@ class TUI:
try: try:
cand = validate_settings(collect(), is_global) cand = validate_settings(collect(), is_global)
if not is_global: if not is_global:
resolve(cand, g) eff, _ = resolve(cand, g)
check_mode(eff)
except ProxyError as e: except ProxyError as e:
error = "Nicht gespeichert: %s" % e error = "Nicht gespeichert: %s" % e
continue continue
@@ -1210,7 +1443,7 @@ def fields_from_args(args):
vals = {} vals = {}
if args.proxy is not None: if args.proxy is not None:
vals.update(parse_proxy_url(args.proxy)) 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) v = getattr(args, key)
if v is not None: if v is not None:
vals[key] = v vals[key] = v
@@ -1248,11 +1481,12 @@ def cmd_set(cfg, args):
svc = validate_settings(base, is_global=False) svc = validate_settings(base, is_global=False)
try: try:
eff, _ = resolve(svc, cfg.global_) eff, _ = resolve(svc, cfg.global_)
check_mode(eff)
except ProxyError as e: except ProxyError as e:
raise ProxyError("%s: %s" % (unit, e)) raise ProxyError("%s: %s" % (unit, e))
staged[unit] = svc staged[unit] = svc
print("%s -> %s%s" % (unit, proxy_url(eff, mask=True), print("%s -> %s%s%s" % (unit, proxy_url(eff, mask=True),
" (global)" if is_inheriting(svc) else "")) " (global)" if is_inheriting(svc) else "", mode_suffix(eff)))
cfg.services.update(staged) cfg.services.update(staged)
cfg.save() cfg.save()
changed, warnings = set(), [] changed, warnings = set(), []
@@ -1318,7 +1552,7 @@ def cmd_list(cfg, args):
mark = "G" if is_inheriting(svc) else "E" mark = "G" if is_inheriting(svc) else "E"
try: try:
eff, _ = resolve(svc, cfg.global_) eff, _ = resolve(svc, cfg.global_)
text = proxy_url(eff, mask=True) text = proxy_url(eff, mask=True) + mode_suffix(eff)
except ProxyError as e: except ProxyError as e:
text = "FEHLER: %s" % e text = "FEHLER: %s" % e
print(" %s %-*s %-10s %s" % (mark, width, name, state, text)) print(" %s %-*s %-10s %s" % (mark, width, name, state, text))
@@ -1347,12 +1581,13 @@ def cmd_show(cfg, args):
if svc is None: if svc is None:
print("Proxy: keiner") print("Proxy: keiner")
return 0 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: try:
eff, src = resolve(svc, cfg.global_) eff, src = resolve(svc, cfg.global_)
except ProxyError as e: except ProxyError as e:
print("FEHLER: %s" % e) print("FEHLER: %s" % e)
return 1 return 1
print("Modus: %s" % MODE_LABELS[eff["mode"]])
for key in FIELDS: for key in FIELDS:
val = eff[key] val = eff[key]
if key == "password" and val: if key == "password" and val:
@@ -1361,6 +1596,8 @@ def cmd_show(cfg, args):
print("Proxy-URL: %s" % proxy_url(eff, mask=True)) print("Proxy-URL: %s" % proxy_url(eff, mask=True))
print("Drop-in: %s" % cfg.dropin_file(unit)) print("Drop-in: %s" % cfg.dropin_file(unit))
print("Env-Datei: %s" % cfg.env_file(unit)) print("Env-Datei: %s" % cfg.env_file(unit))
if eff["mode"] == "proxychains":
print("proxychains-Konfiguration: %s" % cfg.pc_file(unit))
return 0 return 0
@@ -1387,6 +1624,7 @@ def build_parser():
fields.add_argument("--password") fields.add_argument("--password")
fields.add_argument("--password-stdin", action="store_true") fields.add_argument("--password-stdin", action="store_true")
fields.add_argument("--no-proxy", dest="no_proxy") 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 = argparse.ArgumentParser(prog=PROG, add_help=False, parents=[common(False)])
parser.add_argument("-V", "--version", action="version", version="%s %s" % (PROG, VERSION)) parser.add_argument("-V", "--version", action="version", version="%s %s" % (PROG, VERSION))