Windows Vista's DNS server priority issues in VPNs
Today I ran into a subtle issue regarding the order in which Windows Vista queries connection-specific DNS servers. I tested a setup with a PPTP VPN server that also provides DNS name resolution services to its VPN clients. For that purpose I ran both a BIND 9 name server and a Poptop PPPD daemon on the same box. It is dual-homed, i.e. one interface is the private interface of the VPN tunnel endpoint and the other one is the public Ethernet interface through which the server is linked to the internet. I configured BIND to listen on both interfaces.
A PPTP server can advertise ("push") certain connection-specific options to its client. One of these options is the IP address of a DNS server. I had two possible values for that option: the servers private and its public IP. Either should work because BIND is listening on both. Furthermore, I wanted the VPN connection's DNS server to be prioritized over the client's LAN connection's DNS server such that I could resolve local domain names (usually ending in .local
) that only make sense in the context of the private network.
Under Windows XP this works as expected. It always prioritizes the VPN connection's DNS server. Windows Vista, on the other hand, only does it conditionally: for a VPN connection's DNS server to be queried first at least one of two conditions must be met: Either
- the VPN connection has the "Use default gateway on remote network" option set or
- the advertised DNS server IP address lies within the subnet of the VPN tunnel.
The following example illustrates what happens if neither condition is met. Assume that server.bar.com/11.22.33.44 is the domain name/IP address of the server's public interface, server.foo.local/192.168.222.1 that of the private tunnel interface and that the server advertises a DNS server IP address of 11.22.33.44, i.e. the public address. When I tried to ping a local domain name, i.e. one that can only be resolved correctly by the DNS server software running on the VPN server
C:\> ping server.foo.local Ping request could not find host server.foo.local. Please check the name and try again.
Note that this is not a ICMP problem. Pinging the IP address works fine. It's just that the operating system's resolver used by the ping utility queries the LAN connection's DNS server first and if that server authoritatively returns a "not-found", the resolver will give up. The VPN DNS is never given a chance to resolve that host name. Interestingly, the nslookup program on Vista correctly prioritizes the VPN DNS server, regardless of whether the conditions above are met:
C:\> nslookup Default Server: server.bar.com Address: 11.22.33.44:53 > server.foo.local Server: server.bar.com Address: 11.22.33.44:53 Name: server.foo.local Address: 192.168.222.1
If the private IP address of the DNS server is advertised instead of the public one, Vista recognizes that it's part of the VPN subnet and correctly prioritizes it.
C:\> ping server.foo.local Pinging server.foo.local [192.168.222.1] with 32 bytes of data: Reply from 192.168.222.1: bytes=32 time=201ms TTL=64 C:\> nslookup Default Server: server.foo.local Address: 192.168.222.1:53 > server.foo.local Server: server.foo.local Address: 192.168.222.1:53 Name: server.foo.local Address: 192.168.222.1
Satisfying the second condition by enabling the "Use default gateway on remote network" option also works even if the DNS server's public IP is advertised:
C:\> ping server.foo.local Pinging server.foo.local [192.168.222.1] with 32 bytes of data: Reply from 192.168.222.1: bytes=32 time=207ms TTL=64 C:\> nslookup Default Server: server.bar.com Address: 11.22.33.44:53 > server.foo.local Server: server.bar.com Address: 11.22.33.44:53 Name: server.foo.local Address: 192.168.222.1
Rant against BIND 9
Why didn't I configure PPTP to advertise the tunnel endpoint as the DNS server's IP address in the first place? Had I done so I would never have run into the problem on the Vista clients. Again, the reason for this is complicated: The PPP interfaces on the server box go up and down whenever a client connects. When no client is connected there is no interface with the endpoint IP. This doesn't go well with BIND being very particular about setting up its listening sockets. To begin with, BIND doesn't provide a way to listen on any IP address regardless of whether an interface with that IP is currently up or not. Every other networking daemon I know of does it by simply calling the kernel's bind() function with INADDR_ANY. However, BIND insists on enumerating all interfaces and binding to their IP explicitly. This is fundamentally different to passing with INADDR_ANY to bind() because it doesn't work for interfaces that come up later on, like the PPP interfaces on my VPN server. Even if I explicitly specify an IP address to listen on using the listen-on option statement in named.conf, BIND will ignore it if there is no interface with that IP.
There also is the useless interface-interval
option which configures the interval at which BIND reenumerates networks interfaces to listen on. The default value for it is 60 minutes. Can someone please explain to me why I would want to wait 59 minutes for BIND to start listening on a new interface? I know I can reduce the value to 1 minute but even that would be too long a wait in most cases. If an interface comes up, BIND should listen on it immediately. Luckily, interface-interval
can be reduced to 0, disabling this enchanting "feature" entirely.
Making BIND listen
Disabling interface-interval still leaves the problem of how to get BIND to listen on an interface that's down when BIND starts up. The following technique worked for me. Before BIND starts, it simply aliases the loopback interface to the IP address that will later be used by the PPP interfaces.
# ipconfig lo:0 192.168.222.1 # /etc/init.d/named start # ipconfig lo:0 down
For this to work I had to specify interface-interval 0;
because otherwise BIND will stop listening after 60 minutes if there isn’t an open PPP connection at that time. Furthermore, I had to explicitly list all IPs to listen on in named.conf
which now reads:
... options { ... listen-on { 192.168.222.1; 127.0.0.1; 11.22.33.44; }; interface-interval: 0; }; ...