OpenVPN Client Names in dnsmasq: Script or Static?
Client addresses change, service names do not. Generate client names from the VPN session, pin service names in a drop-in, then make clients use it.
Two name problems appear on almost every small OpenVPN gateway. You cannot reach a connected laptop by the name on its certificate, and the services behind the tunnel have no names at all. They look like one problem and they need opposite treatments, because one address moves with every session and the other does not. Decide which kind of name you are adding before you edit anything.
Decide per name, not per gateway
The test is simple: does this address change when a client reconnects? If yes, the name has to be generated from the VPN session. If no, it belongs in a static entry you can read and review.
| Name kind | Source of truth | Where it lives |
|---|---|---|
| Connected client | The address OpenVPN just assigned | Generated hosts file, rewritten per session |
| Service behind the tunnel | A fixed address you chose | Static address= drop-in |
| Anything with a certificate name | The certificate, not the pool | Either, but the name must match the certificate |
Mixing them is the usual failure. A service pinned into the generated file disappears the first time the script rewrites it, and a client written into a static entry points at whoever held that address last. Both mistakes stay invisible until a name resolves to the wrong machine. By then the person debugging it assumes DNS is broken rather than the file.
The dynamic path: learn-address and the pool
OpenVPN knows the assignment, so let it tell you. These server options make the assignment durable and scriptable:
client-config-dir /etc/openvpn/ccd
ifconfig-pool-persist /etc/openvpn/ip-pool 60
script-security 2
learn-address /etc/openvpn/learn-address.sh
learn-address runs your script whenever an address is added, updated or deleted, and passes the operation, the address and the common name. A minimal script appends one line per client and removes it on delete:
#!/bin/sh
HOSTS=/etc/openvpn/hosts.lan
case "$1" in
add|update) echo "$2 $3" >> "$HOSTS" ;;
delete) sed -i "/^$2 /d" "$HOSTS" ;;
esac
kill -HUP $(cat /var/run/dnsmasq.pid)
On a tun device the second argument is the client's tunnel address, so the line is directly usable. On a tap device OpenVPN passes the client MAC address instead, because a tap segment can carry protocols other than IPv4, and then you have to look the address up in the pool file by common name before writing anything.
Be clear about what the generated file is: a snapshot of who is connected right now. Names vanish on disconnect, which is exactly what you want for laptops that move between networks, and exactly what you do not want for a server that is up all night.
The static path: address= and domain expansion
Point dnsmasq at the generated file instead of the system hosts file, and give it a domain so short names expand into fully qualified ones:
domain=corp.example
no-hosts
addn-hosts=/etc/openvpn/hosts.lan
listen-address=127.0.0.1,10.8.0.1
bind-interfaces
With domain set, a line reading 10.8.0.6 alice answers for both alice and alice.corp.example. Keep listen-address to the tunnel gateway and loopback, because a resolver published on the LAN interface by accident is a service you did not mean to offer.
Services that never come from the pool get their own entries. Put them in a separate drop-in file under the distribution's configuration directory so a package upgrade cannot overwrite them:
address=/files.corp.example/10.8.0.20
address=/db.corp.example/10.9.0.5
Hand the resolver over or nothing happens
A resolver nobody queries changes nothing. The server has to advertise it:
push "dhcp-option DNS 10.8.0.1"
push "dhcp-option DOMAIN corp.example"
Windows clients apply both options themselves. Most Linux clients do not: they need a supported resolver integration such as the distribution's update-resolv-conf hook or NetworkManager's DNS handling, or a manual search domain and nameserver. Check the client's actual resolver instead of assuming the push was honoured, because a push that is ignored looks exactly like a server that is not answering.
Verify at three layers
Ask the server directly first, then a client, then the application:
dig +short @10.8.0.1 alice.corp.example
getent hosts files.corp.example
If direct queries answer and the application does not, the fault is the client handoff rather than the zone. If the name resolves and the connection still fails, the name is fine and the route or firewall is the next thing to look at.
Write the two categories down next to the configuration: which names are generated, which are pinned, and which certificate each one has to match. The next person to add a server will copy whatever pattern they find first.