Verbindungstest für Proxys hinzufügen
Neuer Befehl 'svc-proxy test [DIENST]' sowie Taste 't' in der Liste und Schaltfläche/F3 im Formular (testet ungespeicherte Werte). - prüft Erreichbarkeit, Handshake und Anmeldung für SOCKS5/SOCKS4/HTTP(S) mit klaren Meldungen (Kennwort falsch, Proxy-Typ vertauscht, Port gesperrt) - baut Tunnel zu Testzielen auf und liest die Begrüßung (SMTP, POP3, IMAP, HTTP), TLS für 443/465/993/995 - --target HOST:PORT, --mail (SMTP 25/587/465, POP3S, IMAPS), --timeout - im proxychains-Modus zusätzlicher Test über die proxychains-Bibliothek mit der Konfiguration des Dienstes - Ad-hoc-Test mit --proxy ohne root und ohne Speichern - Hilfe und README ergänzt, Version 1.2.0 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
3968c1a7b8
commit
9270994e82
@@ -196,6 +196,7 @@ Markierungen: **G** = nutzt globalen Proxy, **E** = eigene Werte,
|
|||||||
| Entf / d | Proxy vom Dienst entfernen |
|
| Entf / d | Proxy vom Dienst entfernen |
|
||||||
| / | Filter (Name oder Beschreibung), Esc löscht den Filter |
|
| / | Filter (Name oder Beschreibung), Esc löscht den Filter |
|
||||||
| c | nur konfigurierte Dienste anzeigen (an/aus) |
|
| c | nur konfigurierte Dienste anzeigen (an/aus) |
|
||||||
|
| t | Verbindungstest für den gewählten Dienst (ohne Proxy: globaler Proxy) |
|
||||||
| r | gewählten Dienst sofort neu starten |
|
| r | gewählten Dienst sofort neu starten |
|
||||||
| F5 | Dienstliste neu einlesen |
|
| F5 | Dienstliste neu einlesen |
|
||||||
| F1 / ? | Hilfe |
|
| F1 / ? | Hilfe |
|
||||||
@@ -209,6 +210,7 @@ Markierungen: **G** = nutzt globalen Proxy, **E** = eigene Werte,
|
|||||||
| ← → / Leertaste | Modus bzw. Proxy-Typ wählen (beim Typ 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 |
|
||||||
|
| F3 / „Testen“ | Verbindungstest mit den eingegebenen, noch nicht gespeicherten Werten |
|
||||||
| Strg+U | Feld leeren |
|
| Strg+U | Feld leeren |
|
||||||
| Esc | Abbrechen |
|
| Esc | Abbrechen |
|
||||||
|
|
||||||
@@ -236,6 +238,7 @@ svc-proxy [BEFEHL] [OPTIONEN]
|
|||||||
| `set DIENST... [FELDER] [--reset]` | Proxy für Dienst(e) setzen/ändern |
|
| `set DIENST... [FELDER] [--reset]` | Proxy für Dienst(e) setzen/ändern |
|
||||||
| `unset DIENST...` / `unset --all` | Proxy entfernen |
|
| `unset DIENST...` / `unset --all` | Proxy entfernen |
|
||||||
| `apply` | alle Drop-ins aus der Konfiguration neu erzeugen |
|
| `apply` | alle Drop-ins aus der Konfiguration neu erzeugen |
|
||||||
|
| `test [DIENST] [--target H:P] [--mail]` | Verbindungstest über den Proxy (siehe unten) |
|
||||||
| `help`, `-h`, `--help` | Hilfe mit Beispielen |
|
| `help`, `-h`, `--help` | Hilfe mit Beispielen |
|
||||||
|
|
||||||
### Felder
|
### Felder
|
||||||
@@ -287,6 +290,59 @@ praktisch zum Testen, z.B. zusammen mit `--no-reload`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Verbindungstest
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo svc-proxy test kerio-connect --mail
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Verbindungstest: Dienst kerio-connect.service
|
||||||
|
Proxy: socks5://max:****@192.168.1.50:1080 [proxychains]
|
||||||
|
[ OK ] Proxy erreichbar: 192.168.1.50:1080 (1 ms)
|
||||||
|
[FEHLER] gmail-smtp-in.l.google.com:25 → Proxy meldet: vom Regelwerk des Proxys nicht erlaubt (Port/Ziel gesperrt?)
|
||||||
|
[ OK ] smtp.gmail.com:587 → Antwort: 220 smtp.gmail.com ESMTP ... (46 ms)
|
||||||
|
[ OK ] smtp.gmail.com:465 → TLS ok, Antwort: 220 smtp.gmail.com ESMTP ... (61 ms)
|
||||||
|
[ OK ] pop.gmail.com:995 → TLS ok, Antwort: +OK Gpop ready for requests ... (67 ms)
|
||||||
|
[ OK ] imap.gmail.com:993 → TLS ok, Antwort: * OK Gimap ready for requests ... (105 ms)
|
||||||
|
Test über proxychains (so verbindet der Dienst):
|
||||||
|
[FEHLER] proxychains: gmail-smtp-in.l.google.com:25 → Verbindung abgelehnt
|
||||||
|
[ OK ] proxychains: smtp.gmail.com:587 → Antwort: 220 smtp.gmail.com ESMTP ...
|
||||||
|
...
|
||||||
|
Ergebnis: 2 Test(s) fehlgeschlagen.
|
||||||
|
```
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
|
||||||
|
1. **Proxy erreichbar?** TCP-Verbindung zum Proxy.
|
||||||
|
2. **Handshake und Anmeldung:** SOCKS5, SOCKS4 oder HTTP CONNECT (auch `https`-Proxy).
|
||||||
|
Verständliche Meldungen z.B. bei falschem Kennwort, fehlender Anmeldung,
|
||||||
|
falschem Proxy-Typ (socks/http vertauscht) oder einem vom Proxy gesperrten Port.
|
||||||
|
3. **Ziele:** Tunnel zu jedem Ziel aufbauen und die Begrüßung lesen. Das zeigt,
|
||||||
|
dass wirklich Daten fließen (SMTP `220`, POP3 `+OK`, IMAP `* OK`, HTTP-Status).
|
||||||
|
Die Ports 443, 465, 993 und 995 werden per TLS angesprochen (ohne
|
||||||
|
Zertifikatsprüfung, es geht nur um die Verbindung).
|
||||||
|
4. **Nur im Modus proxychains:** Derselbe Test läuft noch einmal als normales
|
||||||
|
Programm über die proxychains-Bibliothek mit der Konfiguration des Dienstes.
|
||||||
|
Damit ist der komplette Weg geprüft, den auch der Dienst nimmt.
|
||||||
|
|
||||||
|
| Option | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `--target HOST:PORT` | Testziel, mehrfach möglich (Standard: `example.com:80`, `example.com:443`) |
|
||||||
|
| `--mail` | Mail-Ziele: SMTP 25/587/465, POP3S 995, IMAPS 993 (Gmail) |
|
||||||
|
| `--timeout SEK` | Zeitlimit pro Schritt (Standard 6 s) |
|
||||||
|
| `--proxy`, `--host`, … | Werte nur für den Test überschreiben, nichts wird gespeichert |
|
||||||
|
|
||||||
|
Ohne `DIENST` wird der globale Proxy getestet. Mit `--proxy URL` (ohne DIENST)
|
||||||
|
läuft der Test auch ohne root und ohne gespeicherte Konfiguration. So lässt sich
|
||||||
|
ein Proxy prüfen, bevor man ihn einträgt. Rückgabewert: `0` = alles ok,
|
||||||
|
`1` = mindestens ein Test fehlgeschlagen, praktisch für Monitoring-Skripte.
|
||||||
|
|
||||||
|
Hinweis zu Port 25: Viele Proxys und Provider sperren ausgehend Port 25. Der Test
|
||||||
|
zeigt genau das. Abhilfe ist ein SMTP-Relay (Smarthost) auf Port 587 oder 465.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Beispiele
|
## Beispiele
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -334,6 +390,12 @@ sudo svc-proxy set myapp --reset
|
|||||||
sudo svc-proxy unset myapp --restart
|
sudo svc-proxy unset myapp --restart
|
||||||
sudo svc-proxy unset --all --no-restart
|
sudo svc-proxy unset --all --no-restart
|
||||||
|
|
||||||
|
# Verbindungstests
|
||||||
|
sudo svc-proxy test # globaler Proxy
|
||||||
|
sudo svc-proxy test kerio-connect --mail # Mail-Ports über Kerios Proxy
|
||||||
|
sudo svc-proxy test kerio-connect --target mx.example.com:25 --target pop.example.com:995
|
||||||
|
svc-proxy test --proxy socks5://192.0.2.10:1080 --mail # ad hoc, ohne Speichern, ohne root
|
||||||
|
|
||||||
# Anzeigen
|
# Anzeigen
|
||||||
sudo svc-proxy list
|
sudo svc-proxy list
|
||||||
sudo svc-proxy list --all
|
sudo svc-proxy list --all
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Ausführliche Hilfe: svc-proxy --help
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
import curses
|
import curses
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
@@ -16,13 +17,16 @@ import os
|
|||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
import ssl
|
||||||
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
from glob import glob as glob_paths
|
from glob import glob as glob_paths
|
||||||
from urllib.parse import quote, unquote, urlsplit
|
from urllib.parse import quote, unquote, urlsplit
|
||||||
|
|
||||||
VERSION = "1.1.0"
|
VERSION = "1.2.0"
|
||||||
PROG = "svc-proxy"
|
PROG = "svc-proxy"
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = "/etc/svc-proxy"
|
DEFAULT_CONFIG_DIR = "/etc/svc-proxy"
|
||||||
@@ -70,6 +74,29 @@ PROXYCHAINS_LOCALNETS = [
|
|||||||
"[::]:53/0",
|
"[::]:53/0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Verbindungstest
|
||||||
|
TEST_TARGETS_WEB = ["example.com:80", "example.com:443"]
|
||||||
|
TEST_TARGETS_MAIL = [
|
||||||
|
"gmail-smtp-in.l.google.com:25", # SMTP (MX-Zustellung)
|
||||||
|
"smtp.gmail.com:587", # SMTP Submission
|
||||||
|
"smtp.gmail.com:465", # SMTPS
|
||||||
|
"pop.gmail.com:995", # POP3S
|
||||||
|
"imap.gmail.com:993", # IMAPS
|
||||||
|
]
|
||||||
|
TEST_TIMEOUT = 6.0
|
||||||
|
TLS_PORTS = {443, 465, 563, 636, 853, 993, 995, 8443}
|
||||||
|
HTTP_PORTS = {80, 443, 8080, 8443}
|
||||||
|
SOCKS5_ERRORS = {
|
||||||
|
1: "allgemeiner Fehler des Proxys",
|
||||||
|
2: "vom Regelwerk des Proxys nicht erlaubt (Port/Ziel gesperrt?)",
|
||||||
|
3: "Netz nicht erreichbar",
|
||||||
|
4: "Host nicht erreichbar",
|
||||||
|
5: "Verbindung vom Ziel abgelehnt",
|
||||||
|
6: "Zeitüberschreitung (TTL abgelaufen)",
|
||||||
|
7: "Befehl nicht unterstützt",
|
||||||
|
8: "Adresstyp nicht unterstützt",
|
||||||
|
}
|
||||||
|
|
||||||
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.,:*/_\[\]-]*$")
|
||||||
@@ -107,6 +134,8 @@ BEFEHLE
|
|||||||
unset DIENST... Proxy von Diensten entfernen
|
unset DIENST... Proxy von Diensten entfernen
|
||||||
unset --all Proxy von allen Diensten entfernen
|
unset --all Proxy von allen Diensten entfernen
|
||||||
apply Alle Drop-ins aus der Konfiguration neu schreiben
|
apply Alle Drop-ins aus der Konfiguration neu schreiben
|
||||||
|
test [DIENST] [ZIELE] Verbindungstest über den Proxy des Dienstes
|
||||||
|
(ohne DIENST: globaler Proxy, oder ad hoc mit --proxy)
|
||||||
help Diese Hilfe anzeigen
|
help Diese Hilfe anzeigen
|
||||||
|
|
||||||
FELDER (für 'global' und 'set')
|
FELDER (für 'global' und 'set')
|
||||||
@@ -139,6 +168,22 @@ VERERBUNG (Dienst <-> global)
|
|||||||
Ändert man den globalen Proxy, werden alle Dienste, die ihn (teilweise) erben,
|
Ändert man den globalen Proxy, werden alle Dienste, die ihn (teilweise) erben,
|
||||||
automatisch mit angepasst.
|
automatisch mit angepasst.
|
||||||
|
|
||||||
|
VERBINDUNGSTEST ('test')
|
||||||
|
Prüft der Reihe nach:
|
||||||
|
1. Ist der Proxy erreichbar (TCP)?
|
||||||
|
2. Klappt der Handshake inkl. Anmeldung (SOCKS5/SOCKS4/HTTP CONNECT)?
|
||||||
|
Klare Meldung bei falschem Kennwort, falschem Proxy-Typ, gesperrtem Port.
|
||||||
|
3. Für jedes Ziel: Tunnel aufbauen und Begrüßung lesen (SMTP "220",
|
||||||
|
POP3 "+OK", IMAP "* OK", HTTP-Status). Ports 443/465/993/995 per TLS.
|
||||||
|
4. Im Modus proxychains zusätzlich: derselbe Test über die proxychains-
|
||||||
|
Bibliothek mit der Konfiguration des Dienstes (so verbindet der Dienst).
|
||||||
|
--target HOST:PORT Testziel (mehrfach möglich). Standard: {web}
|
||||||
|
--mail Mail-Ziele testen: SMTP 25/587/465, POP3S 995, IMAPS 993 (Gmail)
|
||||||
|
--timeout SEK Zeitlimit pro Schritt (Standard: {timeout:g} s)
|
||||||
|
FELDER (--proxy, --host, ...) überschreiben die gespeicherten Werte nur für den
|
||||||
|
Test. Mit --proxy ohne DIENST geht der Test auch ohne root.
|
||||||
|
Rückgabewert: 0 = alles ok, 1 = mindestens ein Test fehlgeschlagen.
|
||||||
|
|
||||||
NEUSTART / RELOAD
|
NEUSTART / RELOAD
|
||||||
Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt. Die neuen
|
Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt. Die neuen
|
||||||
Werte gelten aber erst nach einem Neustart des Dienstes.
|
Werte gelten aber erst nach einem Neustart des Dienstes.
|
||||||
@@ -194,6 +239,12 @@ BEISPIELE
|
|||||||
# Proxy von einem Dienst entfernen
|
# Proxy von einem Dienst entfernen
|
||||||
sudo {prog} unset myapp --restart
|
sudo {prog} unset myapp --restart
|
||||||
|
|
||||||
|
# Verbindungstests
|
||||||
|
sudo {prog} test # globaler Proxy, Web-Ziele
|
||||||
|
sudo {prog} test kerio-connect --mail # Mail-Ports über Kerios Proxy
|
||||||
|
sudo {prog} test kerio-connect --target mx.example.com:25 --target pop.example.com:995
|
||||||
|
{prog} test --proxy socks5://192.0.2.10:1080 --mail # ad hoc, ohne Speichern
|
||||||
|
|
||||||
# Übersicht und Details
|
# Übersicht und Details
|
||||||
sudo {prog} list
|
sudo {prog} list
|
||||||
sudo {prog} show docker
|
sudo {prog} show docker
|
||||||
@@ -206,6 +257,7 @@ DATEIEN
|
|||||||
|
|
||||||
Dienstnamen ohne Endung erhalten automatisch '.service'.
|
Dienstnamen ohne Endung erhalten automatisch '.service'.
|
||||||
""".format(prog=PROG, version=VERSION, cfg=DEFAULT_CONFIG_DIR,
|
""".format(prog=PROG, version=VERSION, cfg=DEFAULT_CONFIG_DIR,
|
||||||
|
web=", ".join(TEST_TARGETS_WEB), timeout=TEST_TIMEOUT,
|
||||||
sysd=DEFAULT_SYSTEMD_DIR, dropin=DROPIN_NAME)
|
sysd=DEFAULT_SYSTEMD_DIR, dropin=DROPIN_NAME)
|
||||||
|
|
||||||
|
|
||||||
@@ -213,6 +265,10 @@ class ProxyError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyFatal(ProxyError):
|
||||||
|
"""Fehler, bei dem weitere Testziele sinnlos sind (falscher Typ, Anmeldung)."""
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Hilfsfunktionen
|
# Hilfsfunktionen
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -737,6 +793,316 @@ def list_system_services():
|
|||||||
return units
|
return units
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Verbindungstest
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def parse_target(text):
|
||||||
|
text = text.strip()
|
||||||
|
m = re.match(r"^\[([0-9A-Fa-f:.]+)\]:(\d+)$", text) or re.match(r"^([A-Za-z0-9._-]+):(\d+)$", text)
|
||||||
|
if not m or not 1 <= int(m.group(2)) <= 65535:
|
||||||
|
raise ProxyError("Ungültiges Ziel %r (Format: HOST:PORT)" % text)
|
||||||
|
return m.group(1), int(m.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
def format_target(host, port):
|
||||||
|
return "[%s]:%d" % (host, port) if ":" in host else "%s:%d" % (host, port)
|
||||||
|
|
||||||
|
|
||||||
|
def recv_exact(sock, n):
|
||||||
|
buf = b""
|
||||||
|
while len(buf) < n:
|
||||||
|
chunk = sock.recv(n - len(buf))
|
||||||
|
if not chunk:
|
||||||
|
raise ProxyError("Proxy hat die Verbindung unerwartet geschlossen")
|
||||||
|
buf += chunk
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_local(host, ipv4_only=False):
|
||||||
|
family = socket.AF_INET if ipv4_only else 0
|
||||||
|
try:
|
||||||
|
infos = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
|
||||||
|
except socket.gaierror as e:
|
||||||
|
raise ProxyError("Ziel %s lokal nicht auflösbar (%s)" % (host, e.strerror))
|
||||||
|
infos.sort(key=lambda i: i[0] != socket.AF_INET)
|
||||||
|
return infos[0][4][0]
|
||||||
|
|
||||||
|
|
||||||
|
def socks5_connect(sock, eff, host, port):
|
||||||
|
methods = b"\x00\x02" if eff["user"] else b"\x00"
|
||||||
|
sock.sendall(b"\x05" + bytes([len(methods)]) + methods)
|
||||||
|
try:
|
||||||
|
ver, method = recv_exact(sock, 2)
|
||||||
|
except socket.timeout:
|
||||||
|
raise ProxyFatal("Keine Antwort auf den SOCKS5-Handshake – ist der Proxy-Typ richtig (http statt socks5?)")
|
||||||
|
if ver != 5:
|
||||||
|
raise ProxyFatal("Keine SOCKS5-Antwort – ist der Proxy-Typ richtig (http statt socks5?)")
|
||||||
|
if method == 0xFF:
|
||||||
|
raise ProxyFatal("Proxy akzeptiert keine angebotene Anmeldemethode%s"
|
||||||
|
% ("" if eff["user"] else " – verlangt er Benutzer/Kennwort?"))
|
||||||
|
if method == 2:
|
||||||
|
if not eff["user"]:
|
||||||
|
raise ProxyFatal("Proxy verlangt Benutzer/Kennwort")
|
||||||
|
u, p = eff["user"].encode(), eff["password"].encode()
|
||||||
|
sock.sendall(b"\x01" + bytes([len(u)]) + u + bytes([len(p)]) + p)
|
||||||
|
if recv_exact(sock, 2)[1] != 0:
|
||||||
|
raise ProxyFatal("Anmeldung abgelehnt – Benutzer oder Kennwort falsch")
|
||||||
|
elif method != 0:
|
||||||
|
raise ProxyFatal("Proxy verlangt eine nicht unterstützte Anmeldemethode (%d)" % method)
|
||||||
|
|
||||||
|
if eff["type"] == "socks5h":
|
||||||
|
name = host.encode("idna")
|
||||||
|
addr = b"\x03" + bytes([len(name)]) + name
|
||||||
|
else:
|
||||||
|
ip = resolve_local(host)
|
||||||
|
if ":" in ip:
|
||||||
|
addr = b"\x04" + socket.inet_pton(socket.AF_INET6, ip)
|
||||||
|
else:
|
||||||
|
addr = b"\x01" + socket.inet_aton(ip)
|
||||||
|
sock.sendall(b"\x05\x01\x00" + addr + struct.pack(">H", port))
|
||||||
|
try:
|
||||||
|
_, rep, _, atyp = recv_exact(sock, 4)
|
||||||
|
except socket.timeout:
|
||||||
|
raise ProxyError("Proxy erreicht das Ziel nicht (Zeitüberschreitung) – Port/Ziel gesperrt?")
|
||||||
|
if rep != 0:
|
||||||
|
raise ProxyError("Proxy meldet: %s" % SOCKS5_ERRORS.get(rep, "Fehlercode %d" % rep))
|
||||||
|
if atyp == 1:
|
||||||
|
recv_exact(sock, 4)
|
||||||
|
elif atyp == 3:
|
||||||
|
recv_exact(sock, recv_exact(sock, 1)[0])
|
||||||
|
elif atyp == 4:
|
||||||
|
recv_exact(sock, 16)
|
||||||
|
recv_exact(sock, 2)
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def socks4_connect(sock, eff, host, port):
|
||||||
|
ip = resolve_local(host, ipv4_only=True)
|
||||||
|
sock.sendall(struct.pack(">BBH", 4, 1, port) + socket.inet_aton(ip) + eff["user"].encode() + b"\x00")
|
||||||
|
try:
|
||||||
|
reply = recv_exact(sock, 8)
|
||||||
|
except socket.timeout:
|
||||||
|
raise ProxyError("Keine Antwort vom Proxy – falscher Proxy-Typ oder Ziel nicht erreichbar?")
|
||||||
|
if reply[0] != 0:
|
||||||
|
raise ProxyFatal("Keine SOCKS4-Antwort – ist der Proxy-Typ richtig?")
|
||||||
|
if reply[1] != 0x5A:
|
||||||
|
reasons = {0x5B: "Anfrage abgelehnt oder Ziel nicht erreichbar",
|
||||||
|
0x5C: "identd des Clients nicht erreichbar",
|
||||||
|
0x5D: "identd-Benutzer passt nicht"}
|
||||||
|
raise ProxyError("Proxy meldet: %s" % reasons.get(reply[1], "Code %d" % reply[1]))
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def http_connect(sock, eff, host, port):
|
||||||
|
target = format_target(host, port)
|
||||||
|
req = "CONNECT %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s/%s\r\n" % (target, target, PROG, VERSION)
|
||||||
|
if eff["user"]:
|
||||||
|
cred = base64.b64encode(("%s:%s" % (eff["user"], eff["password"])).encode()).decode()
|
||||||
|
req += "Proxy-Authorization: Basic %s\r\n" % cred
|
||||||
|
sock.sendall((req + "\r\n").encode())
|
||||||
|
data = b""
|
||||||
|
while b"\r\n\r\n" not in data:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(4096)
|
||||||
|
except socket.timeout:
|
||||||
|
raise ProxyError("Keine Antwort vom Proxy – falscher Proxy-Typ (socks statt http?) "
|
||||||
|
"oder Ziel nicht erreichbar")
|
||||||
|
if not chunk:
|
||||||
|
if not data:
|
||||||
|
raise ProxyFatal("Proxy hat ohne Antwort geschlossen – ist der Proxy-Typ richtig (socks statt http?)")
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
if len(data) > 65536:
|
||||||
|
raise ProxyFatal("Antwort des Proxys zu groß")
|
||||||
|
head, _, rest = data.partition(b"\r\n\r\n")
|
||||||
|
status = head.split(b"\r\n", 1)[0].decode("latin-1").strip()
|
||||||
|
parts = status.split(None, 2)
|
||||||
|
if len(parts) < 2 or not parts[0].startswith("HTTP/") or not parts[1].isdigit():
|
||||||
|
raise ProxyFatal("Keine HTTP-Proxy-Antwort – ist der Proxy-Typ richtig?")
|
||||||
|
code = int(parts[1])
|
||||||
|
if code == 407:
|
||||||
|
raise ProxyFatal("Anmeldung %s (407)" % ("abgelehnt – Benutzer/Kennwort falsch" if eff["user"]
|
||||||
|
else "erforderlich – Benutzer/Kennwort fehlt"))
|
||||||
|
if code != 200:
|
||||||
|
raise ProxyError("Proxy lehnt ab: %s" % status)
|
||||||
|
return rest
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_connect(eff, host, port, timeout):
|
||||||
|
"""Baut über den Proxy einen Tunnel zu host:port. Liefert (socket, schon empfangene Daten)."""
|
||||||
|
sock = socket.create_connection((eff["host"], int(eff["port"])), timeout)
|
||||||
|
try:
|
||||||
|
if eff["type"] == "https":
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
sock = ctx.wrap_socket(sock, server_hostname=eff["host"])
|
||||||
|
if eff["type"] in ("http", "https"):
|
||||||
|
rest = http_connect(sock, eff, host, port)
|
||||||
|
elif eff["type"] == "socks4":
|
||||||
|
rest = socks4_connect(sock, eff, host, port)
|
||||||
|
else:
|
||||||
|
rest = socks5_connect(sock, eff, host, port)
|
||||||
|
except BaseException:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
return sock, rest
|
||||||
|
|
||||||
|
|
||||||
|
def probe(sock, host, port, early=b"", timeout=TEST_TIMEOUT):
|
||||||
|
"""Prüft, ob über die Verbindung echte Daten fließen. Liefert (ok, text)."""
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
prefix = ""
|
||||||
|
if port in TLS_PORTS:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE # nur Erreichbarkeit testen, keine Zertifikatsprüfung
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(host)
|
||||||
|
is_ip = True
|
||||||
|
except ValueError:
|
||||||
|
is_ip = False
|
||||||
|
sock = ctx.wrap_socket(sock, server_hostname=None if is_ip else host)
|
||||||
|
except (ssl.SSLError, OSError) as e:
|
||||||
|
return False, "TLS-Handshake fehlgeschlagen (%s)" % (getattr(e, "reason", None) or e)
|
||||||
|
prefix = "TLS ok, "
|
||||||
|
early = b""
|
||||||
|
if port in HTTP_PORTS:
|
||||||
|
sock.sendall(("HEAD / HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s/%s\r\n\r\n"
|
||||||
|
% (host, PROG, VERSION)).encode())
|
||||||
|
data = early
|
||||||
|
try:
|
||||||
|
while b"\n" not in data and len(data) < 1024:
|
||||||
|
chunk = sock.recv(1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
return False, prefix + "Verbindung abgebrochen (%s)" % e
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if not data:
|
||||||
|
return True, prefix + "verbunden (keine Begrüßung innerhalb %d s)" % timeout
|
||||||
|
line = data.split(b"\n", 1)[0].decode("utf-8", "replace").strip()
|
||||||
|
return True, prefix + "Antwort: " + (line[:70] or "(leer)")
|
||||||
|
|
||||||
|
|
||||||
|
def probe_direct(targets, timeout):
|
||||||
|
"""Wird im proxychains-Selbsttest im Kindprozess ausgeführt: normale Verbindungen
|
||||||
|
wie ein Programm ohne Proxy-Unterstützung – proxychains leitet sie um."""
|
||||||
|
rc = 0
|
||||||
|
for text in targets:
|
||||||
|
host, port = parse_target(text)
|
||||||
|
t0 = time.monotonic()
|
||||||
|
try:
|
||||||
|
sock = socket.create_connection((host, port), timeout)
|
||||||
|
ok, msg = probe(sock, host, port, timeout=timeout)
|
||||||
|
except (OSError, ProxyError) as e:
|
||||||
|
ok, msg = False, describe_error(e)
|
||||||
|
ms = (time.monotonic() - t0) * 1000
|
||||||
|
print("%s\t%s → %s (%d ms)" % ("OK" if ok else "FAIL", format_target(host, port), msg, ms))
|
||||||
|
rc |= 0 if ok else 1
|
||||||
|
return rc
|
||||||
|
|
||||||
|
|
||||||
|
def describe_error(e):
|
||||||
|
if isinstance(e, ProxyError):
|
||||||
|
return str(e)
|
||||||
|
if isinstance(e, socket.timeout):
|
||||||
|
return "Zeitüberschreitung"
|
||||||
|
if isinstance(e, ConnectionRefusedError):
|
||||||
|
return "Verbindung abgelehnt"
|
||||||
|
if isinstance(e, ConnectionResetError):
|
||||||
|
return "Verbindung zurückgesetzt"
|
||||||
|
if isinstance(e, socket.gaierror):
|
||||||
|
return "Name nicht auflösbar (%s)" % e.strerror
|
||||||
|
if isinstance(e, ssl.SSLError):
|
||||||
|
return "TLS-Fehler (%s)" % (e.reason or e)
|
||||||
|
return getattr(e, "strerror", None) or str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def proxychains_selftest(pc_conf, targets, timeout):
|
||||||
|
try:
|
||||||
|
lib = find_proxychains_lib()
|
||||||
|
except ProxyError as e:
|
||||||
|
return [("fail", "proxychains-Test: %s" % e)]
|
||||||
|
fd, path = tempfile.mkstemp(prefix="svc-proxy-test-", suffix=".conf")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w") as f:
|
||||||
|
f.write(pc_conf)
|
||||||
|
env = dict(os.environ, LD_PRELOAD=lib, PROXYCHAINS_CONF_FILE=path, PROXYCHAINS_QUIET_MODE="1")
|
||||||
|
cmd = [sys.executable, os.path.abspath(__file__), "_probe", str(timeout)]
|
||||||
|
cmd += [format_target(h, p) for h, p in targets]
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
universal_newlines=True, timeout=timeout * (len(targets) + 1) + 10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return [("fail", "proxychains-Test: Zeitüberschreitung")]
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
results = []
|
||||||
|
for line in r.stdout.splitlines():
|
||||||
|
status, _, text = line.partition("\t")
|
||||||
|
results.append(("ok" if status == "OK" else "fail", "proxychains: " + text))
|
||||||
|
if not results:
|
||||||
|
err = (r.stderr.strip().splitlines() or ["keine Ausgabe"])[-1]
|
||||||
|
results.append(("fail", "proxychains-Test fehlgeschlagen: %s" % err))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def run_proxy_test(eff, targets, cfg=None, unit=None, timeout=TEST_TIMEOUT):
|
||||||
|
"""Testet den Proxy. Liefert Liste von (status, text), status: ok | fail | info."""
|
||||||
|
results = [("info", "Proxy: %s%s" % (proxy_url(eff, mask=True), mode_suffix(eff)))]
|
||||||
|
t0 = time.monotonic()
|
||||||
|
try:
|
||||||
|
socket.create_connection((eff["host"], int(eff["port"])), timeout).close()
|
||||||
|
except socket.gaierror as e:
|
||||||
|
results.append(("fail", "Proxy-Host %s nicht auflösbar (%s)" % (eff["host"], e.strerror)))
|
||||||
|
return results
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
results.append(("fail", "Proxy %s:%s lehnt die Verbindung ab – läuft dort ein Proxy auf diesem Port?"
|
||||||
|
% (eff["host"], eff["port"])))
|
||||||
|
return results
|
||||||
|
except socket.timeout:
|
||||||
|
results.append(("fail", "Proxy %s:%s antwortet nicht (Zeitüberschreitung) – Firewall?"
|
||||||
|
% (eff["host"], eff["port"])))
|
||||||
|
return results
|
||||||
|
except OSError as e:
|
||||||
|
results.append(("fail", "Proxy %s:%s nicht erreichbar: %s" % (eff["host"], eff["port"], describe_error(e))))
|
||||||
|
return results
|
||||||
|
results.append(("ok", "Proxy erreichbar: %s:%s (%d ms)"
|
||||||
|
% (eff["host"], eff["port"], (time.monotonic() - t0) * 1000)))
|
||||||
|
|
||||||
|
for host, port in targets:
|
||||||
|
t0 = time.monotonic()
|
||||||
|
label = format_target(host, port)
|
||||||
|
try:
|
||||||
|
sock, early = proxy_connect(eff, host, port, timeout)
|
||||||
|
ok, msg = probe(sock, host, port, early, timeout)
|
||||||
|
except ProxyFatal as e:
|
||||||
|
results.append(("fail", "%s → %s" % (label, e)))
|
||||||
|
return results
|
||||||
|
except (OSError, ProxyError) as e:
|
||||||
|
ok, msg = False, describe_error(e)
|
||||||
|
ms = (time.monotonic() - t0) * 1000
|
||||||
|
results.append(("ok" if ok else "fail", "%s → %s (%d ms)" % (label, msg, ms)))
|
||||||
|
|
||||||
|
if eff["mode"] == "proxychains":
|
||||||
|
try:
|
||||||
|
conf = (cfg or Config(DEFAULT_CONFIG_DIR, DEFAULT_SYSTEMD_DIR)).render_proxychains(unit or "test", eff)
|
||||||
|
except ProxyError as e:
|
||||||
|
results.append(("fail", "proxychains-Test: %s" % e))
|
||||||
|
else:
|
||||||
|
results.append(("info", "Test über proxychains (so verbindet der Dienst):"))
|
||||||
|
results += proxychains_selftest(conf, targets, timeout)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# ncurses-Oberfläche
|
# ncurses-Oberfläche
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -932,8 +1298,8 @@ class TUI:
|
|||||||
if self.filter_mode:
|
if self.filter_mode:
|
||||||
self.key_bar("Filter eingeben Enter übernehmen Esc Filter löschen")
|
self.key_bar("Filter eingeben Enter übernehmen Esc Filter löschen")
|
||||||
else:
|
else:
|
||||||
self.key_bar("↑↓ Wählen Enter Bearbeiten g Global Entf Proxy entfernen "
|
self.key_bar("↑↓ Wählen Enter Bearbeiten g Global Entf Entfernen "
|
||||||
"/ Filter c Nur konfigurierte r Neustart F1 Hilfe q Ende")
|
"t Test / Filter c Nur konfigurierte r Neustart F1 Hilfe q Ende")
|
||||||
scr.refresh()
|
scr.refresh()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -978,6 +1344,8 @@ class TUI:
|
|||||||
elif k in ("c", "C"):
|
elif k in ("c", "C"):
|
||||||
self.only_configured = not self.only_configured
|
self.only_configured = not self.only_configured
|
||||||
self.apply_filter()
|
self.apply_filter()
|
||||||
|
elif k in ("t", "T") and n:
|
||||||
|
self.test_service(self.visible[self.sel])
|
||||||
elif k == "r" and n:
|
elif k == "r" and n:
|
||||||
self.restart_now([self.visible[self.sel]])
|
self.restart_now([self.visible[self.sel]])
|
||||||
elif k == curses.KEY_F5:
|
elif k == curses.KEY_F5:
|
||||||
@@ -1110,6 +1478,122 @@ class TUI:
|
|||||||
del win
|
del win
|
||||||
self.scr.touchwin()
|
self.scr.touchwin()
|
||||||
|
|
||||||
|
def busy(self, text):
|
||||||
|
win, bh, bw = self.box("Bitte warten", [text], height_extra=0)
|
||||||
|
put(win, 1, 2, text, curses.A_BOLD, bw - 4)
|
||||||
|
win.refresh()
|
||||||
|
del win
|
||||||
|
|
||||||
|
def prompt(self, title, label, value=""):
|
||||||
|
"""Einzeilige Eingabe in einem Dialog. Liefert Text oder None (Esc)."""
|
||||||
|
while True:
|
||||||
|
win, bh, bw = self.box(title, [label, " " * 50])
|
||||||
|
put(win, 1, 2, label, 0, bw - 4)
|
||||||
|
field_w = bw - 4
|
||||||
|
vis = value[-(field_w - 1):]
|
||||||
|
put(win, 2, 2, vis.ljust(field_w), self.c_sel)
|
||||||
|
put(win, bh - 2, 2, "Enter übernehmen Esc abbrechen", self.c_dim)
|
||||||
|
self.cursor(True)
|
||||||
|
try:
|
||||||
|
win.move(2, 2 + len(vis))
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
win.refresh()
|
||||||
|
k = win.get_wch()
|
||||||
|
del win
|
||||||
|
if is_enter(k):
|
||||||
|
self.cursor(False)
|
||||||
|
self.scr.touchwin()
|
||||||
|
return value
|
||||||
|
if k == "\x1b":
|
||||||
|
self.cursor(False)
|
||||||
|
self.scr.touchwin()
|
||||||
|
return None
|
||||||
|
if is_backspace(k):
|
||||||
|
value = value[:-1]
|
||||||
|
elif k == "\x15":
|
||||||
|
value = ""
|
||||||
|
elif isinstance(k, str) and k.isprintable():
|
||||||
|
value += k
|
||||||
|
|
||||||
|
def choose_targets(self):
|
||||||
|
lines = [
|
||||||
|
"Welche Ziele sollen über den Proxy getestet werden?",
|
||||||
|
"",
|
||||||
|
" w Web: " + ", ".join(TEST_TARGETS_WEB),
|
||||||
|
" m Mail: SMTP 25/587/465, POP3S 995, IMAPS 993 (Gmail)",
|
||||||
|
" b beides",
|
||||||
|
" e eigenes Ziel (HOST:PORT, mehrere mit Komma)",
|
||||||
|
]
|
||||||
|
win, bh, bw = self.box("Verbindungstest", lines)
|
||||||
|
for i, l in enumerate(lines):
|
||||||
|
put(win, 1 + i, 2, l, 0, bw - 4)
|
||||||
|
put(win, bh - 2, 2, "w / m / b / e wählen, Esc abbrechen", self.c_dim)
|
||||||
|
win.refresh()
|
||||||
|
while True:
|
||||||
|
k = win.get_wch()
|
||||||
|
if k in ("w", "W", "\n", "\r", curses.KEY_ENTER):
|
||||||
|
names = TEST_TARGETS_WEB
|
||||||
|
elif k in ("m", "M"):
|
||||||
|
names = TEST_TARGETS_MAIL
|
||||||
|
elif k in ("b", "B"):
|
||||||
|
names = TEST_TARGETS_WEB + TEST_TARGETS_MAIL
|
||||||
|
elif k in ("e", "E"):
|
||||||
|
del win
|
||||||
|
self.scr.touchwin()
|
||||||
|
text = self.prompt("Eigenes Ziel", "Ziel(e) als HOST:PORT, z.B. mail.example.com:25",
|
||||||
|
getattr(self, "last_target", ""))
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
self.last_target = text
|
||||||
|
names = [t for t in text.split(",") if t.strip()]
|
||||||
|
elif k in ("\x1b", "q"):
|
||||||
|
del win
|
||||||
|
self.scr.touchwin()
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return [parse_target(t) for t in names]
|
||||||
|
except ProxyError as e:
|
||||||
|
self.message("Fehler", [str(e)], error=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_test(self, eff, unit, title):
|
||||||
|
targets = self.choose_targets()
|
||||||
|
if not targets:
|
||||||
|
return
|
||||||
|
self.busy("Verbindungstest läuft ... (bis zu %d s pro Ziel)" % TEST_TIMEOUT)
|
||||||
|
results = run_proxy_test(eff, targets, self.cfg, unit)
|
||||||
|
failed = sum(1 for st, _ in results if st == "fail")
|
||||||
|
results.append(("info", ""))
|
||||||
|
results.append(("ok" if not failed else "fail",
|
||||||
|
"Alles in Ordnung." if not failed else "%d Test(s) fehlgeschlagen." % failed))
|
||||||
|
self.results_box(title, results)
|
||||||
|
|
||||||
|
def results_box(self, title, results):
|
||||||
|
lines = [("[ OK ] " if st == "ok" else "[FEHL] " if st == "fail" else " ") + text
|
||||||
|
for st, text in results]
|
||||||
|
win, bh, bw = self.box(title, lines)
|
||||||
|
for i, (line, (st, _)) in enumerate(list(zip(lines, results))[:bh - 4]):
|
||||||
|
attr = self.c_ok if st == "ok" else self.c_err if st == "fail" else curses.A_BOLD
|
||||||
|
put(win, 1 + i, 2, line, attr, bw - 4)
|
||||||
|
put(win, bh - 2, 2, "[ OK – beliebige Taste ]", curses.A_REVERSE)
|
||||||
|
win.refresh()
|
||||||
|
win.get_wch()
|
||||||
|
del win
|
||||||
|
self.scr.touchwin()
|
||||||
|
|
||||||
|
def test_service(self, unit):
|
||||||
|
svc = self.cfg.services.get(unit)
|
||||||
|
try:
|
||||||
|
eff, _ = resolve(svc or empty_settings(), self.cfg.global_)
|
||||||
|
except ProxyError as e:
|
||||||
|
self.message("Verbindungstest", [str(e)], error=True)
|
||||||
|
return
|
||||||
|
title = "Test %s%s" % (unit, "" if svc else " (globaler Proxy)")
|
||||||
|
self.run_test(eff, unit if svc else None, title)
|
||||||
|
|
||||||
def confirm(self, text):
|
def confirm(self, text):
|
||||||
win, bh, bw = self.box("Bestätigen", [text])
|
win, bh, bw = self.box("Bestätigen", [text])
|
||||||
put(win, 1, 2, text, 0, bw - 4)
|
put(win, 1, 2, text, 0, bw - 4)
|
||||||
@@ -1181,7 +1665,8 @@ class TUI:
|
|||||||
" (z.B. Kerio SMTP/POP3). Lokale/private Netze und DNS gehen direkt.",
|
" (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, t Verbindungstest,",
|
||||||
|
"F5 Liste neu laden, q Ende. Im Formular: F3 testet die eingegebenen Werte.",
|
||||||
"Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt.",
|
"Nach jeder Änderung wird 'systemctl daemon-reload' ausgeführt.",
|
||||||
"Beim Beenden kann man geänderte Dienste neu starten lassen.",
|
"Beim Beenden kann man geänderte Dienste neu starten lassen.",
|
||||||
]
|
]
|
||||||
@@ -1213,6 +1698,7 @@ class TUI:
|
|||||||
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)))
|
||||||
buttons = [FormField("save", "Speichern", "", kind="button"),
|
buttons = [FormField("save", "Speichern", "", kind="button"),
|
||||||
|
FormField("test", "Testen", "", kind="button"),
|
||||||
FormField("cancel", "Abbrechen", "", kind="button")]
|
FormField("cancel", "Abbrechen", "", kind="button")]
|
||||||
if exists:
|
if exists:
|
||||||
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",
|
||||||
@@ -1303,7 +1789,7 @@ class TUI:
|
|||||||
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 ←→ Auswahl Enter weiter F2 Speichern Strg+U Feld leeren Esc Abbrechen")
|
self.key_bar("Tab/↑↓ Feld ←→ Auswahl Enter weiter F2 Speichern F3 Testen Strg+U 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:
|
||||||
@@ -1327,6 +1813,8 @@ class TUI:
|
|||||||
action = "cancel"
|
action = "cancel"
|
||||||
elif k == curses.KEY_F2:
|
elif k == curses.KEY_F2:
|
||||||
action = "save"
|
action = "save"
|
||||||
|
elif k == curses.KEY_F3:
|
||||||
|
action = "test"
|
||||||
elif k in ("\t", curses.KEY_DOWN):
|
elif k in ("\t", curses.KEY_DOWN):
|
||||||
cur = (cur + 1) % len(items)
|
cur = (cur + 1) % len(items)
|
||||||
elif k in (curses.KEY_BTAB, curses.KEY_UP):
|
elif k in (curses.KEY_BTAB, curses.KEY_UP):
|
||||||
@@ -1370,6 +1858,17 @@ class TUI:
|
|||||||
item.value = item.value[:item.pos] + k + item.value[item.pos:]
|
item.value = item.value[:item.pos] + k + item.value[item.pos:]
|
||||||
item.pos += len(k)
|
item.pos += len(k)
|
||||||
|
|
||||||
|
if action == "test":
|
||||||
|
self.cursor(False)
|
||||||
|
try:
|
||||||
|
cand = validate_settings(collect(), is_global)
|
||||||
|
eff, _ = resolve(empty_settings() if is_global else cand, cand if is_global else g)
|
||||||
|
except ProxyError as e:
|
||||||
|
error = "Test nicht möglich: %s" % e
|
||||||
|
continue
|
||||||
|
self.run_test(eff, unit, "Test %s (ungespeicherte Werte)" % (unit or "globaler Proxy"))
|
||||||
|
error = ""
|
||||||
|
continue
|
||||||
if action == "cancel":
|
if action == "cancel":
|
||||||
self.cursor(False)
|
self.cursor(False)
|
||||||
return None
|
return None
|
||||||
@@ -1601,6 +2100,43 @@ def cmd_show(cfg, args):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def print_test_results(results):
|
||||||
|
marks = {"ok": "[ OK ] ", "fail": "[FEHLER]", "info": " "}
|
||||||
|
for status, text in results:
|
||||||
|
print("%s %s" % (marks[status], text))
|
||||||
|
failed = sum(1 for st, _ in results if st == "fail")
|
||||||
|
print()
|
||||||
|
print("Ergebnis: alles in Ordnung." if not failed else "Ergebnis: %d Test(s) fehlgeschlagen." % failed)
|
||||||
|
return 1 if failed else 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_targets(args):
|
||||||
|
targets = [parse_target(t) for t in (args.target or [])]
|
||||||
|
if args.mail:
|
||||||
|
targets += [parse_target(t) for t in TEST_TARGETS_MAIL]
|
||||||
|
if not targets:
|
||||||
|
targets = [parse_target(t) for t in TEST_TARGETS_WEB]
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_test(cfg, args):
|
||||||
|
vals = fields_from_args(args)
|
||||||
|
targets = test_targets(args)
|
||||||
|
if args.unit:
|
||||||
|
unit = normalize_unit(args.unit)
|
||||||
|
base = dict(cfg.services.get(unit) or empty_settings())
|
||||||
|
base.update(vals)
|
||||||
|
svc = validate_settings(base, is_global=False)
|
||||||
|
what = "Dienst %s%s" % (unit, "" if unit in cfg.services else " (nicht konfiguriert → globaler Proxy)")
|
||||||
|
else:
|
||||||
|
unit = None
|
||||||
|
svc = validate_settings(vals, is_global=False)
|
||||||
|
what = "angegebener Proxy" if any(svc[k] for k in FIELDS) else "globaler Proxy"
|
||||||
|
eff, _ = resolve(svc, cfg.global_)
|
||||||
|
print("Verbindungstest: %s" % what)
|
||||||
|
return print_test_results(run_proxy_test(eff, targets, cfg, unit, args.timeout))
|
||||||
|
|
||||||
|
|
||||||
def build_parser():
|
def build_parser():
|
||||||
def common(sub):
|
def common(sub):
|
||||||
d = argparse.SUPPRESS if sub else None
|
d = argparse.SUPPRESS if sub else None
|
||||||
@@ -1645,11 +2181,18 @@ def build_parser():
|
|||||||
p.add_argument("units", nargs="*", metavar="DIENST")
|
p.add_argument("units", nargs="*", metavar="DIENST")
|
||||||
p.add_argument("--all", action="store_true")
|
p.add_argument("--all", action="store_true")
|
||||||
sub.add_parser("apply", add_help=False, parents=[c])
|
sub.add_parser("apply", add_help=False, parents=[c])
|
||||||
|
p = sub.add_parser("test", add_help=False, parents=[c, fields])
|
||||||
|
p.add_argument("unit", nargs="?")
|
||||||
|
p.add_argument("--target", action="append", metavar="HOST:PORT")
|
||||||
|
p.add_argument("--mail", action="store_true")
|
||||||
|
p.add_argument("--timeout", type=float, default=TEST_TIMEOUT)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
argv = sys.argv[1:] if argv is None else argv
|
argv = sys.argv[1:] if argv is None else argv
|
||||||
|
if argv and argv[0] == "_probe": # intern: Kindprozess des proxychains-Selbsttests
|
||||||
|
return probe_direct(argv[2:], float(argv[1]))
|
||||||
if "-h" in argv or "--help" in argv or (argv and argv[0] == "help"):
|
if "-h" in argv or "--help" in argv or (argv and argv[0] == "help"):
|
||||||
print(HELP_TEXT)
|
print(HELP_TEXT)
|
||||||
return 0
|
return 0
|
||||||
@@ -1658,7 +2201,9 @@ def main(argv=None):
|
|||||||
|
|
||||||
custom_dirs = bool(args.config_dir or args.systemd_dir)
|
custom_dirs = bool(args.config_dir or args.systemd_dir)
|
||||||
cfg = Config(args.config_dir or DEFAULT_CONFIG_DIR, args.systemd_dir or DEFAULT_SYSTEMD_DIR)
|
cfg = Config(args.config_dir or DEFAULT_CONFIG_DIR, args.systemd_dir or DEFAULT_SYSTEMD_DIR)
|
||||||
if os.geteuid() != 0 and not custom_dirs:
|
# 'test' mit eigenem --proxy/--host geht auch ohne root (ohne gespeicherte Konfiguration)
|
||||||
|
adhoc_test = (command == "test" and not args.unit and (args.proxy or args.host))
|
||||||
|
if os.geteuid() != 0 and not custom_dirs and not adhoc_test:
|
||||||
print("%s: Bitte als root ausführen (z.B. sudo %s)." % (PROG, PROG), file=sys.stderr)
|
print("%s: Bitte als root ausführen (z.B. sudo %s)." % (PROG, PROG), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
@@ -1667,14 +2212,18 @@ def main(argv=None):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cfg.load()
|
try:
|
||||||
|
cfg.load()
|
||||||
|
except ProxyError:
|
||||||
|
if not adhoc_test:
|
||||||
|
raise
|
||||||
if command == "tui":
|
if command == "tui":
|
||||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||||
raise ProxyError("Das Menü braucht ein Terminal. Für Skripte die Befehle nutzen (siehe --help).")
|
raise ProxyError("Das Menü braucht ein Terminal. Für Skripte die Befehle nutzen (siehe --help).")
|
||||||
return run_tui(cfg, args)
|
return run_tui(cfg, args)
|
||||||
handler = {
|
handler = {
|
||||||
"list": cmd_list, "show": cmd_show, "global": cmd_global, "set": cmd_set,
|
"list": cmd_list, "show": cmd_show, "global": cmd_global, "set": cmd_set,
|
||||||
"unset": cmd_unset, "apply": cmd_apply,
|
"unset": cmd_unset, "apply": cmd_apply, "test": cmd_test,
|
||||||
}[command]
|
}[command]
|
||||||
return handler(cfg, args)
|
return handler(cfg, args)
|
||||||
except ProxyError as e:
|
except ProxyError as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user