All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m11s
- Automatische detectie van het hostnetwerk: interface, subnet, gateway en een voorgestelde vrije IP-range worden bovenaan het formulier getoond en vullen met een klik alles in. - Detectie gebeurt via een kortstondige container in de netwerknamespace van de host, die de routetabel uitleest. Dat repareert meteen een fout: de vorige host_interfaces() las /sys/class/net binnen de container en toonde dus de bridge-interface van de container in plaats van de netwerkkaart van de host. - Live beoordeling terwijl je typt (nieuwe endpoint /api/networks/review): laat zien hoeveel adressen je overhoudt en welke, en legt in gewone taal uit wat er niet klopt. Onderscheid tussen blokkerende fouten en waarschuwingen. - Knop die een vrij blok voorstelt op basis van het subnet, met de gateway ontweken. - Uitklapbare uitleg in het formulier over hoe subnet, gateway, IP-range en host-interface samenhangen, met voorbeeld. - docs/netwerken.md uitgebreid met een schema van de adresverdeling, een tabel met veelgebruikte ranges, en de nadruk op het punt dat het instellen van de range in Server Up niets aan de router doet: daar moet het DHCP-bereik zelf verkleind worden. - Bijgesteld tijdens het testen: een gateway binnen het subnet is de normale situatie en gold ten onrechte als fout wanneer er geen eigen range was opgegeven. Nu alleen een waarschuwing bij een expliciete range. - 248 tests (21 nieuwe). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""Netwerkdetectie: /proc/net/route parsen, ranges voorstellen en beoordelen."""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
|
|
|
|
from core import networks
|
|
|
|
# Zoals /proc/net/route eruitziet op een host met één LAN-interface.
|
|
# Adressen staan als little-endian hex: 0100A8C0 = 192.168.0.1
|
|
ROUTE_ENKEL = """Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT
|
|
eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0
|
|
eth0\t0001A8C0\t00000000\t0001\t0\t0\t100\t00FFFFFF\t0\t0\t0
|
|
docker0\t000011AC\t00000000\t0001\t0\t0\t0\t0000FFFF\t0\t0\t0
|
|
"""
|
|
|
|
|
|
def test_parse_vindt_lan_interface():
|
|
n = networks.parse_proc_route(ROUTE_ENKEL)
|
|
assert len(n) == 1
|
|
assert n[0] == {"interface": "eth0", "subnet": "192.168.1.0/24",
|
|
"gateway": "192.168.1.1"}
|
|
|
|
|
|
def test_parse_negeert_interfaces_zonder_default_route():
|
|
"""docker0 heeft wel een subnet maar geen default route — geen LAN dus."""
|
|
assert all(x["interface"] != "docker0"
|
|
for x in networks.parse_proc_route(ROUTE_ENKEL))
|
|
|
|
|
|
def test_parse_meerdere_interfaces():
|
|
tekst = ROUTE_ENKEL + "eth1\t00000000\t0102A8C0\t0003\t0\t0\t200\t00000000\t0\t0\t0\n" \
|
|
"eth1\t0002A8C0\t00000000\t0001\t0\t0\t200\t00FFFFFF\t0\t0\t0\n"
|
|
n = networks.parse_proc_route(tekst)
|
|
assert [x["interface"] for x in n] == ["eth0", "eth1"]
|
|
assert n[1]["subnet"] == "192.168.2.0/24"
|
|
|
|
|
|
def test_parse_lege_of_onzinnige_invoer():
|
|
assert networks.parse_proc_route("") == []
|
|
assert networks.parse_proc_route("kop\nonzin regel\n") == []
|
|
|
|
|
|
# ── Voorgestelde range ───────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.parametrize("subnet,gateway,verwacht", [
|
|
("192.168.1.0/24", "192.168.1.1", "192.168.1.240/28"),
|
|
("10.0.0.0/24", "10.0.0.1", "10.0.0.240/28"),
|
|
("192.168.0.0/16", "192.168.0.1", "192.168.255.240/28"),
|
|
])
|
|
def test_suggest_range_pakt_het_einde_van_het_subnet(subnet, gateway, verwacht):
|
|
assert networks.suggest_range(subnet, gateway) == verwacht
|
|
|
|
|
|
def test_suggest_range_vermijdt_de_gateway():
|
|
"""Een gateway aan het eind van het subnet mag niet in de range vallen."""
|
|
r = networks.suggest_range("192.168.1.0/24", "192.168.1.250")
|
|
import ipaddress
|
|
assert ipaddress.ip_address("192.168.1.250") not in ipaddress.ip_network(r)
|
|
|
|
|
|
def test_suggest_range_bij_klein_subnet():
|
|
assert networks.suggest_range("192.168.1.0/28", "192.168.1.1") == ""
|
|
|
|
|
|
def test_suggest_range_bij_onzin():
|
|
assert networks.suggest_range("geen-subnet") == ""
|
|
|
|
|
|
# ── Beoordeling ──────────────────────────────────────────────────────────────
|
|
|
|
BASIS = {"name": "lan", "driver": "macvlan", "parent": "eth0",
|
|
"subnet": "192.168.1.0/24", "gateway": "192.168.1.1",
|
|
"ip_range": "192.168.1.240/28"}
|
|
|
|
|
|
def test_review_goede_definitie():
|
|
r = networks.review(BASIS)
|
|
assert r["ok"] is True
|
|
assert r["errors"] == []
|
|
assert r["info"]["count"] == 14 # .241 t/m .254
|
|
assert r["info"]["first"] == "192.168.1.241"
|
|
assert r["info"]["last"] == "192.168.1.254"
|
|
|
|
|
|
def test_review_waarschuwt_bij_gateway_binnen_een_expliciete_range():
|
|
r = networks.review({**BASIS, "ip_range": "192.168.1.0/25"})
|
|
assert r["ok"] is True # Docker houdt het adres vrij, dus geen fout
|
|
assert any("gateway" in w for w in r["warnings"])
|
|
|
|
|
|
def test_review_ziet_gateway_in_het_subnet_niet_als_probleem():
|
|
"""Zonder eigen range is 'gateway zit in het subnet' de normale situatie."""
|
|
r = networks.review({**BASIS, "ip_range": ""})
|
|
assert not any("gateway" in w.lower() for w in r["warnings"])
|
|
|
|
|
|
def test_review_waarschuwt_zonder_range():
|
|
r = networks.review({**BASIS, "ip_range": ""})
|
|
assert r["ok"] is True
|
|
assert any("DHCP" in w for w in r["warnings"])
|
|
|
|
|
|
def test_review_waarschuwt_zonder_gateway():
|
|
r = networks.review({**BASIS, "gateway": ""})
|
|
assert r["ok"] is True
|
|
assert any("internet" in w for w in r["warnings"])
|
|
|
|
|
|
def test_review_waarschuwt_bij_erg_kleine_range():
|
|
r = networks.review({**BASIS, "ip_range": "192.168.1.252/30"})
|
|
assert r["ok"] is True
|
|
assert any("adressen" in w for w in r["warnings"])
|
|
|
|
|
|
def test_review_geeft_blokkerende_validatiefout_door():
|
|
r = networks.review({**BASIS, "subnet": "onzin"})
|
|
assert r["ok"] is False
|
|
assert any("subnet" in e.lower() for e in r["errors"])
|
|
|
|
|
|
def test_review_range_buiten_subnet():
|
|
r = networks.review({**BASIS, "ip_range": "10.9.9.0/28"})
|
|
assert r["ok"] is False
|
|
|
|
|
|
# ── Endpoints ────────────────────────────────────────────────────────────────
|
|
|
|
def test_review_endpoint(client):
|
|
from conftest import login
|
|
csrf = login(client)
|
|
r = client.post("/api/networks/review", json=BASIS,
|
|
headers={"X-CSRF-Token": csrf})
|
|
d = r.get_json()
|
|
assert d["ok"] is True
|
|
assert d["info"]["count"] == 14
|
|
|
|
|
|
def test_networks_endpoint_geeft_detectie_terug(client, monkeypatch):
|
|
from conftest import login
|
|
from core import networks as nw
|
|
login(client)
|
|
monkeypatch.setattr(nw, "detect_host_networks", lambda force=False: {
|
|
"networks": [{"interface": "eth0", "subnet": "192.168.1.0/24",
|
|
"gateway": "192.168.1.1", "suggested_range": "192.168.1.240/28",
|
|
"driver": "macvlan", "name": "lan"}],
|
|
"error": ""})
|
|
d = client.get("/api/networks").get_json()
|
|
assert d["detected"][0]["interface"] == "eth0"
|
|
assert d["interfaces"] == ["eth0"]
|
|
|
|
|
|
def test_detectiefout_wordt_doorgegeven(client, monkeypatch):
|
|
from conftest import login
|
|
from core import networks as nw
|
|
login(client)
|
|
monkeypatch.setattr(nw, "detect_host_networks", lambda force=False: {
|
|
"networks": [], "error": "docker niet bereikbaar"})
|
|
d = client.get("/api/networks").get_json()
|
|
assert d["detect_error"] == "docker niet bereikbaar"
|
|
assert d["detected"] == []
|