Advanced search


Mit der heutigen Veröffentlichung von RFC 6540 ist IPv6 für alles, was sich Internet nennen will, verpflichtend.

Insbesondere sollten nun darauf geachtet und eingefordert werden, daß

  • Neue Netze und Anschlüsse IPv6 bereitstellen.
  • Bei Umbauten bestehender Netze IPv6 nachgerüstet wird.
  • Die IPv6 Versorgung mindestens genauso gut ist, wie die IPv4 Versorgung.
  • Die Versorgung von Netzen vorzugsweise mit IPv6 und IPv4 stattfindet, wobei die IPv4 Versorgung auch eingeschränkt (via NAT) oder ganz entfallen kann.
  • Hersteller ihre Produkte IPv6-fähig ausliefern sollen, sobald das überhaupt technisch realisierbar ist.

Bei meinem Arbeitgeber IKS versorgen wir die Kunden standardmäßig seit mehreren Jahren mit IPv6. Wir betrachten IPv6 als Grundversorgung. Wir freuen uns, daß diese Ansicht seit heute weltweit empfohlen wird.

Während einer Schulung zum Thema IPv6 kam die Frage auf, ob man IPv6 nicht einfach ignorieren könne. Das kann man nicht: Besonders Übergangstechniken wie 6to4 und Teredo bohren Löcher in die (Firmen)-Firewall, die man nicht mehr gestopft bekommt. Und diese Löcher sind Einfallstore für beliebige Dritte.

Wenn man kein IPv6 im lokalen Netz anbietet, suchen sich die meisten Rechner IPv6 aus anderen Quellen. Dabei verwenden Sie ausgefuchste Tunneltechniken, wie Teredo.

Löcher in der Firewall

Diese Techniken bohren geschickt Löcher in die Firewall und gestatten dann dem so versorgten Rechner eine bidirektionale Kommunikation. Dabei analysieren sie die Art der Firewall (stateful oder nicht) und sorgen für eine möglichst effiziente Umgehung.

Schaut man sich eine IPv6-Teredo-Adresse im Detail an, so ist sie ziemlich einfach aufgebaut.

teredo-address

Die einzelnen Komponenten sind praktisch alle bekannt:

  • Der Teredo-Prefix ist konstant 2001:0000
  • Die IPv4 Adresse des Teredo-Servers ist praktisch immer die des Microsoft-Servers.
  • Die Flags sind immer nur 0000 oder 8000, je nach Firewalltyp. Einige Versionen von Windows benutzen die undefinierten 12 Bit dieses Feldes als Scan-Schutz.
  • Der externe Port ist ein unbekannter 16bit Wert, der nach dem NAT der Firmen-Firewall extern sichtbar war. Das Verschleiern erfolgt durch Bitinversion.
  • Die exterene IPv4 Adresse ist die externe NAT Adresse der Firmen-Firewall. Das Verschleiern erfolgt durch Bitinversion.

Freier Zugriff von außen

Von der ganzen Adresse sind also praktisch nur die 12 bis 16bit des UDP Ports zu erraten, um einen Rechner hinter einer Firmen-Firewall ansprechen zu können. Kommen die 12bit aus dem Flag Feld hinzu, sind es 24 bis 30bit. Für viele noch existierende Windows-Versionen in den Firmen ist das Flag Feld aber schlicht konstant 0000.

Hat man eine größere Firma vor sich, so ist es nicht notwendig, einen konkreten Rechner zu erwischen. Es genügt, irgend einen zu konnektieren, um erfolgreich in die Firma einzubrechen. Hier spielt das Geburtstagsparadoxon die tragende Rolle: Je mehr Rechner die Firma hat, desto wahrscheinlicher ist es, mit einem zufälligen UDP Port einen davon ansprechen zu können.

Der Firewall-Admin hat keine Chance, diesen Datenverkehr zu unterbinden, da die Gegenstellen nicht konstant bei Microsoft liegen, sondern beliebige (dank anycast) Teredo-Relays weltweit sein können.

Was tun?

Als Firewall-Admin kann man eigentlich nur sämtliche irgendwie bekannte Teredo-Server blockieren, um den Verbindungsaufbau zu stören. Es ist jedoch günstiger, Teredo komplett zu deaktivieren.

Die Defaulteinstellung von Windows-Teredo schaltet sich in Active-Directory Umgebung inaktiv. Der Nutzer kann das Protokoll aber mit einem beherzten netsh interface ipv6 set teredo enterpriseclient trotzdem aktivieren.

Will man Teredo wirklich dauerhaft deaktivieren, ohne auf jedem Client per Registry oder Gruppenrichtlinie einzugreifen, dann gibt es nur den Königsweg: IPv6 ausrollen.

Liegt im lokalen Netz IPv6 an, werden sämtliche Übergangstechniken auf egal welchem Client deaktiviert. Und dann hat man wieder die Möglichkeit die Firewall das tun zu lassen, was sie soll: Eine Policy durchzusetzen.

Cisco responded to the IPv4 shortage with a clever approach: Extended PAT. But it has drawbacks when used in real 

From NAT to CGN

Classical NAT does use a different port number on the global site for each connection. This allows to map other external sources to reach the same internal host. If such additional mappings are generally allowed it is called "Cone NAT". Otherwise "Restricted NAT". Even in "Restricted NAT", the mapping can be allowed if approbriate protocol helpers are in action.

Extended PAT does allow a port number of a NAT global address to be reused for multiple connections, i.e. different internal clients, as long as they try to reach different external addresses. So you can map much more than 60000 connections to a single public address. For CGN that means you can add an order of magnitude more customers to the same pool of public addresses than before.

Of course extended PAT does not allow any kind of additional mappings. Cisco describes clearly which protocol helpers will not work anymore.On the other hand, the port mappings become more sticky, so there is a much greater chance to get the same global port as used internally. Using round robin pools help a lot to further increase this chance. A lot of port-sensible protocols does benefit from this port stickyness.

Practical Issues

After running extended PAT for a few days, the system was going to collaps today. Memory is running short:

asa-mem-shortage-xpat

The config was changed two and a half days ago and started to consume memory quickly. After putting more and more customers on the platform the memory run out this afternoon.

Cisco troubleshooting hints does not help much: "During normal operation, the free memory on the ASA should change very little, if at all. Typically, the only time you should run low on memory is if you are under attack and hundreds of thousands of connections go through the ASA."

There are not an unusual amount of connections. But even "show memory" and "show blocks" does not reveal more than the usual memory footprint. A memory leak?

No, the extened NAT does require more (and dynamically more) memory, because it depends on the number of possible ports (number of IPs in the round robin pool mulitiplied by the number of ports usable) times the number of used destinations. That's a huge number.

So reverting the "extended" and "round-robin" entries results in instant solution ...The free memory jumps back to the typical value.

Problem solved.

Die Prüfsummenberechnung der grundlegenden IP Protokolle benutzt das Einerkomplement. Wenn man, wie ich, gern an Paketen rumspielt, sollte man die Prüfsummenberechnung beherrschen.

Ein Ruf aus alten Tagen

Nach einem längeren Studium von Online-Quellen, die durch die Bank unergiebig waren, verwies man mich auf TAoCP Band 2. Verdammt. Das steht vor mir. Leider ist es diesmal nicht mehr als eine Sammlung von Stichpunkten. Knuth benutzt für MIX eine Darstellung mit getrenntem Vorzeichenbit.

Richtig erklärt (lesen!) wird das Einerkomplement von den Entwicklerns und Nutzern der Univac.

Kurz gesagt bietet das Einerkomplement eine einfache Ausführung von Negation (bitweises nicht), Addition (Volladdierer), Subtraktion (modifizierter Volladdierer), Multiplikation (nach links schieben) und Division (nach rechts schieben) vorzeichenbehafteter Zahlen. Diese Vorteile werden durch ein umlaufendes Carry, sowie zwei Darstellungen der Null (+0 und -0) erkauft.

Beim wesentlich bekannterem Zweierkomplement entfallen die Probleme mit der doppelten Null und dem umlaufenden Carry. Im Gegenzug scheitert die Multiplikation und Division mit negativen Zahlen und man bekommte eine Zahl für die es kein Inverses gibt.

Altlasten oder Designziel?

Die Auswahl des Einerkomplements für die Prüfsummen der IP-Protokolle gründet sich auf zwei Eigenschaften:

  • Es bildet eine abelsche Gruppe: Man kann beliebig umsortieren, vertauschen, umgruppieren und es gibt eine Umkehroperation. Man kann subtrahieren.Veränderungen an dem IP-Paket sind so ohne komplexe Neuberechnung in Änderungen an der Prüfsumme umrechenbar. Die Prüfsumme wird zum Abschluß negiert und ist Bestandteil der Berechnung, so daß der Empfänger bei korrekten Paketen eine -0 erhalten muß.
  • Invarianz gegen Endianess: Durch das umlaufende Carry ist es egal, ob die Zahlen auf Big oder Little Endian Systemen verarbeitet werden. Solange der Überlauf die Bits in der gleichen Reihenfolge durchläuft, ist das Ergebnis immer das Gleiche.

Beide Eigenschaften zusammen gestatten den Einsatz größerer Berechnungseinheiten, ja sogar massiv parallele Berechnungen durch Vektoroperationen. Es ist so möglich extrem schnell und einfach die Berechnungen vorzunehmen.

Desweiteren kann die Prüfsummenberechnung auf praktisch allen damals existierenden Plattformen ohne Konvertierungsaufwand durchgeführt werden. Was erstmal nicht geht, sind middle-endian Systeme wie die PDP11. Die Begrenzung der Prüfsumme auf 16bit gestattet auch der PDP11 mit einer konsitenten Endianess und damit ohne Konvertierungsaufwand zu arbeiten.

Besonders relevant ist diese Invarianz, da Netzwerke big-endian (lesen!) sind, während viele Prozessoren little-endian (lesen!) einsetzen.

Die Existenz einer +0 und einer -0 gestattet es, zu signalisieren, ob eine Prüfsumme berechnet wurde oder nicht. So kann man die Prüfsummenberechnung aktiv auslassen um UDP-Pakete auch mit leichten Fehlern durchzulassen. Dies ist beispielsweise bei VoIP nützlich, wenn Bitfehler weniger Schaden anrichten als komplett verlorene Pakete. +0 signalisiert hier die Abwesenheit von Prüfungen.

