OpenVPN and dynamic DNS

My laptop’s hostname is dent. I want my DNS records to point to that laptop whether I’m connected to my LAN directly (via WIFI or ethernet cable) or via OpenVPN (my VPN of choice).

SIDE NOTE: You will see references to nsupdate -k below. Note that in recent versions of this program, the option you want has changed to nsupdate -y. You will find an example of the new format later in this post.

My first thoughts on these were a couple of year ago, and I foolishly tried changing my VPN from routing to bridging. I failed. Terribly. I was out of town and had to get a friend to drop by and reboot my gateway for me (thanks Jerry).

Why is having the DNS correct important to me? So my backups work. That’s just for starters. My laptop actually has two hostnames; dent and dent-vpn (assigned via the VPN). These IP addresses are on different subnets. At present, if I’m away and using the VPN, I go into the Bacula configuration files and change the FQDN in to reflect dent-vpn. This works, but there is a better solution. Dynamic DNS.

NOTE: backups in my world are controlled centrally. The backup server initiates backups and thus must know the IP address of each client.

Dynamic DNS allows you to update DNS records on the fly by sending commands to your name server.

Big WARNING

What I am doing here is updating my own name servers for my own domain. If you have a dynamic IP address at home, and you’re trying to use DYNDNS on that, this article is not what you want. You want a third party service, such as NOIP.com.

Reading

I found these to be useful:

The tools

I am using OpenVPN and bind to achieve my goals. If you aren’t using those tools, perhaps this article will still be of some use.

DNS server configuration

To configure my DNS server so that I could use dynamic DNS, I used instructions suggested by a post in the FreeBSD Forums. I will outline the steps here.

Create your secret key

This shared secret key will be used by both bind and OpenVPN.

On my OpenVPN server, I created a new directory, and created the secret key:

mkdir /usr/local/openvpn
chown openvpn:openvpn /usr/local/openvpn
chmod 750 /usr/local/openvpn
ch /usr/local/openvpn
dnssec-keygen -a hmac-md5 -b 128 -n USER Kopenvpnupdate
chmod 700 Kopenvpnupdate*

This will result in two files, a .key file, and a .private file. Here are some test files I created, but never used:

# ls
Kkopenvpnupdate.+157+50354.key     Kkopenvpnupdate.+157+50354.private
# cat Kkopenvpnupdate.+157+50354.key
Kopenvpnupdate. IN KEY 0 3 157 gBOAX68PXamnGH4RYaItHQ==
# cat Kkopenvpnupdate.+157+50354.private
Private-key-format: v1.3
Algorithm: 157 (HMAC_MD5)
Key: gBOAX68PXamnGH4RYaItHQ==
Bits: AAA=
Created: 20130820220748
Publish: 20130820220748
Activate: 20130820220748

Do not use these files. For security, create your own.

I will refer to these files later.

named.conf

The following steps were performed on my DNS server.

NOTE: Be aware: this will rewrite files on disk. That is, your existing zone files will be rewritten by named. If your zone files are under revision control, you should be aware of this. For example, my zone files looked like this after my testing below:

+$TTL 3600      ; 1 hour
+60                     PTR     dent.example.org.

I added these entries to named.conf (in my case, /var/named/etc/namedb/named.conf):

key openvpnupdate {
  algorithm hmac-md5;
  secret "gBOAX68PXamnGH4RYaItHQ==";
};

That secret is from the previous step. Copy and paste it into your named.conf file.

This key will be used to update two zones. The forward DNS and the reverse DNS.

This is the forward DNS:

zone "example.org" {
        type master;
        file "zones/example.org.db";
        allow-update { key openvpnupdate; };
};

The particular may vary, but the important part is the allow-update phrase, as seen above, and as shown below in the reverse DNS:

zone "0.0.10.in-addr.arpa" {
        type master;
        file "zones/example.org.rev.db";
        allow-update { key openvpnupdate; };
};

After making those changes, you want to restart named:

# service named restart

OpenVPN configuration

Back to the OpenVPN server now. In the /usr/local/openvpn directory, I added this file.

