Background
My home setup uses the full Unifi stack, with a UDM SE as the gateway. However, the UDM SE does not support PPPoE Hardware Offload, which means when it handles the dial-up itself, single-thread throughput is only about 500 Mbps (while multi-thread can saturate gigabit). If I switch to having the ONU handle the dial-up, it introduces a Double NAT problem, and the UDM also cannot detect IP changes in time to trigger DDNS updates.
So is there a way to use a dedicated device to handle PPPoE dial-up (and benefit from hardware offload performance), while still allowing the UDM SE to directly obtain the public IP address? The answer is the topic of this article — PPPoE Half Bridge.
What is PPPoE Half Bridge?
PPPoE Half Bridge is not a new concept. Some overseas ISPs' ONUs natively support PPPoE Half Bridge (also called PPPoE IP Extension or PPPoE IP Passthrough). The core idea is:
A front-end device (ONU or router) is responsible for establishing the PPPoE session, but it does not use the acquired public IP. Instead, it "passes through" the public IP to the downstream gateway via DHCP. This makes the downstream gateway think it obtained the public IP directly.
Comparison of several common solutions:
| Solution | Dial-up device | Public IP owner | NAT layers | Applicable scenario |
|---|---|---|---|---|
| ONU dial-up | ONU | ONU | Double NAT | Simple and convenient but inflexible |
| Router dial-up / ONU bridge mode | Router | Router | Single-layer NAT | Most common solution |
| PPPoE Half Bridge | Front-end device | Downstream gateway | Single-layer NAT | The solution in this article |
Simply put, PPPoE Half Bridge makes the front-end device act only as a "dial-up proxy," while the real public IP is held and used by the downstream gateway. The advantages are obvious:
- The front-end device (such as a router running OpenWrt) handles PPPoE dial-up and can use its hardware offload capability to fully utilize the bandwidth;
- The downstream gateway device (such as the UDM SE) directly obtains the public IP, avoiding Double NAT;
- The downstream gateway can normally handle NAT, firewall, DDNS, and all other functions, just as if it were dialing up directly.
This article shares an OpenWrt-based implementation of PPPoE Half Bridge, which I have been running stably for more than half a year with verified reliability. This solution can also be easily migrated to Debian-based systems.
Why does this work?
A PPPoE dial-up establishes a point-to-point connection (Point-to-Point). After the dial-up succeeds, the IP address on the PPPoE interface is really just an identifier. Unlike an Ethernet interface, it is not required for communication itself. Packet forwarding depends on the PPPoE Session and the routing table, not on the IP address bound to the interface.
Therefore, we can:
- Keep the PPPoE session alive
- Remove the IP address from the PPPoE interface (
ip addr flush) - Manually create routing rules to direct downstream gateway traffic to the PPPoE interface
- Assign the public IP to the downstream gateway via DHCP
After the downstream gateway gets the public IP, all outbound traffic is forwarded through the front-end device's PPPoE interface to the ISP, and return traffic comes back through the PPPoE interface to the front-end device and is then forwarded to the downstream gateway. The whole process requires no NAT; the front-end device only performs Layer 2 / Layer 3 forwarding.
Hardware preparation
You need an OpenWrt device that supports PPPoE Hardware Offload, which will be responsible for dialing and forwarding.
My hardware setup:
- Banana Pi BPI-R4: flashing the latest OpenWrt firmware enables PPPoE hardware acceleration (
flow_offloading_hw), and it can saturate gigabit with barely any CPU usage - ONU stick: inserted into the BPI-R4's SFP port to replace a traditional ONU
- DAC cable: connects the other SFP port of the BPI-R4 to the UDM SE
TipThere are now also native OpenWrt 10G XGPON ONUs on the market. If your ISP supports GPON, that would be even more convenient. However, Shanghai Telecom uses EPON, so that is not an option.
Network topology
Topology diagram
Interface description:
| Interface name | Physical port | Purpose |
|---|---|---|
wan (br-wan) | eth2 | Connects to the ONU / ONU stick; PPPoE is established on top of it |
ppp | pppoe-ppp | PPPoE dial-up interface |
lann | eth1 | Connects to the downstream gateway (UDM SE) WAN port |
lan (br-lan) | lan1/lan2/lan3 | Management port |
Overall approach
Before diving into the specific configuration, let's first look at the overall idea and traffic flow:
- OpenWrt completes PPPoE dial-up through the WAN port and obtains a public IP (for example
a.b.c.d); - Strip the IP address from the PPPoE interface while keeping the PPPoE session alive;
- Configure the gateway IP of the public subnet on the LAN port (eth1) connected to the downstream gateway, so OpenWrt "impersonates" the ISP gateway;
- Assign the public IP to the downstream gateway via DHCP;
- Configure policy routing so that the downstream gateway's traffic is forwarded to the public network through the PPPoE tunnel;
- Disable NAT (masquerade) on OpenWrt — once the PPPoE interface IP is cleared, masquerade cannot work, and NAT should be handled by the downstream gateway.
After completing the above steps, the downstream gateway behaves as if it dialed up directly: it gets the public IP and accesses the Internet through the PPPoE tunnel, while OpenWrt becomes a transparent "bridged dialer."
Key points for the prerequisite configuration
Before getting into the core operations, there are several critical points to pay attention to in OpenWrt's static configuration:
network:
- The
defaultrouteof the PPPoE interface (ppp) must be set to'0'— the default route is managed manually via policy routing, and if PPPoE adds a default route automatically it will cause conflicts - The
lanninterface (eth1) should first be assigned a placeholder IP (such as192.168.2.1), which will be modified dynamically after dial-up
dhcp:
- Use odhcpd (the full version, not
odhcpd-ipv6only) instead of dnsmasq as the DHCPv4 server. odhcpd is lightweight and flexible; after dynamically modifying the DHCP range, restarting it takes effect within seconds, which makes it ideal for this scenario - Configure DHCP on the
lanninterface asstart=2, limit=1, meaning the pool has only one assignable address, and later dynamically adjuststartso that this single address becomes exactly the public IP - Set
leasetimeto60s; this very short lease ensures rapid updates when the IP changes
firewall:
- Create a separate zone (
wann) for the PPPoE interface, withmasqdisabled — because the PPPoE interface IP will be cleared, masquerade will not be able to find a source address and Internet access will fail - Enable bidirectional forwarding between
lannandwann - Enable
flow_offloading_hwhardware flow offloading
Core operation steps
The following is the sequence of operations that needs to be performed after PPPoE dial-up succeeds. Assume the acquired public IP is 58.32.1.100, and the subnet assigned by the ISP is /22.
Step 1: Obtain the public IP and calculate the gateway address
After PPPoE dial-up succeeds, first get the assigned public IP via ifstatus:
Then calculate the gateway address from the subnet mask. Shanghai Telecom Premium Network assigns a /22 subnet (255.255.252.0), so align the third octet to the subnet boundary with a bitwise operation:
[!TIP] If your ISP assigns a different subnet mask, you will need to modify the calculation logic here. For example, with a /24 subnet, you can directly use
$i1.$i2.$i3.1.
Step 2: Clear the PPPoE interface IP and establish policy routing
This is the most critical step in the entire solution.
This removes the public IP from the PPPoE interface, but the PPPoE session itself is not affected.
Why remove it? Because next we are going to assign an IP from the same subnet to the downstream gateway. If OpenWrt's PPPoE interface still keeps the public IP, it will create a conflict where "two devices in the same subnet have IPs from the same range," resulting in routing confusion. After removing the PPPoE interface IP, OpenWrt no longer has a directly connected route for that subnet, and traffic direction is then entirely controlled by the routing rules we manually add.
Why can it be removed? PPPoE is a point-to-point tunnel protocol. Session maintenance depends on the underlying PPPoE session, not on IP-layer addressing. As long as the PPPoE session remains up, the tunnel exists and packets can be forwarded through it. The IP address is only for convenience at the upper-layer routing level and does not affect the tunnel itself.
Since the PPPoE interface IP has been removed, the system default route also becomes invalid. We need to manually create routing rules to tell the system, "traffic coming from the downstream gateway should go out through the PPPoE tunnel."
Policy routing is used here:
- Create an independent routing table (table 100), and add a default route in it pointing to the
pppoe-pppdevice; - Add a routing rule: all traffic entering from
eth1(the downstream gateway interface) should consult table 100 for routing.
This way, traffic sent by the downstream gateway is forwarded to the public Internet through the PPPoE tunnel, while OpenWrt's own management traffic remains unaffected.
ImportantAll IP changes here are made directly using the
ip/ifconfigcommands, and do not modify/etc/config/network. After OpenWrt reboots, it will automatically return to the default configuration, and these operations just need to be executed again after the next successful PPPoE dial-up. This is an intentional design choice to ensure the system remains recoverable.
Step 3: Configure eth1 as the gateway for the downstream gateway
Configure the calculated gateway IP (for example 58.32.0.1) on eth1 so that OpenWrt's eth1 "impersonates" the ISP gateway. Then, after the downstream gateway gets the public IP via DHCP, its default gateway will naturally point to that address, and routing will work normally.
NoteIn practice, I found that modifying the IP directly sometimes does not take effect. It is more reliable to bring the interface
downfirst and then reconfigure itup.If you do not need to access neighbor IPs within the same subnet, simply keep the subnet mask consistent with what the ISP provides. If you do need that, you should make the subnet as small as possible. In theory, you could even use a /31 mask — as long as the gateway IP and your public IP are in the same subnet.
Step 4: Dynamically adjust the DHCP range to assign the public IP precisely
In the prerequisite configuration, we already set the odhcpd DHCP configuration for the lann interface as start=2, limit=1, so only one IP can be assigned from the pool. What we need to do now is: based on the currently acquired public IP, dynamically modify the start value so that the one and only IP handed out by DHCP is exactly the public IP.
The start parameter in odhcpd represents the offset relative to the start address of the subnet. After updating the configuration, restart the odhcpd service to make it effective.
A concrete example:
- PPPoE obtains IP
58.32.1.100, subnet/22 - The subnet on
eth1is58.32.0.0/22 - DHCP
start = 356,limit = 1 - The downstream gateway obtains
58.32.0.0 + 356 = 58.32.1.100via DHCP
Step 5: Make sure the PPPoE zone does not perform NAT
Check the masq (NAT) setting of the firewall zone where PPPoE resides, and ensure it is 0:
This step is absolutely crucial. In Step 2 we already flushed the PPPoE interface IP address, and masquerade works by using the egress interface IP as the SNAT source address. At this point, the PPPoE interface no longer has an IP, so the MASQUERADE rule cannot find a usable source address, which causes all packets passing through NAT to be dropped directly, appearing as complete loss of Internet connectivity. Therefore, you must set masq to 0 for the firewall zone containing PPPoE, so that traffic is forwarded through the PPPoE tunnel using the original source IP (that is, the downstream gateway's public IP), and NAT is handled entirely by the downstream gateway.
Step 6: Notify the downstream gateway to refresh DHCP immediately
After all the above configuration is complete, the downstream gateway needs to detect the new IP immediately. However, when OpenWrt redials (and the IP changes), the downstream gateway does not proactively refresh DHCP — it waits until the current lease expires before requesting a new one. But here we run into a practical problem: how can we make the downstream gateway immediately request DHCP again?
I tried the following approaches:
- DHCP FORCERENEW (RFC 3203): the server proactively notifies the client to refresh the DHCP lease. However, after testing, I found that the UDM SE does not support it.
- Link Down/Up: physically disconnect and reconnect eth1 on OpenWrt to trigger the downstream gateway to request DHCP again. But I used a DAC cable to connect the SFP ports of the two devices, and after the link state changed, the UDM still did not initiate a new DHCP request.
In the end, I came up with a simple and fast solution: deploy a simple listener service on the UDM SE that forcibly triggers DHCP renewal when it receives a specified message.
The principle is simple: use nc on the UDM to listen on a port (such as 99). When it receives the message dhcp_renew, send SIGUSR2 (release the current lease) and SIGUSR1 (request a new lease) to udhcpc (the DHCP client on the UDM).
After OpenWrt completes dial-up and all configuration, just send a message:
When the IP changes, the downstream gateway can update almost within seconds.
Automated execution
All the above steps can be packaged into a Shell script and configured to run automatically after PPPoE dial-up succeeds. In OpenWrt, you can place the script in the /etc/ppp/ip-up.d/ directory, and it will be triggered automatically each time PPPoE connects successfully.
This way, whether it is the first dial-up or a reconnection after a drop, the Half Bridge configuration is completed automatically for fully unattended operation.
Create /etc/hotplug.d/iface/99-half-bridge:
Each function has been made idempotent — if the current state is already the target state, the operation is skipped to avoid problems caused by repeated execution.
Overall data flow
Traffic flow after the configuration is complete:
Outbound (downstream devices → Internet):
Inbound (Internet → downstream devices):
The front-end device performs no NAT throughout the entire process. It only does Layer 3 routing and PPPoE encapsulation/decapsulation. Combined with hardware flow offloading, the performance overhead is almost zero.
Final thoughts
I have personally been running this PPPoE Half Bridge solution stably for more than half a year without any issues. In essence, it is a manual implementation on OpenWrt of the "ONU PPPoE IP Passthrough" feature.
That said, this solution really is quite Geeky. It involves coordinated interaction among PPPoE, DHCP, policy routing, firewall, and several other modules, so it is hard to abstract into a universal one-click script. Everyone's network environment, ISP-assigned subnet, and hardware are different, so adjustments are needed according to the actual situation.
NoteI have actually switched to the UCG Fiber — Unifi finally came to its senses and built in PPPoE hardware acceleration. So this article is basically my way of closing the loop, pointing people who are still tinkering with PPPoE Hardware Offload toward a workable path.
However, if you are using a device such as an OpenWrt XGPON ONU, this solution is still very valuable — it can give you a "pseudo-IPoE" broadband experience, making the downstream gateway completely unaware of PPPoE.
If you're interested, feel free to discuss and tinker together 🛠️
Appendix: Configuration and script reference
Network configuration (/etc/config/network)
Key parts of the firewall configuration (/etc/config/firewall)
Key parts of the DHCP configuration (/etc/config/dhcp)
PPPoE Half Bridge startup script (/etc/ppp/ip-up.d/start-half-bridge.sh)
DHCP listener service on UDM SE (dhcp_listener.service)
Deploy it to /etc/systemd/system/dhcp_listener.service. After enabling it, it can receive DHCP refresh notifications from OpenWrt: