Pages

05/02/2026

Running a PPPoE "LAC" with open source software (L2TPNS)

Some time ago now I was fortunate enough acquire a ZyXEL VES1724-55C VDSL DSLAM.

I had a quick play with it, and then loaned it to someone that also had a hankering to play with a DSLAM. They did a great job of documenting their findings on their blog, alexhorner.cc

One of my aspirations was to replicate a UK style wholesale internet provider similar to Openreach. In this model, DSL users send PPPoE, which is terminated on a LAC (L2TP Access Concentrator) operated by the wholesaler. The LAC then does a look up based on the domain in the users username (usually via RADIUS) and then tunnels the PPP via L2TP to the ISPs LNS (L2TP Network Server) where the ISP can terminate the PPP and provide one of their IP addresses. 

Or at least that's how it used to work - I'm not sure how things have since evolved with FTTP etc, with other words like BRAS and BNG being thrown into the mix, so assume this information is out-of-date.

Forewarning: This isn't meant to be a complete step-by-step guide, just some clues to get you started. Sorry to disappoint! 


A high resolution diagram showing the basics of a LAC/LNS setup.


I already had some experience with running an LNS on a Mikrotik router but crucially Mikrotik do not support the LAC part of the equation. I have a plethora of aging Cisco equipment that could do it, but none of it was modern enough to handle ~1Gb/s of DSL traffic.

Other open source options will happily terminate the PPPoE but won't tunnel PPP over L2TP to an LNS. BSD's MPD5 can do it, but the LNSs must be statically defined in the configuration, which isn't quite what I wanted.

So I shelved the idea and got distracted by another project or five. But EMF is coming up again this year, and its time to start thinking about cosplaying as a Telco again. So I took a punt and asked in the DN42 IRC channel, which is full of friendly networking folk, and welterde suggested I check out l2tpns

And this is where things get confusing. The sourceforge page says "l2tpns is half of a complete L2TP implementation. It supports only the LNS side of the connection". The description on the  FFDN Repo says "l2tpns is a layer 2 tunneling protocol network server (LNS)" but in the README it mentions it can function as a LAC. As far as I can tell, at some point the project was forked and/or acquired a new maintainer and that's when the features diverged and the conflicting information appeared.

So, lets test it out and see if it does what we need.

Building it is straightforward. Here's what I did on Debian 13:

apt install libcli-dev build-essential git
cd /usr/src
git clone https://code.ffdn.org/l2tpns
cd l2tpns
make
make install

You'll then need to edit /etc/l2tpns/startup-config to suit your environment. It's self-explanatory, but here's the obvious ones that need changing. Commenting out the log_file will send the output straight to stdout whilst you experiment: 
#set log_file "/var/log/l2tpns"
set primary_radius 10.0.0.10
set radius_secret "secret"
set pppoe_if_to_bind enp4s0.444

Next you'll need to configure a RADIUS server.  I've created a simple mock in python that you can use here - Spoiler alert, I end up using docker later.

l2tpns is expecting the following attributes in the response:
Tunnel-Type: 3
Tunnel-Medium-Type: 1
Tunnel-Server-Endpoint: # IP of your LNS
Tunnel-Assignment-Id: 1 # Must be unique per LNS!
Tunnel-Password: Hashed Secret

You'll also need an LNS. l2tpns will do it but its not documented here (sorry). You can however run an LNS on a Mikrotik router

Start l2tpns:
l2tpns

Initiate a connection with your PPPoE client of choice. If everything is working (including the LNS) you should see something like:

2026-02-03 22:25:32 00/00 RCV pppoe_disc: Code PADI from BC:24:11:9D:D7:68
2026-02-03 22:25:32 00/00 SENT pppoe_disc: Code PADO to BC:24:11:9D:D7:68
2026-02-03 22:25:32 00/00 RCV pppoe_disc: Code PADR from BC:24:11:9D:D7:68
2026-02-03 22:25:32 00/00 SENT pppoe_disc: Code PADS to BC:24:11:9D:D7:68
2026-02-03 22:25:32 01/01 LCP: send ConfigReq (PAP) including MP options
2026-02-03 22:25:32 01/01 LCP: recv ConfigReq
2026-02-03 22:25:32 01/01 LCP: send ConfigAck
2026-02-03 22:25:33 01/01 LCP: recv ConfigReq
2026-02-03 22:25:33 01/01 LCP: send ConfigAck
2026-02-03 22:25:34 01/01 LCP: recv ConfigReq
2026-02-03 22:25:34 01/01 LCP: send ConfigAck
2026-02-03 22:25:35 01/01 No ACK for LCP ConfigReq... resending
2026-02-03 22:25:35 01/01 LCP: send ConfigReq (PAP) including MP options
2026-02-03 22:25:35 01/01 LCP: recv ConfigAck
2026-02-03 22:25:35 01/01 LCP: Opened, phase Authenticate
2026-02-03 22:25:35 01/01 PAP login foo/foo
2026-02-03 22:25:35 01/01 Allocated radius 1
2026-02-03 22:25:35 01/01 Sending login for foo/foo to RADIUS
2026-02-03 22:25:35 01/01 Received RADIUSAUTH, radius 1 response for session 1 (Access-Accept, id 0)
2026-02-03 22:25:35 01/01    Radius reply Tunnel-Type:0 3
2026-02-03 22:25:35 01/01    Radius reply Tunnel-Medium-Type:0 1
2026-02-03 22:25:35 01/01    Radius reply Tunnel-Server-Endpoint:0 10.222.0.156
2026-02-03 22:25:35 01/01    Radius reply Tunnel-Assignment-Id:0 4
2026-02-03 22:25:35 01/01    Radius reply Tunnel-Password:0 secret123
2026-02-03 22:25:35 01/01 Select Tunnel Remote LNS for assignment_id == 4
2026-02-03 22:25:35 02/00 Create New tunnel to REMOTE LNS 10.222.0.156 for user foo
2026-02-03 22:25:35 02/00 Sent SCCRQ to REMOTE LNS
2026-02-03 22:25:35 02/00 Control message (127 bytes): (unacked 1) l-ns 1 l-nr 0 r-ns 0 r-nr 1
2026-02-03 22:25:35 02/00 received challenge response from REMOTE LNS
2026-02-03 22:25:35 02/00 Received SCCRP
2026-02-03 22:25:35 02/00 sending SCCCN to REMOTE LNS
2026-02-03 22:25:35 02/00 sent SCCCN as 1
2026-02-03 22:25:35 02/00 Control message (0 bytes): (unacked 1) l-ns 2 l-nr 1 r-ns 1 r-nr 2
2026-02-03 22:25:35 02/00 REMOTE LNS acked our SCCCN 1
2026-02-03 22:25:35 02/00 Open New session to REMOTE LNS 10.222.0.156 for user: foo
2026-02-03 22:25:35 02/02 Sent ICRQ to REMOTE LNS (far ID 25)
2026-02-03 22:25:35 02/02 Control message (16 bytes): (unacked 1) l-ns 3 l-nr 1 r-ns 1 r-nr 3
2026-02-03 22:25:35 02/02 Received ICRP
2026-02-03 22:25:35 02/02 Sending ICCN
2026-02-03 22:25:35 02/00 Control message (0 bytes): (unacked 1) l-ns 4 l-nr 2 r-ns 2 r-nr 4
2026-02-03 22:26:35 02/00 Control message (8 bytes): (unacked 0) l-ns 4 l-nr 2 r-ns 2 r-nr 4
2026-02-03 22:26:35 02/00 Received HELLO
2026-02-03 22:27:35 02/00 Control message (8 bytes): (unacked 0) l-ns 4 l-nr 3 r-ns 3 r-nr 4

