Aussetzer im L2TP

PPP-Verbindungen werden von Carrier zum ISP per L2TP-Tunnel zugeführt. Diese Tunnel tragen einige tausend Sessions gleichzeitig. Deswegen sollten sie gegen Störungen sehr nachsichtig sein. Und dazu hat man sich einiges einfallen lassen.

Der TCP-Nachbau

L2TP ist gemäß RFC 2661 ein Tunnelprotokoll mit gesichertem Kanal für Kontrollnachrichten.

Dazu führt jede Seite 16bit-Sequenznummern für die eignenen Nachrichten ein, die sich mit jeder Kontrollnachricht erhöhen. Empfängt der L2TP-Stack der Gegenstelle eine neue Nachricht, so sendet es selbst die fremde Sequenznummer wieder als Bestätigung bei der nächsten ausgehenden Kontrollnachricht mit. Liegt keine Kontrollnachricht vor, so kann auch eine leere Nachricht gesendet werden, die praktisch nur das ACK transportiert.

Damit die Verbindung nicht so stottert, vereinbaren beide Seiten ein Fenster an "unbestätigten" Nachrichten, die sich noch im Transport befinden dürfen. Dieses Fenster ist üblicherweise vier bis acht Nachrichten groß.

Das entspricht vollständig dem bekannten TCP Protokoll.

Um die Funktionsweise dieses L2TP-Kanals muß man sich normalerweise nicht kümmern: Man wird eine Kontrollnachricht ein und wartet auf die Antwort bzw. die Kontrollnachricht der Gegenseite. Dafür reicht ein Fenster für eine Handvoll Nachrichten völlig aus.

Eine explizite Möglichkeit, die Unerreichbarkeit der Gegenseite zu bemerken, gibt es nicht. (TCP hat dafür RST und FIN.) Deswegen definiert man sich einen Timeout, innerhalb dessen eine Nachricht von der Gegenseite bearbeitet bekommen haben möchte.

Wenn es kurz klemmt?

Wie immer bringt ein Blick in den Sourcecode (hier sys/netgraph/ng_l2tp.c von FreeBSD) Klarheit:

/* Some hard coded values */
#define L2TP_MAX_XWIN           128                     /* my max xmit window */
#define L2TP_MAX_REXMIT         5                       /* default max rexmit */
#define L2TP_MAX_REXMIT_TO      30                      /* default rexmit to */
#define L2TP_DELAYED_ACK        ((hz + 19) / 20)        /* delayed ack: 50 ms */

Insgesamt läßt sich das Fenster auf bis zu 128 Nachrichten vergrößern. Aber wie erfolgt die Wiederholung?

/* Restart timer, this time with an increased delay */
delay = (seq->rexmits > 12) ? (1 << 12) : (1 << seq->rexmits);
if (delay > priv->conf.rexmit_max_to)
    delay = priv->conf.rexmit_max_to;
ng_callout(&seq->rack_timer, node, NULL,
    hz * delay, ng_l2tp_seq_rack_timeout, NULL, 0);

Auf die ACKs von Nachrichten wird also bis zu 30 Sekunden gewartet. Und zwar immer expotentiell länger werdend:

Resend Delay
Versuch 1 2 3 4 5 6 7 ...
Wartezeit 1 2 4 8 16 30 30 30

Der Default liegt bei fünf Wiederholungen, was insgesamt eine Kommunikationslücke von 31 Sekunden abdecken kann.

Es nützt also nichts, einen globalen Timeout von mehr als diesen Zeitraum anzusetzen, da spätestens nach dieser halben Minute die Verbindung lokal für tot erklärt wird, wenn Nachrichten vorliegen.

Will man höhere Timeouts haben, so muß man die Wiederholungsraten auf beiden Seiten anheben. Diese Parameter werden ausgehandelt:

ncgtl> msg [0x00030854]: getconfig
Args:   { enabled=1 match_id=1 tunnel_id=0x6e9f peer_id=0x5685
         peer_win=8 rexmit_max=8 rexmit_max_to=10 }

Konkret werden hier also ein Fenster von acht Nachrichten, sowie acht Wiederholungen bis zu einem Maximalwert von 10 Sekunden pro Wiederholung vereinbart. Dies bedeutet also, daß diese Kontrollnachrichten nach 1+2+4+8+10+10+10+10 = 45 Sekunden bestätigt sein müssen.

