Este fue un enorme Es difícil de entender, así que escribí una pequeña guía con la esperanza de que otros la encuentren útil:
Cómo convencer a MacOS de que haga búsquedas de DNS IPv6 cuando su única dirección IPv6 es a través de una VPN o un túnel de algún tipo
El problema
El resolvedor de nombres de dominio de MacOS sólo devolverá direcciones IPv6 (a partir de registros AAAA) cuando piense que tiene una dirección IPv6 válida y enrutable. Para las interfaces físicas como Ethernet o Wi-Fi es suficiente con establecer o que se le asigne una dirección IPv6, pero para los túneles (como los que utilizan utun
interfaces) hay algunos molestos pasos adicionales que hay que dar para convencer al sistema de que sí, efectivamente tienes una dirección IPv6, y sí, te gustaría recuperar las direcciones IPv6 para las búsquedas de DNS.
Yo uso wg-quick
para establecer un túnel WireGuard entre mi portátil y un servidor virtual Linode. WireGuard utiliza un utun
dispositivo de túnel del espacio de usuario para realizar la conexión. Así es como se configura ese dispositivo:
utun1: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1420
inet 10.75.131.2 --> 10.75.131.2 netmask 0xffffff00
inet6 fe80::a65e:60ff:fee1:b1bf%utun1 prefixlen 64 scopeid 0xc
inet6 2600:3c03::de:d002 prefixlen 116
nd6 options=201<PERFORMNUD,DAD>
Y aquí hay algunas líneas relevantes de mi tabla de enrutamiento:
Internet:
Destination 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 link#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:
Destination Gateway Flags Netif Expire
::/1 utun1 USc utun1
2600:3c03::de:d000/116 fe80::a65e:60ff:fee1:b1bf%utun1 Uc utun1
8000::/1 utun1 USc utun1
10.20.4/24
es mi red ethernet local.
10.20.4.5
es la dirección IP de la LAN de mi portátil.
10.20.4.4
es la dirección IP de la LAN de mi puerta de enlace.
10.75.131.2
es la dirección IPv4 de mi extremo del túnel punto a punto de WireGuard.
2600:3c03::de:d002
es la dirección IPv6 de mi extremo del túnel punto a punto de WireGuard.
50.116.51.30
es la dirección pública de mi servidor Linode.
Esto debería ser suficiente para tener conectividad IPv6, ¿verdad? Bueno, la resolución de nombres funciona cuando host
habla directamente con mi servidor de nombres:
sam@shiny ~> host ipv6.whatismyv6.com
ipv6.whatismyv6.com has IPv6 address 2607:f0d0:3802:84::128
El ping por dirección IPv6 funciona:
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 received, 0.0% packet loss
round-trip min/avg/max/std-dev = 80.991/80.991/80.991/0.000 ms
Y las conexiones HTTP por dirección IPv6 funcionan:
sam@shiny ~> curl -s 'http://[2607:f0d0:3802:84::128]' -H 'Host: ipv6.whatismyv6.com' | html2text | head -3
This page shows your IPv6 and/or IPv4 address
You are connecting with an IPv6 Address of:
2600:3c03::de:d002
Sin embargo, las conexiones HTTP por nombre de host sólo IPv6 no funcionan:
sam@shiny ~> curl 'http://ipv6.whatismyv6.com'
curl: (6) Could not resolve host: ipv6.whatismyv6.com
El resultado es el mismo en wget
así como en aplicaciones GUI como Firefox: la conexión mediante una dirección IPv6 literal funciona bien, pero la conexión mediante un nombre de host que sólo tiene un registro AAAA (y ningún registro A) asociado no.
Es interesante, ping6
es capaz de hacer una búsqueda de DNS y obtener una dirección IPv6 de vuelta:
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 received, 0.0% packet loss
round-trip min/avg/max/std-dev = 49.513/49.513/49.513/0.000 ms
¿Por qué puede ping6
¿hacer esto cuando nada más puede hacerlo? Resulta que cuando ping6
llama a getaddrinfo
sobrescribe las banderas por defecto. Una de las banderas por defecto es AI_ADDRCONFIG
, que indica al resolvedor que sólo devuelva las direcciones de las familias de direcciones para las que el sistema tiene una dirección IP. (Es decir, que no devuelva direcciones IPv6 a menos que el sistema tenga una dirección IPv6 (no link-local)). La mayoría de los otros programas añadir a las banderas por defecto en lugar de machacarlas, lo que supongo que es sensato.
Si se ejecuta scutil --dns
te dirá cómo está configurado el resolver. Aquí está la salida en mi sistema (menos un montón de cosas mdns que no importa):
DNS configuration
resolver #1
search domain[0] : home.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Request A records
reach : 0x00020002 (Reachable,Directly Reachable Address)
DNS configuration (for scoped queries)
resolver #1
search domain[0] : home.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Scoped, Request A records
reach : 0x00020002 (Reachable,Directly Reachable Address)
Tenga en cuenta que bajo flags
dice Request A records
pero no Request AAAA records
. Así que nos queda tratar de convencer al resolvedor de MacOS de que, de hecho, tenemos una dirección IPv6 válida, aunque esté en una interfaz de túnel.
SystemConfiguration
La forma "correcta" de que esto ocurra es que cualquier programa que configure el túnel utilice el extraño y en gran medida indocumentado SystemConfiguration
API para registrar el "servicio" de red y sus propiedades IPv6. La aplicación Viscosity hace esto. Tunnelblick no lo hace, el cliente oficial de OpenVPN no lo hace, y wg-quick
seguro que no.
El scutil
Kludge
Podemos crear las mismas estructuras de "servicio" de SystemConfiguration manualmente utilizando la función scutil
comando:
Primero creamos la parte IPv4 del servicio:
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:/Network/Service/my_ipv6_tunnel_service/IPv4
> set Setup:/Network/Service/my_ipv6_tunnel_service/IPv4
Y luego creamos la parte de IPv6:
> 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:/Network/Service/my_ipv6_tunnel_service/IPv6
> set Setup:/Network/Service/my_ipv6_tunnel_service/IPv6
> quit
Una vez hecho esto, la salida de scutil --dns
(de nuevo modulando las cosas de mdns) cambia:
DNS configuration
resolver #1
search domain[0] : home.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Request A records, Request AAAA records
reach : 0x00020002 (Reachable,Directly Reachable Address)
DNS configuration (for scoped queries)
resolver #1
search domain[0] : home.munkynet.org
nameserver[0] : 10.20.4.4
if_index : 14 (en3)
flags : Scoped, Request A records
reach : 0x00020002 (Reachable,Directly Reachable Address)
Ahora vemos Request AAAA records
¡en las banderas! No estoy muy seguro de lo que son las "scoped queries" o por qué la configuración de DNS para ellas no cambió, pero las cosas parecen funcionar ahora, así que da igual:
sam@shiny ~> curl -s 'http://ipv6.whatismyv6.com' | html2text | head -3
This page shows your IPv6 and/or IPv4 address
You are connecting with an IPv6 Address of:
2600:3c03::de:d002
Al desconectarse del túnel, lo único que tiene que hacer es eliminar las claves de SystemConfiguration que ha añadido:
sam@shiny ~> sudo scutil
> remove State:/Network/Service/my_ipv6_tunnel_service/IPv4
> remove Setup:/Network/Service/my_ipv6_tunnel_service/IPv4
> remove State:/Network/Service/my_ipv6_tunnel_service/IPv6
> remove Setup:/Network/Service/my_ipv6_tunnel_service/IPv6
> quit
Un par de cosas a tener en cuenta:
- El nombre
my_ipv6_tunnel_service
es totalmente arbitraria.
- Según la información que he obtenido de las secuencias de comandos scripts en el Mullvad
.ovpn
tiene que crear tanto el perfil Setup:
y State:
llaves. No he verificado esto porque soy perezoso.
- No tengo ni idea de dónde está el IPv6
DestAddresses
de la que viene. Los copié de Viscosity porque parecían funcionar allí. ::ffff:ffff:ffff:ffff:0:0
para la dirección local de enlace y ::
para el público
- Ni siquiera sé realmente qué
DestAddresses
significa o para qué se utiliza.
Un bonito script
Escribí un script de Python que obtiene direcciones y longitudes de prefijos de ifconfig
de salida. Requiere Python 3.6 o posterior, así que asegúrate de tenerlo en tu ruta. Se llama wg-updown
y llama a su servicio SystemConfiguration wg-updown-utun#
pero no es realmente específico de WireGuard. Puedes llamarlo como un post-up/pre-down script para cualquier túnel VPN antiguo o ejecutarlo manualmente. Llámalo así:
# After tunnel comes up
wg-updown up IFACE
# Before tunnel goes down
wg-updown down IFACE
sustituir IFACE
con el nombre de la interfaz que su cliente de túnel/VPN está utilizando, por ejemplo utun1
. Imprimirá los comandos que está enviando a scutil
para que puedas ver lo que está haciendo en detalle.
#!/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()