The Sisyphean Task of DNS Client Config on Linux
tailscale.com
tailscale.com
This Linux DNS stuff drives us batty at Fly.io. We run user containers as Firecracker VMs; users belong to "organizations", and organizations share a private IPv6 network. We do DNS for that private network under the fake "internal" TLD, so if you have an app "phoenix-frontend" and another app "rabbitmq-cluster", they can see each other at "phoenix-frontend.internal" and "rabbitmq-cluster.internal".
What you'd want in a perfect work is an option in `/etc/resolv.conf` that sends `.internal` to a special nameserver, and everything else to a normal nameserver. But as far as I can tell, there's no way to take a bare Linux VM that can accept an arbitrary container and set that capability up.
So instead, we end up (by default; you could override) serving _all_ customer DNS, and our `.internal` server has to forward recursive queries to things that aren't `.internal` somewhere else. This sucks; we shouldn't have to be inline for arbitrary customer DNS.
If there's a clean way to resolve this, so that the VM itself can just send `.internal` queries to us, and everything else to `1.1.1.1` or `8.8.8.8` or whatever the customer's container had, I would _love_ to hear it. I've come pretty close to breaking out preload in anger over this problem.
If you run CoreDNS instead, they have a kubernetes mode of operation where it gives different answers based on the source IP of the query to optimize search domain lookups. So if the node hosting the VMs here did similar, you could solve this pretty easily using CoreDNS with a custom plugin but I don't love putting all my traffic through CoreDNS vs a more mature DNS server.
bind can do split horizon stuff based on source IP so that could be a potential solution, though I'm not sure if it's capable of split horizon forwarders.
Here's a random post I just found: https://gist.github.com/brasey/fa2277a6d7242cdf4e4b7c720d42b...
(Ha, I did the thing where I read the comments before the post, and the post describes how to do this. So what's still missing?)
What, I think, you really care about here is glibc's behavior. But then, you can't depend on any one libc, either.
https://github.com/containernetworking/cni
https://github.com/firecracker-microvm/firecracker-go-sdk
Firecracker, by design, only supports Linux tap devices. The SDK provides facilities to:
Attach a pre-created tap device, optionally with static IP configuration, to the VM. This is referred to as a "static network interface".
Create a tap device via CNI plugins, which will then be attached to the VM automatically by the SDK. This is referred to as a "CNI-configured network interface"As far as I can tell, my way forward is to use v246, then set my TLD to my custom nameserver with my custom port and add a stub listener on localhost (instead of 127.0.0.53, which is the default). Then I can offer my VMs to use their tap gateway (which is the Ubuntu host) as a DNS server if they want, and that will resolve my custom TLD and forward all other stuff to its own nameserver. Or, if the user wants, they can also do their own stuff.
It seems that a lot has happened with systemd-resolve in the last few months.
Linux should copy that.
In any case, on macOS, you use scutil to adjust resolvers when you change networking state as when bringing up a VPN.
With systemd-resolved, you don't have to adjust resolvers manually as links come up and down, NetworkManager's VPN connections play nice with it, while DNS resolver handling in Mac VPN software (including Wireguard and Tunnelblick) is a nuasance :(.
The interface for DNS from an arbitrary container is the DNS protocol, and since you can’t control the software that is making the queries on the client side of the protocol, you have to do the special logic on the server side.
So you have two options: use a different NSS module (maybe write your own?) or have a proxy DNS resolver that sends different requests to different places.
systemd-resolved actually handles the first option pretty well (although it would prefer that you use the dbus interface over gai). It can handle multiple interfaces with separate domains and split DNS fairly well! (Not so good with reverse DNS, unfortunately. But I get it, reverse DNS is pretty hacky anyways.)
If you prefer the forwarder route, dnsmasq seems to be fairly popular these days in the embedded world and elsewhere.
If I were you, I think I'd write a short NSS module or use dnsmasq, depending upon your needs.
Also, your resolv.conf becomes trivial.
The trick is to grab the DHCP-provided DNS server address on reconnections and update the forwarding, if you use a laptop. For VMs in the cloud, it's not a problem, of course.
Unlikely to fit your usecase. But it’s not impossible for us plebs.
The downside is that Go code will drop to cgo for all resolutions (because it will see something it can't handle in pure Go in resolv.conf) and non-glibc code (like Alpine containers using musl, or non-libc resolvers like ares) won't get anywhere at all.
But you're kind of doomed in that latter case, anyway, because since there currently isn't a standard for how resolv.conf should express the rules you want, the effort to get it adopted by glibc, musl, Go, Chrome, ares, and all the other various interpreters of resolv.conf will take way too long to make a practical impact on a startup's product.
Why not? Someone has to do it, and if I were paying Fly for service, I'd rather have Fly handle my DNS queries than be another freeloader on Google or Cloudflare.
So, your solution to proxy all the traffic and split out as needed is the right way to do it without adding more software into each VM :(
Doing stuff via LD_PRELOAD would be hilarious, you should definitely do that and report back ducks behind the blast shield
1. Use ".internal.someotherdomain.io" and the regular old public DNS. (this probably only does half of what you want)
2. Use dnsmasq at the edge of each .internal subnet, and maybe require the customer to configure your DNS in their container. (I believe this is what AWS does?)
3. Intercept tcp/udp port 53 at the subnet edge, spoof your own responses for .internal, pass through anything else to wherever it's going. Dealing with tcp could be tricky, udp is easy. Probably this will break some clients, and generally be the hardest thing to implement.
I think the best option is #2, as it will actually work reliably and with the least difficulty for everyone.
I'm not persuaded by appeals to what DNS was or wasn't built for (unfortunately neither is the IETF; to wit: DNSSEC). I agree with you that the ask here is big! But I'm not convinced there isn't a cleaner solution that works within our constraints.
To start with, first implement (1).
Assuming .internal points to internal-only IPs, and you are reusing internal IPs (each customer VPC can have arbitrary private IPs), you can have ni-172-16-0-5.customer.internal.somedomain.io return A record to "172.16.0.5". (You don't have to add .customer, but you might gain some useful analytics)
A customer needing a hostname for 172.16.0.5 can resolve ni-172-16-0-5.customer.internal.somedomain.io. By default it'll use whatever DNS server/resolver they have configured (or yours with option (2)). The request goes through public DNS until it gets back to somedomain.io (your domain) and you return the result.
Upside: You're not hosting all the customer's DNS resolution. Downside: The customer will see a longer round-trip for .internal requests, and their VPC needs a NAT gateway.
Next implement (3).
Intercept all DNS traffic that matches ".internal.somedomain.io" and spoof responses. Write this tight enough that it will only return responses when you are sure they won't break clients, and pass the rest to the public DNS using option (1), which prevents from having to mess with TCP connections (they'll be slower though the public internet, but it's TCP, what did they expect?).
Upside: You are only returning results for .internal, everything else can use somebody else's DNS, and the customer doesn't need a NAT gateway. Downside: Requires custom engineering which is subtly complex, and is hidden magic the customer doesn't expect, so could cause troubleshooting headaches.
Next, implement (2) as an opt-in.
Have your VPC DHCP server return your internal dns server. You resolve .internal locally and handle the rest as a big hulking resolver. You can also slip this DNS server into their containers, but as a customer I'd want a giant red flag in the docs that advertises this.
Upside: Works in every customer scenario. Bonus: now you can return reverse records. Downside: Now you really are hosting the customer's DNS resolver.
If you're at that point, rather than preload, why not simply add the code to glibc and contribute it back? Has there been some resistance from the maintainers already?
It would seem like it should be doable to add it on in a safe way, e.g. something that would work for both: `nameserver 192.168.0.1`
and ``` nameserver 192.168.0.1 nameserver 10.10.10.1 .internal .private .etc ```
Even if there's hesitance in accepting a change to the `nameserver` options, you easily make this an option, e.g.: `options tld-nameserver:.internal:<addr>`
Perhaps it doesn't count as bare.
[EDIT: not only do many other mention this, but the article does too! my bad]
The prospect of running dnsmasq on every VM we launch is to aversive to consider. At that point, we'd just put a DNS recursor in our init.
Sounds to me that your .internal TLD is causing a lot of headache and sticking to avahi and .local would be easier?
People on this thread have a lot of ideas about what they'd do if they got to be the sysadmin for every container running on our fleet.
It -is- possible, as others have commented, to do this within the VM or container but they need a way to get that information. systemd-resolved does this. From what I understand of fly.io, the container is setup by the user, so you're not going to have control over their networking configuration or environment - is that right?
Now there is an alternative, which is instead of using .internal, to use a public facing internet hosting that is delegated to your internal DNS servers. That way they use their normal DNS, and it resolves to your nameserver. But the DNS needs to be public, and critically, if the internet DNS goes down, your .internal resolution will also stop working. Which is highly likely to cause problems for peoples applications. And also adds un-predictable latency to their local service connections which is almost certainly going to cause problems - it's not a good idea.
Having said all of that, generally speaking, it makes sense that -you- provide the DNS. It's pretty much the standard way that your 'ISP', 'Hosting Provider', etc, provides the DNS rather than relying on the public internet DNS. Only a few years ago there was no such public DNS.
The other possible alternative would be to leverage multicast DNS (libnss-mdns/avahi-daemon) or LLMNR but I'm not sure those are really ideal for this use case either. And in both cases requires resolver support that you dont get from the basic glibc resolver. So for your use case, may still not be workable.
So I think you're stuck, unless you can mandate that all of your VM images use systemd-resolved and somehow communicate the config into them.
Additionally: the basic problem of routing DNS requests based on domains is trivial. It's probably less than 100 lines of Rust to send `.internal` to our servers and anything else to a public recursor. But the goal is to get our own code out of the path of people's DNS lookups, even if it's just 100 lines of Rust.
So running dnsmasq or some systemd resolver server thingy doesn't really solve anything for us. Before we did that, we'd just make our `init` a DNS server. But it'd be neat to come up with a way to to solve the problem without having a SPOF server dependency. It might could be possible! You can already see that if you assume a specific libc, you can just preload a libc stub resolver that routes requests. But, of course, we can't assume a particular libc.
Most of my point here is just that "standard" Linux DNS is bad.
https://github.com/systemd/systemd/issues/6490 (fixed in v248)
https://github.com/systemd/systemd/issues/8451
Everything you never wanted to know.
https://zwischenzugs.com/2018/06/08/anatomy-of-a-linux-dns-l...
- ISC Bind9 is still king.
For example: there are known issues with systemd-resolved & Kubernetes (at least on Ubuntu that defaults to systemd-resolved) https://kubernetes.io/docs/tasks/administer-cluster/dns-debu...
As much as I'm not always comfortable with the larger complexity, the CoreDNS pod really needs to be doing something like the flowchart described in the article. If systemd-resolved or NetworkManager are in play, it should be using D-Bus to talk to them to get the information it needs.
resolv.conf is an old idea that is too inflexible for today's DNS needs; a more complex solution with more complex interface is unfortunately warranted here.
Dnsmasq DNS support is redundant when you are running systemd-resolved. It can do everything dnsmasq does and more.
> resolv.conf is an old idea that is too inflexible for today's DNS needs; a more complex solution with more complex interface is unfortunately warranted here.
That's exactly what systemd-resolved is.
I have to disable systemd-resolved and manually configure resolv.conf to get my containers to resolve each other properly. (Also Ubuntu)
https://gist.github.com/brasey/fa2277a6d7242cdf4e4b7c720d42b...
I have mostly managed to avoid resolv.conf issues since the default Gentoo image has a sane setup (even if using dhcp on a laptop and switching wifi and wired). However, every single time I have tried to set up a system using another distro something has gone wrong, and in support of the theory that NetworkManager is a major contributor to the problem the one Gentoo system with NetworkManager installed had the same issues.
To echo the plea in the original article, in nearly every case the primary challenge has been to figure out exactly what documentation actually applies to the system at hand because distros seem to change this completely out of sync with any attempt to correct or align the documentation.
I suspect that on an individual user level this leads to a happy path situation where everyone who does the right thing by accident is quiet and the ones who need something slightly different are never heard from because they weren't able to even connect to the internet (probably not quiet that bad).
Anything else is just prolonging the agony that the linux networking configuration had to endure for years.
It affects which sources are consulted to lookup a name (including maybe asking DNS), but it doesn’t configure things like which DNS servers to ask or what options to set.
> The Name Service Switch (NSS) configuration file, /etc/nsswitch.conf, is used by the GNU C Library and certain other applications to determine the sources from which to obtain name-service information in a range of categories, and in what order.
So yeah, if the article is contrasting resolv.conf vs systemd-resolved, nsswitch.conf is how you select which of those approaches is used.
I only addressed why nsswitch was likely not mentioned, not that it doesn’t serve a purpose.
Some apps go even further and do their DNS entirely on their own (Firefox DoH controversy).
nsswitch however doesn't just configure mechanism for public resolver, it is a hook where alternate mechanisms like nss-mdns or nss-mymachines can hook up.
Hint: it is waaay worse
If you like sports, it is like watching a basketball game, with three teams and Dave as the play by play announcer.
I'm a linux user, so I'm always cheering when OSX or Windows "loses" but then very quickly Dave will, paraphrasing say: "And, OSX hits a deep 3 pointer..." ("And, OSX does this right, and Linux sucks").
But, it is great theater and very informative. On Dave's recommendation, I switched to systemd-resolved, and it is working flawlessly for me (with my own wireguard setup, not using Tailscale yet).
In general, a modern DNS client wants: a set of "default route" resolvers; a set of "DNS routes" that point certain suffixes to other resolver configs; a set of search paths to expand single-label queries; integration with mdns and LLMNR, for seamless zero-config resolution on LANs (super important for printers, in particular); all of the above tied to interface lifetimes, so you can tie resolver reachability to underlying network state; very detailed documentation on the algorithm used to resolve a name, and how you traverse all the above configuration.
macOS has default resolvers, DNS routes, mdns integration (but no LLMNR), interface-tied configs, and knows about search paths.
But then you look at the NetworkExtension API for configuring DNS, and it turns out the search paths field doesn't actually configure the search paths in ways you'd expect, instead all the suffixes you install as "routes" end up also becoming search paths, and your only option is have all or none of them be search paths. Meanwhile, the search paths you specified do get installed... In an interface-scoped config that doesn't actually get used in the majority of name lookups that need name expansion.
It's so frustrating because it's this close to being excellent, and instead ends up being the most limiting of APIs we have to work with, because being apple, it's either their API or go screw yourself and don't configure DNS.
Oh dear, I've ranted again, haven't I. Anyway, every OS is its own beautiful little snowflake of weirdery an brokenness. Linux's particular flavor is "there's 15 ways to do it, most of which require polyfills". macOS's flavor is "we have an API that should be amazing but somehow does the wrong thing almost always". Windows's flavor is "we can do really cool things but the main source of documentation is people exchanging superstitions about registry keys on stack overflow".
Given that choice, I think I prefer linux. It's way more code to write to make it work, but at least the code can be derived from documentation+source code, and has half a chance of working as desired.
I now have to scenario for when Zscaler intercepts/rewrites DNS requests along with when the Cisco VPN is active. Given that a large amount of folks are still WFH, it’s enough to make me want to tear the remainder of my hair out.