Skip to content
Thomas Mangin edited this page Jul 20, 2026 · 7 revisions

Pre-Alpha. This page describes behavior that may change.

Ze includes a native L2TPv2 (RFC 2661) LNS/LAC implementation for use as a BNG (Broadband Network Gateway) component. It terminates L2TP tunnels over UDP, runs PPP negotiation (LCP, authentication, IPCP/IPv6CP), assigns peer IPs, and hands the kernel data plane to the l2tp_ppp module via netlink. Subscriber routes are tracked for redistribution into BGP.

The reliable delivery engine supports control message priority: Hello keepalives and StopCCN teardowns are sent ahead of bulk ICRQ/ICRP session-setup traffic, preventing tunnel liveness issues under high session churn.

Configuration

l2tp {
    enabled true;
    shared-secret <secret>;       // CHAP-MD5 tunnel auth (RFC 2661 S4.2)
    auth-method chap-md5;          // PPP Auth-Protocol first advertised
    allow-no-auth false;           // explicit opt-in required for no-auth
    hello-interval 60;             // seconds of peer silence before HELLO
    max-tunnels 1024;              // 0 = unbounded (16-bit ceiling)
    max-sessions 1024;             // per-tunnel, 0 = unbounded
}

environment {
    l2tp {
        server main {
            ip 0.0.0.0;
            port 1701;
        }
    }
}

Presence of l2tp {} with any content implies the subsystem is enabled. Set enabled false to disable explicitly.

Originating tunnels (LAC / initiator)

L2TP previously only answered tunnels (the LNS role). It can now also originate them (the LAC / initiator role). It implements the initiator half of the L2TPv2 control channel: it encodes the request messages it previously only parsed (SCCRQ, SCCCN, ICRQ, ICCN, OCRQ) and parses the replies it previously only encoded (SCCRP, ICRP, OCRP). A dial sends SCCRQ, completes the SCCRP handshake with mutual CHAP-MD5, and adopts the tunnel ID the peer assigns in its SCCRP as the tunnel ID of every message it sends afterwards.

A remote list under l2tp {} declares dial targets. Declaring a remote grants no dial by itself: an operator command (RPC) or a PPPoE relay binding drives the actual dial.

l2tp {
    enabled true;
    remote lns-retail {
        address 203.0.113.10;   // control-plane IP to dial (SCCRQ destination)
        port 1701;              // remote UDP port (default 1701)
        shared-secret <secret>; // per-remote CHAP-MD5 (empty = no Challenge)
        outgoing-calls true;    // permit `request l2tp outgoing-call` to this remote
    }
    relay internet {            // PPPoE Service-Name -> L2TP incoming call (LAC)
        remote lns-retail;      // must reference an l2tp/remote above
    }
}

Outgoing call

Place an LNS-side outgoing call (RFC 2661 S10.4) with:

request l2tp outgoing-call remote lns-retail called <number>

The command dials the remote if no tunnel is up, sends an OCRQ, and blocks until the call establishes (returning the local and remote session IDs) or fails. On failure it reports the cause and, when known, the RFC 2661 Result Code: tunnel auth reject, tie-breaker loss, peer CDN, or timeout. The remote must have outgoing-calls true.

Relay binding

The relay binding is the LAC path: a PPPoE subscriber whose Service-Name matches is relayed into an L2TP incoming call (ICRQ) toward the bound remote instead of terminating PPP locally. The binding key is the PPPoE Service-Name (empty string matches a subscriber that requests no specific service), and remote must name a declared l2tp/remote. See PPPoE for the subscriber-side view.

Initiator tunnel interop is verified against xl2tpd.

PPP negotiation

When a session is established (ICRQ/ICRP/ICCN exchange), the subsystem creates kernel L2TP tunnel and session resources via Generic Netlink, opens a PPPoL2TP socket, and attaches a /dev/ppp channel and unit. The kernel creates a pppN interface.

PPP negotiation proceeds through these phases:

  1. LCP (RFC 1661): MRU, authentication method, magic number, echo keepalive. LCP proxy (RFC 2661 S18) is supported.
  2. Authentication: PAP (RFC 1334), CHAP-MD5 (RFC 1994), or MS-CHAPv2 (RFC 2759). Dispatched to the registered auth handler.
  3. IPCP (RFC 1332): IPv4 address assignment + DNS options (RFC 1877).
  4. IPv6CP (RFC 5072): interface identifier negotiation. Runs in parallel with IPCP when both NCPs are enabled.

LCP Auth-Protocol Nak/Reject fallback is supported. Periodic re-auth is configurable via the reauth-interval YANG leaf under l2tp > auth (the former ze.l2tp.auth.reauth-interval env var has been promoted to YANG config). NCP-related env vars (ze.l2tp.ncp.*) have also been promoted to YANG config leaves.

Authentication

Ze separates PPP auth wire format from credential validation. Two auth handlers ship:

l2tp-auth-local

Built-in static user list:

l2tp {
    auth {
        local {
            user alice {
                password hunter2;
            }
        }
    }
}

l2tp-auth-radius

RADIUS client plugin providing:

  • Access-Request: PAP/CHAP-MD5/MS-CHAPv2 credential forwarding with failover and retry.
  • Accounting: Start, Stop, and Interim-Update records (RFC 2866).
  • CoA/DM: Change of Authorization and Disconnect-Message listener (RFC 5176) for RADIUS-initiated session changes and disconnects. The listener is off until coa-port is set (no default, so an upgrade never starts accepting CoA on its own), and requests are accepted only from the configured RADIUS server addresses.
