Same problem as https://aottr.dev/posts/2024/08/homelab-using-the-same-local-domain-to-access-my-services-via-tailscale-vpn/: I have setup a tailscale mesh VPN, and want to access services in my home network even without their hosts being part of the VPN (think IoT devices without resources or support for the tailscale daemon).
Subnet Routes
The tailscale client (and headscale, the self-hosted bootstrap server) support subnet routers for exactly this purpose:
I configure the tailscale client on my NAS to --advertise-routes 192.168.168.0/24,<2003:... prefix from Telekom>::/56 and then use headscale nodes approve-routes on my VPS to allow those routes to be published for all other devices in the VPN.
TODO: whenever my CPE at home (the router which terminates the internet connection using e.g. PPPoE) gets a new IPv6 prefix via Prefix Delegation, update the advertised route and approve it.
Now other devices can tailscale up --accept-routes and the daemon will inject some fancy firewall rules which route traffic to addresses in my home network over the tunnel -- first part done.
home network DNS
My homeserver runs a DynDNS client, which ensures a publicly resolvable DNS name nas.example.com always points to
- (A) the public IPv4 assigned to the CPE, which can port-forward single ports to the NAS to expose some of its services directly to the internet
- (AAAA) the globally routable IPv6 it has assigned to itself (read out locally using
ip -6 -json a s scope global -deprecated -temporary -mngtmpaddr | jq -r '.[0].addr_info[].local | strings' | head -1)
A static alias record *.nas CNAME nas (expanded: *.nas.example.com CNAME nas.example.com) lets me use any name below that zone for services on the nas, so allowing
- every service to have its own hostname, so no messing with ports, and a different cookie namespace in web browsers
- the NAS to get a wildcard certificate via ACME from Let's Encrypt valid for all its services. I made sure the NAS responds to unconfigured hostnames with an error (instead of responding e.g. with the first one in the configuration).
...conflicts with VPN
This setup is not reliable with tailscale subnet routes. When a client tries to access myservice.nas.example.com:
- If it is in some other network, without VPN, my router firewall blocks the access (except for configured port forwardings) -- as it should
- If it is in my home network, it can access myservice just fine: It has an IPv6 addresses from the same prefix and thus a local route. Apparently that is always used to connect to the NAS, because the A record would let it use the public IPv4 of the CPE, i.e. take a turn just "outside" my router and get blocked by its firewall.
- If it is in some other network and connected to the tailscale VPN, the (AAAA) record points it to an IPv6 address in my home network, which gets routed correctly through the tailnet, but the (A) record points to the public IPv4 of my CPE on which the firewall blocks access. Thus if it's possible to access the service depends on the IP stack used, which I cannot influence
solution: IPv6 only internal services
The article linked at the start solves this problem using split DNS: VPN-connected nodes get a different DNS resolver for the Zone(s) used in my home (or any network subnet-routed into the VPN). However this
- would require me to maintain that resolving DNS server and
- let the tailscale client configure that resolver on all clients whishing to communicate to my home net (MagicDNS is a standard function in tailscale, but by default of in linux clients, probably because it can mess with the local configuration) and
- could lead to issues when clients use cached DNS responses from before connecting to the VPN (e.g. Browsers do so: Inspect your Firefox' cache at about:networking#dns, disable using
network.dnsCacheEntriesandnetwork.dnsCacheExpiration)
So instead I switched my internal services (i.e. those not exposed to the internet with a port forwarding) to IPv6 only:
DynDNS on my NAS now updates not only the A and AAAA records for nas.example.com, but also an extra AAAA (but no A) record nas-services.example.com with its (globally routable) IPv6 address. The CNAME *.nas.example.com now (statically) points to nas-services.example.com, leaving all services without an A record thus only resolving to IPv6 which is (the three client cases from before) either locally reachable, globally reachable but firewall-blocked, or routed via VPN and thus reachable.