IPv6 IP address leak using policy-based routing

First of all you should be delegating some prefix from Mullvad to the lan. But if the prefix is also ULA, the hosts in the lan will prefer to use the GUA prefix from your provider. So you would have to drop the prefix from your provider to force the hosts to use the address from Mullvad.

Thanks; feared as much. I'll see if I can get something working with nat6.

If Mullvad is providing you a ULA prefix, then they are doing NAT6 on it so you don't need to do it again.

Sorry, a bit out of my depth here. Does that mean setting the ULA prefix in the network config to the prefix provided by Mullvad?

config globals 'globals'
	option ula_prefix '<mullvad prefix goes here>::/48'

This would be an option and I think it is the easiest.
Use ip6class local in downstream configuration to let only local addresses be assigned to lan hosts.

Perfect; thanks. The mullvad prefix is fc00:bbbb:bbbb:bb01, so I'll do this:

config globals 'globals'
	option ula_prefix 'fc00:bbbb:bbbb:bb01::/48'

and then in /etc/config/network, the following stanza:

config interface lan
        ...
        list ip6class local
        ...
       

Will let you know how I get on. Thanks for your assistance.

No dice, unfortunately:

I can see that my client has an IPv6 address with the correct prefix. However, there's no IPv6 connectivity; ping6 openwrt.org fails ('no route to host', for instance and I can't load https://ipv6.am.i.mullvad.net.

Any chance we're looking at a mis-configuration with my policy-based rules?

What is the output of the following?
ip -6 addr; ip -6 ru; ip -6 ro; ifstatus mullvad

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
5: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fe80::20d:b9ff:fe51:2638/64 scope link 
       valid_lft forever preferred_lft forever
6: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fe80::20d:b9ff:fe51:2639/64 scope link 
       valid_lft forever preferred_lft forever
90: br-family: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fe80::20d:b9ff:fe51:2639/64 scope link 
       valid_lft forever preferred_lft forever
92: br-guest: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fe80::20d:b9ff:fe51:2639/64 scope link 
       valid_lft forever preferred_lft forever
94: br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fc00:bbbb:bbbb:bb01::1/60 scope global noprefixroute 
       valid_lft forever preferred_lft forever
    inet6 fe80::20d:b9ff:fe51:2639/64 scope link 
       valid_lft forever preferred_lft forever
95: br-streaming: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fe80::20d:b9ff:fe51:2639/64 scope link 
       valid_lft forever preferred_lft forever
99: pppoe-wan: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 state UNKNOWN qlen 3
    inet6 2a02:8011:d000:71f::1/64 scope global dynamic noprefixroute 
       valid_lft 17987sec preferred_lft 1787sec
    inet6 fe80::1/10 scope link 
       valid_lft forever preferred_lft forever
100: mullvad: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 state UNKNOWN qlen 1000
    inet6 fc00:bbbb:bbbb:bb01::39a6/128 scope global 
       valid_lft forever preferred_lft forever
0:	from all lookup local 
32746:	from all fwmark 0x20000/0xff0000 lookup 202 
32747:	from all fwmark 0x10000/0xff0000 lookup 201 
32766:	from all lookup main 
4200000001:	from all iif lo failed_policy
4200000005:	from all iif eth0 failed_policy
4200000090:	from all iif br-family failed_policy
4200000092:	from all iif br-guest failed_policy
4200000094:	from all iif br-lan failed_policy
4200000095:	from all iif br-streaming failed_policy
4200000098:	from all iif wgserver failed_policy
4200000099:	from all iif pppoe-wan failed_policy
4200000099:	from all iif pppoe-wan failed_policy
4200000099:	from all iif pppoe-wan failed_policy
4200000100:	from all iif mullvad failed_policy
default from 2a02:8010:672e::/48 via fe80::d2f0:dbff:fe6c:e000 dev pppoe-wan proto static metric 512 pref medium
default from 2a02:8011:d000:71f::/64 via fe80::d2f0:dbff:fe6c:e000 dev pppoe-wan proto static metric 512 pref medium
unreachable 2a02:8010:672e::/48 dev lo proto static metric 2147483647 error 4294967183 pref medium
2a02:8011:d000:71f::/64 dev pppoe-wan proto static metric 256 pref medium
fc00:bbbb:bbbb:bb01::39a6 dev mullvad proto kernel metric 256 pref medium
fc00:bbbb:bbbb:bb01::/64 dev br-lan proto static metric 1024 pref medium
unreachable fc00:bbbb:bbbb::/48 dev lo proto static metric 2147483647 error 4294967183 pref medium
fe80::/64 dev eth1 proto kernel metric 256 pref medium
fe80::/64 dev br-lan proto kernel metric 256 pref medium
fe80::/64 dev br-streaming proto kernel metric 256 pref medium
fe80::/64 dev br-family proto kernel metric 256 pref medium
fe80::/64 dev br-guest proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
fe80::/10 dev pppoe-wan metric 1 pref medium
fe80::/10 dev pppoe-wan proto kernel metric 256 pref medium
{
	"up": true,
	"pending": false,
	"available": true,
	"autostart": true,
	"dynamic": false,
	"uptime": 24,
	"l3_device": "mullvad",
	"proto": "wireguard",
	"updated": [
		"addresses"
	],
	"metric": 0,
	"dns_metric": 0,
	"delegation": true,
	"ipv4-address": [
		{
			"address": "10.99.57.166",
			"mask": 32
		}
	],
	"ipv6-address": [
		{
			"address": "fc00:bbbb:bbbb:bb01::39a6",
			"mask": 128
		}
	],
	"ipv6-prefix": [
		
	],
	"ipv6-prefix-assignment": [
		
	],
	"route": [
		
	],
	"dns-server": [
		
	],
	"dns-search": [
		
	],
	"neighbors": [
		
	],
	"inactive": {
		"ipv4-address": [
			
		],
		"ipv6-address": [
			
		],
		"route": [
			
		],
		"dns-server": [
			
		],
		"dns-search": [
			
		],
		"neighbors": [
			
		]
	},
	"data": {
		
	}
}

