Naar de inhoud

Blog ·

IPv4-adrespools plannen voor een VPS-product

Adressen zijn de enige grondstof die je niet bijkoopt. Hoe je pools indeelt, waarom de database de race beslecht, en wat er gebeurt als er een leegloopt.

Door Provibr

Al het andere in een VPS-product is elastisch. Meer CPU nodig, koop een node. Meer schijf nodig, koop schijf. Meer adressen nodig en je staat op een wachtlijst, betaalt een tussenhandelaar, of vult een motivatieformulier in voor een regionaal register.

IPv4 is de enige grondstof die zich als een fysiek goed gedraagt, en hij krijgt meestal de minste ontwerpaandacht, want in het begin heb je een /24 en voelen 254 adressen als ruim voldoende. Dat gevoel verdwijnt precies op het moment dat het duur is om de indeling over te doen.

Begin bij wat een blok je werkelijk geeft

Het bruikbare aantal is nooit de omvang van het blok. Twee adressen gaan naar het netwerk en de broadcast, en nog één naar de gateway. Bij kleine formaten is dat geen afrondingsverschil maar het grootste deel van het blok:

BlokAdressenNetwerk + broadcastGatewayVerkoopbaarOverhead
/24256212531%
/273221299%
/2816211319%
/29821538%
/30421175%

Een /29 klinkt als acht adressen, precies tot je er vijf van verkoopt. Koop je adresruimte in kleine blokken, begroot de overhead dan als een aparte post; bij een /29 gooi je drie achtste weg van wat je betaald hebt.

Twee grenzen zijn het waard om vast te leggen in wat je ook bouwt. Wij accepteren een /8 als grootste, want bij nog grotere blokken betekent een gebruiksbalk niets meer, en een /30 als kleinste, want /31 en /32 hebben onder normale hostregels nul bruikbare adressen en ze accepteren levert alleen een pool op die nooit iets kan uitgeven. Ze expliciet weigeren is beter dan ze accepteren en met een lege pool zitten.

Nog een subtiliteit: het gatewayadres mag binnen je uitgifterange liggen en wordt dan simpelweg overgeslagen. Dat is bewust, want het is de indeling die voor een klant het makkelijkst te doorzien is, maar het betekent wel dat de capaciteit van een pool een berekening is en nooit een opgeslagen getal. Alles wat een teller van vrije adressen bijhoudt is het oneens met de werkelijkheid op de dag dat iemand een gateway verplaatst.

Deel pools in naar wat je verkoopt, niet naar wat je kocht

De eenheid die een klant ziet is een plan. De eenheid die je van een tussenhandelaar kocht is een blok. Die twee hebben bijna nooit dezelfde vorm, en ze aan elkaar knopen gaat mis zodra je een tweede blok koopt.

De indeling die standhoudt: een plan wijst naar een pool, en een pool is óf één prefix óf een benoemde groep prefixen. Capaciteit toevoegen aan een product is dan een prefix toevoegen aan een groep, en dat is één rij. De toewijzer loopt de groep op volgorde af en pakt het eerste adres dat hij kan krijgen, dus een groep is meteen ook een leegloopvolgorde: zet het blok dat je als eerste opgemaakt wilt hebben vooraan.

Subblokken, en de fout die stil dubbel uitgeeft

Klanten gebruiken een /24 niet als één plat netwerk. Ze knippen hem op: een stuk achter de ene router, een stuk op een ander VLAN, een stuk met een eigen gateway. Een adresplan heeft dus kindblokken nodig.

Hier zit het interessante faalbeeld, en het is de moeite waard om het precies te zeggen, want het faalt stil.

Het mechanisme dat voorkomt dat twee gelijktijdige bestellingen hetzelfde adres krijgen, is een partiële unieke index op (prefix, adres), beperkt tot rijen die niet zijn vrijgegeven. Die index werkt per prefix. Als twee prefixrijen allebei hetzelfde adres kunnen uitgeven, dan zijn dat wat de index betreft twee verschillende adressen, en geeft het platform hetzelfde IP aan twee machines zonder dat er ook maar iets misgaat in de software.

