From 905845187d4f697b817a8a4380f6a79c6c7feecc Mon Sep 17 00:00:00 2001 From: Robert Syrnicki Date: Sat, 19 Sep 2026 13:34:45 +0200 Subject: [PATCH] fix: thread wg-easy v15's preshared key into the box's wg-quick config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit furtka-gateway's wg-easy v15 client (wgeasy_v15.py) generates a PresharedKey per WireGuard client by default, which the server enforces even if the box's own conf omits it — dropping it here breaks the handshake silently. Include it in [Peer] when present; v14 has no preshared_key field at all, so this stays a no-op for that path. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01R3jVwAqESRjPdW9P9nvfem --- furtka/wireguard.py | 10 ++++++++++ tests/test_wireguard.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/furtka/wireguard.py b/furtka/wireguard.py index 55208ac..006f949 100644 --- a/furtka/wireguard.py +++ b/furtka/wireguard.py @@ -39,7 +39,16 @@ def _conf_content(wg: dict) -> str: subnet only), which this module passes through verbatim rather than widening it, so a compromised gateway can't pull the box's other traffic through it. + + `preshared_key` is only present when the gateway's wg-easy is v15 + (which generates one per client by default and enforces it + server-side) — omitted (None/missing) for v14, which has none. + Dropping a present preshared key here would break the handshake + silently, since the server side would still expect it. """ + preshared_key_line = "" + if wg.get("preshared_key"): + preshared_key_line = f"PresharedKey = {wg['preshared_key']}\n" return ( "[Interface]\n" f"PrivateKey = {wg['private_key']}\n" @@ -47,6 +56,7 @@ def _conf_content(wg: dict) -> str: "\n" "[Peer]\n" f"PublicKey = {wg['server_public_key']}\n" + f"{preshared_key_line}" f"Endpoint = {wg['endpoint']}\n" f"AllowedIPs = {wg['allowed_ips']}\n" "PersistentKeepalive = 25\n" diff --git a/tests/test_wireguard.py b/tests/test_wireguard.py index 486cb01..7a8a43d 100644 --- a/tests/test_wireguard.py +++ b/tests/test_wireguard.py @@ -32,6 +32,26 @@ def test_conf_content_shape(): assert "PersistentKeepalive = 25" in content +def test_conf_content_omits_preshared_key_when_absent(): + # WG has no "preshared_key" key at all — the v14-gateway shape. + content = wireguard._conf_content(WG) + assert "PresharedKey" not in content + + +def test_conf_content_includes_preshared_key_when_present(): + # v15-gateway shape (see furtka-gateway's wgeasy_v15.py) — the server + # enforces this even if the client's own conf omits it, so dropping it + # here would break the handshake silently. + wg = {**WG, "preshared_key": "cHJlc2hhcmVkLWtleQ=="} + content = wireguard._conf_content(wg) + assert "PresharedKey = cHJlc2hhcmVkLWtleQ==" in content + # Must sit between PublicKey and Endpoint, inside [Peer]. + peer_section = content.split("[Peer]\n", 1)[1] + assert peer_section.index("PublicKey") < peer_section.index( + "PresharedKey" + ) < peer_section.index("Endpoint") + + def test_enable_requires_wg_config(tmp_path): with pytest.raises(ValueError, match="wg config is required"): wireguard.set_tunnel_enabled(True, conf_path=tmp_path / "furtka-gw.conf")