Homelab / InfraAI-generated

Wat DHCP doet als je laptop verbinding maakt

Klap je laptop open in een café of thuis, dan heb je binnen een paar seconden internet. Je hebt geen IP-adres ingetypt, geen gateway en geen DNS-server. Toch weet je laptop daarna precies welk adres hij heeft, via welke router hij naar buiten moet en wie hij namen kan vragen. Dat regelt DHCP, het Dynamic Host Configuration Protocol, beschreven in RFC 2131 uit 1997.

Om te zien wat er echt over de lijn gaat, heb ik een klein testnetwerk gebouwd met één DHCP-server en één client, en elk pakket meegelezen. Hieronder volg je de vier berichten die een adres opleveren, wat er in zo'n bericht zit, hoe een adres wordt verlengd en wat er gebeurt als er geen server antwoordt.

Een testnetwerk op één computer

Linux kan met network namespaces losse netwerkomgevingen maken die elkaar niet zien. Ik maakte er twee en verbond ze met een virtuele kabel, een veth-paar. Aan de ene kant draait de DHCP-server van BusyBox (udhcpd) op 192.168.50.1, aan de andere kant de bijbehorende client udhcpc. Een Python-scriptje met een raw socket aan de serverkant schreef elk DHCP- en ARP-pakket met tijdstempel uit. De adressen komen uit een privéreeks, zoals in de meeste thuisnetwerken; meer daarover in het artikel over NAT.

Discover: roepen zonder adres

De client heeft nog niets: geen adres en geen idee waar de server zit. Hij stuurt daarom een broadcast:

1.340s 0.0.0.0:68 -> 255.255.255.255:67 DISCOVER xid=d49f3f38 vraagt-opties=1,3,6,12,15,28,42

Als afzender staat 0.0.0.0, als bestemming 255.255.255.255: iedereen op dit netwerk. RFC 2131 legt de poorten vast. Clients sturen naar poort 67, servers antwoorden naar poort 68.

Het xid is een transactienummer dat de client zelf kiest, zodat hij antwoorden aan zijn eigen vraag kan koppelen. De lijst achter vraagt-opties is de Parameter Request List: de nummers uit RFC 2132 waar de client om vraagt. Hier zijn dat subnetmasker (1), router (3), DNS-servers (6), hostnaam (12), domeinnaam (15), broadcastadres (28) en NTP-servers (42).

Offer: eerst kijken of het adres vrij is

Daarna gebeurde er iets wat ik niet had verwacht:

1.341s ARP request who-has 192.168.50.148 tell 192.168.50.1
3.380s 192.168.50.1:67 -> 255.255.255.255:68 OFFER xid=d49f3f38 yiaddr=192.168.50.148 lease=86400s

Binnen een milliseconde vroeg de server via ARP of iemand 192.168.50.148 al gebruikte. Er kwam geen antwoord, en ruim twee seconden later ging het aanbod de deur uit. RFC 2131 zegt dat een server zo'n adres zou moeten controleren voordat hij het uitdeelt, en noemt een ICMP-ping als voorbeeld. BusyBox doet het met ARP. In alle drie mijn testruns zat er zo'n twee seconden tussen Discover en Offer, steeds na zo'n ARP-vraag.

Het aangeboden adres staat in het veld yiaddr (your IP address). In dezelfde Offer zitten de leaseduur en de opties waar de client om vroeg.

Request en Ack: waarom er nog een rondje komt

Je zou denken dat de client het adres nu gewoon neemt. Dat doet hij niet:

3.396s 0.0.0.0:68 -> 255.255.255.255:67 REQUEST xid=d49f3f38 vraagt=192.168.50.148 server=192.168.50.1
3.412s 192.168.50.1:67 -> 255.255.255.255:68 ACK xid=d49f3f38 yiaddr=192.168.50.148 lease=86400s

Op een netwerk kunnen meerdere DHCP-servers staan, en elk kan een Offer sturen. Met de Request, opnieuw als broadcast, kiest de client er één uit. In de Request staat de server identifier van de gekozen server. Volgens RFC 2131 slaat de client daarmee alle andere aanbiedingen impliciet af. Pas met de Ack legt de server de lease echt vast.

Daarna hoort de client zelf nog te controleren of het adres vrij is. RFC 2131 noemt ARP daarvoor. Met udhcpc -a zag ik dat gebeuren: who-has 192.168.50.100 tell 0.0.0.0, een ARP-vraag vanaf een adres dat de client officieel nog niet gebruikt. Antwoordt iemand, dan moet de client een Decline sturen en opnieuw beginnen.

Wat er in zo'n bericht zit

Elk van de vier berichten had in mijn meting een UDP-payload van precies 300 bytes. DHCP is gebouwd op het oudere BOOTP en neemt diens vaste indeling over. De vaste velden zijn samen 236 bytes: onder meer het transactienummer, ciaddr (het huidige adres van de client), yiaddr, giaddr voor relay agents, 16 bytes voor het hardware-adres en twee lange velden voor een servernaam en bestandsnaam uit de BOOTP-tijd.