Interessant ist die Tatsache, daß man zum Zeitpunkt der UDP-Norm noch beide Nullen als mögliche Ergebnisse der Rechnung in Erwägung zog und explizit behandelt. Bei TCP und IP gibt es keine Sonderbedeutung von ±0. Man hielt es nicht für notwendig, auf solche Besonderheiten im Standard einzugehen.

Erst mit der Zeit hat man erst erkannt, daß verschiedene Tricks der inkrementellen Berechnung das Nullproblem gar nicht erkannt hatten. Der Standardcode kann gar keine +0 generiern. Warum das so ist, erkläre ich weiter unten. Wie sehr die beiden Nullen in Vergessenheit geraten sind, offenbart Code, der die legitim auftretende -0 als Fehlerkennung verwendet.

Andererseits kann man die Berechnung der Prüfsumme der Netzkartenhardware überlassen. Beim "Checksum Offloading" muß entschieden werden, ob die Hardware die Prüfsumme berechnen soll oder nicht. Auch hier signalisiert nur die +0 im Checksum-Feld den Wunsch nach Berechnung. Eine zwangsweise Neuberechnung der Prüfsumme würde die Fehlererkennung (insbesondere beim Routing) ad absurdum führen.

Betrachtet man die Alternativen, klären sich weitere Vorbehalte:

  • CRC Prüfsummen sind zwar sehr gute Fehlerdetektoren, sind aber keine Gruppen. Dadurch erfordern aber selbst kleine Änderungen vollständige Neuberechnungen. Das TTL Feld könnte dann nicht Bestandteil der Prüfsumme sein. Auch NAT hätte es vermutlich nicht (so schnell) gegeben.
  • Das Zweierkomplement ist heute die dominierende Implementierung der Binärarithmetik. Sie ist aber nicht endianess invariant. Zur Prüfsummenberechnung wäre also stets eine Konvertierung der Daten notwendig.
  • Einfaches XOR erfüllt ebenfalls alle Anforderungen, ist aber deutlich schwächer bei der Fehlerdetektion. Da es damals rechenmäßig kaum einen Unterschied zwischen Einerkomplement und XOR gab, wurde das stärkere Verfahren bevorzugt.

Das magische "End Around Carry"

Zunächst sind noch einige Überlegungen notwendig, um sich elementare Eigenschaften des Einerkomplements zu vergegenwärtigen. Die Additionsstufe des Einerkomplements entspricht der des Zweierkomplements mit dem Unterschied, daß das Carry rückgekoppelt ist. Beim Zweierkomplement wird das Carry aus einem extra CPU-Flag eingeschoben und nach der Addition wieder dort verstaut. Dieses CPU-Flag gibt es bei Einerkomplement-CPUs schlicht nicht.

einerkomplement-adder

Das umlaufende Carry kann jedoch nicht zu einer Endlosschleife führen. Jeder Volladdierer muß (das Carry sei c) nur die Kombinationen 0+0+c = 0c, 1+1+c=1c sowie den 1+0+c=c(~c) bzw. 0+1+c=c(~c) bearbeiten. In den ersten beiden Fälle wird das ausgehende Carry Bit zwangsweise auf einen festen Wert gesetzt. Die Mischfälle sind interessanter, aber hier wird das Carry unverändert durchgereicht. Es gibt keinen Fall, wo die Rückkopplung des Carry zu einem Widerspruch, also einem flappenden Bit führen kann. Die Einerkomplementaddition ist also trotz umlaufenden Carry ist also binnen einer Runde stabil.

Von besonderem Interesse ist eine Eingabe, bei der nur die Mischfälle 1+0 oder 0+1 auftreten. In diesen Fällen gibt es keinen Volladdierer, der den Wert des Carry festlegen könnte. Ist das umlaufende Carry eins, so lautet das Ergebnis der Addition +0. Ist das umlaufende Carry null, so lautet das Ergebnis stattdessen -0. Beides sind valide Ergebnisse, die von Architektur, Algorithmus und Kontext der Berechnung abhängen. Dieses umlaufende Carry und die Existenz der zwei Nullwerte sind also unmittelbar verwand. Ja, sie sind der gleiche Effekt. Die ±0 ist die explizite Darstellung des umlaufenden Carry.

Andererseits ist es bei der Implementierung der Addition auf Zweierkomplement-Architekturen nicht möglich, Bits zu verlieren. Eventuelle Überläufe werden später hinzuaddiert und nicht rückgekoppelt. Einmal gesetzte Bits (z.B. die IP Version) leaken bis zum Ergebnis durch. Die Algorithmen zur Prüfsummenberechnung auf Zweierkomplementsysteme haben so durch die normative Kraft der Faktischen die Generierung der +0 verhindert.

Summiert man die Carries durch 32bit Arithmetik in den höherwertigen Bits auf, so kann man die Einerkomplement Rechnungen auch im Zweierkomplement ausführen. Alle Carry-Bits, die man vergessen hatte, liegen in den höherwertigen 16bit. Wieviele solche Additionen kann man ausführen, ohne daß einem der Carry-Speicher selbst überläuft? Im schlimmsten Fall hat jede Addition ein unverarbeitetes Carry. Man kann also 216-1 Additionen ausführen. Dies entspricht einem Speicherbereich von 217-2 Byte. also 130 kByte. Für IP-Pakete ist das unbedenklich, da diese maximal halb so groß werden dürfen. Problematisch bei IPv6 sind paketübereifende Payloads, deren Prüfsummen länger werden dürfen, als ein Paket an Daten faßt. Man kann also mit 32bit Arithmetik bedenkenlos paketweise rechen.

Eigener Code

Jetzt sind die Voraussetzungen geschaffen, eignen Einerkomplement-Code zu schreiben. Das steht dann aber in einem extra Artikel.

In order to terminate a large number of broadband customers on an LNS, MPD on FreeBSD was recommended to me. There seems to exists a group of Russian operators using this software for really large deployments. Alternativly OpenL2TP could be used. However  OpenL2TP does handle all the PPP sessions in user space, while MPD relies on NetGraph.

First contact

The setup is simple, even through the documentation of MPD lacks expressivness. What a command does, is documented at a single point at most. If you do not know where to look, you're stuck.

Additionaly MPD strictly distinguishes between the layers of operation. So you can only configure something at the place (and find documentation) in which this setting is relevant. I.e. vanJacobson compression occurs in the IPCP part of (PPP) bundles. Protocol field compression as part of the PPP session is negotiated in the link layer, which is responsible for setting up PPP. Therefore, all such settings are on this layer—even the authentication protocols to be used by LCP.

The configuration file is strictly linear. Each command can change the current layer. Thus the following commands might be executed in a different context. I can recommend to create the configuration manually (online) and enable "log +console".

Disillusionment

First experiments turned into pure horror.

lasttest-mpd-patch1

After a quite funny start "no more threads" and "Fatal message queue overflow!" messages were logged. The MPD crashed. But it crashed controlled, so finished all sessions before terminating itself.

The event handling for radius requests caused the problems. MPD implemented event queuing by reading and writing an internal pipe. This pipe correlated to a ring buffer containing the real event information. In order to prevent blocking writes to the pipe, no more bytes (dummy values for "there's an event") should be written beside those that fit into the kernel buffer.

The obvious patch is to increase the pipe size and extend the ring buffer. But I did not make any success: it got a segfault.

Since each event on each layer generates several further events, a event flood can happen. But the single thread for event processing must not block! If any potentional blocking may occur, a new thread is spawned and handled asynchonly. So RADIUS authenication and accounting exceeded the limit of 5000 threads per process. On the RADIUS server, the load looked similar.

A carrier experienced during my tests, how badly RADIUS servers may react. The commercial LAC queries the RADIUS on every login attempt to determine the appropriate LNS for the realm. During a simultanious drop of all sessions, he was overwhelmed by so many requests that his radius servers—specifically the database backend—gave up and the entire DSL infrastructure quit the service. Meanwhile, the realm to LNS mapping is a static configuration.

Slowly, more slowly

It is always better to be kind to its suppliers, particularly to the radius servers. So I droppedthe entire event handling of the MPD and wrote it from scratch using mesg_queue. The required library was already used by MPD.

A distinction is made for "sequential" and "parallel" events. Sequential events run under the global lock the MPD. They keep serialized in the same order in which they were generated. Thus, the layers do not get confused, and event processing can rely on the previous actions. Parallel processing is used for external communication. They are handeled by a fixed number of active worker threads.

The length of both queues determine MPD's overload (Load = 2×queue_len(serial) + 10×queue_len(parallel)). An overloaded MPD does refuse new connections.

Well equipped, I was full of hope and suddenly disappointed. The kernel paniced: deep, deep inside the NetGraph code.

bsd-lasttest-panic-1

And not just once, but also at a completely different place.

bsd-lasttest-panic-2

Is the netgraph code broken? Does the kernel has problems with rapid creation and deletion of interfaces? Do I work too fast?

Even RADIUS accounting created problems, because a test equipment managed to connect, drop, and reconnect three times within one second. My SQL unique constraints refused to accept such data. The MPD is definitely too fast! I added a pause of 20ms (configurable) between the processing of two serial events.

My thread for the serial processing runs separately from the main thread, which was not the case in the original code. I put a separate lock to each NetGraph call. This prevents concurrent access on NetGraph sockets.

So the system started to behave stable.

Stress testing

I choosed an OpenL2TP on Linux for generating lots of sessions:

  • Open a random number of L2TP tunnels.
  • Create up to 9000 PPP sessions (round robin over the L2TP tunnels) as fast as possible.
  • Create new PPP sessions at a limited rate up to 9000 sessions are established.
  • Drop and recreate a random selection of 300 sesssion to simulate a living broadband environment.
  • Stay a while and finally drop the sessions one after the other.

A typical test result is given below: "Sessions" are generated by OpenL2TP/pppd, each of them make MPD "Links" within the tunnels, and negotiate the PPP-"Bundles". Queue lengths are plotted too.

mpd-logins-13040106

The initial length of the parallel queue is large, then the overload functionality throttles the connection rate. The plots for sessions, links and bundles are practically identical. There are no differences, because only successful sessions are count.

It is also nice to see how high the serial queue jumps when interface disappear: The original MPD is definitely susceptible to queue overrun. If this effect occurs, the original MPD drops all connections and stops. An unacceptable situation.