De index kan dat niet repareren. De indeling moet dat doen. Dus is de adresruimte een strikte partitie, en de regels die hem dat maken zijn:

  1. Een kind ligt volledig binnen zijn ouder en is strikt kleiner.
  2. Kinderen van dezelfde ouder mogen niet overlappen.
  3. De ouder geeft niets uit dat binnen een kindblok valt. Het hele blok wordt uit het venster van de ouder gesneden, en niet alleen de uitgifterange van het kind zelf, want dan zou het antwoord afhangen van een range die een beheerder volgende week verzet.
  4. Nesting gaat één niveau diep.

Met die vier heeft "bij welke prefix hoort 192.0.2.70" altijd precies één antwoord, en volstaat de bestaande index.

Wat een kind erft is een aparte reeks beslissingen, en de antwoorden zijn niet uniform. Gateway: nooit (een kind zonder eigen gateway heeft geen reden om te bestaan). DNS: ja, als zijn eigen veld leeg is, want resolvers horen bij het netwerk en niet bij het blok. Uitgifterange: alleen waar de range van de ouder het kind werkelijk raakt, want de range van de ouder is uitgedrukt in de adressen van de ouder en overlapt het kind meestal helemaal niet; hem blind overnemen levert een leeg venster op.

Laat de database scheidsrechter zijn

De verleiding, als er twee bestellingen tegelijk binnenkomen, is om iets te vergrendelen of een teller bij te houden. Allebei zijn ze slechter dan het saaie antwoord.

Beide bestellingen berekenen hetzelfde "laagste vrije adres" en proberen allebei in te voegen. Eén insert wint, de andere loopt tegen de uniciteitsschending aan, en de verliezer gaat door naar de volgende kandidaat. De database is de enige partij die elke schrijver ziet, ook die in een ander proces, een andere worker of een ander browsertabblad.

Het gevolg doet ertoe zodra je de code schrijft: controleer niet eerst. "Is dit adres vrij" lezen en daarna invoegen opent precies het venster dat de constraint moet sluiten. De mislukte insert ís het antwoord op "was hij vrij", en het is het enige betrouwbare.

Eén uitvoeringsdetail waar mensen op stuklopen: veel ORM's verpakken driverfouten in hun eigen fouttype, dus de SQLSTATE die je zoekt staat niet op de uitzondering die je vangt maar op zijn oorzaak. Loop de hele keten af, en gebruik de naam van de constraint om te kiezen welke melding je toont. Eén tabel heeft vaak vier verschillende uniciteitsregels, en ze allemaal hetzelfde laten zeggen stuurt de klant naar het verkeerde veld.

Wat er gebeurt als een pool leegloopt

Achter "geen adres beschikbaar" gaan drie verschillende fouten schuil, en ze op één hoop gooien levert elke keer een supportticket op:

SituatieWat het betekentWat de klant hoort te lezen
Het plan wijst naar geen enkele poolConfiguratie, geen capaciteitDit plan heeft geen adrespool
Een prefix heeft onbruikbare gegevensEén kapotte rij, de rest in ordeDeze prefixen konden niet gelezen worden
Elke kandidaat is bezetEchte uitputtingDeze pool is vol, en hier staat welke

De middelste is de extra code waard. Eén prefix met onleesbare gegevens hoort de andere prefixen in zijn groep niet te blokkeren; noteer hem, probeer de rest, en meld hem alleen als er helemaal niets werkte. Anders legt één kapotte rij het bestellen van een hele productlijn plat.

En de uitputtingsmelding hoort de prefix en zijn CIDR te noemen. "Geen IP beschikbaar" stuurt iemand naar de logs. "Pool vol: klantblok-2 (198.51.100.0/28)" stuurt hem naar adressen kopen.

Geef adressen terug, en let op wanneer

Vrijgeven is de helft die stilletjes capaciteit weglekt.

