Stop Dnsmasq sending ULA prefixes in stateless RAs

Started by OPNenthu, September 15, 2025, 12:54:10 AM

Previous topic - Next topic
September 15, 2025, 12:54:10 AM Last Edit: September 15, 2025, 01:10:53 AM by OPNenthu
My interfaces are set to WAN tracking for IPv6 which works fine with Dnsmasq using the 'ra-stateless' mode.  Clients get the dynamic /64 prefix in the RA and auto-configure their GUAs with SLAAC.

I want to additionally configure ULA ranges with DHCPv6 for internal networking and firewalling purposes.  The desire is that hosts with ULA addresses should communicate that way and only use their GUAs for internet, if this is possible.

The first thing I did was to add ULA virtual IPs (Interfaces->Virtual IPs) for each interface.  For example, on the LAN interface I added an IP alias like fd00:db8:1001::1/64.

The problem now is that Dnsmasq is picking up the ULA prefix on the interface in addition to the GUA dynamic prefix and advertising both in the RAs.  Therefore hosts are auto-configuring IPs in both ULA and GUA subnets, including privacy addresses for each.

Is there a way to prevent Dnsmasq from including the ULA prefix in the stateless RA?  I would like to manually specify a DHCPv6 range for those like fd00:db8:1001::1000 - fd00:db8:1001::ffff and for them to be excluded from host auto-configuration and privacy extensions.  Ideally, I'd like to specify the ranges as partial IPs in Dnsmasq like ::1000 - ::ffff using the interface Constructor, so that Dnsmasq will auto-register hosts in DNS.

I tried to set the 'slaac' mode in addition to the 'ra-stateless' mode as suggested in the OPNsense docs but I can't get the desired result.

September 16, 2025, 08:34:19 AM #1 Last Edit: September 16, 2025, 08:52:18 AM by OPNenthu
I've been doing some testing on OPNsense 25.7.3_7 and it works fine with Kea DHCPv6 + radvd, but Dnsmasq has some problems with this setup.  Maybe it's not capable at this time?

Starting assumption: IA_PD is configured with WAN tracking and /64 prefixes are being advertised via Unmanaged RAs (for auto-configured GUAs with SLAAC), very similar to this guide: https://forum.opnsense.org/index.php?topic=45822.0

Steps for adding ULA addressing via DHCPv6:

1) Add Virtual IP to interface (Interfaces->Virtual IPs->Settings):
- Mode: IP Alias
- Interface: <if_name>
- Network / Address: fd00:db8:abcd:1000::1/128

The /128 net mask is important so that the ULA prefix is not advertised by RA daemons.  Otherwise you get auto-configured ULAs, defeating the purpose.  As a side-effect of this, steps 2 & 3 are also needed.

2) Add gateway for ULA VIP (System->Gateways->Configuration):
- Name: ULA_<if_name>_GW
- Interface: <if_name>
- Address Family: IPv6
- IP Address: fd00:db8:abcd:1000::1

3) Add static route for subnet (System->Routes->Configuration):
- Network Address: fd00:db8:abcd:1000::/64
- Gateway: ULA_<if_name>_GW

4) Configure RAs (Services->Router Advertisements and select <if_name>):
- Router Advertisements: Assisted (M+O+A flags)
- Advertise Default Gateway: ✓
- DNS Options: Use the DNS configuration of the DHCPv6 server

5) Configure Kea DHCPv6 (Services->Kea DHCP->Kea DHCPv6):
- Settings->Enabled: ✓
- Settings->Interfaces: <if_name>
- Subnets->Subnet: fd00:db8:abcd:1000::/64
- Subnets->Interface: <if_name>
- Subnets->Pools: fd00:db8:abcd:1000::1000-fd00:db8:abcd:1000::ffff
- Subnets->DNS servers: <your DNS>
- Subnets->Domain search: <your domain>

