Linux Kernel route_localnet Einstellung



Der Linux Kernel hat Einstellungen für Netzwerk Schnittstellen die es erlauben, das lokale Netz (127.0.0.0/8 für IP Version 4, auch oft Loopback Adresse bezeichnet weil sie typischerweise der lo Loopback Schnittstelle zugewiesen ist) zu routen. Diese Einstellung kann man über die Datei

/proc/sys/net/ipv4/conf/eth0/route_localnet

für die typische eth0 Netzwerk Schnittstelle erreichen. Die Einstellung kann man ändern, indem man dort entweder 0 (Standardeinstellung) für kein Routing oder 1 für Routing hineinschreibt. Einstellungen kann man bei den üblichen Linux Distributionen persistent machen, indem man einen Eintrag in /etc/sysctl.conf vornimmt. Dabei wird das Präfix /proc/sys/ vom obigen Pfad entfernt und alle / Zeichen durch einen Punkt ersetzt, das Resultat für obiges Beispiel wäre dann:

net.ipv4.conf.eth0.route_localnet = 1

Was kann man mit dieser Einstellung tun?

Im Folgenden verwende ich Beispiele für die IP Version 4, das gleiche kann man aber für die IP Version 6 local host Adresse ::1 erreichen.

Im Linux Netzwerk-Stack werden IP Pakete die entweder von einer Adresse aus dem lokalen Netz 127.0.0.0/8 kommen oder eine Zieladresse dort haben verworfen, wenn sie über eine Schnittstelle kommen die nicht für diesen Adressbereich konfiguriert ist. Wir verifizieren das im Folgenden zuerst mal.

Für einen einfachen UDP Server in Python der UDP Pakete empfängt und deren IP Adresse und Port (und Länge) ausdruckt können wir das folgende (gekürzte) Script verwenden:

