Quick&Dirty Hausnetz-Erkennung mittels LLDP (Link Layer Discovery Protocol) 0x88cc und TOTP Absicherung

reinhard@finalmedia.de Sun 04 Dec 2022 11:37:13 AM CET

Konzeptbeschreibung

Zu erreichendes Ziel: Du hast netzwerkfähige Geräte, auf denen ein Linux Kernel läuft und du möchtest dort in Programmen gerne automatisiert erkennen, ob du in deinem Hausnetz oder einem fremden Netz bist. Du möchtest diese Erkennung durchführen können, bevor du deinem Gerät irgendeine IP zugewiesen hast, also auch bevor es evtl. einen dhcp-client startet. Dies dient dir z.B. dazu, das Gerät in einen speziellen Diagnose-Modus zu bringen. Du willst dabei zusätzliche Sicherheitsmerkmale, sodass das nicht jeder Einfach nachbauen kann und sich als dein Heimnetzwerk ausgibt.

Lösungsansatz: Auf den Endgeräten nutzen wir "ladvdc" als Client für das LLDP (LinkLayerDiscoveryProtocol) 802.1AB. Wir verwenden dabei einen Zufallshostnamen und ergänzen diesen um eine nummerische Sequenz, die sich alle 30 Sekunden ändert und aus einem shared secret zeitbasiert erzeugt wird (TOTP).

Master-Host

Der Master-Host ist z.B. eine kleine eigenständige VM in deinem Netzwerk. Er hat nur diese eine Aufgabe, ein LLDP Server zu sein und dabei einen speziellen Hostnamen zu tragen ;)

Erzeuge einen Zufallsnamen. In unserem Beispiel: 7f029c2f7a46d2453620.

tr -dc "0-9a-f" < /dev/urandom | head -c 20

Wir nutzen auf unserem Master-System diesen hostnamen. Er dient der Erkennung, ob wir uns im Hausnetz befinden oder nicht, weil wir einfach davon ausgehen, dass dieser Name dann weltweit eindeutig ist. Beachte: Das ist kein Sicherheitsfeature! Das kommt erst mit dem TOTP in den nächsten Schritten. Das ganze Konzept ist ein einfachster Hack zu Hausnetz-Erkennung, der mit Standardprotokollen und Bordmitteln auskommt.

Nun installieren wir auf unserem Master-System einen LLDP daemon, der in periodischen Abständen automatisch über alle Interfaces des Hosts diese Information via LLDP versendet.

Unser Host braucht dazu im übrigen keine IP-Adresse. Er muss also nicht über TCP/IP oder irgendeine andere Art und Weise auf Layer 3 im Netzwerk erreichbar sein. Es genügt ein aktiver Carrier und Layer 2 mit LLDP.


apt-get install ladvd
systemctl enable ladvd
hostname 7f029c2f7a46d2453620
systemctl restart ladvd

Um den Hostname persistent zu setzen, einfach


echo 7f029c2f7a46d2453620 > /etc/hostname

Home-Check auf den Client-Hosts


ladvdc -f -L | grep -F -q "System Name: 7f029c2f7a46d2453620" && echo i_am_at_home || echo foreign_net

Beispiel: Dauerhafter Testloop, der das Terminal rot oder grün färbt, je nachdem ob man sich im Hausnetzt befindet oder nicht:


while true
do
ladvdc -L -f | grep -F -q "System Name: 7f029c2f7a46d2453620" && tput setab 2 || tput setab 1
clear
sleep 4
done

Debugging

Zur Diagnose kannst du in deinem Netzwerk auf jedem host tcpdump verwenden:

tcpdump -i eth0 -e ether proto 0x88cc

Einschränkungen

Aufgrund des periodischen Broadcasts, kann es z.B. 30-60 Sekunden dauern, bis die Netzänderung erkannt wird. Das musst du berücksichtigen.

Wenn du Testweise mit dem Hostname spielst und diesen veränderst, musst du jedes Mal systemctl restart ladvd durchführen, damit ladvd den geänderten Hostnamen erkennt.

Upgrade zur Sicherheit

Nun hacken wir weiter. Den Zufalls-Hostnamen ergänzen wir einfach um einen TOTP, also 6 stellige zeitbasierte PIN, die von einem shared Secret abhängt. Bekannt ist das Verfahren bei der 2FA.

