fix: WireGuard peers keep their tunnel key when others come and go (#32)
driver / lint (pull_request) Successful in 17s
driver / standard (pull_request) Failing after 17s
driver / test (3.10) (pull_request) Failing after 3s
driver / test (3.11) (pull_request) Failing after 6s
driver / types (pull_request) Successful in 1m2s
driver / lowest (pull_request) Successful in 59s
driver / test (3.13) (pull_request) Successful in 1m39s
driver / test (3.12) (pull_request) Successful in 1m42s
driver / build (pull_request) Successful in 29s
driver / security (pull_request) Successful in 1m11s
CI / driver (pull_request) Failing after 3m12s
driver / publish (pull_request) Skipped
driver / lint (pull_request) Successful in 17s
driver / standard (pull_request) Failing after 17s
driver / test (3.10) (pull_request) Failing after 3s
driver / test (3.11) (pull_request) Failing after 6s
driver / types (pull_request) Successful in 1m2s
driver / lowest (pull_request) Successful in 59s
driver / test (3.13) (pull_request) Successful in 1m39s
driver / test (3.12) (pull_request) Successful in 1m42s
driver / build (pull_request) Successful in 29s
driver / security (pull_request) Successful in 1m11s
CI / driver (pull_request) Failing after 3m12s
driver / publish (pull_request) Skipped
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; netOrk carries its per-tunnel metadata across (netork#1654).
This commit is contained in:
1 parent
63ce6e78bd
commit
9f45952605
3 files changed
+137
-3
No files matched your search
@@ -0,0 +1,106 @@
|
||||
"""WireGuard peers in ``get_vpn_tunnels()`` keep their key when others come and go (#32).
|
||||
|
||||
The key was a running counter over the peer rows, so a peer added ahead of an
|
||||
existing one renumbered every peer after it, and whatever a consumer stored per
|
||||
key (netOrk's site link, its tunnel check) silently moved to another peer. A
|
||||
peer is its interface and its public key, so that is what the key is made of.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
OFFICE = {
|
||||
"if": "wg0",
|
||||
"type": "peer",
|
||||
"name": "office",
|
||||
"public-key": "aBcD1234efGH5678ijKL9012mnOP3456qrST7890uvw=",
|
||||
"endpoint": "198.51.100.7:51820",
|
||||
"peer-status": "online",
|
||||
"transfer-rx": 10,
|
||||
"transfer-tx": 20,
|
||||
"latest-handshake-age": 45,
|
||||
}
|
||||
LAPTOP = {
|
||||
"if": "wg0",
|
||||
"type": "peer",
|
||||
"name": "laptop",
|
||||
"public-key": "Zz9/+yXw8765vuTS4321rqPO0987nmLK6543jiHG210=",
|
||||
"endpoint": "(none)",
|
||||
"peer-status": "offline",
|
||||
}
|
||||
SERVER = {
|
||||
"if": "wg1",
|
||||
"type": "peer",
|
||||
"name": "server",
|
||||
"public-key": "QqQqWwWwEeEeRrRrTtTtYyYyUuUuIiIiOoOoPpPp123=",
|
||||
"endpoint": "203.0.113.9:51821",
|
||||
"peer-status": "online",
|
||||
}
|
||||
|
||||
|
||||
def _driver(*rows: dict) -> OPNsenseDriver:
|
||||
with patch("napalm_opnsense.opnsense.requests.Session"):
|
||||
drv = OPNsenseDriver(hostname="fw", username="k", password="s")
|
||||
drv.session = MagicMock()
|
||||
show = {"rows": [{"if": "wg0", "type": "interface"}, *rows]}
|
||||
|
||||
def get(path: str) -> dict:
|
||||
if path == "/api/wireguard/service/show":
|
||||
return show
|
||||
raise RuntimeError("not installed") # IPsec and OpenVPN are not part of this
|
||||
|
||||
drv._get = get # type: ignore[method-assign]
|
||||
return drv
|
||||
|
||||
|
||||
def _by_name(tunnels: dict) -> dict[str, str]:
|
||||
return {t["description"]: key for key, t in tunnels.items()}
|
||||
|
||||
|
||||
class TestTheKey:
|
||||
def test_is_the_interface_and_the_start_of_the_public_key(self):
|
||||
assert list(_driver(OFFICE).get_vpn_tunnels()) == ["wireguard-wg0-aBcD1234"]
|
||||
|
||||
def test_survives_a_peer_added_ahead_of_it(self):
|
||||
before = _by_name(_driver(OFFICE, SERVER).get_vpn_tunnels())
|
||||
after = _by_name(_driver(OFFICE, LAPTOP, SERVER).get_vpn_tunnels())
|
||||
assert after["server"] == before["server"]
|
||||
assert after["office"] == before["office"]
|
||||
|
||||
def test_is_safe_in_a_url_path(self):
|
||||
# The key is a path segment (PATCH /devices/{id}/vpn-tunnels/{key}), and
|
||||
# a "/" from base64 would split it even when encoded.
|
||||
key = _by_name(_driver(LAPTOP).get_vpn_tunnels())["laptop"]
|
||||
assert key == "wireguard-wg0-Zz9_-yXw"
|
||||
|
||||
def test_two_peers_with_the_same_key_start_both_stay(self):
|
||||
twin = {
|
||||
**SERVER,
|
||||
"if": "wg0",
|
||||
"name": "twin",
|
||||
"public-key": OFFICE["public-key"][:8] + "x=",
|
||||
}
|
||||
tunnels = _driver(OFFICE, twin).get_vpn_tunnels()
|
||||
assert len(tunnels) == 2
|
||||
|
||||
def test_without_a_public_key_falls_back_to_the_position(self):
|
||||
keyless = {k: v for k, v in OFFICE.items() if k != "public-key"}
|
||||
assert list(_driver(keyless).get_vpn_tunnels()) == ["wireguard-1"]
|
||||
|
||||
|
||||
class TestTheEntry:
|
||||
def test_carries_the_peer_s_reading(self):
|
||||
tunnel = _driver(OFFICE).get_vpn_tunnels()["wireguard-wg0-aBcD1234"]
|
||||
assert tunnel == {
|
||||
"type": "WireGuard",
|
||||
"local_endpoint": "",
|
||||
"remote_endpoint": "198.51.100.7",
|
||||
"is_up": True,
|
||||
"uptime": 45,
|
||||
"bytes_in": 10,
|
||||
"bytes_out": 20,
|
||||
"description": "office",
|
||||
}
|
||||
Reference in new issue
Block a user