Sign inSign up

eukhlg/l2tp-go

By eukhlg

Updated 13 days ago

Image
0

2.6K

eukhlg/l2tp-go repository overview

l2tp-go

A lightweight Go L2TPv2 server. The current server supports raw (unencrypted) L2TP, PPP LCP, MS-CHAPv2 and PAP authentication, IPCP, IPv4, and a native TUN data path on Linux and macOS.

The protocol and server packages are also usable as a public Go module. Network frontends can inject their own packet connections and consume authenticated sessions as raw IPv4 packet streams without enabling TUN or host routing. See Embedding l2tp-go.

IPsec status

The server includes an IKEv1/IPsec implementation that has been successfully tested with the built-in L2TP/IPsec clients on Windows, Linux, and macOS using pre-shared-key authentication.

The implementation is functional but has not undergone an independent security audit or extensive production-scale testing. It is suitable for interoperability testing and controlled deployments, but should not yet be treated as a production security boundary.

Supported capabilities
  • IKEv1 Main Mode responder with pre-shared-key authentication
  • IKEv1 Quick Mode
  • AES-128/192/256-CBC and 3DES-CBC encryption
  • SHA-1 and SHA-256 for IKE Phase 1
  • Diffie-Hellman groups 2 and 14
  • ESP transport mode with AES or 3DES
  • HMAC-SHA1-96 ESP authentication
  • RFC 3947/3948 NAT traversal over UDP port 4500
  • IPv4 address, subnet, and range traffic selectors
  • NAT Original Address payloads
  • ESP anti-replay protection
  • IKE and ESP lifetimes
  • RFC 3706 Dead Peer Detection
  • Authenticated IKE and ESP deletion
  • Client-initiated Quick Mode rekeying with overlapping ESP associations
  • Client-initiated Phase 1 replacement with child-SA ownership transfer
  • Raw ESP transport for native Linux deployments
  • Protected L2TP handoff restricted to authenticated UDP port 1701 traffic
Current limitations and protocol behavior

The implementation intentionally rejects obsolete DH group 1 and MD5-based transforms.

Current limitations
  • No Phase 2 Perfect Forward Secrecy
  • No server-initiated IKE (Phase 1) rekeying
  • No IKE fragmentation
  • No IPsec tunnel mode
  • No arbitrary protected protocols
  • No L2TPv3 or EtherIP site-to-site operation
  • No RADIUS, Active Directory, or NT-domain authentication
  • No independently verified iOS or Android interoperability
  • Limited long-duration, high-load, malformed-traffic, and denial-of-service testing
  • No independent security audit
Protocol behavior

Main Mode preserves the initiator's exact SA bytes for HASH_I and HASH_R, validates proposal attributes, requires explicit AES key lengths, caches byte-identical replies for retransmissions, and clears sensitive exchange state after failure or expiry. It recognizes common Microsoft, DPD, and NAT-T vendor identifiers. RFC 3947 NAT detection enables authenticated migration to UDP port 4500 when NAT is detected or NAT-T is explicitly preferred.

Quick Mode validates HASH(1) and HASH(3), generates selector-bound HASH(2), derives independent inbound and outbound ESP key material, and installs an ESP pair only after the initiator's final confirmation. Phase 2 proposals requesting PFS are rejected until Quick Mode key exchange support is implemented.

Authenticated informational exchanges process Dead Peer Detection and IKE/ESP deletion messages. Proactive DPD probes retire a peer after three missed intervals. Client-initiated Quick Mode rekeys retain the previous ESP association during transition and automatically move active L2TP flows to the replacement association. Server-initiated Quick Mode is limited to three attempts with increasing delays; after that, the working child SA is retained while still valid and the server waits for a peer-initiated rekey. An established Phase 1 lifetime is a soft boundary: the responder keeps the working IKE SA until the client opens a replacement Main Mode SA. It transfers the existing child SA to the replacement without changing its SPIs, keys, sequence state, or remaining lifetime. Later Quick Mode rekeying remains independent of the Phase 1 switch. The server does not originate Main Mode, so server-initiated Phase 1 rekeying remains a limitation. Graceful server shutdown sends encrypted IKE deletion messages before closing the IKE and NAT-T listeners.

Configuration

The current release supports multiple users and automatically leases client addresses from a configured IPv4 pool.

The recommended starting point is l2tp-go.toml.example. Run it natively with:

sudo ./l2tp-go --config ./l2tp-go.toml

The configuration is strict: unknown TOML keys and invalid values stop startup with a descriptive error. User credentials are ordinary TOML values for now:

[authentication]
method = "mschapv2"

[[authentication.users]]
username = "alice"
password = "replace-me"

[[authentication.users]]
username = "bob"
password = "replace-me-too"

Server and pool-start settings accept either a full IPv4 address or a host offset within the subnet. For example, subnet 10.0.0.0/24, server address 1, pool start 100, and pool size 50 produce server 10.0.0.1 and leases 10.0.0.100 through 10.0.0.149.

[network]
subnet = "10.0.0.0/24"
server_address = "1"
client_pool_start = "2"
client_pool_size = 50
dns = "1.1.1.1"
mtu = 1400

Keep the real file out of source control and readable only by its owner. Legacy environment-variable overrides remain available for compatibility, but TOML is the primary and recommended configuration source.

Docker Compose

Docker deployment is intended for a Linux Docker host with /dev/net/tun. Docker Desktop on macOS does not expose a macOS utun device to Linux containers; use the native macOS instructions below instead.

Multi-platform images for linux/amd64 and linux/arm64 are published to both registries:

  • eukhlg/l2tp-go on Docker Hub
  • ghcr.io/eukhlg/l2tp-go on GitHub Container Registry

