2020-03-30

Paket basiertes Routing mit Iptables

Teil 2 vom Routing Projekt

Probleme mit reinem IP Routing

Mit dem "alten" Script bin ich nun abseits von einigen Verbesserungen ca. 1 Jahr gut zurecht gekommen, jedoch mit 2 nervigen Einschränkungen:

Ich habe einen Fehler gemacht, und das Script auch per UDEV-Regel gestartet. Udev hat aber mit dem Initialisieren der eigentlichen Ethernet-Verbidnung nichts mehr zu tun, und macht dann dementsprechend nichts mehr, wenn man z.B. am Handy den Hotsport ausstellt, oder die Netzwerkverbindung anderweitig down und wieder up geht, dann verschwinden zwar die Routen aus den Tabellen, das Script wird dann jedoch nicht mehr aktiv.

Das zweite Problem war eine Art Racing Condition. Das eigentliche Einrichten der Netwerk-Adapter übernimmt unter Debian-Jessie das "ifupdown" Package. Dieses hat immer genau dann losgelegt, wärend gleichzeitig mein Script von UDEV ausgeführt wurde. Manchmal war dann einfach der Netzwerkadapter noch nicht komplett hochgefahren, und der ifstate auf DOWN, dann konnte mein Script natürlich noch keine Routen hinzufügen.

Als Workaround habe ich den ifup-Service, den ifupdown über System-D bereitstellt, überschrieben, sodass immer wenn ein NIC angeschlossen wird mein Script aufgerufen wird. Zwar muss mein Script sich nun um alles selbst kümmern (IP-Adresse zuweisen, Interface "Uppen"), aber man hat so endlich keine hässlichen Racing-Conditions mehr, und die volle Kontrolle darüber, was gerade passiert.

Slice=system.slice
ExecStart=/bin/sh -ec '/opt/net/setup_with_log'
ExecStop=/bin/sh -ec '/opt/net/setup_with_log'
#ExecStop=/sbin/ifdown %I
#ExecStart=/bin/sh -ec 'ifup --allow=hotplug %I; ifquery --state %I; /opt/net/setup_with_log'
RemainAfterExit=true
TimeoutStartSec=5min
/etc/systemd/system/ifup@.service

Mit der Änderung, dass nur noch auf die Existenz des NIC geprüft wird, und nicht auf den State funktioniert nun alles endlich komplett automatisch.

Dies könnte jedoch zu Problemen bei fest integrieten NICs kommen. USB Geräte verschwinden einfach komplett, wenn sie abgesteckt werden, eth0 z.B. wechselt dann einfach auf den Down-State, das Script würde dann das NIC als present bewerten und dann nicht funktionierende Routen erstellen. Da ich gerade die Festnetzleitung immer benutze, macht dies momentan noch keine Probleme.

Zum einen musste Ich die Subnetze für jeden Online-Dienst herausfinden, den ich anders geroutet haben wollte. Dies an sich ist schon ein zeitintensives, und fehlerbehaftetes Unterfangen.

Auch sämtlichen Traffic nur basierend auf der IP Adresse zu routen ist in meinem Fall nicht zielführend, da man nicht differenzieren kann, welchen Traffic man über LTE routen möchte, und welchen nicht. Beispielsweise gibt es bei Online-Spielen, oder Konferenzsystemen die Unterscheidung von zeitkritischen Funktionen, wie der Game-Traffic oder Sprachpaketen, und weniger Zeitkritischen, aber "Teuren" Traffic, wie Patches oder Assets, die Übertragen werden. Beim reinen IP-Routing würde so im schlimmsten Fall sämtlicher Traffic über eine Volumen-LTE-Verbindung geroutet, und würde das Volumen in kürzester Zeit aufbrauchen.

Bei League of Legends besteht das Problem nicht, da die Game-Server keine Patches ausliefern, bei anderen Spielen müsste man das Verhalten erst analysieren, die Gameserver-IPs herausfinden, und diese dann ggf. mit in das Script aufnehmen, dies ist aber ein Kampf gegen Windmühlen.

Einfacher ist es, den Traffic nach Protokoll aufzuschlüsseln, und diesen dann demenstprechend zu routen, zeitkritischer Traffic wird meist über UDP-Pakete verschickt, diesen kann man mit Iptables markieren und über eine gesonderte Routing-Tabelle routen. Ein Vorteil ist, dass man so Dienstübergreifend sämtlichen Echtzeittraffic von Bandbreitenintensiven Traffic trennen kann, in der UDP-Routing-Tabelle kann man dann noch spezifischere Fälle abdecken.

Meine Routing Idee sieht gerade so aus:

