This commit is contained in:
2026-06-01 23:22:18 +02:00
parent f061da654d
commit d23d0c15be
14 changed files with 1858 additions and 202 deletions
+275
View File
@@ -143,6 +143,281 @@ def test_connection(cfg: Config, timeout: int = 5) -> tuple[bool, str]:
return False, str(e)
def parse_conf_file(path: str) -> dict | None:
"""Parse un fichier .conf WireGuard et retourne les valeurs extraites."""
result: dict = {}
section = None
try:
with open(path, "r", encoding="utf-8") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#"):
continue
if line.startswith("["):
section = line.strip("[]").lower()
continue
if "=" not in line:
continue
key, val = (x.strip() for x in line.split("=", 1))
if section == "interface":
if key == "PrivateKey":
result["client_private_key"] = val
elif key == "Address":
result["client_address"] = val.split(",")[0].strip()
elif key == "DNS":
result["dns"] = val.split(",")[0].strip()
elif section == "peer":
if key == "PublicKey":
result["server_public_key"] = val
elif key == "Endpoint" and ":" in val:
host, port = val.rsplit(":", 1)
result["server_endpoint"] = host.strip("[]")
try:
result["server_port"] = int(port)
except ValueError:
pass
elif key == "AllowedIPs":
result["allowed_ips"] = val
elif key == "PersistentKeepalive":
try:
result["keepalive"] = int(val)
except ValueError:
pass
# Dériver la clé publique depuis la clé privée importée
if "client_private_key" in result and "client_public_key" not in result:
try:
priv_bytes = base64.b64decode(result["client_private_key"])
priv = X25519PrivateKey.from_private_bytes(priv_bytes)
pub_bytes = priv.public_key().public_bytes_raw()
result["client_public_key"] = base64.b64encode(pub_bytes).decode()
except Exception:
pass
return result or None
except Exception:
return None
def export_conf_file(cfg: Config, dest_path: str) -> tuple[bool, str]:
"""Exporte la configuration courante vers un fichier .conf."""
try:
content = build_client_config(cfg)
with open(dest_path, "w") as f:
f.write(content)
if not is_windows():
os.chmod(dest_path, 0o600)
return True, dest_path
except Exception as e:
return False, str(e)
def get_interface_bytes(iface: str) -> tuple[int, int] | None:
"""Retourne (rx_bytes, tx_bytes) depuis /proc/net/dev (Linux)."""
try:
with open("/proc/net/dev") as f:
for line in f:
if iface in line:
parts = line.split()
return int(parts[1]), int(parts[9])
except Exception:
pass
return None
def ping_server(host: str, timeout: int = 2) -> int | None:
"""Ping ICMP du serveur. Retourne la latence en ms, ou None si inaccessible."""
if not host:
return None
if is_windows():
code, out, _ = run_command(
["ping", "-n", "1", "-w", str(timeout * 1000), host], timeout + 2
)
else:
code, out, _ = run_command(
["ping", "-c", "1", "-W", str(timeout), host], timeout + 2
)
if code != 0:
return None
# Parser "time=X.X ms" ou "temps=X.X ms"
import re
m = re.search(r"[Tt]ime[<=]([\d.]+)\s*ms", out)
if m:
return int(float(m.group(1)))
return None
# ── Kill switch ──────────────────────────────────────────────────────────
_KS_CHAIN = "WGS_KILLSWITCH"
def enable_kill_switch(cfg: Config) -> tuple[bool, str]:
"""Active le kill switch : bloque tout trafic hors du tunnel WireGuard."""
if is_windows():
return False, "Kill switch non disponible sur Windows (utilisez le pare-feu Windows)"
iface = cfg.wg.get("interface_name", "wgs0")
server = cfg.wg.get("server_endpoint", "")
port = str(cfg.wg.get("server_port", 51820))
# Créer la chaîne dédiée (idempotent)
run_privileged(["iptables", "-N", _KS_CHAIN])
run_privileged(["iptables", "-F", _KS_CHAIN])
rules = [
["iptables", "-A", _KS_CHAIN, "-o", "lo", "-j", "ACCEPT"],
["iptables", "-A", _KS_CHAIN, "-o", iface, "-j", "ACCEPT"],
["iptables", "-A", _KS_CHAIN, "-m", "state",
"--state", "ESTABLISHED,RELATED", "-j", "ACCEPT"],
]
if server:
rules.append(["iptables", "-A", _KS_CHAIN,
"-d", server, "-p", "udp", "--dport", port, "-j", "ACCEPT"])
rules.append(["iptables", "-A", _KS_CHAIN, "-j", "REJECT",
"--reject-with", "icmp-net-unreachable"])
for rule in rules:
code, _, err = run_privileged(rule)
if code != 0:
disable_kill_switch(cfg)
return False, f"Erreur iptables : {err}"
# Insérer la chaîne dans OUTPUT (évite les doublons)
run_privileged(["iptables", "-D", "OUTPUT", "-j", _KS_CHAIN])
code, _, err = run_privileged(["iptables", "-I", "OUTPUT", "-j", _KS_CHAIN])
if code != 0:
disable_kill_switch(cfg)
return False, err
return True, f"Kill switch activé (interface {iface})"
def disable_kill_switch(cfg: Config) -> tuple[bool, str]:
"""Désactive le kill switch."""
if is_windows():
return True, "N/A"
run_privileged(["iptables", "-D", "OUTPUT", "-j", _KS_CHAIN])
run_privileged(["iptables", "-F", _KS_CHAIN])
run_privileged(["iptables", "-X", _KS_CHAIN])
return True, "Kill switch désactivé"
def kill_switch_active() -> bool:
if is_windows():
return False
code, out, _ = run_command(["iptables", "-L", "OUTPUT", "-n"])
return _KS_CHAIN in out
# ── Test DNS leak ─────────────────────────────────────────────────────────
def dns_leak_test(cfg: Config) -> dict:
"""
Teste si le DNS fuit en dehors du tunnel.
Retourne {"status": ok|leak|unknown, "resolvers": [...], "expected": str}
"""
expected_dns = cfg.wg.get("dns", "")
resolvers: list[str] = []
# 1. Lire /etc/resolv.conf (Linux) ou ipconfig /all (Windows)
if not is_windows():
try:
with open("/etc/resolv.conf") as f:
for line in f:
if line.startswith("nameserver"):
parts = line.split()
if len(parts) >= 2:
resolvers.append(parts[1])
except Exception:
pass
else:
code, out, _ = run_command(["ipconfig", "/all"])
if code == 0:
import re
resolvers = re.findall(r"DNS Servers.*?:(.*?)(?:\n\S|\Z)",
out, re.DOTALL)
# 2. Résoudre un domaine test via dig/nslookup pour voir quel serveur répond
probe_ip: str = ""
code, out, _ = run_command(["dig", "+short", "+time=3", "whoami.akamai.net"], 6)
if code == 0:
lines = [l.strip() for l in out.splitlines() if l.strip()]
if lines:
probe_ip = lines[-1]
if not resolvers:
return {"status": "unknown", "resolvers": [], "expected": expected_dns,
"probe_ip": probe_ip}
# 3. Comparer avec le DNS configuré
if expected_dns and any(expected_dns in r for r in resolvers):
status = "ok"
elif expected_dns:
status = "leak"
else:
status = "unknown"
return {
"status": status,
"resolvers": resolvers,
"expected": expected_dns,
"probe_ip": probe_ip,
}
# ── Génération config serveur ─────────────────────────────────────────────
def generate_server_config(
server_port: int = 51820,
server_address: str = "10.8.0.1/24",
client_address: str = "10.8.0.2/24",
client_allowed_ips: str = "10.8.0.2/32",
) -> dict:
"""
Génère une paire complète (server_conf, client_conf) avec de nouvelles clés.
Retourne un dict avec server_priv/pub, client_priv/pub, server_conf, client_conf.
"""
srv_priv, srv_pub = generate_keypair()
cli_priv, cli_pub = generate_keypair()
psk = generate_preshared_key()
server_conf = "\n".join([
"[Interface]",
f"PrivateKey = {srv_priv}",
f"Address = {server_address}",
f"ListenPort = {server_port}",
"PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
"PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE",
"",
"# === Peer client ===",
"[Peer]",
f"PublicKey = {cli_pub}",
f"PresharedKey = {psk}",
f"AllowedIPs = {client_allowed_ips}",
])
client_conf = "\n".join([
"[Interface]",
f"PrivateKey = {cli_priv}",
f"Address = {client_address}",
"DNS = 1.1.1.1",
"",
"[Peer]",
f"PublicKey = {srv_pub}",
f"PresharedKey = {psk}",
"AllowedIPs = 0.0.0.0/0",
"Endpoint = <SERVER_IP>:" + str(server_port),
"PersistentKeepalive = 25",
])
return {
"server_priv": srv_priv, "server_pub": srv_pub,
"client_priv": cli_priv, "client_pub": cli_pub,
"psk": psk,
"server_conf": server_conf,
"client_conf": client_conf,
}
def get_status_info(cfg: Config) -> dict:
name = cfg.wg.get("interface_name", "wgs0")
info = {