Da L2TP Pakete über normales IP-Routing laufen, sollte die Konvergenzzeit dieser Netze kleiner sein als diese halbe Minute.

Überlastverhalten

Ganz anders ist die Situation, wenn auf einem L2TP-Tunnel einige tausend Sessions gleichzeitig laufen. Die Anzahl der hier benötigten Kontrollnachrichten ist dann sehr viel größer. Es erreicht schnell mal einige hundert Nachrichten auf einen Schlag.

Aber was passiert, wenn mehr Nachrichten gesendet werden sollen, als in den Puffer der Nachrichtenfensters passen?

Auch hier schult wieder ein Blick in den Quellcode die Sachkenntnis:

struct l2tp_seq {
        ...
        struct mbuf             *xwin[L2TP_MAX_XWIN];   /* transmit window */
};
/*
 * Handle an outgoing control frame.
 */
static int
ng_l2tp_rcvdata_ctrl(hook_p hook, item_p item)
{
        ...
        /* Find next empty slot in transmit queue */
        for (i = 0; i < L2TP_MAX_XWIN && seq->xwin[i] != NULL; i++);
        if (i == L2TP_MAX_XWIN) {
                mtx_unlock(&seq->mtx);
                priv->stats.xmitDrops++;
                m_freem(m);
                ERROUT(ENOBUFS);
        }
        seq->xwin[i] = m;
        ...
}

Eine Kontrollnachricht wird also zur Versendung in den 128 Nachrichten großen Kernelpuffer geworfen. Aus diesem werden soviele Nachrichten versendet, wie in das vereinbarte Sendefenster passen. Nach Bestätigung einer Aussendung werden die so bestätigten Nachrichten durch umkopieren nach vorn entfernt. Das Feld xwin ist also stets einseitig bündig mit Nachrichten gefüllt.

Kommen nun mehr Kontrollnachrichten herein als versendet werden können, puffert der Kernel bis zu 128 Nachrichten intern zwischen.

Reicht der Platz für diese 128 Nachrichten nicht aus, weil beispielsweise das Routing zwischen den Gegenstellen kurz gestört ist, oder weil die Gegenstelle gerade mächtig beschäftigt ist, so meldet der Kernel an den sendenden Prozess ENOBUFS "Kein Platz mehr".

Der MPD reagiert darauf verschnupft:

        if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1)
                goto fail;

        /* Done */
        goto done;

fail:
        /* Close up shop */
        Perror("L2TP: error sending ctrl packet");
        ppp_l2tp_ctrl_close(ctrl, L2TP_RESULT_ERROR,
            L2TP_ERROR_GENERIC, strerror(errno));

done:
        /* Clean up */

Unabhängig vom Grund des Fehlers wird die gesamte L2TP Verbindung hingeworfen. Im Log sieht das dann so aus.

mpd: L2TP: error sending ctrl packet: No buffer space available

Der offenkunde Fix besteht darin, es mehrfach zu probieren:

    int rtn, retry = 10, delay = 1000;