The FreeBSD box with MPD runs at a maximum load of 3 and requires 280 MB of RAM. The Linux machine generates a load of 20 during connection setup and a load of 50 on connection release. The memory required on the Linux side is about 2 GB to hold the many pppd instances.

Touch the limits

Next test simulates a interconnection breakdown: Instead of terminating pppd instances, let's drop the L2TP tunnels.

mpd-logins-13040211

Clearly bundles, as the highest layer, are removed first. Then the associated L2TP links go down. The load on BSD box rises up to 6.

On Linux side a disaster starts: All pppd instances compete for the scheduler in order to stop working. The load rises to over 700, and the machine will not calm down. A courageous "killall -9 pppd" redeemed the machine after half an hour.

But how far can we go? More than 10000? More than 20000? Let's try it out:

mpd-logins-13040213

The setup rises rapidly to 11,500 sessions and stopped spontaneously. On the Linux side OpenL2TP reports erroneous "RPC" parameters: It reached the limits of its implementation. The machine gets tangled and needs to be rebooted. The BSD box is fine.

And now several times in succession:

mpd-logins-130401

Up and down, again and again. Sometimes a crash by my own stupidity, but it does!

Going live

In the production environment, it works surprisingly quiet. Nevertheless, the system keeps crashing:

bsd-lasttest-panic-3

All crashes are now only located in the libalias module. Although the code was moved to svn-HEAD … Read more.

But there are further changes in the MPD code:

  • In order to replace the code in production quickly, the MPD quit itself when there are no more sessions open. The configuration option is called "delayed-one-shot". So MPD needs now to be called in a loop (/usr/local/etc/rc.d/mpd5 contains command="/usr/local/sbin/${name}.loop")
$ cat /usr/local/sbin/mpd5.loop
#! /usr/local/bin/bash

nohup /usr/local/bin/bash -c "
cd /
while true; do
  /usr/local/sbin/mpd5 -k -p /var/run/mpd5.pid -O
  sleep 5
done
"  >/dev/null 2>/dev/null </dev/null &
  • The RADIUS accounting reports only a limited set of termination cause codes, but I do need the complete error message:
ATTRIBUTE      mpd-term-cause  23      string
  • Relevant system logging (interfaces come and go, users log on and off) is now activated permanently, not only for debugging. Similarly, errors are always relevant. Otherwise, the server would run blind.

Oh, and the patch: Here you are. Please feel free to ask for commercial Support.

Gigabit-Ethernet ist für Server inzwischen die bestimmende Anschlußart. Setzt man einen solchen als Router ein, dann möchte man auch die Interfaces bis zum Anschlag auslasten. Aber das ist gar nicht so einfach.

Die Überschlagsrechnung für Broadband-Services für Privatkunden ist, daß man jedem Kunden im Durchschnitt ISDN zur Verfügung stellen muß, damit die Infrastruktur beim typischen Nutzungsmix nicht überlastet wird. So lastet man 1Gbps mit 8000 Nutzern zu je 128kbps aus. Diese Kenngrößen sind zu halten, auch wenn nicht alle Systeme zur Verfügung stehen. Üblicherweise verteilt man die Last also auf mehr Server, erhält so Redundanz, Lastverteilung und höhere Durchsätze für die Kunden.

Im aktuellen Fall sollen die FreeBSD Server Ihre Gigabit-Interfaces jeweils in Volllast bedienen können. Die typischen Empfehlungen bzgl. Performance und Network-Tuning sind weitestgehend berücksichtigt:

  • Puffergrößen und Interruptraten sind hochgesetzt.
  • Das ipfw-Regelwerk ist entschlackt.
  • Devd und per-Connection Scripte gibt es nicht mehr.

Es tut.

Fragen statt unterbrechen

Nur der NAT-Teil führt regelmäßig zum Absturz: "Kernel Trap 12", "Generel protection fault" und so weiter und so fort.

Den Abstürzen ist gemein, daß sie im libalias-Code auftreten, der im Inteface-Interrupt-Handler aufgerufen wurde. Es sieht so aus, als ob die internen Datenstrukturen zerstört sind. Vielleicht klappt ja das Locking nicht?

Was liegt also näher, auf das Interrupt-Handling zu verzichten und die Interfaces auf Polling umzustellen? Außerdem steht Polling im Ruf, besseren Durchsatz zu erzielen.

Gigabit erreicht Paketraten von bis zu 2 Mpps. Die Manpage spricht von Interrupt-Raten von 1kHz bis 2kHz. Es wären also pro Poll bis zu 1000 Pakete aus den Buffern der Ethernetkarten zu lesen. Und diese 1000 sind der Maximalwert, der pro Poll geholt werden kann. Scheint also zu passen. Wenn man noch Idlepoll anmacht, kann er sogar außerhalb der Interrupts Daten auslesen. Sicher ist sicher.

Zuerst fällt auf, daß DEVICE_POLLING und HZ=2000 nicht im Standardkernel eingebaut ist. Also erstmal einen neuen Kernel bauen. Für jemanden, der das noch nie mit FreeBSD gemacht hat, ein spannendes Spiel.

Nachdem das System glücklich wieder läuft, kann man auf einem Interface probeweise Polling aktivieren:

# ifconfig em1 polling

Es lebt noch. Aber wie bekommt man heraus, ob es wirklich funktioniert? Die Manpage spricht von Zähler, die man per sysctl auslesen kann. Die schauen gut aus.

Unter Last kommt eine Ernüchterung: Die Maschine macht nicht mehr über 800Mbps, sondern limitiert sich selbst auf 650Mbps. Die kern.polling.burst Werte zeigen, daß bis zu 1000 Pakete pro Poll gelesen werden. Dann aber brechen die Werte ein: Unter Last werden kaum noch 10, höchstens mal 100 Pakete gelesen!

Wo sind die Pakete hin? Konnte die Netzkarte die Daten nicht mehr wegspeichern?

Auch hier helfen die sysctl Zähler: dev.em.1.mac_stats.recv_no_buff ist normalerweise 0, steigt aber drastisch an, wenn die Überlast eintritt. Offenbar zerstört er dabei auch die existierenden Buffer, so daß Polling nichts mehr abzuholen hat.

Polle ich zu langsam?

In der /boot/loader.conf beherzt ein kern.hz=20000 eingetragen und nochmal probiert. Es gibt keine Verbesserung. Polling ist und bleibt auf ca. 650 Mbps limitiert. vmstat -i zeigt, daß die gewünschten Interrupt-Raten auch erreicht werden, das ist es also nicht.

Bei einer näheren Betrachtung mit top -nCHSIzs1 erschrecke ich dann doch: Polling läuft nur auf einer CPU! Diese ist zu 100% ausgelastet. Aber die Kiste hat doch 8 Kerne, die mitspielen könnten! Warum greifen deren Timerinterrupts nicht?

Freundlichkeit lohnt sich nicht

Da auch unter Polling die NAT-Abstüze bestehen bleiben und da die igb-Interfaces (aus meinem lagg-Bundle) zwar Polling unterstützen, dieses aber offenbar Work in Progress ist, so daß gar keine Daten mehr durchkommen, wenn die Last über 100 Mbps steigt, lasse ich Polling aus.

Polling ist hier auf einen CPU Kern limitiert und damit für höhere Bandbreiten schlicht ungeeignet. Schade.

Nachwirkungen

Das Experiment Polling schien beendet, trotzdem kamen die Maschinen nicht wieder aus dem Knie. Es blieb zäh.

Beim Rückbau von Polling zum Interrupt-Betrieb wurde – wegen der NAT-Probleme – die Firewall-Funktionalität aus dem Interface-Interrupt entfernt: net.isr.direct=0. Desweiteren wurden mehrere Kernel-Threads aktiviert, um die Last zu verteilen: net.isr.maxthreads=5.

Wie groß war also die Überraschung, als diese Änderungen trotz Reboots nicht aktiv wurden. Die ISR Verarbeitung klemmte auf  einer CPU fest. Und die hatte 100% Last. Die Interfaces waren mit 600Mbps an der Grenze angelangt.

Erst etwas Recherche zeigte, wo mein Problem liegt: Ein Kernel mit POLLING läßt den Netzwerkkram immer nur auf einer CPU laufen! Man darf POLLING gar nicht erst einkompilieren. Huh? Deswegen hat der Standardkernel diese Option nicht aktiviert? Hinterher ist man immer schlauer.

Also nehme ich vorerst den Firewall-Code mit in den Interface-Interrupt und warte auf den nächsten Reboot: net.isr.direct=1. Ohje, das reicht nicht! Also noch net.ist.direct_force=1. Jetzt aber!

Carrier Grade NAT, also NAT auf Providerseite, hat besondere Anforderungen an die Technik. Eine Lösung mit OpenSource auf Basis von FreeBSD stößt dabei schnell an erstaunliche Grenzen.

Seltsame Reboots

Nach Inbetriebnahme einer neuen LNS-Plattform, bootete sporadisch einer der Server in der LNS Farm neu. Dabei wurden die gerade auf diesem LNS angemeldeten Kunden getrennt.

Wenn das System abstürzt und nicht von allein rebootet, steht der Absturzbericht auf der Konsole. Teile davon gelangen manchmal auch ins Logfile. Typischerweise schaut das dann so aus:

bsd-lasttest-panic

Was mag das sein?

Die erste Vermutung lautet, daß irgendein Kundensystem irgend etwas "falsch" macht: Es könnte so ungewöhnliche Daten schicken, daß der Kernel die Hufe reißt. Dafür lassen sich aber keine Korrelationen finden: Die Kunden die regelmäßig zum Absturzzeitpunkt online sind, sind die, die häufig Ihre Verbindung neu aufbauen und so mit hoher Wahrscheinlichkeit auf dem betreffenden System landen.

Eine kurzer Blick in die Kernelquellen offenbart, daß dort aber auch nicht alles ganz koscher ist. Es werden Locks getestet, bevor die Zeiger auf die Locks überhaupt initialisiert sind. Ein Bugreport für diesen offenkundigen Fehler löste mir aber auch nicht das Problem.

Was kann es noch sein? Vielleicht einige der Protokolle, die die neue Plattform mehr unterstützt? Schalten wir doch auf einem Server mal VanJacobsen Compression in IPCP und IPV6CP Aushandlung komplett ab. Die anderen Server bleiben, wie sie sind.

