ndp-proxy-go: Proxy ISP provided /64 Prefix from WAN to LAN

Started by Monviech (Cedrik), November 17, 2024, 09:15:09 PM

Previous topic - Next topic
December 06, 2025, 03:51:36 PM #30 Last Edit: December 06, 2025, 03:54:36 PM by Monviech (Cedrik)
Experimental PPPoE support is in 25.7.9.

The last feature I added is PF table (firewall alias) support to help with the network segmentation for highly dynamic setups.

It will most likely hit 25.7.10.

With that the proxy should be complete for now, I personally do not miss any feature when using it, it just worksTM and is quite possibly the leading most complete implementation to fix IPv6 for many setups.

I would call it generic since you can chain the proxy over multiple routers. You dont even need DHCPv6-PD anymore, this proxy handles dynamic IPv6 so gracefully that you won't believe it.

https://github.com/opnsense/docs/commit/5bb5fca5c67ac9162c8f76d6261ca6cc90f34076

Hardware:
DEC740

I'm testing ndp-proxy-go, but it's not working as I expected. I have a multi-WAN setup with NPTv6.

I recently got a new ISP, but their router don't support DHCPv6-PD. I manage to get multiples "/64" in the same interface and I was hopping to route each "/64" to a different LAN.


WAN_DIGI:
2a12:1111:1111:c200::/64
2a12:1111:1111:c201::/64
2a12:1111:1111:c203::/64

LAN:
fdaa:1111:1111::/64 (using NPTv6 from 2a12:1111:1111:c201::/64)

GATEWAY_TEST:
Tier 1: WAN_DIGI
Tier 2: (other WAN)

VM (connected to LAN):
fdaa:1111:1111::da4d (via DHCPv6)


------------

In theory, I was hopping to:

2a12:1111:1111:c201::/64 <-> fdaa:1111:1111::/64

That already works for my other WAN and even Wireguard (which I get /64 externally).

------------

When I test `ping google.com` from VM, the OPNSense shows:

16:23:25.354572 00:1b:21:bc:fa:9e > 58:72:c9:40:e8:fc, ethertype IPv6 (0x86dd), length 70: 2a12:1111:1111:c201::da4d > 2a00:1450:4003:809::200e: ICMP6, echo request, id 57938, seq 49995, length 16
However, the router reply:

16:23:25.306554 58:72:c9:40:e8:fc > 33:33:ff:00:da:4d, ethertype IPv6 (0x86dd), length 86: fe80::fff1 > ff02::1:ff00:da4d: ICMP6, neighbor solicitation, who has 2a12:1111:1111:c201::da4d, length 32

I don't see any reply to that. Sounds like the ISP router can't find who has that IP.

The NDP Proxy setup is:

Enable [X]

Upstream Interface: WAN_DIGI

Downstream Interfaces: LAN

Proxy router advertisements [_] (unchecked)

Install host routes [X] (I already test as unchecked)

The Log File is empty, even with Debug Log enabled.

-----

The documentation states "It can proxy SLAAC on-link prefixes to several downstream interfaces by proxying", so it don't work with DHCPv6? Or I'm missing something?

Maybe the NPTv6 is causing the issue?


July 17, 2026, 08:10:35 PM #32 Last Edit: July 17, 2026, 08:13:28 PM by Monviech (Cedrik)
The proxy is not compatible with NPTv6 setups.

It only proxies single host addresses it discovers on the downstream interfaces, and only answers NDP for these hosts inside the learned prefix.

It's very strict here, NPTv6 would need arbitrary answers for any address in the prefix. But ndp-proxy-go keeps an in memory database of discovered hosts and only answers for their real address, not a translated one.

If you received your prefixes only via DHCPv6 PD then ndp proxy is not the right tool for you.

Empty log file is weird, it should at least emit that it starts and what the configuration is.
Hardware:
DEC740

Quote from: Monviech (Cedrik) on July 17, 2026, 08:10:35 PMThe proxy is not compatible with NPTv6 setups.

It only proxies single host addresses it discovers on the downstream interfaces, and only answers NDP for these hosts inside the learned prefix.

Have any alternative to it?

I think the old "ndproxy" works with NPTv6. I use ndproxy with wireguard, in another FreeBSD VM, but not on OPNSense.

July 17, 2026, 09:24:00 PM #34 Last Edit: July 17, 2026, 09:38:22 PM by Monviech (Cedrik)
The old ndproxy works because it proxies the whole prefix. It simply answers "that's me" for any address within the configured prefix.