In my case a router reboot and a client interface reset helped to get things working and the result was 2 GUAs from my ISP prefix (one is a temporary address from privacy extensions) and only one ULA address from DHCP:

$ ip -6 a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
3: enp10s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fdf8:fb25:3a87:1030::1000/128 scope global dynamic noprefixroute
       valid_lft 3078sec preferred_lft 1578sec
    inet6 2601:xx:xxxx:6db3:f996:f5:7061:e6d/64 scope global temporary dynamic
       valid_lft 86069sec preferred_lft 14069sec
    inet6 2601:xx:xxxx:6db3:8551:f7a2:xxxx:xxxx/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 86069sec preferred_lft 14069sec
    inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

---

I tried the same with Dnsmasq and with various combinations of RA modes in the Dnsmasq ranges.  I even tried splitting the IPv6 ranges: one for GUAs and one for ULAs.  In no cases could I reproduce the desired result.

When I don't use an interface constructor and specify the ranges as full IPs, then DHCPv6 doesn't work and I get an error in Dnsmasq logs:

Warning  dnsmasq-dhcp  no address range available for DHCPv6 request via vlan0.30

When I used the interface constructor and specified the ranges as suffixes (::1000-::ffff), then my client would get assigned DHCP addresses under both the GUA and ULA prefixes.  This is bad because it conflicts with with GUAs from SLAAC with privacy extensions.

When I tried to work around it by assigning the ULA to a Loopback device and then specifying that loopback interface as the constructor for the DHCP range, it also didn't work.  I suspect this creates some kind of routing issue that trips up Dnsmasq.

When I tried without a static route and gateway, using only a VIP with a net mask >= /64 (I tried several prefix lengths), then again the ULA prefix is advertised in the RAs and auto-configured by SLAAC.  I think this option came the closest and might have worked if Dnsmasq had an option to exclude interface-assigned prefixes from RAs.


References:

https://github.com/opnsense/core/issues/7513
https://github.com/opnsense/core/issues/8455

For more complicated configurations, using radvd is fine.

You can also use dnsmasq only for dhcpv6 and pair it with radvd for RA.
Hardware:
DEC740

September 17, 2025, 03:57:04 PM #3 Last Edit: September 17, 2025, 04:04:20 PM by OPNenthu
No doubt I'm probably over-complicating it.  Is there a better way to add DHCPv6 on top of SLAAC with dynamic prefixes?  The guide suggests it's possible within Dnsmasq itself:

https://docs.opnsense.org/manual/dnsmasq.html#dhcpv6-and-router-advertisements

QuoteWith ra-stateless, clients will only generate a SLAAC address. If clients should additionally receive a DHCPv6 address, set slaac instead.

... but as I found out, it doesn't have a mechanism to discriminate which prefixes to include or exclude for each.  Anything which is assigned to the interface will get used for both SLAAC and for DHCP.  What am I missing?

I should say that the goal is just to be able to assign static IPv6 IPs to internal hosts that don't change with ISP updates.

Check the dnsmasq man page or mailing list.

Maybe what you do is just not possible, or it is and theres something configuration wise missing.
Hardware:
DEC740

I'm getting closer to a working solution with pure Dnsmasq (no Kea or radvd).

To recap, there are two constraints to what I want to do:

1) RAs (regardless of radvd or Dnsmasq) advertise all of the assigned network prefixes from the interface.
2) Dnsmasq includes all of the interface assigned networks in its ranges.

#1 is workable with the steps listed earlier, using a /128 netmask for the ULA address on the interface.  This was proven out with radvd and seems to work also with Dnsmasq RAs. ✅

Credit to @franco for the idea in this GH comment: https://github.com/opnsense/core/issues/7513#issuecomment-2151416968

#2 is the expected behavior as per the Dnsmasq manual:

Quote--dhcp-range=::1,::400,constructor:eth0

