30 Dec 2017 • Least effort self hosted dynamic DNS

I wanted to set up a reverse SSH tunnel from my work PC to home because I can't figure out how to use OpenVPN. I have a dynamic IP at home so I needed to set up some kind of dynamic DNS. I was hoping someone had already done it for me because my requirements are so simple:

but they haven't so here's my take on it.

My solution is to host an authoritative DNS server on my VPS and update the zone file from cron running on my home server.

Setting up the server

First, you need a DNS provider that lets you add NS records for subdomains (Vultr's DNS lets you do this). In my case I have a record saying that lookups for domains under dyn.mikejsavage.co.uk should get forwarded to my VPS.

Next, install nsd. nsd.conf looks like this:

server:
        username: _nsd
        database: "" # disable database

remote-control:
        control-enable: yes

zone:
        name: dyn.mikejsavage.co.uk
        zonefile: dyn.mikejsavage.co.uk

The dyn.mikejsavage.co.uk zone file looks like this: (Google for "zone file syntax" if you care, it's not very exciting)

@       IN      SOA     ns0.mikejsavage.co.uk.  mike.mikejsavage.co.uk. ( 0 21600 3600 43200 300 )
$INCLUDE /zones/dyn/test

and the dyn/test include section is just one line:

test 5m IN A 1.2.3.4

To update the DNS I ssh into the VPS, overwrite that dyn/test file, and reload nsd. So the zones/dyn directory needs to be writeable by whatever user, and you need a few doas entries so that user can reload nsd.

The script to update the zone file looks like this:

#! /bin/sh
ip=$(echo "$SSH_CLIENT" | cut -d " " -f 1)
echo "test 5m IN A $ip" > /var/nsd/zones/dyn/test
doas /usr/bin/touch /var/nsd/zones/dyn.mikejsavage.co.uk
doas -u _nsd /usr/sbin/nsd-control reload dyn.mikejsavage.co.uk

and the only part that's non-obvious is the touch. nsd checks file modified times to see if it needs to really reload zones, but it doesn't look at the modified times of included files. So you need to touch the main zone file or nsd won't reload it.

Setting up the router

All the computers I have sit behind a router, so they all actually have the same public IP address, which is the address the router negotiates when it boots.

To actually send packets to my computers, the router keeps a map from ports to private IPs, so packets that come in on certain ports get forwarded on to the right machine. The router automatically adds entries when my computers send packets, so things like TCP and UDP expecting replies work just fine.

But when an outside PC tries to open a connection, the router doesn't know who to forward it to and ignores it. So to make the reverse SSH tunnel work you need to explicitly add a NAT rule (and probably a firewall rule) to your router, which forwards connections to a specific port on the router to some port on some computer behind the router.

You'll need to look at your router docs for this one. Keep in mind that your router might or might not apply NAT rules before firewall rules. My ERX does NAT first, so even though I expose port X (not 22) on the router, I need to allow port 22 in the firewall.

Setting up the client

Add a cron job:

*/30 * * * * ssh mikejsavage.co.uk "/path/to/update-dyndns"

and there you have it.

Actually the title of this post is a lie. For my use case it would have been less effort to put the IP in a text file and serve it over HTTP, but this way I got to learn some networking junk.