ndp-proxy-go is intentionally stricter. It only answers for host addresses it has actually learned from downstream.

With NPTv6, the translated address never exists downstream, so there is nothing to learn and therefore nothing to answer for.

I also maintained the ndproxy plugin but its a kernel module and it was defunct on FreeBSD 15 for a while so the decision was made to remove it, even though now it seems fixed upstream. But who knows how long.

-> Probably the simplest solution would be to not use a proxy, but to just use NAT66, just like using NAT44 (Source NAT). So not a prefix translation, just good old Source NAT Masquerading.
Hardware:
DEC740

Quote from: Monviech (Cedrik) on July 17, 2026, 09:24:00 PMndp-proxy-go is intentionally stricter. It only answers for host addresses it has actually learned from downstream.

It would be great to have a optional feature that simply answers "that's me" for any address within the configured prefix. I understand that it's not the main goal, but it could be used for backward compatibility with the old ndproxy.

Quote from: Monviech (Cedrik) on July 17, 2026, 09:24:00 PM-> Probably the simplest solution would be to not use a proxy, but to just use NAT66, just like using NAT44 (Source NAT). So not a prefix translation, just good old Source NAT Masquerading.

The issue with NAT is the need for port-forwarding, and I can't connect using <my_isp_prefix>::da4d directly. Currently, any connection to ::da4d goes to a specific VM. I can also set up a firewall rule like: 'Allow ANY_WAN to LAN_GAME_SERVERS on port 4000'. This way, anyone can connect to it on a specific port without port-forwarding and each VM has a unique IP.

July 18, 2026, 06:51:04 AM #36 Last Edit: July 18, 2026, 07:48:22 AM by Monviech (Cedrik)
Static hosts / WireGuard setup (unsupported workaround):

For hosts whose ULA addresses are fixed and known in advance, you could create the corresponding translated GUAs as IPv6 virtual IPs on the WAN interface that owns the prefix.

For example:

fdaa:1111:1111::da4d
2a12:1111:1111:c201::da4d

The firewall would then explicitly own the external address and answer NDP for it, while NPTv6 translates traffic to the internal ULA.

This is practical for a small number of statically addressed servers, but not for arbitrary or dynamically assigned clients.

Normal WAN-to-LAN setup (supported):

For an ordinary WAN-to-LAN scenario, NPTv6 should not be necessary. ndp-proxy-go can extend the real on-link GUA prefix to the downstream LAN, allowing clients to use addresses from 2a12:1111:1111:c201::/64 directly.

No ULA or prefix translation is needed in that case. This is the setup described in the documentation.

If that does not work, the debug log is needed. An entirely empty log suggests the service may not be starting at all.
Try "configctl ndpproxy start" or "service ndp-proxy-go start". Check /var/log/system and /var/log/configd for errors. Check /var/log/ndpproxy if a logfile is created.

Only reason I know for a full service start refusal is if "Enable CARP failover" is activated in the plugin (check advanced configuration)

Multi-WAN failover is a separate limitation. Since downstream clients use real GUAs belonging to one ISP prefix, those addresses do not automatically remain usable through another WAN.
Hardware:
DEC740

Quote from: Monviech (Cedrik) on July 18, 2026, 06:51:04 AMFor an ordinary WAN-to-LAN scenario, NPTv6 should not be necessary. ndp-proxy-go can extend the real on-link GUA prefix to the downstream LAN, allowing clients to use addresses from 2a12:1111:1111:c201::/64 directly.

I understand that NPTv6 is not necessary. I use NPTv6 for multi-wan. In my case I don't want to expose the GUA. If I expose every GUA, from each WAN/ISP, each client will need to balance and fallback on their own. In other word:

A) With GUA, VM_1 will get:
2a12:1111:1111:c201::da4d // from ISP 1
2222:1111:1111:c201::da4d // from ISP 2
2003:1111:1111:c201::da4d // from ISP 3

B) With NPTv6, VM_1 will get:
fdaa:1111:1111::da4d


The case A works with ndp-proxy-go. But that is terrible. Every device/VM will use a single ISP/"WAN", no loading balance, no routing (forcing specific ISP). Also, in some cases fallback doesn't work too, it gets stuck using a single GUA. Every client/VM is responsible for everything.

The case B is easier. If I change the Gateway in OPNSense, everyone will use it. The responsibility relies on OPNSense, which is a single point. I can also force some connections (to specific external IP) to use specific Gateway/ISP, without touching on each device/client.