Ich route z.B. so Teamspeak und Discord über meine Festnetz-Leitung, und League of Legends priorisiert über meinen 1GB LTE Tarif von Congstar, da ich weiß, dass eine Runde LoL sehr wenig Bandbreite verbraucht, und der Ping meistens konstanter ist als beim Festnetz DSL. Sämtlicher anderer UDP Traffic läuft so über die Leitung, die das beste Verhältnis zwischen Stabilität und Datenvolumen hat.

Realisierung

abgespecktes routing-flowchart

Das Ausführen des setup-Scripts wurde von den Udev-Rules entfernt, jetzt werden hier nur die Namen der NICs festgelegt.

SUBSYSTEM=="net", ACTION=="add", ATTR{address}==xx:xx:xx:xx:xx:xx", NAME="funk"
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="xx:xx:xx:xx:xx:xx", NAME="vodafone"
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="xx:xx:xx:xx:xx:xx", NAME="congstar"
/etc/udev/rules.d/70-persistent-net.rules

Im setupNetworking Script wird analog zu den IP-Routen noch eine Iptables Regel erstellt, sowie eine neue Routing-Tabelle angelegt, die sich um das Routing der UDP Pakete kümmert:

echo "201 udp_routing_table" >> /etc/iproute2/rt_tables

Mit diesem Befehl wird eine neue Routing-Tabelle angelegt.

iptables -A PREROUTING -s ${HOME_SUBNET}/24  -i $WIRE_NIC -t mangle -p udp -j MARK --set-mark 1

Mit diesem Iptables Befehl werden sämtliche UDP-Pakete, die aus dem Heimnetz-Subnetz kommen mit einer Markierung versehen. Das muss unbedingt in der PREROUTING Chain passieren, damit die Pakete danach basierend auf dem Ergebnis geroutet werden können.

ip rule add fwmark 1 table udp_routing_table
ip route add default via $FRITZ_BOX_GATEWAY dev $WIRE_NIC table udp_routing_table

Mit diesen beiden iproute-Befehlen werden die markierten Pakete dann unter Berücksichtigung der udp_routing_table Tabelle geroutet.

Das Ergebnis:

alter vs. neuer ping bei discord

Der Ping unter Discord ist nun deutlich konstanter, weil nun der UDP Traffic über die Festnetz Leitung läuft, sätmliche Updates, Bilder/Videos in Chats oder anderer TCP-Verkehr wird trotzdem noch über die schnelle Freenet-Verbindung geroutet.

Todo

Das Hauptproblem ist so gelöst, alle zeitkritischen Dienste laufen nun über eine Leitung mit geringer Latenz.

Doch wie stark ist die monatliche Belastung für Volumentarife, wenn jetzt nicht nur ein Online-Spiel über die Handy-Leitung geroutet wird, sondern auch Voip, und andere Online-Spiele, die eventuell mehr Nutzdaten übertragen?

Mit Iptables lässt sich dies herausfinden, indem man die markierten Pakete zählen kann. Nach einem Monat kann man dann genau ablesen, wie viel Traffic erzeugt wurde, und welchen LTE Tarif, man dann bestellen sollte, oder ob man bestimmte Dienste nicht über LTE routen sollte.

Ein potentielles Problem wären dann noch VPN-Dienste, die den Traffic über UDP verschicken, diese müssten noch explizit mit einer anderen iptables-Regel anders markiert werden, sodass diese nicht über eine eventuell gecappte Verbindung fließen, doch damit habe ich mich noch nicht beschäftigt.

Script

Das komplette Script:

#!/bin/bash
#Prios
#League IPs = congstar, vodafone, eth0
#Twitch IPs = eth0, funk
#Russendisco = eth0 (wegen nat)
#TS = vodafone, funk, eth0
#Default Route = funk, eth0

USE_UDP_ROUTING=1

NET_DIR=/sys/class/net/
APPLE_GATEWAY=172.20.10.1
FRITZ_BOX_GATEWAY=192.168.178.1
HOME_SUBNET=192.168.178.0

#Die Netzwerkadapter, die ich definiert habe
CONGSTAR_NIC=congstar
FUNK_NIC=funk
VODAFONE_NIC=vodafone
WIRE_NIC=eth0

#Zuweisung der Standardgateways für die definierten NICs
#Diese kann man dann als key-value Wert auslesen
declare -A configs
configs[$CONGSTAR_NIC]=$APPLE_GATEWAY
configs[$FUNK_NIC]=$APPLE_GATEWAY
configs[$VODAFONE_NIC]=$APPLE_GATEWAY
configs[$WIRE_NIC]=$FRITZ_BOX_GATEWAY

#Zuweisung der statischen IP Adressen für die NICs
declare -A ips
ips[$CONGSTAR_NIC]="172.20.10.5/28"
ips[$FUNK_NIC]="172.20.10.2/28"
ips[$VODAFONE_NIC]="172.20.10.4/28"
ips[$WIRE_NIC]="192.168.178.46/24"

#In diese Variable wird der Output für Telegram geschrieben
telegramString=""

