diff --git a/CHANGELOG.md b/CHANGELOG.md index ec20255..8192307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# v0.5.41-beta β€” Netwerk toevoegen liep vast tijdens het typen + +Een netwerk toevoegen gaf een JSON-parsefout in de browser. + +Oorzaak: de live beoordeling die bij elke toetsaanslag afgaat, bouwde een lijst +van alle bruikbare adressen in het bereik. Tijdens het intypen van +`192.168.1.0/24` is de tussenstand `192.168.1.0/2` een volkomen geldig +netwerk β€” met 1.073.741.824 adressen. Het verzoek liep daarop vast en de browser +kreeg geen JSON meer terug. + +- `review()` rekent eerste adres, laatste adres en aantal nu uit in plaats van + ze op te sommen. Antwoord binnen 0,05 seconde bij elk prefix. +- `suggest_range()` had hetzelfde probleem: die somde alle /28-blokken op, wat + bij een `/8` ruim een miljoen blokken zijn. Rekent nu van achteren naar voren. +- Waarschuwing bij een bereik van meer dan 4096 adressen, want dat is vrijwel + altijd een typefout in het prefix. +- Ook `.env` van de deploy-map werd bij elke deploy gewist door `rsync --delete`, + omdat het bestand in `.gitignore` staat en dus niet in de checkout zit. Daardoor + raakte je `BIND` telkens kwijt. `--exclude='.env'` toegevoegd. +- Nieuw `.env.example` met alle instellingen bij elkaar en uitleg per sleutel. + +--- + # v0.5.40-beta β€” Netwerken instellen zonder uitzoekwerk ## πŸ” Automatische detectie diff --git a/VERSION b/VERSION index 4ca6d5d..bf882bd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.40-beta +0.5.41-beta diff --git a/server-up/core/networks.py b/server-up/core/networks.py index fb3c058..23e4bd6 100644 --- a/server-up/core/networks.py +++ b/server-up/core/networks.py @@ -144,14 +144,25 @@ def suggest_range(subnet: str, gateway: str = "") -> str: # het bereik en heeft een aparte range geen toegevoegde waarde. if net.version != 4 or net.prefixlen >= 28: return "" - blokken = list(net.subnets(new_prefix=28)) - for blok in reversed(blokken): - if gateway: - try: - if ipaddress.ip_address(gateway) in blok: - continue # nooit een blok met de gateway erin - except ValueError: - pass + + # Van achteren naar voren rekenen in plaats van alle /28-blokken opsommen: + # bij een /8 zouden dat er ruim een miljoen zijn. + gw = None + if gateway: + try: + gw = ipaddress.ip_address(gateway) + except ValueError: + gw = None + + laatste_start = int(net.broadcast_address) - 15 + for stap in range(16): # hoogstens 16 blokken terugzoeken + start = laatste_start - stap * 16 + if start < int(net.network_address): + break + blok = ipaddress.ip_network( + f"{ipaddress.ip_address(start)}/28", strict=False) + if gw is not None and gw in blok: + continue # nooit een blok met de gateway erin return str(blok) return "" @@ -294,19 +305,18 @@ def review(net: dict) -> dict: "geen internet. Meestal is dat het adres van je router.") bereik = ipaddress.ip_network(rng, strict=False) if rng else subnet - bruikbaar = [h for h in bereik.hosts()] - if gateway: + eerste, laatste, aantal = _bruikbaar_bereik(bereik, gateway) + + if gateway and rng: try: - gw = ipaddress.ip_address(gateway) # Alleen relevant bij een expliciete range: dat de gateway ergens in # het subnet zit is normaal. Docker houdt het gateway-adres apart, # maar een range eromheen kiezen is duidelijker en scheelt verwarring. - if rng and gw in bereik: + if ipaddress.ip_address(gateway) in bereik: waarschuwingen.append( f"De gateway ({gateway}) valt binnen je IP-range. Docker houdt " "dat adres vrij, maar het is overzichtelijker om een range te " "kiezen die de gateway niet bevat.") - bruikbaar = [h for h in bruikbaar if h != gw] except ValueError: pass @@ -316,22 +326,58 @@ def review(net: dict) -> dict: "adressen die je DHCP-server aan telefoons en laptops uitdeelt. Dat " "geeft vroeg of laat twee apparaten op hetzelfde adres.") - if len(bruikbaar) == 0: + if aantal <= 0: fouten.append("Er blijft geen enkel bruikbaar adres over in dit bereik.") - elif len(bruikbaar) < 4: + elif aantal < 4: waarschuwingen.append( - f"Er passen maar {len(bruikbaar)} adressen in dit bereik; je kunt dus " - f"{len(bruikbaar)} stacks een eigen IP geven.") + f"Er passen maar {aantal} adressen in dit bereik; je kunt dus " + f"{aantal} stacks een eigen IP geven.") + elif aantal > 4096: + # Meestal een typefout in het prefix (/8 in plaats van /24). + waarschuwingen.append( + f"Dit bereik beslaat {aantal:,} adressen. Klopt het prefix? Een thuis- " + "of kantoornetwerk is doorgaans /24, en een handvol adressen voor je " + "containers is genoeg.".replace(",", ".")) - if bruikbaar: - info = {"first": str(bruikbaar[0]), "last": str(bruikbaar[-1]), - "count": len(bruikbaar), "subnet": str(subnet), - "range": str(bereik)} + if aantal > 0: + info = {"first": str(eerste), "last": str(laatste), "count": aantal, + "subnet": str(subnet), "range": str(bereik)} return {"ok": not fouten, "errors": fouten, "warnings": waarschuwingen, "info": info} +def _bruikbaar_bereik(net, gateway: str = ""): + """(eerste, laatste, aantal) bruikbare adressen β€” puur rekenkundig. + + Bewust zΓ³nder net.hosts() te doorlopen: tijdens het typen van een subnet is + de tussenstand '192.168.1.0/2' een geldig netwerk met ruim een miljard + adressen. Een lijst daarvan opbouwen liet het verzoek vastlopen, waarna de + browser geen JSON meer terugkreeg. + """ + if net.prefixlen >= 31: + # /31 en /32 kennen geen netwerk- en broadcastadres. + eerste, laatste = net.network_address, net.broadcast_address + aantal = net.num_addresses + else: + eerste = net.network_address + 1 + laatste = net.broadcast_address - 1 + aantal = net.num_addresses - 2 + + if gateway: + try: + gw = ipaddress.ip_address(gateway) + except ValueError: + return eerste, laatste, aantal + if eerste <= gw <= laatste: + aantal -= 1 + if gw == eerste and aantal > 0: + eerste = eerste + 1 + elif gw == laatste and aantal > 0: + laatste = laatste - 1 + return eerste, laatste, max(aantal, 0) + + def create(net: dict) -> tuple[bool, str]: """Maak het netwerk aan bij de Docker-daemon (idempotent).""" probleem = validate(net) diff --git a/tests/test_network_detect.py b/tests/test_network_detect.py index 85f4589..4cc399e 100644 --- a/tests/test_network_detect.py +++ b/tests/test_network_detect.py @@ -115,6 +115,77 @@ def test_review_waarschuwt_bij_erg_kleine_range(): 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