This was a large trouble to determine, so I wrote up just a little information in hopes that others would discover it useful:
The Drawback
macOS’s area identify resolver will solely return IPv6 addresses (from AAAA data) when it thinks that you’ve a legitimate routable IPv6 tackle. For bodily interfaces like Ethernet or Wi-Fi it is sufficient to set or be assigned an IPv6 tackle, however for tunnels (equivalent to these utilizing utun
interfaces) there are some additional annoying steps that have to be taken to persuade the system that sure, you certainly have an IPv6 tackle, and sure, you’d wish to get IPv6 addresses again for DNS lookups.
I take advantage of wg-quick
to determine a WireGuard tunnel between my laptop computer and a Linode digital server. WireGuard makes use of a utun
user-space tunnel machine to make the connection. This is how that machine will get configured:
utun1: flags=8051 mtu 1420
inet 10.75.131.2 --> 10.75.131.2 netmask 0xffffff00
inet6 fe80::a65e:60ff:fee1:b1bfpercentutun1 prefixlen 64 scopeid 0xc
inet6 2600:3c03::de:d002 prefixlen 116
nd6 choices=201
And here is a number of related traces from my routing desk:
Web:
Vacation spot Gateway Flags Refs Use Netif Expire
0/1 utun1 USc 0 0 utun1
default 10.20.4.4 UGSc 0 0 en3
10.20.4/24 hyperlink#14 UCS 3 0 en3 !
10.75.131.2 10.75.131.2 UH 0 0 utun1
50.116.51.30 10.20.4.4 UGHS 7 2629464 en3
128.0/1 utun1 USc 5 0 utun1
Internet6:
Vacation spot Gateway Flags Netif Expire
::/1 utun1 USc utun1
2600:3c03::de:d000/116 fe80::a65e:60ff:fee1:b1bfpercentutun1 Uc utun1
8000::/1 utun1 USc utun1
10.20.4/24
is my native ethernet community.10.20.4.5
is my laptop computer’s LAN IP tackle.10.20.4.4
is my gateway’s LAN IP tackle.10.75.131.2
is the IPv4 tackle of my finish of the WireGuard point-to-point tunnel.2600:3c03::de:d002
is the IPv6 tackle of my finish of the WireGuard point-to-point tunnel.50.116.51.30
is the general public tackle of my Linode server.
This must be sufficient to have IPv6 connectivity, proper? Effectively, identify decision works when host
talks on to my identify server:
sam@shiny ~> host ipv6.whatismyv6.com
ipv6.whatismyv6.com has IPv6 tackle 2607:f0d0:3802:84::128
Pinging by IPv6 tackle works:
sam@shiny ~> ping6 -c1 2607:f0d0:3802:84::128
PING6(56=40+8+8 bytes) 2600:3c03::de:d002 --> 2607:f0d0:3802:84::128
16 bytes from 2607:f0d0:3802:84::128, icmp_seq=0 hlim=55 time=80.991 ms
--- 2607:f0d0:3802:84::128 ping6 statistics ---
1 packets transmitted, 1 packets acquired, 0.0% packet loss
round-trip min/avg/max/std-dev = 80.991/80.991/80.991/0.000 ms
And HTTP connections by IPv6 tackle work:
sam@shiny ~> curl -s 'http://[2607:f0d0:3802:84::128]' -H 'Host: ipv6.whatismyv6.com' | html2text | head -3
This web page reveals your IPv6 and/or IPv4 tackle
You're connecting with an IPv6 Handle of:
2600:3c03::de:d002
Nevertheless, HTTP connections by IPv6-only hostname do not work:
sam@shiny ~> curl 'http://ipv6.whatismyv6.com'
curl: (6) Couldn't resolve host: ipv6.whatismyv6.com
The end result is similar in wget
in addition to in GUI apps like Firefox: connecting by a literal IPv6 tackle works positive, however connecting by a hostname that solely has an AAAA file (and no A file) related to it doesn’t.
Curiously, ping6
is in a position to do a DNS lookup and get an IPv6 tackle again:
sam@shiny ~ [6]> ping6 -c1 ipv6.whatismyv6.com
PING6(56=40+8+8 bytes) 2600:3c03::de:d002 --> 2607:f0d0:3802:84::128
16 bytes from 2607:f0d0:3802:84::128, icmp_seq=0 hlim=55 time=49.513 ms
--- ipv6.whatismyv6.com ping6 statistics ---
1 packets transmitted, 1 packets acquired, 0.0% packet loss
round-trip min/avg/max/std-dev = 49.513/49.513/49.513/0.000 ms
Why can ping6
do that when nothing else can? It seems that when ping6
calls getaddrinfo
it overwrites the default flags. One of many default flags is AI_ADDRCONFIG
, which tells the resolver to solely return addresses in tackle households that the system has an IP tackle for. (That’s, do not return IPv6 addresses except the system has a (not link-local) IPv6 tackle.) Most different applications add to the default flags reasonably than clobbering them, which I suppose is wise.
In the event you run scutil --dns
it can let you know how the resolver is about up. This is the output on my system (minus a bunch of mdns stuff that does not matter):
DNS configuration
resolver #1
search area[0] : dwelling.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Request A data
attain : 0x00020002 (Reachable,Immediately Reachable Handle)
DNS configuration (for scoped queries)
resolver #1
search area[0] : dwelling.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Scoped, Request A data
attain : 0x00020002 (Reachable,Immediately Reachable Handle)
Notice that underneath flags
, it says Request A data
however not Request AAAA data
. So it is left to us to attempt to persuade macOS’s resolver that we do the truth is have a legitimate IPv6 tackle, despite the fact that it is on a tunnel interface.
SystemConfiguration
The “proper” manner for this to occur is for no matter program units up the tunnel to make use of the weird and largely undocumented SystemConfiguration
API to register the community “service” and its IPv6 properties. The Viscosity app does this. Tunnelblick doesn’t, the official OpenVPN Consumer doesn’t, and wg-quick
certain as hell would not.
The scutil
Kludge
We will create the identical SystemConfiguration “service” strucures manually utilizing the scutil
command:
First we create the IPv4 a part of the service:
sam@shiny ~> sudo scutil
> d.init
> d.add Addresses * 10.75.131.2
> d.add DestAddresses * 10.75.131.2
> d.add InterfaceName utun1
> set State:/Community/Service/my_ipv6_tunnel_service/IPv4
> set Setup:/Community/Service/my_ipv6_tunnel_service/IPv4
After which we create the IPv6 half:
> d.init
> d.add Addresses * fe80::a65e:60ff:fee1:b1bf 2600:3c03::de:d002
> d.add DestAddresses * ::ffff:ffff:ffff:ffff:0:0 ::
> d.add Flags * 0 0
> d.add InterfaceName utun1
> d.add PrefixLength * 64 116
> set State:/Community/Service/my_ipv6_tunnel_service/IPv6
> set Setup:/Community/Service/my_ipv6_tunnel_service/IPv6
> give up
As soon as that is carried out, the output of scutil --dns
(once more modulo mdns stuff) adjustments:
DNS configuration
resolver #1
search area[0] : dwelling.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Request A data, Request AAAA data
attain : 0x00020002 (Reachable,Immediately Reachable Handle)
DNS configuration (for scoped queries)
resolver #1
search area[0] : dwelling.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Scoped, Request A data
attain : 0x00020002 (Reachable,Immediately Reachable Handle)
Now we see Request AAAA data
within the flags! I am not likely certain what “scoped queries” are or why the DNS configuration for them did not change, however issues appear to work now so no matter:
sam@shiny ~> curl -s 'http://ipv6.whatismyv6.com' | html2text | head -3
This web page reveals your IPv6 and/or IPv4 tackle
You're connecting with an IPv6 Handle of:
2600:3c03::de:d002
When disconnecting from the tunnel, all you need to do is take away the SystemConfiguration keys you added:
sam@shiny ~> sudo scutil
> take away State:/Community/Service/my_ipv6_tunnel_service/IPv4
> take away Setup:/Community/Service/my_ipv6_tunnel_service/IPv4
> take away State:/Community/Service/my_ipv6_tunnel_service/IPv6
> take away Setup:/Community/Service/my_ipv6_tunnel_service/IPv6
> give up
A pair issues to notice:
- The identify
my_ipv6_tunnel_service
is completely arbitrary. - Based on info I gleaned from the up/down scripts within the Mullvad
.ovpn
profile, you need to create each theSetup:
andState:
keys. I did not confirm this as a result of I’m lazy. - I’ve no clue the place the IPv6
DestAddresses
come from. I copied these from Viscosity as a result of they appeared to work there.::ffff:ffff:ffff:ffff:0:0
for the link-local tackle and::
for the general public - I do not even actually know what
DestAddresses
means or what it is used for.
A pleasant script
I wrote a python script that gleans addresses and prefix lengths from ifconfig
output. It requires Python 3.6 or later so be sure to’ve bought that in your path. It is known as wg-updown
and calls its SystemConfiguration service wg-updown-utun#
, nevertheless it’s not likely WireGuard-specific. You may name it as a post-up/pre-down script for any previous VPN tunnel or run it manually. Name it like this:
# After tunnel comes up
wg-updown up IFACE
# Earlier than tunnel goes down
wg-updown down IFACE
change IFACE
with the identify of the interface that your tunnel/VPN shopper is utilizing, e.g. utun1
. It would print the instructions that it is sending to scutil
so you’ll be able to see what it is doing intimately.
#!/usr/bin/env python3
import re
import subprocess
import sys
def service_name_for_interface(interface):
return 'wg-updown-' + interface
v4pat = re.compile(r'^s*inets+(S+)s+-->s+(S+)s+netmasks+S+')
v6pat = re.compile(r'^s*inet6s+(S+?)(?:%S+)?s+prefixlens+(S+)')
def get_tunnel_info(interface):
ipv4s = dict(Addresses=[], DestAddresses=[])
ipv6s = dict(Addresses=[], DestAddresses=[], Flags=[], PrefixLength=[])
ifconfig = subprocess.run(["ifconfig", interface], capture_output=True,
verify=True, textual content=True)
for line in ifconfig.stdout.splitlines():
v6match = v6pat.match(line)
if v6match:
ipv6s['Addresses'].append(v6match[1])
# That is cribbed from Viscosity and possibly fallacious.
if v6match[1].startswith('fe80'):
ipv6s['DestAddresses'].append('::ffff:ffff:ffff:ffff:0:0')
else:
ipv6s['DestAddresses'].append('::')
ipv6s['Flags'].append('0')
ipv6s['PrefixLength'].append(v6match[2])
proceed
v4match = v4pat.match(line)
if v4match:
ipv4s['Addresses'].append(v4match[1])
ipv4s['DestAddresses'].append(v4match[2])
proceed
return (ipv4s, ipv6s)
def run_scutil(instructions):
print(instructions)
subprocess.run(['scutil'], enter=instructions, verify=True, textual content=True)
def up(interface):
service_name = service_name_for_interface(interface)
(ipv4s, ipv6s) = get_tunnel_info(interface)
run_scutil('n'.be part of([
f"d.init",
f"d.add Addresses * {' '.join(ipv4s['Addresses'])}",
f"d.add DestAddresses * {' '.be part of(ipv4s['DestAddresses'])}",
f"d.add InterfaceName {interface}",
f"set State:/Community/Service/{service_name}/IPv4",
f"set Setup:/Community/Service/{service_name}/IPv4",
f"d.init",
f"d.add Addresses * {' '.be part of(ipv6s['Addresses'])}",
f"d.add DestAddresses * {' '.be part of(ipv6s['DestAddresses'])}",
f"d.add Flags * {' '.be part of(ipv6s['Flags'])}",
f"d.add InterfaceName {interface}",
f"d.add PrefixLength * {' '.be part of(ipv6s['PrefixLength'])}",
f"set State:/Community/Service/{service_name}/IPv6",
f"set Setup:/Community/Service/{service_name}/IPv6",
]))
def down(interface):
service_name = service_name_for_interface(interface)
run_scutil('n'.be part of([
f"remove State:/Network/Service/{service_name}/IPv4",
f"remove Setup:/Network/Service/{service_name}/IPv4",
f"remove State:/Network/Service/{service_name}/IPv6",
f"remove Setup:/Network/Service/{service_name}/IPv6",
]))
def important():
operation = sys.argv[1]
interface = sys.argv[2]
if operation == 'up':
up(interface)
elif operation == 'down':
down(interface)
else:
increase NotImplementedError()
if __name__ == "__main__":
important()