Use latest for releases and testing for the newest feature-branch build. Copy the example configuration, set the PSK, server identity, and user passwords, then start the released image:

cp l2tp-go.toml.example l2tp-go.toml
chmod 600 l2tp-go.toml
docker compose pull
docker compose up -d
docker compose logs -f l2tp

To test the newest feature-branch image or use GHCR instead, select the image before pulling:

export L2TP_IMAGE=eukhlg/l2tp-go:testing
# Or: export L2TP_IMAGE=ghcr.io/eukhlg/l2tp-go:testing
docker compose pull
docker compose up -d

For a local source build, bypass the registry with:

docker compose build
docker compose up -d

To keep the configuration at another path, set the Compose-only L2TP_CONFIG_FILE variable:

L2TP_CONFIG_FILE=/srv/l2tp/server.toml docker compose up -d

Compose publishes UDP ports 1701, 500, and 4500, grants NET_ADMIN and NET_RAW, mounts /dev/net/tun, and enables container-local forwarding and NAT. Docker port publishing cannot expose raw IP protocol 50, so containers use UDP/4500 NAT-T; native Linux deployments may use raw ESP. A graceful stop sends StopCCN to active tunnels before shutdown.

Stop and remove the service with docker compose down.

Run with TUN routing

Build the server:

go build -o l2tp-go ./cmd/l2tp-go

Set [tun].enabled = true in the TOML configuration. Command-line flags can override TOML when needed.

sudo ./l2tp-go --config ./l2tp-go.toml

macOS (-tun-name may be omitted to select an available utun):

sudo ./l2tp-go --config ./l2tp-go.toml \
  -tun-name ''

The process creates the native interface, configures its address and MTU, forwards client packets into it, reads return packets, and sends those packets back through the matching PPP session. It brings the interface down during a clean shutdown.

Native execution does not modify host IP forwarding or NAT. Configure those with the host firewall/router policy appropriate for the machine. The Docker entrypoint performs container-local NAT when [nat].enabled = true.

Client settings

For protected operation, configure a client for L2TP/IPsec with a pre-shared key, the server's public IPv4 address, and MS-CHAPv2 PPP authentication. Permit UDP 500 and UDP 4500 through host and cloud firewalls; permit IP protocol 50 for clients that do not need NAT traversal.

For raw test operation, configure a client for L2TP without IPsec, with MS-CHAPv2 authentication (or PAP when [authentication].method = "pap"), UDP port 1701, and the configured username and password. Many operating-system VPN interfaces require IPsec for L2TP and will not offer raw L2TP; use a client such as xl2tpd/pppd that explicitly permits raw L2TP.

Successful startup does not prove that UDP 1701 is reachable from outside the host. Verify host and cloud firewalls separately.

Interoperability test matrix

Run tests with [server].debug = 1 and [ipsec].force_natt = true first. Test the Windows built-in L2TP/IPsec client, NetworkManager-l2tp on Linux, and the macOS built-in L2TP over IPsec profile where available. Each client should be tested from a different NATed network and, when possible, from a directly addressed host.

For every platform verify: Main Mode and Quick Mode completion, UDP/4500 migration, PPP authentication, assigned address and DNS, traffic through TUN, DPD acknowledgement, an hour-bound client-initiated rekey, manual disconnect, container restart/reconnect, and two simultaneous users. Capture the server side with:

docker compose logs -f --since=5s --no-color l2tp 2>&1 | tee l2tp-ipsec.log

If negotiation stops, preserve the complete log from the first ike rx line through the rejection. The trace deliberately omits PSKs, passwords, key material, and decrypted traffic, so it is suitable for interoperability bug reports after reviewing public IP addresses.

Troubleshooting

If Compose restarts the container, inspect both its status and previous logs:

docker compose ps
docker compose logs --tail=200 l2tp
docker inspect --format '{{.State.ExitCode}} {{.State.Error}}' l2tp-go-l2tp-1

IPv6 and packets without an active PPP destination may appear on TUN. They are dropped without stopping the server. A fatal listener or TUN error is logged with the component name before the process exits.

For connection negotiation failures, enable tracing in the TOML configuration:

[server]
debug = 1

Then recreate the container and follow its output:

docker compose up -d --force-recreate
docker compose logs -f --tail=200 l2tp

Debug level 1 records IKE cookies/exchanges and state, proposal selection, NAT-D and migration decisions, Quick Mode SPIs and lifetimes, authentication, DPD sequences, ESP rejection reasons, connections, disconnections, and lifecycle events. Stable prefixes include DEBUG ike, DEBUG natt, DEBUG quick, DEBUG esp, and DEBUG ipsec lifecycle. It does not record PSKs, passwords, DH private values, derived keys, authentication responses, or plaintext packet payloads. Level 2 additionally emits TRACE entries for every ESP, L2TP, PPP, and IPv4 packet and should be enabled only for short captures because it can produce logs faster than Docker can display them.

Successful PPP session events are always written to standard output at every debug level. Each connect/disconnect line includes the authenticated username, assigned VPN address, transport (l2tp or l2tp/ipsec), peer address, tunnel ID, and session ID. Sessions that fail before authentication and IPCP open do not produce a misleading connected/disconnected pair.

Tests

go test ./...
go test -race ./...

Native TUN creation is an opt-in privileged integration test:

sudo go test -tags=integration ./tun -run TestOpenNativeDevice -v

Tag summary

Content type

Image

Digest

sha256:6329086f4

Size

7 MB

Last updated

about 1 month ago

docker pull eukhlg/l2tp-go