Es ist keine signifikante Änderung zu erkennen. Das war es wohl nicht.

Es gab noch Überreste des Umbaus beim Plattformwechsel, schließlich wurden beide System einige Zeit parallel betrieben. Vielleicht kommen die Fehler aus dieser doch hinreichend umständlichen Topologie. Wieder kein Erfolg.

Etwas Systematik

Zuerst einmal müssen Fakten her. Dazu werden Crashdumps ativiert.

Alle beobachtbaren Abstürze finden im Netzkartentreiber statt. Nun werden alle Maschinen also in Zukunft primär die andere Netzkartenart benutzen (em statt igb). Außerdem ist bei em-Karten die Systemlast niedriger. Darüberhinaus lassen sich die em-Karten bei Bedarf von Interrupt in Pollingmodus versetzen. Das wäre der nächste Schritt.

Aber auch das hilft nichts. Die Fehler treten alle paar Tage wieder und wieder auf: Quer über den Maschinenpark, aber immer im libalias Code. Es steht also wieder mal die Vermutung im Raum, daß dieser Fehler von einem Datenpaket, nicht von einer PPP-Anmeldung verursacht wird.

Hier mal ein Crashdump mit Verbindung zum Sourcecode:

#6  0xffffffff80fa2829 in _FindLinkOut (la=0xffffff8001436000, src_addr=
 {s_addr = 403271268}, dst_addr={s_addr = 4143199550}, src_port=17143,
 dst_port=59294, link_type=17, replace_partial_links=1)
 at /usr/src/sys/netinet/libalias/alias_db.c:1111

       LIBALIAS_LOCK_ASSERT(la);
       i = StartPointOut(src_addr, dst_addr, src_port, dst_port, link_type);
->   LIST_FOREACH(lnk, &la->linkTableOut[i], list_out) {
           if (lnk->dst_addr.s_addr == dst_addr.s_addr &&
               lnk->src_addr.s_addr == src_addr.s_addr &&
               lnk->src_port == src_port &&
               lnk->dst_port == dst_port &&
               lnk->link_type == link_type &&
               lnk->server == NULL) {
                 lnk->timestamp = la->timeStamp;
                 break;
               }
       } 

Sehr seltsam. Warum sollte ein solcher Code abstürzen? Das Lock ist gesetzt (und validiert). Die Datenstrukturen sind einfach und enthalten eigene Kopien der Originaldaten. Dafür gibt es keine logische Erklärung.

Abstürzende Abstürze

Wieder ist ein Server abgestürzt. Er hat aber nicht rebootet, sondern ist beim Anlegen des Absturzberichtes hängen geblieben. Remote wurde die Maschine rebootet. Der eigentliche Grund liegt an der gleichen Stelle: FindLinkOut in libalias.

Warum kein Crashdump? Stehen geblieben ist er beim Schreiben des Crashdumps auf die Platte. Die schnelle Lösung ist, per crontab nachts die Crashdumps abzuschalten. Dann bleibt nur auf das Glück zu hoffen und einen hängen bleibenden Crashdump live zu erleben.

Dumping xx out of yy MB: 1% … 11% ... 21% carp1: BACKUP -> MASTER

Was zur Hölle macht der carp Code da? Wir sind im Shutdown!

Ein Blick in den Kernelcode zeigt, daß die Interrupt-Verarbeitung und damit ein Teil des Kernelcodes aktiviert bleibt. Dies tut er, damit er überhaupt auf die Platte schreiben kann. Aber offenbar dauert das Schreiben zu lange.

Die Probleme, die beim Erstellen von Crashdumps auftreten, liegen also darin begründet, daß für die Erzeugung der Crashdumps ein Teil der Betriebssystemfunktionalität vorhanden sein muß. Diese ist aber schon zum Teil abgeschaltet. Damit kann es vorkommen, daß der Überrest des Systems einen Deadlock generiert, z.B. weil kein Kernelthread mehr da ist, der ihn auflösen kann.

Kann man schneller Dumpen? Ja, mit textdumps. Auch hier ist wieder ein neuer Kernel fällig, aber der lohnt sich wirklich. Denn nun schreibt er die Crashdumps zuverlässig und bootet auch danach korrekt.

Nach einiger Zeit bleibt wieder ein Crash hängen: Er steht im Kerneldebugger. Wieso das?

Der Kernel hatte keine panic, sondern einen Page Fault ausgelöst. Und ddb hat pro möglichem Event einen Handler, an den Scripte gebunden werden können. Defaultmäßig liefert FreeBSD eine ddb.conf aus, die nur kernel.enter.panic belegt.

# /etc/ddb.conf

script kdb.enter.default=textdump set; capture on; show pcpu; bt; ps; alltrace; capture off; call doadump; reset

Nachdem nun alle Events abgefangen werden, ist klappt das mit den Crashdumps endlich wie gewünscht. Ein ernsthaftes Debugging ist nun erstmals möglich.

Spurensuche

Immer und immer wieder treten diese Art von Fehlern auf:

#7  0xffffffff80fa0cf1 in DeleteLink (lnk=0xffffff018feab700)
 at /usr/src/sys/netinet/libalias/alias_db.c:859
#8  0xffffffff80fa0f01 in HouseKeeping (la=0xffffff80011fa000)
 at /usr/src/sys/netinet/libalias/alias_db.c:849 

Vielleicht klappt das mit dem Locking im NAT-Code nicht richtig? Vielleicht wurde der Fehler inzwischen behoben?

Ein Vergleich des Sourcecodes des für NAT zuständigen libalias Moduls zwischen der eingesetzten Version 8.3-RELEASE und der aktuellen Entwicklerversion HEAD ergibt, daß eigentlich nur die SVN Änderung 241648 dazu gekommen ist. Diese wird eingespielt, aber bringt keine Besserung.

Der Fehler wurde offenbar auch anderswo noch nicht gefunden, i.d.R. wird darauf verwiesen, daß der RAM defekt sei. Dies ist aber inzwischen sehr unwahrscheinlich. Es können nicht alle Systeme gleichartig kaputt sein.

Ist das Locking defekt? Wenn ja, dann müßte doch LIBALIAS_LOCK_ASSERT sich melden. Wieder zeigt ein Blick in den Source, wie irregeleitet eine solche Annahme sein kann:

 * $FreeBSD: releng/8.3/sys/netinet/libalias/alias_local.h

#ifdef _KERNEL
#define LIBALIAS_LOCK_INIT(l) \
        mtx_init(&l->mutex, "per-instance libalias mutex", NULL, MTX_DEF)
#define LIBALIAS_LOCK_ASSERT(l) mtx_assert(&l->mutex, MA_OWNED)
#define LIBALIAS_LOCK(l) mtx_lock(&l->mutex)
#define LIBALIAS_UNLOCK(l) mtx_unlock(&l->mutex)
#define LIBALIAS_LOCK_DESTROY(l)        mtx_destroy(&l->mutex)
#else
#define LIBALIAS_LOCK_INIT(l)
#define LIBALIAS_LOCK_ASSERT(l)
#define LIBALIAS_LOCK(l)
#define LIBALIAS_UNLOCK(l)
#define LIBALIAS_LOCK_DESTROY(l)
#endif 

Schaut doch gut aus, oder?

 * $FreeBSD: releng/8.3/sys/sys/mutex.h

/*
 * The INVARIANTS-enabled mtx_assert() functionality.
 *
 * The constants need to be defined for INVARIANT_SUPPORT infrastructure
 * support as _mtx_assert() itself uses them and the latter implies that
 * _mtx_assert() must build.
 */
#if defined(INVARIANTS) || defined(INVARIANT_SUPPORT)
#define MA_OWNED        LA_XLOCKED
#define MA_NOTOWNED     LA_UNLOCKED
#define MA_RECURSED     LA_RECURSED
#define MA_NOTRECURSED  LA_NOTRECURSED
#endif

#ifdef INVARIANTS
#define mtx_assert(m, what)  \
        _mtx_assert((m), (what), __FILE__, __LINE__)

#define GIANT_REQUIRED  mtx_assert(&Giant, MA_OWNED)

#else   /* INVARIANTS */
#define mtx_assert(m, what)     (void)0
#define GIANT_REQUIRED
#endif  /* INVARIANTS */

Der gesamte Code wird schon vom Präprozessor entsorgt! Na zum Glück kann man das libalias-Modul separat kompilieren und einbinden.

Oh, nein! Die Basisfunktionalität "INVARIANT_SUPPORT" ist nicht im Standardkernel eingebunden. Also wieder mal einen komplett neuen Kernel!

Mit einem neuen Kernel kann man ja mal schon präventiv Polling aktivieren. Aber das war ein extra Schuß in den Ofen.

Folge dem Lock

Nun ist also im betroffenen NAT Modul der Überwachungscode für Locking aktiviert. Wenn die Abstütze mit fehlerhaftem Locking zusammen hängen, sollten sie damit gefunden werden.

Alternativ kann es sein, daß NAT aufgrund einer bestimmten Protokollbenutzung eines Kunden durcheinander kommt. Dies kann ein defekter NAT-Helper sein, der auf bestimmte Protokollverletzungen nicht korrekt reagiert und dabei die Verwaltungsdaten zerstört.

Und tatsächlich gibt starke Hinweise auf ein Lockingproblem:

KDB: enter: panic mutex per-instance libalias mutex not owned
 at .../libalias/alias_db.c:861 

Es ist nicht klar, ob die Lockingprobleme durch die Interruptsynchronisation auf mehreren CPUs kommt, oder durch einen NAT Helper, der die falsche Tabelle anfaßt.

Es wurde ein neues NAT-Modul erstellt, das einige Vermutungen belegen oder widerlegen soll:

  • Wenn die internen Rücklinks der NAT Einträge auf die Verwaltungsstruktur nicht stimmen, wird das System mit einem panic anhalten und Details melden.
  • Bevor internen Verwaltungsstrukturen entsorgt werden, werden diese mit einem wiedererkennbaren Bitmuster ECECEC... überschrieben. Damit ist eine spätere Verwendung von bereits freigegebenen Speicherbereichen erkennbar.
--- bsd-head/sys/netinet/libalias/alias_db.c    2013-05-09 14:25:35.000000000 +0200
+++ 8.3/sys/netinet/libalias/alias_db.c 2013-05-20 22:36:07.125123000 +0200
@@ -828,6 +828,8 @@
                lnk = LIST_FIRST(&la->linkTableOut[i]);
                while (lnk != NULL) {
                        struct alias_link *link_next = LIST_NEXT(lnk, list_out);
+
+                       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                        DeleteLink(lnk);
                        lnk = link_next;
                }