import socket
[...]
sock = socket.socket (socket.AF_INET, socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
sock.bind ((args.address, args.port))
while True:
    buf, adr = sock.recvfrom (1024)
    l = len (buf)
    print ('Received %(l)s bytes from %(adr)s' % locals ())

Alle Werte in der Variable args kommen von der Kommandozeile mit der Klasse ArgumentParser. Die Adresse auf die gebunden wird (bind) ist im folgenden standardmässig 0.0.0.0 was bedeutet dass Pakete von allen Netzwerk-Schnittstellen empfangen werden.

Wir testen das erstmal mit einem einfachen Client-Programm. Im folgenden haben wir zwei Maschinen, .9 sendet und .5 empfängt. Zum Versenden von UDP Paketen können wir den folgenden einfachen Code verwenden:

import socket
sock = socket.socket (socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.sendto (b'This is a test', (args.address, args.port))

Auch hier werden die Parameter auf der Kommandozeile angegeben. Wenn wir von .9 an .5 senden bekommen wir vom obigen Server

Received 14 bytes from ('XXXXXXX.9', 38232)

wobei der der Quell-Port 38232 eine zufällige vom Linux Kernel erzeugte Portnummer ist und XXXXXXX die oberen Bytes der IP-Adresse des Senders repräsentiert. Wenn wir auf der empfangenden Maschine an 127.0.0.1 senden bekommen wir:

Received 14 bytes from ('127.0.0.1', 58196)

Wir sehen dass der Server sowohl auf der Ethernet Schnittstelle als auch auf der Loopback Schnittstelle empfängt.

Soweit nichts neues. Schauen wir mal was passiert wenn wir Adressen fälschen. Um beliebige IP Pakete (oder sogar Nicht-IP Pakete) zu erzeugen verwende ich im Folgenden das Python Paket Scapy.

Ein Ausschnitt aus dem Script:

p = Ether (dst = args.mac_address, src = args.mac_address) \
    / IP (dst = args.address, src = args.source_address) \
    / UDP (dport = args.port, sport = args.source_port) \
    / Raw (b'This is a test')
sendp (p, iface = args.interface)

Das baut ein Ethernet Paket (Ether) mit IP Inhalt. Das IP Paket wiederum enthält ein UDP Paket was wiederum einen Raw Zeichenfolge enthält. Das Paket wird über einen Netzwerk Layer 2 Socket versendet. Wiederum kommen die Parameter von der Kommandozeile. Standard für alle Ports ist 22222, die Quell-IP ist XXXXXXX.9 und die Ziel-IP ist XXXXXXX.5. Ziel- und Quell MAC Adresse werden beide auf die MAC-Adresse der Zielmaschine gesetzt. Wir wiederholen was wir mit dem einfachen Client ausprobiert haben:

sudo scapyclient.py

Was in der folgenden Ausgabe resultiert:

Received 14 bytes from ('XXXXXXX.9', 22222)

Hier wird standardmässig immer Port 22222 verwendet es sei denn wir setzen den Port über eine Kommandozeilen-Option des Scripts. Wir müssen sudo verwenden weil nur der Superuser beliebige Pakete auf Layer-2 des Netzwerk-Stacks senden darf.

Was passiert nun wenn wir Pakete fälschen? Damit wir sehen was gesendet (und empfangen) wird starten wir tcpdump wie folgt:

sudo tcpdump -n -i eth0 udp port 22222

Natürlich muss das Argument der -i Option geändert werden wenn die Netzwerk-Schnittstelle anders heisst als eth0. Die Option -n sagt tcpdump dass keine DNS-Auflösung von IP Adressen erfolgen soll. Wir senden ein Paket mit einer localhost Adresse als Quell-Adresse:

sudo scapyclient.py --source-address=127.0.0.1

Auf beiden Maschinen sehen wir in der tcpdump Ausgabe

TT:TT:TT.TTTTTT IP 127.0.0.1.22222 > 10.23.5.5.22222: UDP, length 14

wobei TT:TT:TT.TTTTTT ein Zeitstempel ist. Das Paket geht also auf der Ethernet-Schittstelle der sendenden Maschine raus und wird von der empfangenden Maschine empfangen (weil die MAC-Adresse richtig ist) aber von unserem Server wird nichts ausgegeben.

Das gleich passiert wenn wir die Zieladresse ändern:

sudo scapyclient.py --address=127.0.0.1

Die tcpdump Ausgabe ist jetzt:

TT:TT:TT.TTTTTT IP 10.23.5.9.22222 > 127.0.0.1.22222: UDP, length 14

Wieder wird das von beiden Maschinen von tcpdump ausgegeben aber nichts wird von unserem Server ausgegeben.

Schauen wir nun was passiert wenn wir route_localnet für unsere Ethernet Schnittstelle einschalten (das folgende funktioniert natürlich nur als root):

echo 1 > /proc/sys/net/ipv4/conf/eth0/route_localnet

Wir fälschen wieder die Quelladresse:

sudo python3.11 scapyclient.py --source-address=127.0.0.1

Wieder wird das Paket von beiden laufenden tcpdump Prozessen ausgegeben aber wieder nicht von unserem Server. Aber wenn wir die Zieladresse fälschen:

sudo python3.11 scapyclient.py --address=127.0.0.1

Bekommen wir zusätzlich zur tcpdump Ausgabe auch eine Antwort von unserem Server:

Received 14 bytes from ('10.23.5.9', 22222)

Und bemerkenswert ist, dass das auch funktioniert wenn wir unserem Server sagen dass er nur auf der localhost Adresse hören soll:

python3 server.py --address=127.0.0.1

Der Server empfängt trotzdem das gefälschte Paket obwohl es nicht über die Loopback Schnittstelle empfangen wurde:

Received 14 bytes from ('10.23.5.9', 22222)

Sicherheit

Manche Anwendungen verwenden lokale Server die nur auf die Loopback Schnittstelle hören. Es wird erwartet dass diese nur Pakete über die Loopback Schnittstelle empfangen – gesendet von Prozessen die auf der selben Maschine laufen wie der Server.

Dies ist immer eine gefährliche Annahme, es sollten nie gefährliche Dinge getan werden die sich auf diese (Nicht-) Authentifizierung verlassen. Zumindest unter Linux gibt es eine gewisse Sicherheit solange die Einstellung route_localnet auf 0 gesetzt ist. Auf anderen Betriebssystemen ist das Glück vielleicht nicht so hold. Und auch unter Linux kann dieses Verhalten ausgeschaltet werden.

Warum wollen wir diese Einstellung vornehmen?

Manchmal sollen Verbindungen die auf der lokalen Maschine zu einem Port auf der lokalen Maschine aufgebaut werden zu einer anderen Maschine verbunden werden, wenn z.B. /etc/hosts folgenden Eintrag hat

127.0.0.1 some.remote.example.com

und eine Anwendung zu dieser Maschine verbinden soll können wir Firewall Regeln konfigurieren die die Pakete so umschreiben dass das funktioniert:

iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp --dport 443 \
    -j DNAT --to-destination XXXXXXX.5:1443
iptables -t nat -A POSTROUTING -d XXXXXXX.5 -p tcp --dport 1443 \
    -j SNAT --to-source XXXXXXX.9

Die erste Regel schreibt die Zieladresse um (und den Zielport) während die zweite Regel sicherstellt dass Antwortpakete ankommen indem die Quelladresse umgeschrieben wird. Aber ohne die Einstellung route_localnet für die ausgehende Ethernet-Schnittstelle wird das nicht funktionieren, die Pakete werden verworfen.

Aber Vorsicht: Weil diese Einstellung auch auf die empfangenen Pakete einen Einfluss hat ist zu beachten was im Abschnitt Sicherheit beschrieben wurde.