#!/bin/sh # Copyright (c) 2013 Taylor R. Campbell # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN # NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Example: # # You, Ronald T. Frobnitz, or ron, are a user on the multi-user host # eland.example.com, which is the authoritative nameserver for your # zone ron.example.com. Create a directory # # /home/ron/dns # # for your zone. Fill # # /home/ron/dns/glue # # with an SOA record and any parameters (e.g., $TTL) for your zone, # writing @SERIAL@ instead of a serial number. Create an empty file # # /home/ron/dns/zone # # and tell your administrator to point the nameserver configuration at # that file for the zone ron.example.com. Assume your administrator is # watching the fifo @STATEDIR@/nsd-update.fifo for notifications, # e.g. using fifowatchd with nsd-update (q.v.). # # For your machine quagga, which moves around on the internet but wants # the name quagga.ron.example.com, generate an ssh key pair on that # machine, say # # /root/keys/eland.pub, /root/keys/eland # # Add the public key, at /root/keys/eland.pub, to # # /home/ron/.ssh/authorized_keys # # on eland, with a `command="...",no-pty' directive to force it to run # ssh-dyndns-set-addr: # # command="/usr/local/bin/ssh-dyndns-set-addr quagga ron.example.com /home/ron/dns",no-pty ssh-rsa ... root@quagga.local # # When quagga gets a new IP address, have it ssh in using that key so # that it will run the command to update its address: # # ssh -i /root/keys/eland ron@eland.example.com # # The current zone data for quagga.ron.example.com will be stored in # # /home/ron/dns/host.quagga # # and completely rewritten every time you run ssh-dyndns-set-addr. # # ssh-dyndns-set-addr will concatenate the file glue with every file # named host.* in the zone directory to create the new zone file. If # you want extra records for quagga, you could store them in the glue # file or in # # /home/ron/dns/host.quagga.extra # # although then you would not be able to maintain the host # quagga.extra.ron.example.com using the same method. (XXX Kludgey!) set -Ceu # Set some parameters. : ${NSD_BINDIR:='@NSD_BINDIR@'} : ${NSD_UPDATE_FIFO:='@STATEDIR@/nsd-update.fifo'} ZONEC="${NSD_BINDIR}/zonec" # Utilities. progname="${0##*/}" note () { printf >&2 '%s: ' "$progname" printf >&2 "$@" printf >&2 '\n' } warn () { note "warning: $@" } fail () { note "error: $@" exit 1 } rm_variables= rm_on_exit () { rm_variables="${rm_variables+${rm_variables} }$@" } clean () { for var in $rm_variables; do eval "pathname=\$${var}" if [ -n "$pathname" ]; then rm -f -- "$pathname" fi done } reset_clean () { rm_variables= trap clean EXIT HUP INT TERM } reset_clean # Parse arguments. if [ $# -ne 3 ]; then fail 'usage: %s ' "$progname" fi host="$1" zone="$2" zonedir="$3" # Parse SSH_CONNECTION. if [ "x${SSH_CONNECTION+set}" != xset ]; then fail 'must be invoked via ssh' fi set -- $SSH_CONNECTION if [ $# -ne 4 ]; then fail 'invalid SSH_CONNECTION: %s' "$SSH_CONNECTION" fi client_addr="$1" client_port="$2" server_addr="$3" server_port="$4" address="$client_addr" # Open the fifo now, before we change directory. if [ ! -p "$NSD_UPDATE_FIFO" ]; then fail 'bad nsd update fifo: %s' "$NSD_UPDATE_FIFO" fi fifofd=3 exec 3>> "$NSD_UPDATE_FIFO" # Enter and sanity-check the zone directory. cd -- "$zonedir" lock_file="lock" host_file="host.${host}" zone_file="zone" if [ ! -f "$zone_file" ]; then fail 'no zone file in zone directory: %s' "$zonedir" fi # Lock the zone directory. lockfd=4 exec 4>> "$lock_file" flock --wait 3 "$lockfd" # Make a temporary host file with the replacement address. tmp_host_file= rm_on_exit tmp_host_file tmp_host_file="$(mktemp "tmp.${host_file}.XXXXXX")" printf '%s A %s\n' "$host" "$address" >> "$tmp_host_file" if cmp -s -- "$host_file" "$tmp_host_file"; then note 'nothing changed, not updating' exit 0 else # Make a temporary zone file with the new entry. tmp_zone_file= rm_on_exit tmp_zone_file tmp_zone_file="$(mktemp "zone.XXXXXX")" exec 5>> "$tmp_zone_file" printf '; DO NOT EDIT!\n' >&5 printf '; This file was automagically generated!\n\n' >&5 sed -e "s,@SERIAL@,$(date +%s),g" < glue >&5 for file in host.* "$tmp_host_file"; do # If the file doesn't exist, means host.* didn't match anything. [ -f "$file" ] || continue # Skip the file we're changing. [ "x$file" != "x$host_file" ] || continue cat < "$file" done | sort -u >&5 exec 5>&- # Make a temporary database with the new zone file. tmp_db= rm_on_exit tmp_db tmp_db="$(mktemp "db.XXXXXX")" "$ZONEC" -C -o "$zone" -z "$tmp_zone_file" -f "$tmp_db" # Zonec succeeded. Commit the host and zone files. mv -f -- "$tmp_host_file" "$host_file" mv -f -- "$tmp_zone_file" "$zone_file" # Notify the update daemon. [ "$fifofd" -eq 3 ] || fail 'fifo fd is wrong' printf 0 >&3