Ping is geen diagnose. Het is een tunnel. Het echo van ICMP (het verzoek dat een machine stuurt en het antwoord dat het terugkrijgt) draagt elke byte die je erin stopt, in beide richtingen, over een protocol dat de meeste firewalls doorlaten zonder te kijken en de meeste logging telt in plaats van leest. Een netwerk dat “alleen ping toestaat” heeft al een volledige, versleutelbare VPN naar buiten, en iedereen die dat netwerk kan bereiken kan er een besturen.
Dit is oud, en het is gedocumenteerd. Loki1 publiceerde de techniek in Phrack 49 in 1996. Ptunnel2 draagt al twintig jaar hele TCP-sessies binnen ping en zit één pakket weg op elke Linux-bak. Geen zeroday. Het protocol dat zich precies gedraagt zoals het is voorgeschreven.
Dat is de oorzaak: de standaard, geen fout in iemands product. Elke host op aarde is verplicht de data in een echo-verzoek te nemen en die onveranderd terug te geven. Willekeurige data erin, dezelfde data eruit: dat is het hele wat een tunnel nodig heeft, en het is voorgeschreven sinds 1981.
De oplossing is één smalle wijziging. Gooi ICMP-echo weg, verzoek en antwoord, op IPv4 en IPv6, aan de grens, en houd elk ander ICMP-bericht. De fouten — Time Exceeded, Packet Too Big, Destination Unreachable — zijn dragend; blokkeer die en je maakt path MTU discovery en traceroute kapot voor niets. Dit is dus niet “blokkeer ICMP”. Het is het ene bericht weggooien dat een risico is, en de berichten houden die de waarheid dragen.
Wat je uitgaande beleid eigenlijk toestond
Draai je een netwerk waar uitgaand verkeer wordt gefilterd (en zo niet, dan is dat een eigen gesprek), dan is dit de rekening voor één regel erin. Het beleid dat zegt “blokkeer alles uitgaand, sta ICMP toe want we moeten dingen kunnen pingen” is geen uitgaand beleid. Het is een volledige tunnel met het papierwerk onder diagnose gearchiveerd. Alles aan de binnenkant dat een echo-verzoek kan sturen en het antwoord kan lezen, kan data verplaatsen naar overal aan de buitenkant dat er een beantwoordt, op welk tempo de link ook aankan, en voorbij elke inhoudscontrole die je hebt gekocht. In de logs is het iemand die nakijkt of het internet werkt, en verder niets. Malware levert dit al jaren mee om precies die reden. Stil, standaard, en meestal al toegestaan.
Daarom is het een eerstekeuskanaal voor iedereen die al binnen is en dat niet zou moeten zijn. Het is de huur, niet de smash-and-grab. Wie een blijvende weg in en uit wil, mijdt de poort die een alarm afgaat. Ze zetten een tunnel op echo op en laten hem maanden draaien. Een host die veel pingt is een host waar niemand naar kijkt. Tweewegverkeer de omgeving in: commando erin, data eruit, over het ene protocol dat niemand van een ratelimiet voorziet, alarmeert of leest. En het is versleuteld, zoals elk echt gereedschap het versleutelt, dus op de draad is de payload de willekeurig ogende bytes die een ping toch al draagt en heeft inhoudsinspectie niets te lezen. Grootte en timing kunnen het nog steeds verraden aan wie echt kijkt. In het pakket kijken kan dat niet.
En de machine die het bestuurt hoeft van niemand te zijn die daar werkt. Het enige dat nodig is, is iets dat je netwerk kan bereiken en een ping op het internet kan zetten, en je netwerk bereiken is makkelijker dan iemand graag toegeeft. De wifi draagt voorbij de muren, de gang in, de parkeerplaats, de flat erboven. De ethernetpoort in de muur van de vergaderruimte, of de receptie, of het lege bureau bij het raam, staat heel vaak aan en praat met alles wat je inprikt, zonder een 802.1X die vraagt wie je bent. Een signaal in bereik, of een stopcontact dat niemand heeft vergrendeld. Dat is het toegangsgeld. Geen badge, geen account, geen uitnodiging.
Stel je de bezoeker voor die je wel hebt uitgenodigd. Ze zijn in de vergadering, aangenaam, maken aantekeningen, dragen bij. Hun laptop niet. Op het moment dat die je netwerk bereikte, over de lucht of via de poort onder de tafel, zat hij binnen de muur, en kan dat netwerk een echo-verzoek uitstoten, dan heeft hij een weg naar buiten. Er is niets op je omgeving neergezet. Geen account, geen recht op iets wat je bezit, niets voor je endpointagents om te vangen, want je agents staan niet op hun machine. De persoon zit aan de overkant van de tafel. Het verkeer verlaat je voordeur, versleuteld, niet te onderscheiden van een laptop die meer pingt dan zou moeten.
De twee dingen waar je naar grijpt helpen niet. NAT is een vertaaltabel, geen filter: een echo-verzoek van een host binnen opent een koppeling op de ICMP-id, die NAT precies zo behandelt als een poort, en het antwoord komt er net als elke andere stroom door terug. Ik draaide een client achter mijn eigen NAT thuis en die merkte nooit dat de NAT er was. Een apart gast-VLAN is niet beter. Scheiding houdt de bezoeker van je servers, maar het doet niets om ze van het internet te houden, en het internet, bereikt met een ping, is de hele vereiste. Eén controle raakt dit: laat dat netwerk echo naar buiten?
Je inspecteert dit niet weg, en je scheidt het niet weg. Je sluit de deur. Alles hierna is waarom de standaard hem open laat, drie manieren om de tunnel te bouwen zodat de bewering op meer dan mijn woord rust, en de ene wijziging die hem sluit.
Het deel van ICMP dat je niets nuttigs verschuldigd is
Begin met wat de standaard echt zegt, want het hele argument rust op één zin en mensen wuiven die weg zonder te lezen.
Een echo-verzoek draagt een dataveld. In IPv4 zegt RFC 7923 onomwonden dat “the data received in the echo message must be returned in the echo reply message”. IPv6 heeft de bewoording aangescherpt in plaats van versoepeld: RFC 44434 definieert het veld als “zero or more octets of arbitrary data” en eist dan dat het “MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message”.
Lees dat als operator en het is een keepalive. Lees het als iemand die data verplaatst en het is een cadeau. De standaard verplicht elke bereikbare host een blok bytes dat jij kiest aan te nemen en ze onveranderd, op verzoek, direct terug te sturen. Willekeurige lengte. Willekeurige inhoud. Geen handshake, geen poort, geen applicatie aan de overkant die ergens mee akkoord hoeft te gaan. De kernel doet het, voordat een proces in userland iets ziet.
En het is beide families. Naar IPv6 overstappen ruimt dit niet op. Het opent het wijder. RFC 792 zei dat de data “must be returned”; RFC 4443 zegt dat het “MUST be returned entirely and unmodified”, en dat is dezelfde deur met een steviger slot dat hem openhoudt. Het kanaal bestaat dan ook zowel op ICMP-echo als op ICMPv6-echo, en een regel die het op de ene familie sluit en op de andere niet, heeft niets gesloten. De tunnel verhuist gewoon naar de familie die je open liet. Wat je hier ook aan doet, doe het op ip en ip6 samen.
Niets anders in het protocol doet dit. Een Time Exceeded draagt de header van het pakket dat stierf en niet meer. Een Destination Unreachable is een verslag over iets dat al is gebeurd. Die berichten vertellen je feiten over het netwerk. Echo draagt wat je erin stopt, in beide richtingen, en noemt het een diagnose.
Fouten zijn dragend. Echo niet.
Dit is het onderscheid dat de “blokkeer gewoon ICMP”-menigte nooit maakt, en het is het hele punt, dus ik maak het één keer en goed.
ICMP-fouten blokkeren maakt het netwerk stil kapot. Filter Packet Too Big en je doodt path MTU discovery: de handshake rondt af, kleine overdrachten werken, en alles dat een pakket van volle grootte draagt hangt voor eeuwig met niets in de logs. Op IPv6 is dat niet eens een kwestie van smaak. Routers fragmenteren niet, dus RFC 48905 noemt Packet Too Big onder de berichten die een firewall “must not drop” en waarschuwt dat zonder dat “parts of the Internet will become inaccessible”. Filter Time Exceeded en je maakt traceroute kapot, dat RFC 18126 noemt als de reden dat het bericht überhaupt verplicht is. Die zijn niet optioneel. Ze zijn de terugkoppeling die het netwerk zelfcorrigerend maakt, en ik heb de kosten van ze kwijtraken vorige keer uitgebreid behandeld.
Gooi nu echo weg en ga zoeken naar wat er brak. ping over de grens werkt niet meer. Dat is de lijst. De hele lijst.
Path MTU discovery kan het niet schelen, want dat loopt op Packet Too Big, wat een fout is. Traceroute kan het niet schelen, want traceroute -T en -U lopen TCP en UDP af en lezen de fouten die terugkomen. Geen ervan stuurt een echo. Neighbour discovery op IPv6 kan het niet schelen, want dat zijn de types 133 tot 137, en die houd je of het segment sterft. De omgekeerde-TTL-truc uit het vorige bericht werkt op elk antwoord, en een TCP-handshake geeft je er een. Alles wat de diagnoses uit het vorige bericht liet werken blijft werken, want geen enkele meting daarin stuurde een echo-verzoek.
De twee helften van het protocol konden dus minder op elkaar lijken. De ene helft is het netwerk dat je de waarheid over zichzelf vertelt, en die maak je op eigen risico kapot. De andere helft is een voorgeschreven dienst die je payload teruggeeft en toevallig een diagnose heet, en het sterkste dat iemand voor openhouden kan zeggen is dat ping handig is. Het is handig. Het is ook het enige deel van ICMP dat een aanvaller kan besturen, en ik kan je precies laten zien waarmee ze het besturen.
ping, en het enige deel dat een aanvaller kan besturen. Gooi dat weg, houd de rest, op beide families.Het bewijs: een VPN gemaakt van ping
Het gereedschap is Hans7, geschreven door Friedrich Schöller. De eigen beschrijving is één regel: het “makes it possible to tunnel IPv4 through ICMP echo packets, so you could call it a ping tunnel.” Het brengt aan elk einde een tun-interface op, geeft ze adressen, en verplaatst elk pakket ertussen binnen ICMP-echo. Voor het netwerk in het midden is het iemand die een server pingt en de server die antwoordt. Voor mij is het een route.
Ik draaide het over mijn eigen lijn, een server op een publiek adres en een client achter mijn NAT thuis, en duwde er echt verkeer doorheen. Geen synthetische pings met een vlag gezet. Een SSH-aanmelding en een bestandsdownload, rijdend binnen echo.
Het zit één pakket weg waar ik de server draaide, en het bouwt overal elders uit de broncode van Schöller. De server moet Linux zijn en heeft root nodig, want een tun-apparaat openen en een ruwe ICMP-socket doen dat allebei. De syntaxis is met opzet klein:
# on the server (public IP), pick the tunnel network and a password
hans -s 10.8.0.0 -p '<password>'
# server takes 10.8.0.1; clients are handed 10.8.0.2 and up
# on the client, point it at the server's public address
hans -c <server-public-ip> -p '<password>'
Zodra beide einden op zijn, is er aan elke kant een nieuwe interface met een adres op het tunnelnetwerk, en hij gedraagt zich als elke andere punt-tot-puntlink:
Niets aan die sessie weet dat het binnen ping zit. SSH opent een TCP-verbinding naar 10.8.0.1, de kernel routeert die naar buiten via tun0, Hans verpakt elk pakket als de payload van een echo-verzoek, en de overkant pakt het uit en voert het aan zijn eigen tun0. Het antwoord komt terug als de payload van een echo-antwoord. Voor zover SSH weet praat het over een gewone link. Voor zover de firewall weet, opende niemand iets. Een host wordt gepingd.
Hoe het eruitziet op de draad
Dit is het deel dat het argument beëindigt, dus kijk naar de firewall-blik in plaats van de mijne.
Ga op de buiteninterface zitten met tcpdump en vang alleen ICMP terwijl de SSH-sessie loopt. Geen TCP naar poort 22 steekt de grens over. Wat oversteekt is echo-verzoek en echo-antwoord, heen en weer, elk dikker dan een echte ping omdat het een plak van een TCP-segment in zijn payload draagt:
# on the boundary, watch only ICMP echo while traffic runs over the tunnel
tcpdump -ni <wan-iface> 'icmp[icmptype] = icmp-echo or icmp[icmptype] = icmp-echoreply'
Lees de twee dingen die ertoe doen in die opname. Eerst de payloadlengtes: een normale ping stuurt 56 bytes en elke regel is even groot, terwijl deze variëren en groot uitvallen, omdat de grootte van het ding dat je verplaatst lekt in de grootte van de ping. Ten tweede het tempo: een diagnose-ping is er een per seconde, en dit is een stortvloed, want het verplaatst een bestand. Geen van beide is verborgen. Beide zitten in het volle zicht op een protocol waar niemand naar kijkt.
En dat is het hele punt. Niet dat dit slim is, of moeilijk te zien zodra je kijkt. Bijna niemand kijkt, want de bak is ingesteld om ICMP toe te staan, de logs tellen het als pings, en het alarmeren was afgesteld op de poorten die iemand zich herinnerde te vrezen. Het verkeer vertrekt eruitziend als een gezondheidscontrole en de gezondheidscontrole is een route naar overal wat de server kan bereiken.
Een echte volledige-tunnel-VPN, met de hand gebouwd
Hans bewijst dat het kanaal er is, maar het doet de interface en de adressering voor je en geeft een route terug, dus het laat je niet helemaal zien wat je hebt gebouwd. Om te zien dat dit een VPN in de volle zin is — het verkeer van de hele machine dat via ping vertrekt, geen link tussen twee benoemde hosts — zet het met de hand in elkaar. icmptunnel8, van Dhaval Kapil, is degene daarvoor, en de eigen beschrijving is één regel: “Transparently tunnel your IP traffic through ICMP echo and reply packets.” Hetzelfde idee, tun-apparaat en echo-payloads, maar je legt de leidingen zelf en niets verbergt zich binnen een binary.
Op de server start je de tunnel, breng je de interface op, en doe je dan het ding dat het hele spel verraadt: zeg de kernel dat hij zelf moet ophouden pings te beantwoorden, zodat zijn eigen echo-antwoorden niet vechten met die de tunnel verstuurt.
sudo ./icmptunnel -s 10.0.1.1 # server mode; creates tun0, then blocks
# from a second shell, bring the interface up (iproute2, not the net-tools the repo ships)
sudo ip addr add 10.0.1.1/24 dev tun0
sudo ip addr add 2001:db8:1::1/64 dev tun0
sudo ip link set tun0 mtu 1472 up # 1500 − 20 (IP) − 8 (ICMP); see below
# stop the kernel replying to pings — echo now belongs to the tunnel
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1
# let the server route the client's packets onward
sudo sysctl -w net.ipv4.ip_forward=1
Lees de regel icmp_echo_ignore_all nog eens, want hij zegt meer dan hij lijkt. Die knop is de eigen knop van de kernel, en hij komt mee als een verdediging: zet hem en, in de woorden van de kernel, hij “will ignore all ICMP ECHO requests sent to it”9, zodat een operator een host volledig van de ping-radar kan halen. Hij is nu op beide families. net.ipv4.icmp_echo_ignore_all al jaren, en net.ipv6.icmp.echo_ignore_all later toegevoegd om te passen.10 De tunnel zet die verdedigende schakelaar aan om de tegenovergestelde reden: nu de kernel echo niet meer zelf beantwoordt, zijn zijn twee einden vrij om echo als puur transport te gebruiken. En dat is het teken. De mensen die de stack schreven behandelen echo al als iets dat een host redelijkerwijs mag weigeren — de regel in dit bericht maakt diezelfde keuze één keer, aan de grens, voor elke host erachter.
Op de client breng je de interface op en wijs je dan de standaardroute eronderdoor. Niet één host die via de tunnel wordt bereikt. Alles.
sudo ./icmptunnel -c <server-public-ip> # client mode; creates tun0
sudo ip addr add 10.0.1.2/24 dev tun0
sudo ip addr add 2001:db8:1::2/64 dev tun0
sudo ip link set tun0 mtu 1472 up
# keep the route to the server itself OUT of the tunnel...
sudo ip route add <server-public-ip> via <gateway> dev <iface>
# ...then send everything else down it
sudo ip route replace default dev tun0
De eigen client.sh en server.sh van het project grijpen nog naar ifconfig en route van net-tools. De ip-commando’s hierboven zijn de iproute2-equivalenten en doen hetzelfde werk. Die route naar de server is de regel die mensen vergeten: houd hem uit de tunnel, of de echo-pakketten die de tunnel dragen proberen zelf door de tunnel te reizen, en er vertrekt niets. Al het andere gaat nu door tun0, in echo verpakt, en bereikt de server. Of het verder gaat is een routeringskeuze. Eén masquerade-regel op de server zou het onder het eigen adres van de server op het publieke internet zetten, en die staat er met opzet niet, want de tunnel is het ding dat wordt getoond en heeft hem niet nodig.
Dat is een volledige-tunnel-VPN, gebouwd uit ping in een handvol commando’s. Elk pakket dat de client stuurt — web, DNS, SSH, alles — wordt in tun0 gevangen en vertrekt als een echo-verzoek naar de server, en de antwoorden komen terug als echo-antwoorden. Een machine op een netwerk dat “alleen ICMP toestaat” heeft net zijn hele uitgaande verkeer aan een bak buiten gegeven, en de grens logde een host die graag pingt.
Je eigen bouwen in Python
Hier is het deel dat iedereen zou moeten verontrusten die hoopt zich hiertegen te verdedigen door een gereedschap te herkennen. Noch Hans noch icmptunnel doet iets dat je niet zelf in een middag zou kunnen schrijven. Open een tun-apparaat, verpak elk pakket als de payload van een echo-verzoek, pak de terugkomende uit. Dat is het hele mechanisme, en de kale tunnel is ongeveer zestig regels Python uit de standaardbibliotheek zonder iets te installeren. De versie hieronder voegt er één ding aan toe: het versleutelt de payload, en dat is het enige deel dat een afhankelijkheid meebrengt.
#!/usr/bin/env python3
# pingvpn.py — a VPN tunnel over ICMP echo, in one short file.
#
# Not a product. It exists to show the channel is trivial to rebuild, so a
# defence that hunts for a known tool is chasing the wrong thing entirely.
# Linux, needs root (a tun device and a raw ICMP socket both do), and the
# `cryptography` package for the AES (pip install cryptography).
#
# server: sudo python3 pingvpn.py --server
# client: sudo python3 pingvpn.py --client <server-public-ip>
#
# The payload is encrypted with AES-128-GCM under a pre-shared key before it
# goes on the wire, so what a firewall sees in the echo data is random bytes —
# the same as a real ping's padding, and nothing for content inspection to read.
import argparse
import fcntl
import hashlib
import os
import select
import socket
import struct
import sys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
TUNSETIFF = 0x400454CA
IFF_TUN = 0x0001
IFF_NO_PI = 0x1000
MAGIC = 0x4954 # 'IT' in the id field, so we ignore real pings
ECHO_REQUEST = 8
ECHO_REPLY = 0
PSK = b"change-me-to-a-shared-secret" # pre-shared secret, both ends
KEY = hashlib.sha256(PSK).digest()[:16] # 128-bit key -> AES-128-GCM
AEAD = AESGCM(KEY)
def open_tun(name=b"tun0"):
fd = os.open("/dev/net/tun", os.O_RDWR)
fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", name, IFF_TUN | IFF_NO_PI))
return fd
def encrypt(data): # -> nonce || ciphertext+tag
nonce = os.urandom(12)
return nonce + AEAD.encrypt(nonce, data, None)
def decrypt(blob): # raises on a packet that is not ours
return AEAD.decrypt(blob[:12], blob[12:], None)
def checksum(data):
if len(data) % 2:
data += b"\x00"
total = sum(struct.unpack("!%dH" % (len(data) // 2), data))
total = (total >> 16) + (total & 0xFFFF)
total += total >> 16
return ~total & 0xFFFF
def build_echo(icmp_type, payload):
head = struct.pack("!BBHHH", icmp_type, 0, 0, MAGIC, 0)
csum = checksum(head + payload)
return struct.pack("!BBHHH", icmp_type, 0, csum, MAGIC, 0) + payload
def main():
ap = argparse.ArgumentParser(description="a VPN tunnel over ICMP echo")
group = ap.add_mutually_exclusive_group(required=True)
group.add_argument("--server", action="store_true")
group.add_argument("--client", metavar="SERVER_IP")
args = ap.parse_args()
out_type = ECHO_REQUEST if args.client else ECHO_REPLY
in_type = ECHO_REPLY if args.client else ECHO_REQUEST
peer = args.client # None on the server until a client is seen
tun = open_tun()
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
print("tun0 created. bring it up with an address and route, then send traffic.",
file=sys.stderr)
while True:
readable, _, _ = select.select([tun, sock], [], [])
if tun in readable: # a packet wants to leave this host
packet = os.read(tun, 65535)
if peer:
sock.sendto(build_echo(out_type, encrypt(packet)), (peer, 0))
if sock in readable: # something arrived over ICMP
data, _ = sock.recvfrom(65535)
ihl = (data[0] & 0x0F) * 4 # skip the IP header the kernel adds
icmp = data[ihl:]
if len(icmp) < 8 or icmp[0] != in_type or icmp[4:6] != struct.pack("!H", MAGIC):
continue
try:
packet = decrypt(icmp[8:]) # wrong key or a real ping -> skip
except Exception:
continue
if args.server:
peer = socket.inet_ntoa(data[12:16]) # reply to whoever sent
os.write(tun, packet) # hand the carried packet to the stack
if __name__ == "__main__":
main()
Dat is de hele tunnel. Het opent tun0 en een ruwe ICMP-socket en pendelt pakketten ertussen: wat de host verlaat wordt verpakt als een echo-verzoek, of een echo-antwoord op de server, en naar de overkant gestuurd; wat over ICMP aankomt wordt uitgepakt en aan de stack teruggegeven. Het id-veld is op één waarde vastgezet zodat het over echte pings heen stapt, en de server leert waar hij moet antwoorden uit de bron van het eerste pakket dat hij ziet.
De versleuteling is het punt om bij stil te staan, want het is wat echt gereedschap doet en het is waarom je dit niet zult vangen door in het pakket te kijken. De payload wordt met AES-128-GCM onder een vooraf gedeelde sleutel verzegeld voordat hij wordt verpakt, dus de bytes in de echo-data zijn niet te onderscheiden van de willekeurige opvulling die een echte ping draagt. Haal de twee cryptoregels eruit en de tunnel draait nog op niets dan de standaardbibliotheek — de enige reden dat hij pip install cryptography nodig heeft is de AES, en de AES is juist het deel dat een leesbare tunnel in een onleesbare verandert. Breng hem op dezelfde manier op als eerder, min de masquerade. Je hebt hem niet nodig om het punt te bewijzen:
# server: start it (creates tun0, then blocks), then configure from the same shell
sudo python3 pingvpn.py --server &
sudo ip addr add 10.9.0.1/24 dev tun0
sudo ip addr add 2001:db8:9::1/64 dev tun0
sudo ip link set tun0 mtu 1400 up
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1
# client
sudo python3 pingvpn.py --client <server-public-ip> &
sudo ip addr add 10.9.0.2/24 dev tun0
sudo ip addr add 2001:db8:9::2/64 dev tun0
sudo ip link set tun0 mtu 1400 up
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1
sudo ip route add <server-public-ip> via <gateway> dev <iface>
sudo ip route replace default dev tun0
De reden om het uit te schrijven is niet het gereedschap. Het is dat het gereedschap wegwerpbaar is. Een kort bestand, geen afhankelijkheden tot je de versleuteling toevoegt, en elke kopie die iemand typt ziet er op de draad een tikje anders uit. Een signatuur die deze vangt, vangt dus volgende week niets. Je kunt je hier niet uit blokkeren door de software te benoemen, want er is geen software om te benoemen.
Het heeft niet eens een shell nodig. De logica is bytes erin, bytes eruit, dus het port naar alles dat een socket kan openen. Zelfs WebAssembly: de browsersandbox weigert een pagina normaal een ruwe socket pardoes, maar waar die barrière is opgeheven (een browser die de toestemming van de gebruiker heeft gekregen, op een machine waar de gebruiker met genoeg rechten draait om een ruwe socket te openen) draait hetzelfde korte programma binnen een tab. En dat is de ongemakkelijke helft ervan. Je kunt je gebruikers hier niet vertrouwen. De host die de tunnel bestuurt zit aan de binnenkant, gehouden door iemand van wie je besloot dat hij veilig was omdat hij achter de firewall zit, en de firewall is het ding waar doorheen wordt getunneld. Perimetervertrouwen gaat ervan uit dat de dreiging buiten de muur is. Deze begint erbinnen, elke keer.
Let op de MTU, en de vloer van 1280 op IPv6
De tunnel is niet gratis op de draad. Elk pakket dat je draagt krijgt een buitenste IP-header en een ICMP-header voordat het vertrekt, dus de binnenste interface moet onder wat het pad kan dragen zitten. Op IPv4 kost dat de aanvaller vrijwel niets. Zet de binnen-MTU laag (de 1472 van icmptunnel is 1500 min 20 voor de buitenste IP-header en 8 voor de ICMP-header, en de Python hierboven zakt naar 1400 voor speling), en waar het getal nog verkeerd is, fragmenteert IPv4 het te grote pakket en zet het aan de overkant weer in elkaar in plaats van het weg te gooien. Tussen de lage vloer die je kunt kiezen en fragmentatie die de rest wegwerkt, draait de tunnel over vrijwel elk pad. Die flexibiliteit is precies wat IPv4 de comfortabele plek maakt om dit te doen.
IPv6 is minder vergevingsgezind, en voor één keer is dat aan jouw kant. De vloer is hard: RFC 820011 §5 eist dat “every link in the Internet have an MTU of 1280 octets or greater”, en IPv6-routers fragmenteren onderweg niet. Dat haalt beide terugvallen van IPv4 in één keer weg, en het bijt de tunnel in beide richtingen. Draai hem over ICMPv6 en elke link op het pad moet 1280 dragen, dus een pad dat daaronder zakt haalt het transport onderuit, en er is geen afschaven onder de vloer zoals op IPv4 kan. Draag IPv6 binnen de tunnel en je stuit van de andere kant op dezelfde muur: de binnenste interface kan ook niet onder 1280, terwijl de buitenste ICMPv6-verpakking, 40 byte header en 8 van ICMPv6, het budget al opmaakt, dus het pad moet 1280 plus de overhead sparen. Mis het op een van beide plekken en het pakket wordt weggegooid, niet kleiner gemaakt om te passen: de tunnel komt op, kleine dingen werken, alles van volle grootte hangt. IPv6 is hier dus de veiligere familie, niet de riskantere. De aanval die op IPv4 over vrijwel alles draait is broos op IPv6, en faalt dicht. Veiliger is niet hetzelfde als veilig, let wel: het echo-kanaal is ook op IPv6 open, dus de regel gooit het nog steeds weg op beide families — de aanvaller kan alleen niet op IPv6 leunen zoals hij op IPv4 leunt.
Het herstel daaruit is de ICMP-fout die je zorgvuldig hebt gehouden. Packet Too Big is wat een verzender de werkende grootte laat vinden, en het is een fout, geen echo, dus de regel waarvoor dit bericht pleit laat het met rust. Gooi de tunnel weg en houd de diagnoses — dat is het hele ontwerp, en de MTU is nog een plek waar het zijn plek verdient.
De regel, versmald tot echo
Het vorige bericht gaf de volledige transitregelset: sta de fouten toe onder een ratelimiet, gooi het echo weg. Ik ga het niet herdrukken. De wijziging waarvoor dit bericht pleit is één paar regels, en het is het paar dat het werk doet:
# permit the ICMP errors — these are load-bearing, keep them
ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } \
limit rate 100/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, \
time-exceeded, parameter-problem } limit rate 100/second accept
# drop the tunnel — echo, both directions, both families
ip protocol icmp icmp type { echo-request, echo-reply } drop
ip6 nexthdr ipv6-icmp icmpv6 type { echo-request, echo-reply } drop
De regel is echter niet die van Linux. Het is dezelfde bedoeling op elke firewall die de naam waard is: sta de fouten toe, begrens hun tempo, gooi echo beide kanten op op beide families weg. Dus hier is het in de dialecten die je waarschijnlijker vasthoudt.
BSD pf (pfSense, OPNsense, OpenBSD, FreeBSD):
# permit the errors; block echo both directions, both families
pass in proto icmp icmp-type { unreach, timex, paramprob }
pass in proto icmp6 icmp6-type { unreach, toobig, timex, paramprob, \
routersol, routeradv, neighbrsol, neighbradv }
block in proto icmp icmp-type { echoreq, echorep }
block in proto icmp6 icmp6-type { echoreq, echorep }
Houd de neighbour-discovery-types op de icmp6-regel; dat zijn degene die het segment onderuithalen als je ze kwijtraakt.
Cisco IOS (uitgebreide ACL’s, aan de rand toegepast):
ip access-list extended ICMP-EDGE
permit icmp any any unreachable
permit icmp any any time-exceeded
permit icmp any any parameter-problem
deny icmp any any echo
deny icmp any any echo-reply
!
ipv6 access-list ICMP6-EDGE
permit icmp any any packet-too-big
permit icmp any any unreachable
permit icmp any any time-exceeded
permit icmp any any parameter-problem
permit icmp any any nd-ns
permit icmp any any nd-na
deny icmp any any echo-request
deny icmp any any echo-reply
Op IPv4 is het echo-type echo; op IPv6 is het echo-request. Ratelimieten wonen in CoPP, niet in de ACL.
Juniper Junos (firewallfilter; het inet6-filter spiegelt dit en houdt neighbour discovery):
firewall family inet filter icmp-edge {
term errors {
from { protocol icmp; icmp-type [ unreachable time-exceeded parameter-problem ]; }
then { policer icmp-cap; accept; }
}
term drop-echo {
from { protocol icmp; icmp-type [ echo-request echo-reply ]; }
then discard;
}
}
MikroTik RouterOS — gooi de twee echo-types weg, aanvaard dan de rest van ICMP, wat de fouten houdt en, op v6, neighbour discovery:
/ip firewall filter
add chain=forward protocol=icmp icmp-options=8:0 action=drop comment="echo request"
add chain=forward protocol=icmp icmp-options=0:0 action=drop comment="echo reply"
add chain=forward protocol=icmp action=accept comment="keep the errors (add limit= to cap)"
/ipv6 firewall filter
add chain=forward protocol=icmpv6 icmp-options=128:0 action=drop comment="echo request"
add chain=forward protocol=icmpv6 icmp-options=129:0 action=drop comment="echo reply"
add chain=forward protocol=icmpv6 action=accept comment="keep errors and ND"
Andere syntaxis, één regel. Sta de berichten toe die de waarheid dragen, gooi het ene weg dat je bytes draagt.
Twee waarschuwingen, die je allebei bijten als je erover heen leest.
Gooi echo weg aan de grens, niet op de draad tussen een host en zijn eigen router. Op IPv6 is neighbour discovery ICMP — types 133 tot 137, nd-router-solicit tot en met nd-redirect — en het is hoe het segment het werk doet dat ARP in IPv4 doet. Draag een echo-drop naar een link-lokale of hostketen zonder die te houden en het segment houdt binnen minuten op te werken, en het zal er niet uitzien als een firewallfout. Filter echo waar verkeer je netwerk verlaat, en laat de interne ketens met rust.
Gooi het in beide richtingen weg en het houdt op nuttig te zijn, hoe dan ook. Alleen het verzoek blokkeren stopt dat je hosts worden gepingd maar laat nog steeds een antwoord naar buiten, en een tunnel kan met wat meer moeite op antwoorden alleen worden gebouwd. Gooi verzoek en antwoord weg, op IPv4 en IPv6, en het kanaal is beide kanten op gesloten.
Wat ping weggooien je echt kost
Wees eerlijk over het verlies, want een controle die je hebt oververkocht is een controle die iemand stil terugdraait de eerste keer dat het onhandig is.
Je verliest ping over de grens. Dat is de kost, volledig gesteld. Het is een echte kost. ping is de reflex, het zit in ieders vingers, en de dag nadat je dit uitrolt zal iemand zeggen dat het internet plat ligt omdat hun ping naar de gateway een time-out geeft. Het ligt niet plat. Je zei de gateway op te houden met een verzoek te beantwoorden dat altijd alleen een gemak was.
Bereikbaarheid testen heeft geen echo nodig. Een TCP-verbinding naar een poort waarvan je weet dat hij open is, vertelt je dat de host op is en het pad werkt, en het vertelt je meer dan een ping, want het bewijst dat een dienst antwoordde, niet alleen een kernel:
# "is it up and reachable?" without sending a single echo
nc -zv <host> 443 # did the TCP handshake complete?
traceroute -T -p 443 <host> # walk the path on TCP, read the errors back
Beide rijden op de ICMP-fouten die je hield en de TCP die een echte dienst al spreekt. Geen van beide stuurt een echo. De eerlijke ruil is dus deze: je geeft het minst informatieve gereedschap in de doos op, degene die “is een kernel bereid te antwoorden” beantwoordt en niets meer, en in ruil sluit je het enige deel van ICMP dat een aanvaller in een route van je netwerk af kan veranderen. De diagnoses waar je op een slechte dag echt naar grijpt zitten allemaal aan de andere kant van de streep, onaangeroerd.
De Fisher-Price OS (Windows) is hier geen uitweg
Draai je een winkel op een Fisher-Price OS (Windows), dan lees je dit misschien als het probleem van iemand anders. Dat is het niet. Het risico zit in het protocol, niet in het besturingssysteem. De standaard verplicht elke host die een ping beantwoordt je bytes terug te geven, en hij stopt niet om eerst te vragen wat de host draait.
Een bak op dat besturingssysteem maakt een prima tunneleindpunt. Hans levert een Windows-client, dus een machine binnen kan het clienteinde zijn en zijn verkeer naar buiten geven binnen echo als elke andere. Wat het niet makkelijk kan zijn is het servereinde, want dat wil een tun-apparaat en een ruwe ICMP-socket opengehouden, en op dit besturingssysteem kunnen alleen leden van de groep Administrators sockets van het type SOCK_RAW aanmaken12 — de eigen woorden van Microsoft. De server zit dus op een echt besturingssysteem, zoals de mijne, en de bak achter de firewall die zich stil naar buiten pingt is degene waarvan je is verteld dat hij veilig was omdat hij aan de binnenkant zat.
En dat is het punt voor iedereen die het verdedigt. De grensregel kan het niet schelen wat de binnenkant draait. Gooi echo weg waar het verkeer het netwerk verlaat en elke host erachter is gedekt — degene die je beheert, en degene waarvan je werd verzekerd dat hij zich achter NAT verstopte. Ik draai het ding niet, en ik ga niet doen alsof de tunnel het respecteert. De oplossing is dezelfde regel, op dezelfde plek, waar het eindpunt ook in is opgestart.
Is de bak zelf wat je hardt (de bak, niet het netwerk), dan zal PowerShell hem tenminste laten ophouden een ping op eigen houtje te beantwoorden of uit te stoten:
foreach ($t in 8,0) { New-NetFirewallRule -DisplayName "Drop ICMPv4 Echo $t" -Protocol ICMPv4 -IcmpType $t -Direction Inbound -Action Block }
foreach ($t in 128,129) { New-NetFirewallRule -DisplayName "Drop ICMPv6 Echo $t" -Protocol ICMPv6 -IcmpType $t -Direction Inbound -Action Block }
Voeg tweelingen met -Direction Outbound toe om te stoppen dat hij als tunnelclient naar buiten reikt in plaats van als doelwit te blijven zitten, en laat de ICMPv6-fout- en neighbour-discovery-types met rust, om de reden dat ze overal elders op deze pagina met rust worden gelaten. Maar verwar het niet met de controle. Een hostfirewall is geen transitfirewall. Het hardt de bak en verandert niets aan de grens, en de grens is waar dit echt wordt gesloten.
Kaart dit vandaag aan bij wie je grens beheert
Je hebt dit vermoedelijk zo ver gelezen denkend aan een netwerk dat niet van jou is om te wijzigen. De meeste mensen wel. Het nuttige om te doen is dus niet naar de firewall grijpen, het is de vraag voorleggen aan wie hem vasthoudt — je eigen netwerkteam, je MSP, of de leverancier wiens bak aan de rand zit — en die vandaag voorleggen, want dit is een open deur sinds 1996 en nog een week ervan is een keuze.
Vraag onomwonden, en vraag met een blanco blik verwachtend, want ik zou de hele Britse GDP verwedden dat niemand daar echo een tweede gedachte heeft gegeven. Het is het ene pakket dat iedereen toestaat en niemand bezit, en een onbezeten regel is precies degene die jaren verkeerd heeft gestaan zonder dat iemand het merkt.
Vraag ze vier dingen, in deze woorden, zodat er geen ruimte is om mee te knikken en niets te veranderen:
- Gooien we ICMP-echo-verzoek en echo-antwoord weg aan de grens, op IPv4 en IPv6 allebei? Niet “staan we ICMP toe” — het specifieke bericht, beide richtingen, beide families. Is het antwoord één familie, dan is het geen antwoord, want de tunnel verhuist gewoon naar de andere.
- Staan we de ICMP-fouten nog steeds toe? Destination Unreachable, Time Exceeded, Parameter Problem, en Packet Too Big op IPv6, onder een ratelimiet in plaats van een blokkade. Kunnen ze het niet zeggen, dan is de kans even groot dat ze of alles open hebben gelaten of de hele boel hebben geblokkeerd, en beide zijn verkeerd.
- Hebben we IPv6-neighbour-discovery op de interne ketens gehouden? Types 133 tot 137. Dit is degene die van “we hebben ICMP gehard” een segment maakt dat een week later stil sterft, en het is de fout die een gehaaste wijziging maakt.
- Laat me de regel zien. Geen beleidsverklaring, de echte regel op de echte bak, en de datum dat hij erop kwam. Een controle die niemand kan aanwijzen is een controle die er niet is.
Zal de bak van de leverancier zijn gekomen met dit al gesloten? Niet dus. De standaard is vrijwel overal om echo door te laten en het als een pakkettelling vast te leggen, wat de hele reden is dat de tunnel überhaupt werkt. Het buit geen fout uit, het gebruikt de configuratie die als standaard meekomt. Niemand sluit een deur waarvan hij nooit is verteld dat hij open was.
De reden om op de bewoording te staan is dat de luie oplossing voor dit bericht “goed, we blokkeren ICMP” is, en die oplossing is erger dan het gat. Het maakt path MTU discovery en traceroute kapot, het zal je hangende overdrachten laten najagen met niets in de logs, en op IPv6 haalt het delen van het internet volledig van tafel. Grijpt de persoon die je vraagt daarnaar, hou ze tegen. De instructie is met opzet smal: gooi echo weg, houd de fouten, houd neighbour discovery. Iedereen die firewalls voor de kost draait zou die wijziging dan ook in een middag moeten kunnen maken en je zeggen dat het klaar is.
En zijn ze een MSP die je betaalt om dit te draaien: een leverancier die je je eigen echo-en-foutpositie niet uit het hoofd kan vertellen, of die “we blokkeren ICMP” antwoordt alsof dat de veilige optie was, heeft je net iets verteld over de rest van de omgeving. De volgende keer dat een storing tussen twee netwerken zit en beide zeggen dat ze schoon zijn, onthou welke van hen niet kon beschrijven wat zijn eigen firewall met een ping doet.
De diagnose die nooit een diagnose was
Ping heeft een goede tijd gehad. Mike Muuss schreef het in 1983 om na te kijken of een host antwoordde, noemde het naar sonar, en het deed dat ene werk zo goed dat het het eerste werd waar iedereen naar grijpt en het laatste dat iemand in twijfel trekt. Veertig jaar later is het spiergeheugen, en spiergeheugen is precies hoe een risico overleeft. Niemand herbekijkt het ding dat hij altijd heeft gedaan.
Maar kijk naar wat het echt is, ontdaan van de gewoonte. Een bericht dat geen feit over het netwerk draagt, dat elke host verplicht is te beantwoorden met je eigen bytes teruggegeven, dat over een protocol loopt dat de controles niet inspecteren en de logs niet lezen. Alles wat het onschuldig laat voelen — het is maar een diagnose, het is maar een keepalive, iedereen staat het toe — is hetzelfde dat het de schoonste weg af van een gefilterd netwerk maakt die bestaat. De industrie blokkeert de ICMP-fouten, die de waarheid dragen en het netwerk kapotmaken als ze weg zijn, en zwaait echo door, dat draagt wat je erin laadt. Het heeft het protocol precies op zijn kop.
De oplossing is niet slim. Houd de berichten die je de waarheid vertellen, en gooi degene weg die alleen je bytes teruggeeft. Het kost je een commando dat je toch al geen zaak had te vertrouwen voor een diagnose, en het sluit een deur die open heeft gestaan sinds 1996, gedocumenteerd in een hackerblad, twee decennia lang verpakt, en wagenwijd gelaten omdat hem sluiten zou betekenen dat iemand niet kon pingen. Weet wat een ding kost, niet wat het is geprijsd. Ping is geprijsd op niets. Het kost je het ene kanaal dat je niet kunt zien.
Loki, Phrack 49 — een commandokanaal gedragen binnen de echo-payloads van ICMP, 1996. ↩︎
RFC 792 — ICMP: “the data received in the echo message must be returned in the echo reply message”. ↩︎
RFC 4443 — ICMPv6: echo-data “MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message”. ↩︎
RFC 4890 — de ICMPv6-berichten die een firewall niet mag weggooien. ↩︎
RFC 1812 — routervereisten: Time Exceeded is een MUST, genoemd naar traceroute. ↩︎
icmptunnel — tunnelt IP-verkeer door ICMP-echo- en antwoordpakketten, van Dhaval Kapil. ↩︎
Linux kernel — IP sysctl —
icmp_echo_ignore_all: “the kernel will ignore all ICMP ECHO requests sent to it”. ↩︎Linux commit e6f86b0f — “ipv6: Add icmp_echo_ignore_all support for ICMPv6” — het IPv6-equivalent, van Virgile Jarry. ↩︎
RFC 8200 §5 — IPv6 eist dat elke link een MTU van ten minste 1280 octetten heeft. ↩︎
Microsoft — TCP/IP raw sockets — “only members of the Administrators group can create sockets of type SOCK_RAW”. ↩︎