Performance

l2tpns is quite old and doesn't have any support for multithreading. As a result the throughput is limited by the performance of a single CPU core

That said, a lacklustre Dell Thin Client with a 4x 2.4GHz AMD GX-424CC CPU will handle around 300-400Mb/s of traffic in a single direction which is probably good enough for a handful of VDSL users, but less so for modern ISP.

Can you run multiple instances on the same host?

What if we run multiple instances on the same host to spread load over multiple CPUs?

I gave this a try by setting up a bridge interface and attaching and two veths, and attaching an instance of l2tpns to each interface.

But things got weird. The PPPoE client would send PADIs, the LACs would respond with their PADO, and then the client would send its PADR to select the LAC. So far, so good. But then the L2TP tunnel would fail to come up, and the instance of l2tpns that wasn't handling the PPPoE connection would start throwing l2tp errors in the logs, suggesting they were conflicting somewhere.
I tried disabling kernel acceleration, and renaming the tun interface in the startup-config, but nothing seemed to help. It may well be possible to run multiple instances on the same host, but I couldn't figure it out.

What about Docker?

What if Docker will give us enough isolation to solve this problem? I tried running two instances of l2tpns and it worked well, with the load distributed across two cores. I've stuck the Dockerfile and docker-compose files in the docker-l2tpns repo - it's not for production use, but it should let you experiment.

Other bugs and Observations

I encountered a few issues on the way, and made some observations: 

1) Mikrotik and L2TPNS interoperability

When using l2tpns as the LNS (Not documented here, sorry) and using a Mikrotik as a PPPoE client the connection would fail and go into a loop of CodeReject messages.
I ran some packet captures and could see that l2tpns was offering the IP 0.0.0.0 to the client over and over again, which was being rejected. This didn't make much sense as I could see the RADIUS request had already gone out and an IP had been assigned, so l2tpns should have been able to offer a valid IP. After digging through the code I noticed it would fallback to the l2tp bind_address, which was set to 0.0.0.0. Setting it to the IP of the actual interface resolved the issue.

2) Misbehaviour on ARM

Whilst I was testing I thought I'd try l2tpns on a Raspberry Pi 5. I got it setup but both l2tpns and the RADIUS server were complaining the password was incorrect. After an hour or two of checking, double and triple checking everything, I had the idea that maybe 64 bit arm was causing issues with the MD5 implementation?

I'm not much of a cryptographer or a C developer, but this line in md5.c stood out to me. So without really know what I was doing, I appended || defined(__aarch64__) and recompiled. Sure enough, things started to work again! l2tpns then seemed to be working normally, but I have no idea what other issues with ARM might be buried in there. 

3) Tunnel-Assignment-Id must be unique per Tunnel Endpoints

Whilst writing the mock RADIUS server I noticed that if I restarted it with a new Tunnel-Endpoint defined, l2tpns would attempt to forward the connection to the previous LNS. After doing some digging, it turns out the Tunnel-Assignment-Id must be unique between endpoints.

4) L2TP to L2TP Tunneling works

l2tpns supports operating as an LNS and forwarding the L2TP to another LNS based on the username in the PPP. This might be useful in scenarios where a LAC doesn't support forwarding to dynamic LNSs (e.g MPD5). Instead you could point it at a pool of l2tpns LNSs which handle the radius look ups and forward to the final LNS.

Conclusion

It was great to stumble across some free software that can provide the functions of a proper LAC and LNS. The performance isn't amazing, but it's good enough for a hobbyist experimenting with the technology, and a lot cheaper than the Enterprise alternatives from big vendors. 

No comments:

Post a Comment