"""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 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"] == []