Quick tips: Server Name Indication for internal domains with Turris Omnia
- CommentsOmnia?
The Turris Omnia is quite a nice (although a little pricey) OpenWRT-based router from CZ.NIC. It provides a fairly powerful CPU, relatively unconstrained eMMC space, and quite a lot of hackability (some revisions even have GPIO ports to play with). It runs a modified version of OpenWRT, named TurrisOS.
The problem
A few years ago, I built a custom NAS for my storage needs, using a cheap Intel SoC (J1900 chipset) and a (much pricier) mini-ITX small form factor server tower. While I want it to be primarily a NAS, I wanted to run a few services on it, for example Nextcloud or Pymedusa. Of course, in these days it’s better to use HTTPS, and the process is both cheap and fast using Let’s Encrypt. I also wanted to use SNI with multiple subdomains (files.example.com
, medusa.example.com
, etc.) pointing to the same internal LAN IP (because some of the software I used is much harder to set up with subpaths of a single domain).
Actually setting up the certificates (with the dns-01 challenge) was rather easy to do (I might do a separate blog post on the topic), but then I got hit by a problem: the default domain used by DHCP in Omnia is lan
, which of course is not the same as the one used with the Let’s Encrypt certificates. Also, subdomains meant that I had to insert static host mapping for each one of them, which made testing of new software (on a new subdomain) and maintenance tedious.
Hence, the question:
How could I have an arbitrary number of subdomains (foo.bar
, baz.foo.bar
) all resolve to the same IP without manual mapping?
The solution
Unlike upstream OpenWRT, TurrisOS does not use dnsmasq for name resolution (it is only used as DHCP server), opting instead for Knot Resolver, also developed by CZ.NIC. kresd (the Knot Resolver daemon) is sufficiently nimble and can be scripted and configured with a bit of Lua. Unlike others that absolutely require features that are dnsmasq specific, I had absolutely no problems with it.
Thanks to the extensive documentation and some helpful developers I was able to configure it to do exactly like I wanted.
One first needs to check, on the Omnia, the configuration file which is located at /etc/config/resolver
, and navigate to the options related to kresd:
config resolver 'kresd'
option rundir '/tmp/kresd'
option log_stderr '0'
option log_stdout '0'
option forks '1'
option include_config '/etc/kresd/custom.conf'
option keep_cache '0'
list hostname_config '/etc/hosts'
This is my configuration, which might be different from yours. What’s important is option include_config
, which you can point to a custom file where you can set additional kresd configuration.
What I did was to create this file and add:
local genRR = policy.ANSWER({
[kres.type.A] = { rdata=kres.str2ip('192.168.30.55'), ttl=900 },
}, true)
policy.add(policy.suffix(genRR, { todname('internal.example.com.') }))
What does it do? It sets a query policy, which tells kresd what to do exactly with some requests.
Notice that these statements will only work with kresd >= 5.1, but even the legacy TurrisOS 3.x has the latest version, if you are up-to-date with updates.
In particular, when the request is kres.type.A
, so an A record, it gives back 192.168.30.55
with a time-to-live of 900 seconds. This means that every request that follows this policy will answer the same IP address. The secont line adds a new policy rule to the resolver, which means that every subdomain of internal.example.com
will resolve to 192.168.30.55
.
Since kresd assumes data in the DNS wire format, as defined in RFC 1035, we use a couple of convenience functions (kres.str2ip
and todname
) so we can just type our IP addresses or domain names without any trouble. We can also, potentially, specify multiple subdomains to check with the policy.todnames
function:
policy.add(policy.suffix(genRR, policy.todnames({'internal.example.com', 'external.example.com'})))
Once that is a done deal, you can just restart kresd on the Omnia (/etc/init.d/resolver restart
) to have kresd pick up your configuration. If it doesn’t start… well, you have a problem. In this case you can set log_stderr
and log_stdout
to 1 in the configuration and then check /var/log/messages
for potential configuration errors.
Conclusion
Like this, I was able to set Let’s Encrypt for all the subdomains I needed and I could also use HTTPS internally, without resorting to ideas like adding my own CA (which would have complicated things quite a bit. It took a while to figure out, including troubles due to API changes, but now it performs exactly as I want to.