#NICS
interfaces=(
	$CONGSTAR_NIC \
	$FUNK_NIC \
	$VODAFONE_NIC \
	$WIRE_NIC
)

#Priorities for udp
udprpios=(
	$CONGSTAR_NIC \
	$VODAFONE_NIC\
	$WIRE_NIC \
	$FUNK_NIC
)

#IP Ranges for routing

league=(
	"162.249.72.0/24" \
    "162.249.73.0/24" \
    "162.249.74.0/24" \
    "162.249.75.0/24" \
    "162.249.76.0/24" \
    "162.249.77.0/24" \
    "162.249.78.0/24" \
    "162.249.79.0/24" \
    "185.40.64.0/24" \
    "185.40.65.0/24" \
    "185.40.66.0/24" \
    "185.40.67.0/24" \
)

twitch=(
	185.42.204.33
)

teamspeak=(
	84.200.93.247 \
)

default_route_array=(
	default
)

# Priority Maps
priority_league=(
	$CONGSTAR_NIC \
	$VODAFONE_NIC\
	$WIRE_NIC \
	$FUNK_NIC
)

priority_teamspeak=(
	$VODAFONE_NIC \
	$WIRE_NIC \
	$CONGSTAR_NIC \
	$WIRE_NIC
)

priority_twitch=(
	$WIRE_NIC \
	$FUNK_NIC
)

priority_route=(
	$FUNK_NIC \
	$WIRE_NIC
)

#Hier wird das NIC aktiviert, falls es innerhalb von 20 Versuchen nicht funktioniert bricht das Script ab.
enableNIC() {

	echo "Enabling $1 ..."

	ip link set $1 up
	up=`cat /sys/class/net/$1/carrier`
	counter=20
	echo "waiting for $1 to become up for $counter seconds...."
	while [ "1" != "$up" ]
	do
		
		counter=$((counter-1))

		if [ $counter -eq 0 ]; then
				error="Timed out while waiting for nic to come up!"
				telegram-send $error
				echo $error
				exit -1
		fi

		echo "waiting for up....."
		up=`cat /sys/class/net/$1/carrier`

		sleep 1
	done 

}

#Hier wird die Routing-Tabelle mit den überreichten Einträgen befüllt
# $1 = NICs, $2 = Array With IPs, $3 = Name, $4 = Place in udp routing table
setUpRoute() {
	declare -a nicArray=("${!1}")
	declare -a ipArray=("${!2}")

	echo "Trying to set up priority routing for $3 with canidates: (${nicArray[@]}))"
	for nic in "${nicArray[@]}"; do
		isPresent $nic
		if [ $? -eq 0 ]; then

			#Setup NAT

			if [ -z $4 ]; then
				setupNAT $nic
				echo "Route $3 over $nic ( ${nicArray[@]} )"
				setRoute "${configs[$nic]}" "$nic" ipArray[@]
				telegramString+="$3 -> $nic
				"
			else
				setupNAT $nic
				echo "Route $3 over $nic ( ${nicArray[@]} )"
				setRoute "${configs[$nic]}" "$nic" ipArray[@] "udp_routing_table"
				telegramString+="$3 -> $nic [udp table]
				"
			fi


			break
		fi
	done
}

# $1 = Array, $2 = Array With IPs, $3 = Name, $4 = Place in udp route
tearDownRoute() {
	declare -a nicArray=("${!1}")
	declare -a ipArray=("${!2}")

	echo "Trying to set up priority routing for $3 with canidates: (${nicArray[@]}))"
	for nic in "${nicArray[@]}"; do
		isPresent $nic
		if [ $? -eq 0 ]; then
			#Setup NAT

			if [ -z $4 ]; then
				setupNAT $nic
				deleteRoute "${configs[$nic]}" "$nic" ipArray[@]
			else
				setupNAT $nic
				deleteRoute "${configs[$nic]}" "$nic" ipArray[@] "udp_routing_table"
			fi


			break
		fi
	done
}

setRoute() {	
	declare -a iArray=("${!3}")

	for ip in "${iArray[@]}"
	do
		:
		if [ -z $4 ]; then
			echo "ip route add $ip via $1 dev $2"
			ip route add $ip via $1 dev $2
		else
			echo "ip route add $ip via $1 dev $2 table $4"
			ip route add $ip via $1 dev $2 table $4
		fi
	done
}

deleteRoute() {
	declare -a iArray=("${!3}")
	for ip in "${iArray[@]}"
	do
		:
		if [ -z $4 ]; then
			echo "ip route delete $ip via $1 dev $2"
			ip route delete $ip via $1 dev $2
		else
			echo "ip route delete $ip via $1 dev $2 table $4"
			ip route delete $ip via $1 dev $2 table $4
		fi
	done
}

