fix: WireGuard peers keep their tunnel key when others come and go (#32) (0.4.1)
CI / test (3.10) (push) Failing after 5s
CI / test (3.11) (push) Successful in 33s
CI / test (3.12) (push) Successful in 36s

get_vpn_tunnels() keyed WireGuard peers with a running counter over the
peer rows, so a peer added ahead of an existing one renumbered every peer
after it. Whatever a consumer stores per key moved with it: netOrk's site
link and tunnel check ended up on a new, offline peer and alarmed while the
real tunnel was up.

The key is now the interface and the start of the public key, as in
napalm-linux, made URL-safe because the key is a path segment. Two peers
sharing the prefix get the whole key; a row without a public key keeps its
position. IPsec and OpenVPN keys were already stable.

Every WireGuard key changes once with this release; netOrk carries its
per-tunnel metadata across (netork#1654).
This commit is contained in:
Christian Manivong committed 2026-10-27 09:10:42 +01:00
1 parent 692a584fd2
commit 661b4fd7c7
3 files changed
+132 -4

No files matched your search

+25 -3
View File
@@ -92,6 +92,28 @@ def _carp_master(entry: dict[str, Any]) -> bool:
return str(entry.get("status") or "").strip().upper() == "MASTER"
#: How much of a public key names a peer, the same length as napalm-linux uses.
_WG_KEY_PREFIX = 8
def _wireguard_key(row: dict[str, Any], position: int, taken: dict[str, Any]) -> str:
"""A WireGuard peer's tunnel key: its interface and the start of its public key.
Not its position: a peer added ahead of another would renumber it, and
whatever a consumer keeps per key would move to a different peer (#32). The
base64 is made URL-safe because the key is used as a path segment. Two peers
sharing a prefix get the whole key; one without a key keeps its position.
"""
public_key = (row.get("public-key") or "").strip()
if not public_key:
return f"wireguard-{position}"
safe = public_key.rstrip("=").replace("+", "-").replace("/", "_")
key = f"wireguard-{row.get('if') or 'wg'}-{safe[:_WG_KEY_PREFIX]}"
if key in taken:
key = f"wireguard-{row.get('if') or 'wg'}-{safe}"
return key
class OPNsenseDriver(
OPNsensePingMixin,
ConfigSyncMixin,
@@ -2149,8 +2171,8 @@ class OPNsenseDriver(
# {"if":"wg0", "type":"interface", ...},
# {"if":"wg0", "type":"peer", "public-key":"...",
# "endpoint":"1.2.3.4:51820", "transfer-rx":N, "transfer-tx":N,
# "name":"HCQ", "peer-status":"online",
# "latest-handshake-age":45, "ifname":"HCQ"}, ...
# "name":"office", "peer-status":"online",
# "latest-handshake-age":45, "ifname":"office"}, ...
# ]}
data = self._get("/api/wireguard/service/show")
rows = data.get("rows", []) if isinstance(data, dict) else []
@@ -2179,7 +2201,7 @@ class OPNsenseDriver(
or f"wg-peer-{wg_idx}"
)
tunnels[f"wireguard-{wg_idx}"] = {
tunnels[_wireguard_key(row, wg_idx, tunnels)] = {
"type": "WireGuard",
"local_endpoint": "",
"remote_endpoint": remote_ip,