https://docs.opnsense.org/manual/ndp-proxy-go.html
https://github.com/monviech/ndp-proxy-go
Thanks Cedrik.
I'm guessing this could be particularly helpful in IPv4 only networks where the ISP cannot be bothered to offer IPv6, and the user sets up a HurricaneElectric IPv6 tunnel.
I don't understand. From what I see HE delegates a /48 prefix in their tunnel broker service. You can easily route and split that into multiple /64 nets.
ndproxy is only needed if you /only/ get a single /64 prefix which you can not split into smaller networks since then SLAAC will break.
An additional usecase is if the upstream router does not have a route to your downstream router but resolves all via neighbor discovery.
Its a fix for broken IPV6 implementations by ISPs, in mobile 4g/5g ipv6 networks and also quite helpful for cloud provider stuff like VPS. So the typical environments you find with home users and self hosting hobbyists.
It took way too long to figure this out (finally 0.0% packet loss/dup to cloudflare), so I thought I'd mention it here:
If you're getting dropped & duplicate packets (DUP! with ping on linux, windows doesn't show). It's likely the "Downlink MAC Address", in my case I actually had to set it to the Uplink (WAN) MAC Address. Could be mislabeled, or something with my setup specifically (the PE is not very reliable; XS-2426G-B).
Also if you are always getting an IPv6 /64 address on WAN with "Request Prefix only" enabled: find the gateway address in settings (save); set WAN as static IPv6 with same address but /128. Then manually re-make the IPv6 gateway using the previously saved gateway address. I assume this happens when the PE doesn't really support prefix delegation at all.
Thanks @Monviech for adding an ndproxy to OPNsense! Very useful.
Let me explain some things:
If the provider offers DHCPv6 prefix delegation (even if it's just a single /64), an ndproxy is not required. During prefix delegation, the PE creates a route for the delegated prefix, pointing to the CPE's WAN address.
An ndproxy is required if neither prefix delegation nor static routes are available on the PE. Typical examples are SLAAC-only networks (e.g. mobile broadband routers / "modems", tethered phones) or datacenters.
@fdevd is correct: Downlink MAC Address is the MAC address of the selected Uplink Interface. Promiscous mode is not required because we use the interface's actual address. The reason why the MAC address needs to be specified at all is that ndproxy can also operate on a dedicated machine instead of on the CPE itself. If the CPE doesn't have an ndproxy, you can run it on a separate machine which then handles NDP on behalf of the CPE. Promiscuous mode is only required for this rare use case.
The name "proxy" might be somewhat misleading. Ndproxy does not actually proxy packets between the CPE's WAN and LAN interfaces. It operates exclusively on the WAN interface.
Cheers
Maurice
Thank you both for testing and clarification.
If there's time eventually, can somebody look at the documentation for the plugin and point out the spots where its wrong?
What I wrote there actually works too, it can be verified that a /64 prefix delegation does not automatically make traffic on LAN work. The ndproxy needs to be there for it to work.
I wonder why if it should not be needed.
https://github.com/opnsense/docs/blob/master/source/manual/ndproxy.rst?plain=1
https://docs.opnsense.org/manual/ndproxy.html
The second point about the MAC address seems to be right:
https://man.freebsd.org/cgi/man.cgi?query=ndproxy
When looking at the Network Design again, the MAC address should be the same as the WAN on OPNsense. Though its the label that needs adjusting then, since its a hard requirement for the kernel module to be loaded.
The label for the downlink mac address is correct, the issue is that the description suggests using the LAN interface, but it should be WAN interface.
The ndproxy manpage refers to "uplink" and "downlink" when looking at it from the point of view of the PE and CPE link. Meaning if we think about the wires between the ISP (PE) and the customer router (CPE), the "uplink" is the ISP interface, the "downlink" is the WAN side of opnsense.
The only one that is a bit weird is the label "uplink_interface". Looking at that manpage's network example, there is a switch between the PE and CPE, and a BSD host on a third leg of that switched network. The "uplink interface" is from the bsd host's point of view in that network. What a wild example, and only when I think of this network layout like this does the manpage make sense.
Anyways, right under the network diagram it says "the BSD host and the CPE router can be the same node" as well. That means in this case, the "uplink interface" is now the WAN interface on the CPE........ further adding to the confusion! Unless you have read this manpage and analyzed it, "uplink interface" would seem like contravention to the naming conventions for "uplink" and "downlink" when referring to the PE and CPE interfaces, so it's no wonder the "downlink" interface description incorrectly suggests using the LAN interface!
I feel like the person who wrote the kernel module and the manpage is dealing with the frustration of a super crappy ISP and they had to get real creative. I showed this manpage to a friend and he informed me that there are absolutely crap ISPs out there that will force low grade equipment on you and won't let you bring your own, so creative types have done things like this network diagram to get traffic to re-route to other routers, but leaves the garbage equipment in place to answer authentication queries.
Thanks for the feedback: https://github.com/opnsense/plugins/pull/4553
Small docs update follows soon: https://github.com/opnsense/docs/pull/672
I am currently using NDProxy on our setup.
I am getting Packet losses + high pings during Ping test. I am curious how I should debug this issue.
Ping statistics for 2607:f8b0:400a:807::200e:
Packets: Sent = 100, Received = 73, Lost = 27 (27% loss),
Approximate round trip times in milli-seconds:
Minimum = 2ms, Maximum = 265ms, Average = 12ms
No issue when Pinging on WAN side.
Thanks
I finally got it working... I had to disable "Promiscuous mode" on WAN for NDProxy setup.
Ping statistics for 2607:f8b0:400a:806::200e:
Packets: Sent = 1000, Received = 1000, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 2ms, Maximum = 170ms, Average = 2ms
Only one ping at 170ms, so all is well.
The issue is that the rc.d script forces it when the service starts:
https://github.com/opnsense/ports/blob/b3aa544a28e4946383801d88ef926d853fcb2fd8/net/ndproxy/files/ndproxy.in#L50
This is hardcoded from upstream.
Quote from: Monviech (Cedrik) on February 28, 2025, 08:35:51 PMThe issue is that the rc.d script forces it when the service starts:
https://github.com/opnsense/ports/blob/b3aa544a28e4946383801d88ef926d853fcb2fd8/net/ndproxy/files/ndproxy.in#L50
This is hardcoded from upstream.
This should be noted on the plugin page. This is a pretty big deal and is doing things to networking that end users may not be aware of.
I would rather patch this out of the upstream port, so that it is configurable. The promisc mode is in there for the usecase thats shown in the man page (CPE, PE, and proxy being different devices). I'll see what I can do.
The promisc mode thing will be patched out soon.
https://github.com/opnsense/plugins/pull/4676
I am a little perplexed about promiscuous mode though, so I have retested this with a SLAAC only setup:
WAN configuration:
IPv6 Configuration Type: SLAAC
igc1: flags=1808a43<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,PALLMULTI,LOWER_UP> metric 0 mtu 1500
description: igc1_WAN (wan)
options=4802028<VLAN_MTU,JUMBO_MTU,WOL_MAGIC,HWSTATS,MEXTPG>
ether f4:90:ea:01:3d:b3
inet6 fe80::f690:eaff:fe01:3db3%igc1 prefixlen 64 scopeid 0x2
inet6 2003:a:1704:XXXX:XXXX:eaff:fe01:3db3 prefixlen 64 autoconf pltime 86400 vltime 86400
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active
nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
LAN configuration:
STATIC IPv6: 2003:a:1704:xxxx:xxxx:fe01:3db4/64 (Address is in same prefix as WAN SLAAC address incremented by 1)
Router Advertisements enabled to Stateless
igc0: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1450
description: igc0_LAN (lan)
options=802028<VLAN_MTU,JUMBO_MTU,WOL_MAGIC,HWSTATS>
ether f4:90:ea:01:3d:b2
inet6 fe80::f690:eaff:fe01:3db2%igc0 prefixlen 64 scopeid 0x1
inet6 2003:a:1704:XXXX:XXXX:eaff:fe01:3db4 prefixlen 64
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active
nd6 options=121<PERFORMNUD,AUTO_LINKLOCAL,NO_DAD>
NDproxy configuration:
net.inet6.ndproxycount: 86
net.inet6.ndproxyconf_uplink_ipv6_addresses: fe80::f690:eaff:fe00:d9f4 (uplink router)
net.inet6.ndproxyconf_exception_ipv6_addresses:
net.inet6.ndproxyconf_downlink_mac_address: f4:90:ea:01:3d:b3 (WAN address MAC)
net.inet6.ndproxyconf_uplink_interface: igc1 (WAN interface)
With this setup, a Windows client in the LAN interface pinged 2001:4869:4860::8888
-> and got no response.
I then calculated the multicast group of the LAN client from its auto generated GUA and joined it via port net/mcjoin
mcjoin -i igc1 ff02::1:ff27:c64e
-> The ping worked right away!
This means at least multicast groups must be joined, or all multicasts must be allowed.
e.g., setting ALLMULTI on the WAN interface makes it work right away too.
ifconfig igc1 allmulti
Conclusion:
Promiscuous mode is overkill, but ALLMULTI or calculated multicast group joining is needed for ndproxy to work.
If anybody has any more hints I would be happy.
I did build ndproxy 3.2.1402000_2 and os-ndproxy 1.1 and can't reproduce the behaviour. It just works, without enabling promiscuous mode, joining a multicast group or enabling promiscuous mode for multicast packets (allmulti).
Did you try a ping from OPNsense itself, setting the source address to the LAN interface address (2003:a:1704:XXXX:XXXX:eaff:fe01:3db4)?
Cheers
Maurice
hn0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
description: LAN (lan)
options=80018<VLAN_MTU,VLAN_HWTAGGING,LINKSTATE>
ether 00:15:5d:d2:76:3c
inet6 fe80::215:5dff:fed2:763c%hn0 prefixlen 64 scopeid 0x5
inet6 fd01:2345:6789:abcd::a prefixlen 64
media: Ethernet autoselect (10Gbase-T <full-duplex>)
status: active
nd6 options=121<PERFORMNUD,AUTO_LINKLOCAL,NO_DAD>
hn1: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
description: WAN (wan)
options=80018<VLAN_MTU,VLAN_HWTAGGING,LINKSTATE>
ether 00:15:5d:d2:76:87
inet6 fe80::215:5dff:fed2:7687%hn1 prefixlen 64 scopeid 0x6
inet6 fd01:2345:6789:abcd:215:5dff:fed2:7687 prefixlen 64 autoconf pltime 14400 vltime 86400
media: Ethernet autoselect (10Gbase-T <full-duplex>)
status: active
nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
No my tests always included a client in LAN pinging from their GUA or ULA to a destination on the internet.
We did quite some troubleshooting and checked the source code, and we also have an alternative setup now, which also requires promisc mode in our tests.
So there either must be a difference, or tests influence the result (eg using tcpdump will put interfaces in promisc and ndproxy suddenly works).
Just unsure whats the truth.
https://github.com/opnsense/docs/pull/717
Thank you for getting back to me :)
Just to make sure it's actually a WAN issue (not a LAN issue), I'd try a ping test from OPNsense itself. Source address: LAN interface address, destination address: something on the Internet. This won't work without ndproxy, but doesn't depend on a client in the LAN.
I made sure the interfaces are not in promiscuous mode when testing (no packet capture running).
Are you only testing with physical Intel NICs? So far, I've done all my testing with VMs. Maybe the driver plays a role in this... ND offloading? Just a guess.
Yeah so far I only used physical intel nics with physical DEC750 machines, and the client also has a physical NIC.
I could also test in Hyper-V or Proxmox, though lets wait now for other user reports since the scope of the issue is quite unclear.
If promisc is sometimes needed, and sometimes not, thats also fine in the end as now the user controls it without hidden automatism by the port.
Thanks for your feedback, especially that the MAC should be WAN was quite helpful figuring this out.
The discussion in here has been superseded.
I have written my own ndp proxy in lang/go which circumvents all of the issues described here.
I use it myself to proxy my /64 to multiple internal interfaces, and @Maurice tested it as well.
It's now generally available in 24.7.8. Have fun with it :)
QuoteIf you receive a DNS server from your ISP, but want the router to be the sole DNS server, use a Port Forward to force traffic destined to port 53 to the local running Unbound server instead.
I am very new to IPv6 and this is my hobby project so please be gentle. I have already implemented this in IPv4 with port forward to 127.0.0.1. How do I identify the IPv6 address of the local running Unbound server and implement for IPv6? My IPv6 stack is working well with this plugin with LAN configured as link-local so thanks for this.
That is a bit tricky: AFAIR, you cannot redirect on the loopback address on IPv6 because of RFC4921 section 2.5.3 (https://datatracker.ietf.org/doc/html/rfc4291) saying that the responses should not be routed, so "::1" is out of the question (as are link-local addresses for obvious reasons). For IPv4, this works with a redirection to 127.0.0.1.
What I do is something like this:
2025-11-29 09_36_03-Port Forward _ NAT _ Firewall _ OPNsense.mgsoft — Mozilla Firefox.png
The redirect target IP is an alias, which is a dynamic IPv6 alias on any IPv6-enabled interface with the EUI-64 of that interface (which is the same as the EUI-64 of the link-local IPv6).
Also note that I have an exception for one host alias (BLOB_MAC), which is identified by its MAC, because I cannot be sure if that client uses IPv6 privacy extensions. I need this exception because that client uses ACME with DNS-01 verification which Unbound cannot forward.
My general recommendation for setups which are a little more advanced is to bind services like DNS to loopback interfaces:
- Interfaces: Devices: Loopback, create a loopback interface, name it e.g. "Unbound".
- Assign the interface and configure it with static IP addresses (/128 ULA and /32 RFC1918 is fine).
- Services: Unbound DNS: General, set "Network Interfaces" to this loopback interface (only).
- In the DHCP / RA configuration, set the DNS server addresses to the loopback interface's addresses.
- Optional: If you want to force all DNS traffic to Unbound, forward port 53 to the loopback interface's addresses.
Cheers
Maurice
TWIMC: https://github.com/Monviech/ndp-proxy-go/issues/3
I got the proxy working now for PPPoE interfaces as well.
Also I just tried the port forward and it works for me without any tricks:
it might not be RFC conform but "hey it works I guess xD"
EDIT: DOESNT WORK!
Really? I just tried and it did not work for me like that.
I used ::1 as redirect target and used: "nslookup -query=A www.google.de 2001:4860:4860::8888" and got a communications error from a Linux client. The same thing works when I use a routeable IPv6 alias for OpnSense as a redirect target. Note that by using Google's DNS IPv6 explicitely, I force the IPv6 forwarding rule to be applied.
I recently had a dicussion with Patrick over this where he was surprised as well that it did not work.
His posting is here and OpnSense seems to adhere to RFC4291: https://forum.opnsense.org/index.php?msg=246585
Maybe you got an answer over a redundant DNS over IPv4?
Correct - to pull that link to the FreeBSD source from that other thread so you don't need to go on a scavenger hunt:
https://cgit.freebsd.org/src/tree/sys/netinet6/ip6_input.c?h=releng/14.3#n765
FreeBSD *should* categorically refuse to send a packet with source ::1 to anything but the loopback interface itself if I read that code correctly.
Yeah it seems like my assumption was wrong it fell back after not getting an answer:
Vlan:
21:15:23.740664 IP6 2003:a:177f:8463:b40e:4343:1cc8:df32.54262 > 2003:180:2:7000::53.53: 54276+ AAAA? ipv6.google.com. (33)
21:15:25.741343 IP 172.16.1.150.52057 > 172.16.1.1.53: 54276+ AAAA? ipv6.google.com. (33)
21:15:25.751291 IP 172.16.1.1.53 > 172.16.1.150.52057: 54276 2/0/0 CNAME ipv6.l.google.com., AAAA 2a00:1450:4016:800::200e (92)
Loopback doesnt respond:
21:15:23.740672 IP6 2003:a:177f:8463:b40e:4343:1cc8:df32.54262 > ::1.53: 54276+ AAAA? ipv6.google.com. (33)
Good to know, sorry xD
N.P., Cedrik, this just made it to https://forum.opnsense.org/index.php?topic=42985.0, point 29.
Quote from: meyergru on November 29, 2025, 09:43:57 AMThe redirect target IP is an alias, which is a dynamic IPv6 alias on any IPv6-enabled interface with the EUI-64 of that interface (which is the same as the EUI-64 of the link-local IPv6).
I believe this is working well. Did a DNS leak test https://browserleaks.com/dns (https://browserleaks.com/dns) and the DNS servers reported are those in my Unbound DoT forward list and not the ISP's DNS server. Thanks again.
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
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?
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.
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.
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.
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.
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.
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. :\
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
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 :)