All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m3s
Gemeld met twee screenshots: een foutmelding dat de IP-range buiten het subnet
lag terwijl 10.0.20.240/28 gewoon binnen 10.0.20.0/24 valt, en een geval zonder
melding waarbij aanmaken toch niet lukte. Twee losse oorzaken.
1. Verouderd beoordelingsantwoord
De live beoordeling gaat af bij elke toetsaanslag. Tijdens het typen van
/28 kom je langs /2, en 10.0.20.240/2 normaliseert naar 0.0.0.0/2 - buiten
het subnet. Kwam dat antwoord na het goede binnen, dan bleef die fout staan
en bleef de knop uit.
- Volgnummer op de verzoeken; alleen het antwoord bij de laatste vraag telt.
- Knop niet meer uitgeschakeld op clientstatus; de server beslist bij het
opslaan, zodat een verouderde beoordeling nooit blokkeert.
2. CIDR met hostbits
10.0.20.200/28 leest Python met strict=False als 10.0.20.192/28, maar Docker
weigert het met "has host bits set". We valideerden soepel en stuurden de
ruwe tekst door, dus de interface keurde iets goed dat bij het aanmaken
alsnog strandde.
- normalize() zet subnet en ip_range om naar canonieke vorm voor ze naar
Docker gaan en voor ze worden opgeslagen.
- Waarschuwing die toont wat er van de invoer gemaakt is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
292 lines
12 KiB
Python
292 lines
12 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_loopt_niet_vast_op_een_enorm_bereik():
|
|
"""Tijdens het typen van '192.168.1.0/24' is '192.168.1.0/2' een geldig
|
|
netwerk met ruim een miljard adressen. Dat mag geen lijst worden: het
|
|
verzoek liep vast en de browser kreeg geen JSON meer terug."""
|
|
import time
|
|
start = time.time()
|
|
r = networks.review({**BASIS, "subnet": "192.168.1.0/2", "ip_range": ""})
|
|
duur = time.time() - start
|
|
assert duur < 1.0, f"duurde {duur:.1f}s — bouwt vermoedelijk weer een lijst"
|
|
# 2^30 adressen, min netwerk- en broadcastadres, min de gateway die er
|
|
# toevallig binnen valt.
|
|
assert r["info"]["count"] == 1073741821
|
|
|
|
|
|
@pytest.mark.parametrize("prefix", ["1", "2", "8", "12", "16"])
|
|
def test_review_is_snel_bij_elk_prefix(prefix):
|
|
"""Elke tussenstand tijdens het typen moet direct antwoord geven."""
|
|
import time
|
|
start = time.time()
|
|
networks.review({**BASIS, "subnet": f"10.0.0.0/{prefix}", "ip_range": ""})
|
|
assert time.time() - start < 1.0
|
|
|
|
|
|
def test_suggest_range_is_snel_bij_een_groot_subnet():
|
|
import time
|
|
start = time.time()
|
|
r = networks.suggest_range("10.0.0.0/8", "10.0.0.1")
|
|
assert time.time() - start < 1.0
|
|
assert r == "10.255.255.240/28"
|
|
|
|
|
|
def test_review_waarschuwt_bij_een_verdacht_groot_bereik():
|
|
r = networks.review({**BASIS, "subnet": "10.0.0.0/8",
|
|
"gateway": "10.0.0.1", "ip_range": ""})
|
|
assert any("prefix" in w for w in r["warnings"])
|
|
|
|
|
|
@pytest.mark.parametrize("bereik,eerste,laatste,aantal", [
|
|
("192.168.1.0/24", "192.168.1.1", "192.168.1.254", 254),
|
|
("192.168.1.240/28", "192.168.1.241", "192.168.1.254", 14),
|
|
("192.168.1.0/30", "192.168.1.1", "192.168.1.2", 2),
|
|
("192.168.1.0/31", "192.168.1.0", "192.168.1.1", 2),
|
|
("192.168.1.5/32", "192.168.1.5", "192.168.1.5", 1),
|
|
])
|
|
def test_bruikbaar_bereik_rekent_goed(bereik, eerste, laatste, aantal):
|
|
"""Zelfde uitkomst als hosts() zou geven, maar zonder die te doorlopen."""
|
|
import ipaddress
|
|
net = ipaddress.ip_network(bereik)
|
|
e, l, n = networks._bruikbaar_bereik(net)
|
|
assert (str(e), str(l), n) == (eerste, laatste, aantal)
|
|
assert n == len(list(net.hosts())) # controle tegen de stdlib
|
|
|
|
|
|
def test_bruikbaar_bereik_trekt_de_gateway_eraf():
|
|
import ipaddress
|
|
net = ipaddress.ip_network("192.168.1.0/24")
|
|
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.1")
|
|
assert (str(e), n) == ("192.168.1.2", 253)
|
|
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.254")
|
|
assert (str(l), n) == ("192.168.1.253", 253)
|
|
# Gateway in het midden verschuift de randen niet.
|
|
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.100")
|
|
assert (str(e), str(l), n) == ("192.168.1.1", "192.168.1.254", 253)
|
|
|
|
|
|
def test_bruikbaar_bereik_negeert_gateway_buiten_het_bereik():
|
|
import ipaddress
|
|
net = ipaddress.ip_network("192.168.1.240/28")
|
|
assert networks._bruikbaar_bereik(net, "192.168.1.1")[2] == 14
|
|
|
|
|
|
# ── Normalisatie ─────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.parametrize("ingevoerd,verwacht", [
|
|
("10.0.20.200/28", "10.0.20.192/28"), # hostbits gezet
|
|
("10.0.20.240/28", "10.0.20.240/28"), # al uitgelijnd
|
|
("192.168.1.5/24", "192.168.1.0/24"),
|
|
])
|
|
def test_normalize_lijnt_de_range_uit(ingevoerd, verwacht):
|
|
"""Docker weigert een CIDR met hostbits ('has host bits set'), terwijl wij
|
|
hem soepel inlezen. Zonder omzetting faalt `docker network create` op iets
|
|
dat de interface wél accepteerde."""
|
|
n = networks.normalize({**BASIS, "ip_range": ingevoerd})
|
|
assert n["ip_range"] == verwacht
|
|
|
|
|
|
def test_normalize_laat_onzin_staan_voor_validate():
|
|
n = networks.normalize({**BASIS, "ip_range": "geen-cidr"})
|
|
assert n["ip_range"] == "geen-cidr"
|
|
|
|
|
|
def test_normalize_raakt_de_rest_niet_aan():
|
|
n = networks.normalize(BASIS)
|
|
assert n["name"] == "lan" and n["driver"] == "macvlan" and n["parent"] == "eth0"
|
|
|
|
|
|
def test_review_waarschuwt_bij_niet_uitgelijnde_range():
|
|
r = networks.review({**BASIS, "ip_range": "10.0.20.200/28",
|
|
"subnet": "10.0.20.0/24", "gateway": "10.0.20.1"})
|
|
assert r["ok"] is True
|
|
assert any("10.0.20.192/28" in w for w in r["warnings"])
|
|
|
|
|
|
def test_niet_uitgelijnde_range_uit_het_formulier_wordt_opgeslagen_als_canoniek(client, monkeypatch):
|
|
"""Wat we bewaren moet gelijk zijn aan wat Docker heeft gekregen."""
|
|
from conftest import login
|
|
from core import networks as nw
|
|
csrf = login(client)
|
|
monkeypatch.setattr(nw, "exists", lambda naam: False)
|
|
monkeypatch.setattr(nw, "create", lambda net: (True, "ok"))
|
|
client.post("/api/networks",
|
|
json={**BASIS, "subnet": "10.0.20.0/24", "gateway": "10.0.20.1",
|
|
"ip_range": "10.0.20.200/28"},
|
|
headers={"X-CSRF-Token": csrf})
|
|
opgeslagen = client.get("/api/networks").get_json()["networks"]
|
|
assert opgeslagen[0]["ip_range"] == "10.0.20.192/28"
|
|
|
|
|
|
def test_scherm_uit_de_melding_valt_niet_ten_onrechte_buiten_het_subnet():
|
|
"""De combinatie uit de foutmelding van de gebruiker: 10.0.20.240/28 in
|
|
10.0.20.0/24 is geldig en mag geen fout geven."""
|
|
r = networks.review({"name": "lan", "driver": "macvlan", "parent": "ens18",
|
|
"subnet": "10.0.20.0/24", "gateway": "10.0.20.1",
|
|
"ip_range": "10.0.20.240/28"})
|
|
assert r["ok"] is True
|
|
assert r["errors"] == []
|
|
assert r["info"]["count"] == 14
|
|
|
|
|
|
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"] == []
|