The issue here is: one specific WAN/ISP don't support DHCPv6-PD. :\

July 18, 2026, 09:48:20 AM #38 Last Edit: July 18, 2026, 09:21:33 PM by Monviech (Cedrik)
Multi WAN with provider dependant prefixes is not really working that great.

Real IPv6 Multi WAN would require a provider independant prefix. (eg routed to you via gre or another tunnel)

The same limitations also exist for IPv4.

Faking the host identity on the router via NAT or NPT is a workaround but also has its own limitations.

Your setup is limited by the underlying infrastructure.

The simplest would be to use Masquerading just like with IPv4, sacraficing real end to end. But real end to end isn't possible anyway when faking the host identity.

My proxy only handles a single WAN and a single host identity, but that it can do fully dynamically even with changing prefixes.

------

Whats needed for NPTv6 without having a routed prefix is not an NDP Proxy, but more an NDP responder. It wouldnt need to know anything else but the WAN interface it should respond on, and the prefix it should synthesize NAs for when the Upstream ISP sends an NS. That could be an entirely separate tiny daemon without any host awareness, but it would be a different design. It would resemble proxy ARP, but for a whole prefix (or multiple), not just for a single IP.

It is weird that there is a real gap like this in FreeBSD.

There is "ndp -s ... proxy" but it can only proxy single addresses, like proxy arp. No implementation exists here that can respond for a whole prefix. The way ndproxy worked with NPTv6 could be seen as emergent behavior. A clean solution would be a native prefix responder in userspace or the kernel.

-------

I think this discussion was valuable since it showed something that is missing:

(I compiled this list with AI help, all other text above I wrote myself):

three distinct approaches:

1. Host-learning NDP proxy (ndp-proxy-go)
    * Learns downstream hosts.
    * Answers only for known hosts.
    * Suitable for extending a real on-link prefix.
2. Prefix-owning NDP proxy (old ndproxy)
    * Claims an entire prefix.
    * Answers for everything in it.
    * Suitable for NPTv6 and similar use cases.
3. Prefix responder (currently missing)
    * Doesn't proxy anything.
    * Doesn't know about downstream.
    * Simply synthesizes NAs for configured prefixes on configured interfaces.
    * Would be the perfect NPTv6 choice
Hardware:
DEC740

Quote from: Monviech (Cedrik) on July 18, 2026, 09:48:20 AMMulti WAN with provider dependant prefixes is not really working that great.

Real IPv6 Multi WAN would require a provider independant prefix. (eg routed to you via gre or another tunnel)

The same limitations also exist for IPv4.

I agree. It would be better if I had my own IP and a dedicated /48 GUA prefix.

I'm exploring that option, but none of my ISPs can announce my IP, even with a business contract. I'm afraid that GRE could reduce performance (speed and latency), especially in higher speeds.

To be fair, I already use WireGuard in a setup similar to GRE. I route a /64 from an external server to OPNsense, which is conceptually similar. Its purpose is to reduce latency to Brazil. I have a couple of VPSs connected together, and the end-to-end latency is around 120 ms, compared to about 220 ms when connecting directly with my ISPs.

Quote from: Monviech (Cedrik) on July 18, 2026, 09:48:20 AMFaking the host identity on the router via NAT or NPT is a workaround but also has its own limitations.

Your setup is limited by the underlying infrastructure.

I agree that it's a workaround, but it's better than giving multiple GUAs to each client.

---

Side note: I can remove the ISP router and connect the ONT directly to OPNsense, which would give me the full /56 prefix. However, that ISP uses PPPoE, and I don't think my hardware can handle 10 Gbps over PPPoE. I'm also considering buying another router with PPPoE hardware acceleration and DHCPv6-PD support, then using the following setup: ISP → ONT → My Router (PPPoE + DHCPv6-PD) → OPNsense. That should perform as well as my other two ISPs while still giving OPNsense the delegated IPv6 prefixes. However, I need to buy another router and ONT and pray for the best. Currently, I don't have "ONT → My Router", but a single All-In-One device from ISP (which is a ONT, Router, Switch and Access-Point/WIFI).

 


Thanks for explaining, interesting how you work with the constraints you have.

I think some people use Proxmox and offload the PPPoE on it on hardware level while passing it to an OPNsense VM. I think Ive read threads like these before. Maybe thats also something you could pursue. But I don't know anything more about this.

Anyway good luck and thanks for the discussion :)
Hardware:
DEC740