A proxy system for PPPoE (Point-to-Point Protocol over Ethernet) connections written in Go.
PPPoE Proxy allows tunneling PPPoE connections between networks by proxying both discovery and session packets. This enables:
- Extending PPPoE connectivity across networks
- Controlling and monitoring PPPoE traffic
- Adding authentication layers to PPPoE connections
The system operates in one of three modes:
- Client Mode: Captures PPPoE traffic from a local interface and forwards it to a remote server
- Server Mode: Receives traffic from clients and forwards it to the actual PPPoE server, with authentication
- Tunnel Mode: Connects to a server and terminates PPPoE and PPP itself, exposing the link as a local tun interface. No local PPPoE traffic is forwarded and no
pppdis needed.
- Proxying of PPPoE Discovery packets (0x8863)
- Proxying of PPPoE Session packets (0x8864)
- Raw socket handling for efficient packet capture and injection
- IP-based access control for client connections
- Automatic reconnection for client mode
- Ping/pong keepalive mechanism (60-second interval)
- Thread-safe connection handling
- Built-in PPPoE, LCP, PAP/CHAP and IPCP implementation for tunnel mode
# Server Mode
./pppoeproxy -interface eth0 -mode server -address 0.0.0.0:8000 -allow 192.168.1.2
# Client Mode
./pppoeproxy -interface eth0 -mode client -address 192.168.1.1:8000
# Tunnel Mode
./pppoeproxy -mode tunnel -address 192.168.0.1:2433 -user "your-username" -password "your-password"
-interface: Network interface to capture and inject PPPoE packets (required in client and server mode)-mode: Operation mode: "client", "server" or "tunnel" (default: "client")-address: Address to connect to (client and tunnel mode) or listen on (server mode) (required)-allow: IP address allowed to connect (server mode only, default: "127.0.0.1")
Tunnel mode only:
-user: PPP authentication user name (required)-password: PPP authentication password-tun: Name of the local tun interface to create (default: "pppoe0")-service: PPPoE service name to request (default: empty, which accepts any)-mac: MAC address to use for the PPPoE session (default: a random locally administered address)
-
PPPoE Discovery Phase:
- In client mode, captures PADI, PADO, PADR, and PADS packets
- In server mode, captures and forwards packets to connected clients
- Forwards packets between the client, server, and the actual PPPoE server
-
PPPoE Session Phase:
- Captures and forwards session packets to maintain the tunnel
- Preserves PPPoE session IDs and packet integrity
-
Tunnel Mode:
- Connects to a server and runs the discovery exchange itself (PADI, PADO, PADR, PADS)
- Negotiates LCP, authenticates with PAP or CHAP-MD5, then negotiates IPCP
- Filters out every frame that is not part of its own session, so other PPPoE clients sharing the remote segment are ignored
- Hands the resulting IP link to a local tun interface
Client mode is a dumb relay: it needs a spare interface to inject PPPoE frames
into and a local pppd to drive them. Tunnel mode does the whole job in one
process instead. It performs the PPPoE discovery and the PPP session itself, so
the only thing it needs locally is a tun interface.
sudo ./pppoeproxy -mode tunnel -address 192.168.0.1:2433 \
-user "your-username" -password "your-password"Once IPCP completes, the assigned address is logged along with the DNS servers the peer advertised:
PPPoE: session 0x0042 established with 02:ac:ac:ac:ac:01
PPP: CHAP authentication succeeded
Tunnel: pppoe0 up with address 203.0.113.7, peer 198.51.100.1, MTU 1492
Tunnel: DNS server 1: 8.8.8.8
Tunnel mode never touches the system routing table. Bringing the link up only assigns the address to the tun interface, which implicitly adds a host route to the peer and nothing else. There is no default route, so nothing about the machine's existing connectivity changes.
To actually send traffic over the link, route it explicitly:
# Send one destination over the tunnel
sudo ip route add 198.51.100.0/24 dev pppoe0
# Or use policy routing for everything sourced from the assigned address
sudo ip route add default dev pppoe0 table 100
sudo ip rule add from 203.0.113.7 lookup 100The interface is created and brought up at startup, before the first session, so routes and rules pointing at it can be installed ahead of time. They survive reconnections: the interface is never taken down, only re-addressed.
You almost certainly need -mac. Tunnel mode defaults to a random locally
administered MAC address, and the access concentrator unicasts its PADO back to
that address. The server's capture interface only accepts frames addressed to
its own MAC, so a PADO sent to a random address is dropped by the kernel before
the raw socket ever sees it. The symptom is PADIs going out forever with no
offer ever coming back:
PPPoE: still no offer after 5 attempts, slowing down retries
Pass the MAC address of the interface the server captures on:
sudo ./pppoeproxy -mode tunnel -address 192.168.0.1:2433 \
-mac 00:11:22:33:44:55 -user "your-username" -password "your-password"The alternative is to put the server's capture interface into promiscuous mode, which lets any MAC work:
sudo ip link set dev pppoe-proxy promisc onSharing the capture interface's MAC is fine even when that interface already has its own PPPoE session: sessions are demultiplexed by session ID, and everything that is not ours is dropped. Pinning the MAC also keeps the address stable across restarts, which some access concentrators prefer.
On a clean shutdown (SIGINT/SIGTERM) the client sends a PADT so the access concentrator releases the session immediately.
If the process is killed with SIGKILL it cannot do that, and the concentrator keeps the session allocated until it times out. On a line with a small session limit, such as the two sessions NTT allows, a leaked session means the next start gets no PADO at all until the old one expires.
In Japan, NTT allows up to 2 PPPoE sessions on a single line. This enables an interesting use case:
- Set up a macvlan interface on your primary internet-connected device
- Run this proxy in server mode on that macvlan interface
- Run a client on a remote device (e.g., in another location)
- The remote device can now establish its own PPPoE session through your NTT line
This effectively allows you to share your NTT connection with a remote location while maintaining separate PPPoE sessions, each with its own public IP address.
On your primary device that's connected to the NTT line, create a macvlan interface:
# Create a macvlan interface attached to your physical interface (e.g., eth0)
sudo ip link add link eth0 name pppoe-proxy type macvlan mode bridge
# Bring the interface up
sudo ip link set dev pppoe-proxy upOn the same primary device, run the PPPoE proxy in server mode:
# Run the server on the macvlan interface, listening on port 8000
sudo ./pppoeproxy -interface pppoe-proxy -mode server -address 0.0.0.0:8000 -allow 192.168.1.2Replace 192.168.1.2 with the IP address of your remote client device.
If you're running the server on a Ubiquiti Dream Machine (UDM/UDM-Pro), you'll need to open the port in the firewall to allow incoming connections from your client:
# SSH into your UDM and run:
iptables -I UBIOS_WAN_LOCAL_USER -p tcp --dport 8000 --src 192.168.1.2 -j ACCEPTReplace 8000 with your server port and 192.168.1.2 with your client IP address.
On the remote device:
# Run the client connecting to the server's IP address
sudo ./pppoeproxy -interface eth0 -mode client -address 192.168.1.1:8000Replace eth0 with your network interface and 192.168.1.1 with the IP address of your server.
On the remote device, configure your PPPoE client software to connect through the proxy:
# Example using pppd for Linux
sudo pppd plugin rp-pppoe.so eth0 user "your-username" password "your-password" noauthOnce connected, the remote device will have its own public IP address through the NTT line.
Tunnel mode replaces both the client process and pppd on the remote device,
and needs no spare interface to inject frames into:
sudo ./pppoeproxy -mode tunnel -address 192.168.1.1:8000 \
-user "your-username" -password "your-password"The remote device gets its own public IP address on pppoe0, and its existing
default route keeps working: see Tunnel Mode for how to send
traffic over the link.
You can download a pre-built version of pppoeproxy using the following command:
curl -s https://raw.githubusercontent.com/KarpelesLab/make-go/master/get.sh | /bin/sh -s pppoeproxyThis will automatically download the appropriate binary for your system architecture.
If you prefer to build from source:
git clone https://github.com/KarpelesLab/pppoeproxy.git
cd pppoeproxy
make- Go 1.20 or higher
- Linux system with root access (for raw socket operations)
- Administrative privileges on network interfaces