l2tp {
    auth {
        radius {
            nas-identifier ze-lns;
            timeout 3;
            retries 3;
            acct-interval 300;
            coa-port 3799;             // RFC 5176 CoA/Disconnect listener; unset = off (no default)
            server 10.0.0.10 {
                port 1812;
                shared-key radiussecret;
            }
        }
    }
}

IP address pool

Bitmap-backed IPv4 address pools:

l2tp {
    pool {
        ipv4 {
            gateway 10.100.0.1;
            start 10.100.0.2;
            end 10.100.255.254;
            dns-primary 8.8.8.8;
            dns-secondary 8.8.4.4;
        }
    }
}

Session-down events release allocated addresses back to the pool.

Traffic shaping

The l2tp-shaper plugin applies TC rules on pppN interfaces. RADIUS CoA can update the rate dynamically after the session is up.

l2tp {
    shaper {
        qdisc-type tbf;            // tbf or htb
        default-rate 10mbit;       // download rate for new sessions
        upload-rate 2mbit;         // upload rate (defaults to default-rate)
    }
}

CQM (Call Quality Metrics)

Firebrick-style CQM monitoring for L2TP sessions:

  • Per-session event ring: circular buffer of state transitions (tunnel-up, session-up, session-down, echo-rtt, disconnect-requested). Disconnect events include the actor, reason text, and optional cause code.
  • Per-login CQM sample ring: 100-second aggregated buckets with echo RTT statistics (min, avg, max), echo count and loss ratio, and session state. 24h retention (864 buckets) by default.

LCP echo probes measure RTT. Each 100s bucket is closed and appended to the sample ring. The web UI streams new buckets via SSE for live chart updates.

Prometheus metrics

L2TP metrics

Metric Type Description
ze_l2tp_sessions_active gauge Sessions in established state
ze_l2tp_sessions_starting gauge Sessions in negotiation
ze_l2tp_sessions_finishing gauge Sessions being torn down
ze_l2tp_tunnels_active gauge Active tunnels
ze_l2tp_session_rx_bytes_total counter RX bytes on pppN interface
ze_l2tp_session_tx_bytes_total counter TX bytes on pppN interface
ze_l2tp_lcp_echo_rtt_seconds histogram LCP echo round-trip time
ze_l2tp_lcp_echo_loss_ratio gauge Current 100s bucket echo loss ratio

RADIUS metrics

Metric Type Description
ze_radius_up gauge Server reachability (1=up, 0=down)
ze_radius_auth_sent_total counter Access-Request packets sent
ze_radius_acct_sent_total counter Accounting-Request packets sent
ze_radius_interim_sent_total counter Interim-Update packets sent

Web UI

The web interface at /l2tp provides session management and CQM graphing (requires authentication).

URL Purpose
/l2tp Session list with sortable columns
/l2tp/<sid> Session detail: state, PPP options, CQM chart, event timeline, disconnect
/l2tp/<login>/samples CQM buckets as columnar JSON
/l2tp/<login>/samples.csv CQM buckets as CSV download
/l2tp/<login>/samples/stream SSE stream pushing new CQM buckets every 100s
/l2tp/<sid>/disconnect Disconnect session (requires reason; optional cause code)

The CQM chart renders 24h of historical data, then appends new 100-second buckets in real time via SSE.

CLI commands

Read commands

Command Output
show l2tp Aggregate counters: tunnel count, session count, listener count.
show l2tp tunnels Array of tunnel summaries.
show l2tp tunnel <id> One tunnel's detail (AVPs, capabilities, session list).
show l2tp sessions Array of session summaries (flattened across tunnels).
show l2tp session <id> One session's detail (PPP state, assigned IP, speeds).
show l2tp statistics Protocol counters.
show l2tp listeners Bound UDP endpoints.
show l2tp config Effective runtime config; shared-secret redacted.

Destructive commands

Command Effect
clear l2tp tunnel id <id> Sends StopCCN Result Code 6 (administrative).
clear l2tp tunnel all Same, for every live tunnel.
clear l2tp session id <id> [reason <text>] [cause <code>] Sends CDN Result Code 3 (administrative).
clear l2tp session all Same, for every live session.

The clear verb already means tear down, so no teardown token is needed. reason <text> records a free-text audit reason in the per-session event ring; cause <code> records a RADIUS Disconnect-Cause value (uint16) alongside it.

Offline dispatcher

ze l2tp show, ze l2tp tunnel {id <id> | all}, and ze l2tp session {id <id> | all} are client commands that forward to the running daemon over SSH and print the same JSON the daemon handler returns. Each accepts --user <name> (short alias -u) to name the SSH login user; without it the login resolves to the zefs super-admin. A flag left in the positional tail is rejected rather than forwarded, so a misplaced --user never silently answers for the default user.

Route redistribution

When a PPP NCP completes, the subsystem emits a route-change event with the assigned peer IP as a /32 (IPv4) or /128 (IPv6) prefix:

redistribute {
    destination bgp {
        import l2tp {
            family [ ipv4/unicast ipv6/unicast ];
        }
    }
}

Reload semantics

Field Reload behaviour
shared-secret Hot-apply; takes effect on new SCCRQs.
hello-interval Hot-apply; new tunnels use the new interval.
max-tunnels Hot-apply at next admission decision.
max-sessions Hot-apply at next admission decision.
auth-method Hot-apply to new PPP sessions.
allow-no-auth Hot-apply to new PPP sessions.
enabled flip Rejected with WARN. Restart required.
Listener endpoint change Rejected with WARN. Restart required.

See also

  • PPPoE for direct-attach subscriber access (alternative to L2TP).
  • Firewall for subscriber traffic filtering.
  • Policy Routing for per-subscriber traffic steering.
  • Plugins for the plugin list.

Adapted from main/docs/guide/l2tp.md.

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally