Netflow und Netgraph
Die BNetzA fordert von Betreibern von Endkundenanschlüssen Statistiken über die Ausnutzung der Ressourcen pro Anschlußart. Dazu ist es nun notwendig, die Datenmengen pro Anschluß zu ermitteln und in aggregierter Form auszuwerten. Bei PPP Anschlüssen ist das einfach, weil dort die Volumendaten per Radius-Accounting ausgeleitet werden können. Ganz anders schaut es dagegen bei DHCP Zugängen aus. Hier muß mit Netflow an den Routern gezählt werden.
NAT als Problem
Während die Kunden mit offiziellen Adressen problemlos an den Außenübergängen des Netzes gezählt werden können, ist das bei den NAT-Kunden nicht so einfach. Ihre Pakete "verstecken" sich ja hinter den äußeren Adressen. Ein statistische Zuordnung der Datenvolumina zu dem Kundenprodukt ist also nicht möglich. Die Meßstelle muß vor das NAT-Gerät geschoben werden.
NAT macht nun eine FreeBSD-Kiste. Und die spricht direkt mit den Endkunden. Also muß an dem kundenseitigem Netzwerkinterface der Datenverkehr gezählt werden.
Netgraph
FreeBSD kann den kernelinternen Datenfluß zur Laufzeit umbauen. Dazu wird das System Netgraph benutzt. Dieses kapselt die Daten als Nachricht und reicht sie durch die Netgraph-Knoten (genannt Node).
Diese Knoten führen jeweils eine kleine und klar definierte Aufgabe aus. Sie benötigen dafür nur sehr wenig RAM und keinerlei Kontextswitche. Sie verhalten sie wie Unix-Programme, die per Pipes verbunden wurden. Die Verbindungsansätze der Knoten heißen Hooks und unterscheiden sich je nach Knotentyp.
Aus den verschiedenen Knotentypen lassen sich so komplexe Verarbeitungsstrukturen (ein Graph) zusammenstecken – so man überhaupt erst einmal verstanden hat, was diese Knotentypen überhaupt können und wie sie funktionieren. Zum Erstellen und verknüpfen von Knoten nutzt man das Werkzeug ngctl, auf das ich später eingehe.
Obwohl es an verständlicher Dokumentation mangelt, ist der Code klein genug, um ihn verstehend zu lesen.
Datenabgriff
Die erste Aufgabe besteht darin, überhaupt Daten zwischen dem "normalen" Kernel und dem Netflow-System auszutauschen.
Mit ng_ether erfolgt dieser Datenabgriff direkt an den Ethernet-Interfaces. Abgegriffen und injeziert werden Ethernet-Frames, keine IP-Pakete.
Diese Frames liegen immer kanonifiziert im IEEE 802.3 Format mit 14 Byte Header vor. VLAN-Frames sollte man deswegen an den jeweiligen VLAN-Interfaces abgreifen, andernfalls muß man manuell die VLAN Information entfernen.
Dieser Knotentyp hat drei Hooks, die folgendermaßen funktionieren:
- Ist der Anschuß lower belegt, werden die Frames, die netzwerkseitig am Ethernet-Interface hereinkommen, in Netgraph-Nachrichten umgewandelt. Die urspüngliche Verarbeitung der Frames wird verhindert: Das Interface ist für den Kernel stumm geschaltet.
- Netgraph-Nachrichten, die auf dem Anschluß lower hereinkommen, werden an die externe Netzwerktechnik übergeben: Die Frames werden gesendet.
- Ist der Anschluß lower dagegen nicht belegt, erfolgt die normale Verarbeitung von Frames innerhalb des Kernels.
- Ist der Anschluß orphan belegt, werden die Frames, die normalerweise nicht verarbeitet werden können, in Netgraph-Nachrichten umgewandelt.
- Ist der Anschluß upper belegt, werden die zum Versand vorbereiteten Frames, die aus dem Kernel kommen (von Anwendungen oder vom Routing), in Netgraph-Nachrichten umgewandelt. Der Versand der Frames wird verhindert: Das Interface ist netzseitig stumm geschaltet.
- Netgraph-Nachrichten, die auf dem Anschluß upper hereinkommen, werden an die interne Kernelverarbeitung übergeben: Die Frames werden verarbeitet. Ist das nicht möglich, fallen sie gegebenfalls bei orphan wieder raus.
- Ist der Anschluß upper dagegen nicht belegt, erfolgt die normale Versendung der Frames.
Accounting
Das Zählen von Daten wird vom ng_netflow Knoten ausgeführt.
Dieser Knoten kann viele Datenflüsse überwachen und hat deswegen auch einige zigtausend Eingänge. Die konsolidierten Daten werden am export-Anschluß als Netflow-Pakete ausgeleitet.
Die Daten werden als Ethernetframes (default) oder als IP-Pakete (setdlt) geparst. Da die Daten hier mit ng_ether abgegriffen werden, genügt der Default. Greift man die Daten z.B. in ipfw ab, muß man umstellen.
Grundsätzlich funktioniert der Knoten folgendermaßen:
- Nachrichten, die auf ifaceN hereinkommen, werden – so sie in Ordnung sind – als Ingress gezählt. Anschließend werden sie am korrespondierenden outN Anschluß wieder ausgesendet.
- Nachrichten, die auf outN hereinkommen, werden – so sie in Ordnung sind – als Egress gezählt. Anschließend werden sie am korrespondierenden ifaceN Anschluß wieder ausgesendet.
Da die Daten (als Netgraph-Nachrichten) den Netflow-Knoten unverändert durchlaufen, spricht erst einmal nichts dagegen, diesen direkt an den Ethernet-Knoten anzuhängen. Dazu wird iface0 mit lower und out0 mit upper verbunden und Netflow für beide Richtungen aktiviert.
Unglücklicherweise gibt es erhebliche Probleme mit Frames, die von Netflow als "unverständlich" angesehen werden. Es ist zwar vorgesehen, daß nur bei echten Fehlern die Nachrichten verloren gehen können, aber IPv6 und CARP scheinen das Netflow-Accounting nicht komplett zu überleben.
Datenduplizierung
Im Sinne der Funktionalität sollten die abgegriffenen Frames so schnell wie möglich wieder der normalen Verarbeitung zurückgegeben werden.
Dies leistet der Knoten ng_tee. Er tut folgendes:
- Nachrichten, die auf dem Anschluß left hereinkommen, werden an die Anschlüsse right und left2right ausgesendet, so diese verbunden sind. Sind beide verbunden, wird die Nachricht intern dupliziert.
- Nachrichten, die auf dem Anschluß right hereinkommen, werden an die Anschlüsse left und right2left ausgesendet, so diese verbunden sind. Sind beide verbunden, wird die Nachricht intern dupliziert.
- Nachrichten, die auf dem Anschluß left2right hereinkommen, werden an den Anschluß right ausgesendet, so dieser verbunden ist.
- Nachrichten, die auf dem Anschluß right2left hereinkommen, werden an den Anschluß left ausgesendet, so dieser verbunden ist.
Alles zusammen
Nun kann man alles zusammenstecken.
Die Daten werden von ng_ether abgegriffen und nach einem kurzen Schwenk über ng_tee wieder eingespeist. Dabei werden pro Datenrichtung Kopien angefertigt, die auf zwei verschiedenen Intferfaces von ng_netflow gezählt werden. Nach der Zählung können die Daten verworfen werden, die out Anschlüsse bleiben also unbelegt.
Der Netflow-Export wird dann mittels ng_ksocket in UDP eingepackt und dem externen Netflow-Collector zugeführt. Würde man auf der gleichen Maschine die Netflow-Auswertung betreiben, könnte man die Netflow-Daten gleich intern zustellen.
Scripting
Mittels ngctl wird nun der Aufbau real vorgenommen. Dieses Programm hat seine eigene Syntax und verarbeitet üblicherweise vorgefertigte Scripte.
- Die Adressierung der Knoten erfolgt auf drei Weisen: Entweder man kennt den expliziten Namen des Knotens, oder man folgt den Hooks/Anschlüssen von einem explizit benannten Knoten aus. So bedeutet der PfadX:a.b.c, daß der Knoten X gesucht wird und von dort aus der Knoten genommen wird, der am Anschluß a von X hängt. Von diesem Knoten geht es weiter über b zum dritten Knoten. Der finale und vierte Knoten wird dann von dort aus über c erreicht. Abschließend kann man statt des expliziten Names auch die interene ID verwenden: [id].
- mkpeer X Y a b erzeugt einen neuen Knoten vom Typ Y und verbindet dessen Anschluß b mit den Anschluß a des schon vorhandenen Knotens X.
- connect X y a b verbindet den Anschluß a des Knotens X mit den Anschluß b des Knotens X:y.
- name X Y benennt den durch den X (ein längerer Pfad) herausgesuchten Knoten mit dem neuen Namen Y.
- Mit rmhook und shutdown kann man Verbindungen und Knoten löschen.
- Weitere Befehle existieren, um die Knoten zu konfigurieren, Reports zu erzeugen und Nachrichten zu einzuspeisen. Diese Befehle sind aber je nach Knotentyp verschieden und von Fall zu Fall nachzulesen.
Interessanterweise ist es nicht möglich, einen neuen Knoten für sich allein zu erzeugen. Jeder neue Knoten wird nur als Erweiterung des schon existierenden Graphen erzeugt.
Zuerst erzeugt man zum existierenden ng_ether Knoten einen neuen ng_tee Knoten und schließt schnellstmöglich die Schleife. Anderenfalls ist das betreffende Interface stillgelegt.
# ngctl + mkpeer vlan123: tee lower left + show vlan123: Name: vlan123 Type: ether ID: 00000009 Num hooks: 1 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- lower <unnamed> tee 000494fe left + connect vlan123: lower upper right + show vlan123: Name: vlan123 Type: ether ID: 00000009 Num hooks: 2 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- upper <unnamed> tee 000494fe right lower <unnamed> tee 000494fe left + show vlan123:upper Name: <unnamed> Type: tee ID: 000494fe Num hooks: 2 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- right vlan123 ether 00000009 upper left vlan123 ether 00000009 lower + show [000494fe]: Name: <unnamed> Type: tee ID: 000494fe Num hooks: 2 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- right vlan123 ether 00000009 upper left vlan123 ether 00000009 lower
Es ist schön zu sehen, wie sich der Graph aufbaut und der neu erzeugte Knoten auf unterschiedliche Weise angesprochen werden kann.
Als nächstes wird das ng_netflow angehängt. Da nun öfter auf diesen Knoten zugegriffen werden soll, bekommt er einen Namen.
+ mkpeer vlan123:lower netflow left2right iface0 + show vlan123:lower Name: <unnamed> Type: tee ID: 000494fe Num hooks: 3 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- left2right <unnamed> netflow 000495a7 iface0 right vlan123 ether 00000009 upper left vlan123 ether 00000009 lower + name vlan123:lower.left2right nf123 + show vlan123:lower Name: <unnamed> Type: tee ID: 000494fe Num hooks: 3 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- left2right nf123 netflow 000495a7 iface0 right vlan123 ether 00000009 upper left vlan123 ether 00000009 lower
Mit dem Namen ist es nun einfacher die zweite Verbinung herszustellen.
+ connect nf123: nf123:iface0 iface1 right2left + show vlan123:lower Name: <unnamed> Type: tee ID: 000494fe Num hooks: 4 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- right2left nf123 netflow 000495a7 iface1 left2right nf123 netflow 000495a7 iface0 right vlan123 ether 00000009 upper left vlan123 ether 00000009 lower + show nf123: Name: nf123 Type: netflow ID: 000495a7 Num hooks: 2 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- iface1 <unnamed> tee 000494fe right2left iface0 <unnamed> tee 000494fe left2right
Bleibt nun noch, die gesammelten Daten auch auszuleiten:
+ mkpeer nf123: ksocket export inet/dgram/udp + show nf123: Name: nf123 Type: netflow ID: 000495a7 Num hooks: 3 Local hook Peer name Peer type Peer ID Peer hook ---------- --------- --------- ------- --------- export <unnamed> ksocket 00049609 inet/dgram/udp iface1 <unnamed> tee 000494fe right2left iface0 <unnamed> tee 000494fe left2right + msg nf123:export connect inet/198.51.100.17:9991
Zum Abschluß noch der Test, ob auch gezählt wird:
# flowctl nf123 show vlan125 203.0.113.5 vlan123 192.0.2.110 17 98af 1ba7 13 vlan125 203.0.113.5 vlan123 192.0.2.198 17 13c4 13c4 25 ...
Es funktioniert. Auch im Netflow-Collector kommt etwas an.
Die kleinen Schwierigkeiten
Der erste Versuch, dieses Setup aufzubauen, scheitert an einer einfachen Fehlermeldung:
ngctl: send msg: No such file or directory
Aber dieser Fehler ist kein Bug sondern schlicht der Tatsache geschuldet, daß man das Kernelmodul ng_ether vorher laden muß.
$ tail -2 /boot/loader.conf ng_netflow_load="YES" ng_ether_load="YES"
Problematischer ist es, die Funktionalität dauerhaft zu verankern. Man kann einen eigenen Service bauen oder sich an das Interface hängen.
Mir persönlich gefällt das mit dem Interface besser. Aber wie funktioniert start_if?
# cat /etc/network.subr ... ifn_start() { local ifn cfg ifn="$1" cfg=1 [ -z "$ifn" ] && err 1 "ifn_start called without an interface" ifscript_up ${ifn} && cfg=0 ifconfig_up ${ifn} && cfg=0 ipv4_up ${ifn} && cfg=0 ipx_up ${ifn} && cfg=0 childif_create ${ifn} && cfg=0 return $cfg } ... ifscript_up() { if [ -r /etc/start_if.$1 ]; then . /etc/start_if.$1 return 0 fi return 1 } ...
Kurz gesagt wird das Script /etc/start_if.NAME vor der Interface-Konfiguration ausgeführt, wenn es vorhanden ist. Es ersetzt nicht die bestehende Konfiguration, es wird zusätzlich eingebunden. Analog funktioniert das Script /etc/stop_if.NAME.
Die Scripte sehen dann so aus:
#! /bin/sh /usr/sbin/ngctl -f- <<END mkpeer vlan123: tee lower left connect vlan123: lower upper right mkpeer vlan123:lower netflow left2right iface0 name vlan123:lower.left2right nf123 connect nf123: nf123:iface0 iface1 right2left mkpeer nf123: ksocket export inet/dgram/udp msg nf123:export connect inet/198.51.100.17:9991 END
Beim Wegräumen sollte man nicht vergessen den ng_tee Knoten erst sauber abzutrennen, sonst verknüpft er den ng_ether Knoten mit sich selbst. Die ng_ksocket und ng_tee Knoten löschen sich selbst, sobald sie keine Verbindungen mehr haben. Es genügt nach der Abtrennung also, den ng_netflow Knoten zu entfernen.
#! /bin/sh /usr/sbin/ngctl -f- <<END rmhook vlan123: lower rmhook vlan123: upper shutdown nf123: END
Und noch eine kleine Quizfrage zum Schluß: Was passiert, wenn man das start_if Script mit einem beherzten exit vorzeitig beendet?
Ich hab eigentlich grad nach "netgraph vlan trunk" gesuchmaschint und Du warst derwegen der 6. Treffer von oben^^
Ein schönes langes Wochenende wünsch ich! :)
1 Kommentare