Try this script to add a default gateway:

source /lib/functions/network.sh
network_find_mullvad NET_IF6
network_get_gateway6 NET_GW6 "${NET_IF6}"
uci add network route6
uci set network.@route6[-1].interface="${NET_IF6}"
uci set network.@route6[-1].target="::/0"
uci set network.@route6[-1].gateway="${NET_GW6}"
uci commit network
service network reload

thanks. Not keen on the network_find_mullvad function:

 network_find_mullvad: not found

This looks odd to me; might be 'wild goose' rather than 'golden goose', though:

root@OpenWrt:~# ping6 -I mullvad openwrt.org
PING openwrt.org (2a03:b0c0:3:d0::1af1:1): 56 data bytes
ping6: sendto: Permission denied

solved that one with the following, but still no luck directing IPv6 traffic over the mullvad interface

config route6
	option target '::/0'
    option interface 'mullvad'

Which policy do you have in VPN-PBR for the IPv6 traffic?

This is my VPN-PBR rule. Current status is: 'no IPv6 connectivity at all'; not even via my ISP.

config policy
	option chain 'PREROUTING'
	option name 'Private'
	option proto 'tcp udp'
	option interface 'mullvad'
	option src_addr '192.168.10.1/24 fc00:bbbb:bbbb:bb01::/48'

and this is the diff since my initial post (forcing lan clients to use the prefix supplied by Mullvad and the route6 stanza):

/etc/config/network
config globals 'globals'
        option ula_prefix 'fc00:bbbb:bbbb:bb01::/48'

config interface 'lan'
        option type 'bridge'
        option proto 'static'
        option ip6assign '60'
        list ip6class local           
        option netmask '255.255.255.0'
        option ipaddr '192.168.10.1'
        option ifname 'eth1 eth2'

config route6
	option target '::/0'
    option interface 'mullvad'

ah, maybe because the policy-based rule includes the mullvad interface? How would I solve that, should it be the case?

Let's have a look in the configuration:
uci export vpn-policy-routing; /etc/init.d/vpn-policy-routing support

package vpn-policy-routing

config vpn-policy-routing 'config'
	option verbosity '2'
	option strict_enforcement '1'
	option boot_timeout '30'
	list supported_interface 'wan'
	list supported_interface 'mullvad'
	list ignored_interface 'wgserver'
	option dest_ipset 'ipset'
	list webui_supported_protocol 'tcp'
	list webui_supported_protocol 'udp'
	list webui_supported_protocol 'tcp udp'
	list webui_supported_protocol 'icmp'
	list webui_supported_protocol 'all'
	option src_ipset '0'
	option ipv6_enabled '1'
	option iptables_rule_option 'append'
	option iprule_enabled '0'
	option webui_enable_column '0'
	option webui_chain_column '0'
	option webui_sorting '1'
	option webui_protocol_column '1'
	option enabled '1'

config policy
	option chain 'PREROUTING'
	option name 'Private'
	option proto 'tcp udp'
	option interface 'mullvad'
	option src_addr '192.168.10.1/24 fc00:bbbb:bbbb:bb01::/48'

config policy
	option chain 'PREROUTING'
	option name 'Family'
	option src_addr '192.168.30.1/24'
	option proto 'tcp udp'
	option interface 'mullvad'

