Am I right to infer that at least one of your ISPs is delegating you only a /64?
So some of your devices have no stable L3 identity that they actually source traffic from, so you can't do per-host steering, and you have no way of doing per-class steering either unless you renumber your flat LAN on both IPv4 and IPv6 with an SSID per class and assign devices to the appropriate SSID, which is a potentially non-trivial topology adjustment.
So I can see the case for your first feature request, the MAC match. It's a simple solution to the problem and something that could probably be used by a good number of mwan3 users. Neither of the feature requests is technically challenging to implement on the face of it.
However, most Android and Apple devices randomise their MAC address anyway by default, so you're going to need to turn that feature off on the devices to be able to steer by MAC address.
The second feature request is a rather narrow use-case of VMs behind a laptop: it's a fragile hop-count match and you're the only person asking for it, so harder to justify, but I'll certainly look at it.
I followed through those links you posted and read a couple of the posts:
Following the link in the above post to the thread on IPv6 WAN fail-over without IPv6 NAT, it seems like you're doing this ULA addressing and NAT66 precisely because there's no proper IPv6 failover mechanism in mwan3?
Agreed, and so perhaps you'll be pleased to know that the very thing you're asking for in that thread (quoted above) is one of the pieces of functionality upcoming in the beta release IPv6 extensions I referred to earlier in this thread.
RFC 8475 describes the approach and the beta mwan3 implements it: when a WAN soft fails, mwan3 will deprecate that prefix by setting preferred_lft 0 on the downstream LAN, so new connections from LAN clients will automatically failover to the other non-deprecated prefix. Then when the WAN recovers, the preferred lifetime will be restored and clients will use that prefix again. Valid lifetime is left alone so there is no re-solicitation. This requires SLAAC addressing to work and a delegated GUA prefix per WAN such that the clients get one IPv6 GUA assigned per delegated prefix.
Also in the beta mwan3 code, there's a 1:1 translation capability that auto-carves a translation pool (which can be that WAN's own delegated prefix), avoiding any mapping target collisions by auto-discovering all in-use GUA segments from that prefix, although it currently only works for GUA LAN segments since there's no auto-discovery of ULA LAN segments. ULA segments will just be masqueraded automatically.
Shouldn't be hard to extend the 1:1 feature to ULA, although that's not currently done. This 1:1 translation feature allows for full inbound reachability and uses the netmap feature. It went through a couple of iterations: I originally considered implementing it using NPT stateless translation, but that doesn't preserve hostid at a /64 granularity and so ends up being less useful than it otherwise could be as it's almost impossible to link the translated address to the originating host. So I did a roll-my-own stateless implementation that preserved the hostid portion of the address at the cost of an incremental checksum adjustment, but I baulked at releasing a stateless NPT-style translation that was not RFC 6296 compliant and converted it to netfilter's netmap, which is a standard feature, is 1:1, but is stateful. It preserves the hostid portion of the address, so translated addresses are predictable and it's just a better and simpler all-round choice to deliver the functionality.
The beta mwan3 does NAT (either 1:1 or masquerade) only when needed. So for example, if you have a GUA on your LAN and the egress WAN is the correctly source routed WAN, no translation occurs. If a packet is steered by rules to a WAN that can't route that prefix, only then will NAT of either 1:1 if defined, or masquerade if not defined, apply. Any form of steering or load balancing will always require NAT unless you have provider independent addressing with BGP.
You noted as well that
I hope the beta mwan3 goes some way to fixing that as it takes over IPv6 routing and now does correct source routing, including router originated traffic with a catch-all failover rule in place, only applying NAT when a packet wants to egress a WAN with a source prefix that specific WAN doesn't route. That was a gap I think I've corrected in the beta.
Going back to your original problem, there is actually a feature in recent Android and which is supported by odhcpd, since I've tested it, that could fix your issue, but which (for no reason I can see) enforces GUA addressing and thus cannot use a ULA prefix. Android added DHCPv6-PD (IA_PD) in September 2025, per RFC 8415 and RFC 9762. The device requests its own dedicated prefix and if it obtains a prefix, it self-addresses from that prefix. So someone with a large enough delegation could potentially use this and give android clients each a /64, which then results in predictable addressing on each client that could then be used to steer based on source address, as any temporary address would fall within that prefix. In reality, it's impractical for anything other than at minimum a /56 ISP delegation.
Puzzlingly, the actual Google code rejects a ULA PD, so ULAs can't be used, making such a feature less useful in your own scenario. Not sure why it does so, since it strikes me that this would be a very useful feature to people whose ISPs give them only a small delegation.
If you're curious to test this feature, here's how I configured it on my own router (I did it on a VLANed test interface)
uci set network.test.ip6addr='<YOUR GUA>/60'
uci set dhcp.test.ra='server'
uci set dhcp.test.ra_slaac='1'
uci set dhcp.test.ra_default='2'
uci set dhcp.test.dhcpv6='server'
uci set dhcp.test.dhcpv6_pd='1'
uci set dhcp.test.dhcpv6_pd_min_len='64'
uci set dhcp.test.dhcpv6_pd_preferred='1'
uci commit network
uci commit dhcp
/etc/init.d/odhcpd reload
ifup test
I noticed when testing that when I flipped from GUA to ULA and vice versa in the example above, every time I did the flip, it actually caused my phone to reboot. Probably just a Samsung bug (I'm on Android 16)