"""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"