@@ -845,6 +847,7 @@
        LIBALIAS_LOCK_ASSERT(la);
        LIST_FOREACH_SAFE(lnk, &la->linkTableOut[la->cleanupIndex++],
            list_out, lnk_tmp) {
+               KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                if (la->timeStamp - lnk->timestamp > lnk->expire_time)
                        DeleteLink(lnk);
        }
@@ -875,6 +878,7 @@
                head = curr = lnk->server;
                do {
                        next = curr->next;
+                       memset(curr, 0xEC, sizeof(*curr));     /* detect double free */
                        free(curr);
                } while ((curr = next) != head);
        }
@@ -921,6 +925,7 @@
        }

 /* Free memory */
+        memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
        free(lnk);

 /* Write statistics, if logging enabled */
@@ -994,6 +999,7 @@

                /* Determine alias port */
                if (GetNewPort(la, lnk, alias_port_param) != 0) {
+                       memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
                        free(lnk);
                        return (NULL);
                }
@@ -1026,6 +1032,7 @@
                                fprintf(stderr, "PacketAlias/AddLink: ");
                                fprintf(stderr, " cannot allocate auxiliary TCP data\n");
 #endif
+                               memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
                                free(lnk);
                                return (NULL);
                        }
@@ -1135,6 +1142,7 @@
                            link_type, 0);
                }
                if (lnk != NULL) {
+                       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                        lnk = ReLink(lnk,
                            src_addr, dst_addr, lnk->alias_addr,
                            src_port, dst_port, lnk->alias_port,
@@ -1268,6 +1276,7 @@
                struct in_addr src_addr;
                u_short src_port;

+               KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                if (lnk->server != NULL) {      /* LSNAT link */
                        src_addr = lnk->server->addr;
                        src_port = lnk->server->port;
@@ -2097,6 +2106,9 @@
 void
 SetExpire(struct alias_link *lnk, int expire)
 {
+       struct libalias *la = lnk->la;
+
+       LIBALIAS_LOCK_ASSERT(la);
        if (expire == 0) {
                lnk->flags &= ~LINK_PERMANENT;
                DeleteLink(lnk);
@@ -2141,6 +2153,7 @@

        LIBALIAS_LOCK_ASSERT(la);
        la->deleteAllLinks = 1;
+       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
        ReLink(lnk, lnk->src_addr, lnk->dst_addr, lnk->alias_addr,
            lnk->src_port, cid, lnk->alias_port, lnk->link_type);
        la->deleteAllLinks = 0;
@@ -2432,6 +2446,7 @@

        LIBALIAS_LOCK(la);
        la->deleteAllLinks = 1;
+        KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
        DeleteLink(lnk);
        la->deleteAllLinks = 0;
        LIBALIAS_UNLOCK(la);
@@ -2570,6 +2586,7 @@
        LIST_REMOVE(la, instancelist);
        LIBALIAS_UNLOCK(la);
        LIBALIAS_LOCK_DESTROY(la);
+       memset(la, 0xEC, sizeof(*la));     /* detect double free */
        free(la);
 }

Ungereimtheiten

Es fällt auf, daß die Abstürze einige Zeit nach dem schnellen Verlust von Sessions auf einem Server auftreten, d.h. vor allem Nachts bei den (kundenseitig initierten) Trennungen bzw. nachdem ein Teil der Sessions getrennt wurden.

So besteht die Vermutung, daß beim Entfernen von Interfaces auch IP Adressen und damit zusammenhängende Daten im Kernel entfernt werden. Ein Teil der Daten wird auch von der NAT-Funktionalität nochmal verwaltet. Es ist möglich, daß die NAT-Bibliothek nicht ordnungsgemäß über das Wegräumen von Daten informiert wird, die sie ebenfalls verwaltet. Dadurch greift die NAT-Bibliothek auf eigentlich nicht mehr existente Speicherbereiche zu, die zufällig noch eine Weile die richtigen Daten enthalten. Irgendwann werden diese Speicherbereiche aber überschrieben und spätestens dann kommt es zum Absturz.

Eine solche Vermutung ist zu prüfen. Dazu ist der Kernelcode für das Entfernen von Interfaces zu sichten. Die Hoffnung besteht darin, eine Stelle zu finden, die auf die Vermutung paßt. Dies würde eine Fehlerbehebung gestatten.

Alternative Umgehungsmöglichkeit: Da die Abstürze fast ausschließlich im NAT-Code auftreten und damit den NAT-tenden Server zum Reboot zwingen, kann der Schaden für die Endkunden eingeschränkt werden. Dazu kann man die NAT und die LNS (Terminierung der PPP Verbindungen) Funktionalität trennen. Wenn nun der NAT Server abstürzt, übernimmt die Funktion ein gleichartiger NAT-Server.

Andere Server bilden ausschließlich den LNS, also die PPP-Terminierung ab. Diese wären von den NAT-Ausfällen nicht betroffen: Die Kunden würden also online bleiben. Für dieses Setup ist es notwendig, mit weitere Maschinen zu arbeiten, die Funktionscluster bilden.

Bevor aber für einen solchen Umbau Hardware beschafft beschafft wird, ist es notwendig, Die Vermutung zu untersetzen oder zu widerlegen. Und dazu muß das mit INVARIANTS kompilierte Modul ansprechen. Und mit etwas Glück liefert es einen Beleg, ob ein Use-After-Free Fehler vorliegt.

Es dauert aber erstmal Tage, bis alle Systeme den neuen Kernel mit dem "Fehlersuch-Modul" durch die zufälligen Reboots geladen haben. Neue Erkenntnisse sind bis dahin nicht aufgetreten: Wie immer sind die Abstürze im libalias-Code.

In der Zwischenzeit hat intensives Codestudium doch einige Erkenntisse gebracht:

  • An besonderen Protokollhelper im NAT sind nur GRE und SCTP einkompiliert.
  • Andere Protokollhelper von NAT sind nicht geladen (Modularer Aufbau).
  • Querreferenzen auf Datenstrukturen anderer Kernelbestandteile werden von der NAT-Bibliothek nicht verwaltet.

Damit entfällt die Vermutung, das es mit dem Entfernen von Interfaces zu tun hat.

Erste Ergebnisse

Die Fehlersuche schlägt an:

.../netinet/libalias/alias_db.c:853 IncrementalCleanup:
  la (0xffffff8001476000) != lnk->la (0xffffff80014c4000)

la ist der Zeiger auf die NAT-Struktur (von denen es 32 gibt). lnk ist der Zeiger auf den gerade aktiven NAT-Eintrag, der selbst auf seine NAT-Struktur zurückzeigen soll.

Erwartet war, daß lnk->la den Codewert für 0xececececec enthält, was einen Use-after-Free aufgezeigt hätte. Eine andere Erwartung war, daß lnk->la komplettes Chaos enthält, was eine Fremdnutzung des Speicherbereichs angezeigt hätte.

Die unerwartete Meldung besagt nun, daß ein NAT-Eintrag, die für ihn zuständige Struktur verlassen hat und bei einer anderen Struktur aufgenommen wurde. Dies deutet auf einen Fehler im originären NAT-Code hin.

Es ist grundsätzlich möglich, daß ein einzelnes Datenpaket durch die konfigurierten Firewallregeln, die das NAT ansteuern, zweimal an unterschiedliche NAT-Instanzen übergeben wird. Dies geschieht dann, wenn ein Kunde einen anderen Kunden von NAT zu NAT ansprechen will.

Dies sollte eigentlich kein Problem darstellen, korreliert aber mit dem Regelwerk.

for i in $(seq 0 31); do
  j=$(($i+128))
  ipfw nat $j config ip 198.51.100.$j same_ports
  ipfw add 1$j nat $j ip4 from any to 198.51.100.$j
  ipfw add 2$j nat $j ipv4 from 100.64.0.$i:255.192.0.31 to any
done

Die Firewallregeln werden nun dahingehend angepaßt, daß sie ausschließlich auf dem äußeren Interface (NAT ins Internet und unNAT aus dem Internet) aktiv werden. Erst durch den Abbau der alten Plattform war es überhaupt möglich, diese Einschränkung vorzunehmen. Zuvor gab es weitere, interne Übergänge ins Internet.

Da diese Bedingung nun entfallen ist, wurde das Regelwerk entschlackt und die NAT-Bindungen nur auf das Außeninterface beschränkt.

for i in $(seq 0 31); do
  j=$(($i+128))
  ipfw nat $j config ip 198.51.100.$j same_ports
  ipfw add 1$j nat $j ip4 from any to 198.51.100.$j recv extern in
  ipfw add 2$j nat $j ipv4 from 100.64.0.$i:255.192.0.31 to any xmit extern out
done

Dabei ist zu beachten, daß FreeBSD NAT nur ausgehend ausführt. Eingehende Datenpakete werden ausschließlich "entnattet".

Die Absturzhäufigkeit sinkt drastisch!

Mit etwas Abstand kann man feststellen, daß nach diesen Änderungen an den ipfw-Regeln die beobachteten Fehler nicht mehr auftreten. Das bedeutet natürlich nicht, daß alle Probleme gelöst sind. Denn es geht weiter …

Nachdem der erste Fehler in CGN eingegrenzt und behoben werden konnte, treten weiter Abstürze auf. Natürlich in anderen Ausprägungen. Und so geht die Fehlersuche weiter.

Debugging ist wie Zwiebeln schälen: Man entfernt vorsichtig Schicht um Schicht, um am Ende mit leeren Händen dazustehen.

unknown

Hyperoptimiert

Diese neuen Abstürze sind eher seltsam. So meldete ein Server einen Panic in einer Funktion laut Stacktrace, die diesen Code gar nicht enthält.

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff80012f3000) != lnk->la (0xffffff8001327000)

panic() at panic+0x187
HouseKeeping() at HouseKeeping+0x161
LibAliasOutLocked() at LibAliasOutLocked+0x62
LibAliasOut() at LibAliasOut+0x54

Es handelt sich um eine Compiler-Optimierung, die Code verschiebt und umsortiert. Diese wird aber nur bei höheren Optimierungsstufen aktiviert. Doch nicht im Kernel, oder? Schließlich ist eine höhere Optimierung nicht empfohlen.

# $FreeBSD: releng/8.3/sys/conf/kern.pre.mk

. if ${MACHINE_ARCH} == "amd64"
COPTFLAGS?=-O2 -frename-registers -pipe
. else
COPTFLAGS?=${_MINUS_O} -pipe
. endif

Also doch! Aber das müßte den gesamten Kernel betreffen.

Die Suche fördert die Abstürze zu Tage, die immer nur dann auftreten, während Streit um ein Mutex herrscht. Und das Mutex muß nicht in libalias liegen:

/var/crash# tar xOf textdump.tar.15 | fgrep -2 address_hook
db:0:kdb.enter.default>  bt
Tracing pid 2437 tid 100255 td 0xffffff001df85460
ng_address_hook() at ng_address_hook+0x8e
ng_ppp_link_xmit() at ng_ppp_link_xmit+0x108
ng_apply_item() at ng_apply_item+0x220
--

Tracing command mpd5 pid 2437 tid 100255 td 0xffffff001df85460
ng_address_hook() at ng_address_hook+0x8e
ng_ppp_link_xmit() at ng_ppp_link_xmit+0x108
ng_apply_item() at ng_apply_item+0x220
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xd7
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_iface_send() at ng_iface_send+0xc5
ng_iface_output() at ng_iface_output+0x1b3

Ich habe probeweise auf eingen Servern die Optimierung für den libalias-Code deaktiviert und so ein neues Modul geschaffen.

Damit kann ein Fehler, der durch den optimierenden Compiler verursacht wurde, ausgeschlossen werden. Er könnte die Mutexe unter Last, auf verschiedenen CPU, ggf. im Interrupt-Handler, durch Instruction-Scheduling einer Racecondition ausgesetzt haben.

Um zumindest den Interrupt-Anteil zu minimieren, habe ich net.isr.direct=0 gesetzt. So wird NAT nicht mehr direkt im Interface-Interrupt ausgeführt. Den CPUs tut es gut. Die Last verteilt sich mehr.

Um Schwierigkeiten mit Mutexen und CPU-Switching zu vermeiden, ist zusätzlich net.isr.bindthreads=1 gesetzt.

Aber da ist ja auch noch der Compiler-Optimierer einzubremsen, und zwar im kompletten Kernel. Bisher war ja nur das Modul ohne Optimierung kompiliert.

$ env  CFLAGS='-O -pipe' COPTFLAGS='-O -pipe' \
          MAKEOBJDIRPREFIX=~/mykernel NO_KERNELCLEAN=yes \
          KERNCONF=LNS make buildkernel

Wenn man Glück hat, kracht es nun an anderen Stellen. Dies bedeutet dann, daß man ein weitere Schicht der Zwiebel abgetragen hat.

Fatal trap 12: page fault while in kernel mode
cpuid = 5; apic id = 13
fault virtual address   = 0x58
fault code              = supervisor read data, page not present
instruction pointer     = 0x20:0xffffffff8069c912
stack pointer           = 0x28:0xffffff835f8c3800
frame pointer           = 0x28:0xffffff835f8c3820
code segment            = base 0x0, limit 0xfffff, type 0x1b
                        = DPL 0, pres 1, long 1, def32 0, gran 1
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = 12913 (mpd5)

rtfree() at rtfree+0x72
in_ifadownkill() at in_ifadownkill+0xb0
rn_walktree() at rn_walktree+0x7c
in_ifadown() at in_ifadown+0x8b
in_control() at in_control+0x956
ifioctl() at ifioctl+0x75e
kern_ioctl() at kern_ioctl+0xfe
ioctl() at ioctl+0xfd
amd64_syscall() at amd64_syscall+0xf9
Xfast_syscall() at Xfast_syscall+0xfc

<4>if_rtdel: error 3
<6>in_scrubprefix: err=65, prefix delete failed

Out of area Einsätze

Aber es sind auch alte Bekannte dabei:

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup:
 la (0xffffff8001288000) != lnk->la (0x2000000008)
KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup:
 la (0xffffff80014aa000) != lnk->la (0x2000000008) 

Dieser Fehler ist wirklich interessant. Er deutet darauf hin, daß der Kernel – anderwo – den Speicher überschreibt.

Darüberhinaus ist es bedenkenswert, in diesem Fall aufzuräumen anstatt abzustürzen. Die Gefahr, ein Memory-Leak zu produzieren, ist groß, allerdings tritt der Fehler nur alle paar Tage einmal auf. Beim Absturz gegen die aktuellen NAT-Tabellen sowieso verloren, vom Wegwerfen wird der Schaden also nicht größer.

Es besteht natürlich die Gefahr, daß kein kleiner Fehler vorliegt, sondern ganze RAM-Bereiche vernichtet worden. In diesem Fall wäre der erste Fehler nur der Anfang einer heftigen Kaskade. Es ist also sinnvoll, den Fehler nur begrenzt oft zu ignorieren.

#define VALID_PTR_MASK 0xff00000000000000lu
#define VALID_PTR(p) ((((uint64_t)p)&VALID_PTR_MASK) == VALID_PTR_MASK)

#define CHECK_LNK(la, lnk, i, abortcmd) do {                                   \
   if(!VALID_PTR(lnk)) {                                                       \
      _broken_lnk(la, lnk, i, "lnk", __FILE__, __LINE__, __FUNCTION__);        \
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   if(!VALID_PTR(lnk->la)) {                                                   \
      _broken_lnk(la, lnk->la, i, "lnk->la", __FILE__, __LINE__, __FUNCTION__);\
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p) @%d\n",          \
                           __FILE__, __LINE__, __FUNCTION__, la, lnk->la, i)); \
} while(0)

static void _broken_lnk(struct libalias *la, void * p, int i, char const * prt, char const * file, int line, char const * func);

static int _broken_retry_count = 0;
static void _broken_lnk(struct libalias *la, void * p, int i, char const * ptr, char const * file, int line, char const * func) {
   if(_broken_retry_count >= 10)        /* No mercy */
     panic("%s:%d %s: %s=%p is invalid @%d\n", file, line, func, ptr, p, i);

   printf("%s:%d %s: %s=%p is invalid @%d\n", file, line, func, ptr, p, i);
   printf("%d. retry to throw away broken NAT tables (will leak memory)\n", ++_broken_retry_count);
   LIBALIAS_LOCK_ASSERT(la);
   for (i = 0; i < LINK_TABLE_OUT_SIZE; i++)
     LIST_INIT(&la->linkTableOut[i]);
   for (i = 0; i < LINK_TABLE_IN_SIZE; i++)
     LIST_INIT(&la->linkTableIn[i]);
}

Und dann im Code überall die struct alias_link Zeiger vor jeder Verwendung an CHECK_LNK verfüttern.

Ja, der Code ist häßlich. Besonders die Prüfung mit einer Maskierung. Aber alle Abstürze in _FindLinkOut haben inzwischen Adressen, die nicht mehr dem Testmuster entsprechen. Man müßte eigentlich mit mincore den Zeiger prüfen, aber diese Funktion ist ein richtiger Performancekiller.

Es dauert nun wieder einige Tage, bis diese neue Version durch die zufälligen Reboots ausgerollt ist. Erst wenn diese Versionen wieder abstürzen, gibt es weitere Ergebnisse.

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff800133e000) != lnk->la (0x4800000008) @3545
KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff80013da000) != lnk->la (0x200000002) @918

Oh, damit ist eine weitere Vermutung widerlegt: Die Hashfunktion ist in Ordnung. Es bestand eine gewisse Chance, daß ein negativer Wert als Ergebnis der Hashberechnung entsteht. Ebenfalls möglich wäre ein Off-by-one Error an den Grenzen des Wertebreichs gewesen. Aber dies liegt ganz offensichtlich nicht vor.

Einige Tage später hat die Methode nun tatsächlich Wirkung gezeigt: Zumindest ein Absturz wurde verhindert.

Plötzlich (fast) stabil

Nachdem ein Kunde vom Netz genommen wurde, der selbst CGN machte, treten die Abstürze in der libalias nicht mehr auf. Aktuell ist es nur eine Korrelation, aber man darf hoffen.

Es ist gut möglich, daß die Fehler von einer überproportional hohen Anzahl von Verbindungen mit dem gleichen Quellport und der gleichen Quelladresse verursacht wurden. Die Libalias könnte also in der Hinsicht nicht streßfest sein.

Weitersuchen

Achja, um das auch auszuschließen: Die Systeme haben ECC-RAM. Speicherfehler als Ursache waren eigentlich schon dadurch praktisch unmöglich, weil immer an der gleichen Stelle auf unterschiedlichsten Systemen das Problem auftritt. Es hat nur nach wie vor keine erkennbare Ursache. Andererseits hat die detailierte und geduldige Untersuchung schon einen Fehler in den Firewallregeln aufgedeckt. Es ist also nicht hoffnungslos.

Die ganz gemeine Methode ist radikal upzudaten. Das ist aber ein anderes Thema. Dennoch ist die Überlegung interessant, da es unter den FreeBSD Entwicklern Hinweise darauf gibt, daß es beim schnellen Löschen von Interfaces zum use-after-free im Kernelspeicher kommen kann.

Da ich nicht einfach upgraden kann, variiere ich die Geschwindigkeiten von 20ms bis 120ms in gleichmäßigen Schritten. Jeder LNS hat nun eine andere Verarbeitungsgeschwindigkeit für Ereignisse. Mal sehen, wie sich das auswirkt, wenn nachts die Massen wieder "die Zwansgtrennung vorziehen.

Die Ergebnisse sind sparsam: Die Kiste mit 20ms hat zwei Abstürze mehr als die Kiste mit 120ms. Nun laufen alle Kiste auf 100ms Delay.

Auffällig ist, daß inzwischen immer der gleiche Wert als falscher Pointer auftritt. Möglicherweise läßt sich der überschreibende Prozeß anhand des überschriebenden Umfeldes eingrenzen. Dazu wird der Code abgeändert, um einen Hexdump der struct alias_link zu erhalten.

#define CHECK_LNK(la, lnk, i, abortcmd) do {                                   \
   if(!VALID_PTR(lnk)) {                                                       \
      _broken_lnk(la, lnk, i, "lnk", __FILE__, __LINE__, __FUNCTION__);        \
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   if(!VALID_PTR(lnk->la)) {                                                   \
      hexdump(lnk, sizeof(*lnk), "lnk", 0);                                    \
      _broken_lnk(la, lnk->la, i, "lnk->la", __FILE__, __LINE__, __FUNCTION__);\
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p) @%d\n",          \
                           __FILE__, __LINE__, __FUNCTION__, la, lnk->la, i)); \
} while(0)

Und das gibt tatsächlich Ergebnisse. Ein Absturz der mit querverlinkten Einträgen:

[... kein Dump ...]
libalias/alias_db.c:884 IncrementalCleanup: la (0xffffff80014d6000) != lnk->la (0xffffff800136a000) @2061

Ein Absturz der mit falschen Zeiger und Aufräumcode, der aber nicht hilft:

lnk0000   01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0010   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0030   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0050   00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00  |................|
lnk0060   00 00 00 00 00 00 00 00                          |........        |
alias_db.c:886 IncrementalCleanup: lnk->la=0x100000001 is invalid @3064
1. retry to throw away broken NAT tables (will leak memory)

Fatal trap 12: page fault while in kernel mode
cpuid = 4; apic id = 12
fault virtual address   = 0x34
fault code              = supervisor read data, page not present
instruction pointer     = 0x20:0xffffffff80f6e0d8
stack pointer           = 0x28:0xffffff835f81f360
frame pointer           = 0x28:0xffffff835f81f3c0
code segment            = base 0x0, limit 0xfffff, type 0x1b
                        = DPL 0, pres 1, long 1, def32 0, gran 1
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = 12 (swi1: netisr 4)

Ein Absturz der mit falschen Zeiger und Aufräumcode, der diesmal hilft:

lnk0000   08 00 00 00 20 00 00 00 04 00 00 00 01 00 00 00  |.... ...........|
lnk0010   ec ca 4a 01 03 f6 14 38 63 6d 64 34 00 00 00 00  |..J....8cmd4....|
lnk0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0030   00 00 00 00 00 00 00 00 6f 01 00 00 fb 24 00 00  |........o....$..|
lnk0040   6a 01 00 00 dc 24 00 00 00 00 00 00 00 00 00 00  |j....$..........|
lnk0050   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0060   00 00 00 00 00 00 00 00                          |........        |
alias_db.c:1260 _FindLinkIn: lnk->la=0x2000000008 is invalid @2910
1. retry to throw away broken NAT tables (will leak memory)
[... läuft noch stundenlang weiter ...]

Insgesamt wird beim Aufräumen tatsächlich die Betriebsfähigkeit des Systems erhalten. Drei bis vier Aufräumversuche überlebt eine Maschine, bevor sie entgültig rebootet. Und die Vorfälle liegen immer noch bei ca. ein Vorfall pro Tag bei einem LNS statt der gesamten Plattform.

Die Fehler treten vorzugsweise an Maschinen auf, die nicht nur CGN und PPP/ADSL und dafür machen, sondern auch CGN für DHCP/VDSL. Es ist also möglich, daß ein Fehler mit hohen Verbindungsanzahlen pro Souce-IP und Port bedingt wird.

Welches CMD?

Mittlerweile ist der letzte Hexdump das absolut vorherrschende Phänomen der noch korrigierbaren Fehler geworden.

Aber was zu Hölle sucht ein "cmd" Text mitten in einem NAT-Eintrag? Welcher Prozess konnte es sein, der diesen String hinterlies? Welcher Prozess könnte einen Datensatz generiert, der diesen Text enthält? Könnte das geschehen nachdem ein Interface nicht mehr existiert, so daß ein "use-after-free" tatsächlich zur Wirkung kommt?

Die Suche im Kernelcode findet diesen String ausschließlich im Parser von NetGraph Nachrichten. Und NetGraph Nachrichten werden vom MPD generiert. Und die sollen zu spät kommen? Ich hatte doch die Eventverarbeitung extra verlangsamt, um die Abstürze in den Griff zu bekommen! Na dann setze ich die doch mal wieder ganz runter: Von 0 bis 10ms.

Das Beschleunigen der Eventverarbeitung hat das Problem nur verschärft: Es sind wieder 100ms aktiv. Das war's also auch nicht.

Aber der Memorydump stammt tatsächlich von einer NetGraph Nachricht:

struct ng_mesg {
 struct  ng_msghdr {
  u_char          version;                /* 08 ... NGM_VERSION */
  u_char          spare;                  /* 00 ... pad */
  u_int16_t       spare2;                 /* 00 00 ... pad */
  u_int32_t       arglen;                 /* 0x00000020 ... length of data */
  u_int32_t       cmd;                    /* 0x00000004 ... NGM_NAME */
  u_int32_t       flags;                  /* 0x00000001 ... NGF_RESP */
  u_int32_t       token;                  /* ec ca 4a 01 ... Token für den MPD */
  u_int32_t       typecookie;             /* 03 f6 14 38 ... NGM_PPP_COOKIE */
  u_char          cmdstr[NG_CMDSTRSIZ];   /* "com4" */
 } header;
 char    data[];                 /* 6f 01 00 ... */
};

Speicher, mein Speicher

Es ist für die Stabilität des Systems zwingend notwendig, die Abstürze durch Memory-Corruption zu verhindern. Das ist aber ein anderes Thema.

Nachtrag

Mit dem Verzicht auf das Löschen von Interface-Knoten konnten die Fehler bisher vollständig beseitigt werden.

Trotz aller Bemühungen kommt es immer wieder zum Überschreiben von Kernelspeicher. Grund sind aller Wahrscheinlichkeit nach "use-after-free" Fehler, d.h. Zugriff auf Speicherbereiche, die schon nicht mehr in Benutzung sind. Eine Fehlersuche ist im produktiven System nicht möglich, da dies die Systeme extrem verlangsamt und massiv Abstürze generiert.

Es gibt zwei Strategien, diese Probleme abzumildern:

  • Die Speicherverwaltung wird pro Verwendungszweck getrennt. Damit sollten die Querschläger zwischen den Prozessen (NetGraph schreibt in NAT-Tabellen) eingedämmt werden. Die notwendigen Anpassungen dafür habe ich bereits entworfen.
  • Die Speicherverwaltung des Kernels wird so geändert, daß die Speicherbereiche nicht sofort freigegeben werden, sondern erst nach einer gewissen Wartezeit wieder für die Neuvergabe zur Verfügung stehen. Dies hat zur Folge, daß der Freispeicher knapp werden kann und das System an Speichermangel eingeht. Diese Anpassungen erfordern also besondere Sorgfalt.

In den Tiefen

Pro Verwendungszweck wird in FreeBSD die Struktur struct malloc_type benutzt. Es ist also notwendig, sich an diese anzuhängen.

Glücklicherweise ist für die Erweiterung bereits ein interes Datenfeld struct malloc_type_internal vorgesehen. Die ABI, also die Binärschnittstelle des Kernels zu seinen Modulen, bleibt bei Änderungen an dieser Stelle unbeeinflußt.

An dieser Stelle hänge ich einen Ringpuffer fester Größe ein. Eine dynamische Warteschlange würde zu leicht malloc rekursiv aufrufen, und das will man unbedingt vermeiden.

--- sys/malloc.h        (revision 243948)
+++ sys/malloc.h        (working copy)
@@ -87,10 +87,29 @@
 #define DTMALLOC_PROBE_FREE            1
 #define DTMALLOC_PROBE_MAX             2

+/*
+ * Delayed free definitions.
+ */
+#define DELAYED_FREE_MAX               10000
+struct delayed_free_buffer {
+   int head, tail;
+   u_long bytes;
+   struct delayed_free_entry {
+      void * pointer;
+      u_long size;
+      u_long checksum;
+   } stale[DELAYED_FREE_MAX];
+};
+#define DELAYED_FREE_INCR(i)         (((i)+1)%DELAYED_FREE_MAX)
+#define DELAYED_FREE_ISEMPTY(dfp)    ((dfp)->tail == (dfp)->head)
+#define DELAYED_FREE_ISFULL(dfp)     ((dfp)->tail == DELAYED_FREE_INCR((dfp)->head))
+#define DELAYED_FREE_INIT(dfp)       do { (dfp)->head = (dfp)->tail = 0; } while(0)
+
 struct malloc_type_internal {
        uint32_t        mti_probes[DTMALLOC_PROBE_MAX];
                                        /* DTrace probe ID array. */
        struct malloc_type_stats        mti_stats[MAXCPU];
+       struct delayed_free_buffer      delayed_free;
 };

 /*

Der Ringpuffer arbeitet mit zwei Indizes, die einer Moduloarithmetik gehorchen. Das nächste und unbenutzte Element steht bei head. Mit tail findet man das älteste, benutzte Element. Stehen beide Zeiger aufeinander ist der Puffer leer. Es ist also immer ein Element in der Liste unbenutzt.

Die Logik zum Einfügen und Löschen von Elementen in den Ringpuffer ist also ganz einfach. Es werden die Zeiger auf den Speicherbereich, die Länge des Speicherbereichs und eine Prüfsumme über den Speicher im Ringpuffer abgelegt. Darüberhinaus wird Buch über die gesamte noch nicht freigegebene Speichermenge geführt.

--- kern/kern_malloc.c  (revision 243948)
+++ kern/kern_malloc.c  (working copy)
@@ -61,6 +61,7 @@
 #include <sys/proc.h>
 #include <sys/sbuf.h>
 #include <sys/sysctl.h>
+#include <sys/syslog.h>
 #include <sys/time.h>

 #include <vm/vm.h>
@@ -243,6 +244,140 @@
     &malloc_failure_count, 0, "Number of imposed M_NOWAIT malloc failures");
 #endif

+static u_long delayed_free_max_size = 1000000;
+static int delayed_free_min_free = DELAYED_FREE_MAX/10;
+enum delayed_free_error {
+   DF_NO_ERROR, DF_OVERRUN, DF_UNDERRUN,
+   DF_CONTAINS_NULL, DF_MODFIED_AFTER_FREE
+};
+static char const * delayed_free_errortext(enum delayed_free_error e) {
+   switch(e) {
+    case DF_NO_ERROR          : return "OK";
+    case DF_OVERRUN           : return "Circular buffer overrun";
+    case DF_UNDERRUN          : return "Circular buffer underrun";
+    case DF_CONTAINS_NULL     : return "Delay of a NULL pointer";
+    case DF_MODFIED_AFTER_FREE: return "Detecting modify after free";
+    default                   : return "Unknown error code (Can't happen)";
+   }
+
+static enum delayed_free_error delayed_free_add(struct malloc_type *mtp, void * x, size_t s);
+static enum delayed_free_error delayed_free_remove(struct malloc_type *mtp, void ** to_be_freed);
+static void delayed_free_free(struct malloc_type *mtp, int all);
+static void delayed_free_incremental(struct malloc_type *mtp);
+static void delayed_free_all(struct malloc_type *mtp);
+static u_long delayed_free_checksum(void const * p, size_t l);
+void free2(void *addr, struct malloc_type *mtp, int delayed);
+
+static u_long delayed_free_checksum(void const *p, size_t l) {
+   return crc32(p, 128 < l ? 128 : l);
+}
+
+static enum delayed_free_error
+delayed_free_add(struct malloc_type *mtp, void * x, size_t s)  {
+   enum delayed_free_error error = DF_NO_ERROR;
+
+   critical_enter();
+   {
+      struct malloc_type_internal *mtip  = mtp->ks_handle;
+      struct delayed_free_buffer  *dfp   = &mtip->delayed_free;
+      struct delayed_free_entry   *phead = &dfp->stale[dfp->head];
+
+      if(DELAYED_FREE_ISFULL(dfp)) {
+        struct delayed_free_entry *ptail = &dfp->stale[dfp->tail];
+        ptail->pointer = NULL;
+        ptail->size    = 0;
+        dfp->tail = DELAYED_FREE_INCR(dfp->tail);
+        error = DF_OVERRUN;
+      }
+      phead->pointer  = x;
+      phead->size     = s;
+      phead->checksum = delayed_free_checksum(x,s);
+      dfp->bytes     += s;
+      dfp->head       = DELAYED_FREE_INCR(dfp->head);
+   }
+   critical_exit();
+
+   return error;
+}
+
+static enum delayed_free_error
+delayed_free_remove(struct malloc_type *mtp, void ** to_be_freed) {
+   enum delayed_free_error error = DF_NO_ERROR;
+
+   critical_enter();
+   {
+      struct malloc_type_internal *mtip = mtp->ks_handle;
+      struct delayed_free_buffer  *dfp  = &mtip->delayed_free;
+
+      if(DELAYED_FREE_ISEMPTY(dfp)) {
+        (*to_be_freed) = NULL;
+        error = DF_UNDERRUN;
+      } else {
+        struct delayed_free_entry *ptail = &dfp->stale[dfp->tail];
+
+        if(ptail->pointer == NULL) {
+           error = DF_CONTAINS_NULL;
+        } else if(ptail->checksum != delayed_free_checksum(ptail->pointer, ptail->size)) {
+           error = DF_MODFIED_AFTER_FREE;
+        }
+
+        (*to_be_freed) = ptail->pointer;
+        dfp->bytes    -= ptail->size;
+        ptail->pointer = NULL;
+        ptail->size    = 0;
+        dfp->tail      = DELAYED_FREE_INCR(dfp->tail);
+      }
+   }
+   critical_exit();
+
+   return error;
+}
+
+static void
+delayed_free_free(struct malloc_type *mtp, int all) {
+   struct malloc_type_internal *mtip;
+   struct delayed_free_buffer *dfp;
+   enum delayed_free_error err;
+
+   KASSERT(mtp->ks_magic == M_MAGIC, ("free: bad malloc type magic"));
+
+   mtip = mtp->ks_handle;
+   dfp = &mtip->delayed_free;
+
+   do {
+      void *p = NULL;
+
+      mtx_lock(&malloc_mtx);
+      if(all || (dfp->head + (DELAYED_FREE_MAX - dfp->tail))%DELAYED_FREE_MAX > delayed_free_min_free || dfp->bytes > delayed_free_max_size) {
+        err = delayed_free_remove(mtp, &p);
+      } else {
+        err = DF_UNDERRUN;
+      }
+      mtx_unlock(&malloc_mtx);
+
+      switch(err) {
+       default:
+        log(LOG_ERR, "Freeing delayed in %s returned '%s'\n", mtp->ks_shortdesc, delayed_free_errortext(err));
+       case DF_NO_ERROR:
+        free2(p, mtp, 0);
+       case DF_UNDERRUN:
+        break;
+      }
+   } while(err != DF_UNDERRUN);
+}
+
+static void
+delayed_free_incremental(struct malloc_type *mtp) {
+   delayed_free_free(mtp, 0);
+}
+
+static void
+delayed_free_all(struct malloc_type *mtp) {
+   delayed_free_free(mtp, 1);
+}
+
+
 static int
 sysctl_kmem_map_size(SYSCTL_HANDLER_ARGS)
 {

Es sollte solange verzögert werden, bis der Ringpuffer voll ist. Aber ein voller Ringpuffer kann viel zuviel Freispeicher auffressen. Deswegen wird ab einem Limit (das später tunable werden kann) ebenfalls schon Speicher freigegeben.

Nach jedem malloc und vor jedem free werden die verzögerteten frees ausgeführt, wenn die Limits überschritten sind.

@@ -439,6 +574,7 @@
        if (va != NULL)
                va = redzone_setup(va, osize);
 #endif
+       delayed_free_incremental(mtp);
        return ((void *) va);
 }

@@ -452,6 +588,13 @@
 void
 free(void *addr, struct malloc_type *mtp)
 {
+       delayed_free_incremental(mtp);
+       free2(addr, mtp, 1);
+}
+
+void
+free2(void *addr, struct malloc_type *mtp, int delayed)
+{
        uma_slab_t slab;
        u_long size;

@@ -485,6 +628,9 @@
                struct malloc_type **mtpp = addr;
 #endif
                size = slab->us_keg->uk_size;
+               if(delayed) {
+                  goto delaying;
+               }
 #ifdef INVARIANTS
                /*
                 * Cache a pointer to the malloc_type that most recently freed
@@ -503,9 +649,29 @@
                uma_zfree_arg(LIST_FIRST(&slab->us_keg->uk_zones), addr, slab);
        } else {
                size = slab->us_size;
+               if(delayed) {
+                  goto delaying;
+               }
                uma_large_free(slab);
        }
        malloc_type_freed(mtp, size);
+       return;
+
+delaying:
+     {
+       enum delayed_free_error err;
+       mtx_lock(&malloc_mtx);
+       err = delayed_free_add(mtp, addr, size);
+
+       switch(err) {
+        default:
+          log(LOG_ERR, "Delaying free in %s returned '%s'\n", mtp->ks_shortdesc, delayed_free_errortext(err));
+        case DF_NO_ERROR:
+          break;
+       }
+       mtx_unlock(&malloc_mtx);
+       return;
+     }
 }

 /*
@@ -703,6 +869,7 @@
                panic("malloc_init: bad malloc type magic");

        mtip = uma_zalloc(mt_zone, M_WAITOK | M_ZERO);
+       DELAYED_FREE_INIT(&mtip->delayed_free);
        mtp->ks_handle = mtip;

        mtx_lock(&malloc_mtx);
@@ -727,6 +894,8 @@
            ("malloc_uninit: bad malloc type magic"));
        KASSERT(mtp->ks_handle != NULL, ("malloc_deregister: cookie NULL"));

+       delayed_free_all(mtp);
+
        mtx_lock(&malloc_mtx);
        mtip = mtp->ks_handle;
        mtp->ks_handle = NULL;

Mal sehen, wie sich dieses Codestück tut. Es sollte die "use-after-free" Fehler bei Raceconditions komplett ausschalten und die "modify-after-free" Fehler benennen können.

Mit der Information, welche Codeteile da quer durch den Speicher schreibt, kann man die Fehler, die sich in der libalias offenbart, überhaupt anfangen zu suchen.

Erste Ergebnisse

Die ersten Ergebnisse sahen schrecklich aus und gingen auf fehlerhaftes Locking zurück.

Nun melden einige Allokatoren häufig einen Overrun, weil mehere Prozesse parallel um das Ende der Warteschlange kämpfen. Bei dem fehlerhaften Lock führte das zu willden Fehlermeldungen, insbesondere zu mit NULL überschriebenen Werten.

Allerdings bestätigt auch vmstat, daß einige dieser Allokatoren das maximale 1MB voll ausnutzen.

# vmstat -m | egrep '99.K|1...K|Size' | cut -c-45
         Type InUse MemUse HighUse Requests
     filedesc  1788  1023K       -    37160
         temp  6794  1603K       -    84225
      subproc   536  1290K       -    37259
         cred  6329   993K       -   427648
       bus-sc   293  1416K       -     8601
          bus 10269  1112K       -    20615
         kobj   356  1424K       -      356
  ether_multi 43424  1987K       -    43424
      CAM XPT   977  1136K       -     6553
      sctpnat    96  1280K       -       96
 netgraph_msg 10755  1020K       -  4177567
     netgraph  7200  1824K       -     7231

Läuft die Warteschlange über, leakt der Speicher. Unter Last (normal aber nicht) verliere ich so um die 500 Byte in 10 Sekunden. Mit 10 GB unbenutzem Hauptspeicher halte ich das einige Monate aus.

Trotzdem habe ich den Code abgeändert und warte nicht mehr auf den letzten Slot, sondern lasse 10% frei. Damit ist das Speicherleck geschlossen. Und nun kann ich mich auf die eigentliche Fehler konzentrieren.

Jun 17 15:12:38 kernel: Freeing delayed in netgraph_msg returned 'Detecting modify after free'
Jun 17 15:12:44 kernel: 1. retry to throw away broken NAT tables (will leak memory)

Kurz nachdem netgraph_msg aus dem Ruder gelaufen ist, bemerkt auch die libalias das Problem und findet in ihrem Speicherbereich die schon bekannte netgraph Nachricht. Kurz darauf stürzt das System ab.

All dies deutet auf einen Fehler in der Netgraph Nachrichtenverarbeitung hin. Für ein entsprechendes Debugging brauche ich nun aber einen vollständigen Memorydump und dafür brauche ich eine Testmaschine. Die rennt schon grundsätzlich unter Hyper-V.

Nachtrag

Mit dem Verzicht auf das Löschen von Interface-Knoten konnten die Fehler bisher vollständig beseitigt werden.

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?