delUDPRouting() {

	#Save transmitted packets to have traffic history
	dev=`ip route show table udp_routing_table | grep default | awk '{ print $5 }'`
	traffic=`iptables -L -v -t mangle | egrep "MARK.* udp" | awk '{ print $2 }'`
	echo "$dev $traffic" >> /home/bananapi/udp_bandwidth_history

	ip route del 0/0 table udp_routing_table # Delete all default routes
	ip rule delete fwmark 1 table udp_routing_table

	iptables -D PREROUTING -s ${HOME_SUBNET}/24  -i $WIRE_NIC -t mangle -p udp -j MARK --set-mark 1
}

setupUDPRouting() {

	printf "Check if udp_routing_table already exists..."

	grep "udp_routing_table" /etc/iproute2/rt_tables > /dev/null

	if [ $? -ne 0 ]; then
		printf "...does not exist, gonna create it\n"
		echo "201 udp_routing_table" >> /etc/iproute2/rt_tables
	else 
		printf "...yes\n"
	fi

	ip rule add fwmark 1 table udp_routing_table
	ip route add default via $FRITZ_BOX_GATEWAY dev $WIRE_NIC table udp_routing_table

	#Check, if iptables rule is present
	printf "Check if iptables mark rule already exists..."
	iptables -C PREROUTING -s ${HOME_SUBNET}/24  -i $WIRE_NIC -t mangle -p udp -j MARK --set-mark 1 2>&1 > /dev/null
	
	if [ $? -ne 0 ]; then
		printf "...does not exist, gonna create it\n"
		iptables -A PREROUTING -s ${HOME_SUBNET}/24  -i $WIRE_NIC -t mangle -p udp -j MARK --set-mark 1
	else
		printf "..yes\n"
	fi

}

notify() { 
	telegram-send $1
} 

#Removes all routing entries previously set
purgeTable() {
	echo "Clean up current routes"

	#Clear everything here because we own this table and nothing could break if we delete everything
	ip route flush table udp_routing_table

	#League

	tearDownRoute priority_league[@] league[@] "League" udp

	#Twitch

	tearDownRoute priority_twitch[@] twitch[@] "Twitch"

	#Teamspeak

	# tearDownRoute priority_teamspeak[@] teamspeak[@] "Teamspeak"

	#Default Route

	tearDownRoute priority_route[@] default_route_array[@]  "Default Route"
} 

# Check if network adapter is currently plugged in
isPresent() {
	ip link show $1 | grep "state" > /dev/null
	res=$?
	if [ -d "$NET_DIR/$1" ] && [ $res -eq 0 ]; then
		echo "${1} is present"
		return 0
	else
		return 1
	fi
} 

setDefaultRoute() {
	ip route add default via $1 dev $2	
}

setupNAT() {
	 echo "Setup NAT for $1"
	 iptables -t nat -A POSTROUTING -o $1 -j MASQUERADE
}

telegram-send "Network Change detected..."

sleep 1

#Falls das script schon läuft, die ältere Insanz abbrechen
for pid in $(pidof -x setupNetworking.sh); do
    if [ $pid != $$ ]; then
        echo "[$(date)] : setupNetworking.sh : Process is already running with PID $pid"
        kill $pid
    fi
done

#Assigns the passed interface an ip address, if it does not already have one,
for nic in "${interfaces[@]}"; do
	isPresent $nic
	if [ $? -eq 0 ]; then
		enableNIC $nic
	fi

	ip addr show dev $nic | grep "inet "
	if [ $? -ne 0 ]
	then
		echo "ip addr add ${ips[$nic]} dev $nic"
		ip addr add "${ips[$nic]}" dev $nic
	fi	
	
done

sleep 1

purgeTable

delUDPRouting

if [ $USE_UDP_ROUTING -eq 1 ]; then
	echo "Using hard UDP routing, will route all udp packets over one NIC"
	setupUDPRouting
else
	echo "Will NOT use udp routing"
fi


default=eth0

#Default Route

setUpRoute priority_route[@] default_route_array[@]  "Default Route"

#Russendisco
#echo "Route RUSSENDISCO over WIRE"
#ip route add $RUSSENDISCO via $FRITZ_BOX_GATEWAY dev eth0

#League

setUpRoute priority_league[@] league[@] "League" udp

#Twitch

setUpRoute priority_twitch[@] twitch[@] "Twitch"

#Teamspeak

# setUpRoute priority_teamspeak[@] teamspeak[@] "Teamspeak"

sleep 1

echo "Setup:"

iptables -nvL -t mangle

printf "\n UDP Routing Table:\n\n"

ip route show table udp_routing_table

printf "\n Routing Table:\n\n"

ip route show

entrycount=$(ip route show table udp_routing_table | wc -l)

telegram-send "Routes changed:
 ${telegramString}
 UDP Table Entries: ${entrycount}
"

echo "Setup ended"