retry:
    if ((NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1) {
        if (errno == ENOBUFS && retry > 0) {
            Log(LG_ERR, ("[%s] XMIT stalled, retrying...", ctrl));
            usleep(delay);
            retry--;
            delay *= 2;
            goto retry;
        }

Selbstverständlich hilft das nicht viel. Denn in dieser Schleife kann der Prozeß nicht beliebig viel Zeit vergeuden. Er hat schließlich noch anderes zu tun.

Was man also braucht ist eine echte, dynamisch wachsende Warteschlange für diese Kontrollnachrichten.

--- mpd-5.6/src/l2tp_ctrl.c     2011-12-21 15:58:49.000000000 +0100
+++ mpd-5.6-lutz/src/l2tp_ctrl.c        2013-09-27 16:08:15.000000000 +0200
@@ -153,6 +153,14 @@
        int                     req_avps[AVP_MAX + 1];
 };

+/* Control message queue */
+struct ctrl_queue_entry {
+   void *                      data;
+   unsigned int                        len;
+   STAILQ_ENTRY(ctrl_queue_entry) next;
+};
+STAILQ_HEAD(l2tp_ctrl_queue, ctrl_queue_entry);
+
 /* Control connection */
 struct ppp_l2tp_ctrl {
        enum l2tp_ctrl_state    state;                  /* control state */
@@ -164,6 +172,7 @@
        char                    path[32];               /* l2tp node path */
        int                     csock;                  /* netgraph ctrl sock */
        int                     dsock;                  /* netgraph data sock */
+       struct l2tp_ctrl_queue  *dsock_queue;           /* queue if netgraph is
        u_char                  *secret;                /* shared secret */
        u_int                   seclen;                 /* share secret len */
        u_char                  chal[L2TP_CHALLENGE_LEN]; /* our L2TP challenge
@@ -533,6 +544,10 @@
            ctrl->mutex, ppp_l2tp_data_event, ctrl, PEVENT_READ,
            ctrl->dsock) == -1)
                goto fail;
+
+       /* Initialize send queue */
+       ctrl->dsock_queue = Malloc(CTRL_MEM_TYPE, sizeof(*ctrl->dsock_queue));
+       STAILQ_INIT(ctrl->dsock_queue);

        /* Copy initial AVP list */
        ctrl->avps = (avps == NULL) ?
@@ -616,5 +632,6 @@
        ppp_l2tp_avp_list_destroy(&ctrl->avps);
        ghash_remove(ppp_l2tp_ctrls, ctrl);
        ghash_destroy(&ctrl->sessions);
-       Freee(ctrl->secret);
+       l2tp_ctrl_queue_destroy(&ctrl->dsock_queue);
+       Freee(ctrl->secret);
        Freee(ctrl);
        if (ppp_l2tp_ctrls != NULL && ghash_size(ppp_l2tp_ctrls) == 0)
                ghash_destroy(&ppp_l2tp_ctrls);
@@ -1261,8 +1290,32 @@
                ppp_l2tp_ctrl_dump(ctrl, avps, "L2TP: XMIT(0x%04x) ",
                    ntohs(session_id));
        }
-       if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1)
-               goto fail;
+
+        /* Schedule packet */
+        n = Malloc(CTRL_MEM_TYPE, sizeof(*n));
+       n->data = data; data = NULL;
+       n->len = 2 + len;
+       MUTEX_LOCK(gNgMutex);
+       STAILQ_INSERT_TAIL(ctrl->dsock_queue, n, next);
+       MUTEX_UNLOCK(gNgMutex);
+
+       MUTEX_LOCK(gNgMutex);
+       /* Try to send outstanding messages */
+       while(!STAILQ_EMPTY(ctrl->dsock_queue)) {
+          struct ctrl_queue_entry *o = STAILQ_FIRST(ctrl->dsock_queue);
+          if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, o->data, o->len) == -1
+             if (errno == ENOBUFS) {
+                Log(LG_ERR, ("[%p] L2TP: XMIT stalled, queueing ...", ctrl));
+                break;
+             }
+             MUTEX_UNLOCK(gNgMutex);
+             goto fail;
+          }
+          STAILQ_REMOVE_HEAD(ctrl->dsock_queue, next);
+          Freee(o->data);
+          Freee(o);
+       }
+       MUTEX_UNLOCK(gNgMutex);

        /* Done */
        goto done;
@@ -1277,7 +1330,6 @@
        /* Clean up */
        ppp_l2tp_avp_destroy(&avp);
        ppp_l2tp_avp_list_destroy(&avps);
-       Freee(data);
 }

 /*
@@ -1412,6 +1464,22 @@
 }

 /*
+ * Free all outstanding entries in the control message queue.
+ * Remove the queue.
+ */
+static void
+l2tp_ctrl_queue_destroy(struct l2tp_ctrl_queue ** q) {
+   while(!STAILQ_EMPTY(*q)) {
+      struct ctrl_queue_entry * n = STAILQ_FIRST(*q);
+      STAILQ_REMOVE_HEAD(*q, next);
+      Freee(n->data);
+      Freee(n);
+   }
+   Freee(*q);
+}
+
+
+/*
  * Notify link side that the control connection has gone away
  * and begin death timer.
  *

Damit ist es möglich, die Kontrollnachrichten beliebig lange zwischenzuspeichern, auch wenn es auf dem L2TP Server mal heiß hergeht.

An den Timeouts für die Verbindung ändert sich dabei noch nicht viel. Diese Resende-Timeouts müssen anderweitig angepaßt werden.

Post a comment

Related content