Some checks failed
Deploy server-up (dev) / deploy (push) Has been cancelled
"Ik wil van 10.0.20.200 tot .254" bestaat niet als CIDR-notatie: 55 adressen, terwijl Docker alleen uitgelijnde blokken van een macht van twee accepteert. Dat is niet iets wat een gebruiker zelf hoort uit te rekenen. - Nieuwe range_options(): vertaalt een gewenst begin- en eindadres naar twee bruikbare blokken. "Ruim" is het kleinste blok dat de hele wens omvat, "krap" het grootste blok dat er volledig binnen valt. Met per optie hoeveel adressen erbij komen of ontbreken. - Endpoint POST /api/networks/range en een uitklapbare van-tot-hulp onder het IP-range-veld; klikken op een optie vult het veld. - Voor 10.0.20.200-254 levert dat 10.0.20.192/26 op: 62 adressen, .193 t/m .254. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
351 lines
14 KiB
Python
351 lines
14 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"] == []
|
|
|
|
|
|
# ── Van-tot omrekenen ────────────────────────────────────────────────────────
|
|
|
|
def test_range_options_voor_200_tot_254():
|
|
"""De vraag uit de praktijk: 55 adressen, wat geen geldig CIDR-blok is."""
|
|
r = networks.range_options("10.0.20.200", "10.0.20.254", "10.0.20.0/24")
|
|
assert r["ok"] is True
|
|
ruim = next(o for o in r["options"] if o["kind"] == "ruim")
|
|
assert ruim["cidr"] == "10.0.20.192/26"
|
|
assert ruim["count"] == 62
|
|
assert (ruim["first"], ruim["last"]) == ("10.0.20.193", "10.0.20.254")
|
|
krap = next(o for o in r["options"] if o["kind"] == "krap")
|
|
assert krap["cidr"] == "10.0.20.208/28"
|
|
|
|
|
|
def test_range_options_exact_passend_blok():
|
|
"""Valt de wens toevallig samen met een blok, dan is er maar één antwoord."""
|
|
r = networks.range_options("10.0.20.240", "10.0.20.255", "10.0.20.0/24")
|
|
assert [o["cidr"] for o in r["options"]] == ["10.0.20.240/28"]
|
|
|
|
|
|
def test_range_options_draait_omgekeerde_invoer_om():
|
|
r = networks.range_options("10.0.20.254", "10.0.20.200")
|
|
assert r["ok"] is True
|
|
assert r["options"][0]["cidr"] == "10.0.20.192/26"
|
|
|
|
|
|
def test_range_options_buiten_het_subnet():
|
|
r = networks.range_options("192.168.9.10", "192.168.9.20", "10.0.20.0/24")
|
|
assert r["ok"] is False
|
|
assert "niet allebei" in r["msg"]
|
|
|
|
|
|
@pytest.mark.parametrize("eerste,laatste", [
|
|
("geen-ip", "10.0.20.254"), ("10.0.20.200", ""), ("", ""),
|
|
])
|
|
def test_range_options_ongeldige_invoer(eerste, laatste):
|
|
r = networks.range_options(eerste, laatste)
|
|
assert r["ok"] is False
|
|
assert r["options"] == []
|
|
|
|
|
|
def test_range_options_meldt_hoeveel_er_bij_komt():
|
|
r = networks.range_options("10.0.20.200", "10.0.20.254", "10.0.20.0/24")
|
|
ruim = next(o for o in r["options"] if o["kind"] == "ruim")
|
|
assert ruim["extra"] == 9 # 64 adressen in het blok, 55 gevraagd
|
|
|
|
|
|
def test_range_endpoint(client):
|
|
from conftest import login
|
|
csrf = login(client)
|
|
r = client.post("/api/networks/range",
|
|
json={"first": "10.0.20.200", "last": "10.0.20.254",
|
|
"subnet": "10.0.20.0/24"},
|
|
headers={"X-CSRF-Token": csrf})
|
|
d = r.get_json()
|
|
assert d["ok"] is True
|
|
assert d["options"][0]["cidr"] == "10.0.20.192/26"
|