fix: thread wg-easy v15's preshared key into the box's wg-quick config

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3jVwAqESRjPdW9P9nvfem
This commit is contained in:
Robert Syrnicki 2026-09-19 13:34:45 +02:00
parent 30ee044657
commit 905845187d
2 changed files with 30 additions and 0 deletions

View file

@ -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"

View file

@ -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")