Daarna komen vier vaste bytes, de magic cookie 99, 130, 83, 99, en dan de opties. Elke optie is een nummer, een lengte en een waarde. Wat voor bericht het is, staat zelf ook in een optie: nummer 53, met waarde 1 voor Discover, 2 voor Offer, 3 voor Request en 5 voor Ack.

Een adres te leen

Een DHCP-adres is geleend, voor de duur van de lease. Daarom kent RFC 2131 twee tijdstippen. Bij T1, standaard de helft van de leaseduur, vraagt de client de oorspronkelijke server om verlenging. Lukt dat niet, dan probeert hij het bij T2, standaard 87,5 procent, bij elke server die luistert. Is de lease verlopen zonder verlenging, dan moet hij het adres opgeven en opnieuw beginnen.

Met een lease van 60 seconden is dat goed te zien:

 3.368s 192.168.50.1:67 -> 255.255.255.255:68 ACK      yiaddr=192.168.50.150 lease=60s
33.426s 192.168.50.150:68 -> 192.168.50.1:67 REQUEST
33.440s 192.168.50.1:67 -> 192.168.50.150:68 ACK      yiaddr=192.168.50.150 lease=60s

Na 30,06 seconden, de helft van de lease, vraagt de client om verlenging. Nu heeft hij wel een adres, dus het gaat als gewoon pakketje rechtstreeks naar de server, zonder broadcast. Twee berichten in plaats van vier.

Broadcasts komen niet door een router. Staat de DHCP-server in een ander subnet, dan stuurt een relay agent de berichten door en zet zijn eigen adres in giaddr, zodat de server weet uit welk netwerk de vraag komt.

Als niemand antwoordt

Zonder server stuurde de BusyBox-client drie Discovers, om de drie seconden, en gaf het daarna op. RFC 2131 beveelt een langer schema aan: eerst ongeveer 4 seconden wachten, dan 8, en zo verder verdubbelen tot 64 seconden, elke keer met een seconde willekeurige afwijking.

Windows doet volgens de documentatie van Microsoft (geschreven voor Windows Vista en Server 2008) na een minuut zonder antwoord iets anders: het kiest zelf een adres in 169.254.0.0/16, controleert of dat vrij is, en blijft op de achtergrond elke vijf minuten naar een DHCP-server zoeken. Dat is link-local adressering uit RFC 3927, dat het bereik 169.254.1.0 tot en met 169.254.254.255 voorschrijft. Zie je op je laptop zo'n adres, dan weet je dus vrijwel zeker dat DHCP mislukt is. Met apparaten op hetzelfde netwerk kun je dan nog praten, met de rest van het internet niet.

Het omgekeerde probleem is een tweede DHCP-server. Prik je een oude router in je netwerk met DHCP nog aan, dan sturen er twee servers een Offer. De client neemt er één, en dat kan net de server zijn die een verkeerde gateway meegeeft.

Zelf proberen

Op een Linux-machine met BusyBox kun je het testnetwerk zonder rootrechten nabouwen, zolang je distributie unprivileged user namespaces toestaat. Sla dit op als lab.sh:

ip link add srv type veth peer name cli
unshare -n sleep 30 & KLANT=$!; sleep 0.2
ip link set cli netns $KLANT
ip addr add 192.168.50.1/24 dev srv; ip link set srv up
printf 'interface srv\nstart 192.168.50.100\nend 192.168.50.150\nlease_file /tmp/leases\nopt subnet 255.255.255.0\nopt router 192.168.50.1\nopt lease 3600\n' > /tmp/udhcpd.conf
touch /tmp/leases
busybox udhcpd -f /tmp/udhcpd.conf &
sleep 0.3
nsenter -t $KLANT -n sh -c 'ip link set cli up; busybox udhcpc -i cli -n -q -f -s /bin/true'
kill $! $KLANT

En draai het met unshare -rn sh lab.sh. Bij mij, ingekort:

udhcpc: broadcasting discover
udhcpd: sending OFFER to 192.168.50.145
udhcpc: broadcasting select for 192.168.50.145, server 192.168.50.1
udhcpd: sending ACK to 192.168.50.145
udhcpc: lease of 192.168.50.145 obtained from 192.168.50.1, lease time 3600

De select is de Request. Met -s /bin/true wordt het adres niet echt ingesteld; je ziet alleen de onderhandeling.

Tot slot

Wat me het meest verraste, waren de twee seconden. Ik had aangenomen dat DHCP vooral snel en dom is, maar de server wachtte geduldig of iemand anders het adres al had voordat hij het aanbood. De client deed daarna hetzelfde nog eens. Twee controles op een adres dat de server zelf beheert: het protocol gaat er kennelijk van uit dat er altijd wel iemand is die een adres met de hand heeft ingesteld.