Hoe ARP een MAC-adres vindt op je eigen netwerk
Als je laptop een pakketje naar je printer of naar je router stuurt, staat daar een IP-adres op. Maar een netwerkkaart verstuurt geen IP-adressen. Op het lokale netwerk gaat elk frame naar een hardwareadres, het MAC-adres van 48 bits. Iets moet dus vertalen van 192.168.1.20 naar iets als 02:00:00:00:00:02. Dat is ARP, het Address Resolution Protocol, vastgelegd in RFC 826 uit november 1982.
Ik heb op één Linux-machine een klein testnetwerk gebouwd en elk ARP-pakket meegelezen. Hieronder zie je de vraag die iedereen hoort, het antwoord dat alleen de vrager krijgt, hoe lang een antwoord geldig blijft, hoe een apparaat controleert of een adres vrij is en waarom ARP iedereen op zijn woord gelooft.
Het testnetwerk
Met network namespaces maakte ik drie losse "computers", A, B en C, die via een virtuele switch (een Linux-bridge) aan elkaar hangen. A kreeg 192.168.60.1 en MAC-adres 02:00:00:00:00:01, B kreeg .2 en ...:02, C eindigt op ...:03. Een Python-scriptje met een raw socket schreef op A en B elk frame uit. Dezelfde opzet gebruikte ik eerder voor het artikel over DHCP.
De vraag aan iedereen
A wil B pingen, maar A's ARP-cache is leeg. Het pingpakket blijft dus even liggen en eerst gaat er dit de deur uit:
0.525s uit 42B 02:..:01 -> ff:ff:ff:ff:ff:ff request wie heeft 192.168.60.2? zeg het 192.168.60.1 (02:..:01)
0.525s in 42B 02:..:02 -> 02:..:01 reply 192.168.60.2 is 02:..:02
0.526s uit 98B 02:..:01 -> 02:..:02 ICMP echo request
De vraag gaat naar ff:ff:ff:ff:ff:ff, het broadcastadres: elk apparaat op dit netwerk krijgt hem. Het antwoord gaat alleen naar A. Binnen een milliseconde vertrok daarna de ping zelf.
Het ARP-bericht is klein. Volgens RFC 826 bevat het het type hardware (1 voor Ethernet), het type protocol (0x0800, IPv4), twee lengtevelden (6 en 4), een opcode (1 voor vraag, 2 voor antwoord) en dan vier adressen: MAC en IP van de afzender, MAC en IP van het doel. Opgeteld 28 bytes. Met de Ethernet-header van 14 bytes erbij kom je op de 42 bytes die ik mat. In de vraag staat het doel-MAC op nullen, want dat is juist wat A niet weet.
ARP werkt alleen binnen je eigen subnet. Wil je laptop iets op internet bereiken, dan vraagt hij via ARP het MAC-adres van de router op en geeft het pakket daar af. Vanaf dat punt neemt routering het over, zoals je in het artikel over traceroute kunt zien.
Wie onthoudt wat
RFC 826 bevat een kort algoritme voor de ontvanger, met een slimmigheid erin. Ben jij het doel van de vraag, dan zet je de afzender meteen in je eigen tabel, nog voordat je kijkt of het een vraag of een antwoord is. De gedachte uit 1982: als A iets van B wil, wil B waarschijnlijk straks ook iets terug.
Dat zag ik terug. Na die ene vraag had B al een vermelding voor A, zonder zelf iets te hebben gevraagd. C, die de broadcast ook ontving maar niet het doel was, had niets opgeslagen. Volgens het algoritme werkt een machine een bestaande vermelding wel bij als hij een ARP-pakket ziet van een adres dat hij al kent, ook als het niet aan hem gericht is.
Hoe lang een antwoord geldig blijft
Het antwoord komt in de ARP-cache, die je op Linux met ip neigh ziet. Direct na de ping:
192.168.60.2 dev eth-A lladdr 02:00:00:00:00:02 REACHABLE
Volgens de Linux-documentatie (man 7 arp) blijft een vermelding minstens een willekeurige tijd tussen de helft en anderhalf keer base_reachable_time geldig. Die staat standaard op 30 seconden, dus ergens tussen 15 en 45 seconden. Bij mij sprong de vermelding na ruim 40 seconden naar STALE.
Een STALE-vermelding wordt niet weggegooid en ook niet meteen opnieuw opgevraagd. Toen ik na 70 seconden opnieuw pingde, ging de ping direct de deur uit met het oude MAC-adres. De vermelding ging naar DELAY, en vijf seconden later stuurde A een controlevraag, dit keer niet als broadcast maar rechtstreeks aan B:
71.749s uit 98B 02:..:01 -> 02:..:02 ICMP echo request
76.795s uit 42B 02:..:01 -> 02:..:02 request wie heeft 192.168.60.2?
76.795s in 42B 02:..:02 -> 02:..:01 reply 192.168.60.2 is 02:..:02
Die vijf seconden zijn delay_first_probe_time. Ze geven hogere protocollen de kans om eerst zelf te bevestigen dat B er nog is, bijvoorbeeld met een TCP-ACK. Dan is de controlevraag niet meer nodig. De namen REACHABLE, STALE, DELAY en PROBE komen uit RFC 4861, de IPv6-variant van dit mechanisme. Dat document merkt op dat IPv4 nooit een algemeen afgesproken manier had om te controleren of een buur nog bereikbaar is. Linux gebruikt de IPv6-aanpak gewoon ook voor ARP.
Hetzelfde gebeurde aan B's kant: de vermelding die B gratis had geleerd uit A's vraag, controleerde B ruim vijf seconden later alsnog met een eigen gerichte vraag aan A.
Als niemand antwoordt
Voor een adres waar niets achter zit, stuurde A drie broadcasts, telkens een seconde uit elkaar:
0.577s uit 42B 02:..:01 -> ff:ff:ff:ff:ff:ff request wie heeft 192.168.60.9?
1.605s uit 42B 02:..:01 -> ff:ff:ff:ff:ff:ff request wie heeft 192.168.60.9?
2.629s uit 42B 02:..:01 -> ff:ff:ff:ff:ff:ff request wie heeft 192.168.60.9?
Dat zijn mcast_solicit (3) en retrans_time_ms (1000). Ruim drie seconden na de start meldde ping Destination Host Unreachable en stond de vermelding op FAILED. Die melding komt dus van je eigen computer, niet van een router onderweg.
Is dit adres nog vrij?
Voordat een apparaat een IPv4-adres gaat gebruiken, hoort het te controleren of niemand anders het al heeft. RFC 5227 uit 2008 beschrijft hoe: een ARP Probe, een gewone ARP-vraag waarin het afzender-IP op 0.0.0.0 staat. Dat moet, zegt de RFC, zodat andere apparaten geen verkeerde vermelding in hun cache zetten als het adres al bezet blijkt.
Vanaf C, nog zonder IP-adres, vroeg ik met busybox arping -D naar .2, het adres van B:
7.655s uit 42B 02:..:03 -> ff:ff:ff:ff:ff:ff request wie heeft 192.168.60.2? zeg het 0.0.0.0 (02:..:03)
7.655s in 42B 02:..:02 -> 02:..:03 reply 192.168.60.2 is 02:..:02
B antwoordt: bezet. Voor het vrije adres .3 kwam er na drie probes geen antwoord. BusyBox stuurde ze precies een seconde uit elkaar. RFC 5227 vraagt om drie probes met een willekeurige tussenpoos van één tot twee seconden, daarna twee seconden wachten en dan twee ARP Announcements: vraag-pakketjes waarin afzender-IP en doel-IP allebei het nieuwe adres zijn. Daarmee verversen andere apparaten hun cache als dat adres vroeger bij iemand anders hoorde.
Iedereen mag antwoorden
ARP heeft geen enkele vorm van controle. Elk apparaat op het netwerk mag beweren dat het een bepaald IP-adres heeft. Om dat te zien, liet ik C twee ongevraagde ARP-antwoorden uitsturen met de bewering "192.168.60.2 is 02:00:00:00:00:03". A had .2 op dat moment netjes in zijn cache staan. Daarna:
192.168.60.2 dev eth-A lladdr 02:00:00:00:00:03 STALE
Het volgende pakket van A voor B gaat naar het MAC-adres van C. Dit is ARP-spoofing, en RFC 5227 zegt het in de beveiligingsparagraaf ronduit: een kwaadwillend apparaat kan op elke ARP-vraag antwoorden met zijn eigen adres en zo elk adres op het netwerk claimen.
Linux beperkt wel iets. Met de standaardinstelling arp_accept=0 maakt een ongevraagd ARP-pakket geen nieuwe vermelding aan. Toen C zijn eigen adres .3 aankondigde, verscheen C niet in A's cache. Een vermelding die al bestaat, wordt volgens de kerneldocumentatie wel bijgewerkt, en dat is precies wat er met .2 gebeurde. Iemand op hetzelfde netwerk kan zo verkeer naar zich toe trekken. Dat is een van de redenen waarom versleuteling met gecontroleerde certificaten ertoe doet, ook op je eigen wifi.
IPv6 heeft geen ARP. Daar doet Neighbor Discovery uit RFC 4861 hetzelfde werk, met een multicastadres dat is afgeleid van het gezochte adres in plaats van een broadcast aan iedereen.
Zelf proberen
Je eigen ARP-cache bekijk je met ip neigh op Linux of arp -a op macOS en Windows. Wil je het hele verloop zien zonder je echte netwerk te storen, sla dan dit op als arp-lab.sh:
ip link add buur type veth peer name eth0
unshare -n sleep 60 & B=$!; sleep 0.2
ip link set eth0 netns $B
ip addr add 10.0.0.1/24 dev buur; ip link set buur up
nsenter -t $B -n sh -c 'ip addr add 10.0.0.2/24 dev eth0; ip link set eth0 up'
sleep 0.5
echo "# cache voor:"; ip neigh show
ping -c 1 10.0.0.2 | grep time=
ping -c 1 -W 6 10.0.0.9 | grep loss
echo "# cache na:"; ip neigh show
nsenter -t $B -n busybox arping -D -c 2 -I eth0 10.0.0.1
kill $B
Draai het met unshare -rn sh arp-lab.sh. Daarvoor heb je BusyBox nodig en een distributie die unprivileged user namespaces toestaat. Bij mij:
# cache voor:
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.111 ms
1 packets transmitted, 0 received, 100% packet loss, time 0ms
# cache na:
10.0.0.9 dev buur FAILED
10.0.0.2 dev buur lladdr ae:09:94:d8:d0:b9 REACHABLE
ARPING 10.0.0.1 from 0.0.0.0 eth0
Unicast reply from 10.0.0.1 [6e:1e:46:61:c0:34] 0.028ms
De MAC-adressen van het virtuele kabeltje waren bij mij elke run anders. Met ip -ts monitor neigh in een tweede venster zie je de overgangen naar STALE, DELAY en PROBE live voorbijkomen.
Tot slot
Wat me opviel, is hoe ongelijk de voorzichtigheid is verdeeld. B kreeg A's adres gratis mee uit een vraag en controleerde het vijf seconden later toch nog even na. Twee ongevraagde antwoorden van C werden daarentegen zonder enige vraag geaccepteerd. ARP is gebouwd om fouten op te vangen, zoals verouderde adressen en dubbele configuraties. Tegen iemand die bewust liegt, doet het niets.