vpn-policy-routing 0.2.1-13 running on OpenWrt 19.07.3. WAN (IPv4): wan/dev/62.3.80.17.
============================================================
Dnsmasq version 2.80  Copyright (c) 2000-2018 Simon Kelley
Compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth DNSSEC no-ID loop-detect inotify dumpfile
============================================================
Routes/IP Rules
default         losubs.subs.bng 0.0.0.0         UG    0      0        0 pppoe-wan
IPv4 Table 201: default via 62.3.80.17 dev pppoe-wan
10.0.0.0/24 dev br-guest proto kernel scope link src 10.0.0.1
192.168.20.0/24 dev br-streaming proto kernel scope link src 192.168.20.1
192.168.30.0/24 dev br-family proto kernel scope link src 192.168.30.1
192.168.99.0/24 dev wgserver proto kernel scope link src 192.168.99.1
IPv4 Table 201 Rules:
32739:	from all fwmark 0x10000/0xff0000 lookup 201
IPv4 Table 202: default via 10.99.57.166 dev mullvad
10.0.0.0/24 dev br-guest proto kernel scope link src 10.0.0.1
192.168.20.0/24 dev br-streaming proto kernel scope link src 192.168.20.1
192.168.30.0/24 dev br-family proto kernel scope link src 192.168.30.1
192.168.99.0/24 dev wgserver proto kernel scope link src 192.168.99.1
IPv4 Table 202 Rules:
32738:	from all fwmark 0x20000/0xff0000 lookup 202
IPv6 Table 201: default from 2a02:8010:672e::/48 via fe80::d2f0:dbff:fe6c:e000 dev pppoe-wan proto static metric 512 pref medium
IPv6 Table 201: default from 2a02:8011:d000:71f::/64 via fe80::d2f0:dbff:fe6c:e000 dev pppoe-wan proto static metric 512 pref medium
IPv6 Table 201: 2a02:8011:d000:71f::/64 dev pppoe-wan proto static metric 256 pref medium
IPv6 Table 201: fe80::/10 dev pppoe-wan metric 1 pref medium
IPv6 Table 201: fe80::/10 dev pppoe-wan proto kernel metric 256 pref medium
IPv6 Table 202: fc00:bbbb:bbbb:bb01::39a6 dev mullvad proto kernel metric 256 pref medium
IPv6 Table 202: default dev mullvad proto static metric 1024 pref medium
============================================================
IP Tables PREROUTING
-N VPR_PREROUTING
-A VPR_PREROUTING -s 192.168.30.0/24 -p udp -m comment --comment Family -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -s 192.168.30.0/24 -p tcp -m comment --comment Family -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -s 192.168.10.0/24 -p udp -m comment --comment Private -c 25 3608 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -s 192.168.10.0/24 -p tcp -m comment --comment Private -c 457 344915 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -m set --match-set mullvad dst -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -m set --match-set wan dst -c 0 0 -j MARK --set-xmark 0x10000/0xff0000
============================================================
IP6 Tables PREROUTING
-N VPR_PREROUTING
-A VPR_PREROUTING -s fc00:bbbb:bbbb::/48 -p udp -m comment --comment Private -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_PREROUTING -s fc00:bbbb:bbbb::/48 -p tcp -m comment --comment Private -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
============================================================
IP Tables FORWARD
-N VPR_FORWARD
-A VPR_FORWARD -m set --match-set mullvad dst -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_FORWARD -m set --match-set wan dst -c 0 0 -j MARK --set-xmark 0x10000/0xff0000
============================================================
IPv6 Tables FORWARD
-N VPR_FORWARD
============================================================
IP Tables INPUT
-N VPR_INPUT
-A VPR_INPUT -m set --match-set mullvad dst -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_INPUT -m set --match-set wan dst -c 0 0 -j MARK --set-xmark 0x10000/0xff0000
============================================================
IPv6 Tables INPUT
-N VPR_INPUT
============================================================
IP Tables OUTPUT
-N VPR_OUTPUT
-A VPR_OUTPUT -m set --match-set mullvad dst -c 0 0 -j MARK --set-xmark 0x20000/0xff0000
-A VPR_OUTPUT -m set --match-set wan dst -c 0 0 -j MARK --set-xmark 0x10000/0xff0000
============================================================
IPv6 Tables OUTPUT
-N VPR_OUTPUT
============================================================
Current ipsets
create wan hash:net family inet hashsize 1024 maxelem 65536 comment
create mullvad hash:net family inet hashsize 1024 maxelem 65536 comment
============================================================
Your support details have been logged to '/var/vpn-policy-routing-support'. [✓]

By the way, I've removed option append_src_rules '! -d 192.168.0.0/16' from the VPN-PBR config, since this threw errors with the IPv6 PBR (which makes sense; but is something I will need to have working at some point).

Thanks, as ever, for your continued help.

have just spotted this. Potentially related?

Thu Jun  4 09:48:34 2020 daemon.warn odhcpd[3581]: A default route is present but there is no public prefix on lan thus we don't announce a default route!

I guess this could be. Can you ping from the OpenWrt an IPv6 address using the mullvad vpn?
ping6 -I mullvad ipv6.google.com
If yes, then add the following:

uci set dhcp.lan.ra_default='1'
uci commit dhcp
service dnsmasq restart

Thanks. Yes, ping6 from the router works (but fails on a client with "No route to host"). Have added ra_default='1' so that the stanza now looks as follows; but still no IPv6 Connectivity. I also see:

Thu Jun  4 11:02:39 2020 daemon.err dnsmasq[6556]: failed to send packet: Host is unreachable

and the 'default route / no lan prefix' warning from odhcpd

/etc/config/dhcp
config dhcp 'lan'
	option instance 'main'
	option interface 'lan'
	option start '100'
	option limit '150'
	option dhcpv6 'server'
	option ra 'server'
	option ra_management '1'
	option leasetime '168h'
	option ra_default '1'