will look for addresses on eth0 and then create a range from <network>::1 to <network>::400. If the interface is assigned more than one network, then the corresponding ranges will be automatically created, and then deprecated and finally removed again as the address is deprecated and then deleted. The interface name may have a final "*" wildcard. Note that just any address on eth0 will not do: it must not be an autoconfigured or privacy address, or be deprecated.

Based on my interpretation, I think that #2 should also be workable by simply not using an interface constructor for the DHCPv6 range.

I believe this should be a working solution:

You cannot view this attachment.

You cannot view this attachment.

You cannot view this attachment.


So far I have partial success.

From the Dnsmasq logs it seems like everything is working, but the client is not getting a ULA address.  On the positive side, there is no more ULA being configured by SLAAC when using Dnsmasq RAs.


2025-09-17T14:11:30-04:00 Informational dnsmasq-dhcp DHCPINFORMATION-REQUEST(vlan0.30) 00:04:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:45
2025-09-17T14:11:29-04:00 Informational dnsmasq-dhcp RTR-ADVERT(vlan0.30) 2601:xx:xxxx:6db3::
2025-09-17T14:11:29-04:00 Informational dnsmasq-dhcp RTR-SOLICIT(vlan0.30)
2025-09-17T14:11:28-04:00 Informational dnsmasq-dhcp DHCPACK(vlan0.30) 172.21.30.100 24:xx:xx:xx:xx:cd blackbox
2025-09-17T14:11:28-04:00 Informational dnsmasq-dhcp DHCPREQUEST(vlan0.30) 172.21.30.100 24:xx:xx:xx:xx:cd
...
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp router advertisement on 2601:xx:xxxx:6db3::, constructed for vlan0.30
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp DHCPv4-derived IPv6 names on 2601:xx:xxxx:6db3::, constructed for vlan0.30
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp DHCPv6 stateless on 2601:xx:xxxx:6db3::, constructed for vlan0.30
...
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp DHCPv6 stateless on vlan0.30
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp router advertisement on fdf8:fb25:3a87:1030::
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp DHCPv6, IP range fdf8:fb25:3a87:1030::1000 -- fdf8:fb25:3a87:1030::ffff, lease time 1d
...
2025-09-17T14:10:38-04:00 Informational dnsmasq-dhcp DHCP, IP range 172.21.30.100 -- 172.21.30.199, lease time 1d
...
2025-09-17T14:10:38-04:00 Warning dnsmasq warning: no upstream servers configured
2025-09-17T14:10:38-04:00 Informational dnsmasq compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset no-nftset auth DNSSEC loop-detect no-inotify dumpfile
2025-09-17T14:10:38-04:00 Informational dnsmasq started, version 2.91 cachesize 10000

3: enp10s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 24:xx:xx:xx:xx:cd brd ff:ff:ff:ff:ff:ff
    inet 172.21.30.100/24 brd 172.21.30.255 scope global dynamic noprefixroute enp10s0
       valid_lft 84387sec preferred_lft 84387sec
    inet6 2601:xx:xxxx:6db3:98a1:dce:f579:d397/64 scope global temporary dynamic
       valid_lft 86372sec preferred_lft 83857sec
    inet6 2601:xx:xxxx:6db3:8551:f7a2:xxxx:xxxx/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 86372sec preferred_lft 86372sec
    inet6 fe80::xxxx:xxxx:xxxx:ce08/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

---

Given that this worked under Kea DHCPv6, I wonder if there is something about the ULA assignment on the interface with the /128 netmask that does not play well with Dnsmasq but that Kea doesn't seem to be affected by.  This is where I'm stuck presently.

There's one hint that I overlooked earlier in the Dnsmasq manual, under the --dhcp-range section:

QuoteFor IPv6, the parameters are slightly different: instead of netmask and broadcast address, there is an optional prefix length which must be equal to or larger then the prefix length on the local interface.

I guess it's not possible to use a /128 on the interface with Dnsmasq, then :( 

Kea is apparently more permissive here, or it just doesn't care what's configured on the interface.  Not sure which.