Damit ändern wir den Hostnamen des Master-Servers also alle 30 Sekunden und starten auch den LLDP daemon ladvd in diesem Intervall neu. Mehr Magic ist es nicht.

Wenn also dein client über den zuvor ausgetauschen shared secret key verfügt, dann wird er auch den richtigen Hostname erkennen. Durch die gewählte Window-Size sind auch die 3 vorherige oder nachfolgende Hostnames mit TOTP Suffix gültig, da ja nie eine 100% synchrone Zeit auf Host und den Endgeräten vorausgesetzt wird. Beachte aber, dass zwingend eine funktioniernde und korrekte RTC auf deinen Endgeräten vorausgesetzt wird.

Zur Umsetzung: Nehmen wir an, wir definieren zu unserem Zufallshostnamen noch ein zusätzliches shared_secret 090ed23849843984ed5544. Das kennen nur Hostseite und Clientseite, weil es dort hinterlegt wurde. Die Hostseite MASTER sieht dann so aus:


# +++ MASTER +++
apt-get install oathtool ladvd
systemctl enable ladvd
systemctl start ladvd

#!/bin/sh
while true
do
hostname="7f029c2f7a46d2453620$(oathtool -w 0 --totp 090ed23849843984ed5544)"
hostname "$hostname" && echo "current hostname: $hostname"
systemctl restart ladvd
sleep 30
done

Dann wäre der Check auf der Clientseite so realisierbar (z.B. mit einem TOTP window von 2)


# +++ CLIENT +++
apt-get install oathtool ladvd
systemctl enable ladvd
systemctl start ladvd

#!/bin/sh
while true
do
ladvdc -L -f | grep -q -F -f <(oathtool -w 2 -N "last minute" --totp 090ed23849843984ed5544 | \
sed 's/^/System Name: 7f029c2f7a46d2453620/g') && echo "you are at home" || echo "foreign net"
sleep 4
done

Alternativ zu Demozwecken mit tput im client das terminal rot oder grün einfärben:


#!/bin/bash
while true
do
ladvdc -L -f | grep -q -F -f <(oathtool -w 2 -N "last minute" --totp 090ed23849843984ed5544 | \
sed 's/^/System Name: 7f029c2f7a46d2453620/g') && tput setab 2 || tput setab 1
clear
sleep 4
done

Bedenke, dass du in dem Script auch ggf. mit einer der nachfolgenden Environment-Variablen arbeiten musst - abhängigen von deiner Systemzeit und Locale.


export TZ="Europe/Berlin"
export TZ="UTC"

Nachtrag

Hier noch eine Shell-basierte einfache TOTP Implementierung. Autor ist Rich Felker, unter SPDX-License-Identifier: MIT. Damit wäre oathtool ersetzbar. In diesem Fall wäre das Shell Script nur von openssl abhängig. Allerdings lässt sich damit kein TOTP Time Window definieren. Hier muss t dann noch angepasst werden.


#!/bin/sh
#
# totp.sh - Shell script implementation of TOTP (RFC 6238)
#
# Copyright © 2020 Rich Felker
# Licensed under standard MIT license
#
# SPDX-License-Identifier: MIT
#
# Depends on a base32 utility, date command supporting %s format,
# and openssl command line utility. Otherwise portable sh.
#
# Usage: totp.sh < secretfile
#
# where secretfile contains the base32-encoded secret.
#

t=$(($(date +%s)/30))
k="$(tr 0189a-z OLBGA-Z | base32 -d | od -v -An -tx1 | tr -d ' \n')"

h=$(
printf '%b' $(printf '\\x%.2x' $(
i=0; while test $i -lt 8 ; do
echo $(((t>>(56-8*i))&0xff))
i=$((i+1))
done
)) | openssl dgst -sha1 -mac HMAC -macopt hexkey:"$k" -r | cut -d' ' -f1
)

o=$((0x${h#??????????????????????????????????????}&0xf))
while test $o -gt 0 ; do
h=${h#??}00
o=$((o-1))
done

h=${h%????????????????????????????????}
h=$(((0x$h & 0x7fffffff)%1000000))

printf '%.6d\n' "$h"