Twee regels bleken meer uit te maken dan verwacht. Ten eerste is vrijgeven niet de rij verwijderen: het zet een vrijgavetijdstip. De partiële unieke index kijkt alleen naar levende rijen, dus het adres is meteen weer te gebruiken, terwijl "wie had vorig jaar 192.0.2.10" beantwoordbaar blijft. Dat is de belangrijkste reden om überhaupt adresbeheer te doen, en het is de vraag die een misbruikmelding stelt.

Ten tweede gebeurt het vrijgeven wanneer de machine bevestigd weg is, en niet wanneer de verwijdering werd aangevraagd. Alleen het ding dat naast de hypervisor staat weet dat de VM werkelijk verdwenen is; vrijgeven op het uitgaande commando geeft het adres aan de volgende klant terwijl de oude machine nog draait en er nog op antwoordt. Het spiegelbeeld is een bestelling die halverwege mislukt: daar wordt de toewijzing weggegooid in plaats van vrijgegeven, want hij was nergens op geconfigureerd.

Er zit hier een bewuste asymmetrie die het benoemen waard is, want een laag hoger maken we de omgekeerde keuze. Adressen worden hergebruikt, laagste vrije eerst. Machine-ID's worden nooit hergebruikt. Een laat statusbericht over een verwijderde VM dat op een nieuwe rij met hetzelfde ID landt, verpest de geschiedenis van twee machines; een adres draagt op die manier geen geschiedenis, en nooit hergebruiken zou een /24 na 254 machines voorgoed uitputten. Zelfde product, tegenovergestelde regels, verschillende redenen.

Reserveringen horen in dezelfde tabel als toewijzingen

Je zult adressen achter de hand moeten houden: een loadbalancer, een gateway op een blok dat je nog niet hebt uitgerold, de vijf adressen die iemand een klant voor volgend kwartaal beloofde.

Zet ze in dezelfde tabel als de echte toewijzingen. Elke lezer die vraagt "wat is bezet", de toewijzer, de gebruiksbalk, de suggestielijst in een adresveld, telt ze dan automatisch mee, en er is geen tweede administratie die uit de pas kan lopen. Een aparte reserveringstabel is een tweede bron van waarheid die het binnen een maand oneens is met de eerste.

Met één uitzondering die makkelijk fout gaat: een gereserveerd adres mag nooit in de firewall-allowlist van een machine belanden. Die lijst wordt opgebouwd uit de toewijzingen die bij één server horen, en een reservering hoort bij geen enkele server. Ga daar de fout in en de VM van de ene klant begint verkeer te accepteren dat aan de loadbalancer van iemand anders gericht is.

En IPv6

We herkennen IPv6-invoer en weigeren hem beleefd, in plaats van hem te accepteren en iets plausibels uit te rekenen: een half werkend IPv6-pad is gevaarlijker dan geen, want de getallen die het produceert zien er goed uit. De goedkope beslissing die zichzelf terugverdient is adressen, gateways en DNS-servers als tekst opslaan in plaats van als een 32-bits geheel getal, zodat IPv6 later toevoegen code is en geen migratie van elke rij die je bezit.

De korte versie

  • Begroot de overhead. Een /29 is vijf verkoopbare adressen, geen acht.
  • Laat plannen naar pools wijzen en pools naar prefixen. Meer ruimte kopen hoort één rij te zijn.
  • Sta je subblokken toe, maak de adresruimte dan een strikte partitie, anders werkt de uniciteitsregel die je beschermt stilletjes per prefix en beschermt hij niets.
  • Laat de unieke constraint de race beslissen. Controleer nooit eerst.
  • Geef vrij op bevestiging, niet op aanvraag, en bewaar de geschiedenis.
  • Noem de pool in de uitputtingsmelding. Het is het verschil tussen een ticket en een inkooporder.

Dit kwam voort uit het bouwen van adresbeheer in Provibr, waar adressen worden uitgegeven zodra een bestelling wordt geplaatst en teruggegeven zodra de agent bevestigt dat de machine werkelijk weg is.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.