Alright- was a fun experiment, and there is a working solution if needed.  Let me wind this thread down with a pointed community question:

I'd really rather stick to the simplicity of SLAAC GUAs for my IPv6 needs and not do DHCPv6 at all, but the problem is I don't know how to assign static IPs to servers.  For example when setting up a management interface in Proxmox, how does one fill this out if not using ULAs or static GUAs?

You cannot view this attachment.

I can't presently enable host-side firewalling in Proxmox because all my IPv6 traffic is getting default dropped since I can't specify networks and clients in rules owing to dynamic ISP prefixes.  What are the options, short of business-class internet plans?

Thanks!

SLAAC is automatic but deterministic. You just check which address a system got and it will keep that forever - unless the MAC address changes wich is rarely the case. Or keep the lower 64 bits and change the upper ones in case of a dynamic prefix.

In Proxmox edit /etc/network/interfaces and add this to the entry for e.g. vmbr0 or whatever your management interface is:
post-up echo "2" > /proc/sys/net/ipv6/conf/vmbr0/accept_ra

Afterwards you can use a Dynamic IPv6 Host type alias in OPNsense for firewall rules.

HTH,
Patrick
Deciso DEC750
People who think they know everything are a great annoyance to those of us who do. (Isaac Asimov)

September 18, 2025, 10:01:48 PM #9 Last Edit: September 18, 2025, 10:36:39 PM by meyergru
For what purpose do you need the static IPs?

I see two cases:

1. You have static IPv6 prefixes (which would be true in a business contract) - in that case, you can just use the IPv6 that is derived by the 64 bit VLAN prefix plus the EUI-64 suffix that is associated with the MAC of your server's NIC (i.e. which can be directly calculated from it). This will give you a 128 bit IPv6 with /64 to build your CIDR. The gateway will be either fe80:: plus the EUI-64 of the OpnSense interface or better, create a VIP of fe80::1/64 for any VLAN interface on OpnSense and use that as the gateway address, since it is easier to remember.

Patrick is right about how to allow RA assigments to interfaces in Proxmox and firewall rules, I do it like this in the bridge interface (explanation for bridge-mcsnoop is here):

        bridge-mcsnoop 0
        accept-ra 2
        autoconf 1
        pre-up echo 2 > /proc/sys/net/ipv6/conf/$IFACE/accept_ra
        post-up echo 2 > /proc/sys/net/ipv6/conf/$IFACE/accept_ra

$IFACE will be auto-filled with the interface name that you specify this in, you do not have to replace it yourself.

2. You have dynamic IPv6 prefixes - that is unfortunate, because you can only address your servers via DNS names and not assign any fixed IPv6 to them. I handle that by using a reverse proxy that translates the external WAN IPv6 of OpnSense to an IPv4 backend in a DMZ VLAN, preferably via HAproxy for more complex needs. That way, the internal server is always contacted via its RFC1918 IPv4, but externally, via the WAN IPv6, for which OpnSense does the dynamic DNS updates. For many DynDNS services, it is better to have the WAN prefix the same as for the internal VLANs, so I always use "request prefix only" and use a specific prefix for my WAN. That way, OpnSense can handle DynDNS for any local machine.

If it is a non-proxyable protocol or if the TLS / TCP termination has to be on the internal server, you can also do port-forwarding, which works for IPv6 just as it does for IPv4. Besides the dynamic prefix, which must always be handled via DNS, you can use the fixed EUI-64 part like in #1 and via dynamic IPv6 host aliases, also in firewall rules.


Most of that is explained here, at the end.


There is a small caveat, however: I found that Windows uses SLAAC in a different fashion: They normally do not use a MAC-derived EUI-64, but a fixed, randomized value. However, that is static, too, and the method can also be changed to MAC-based generation if need be via registry settings.
Intel N100, 4* I226-V, 2* 82559, 16 GByte, 500 GByte NVME, ZTE F6005

1100 down / 800 up, Bufferbloat A+