WARNING: You may not want to invoke this script for all hosts which connect via OpenVPN. I want to do this only for my laptop.

#!/bin/sh
#
#
# original script found at http://openvpn.net/archive/openvpn-users/2005-08/msg00146.html
# contribued by Charles Duffy <cduffy@xxxxxxxxxxx> Thu, 11 Aug 2005 19:07:45 -0500
#

DNSSERVER="10.0.0.73"         ## your DNS server
FWDZONE="example.org"         ## forward resolution zone (ie. vpn.company.com)
REVZONE="0.0.10.in-addr.arpa" ## reverse resolution zone (ie. "1.0.0.in-addr.arpa")
NSUOPTS="-k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.privatey" ## extra arguments for nsupdate (ie. "-k /path/to/key")

if [ -n "$DEBUG" ] ; then
        NSUOPTS="$NSUOPTS -d"
        set -x
fi

reverseRecord() {
        echo $1 | sed -re 's/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/\4.\3.\2.\1.in-addr.arpa./'
}

addRecord() {
        local ADDRESS="$1"
        local CN="$2"
        local TEMPFILE=$(mktemp /tmp/nsupdate.XXXXXX)
        local REVERSE=$(reverseRecord $ADDRESS)

        cat >$TEMPFILE <<EOF
server $DNSSERVER
zone $FWDZONE
update delete ${CN}. A
update add ${CN}. 3600 A $ADDRESS
send
EOF
        if [ -n "$DEBUG" ] ; then cat $TEMPFILE; fi
        nsupdate $NSUOPTS $TEMPFILE
        cat >$TEMPFILE <<EOF
server $DNSSERVER
zone $REVZONE
update delete $REVERSE PTR
update add $REVERSE 3600 PTR $CN.
send
EOF
        if [ -n "$DEBUG" ] ; then cat $TEMPFILE; fi
        nsupdate $NSUOPTS $TEMPFILE
        rm -f $TEMPFILE
}

removeRecord() {
        local ADDRESS="$1"
        local CN="$2"
        local TEMPFILE=$(mktemp /tmp/nsupdate.XXXXXX)
        local REVERSE=$(reverseRecord $ADDRESS)

        cat >$TEMPFILE <<EOF
server $DNSSERVER
zone $FWDZONE
update delete ${CN}. A
send
EOF
        if [ -n "$DEBUG" ] ; then cat $TEMPFILE; fi
        nsupdate $NSUOPTS $TEMPFILE
        cat >$TEMPFILE <<EOF
server $DNSSERVER
zone $REVZONE
update delete $REVERSE PTR
send
EOF
        if [ -n "$DEBUG" ] ; then cat $TEMPFILE; fi
        nsupdate $NSUOPTS $TEMPFILE
        rm -f $TEMPFILE
}

getCN() {
        local IPADDR=$1
        local FULLNAME=$(dig +noadditional +noqr +noquestion +nocmd +noauthority +nostats +nocomments -x ${IPADDR} | gawk '{print $5}')
        if [ -n "$FULLNAME" ] ; then
                echo $FULLNAME | sed -re 's/\.$//'
                return 0
        else
                return 1
        fi
}

case "$script_type" in
        learn-address)
                OPERATION=$1
                ADDRESS=$2
                CN=$3

                if [ "$CN" != "dent.example.org" ] ; then
                  if [ -n "$DEBUG" ] ; then echo "not a host I work on"; fi
                  exit;
                fi

                REVERSE=$(reverseRecord $ADDRESS)

                case "$OPERATION" in
                        add|update)
                                addRecord "$ADDRESS" "$CN"
                                ;;
                        delete)
                                CN=$(getCN "$ADDRESS")
                                removeRecord "$ADDRESS" "$CN"
                                ;;
                        *)
                                echo "ERROR: don't know operation \"$OPERATION\"."
                                exit 1
                esac
                ;;
        *)
                echo "\"${script_type}\" not handled"
esac

return 0

Here is what we have in this directory now. Make sure your chmod and chown are similar to this:

