Tunnelblick IPv6 DNS – get it working

Basically – If you are using OpenVPN with IPv6 support, MacOS will not recognize it and only allow IPv4 DNS resolution.

References:
https://openvpn.net/vpn-server-resources/limited-ipv6-support-built-into-the-access-server/
https://apple.stackexchange.com/questions/304215/how-to-add-aaaa-flag-ipv6-to-dns-resolver-configuration-on-sierra
https://www.tunnelblick.net/cUsingScripts.html
https://tunnelblick.net/cConfigT.html#creating-and-installing-a-tunnelblick-vpn-configuration

First, you have to configure IPv6 in your OpenVPN Server. For OpenVPN AS, there is a guide linked above.

Once IPv6 is working in the tunnel, you will probably recognize that some services are working, but in some applications (like Safari), IPv6 only sites won’t work. This is because they use the systems DNS resolver, which doesn’t allow AAAA requests, if it thinks that it has no IPv6 internet access.

You can check this using:

scutil --dns                                                     
DNS configuration

resolver #1
  search domain[0] : openvpn
  nameserver[0] : 8.8.8.8
  nameserver[1] : 8.8.4.4
  flags    : Request A records
  reach    : 0x00000002 (Reachable)

As you can see above, the resolver is only used for A records.

On Stackexchange, user smammy created a nice writeup for Wireguard, that utilizes a script to convince the MacOS resolver to resolve AAAA records using the given DNS servers. This can easily be adapted for Tunnelblick.

You need to create a Tunnelblick configuration package by creating a folder, adding the necessary files to it and adding the .tblk ending to it. MacOS will recognize it as a Tunnelblick package then and allow you to install the configuration including the necessary scripts.

The folder should contain 4 files:

  • profile.ovpn
  • up-suffix.sh
  • down-suffix.sh
  • wg-updown.sh

profile.ovpn
This is your OpenVPN profile, in my case the auto login profile exported from OpenvpnAS.

up-suffix.sh
This is executed when establishing the connection.

./wg-updown.sh up utun3

down-suffix.sh
This is executed when terminating the connection.

./wg-updown.sh down utun3

wg-updown.sh
This is the script from stack exchange

#!/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*inet\s+(\S+)\s+-->\s+(\S+)\s+netmask\s+\S+')
v6pat = re.compile(r'^\s*inet6\s+(\S+?)(?:%\S+)?\s+prefixlen\s+(\S+)')
def get_tunnel_info(interface):
    ipv4s = dict(Addresses=[], DestAddresses=[])
    ipv6s = dict(Addresses=[], DestAddresses=[], Flags=[], PrefixLength=[])
    ifconfig = subprocess.run(["ifconfig", interface], capture_output=True,
                              check=True, text=True)
    for line in ifconfig.stdout.splitlines():
        v6match = v6pat.match(line)
        if v6match:
            ipv6s['Addresses'].append(v6match[1])
            # This is cribbed from Viscosity and probably wrong.
            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])
            continue
        v4match = v4pat.match(line)
        if v4match:
            ipv4s['Addresses'].append(v4match[1])
            ipv4s['DestAddresses'].append(v4match[2])
            continue
    return (ipv4s, ipv6s)

def run_scutil(commands):
    print(commands)
    subprocess.run(['scutil'], input=commands, check=True, text=True)

def up(interface):
    service_name = service_name_for_interface(interface)
    (ipv4s, ipv6s) = get_tunnel_info(interface)
    run_scutil('\n'.join([
        f"d.init",
        f"d.add Addresses * {' '.join(ipv4s['Addresses'])}",
        f"d.add DestAddresses * {' '.join(ipv4s['DestAddresses'])}",
        f"d.add InterfaceName {interface}",
        f"set State:/Network/Service/{service_name}/IPv4",
        f"set Setup:/Network/Service/{service_name}/IPv4",
        f"d.init",
        f"d.add Addresses * {' '.join(ipv6s['Addresses'])}",
        f"d.add DestAddresses * {' '.join(ipv6s['DestAddresses'])}",
        f"d.add Flags * {' '.join(ipv6s['Flags'])}",
        f"d.add InterfaceName {interface}",
        f"d.add PrefixLength * {' '.join(ipv6s['PrefixLength'])}",
        f"set State:/Network/Service/{service_name}/IPv6",
        f"set Setup:/Network/Service/{service_name}/IPv6",
    ]))

def down(interface):
    service_name = service_name_for_interface(interface)
    run_scutil('\n'.join([
        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 main():
    operation = sys.argv[1]
    interface = sys.argv[2]
    if operation == 'up':
        up(interface)
    elif operation == 'down':
        down(interface)
    else:
        raise NotImplementedError()

if __name__ == "__main__":
    main() 

When the folder/package is complete, it can be imported to Tunnelblick. You will get a lot of warnings.

After importing, the connection can be started. After it is established, the output of scutils should look something like this:

scutil --dns
DNS configuration

resolver #1
  search domain[0] : openvpn
  nameserver[0] : 8.8.8.8
  nameserver[1] : 8.8.4.4
  flags    : Request A records, Request AAAA records
  reach    : 0x00000002 (Reachable)

Success! AAAA DNS resolution should now work in all applications.

What I haven’t gotten to work yet, is that MacOS recognizes IPv6 DNS servers pushed trough the VPN config. I could only get it to resolve IPv6 addresses trough IPv4 DNS servers. If you have any success with this, please let me know.