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.

ng_ether

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.

ng_netflow

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.

ng_tee

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.

netflow

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?

Avatar
Be" 30.10.2016 01:22
Es gibt wenig über FreeBSD NETGRAPH, etwas in Englisch und das meiste Kyrillisch. Schön, sowas dann doch in JTown zu finden. Vorzeigbar für Einsteiger, nice!

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

Post a comment

Verwandter Inhalt