# ls -l
total 14
-rw-------  1 openvpn  openvpn    56 Aug 20 23:07 Kkopenvpnupdate.+157+50354.key
-rw-------  1 openvpn  openvpn   165 Aug 20 23:07 Kkopenvpnupdate.+157+50354.private
-rwxr-----  1 openvpn  openvpn  2474 Aug 20 22:21 learn-address

You should test this program from the command line before proceeding.

Testing nsupdate

I restarted named in a previous section. Now that we have everything else configured, it is time to start testing. Let’s see if named will accept updates via nsupdate, which is what the learn-address script will invoke.

Here is a very simple test which checks the current IP address, changes it, then checks it again. Keep in mind the following points:

  1. The host I am on has 10.0.0.76 listed as the first nameserver in /etc/resolv.conf
  2. The DNS server at 10.0.0.76 is authoriative for the domain example.org (the one I am changing)
# host dent.example.org
dent.example.org has address 10.0.0.121

# nsupdate -k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.key
> server 10.0.0.73
> zone example.org
> update delete dent.example.org
> update add dent.example.org 3600 A 10.0.0.122
> send
> quit

# host dent.example.org
dent.example.org has address 10.0.0.122

This demonstrates that updates via nsupdate can be done.

Testing learn-address

The learn-address script, which I place in /usr/local/openvpn, will be invokved by OpenVPN whenever a clent connects. The primary purpose of this hook seems to be validation. If the script returns a non-zero result, the connection will be aborted. That is what the documentation seems to indicate. But it also seems to be a great place from which to update our DNS records.

In this section, we’ll make sure that learn-address works before we get OpenVPN to run it.

The script uses environment variables which are set by OpenVPN. For our testing, we need to test those variables. For testing, you can also set DEBUG to non-blank value.

The following test was performed with debugging enabled. There is another test immediately below, without debugging.

# export DEBUG="d"
# export script_type="learn-address"
# ./learn-address update 10.0.0.60 dent.example.org
+ OPERATION=update
+ ADDRESS=10.0.0.60
+ CN=dent.example.org
+ [ dent.example.org != dent.example.org ]
+ reverseRecord 10.0.0.60
+ echo 10.0.0.60
+ sed -re 's/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/\4.\3.\2.\1.in-addr.arpa./'
+ REVERSE=60.0.0.10.in-addr.arpa.
+ addRecord 10.0.0.60 dent.example.org
+ local ADDRESS=10.0.0.60
+ local CN=dent.example.org
+ mktemp /tmp/nsupdate.XXXXXX
+ local TEMPFILE=/tmp/nsupdate.teuxaT
+ reverseRecord 10.0.0.60
+ echo 10.0.0.60
+ sed -re 's/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/\4.\3.\2.\1.in-addr.arpa./'
+ local REVERSE=60.0.0.10.in-addr.arpa.
+ cat
+ [ -n d ]
+ cat /tmp/nsupdate.teuxaT
server 10.0.0.73
zone example.org
update delete dent.example.org. A
update add dent.example.org. 3600 A 10.0.0.60
send
+ nsupdate -k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.key -d /tmp/nsupdate.teuxaT
Creating key...
Sending update to 10.0.0.73#53
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  27439
;; flags:; ZONE: 1, PREREQ: 0, UPDATE: 2, ADDITIONAL: 1
;; ZONE SECTION:
;example.org.                        IN      SOA

;; UPDATE SECTION:
dent.example.org.    0       ANY     A
dent.example.org.    3600    IN      A       10.0.0.60

;; TSIG PSEUDOSECTION:
openvpnupdate.          0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 1377094801 300 16 J/+p0YsZ5I7rm8KyEbKf2g== 27439 NOERROR 0


Reply from update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  27439
;; flags: qr ra; ZONE: 1, PREREQ: 0, UPDATE: 0, ADDITIONAL: 1
;; ZONE SECTION:
;example.org.                        IN      SOA

;; TSIG PSEUDOSECTION:
openvpnupdate.          0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 1377094801 300 16 hNlGuErqtojk3acM0KMmlw== 27439 NOERROR 0

+ cat
+ [ -n d ]
+ cat /tmp/nsupdate.teuxaT
server 10.0.0.73
zone 0.0.10.in-addr.arpa
update delete 60.0.0.10.in-addr.arpa. PTR
update add 60.0.0.10.in-addr.arpa. 3600 PTR dent.example.org.
send
+ nsupdate -k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.key -d /tmp/nsupdate.teuxaT
Creating key...
Sending update to 10.0.0.73#53
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  16818
;; flags:; ZONE: 1, PREREQ: 0, UPDATE: 2, ADDITIONAL: 1
;; ZONE SECTION:
;0.0.10.in-addr.arpa.          IN      SOA

;; UPDATE SECTION:
60.0.0.10.in-addr.arpa. 0      ANY     PTR
60.0.0.10.in-addr.arpa. 3600   IN      PTR     dent.example.org.

;; TSIG PSEUDOSECTION:
openvpnupdate.          0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 1377094801 300 16 SZG8KtVdVIuTz5+2rIvmyQ== 16818 NOERROR 0


Reply from update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:  16818
;; flags: qr ra; ZONE: 1, PREREQ: 0, UPDATE: 0, ADDITIONAL: 1
;; ZONE SECTION:
;0.0.10.in-addr.arpa.          IN      SOA

;; TSIG PSEUDOSECTION:
openvpnupdate.          0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 1377094801 300 16 O1a7hUFcvBLvin9hH5sXfw== 16818 NOERROR 0

+ rm -f /tmp/nsupdate.teuxaT
+ return 0
[code]

That's a lot to take in, so let's do that again, without debugging.

[code]
# export DEBUG=""
# host dent.example.org
dent.example.org has address 10.0.0.60
# ./learn-address update 10.0.0.61 dent.example.org
# host dent.example.org
dent.example.org has address 10.0.0.61
#

There, it just works. Hopefully, your experience will be as easy.

Setting the hook

These steps are performed on the OpenVPN server.

I added the following two lines to openvpn.conf on my server:

script-security 3 system
learn-address /usr/local/openvpn/learn-address

If you do not add the first line, you’ll see an error such as this:

openvpn[67915]: dent.example.org/x.x.x.x:50846 WARNING: Failed running command (--learn-address): external program fork failed

You will be reminded of the above by messages such as this:

NOTE: OpenVPN 2.1 requires '--script-security 2' or higher to call user-defined scripts or executables

After making those changes, you will need to restart OpenVPN.

Connect your client

Now you should be able to connect your OpenVPN client and have your DNS updated automatically. Please, if this does not work, let me know in the comments and we’ll get it working for you.

but no DHCID, not mine

When I first tried this, I started getting this error right away:

dhcpd: Forward map from laptop.example.org to 10.55.0.60 FAILED: Has an address record but no DHCID, not mine.

I wrote about this in the FreeBSD Forums. This problem is caused by my original A record still being in the zone file. I can delete that zone file with this command (which is similar to that used in my testing above)

# nsupdate -k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.key
> server 10.0.0.73
> zone example.org
> update delete dent.example.org
> send
> quit

In more recent versions of nsupdate, the command was:

# nsupdate -y openvpnupdate:gBOAX68PXamnGH4RYaItHQ==

After that change, the no DHCID messages stopped.

ADDENDA – 16 Nov 2014

In order to get this working with pfSense 2.1.5-RELEASE, I had to make a few changes.

That version of pfSense uses an older version of nsupdate. As I found in this post, I had to create my keys with -C (compatability mode) to generate keys with dnssec-keygen for use with the old nsupdate.

I was getting this error before that:

could not read key from Kkey_name.+157+18051.private: private key is invalid

Next, thanks to this post, I found out I had to alter the script to invoke nsupdate with different options:

NSUOPTS=" -y openvpnupdate:gBOAX68PXamnGH4RYaItHQ=="

instead of the old:

NSUOPTS="-k /usr/local/openvpn/Kkopenvpnupdate.+157+50354.privatey"

I was getting this error:

; TSIG error with server: tsig indicates error
update failed: NOTAUTH(BADSIG)
Website Pin Facebook Twitter Myspace Friendfeed Technorati del.icio.us Digg Google StumbleUpon Premium Responsive

Leave a Comment

Scroll to Top