commit 530888899a9d8b800769c66b0af4ffae43018f68 Author: Pim van Pelt Date: Fri Dec 22 16:42:32 2017 +0100 Initial checkin diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1f9f16 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Source +http://mkaczanowski.com/golang-build-dynamic-dns-service-go/ + +# Compiling + +``` +go get github.com/miekg/dns/... +go get github.com/boltdb/bolt/... +go build dyndns-server.go +ls -la dyndns-server +``` + +# Deploying + +MACH=dyn.paphosting.net +scp dyndns-server root@$MACH:/usr/local/sbin +scp systemd/dyndns-server.service root@$MACH:/etc/systemd/system/ + +# Running + +ssh root@$MACH +adduser dyndns +mkdir /var/dyndns +chown dyndns /var/dyndns +service dyndns-server start + +# Testing + +cat << EOL > update.txt +server $MACH 53 +debug yes +key key bWFyaWVsbGU= +zone dyn.ipng.nl. +update delete test.dyn.ipng.nl. A +update delete test.dyn.ipng.nl. AAAA +update add test.dyn.ipng.nl. 120 A 192.0.2.1 +update add test.dyn.ipng.nl. 120 AAAA 2001:db8::1 +show +send +EOL + +nsupdate update.txt diff --git a/dyndns-server.go b/dyndns-server.go new file mode 100644 index 0000000..7e01195 --- /dev/null +++ b/dyndns-server.go @@ -0,0 +1,322 @@ +package main + +import ( + "errors" + "flag" + "github.com/boltdb/bolt" + "github.com/miekg/dns" + "log" + "math" + "net" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" +) + +var ( + tsig *string + db_path *string + ip *string + port *int + bdb *bolt.DB + logfile *string + pid_file *string +) + +const rr_bucket = "rr" + +func getKey(domain string, rtype uint16) (r string, e error) { + if n, ok := dns.IsDomainName(domain); ok { + labels := dns.SplitDomainName(domain) + + // Reverse domain, starting from top-level domain + // eg. ".com.mkaczanowski.test" + var tmp string + for i := 0; i < int(math.Floor(float64(n/2))); i++ { + tmp = labels[i] + labels[i] = labels[n-1] + labels[n-1] = tmp + } + + reverse_domain := strings.Join(labels, ".") + r = strings.Join([]string{reverse_domain, strconv.Itoa(int(rtype))}, "_") + } else { + e = errors.New("Invailid domain: " + domain) + log.Println(e.Error()) + } + + return r, e +} + +func createBucket(bucket string) (err error) { + err = bdb.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(bucket)) + if err != nil { + e := errors.New("Create bucket: " + bucket) + log.Println(e.Error()) + + return e + } + + return nil + }) + + return err +} + +func deleteRecord(domain string, rtype uint16) (err error) { + key, _ := getKey(domain, rtype) + err = bdb.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(rr_bucket)) + err := b.Delete([]byte(key)) + + if err != nil { + e := errors.New("Delete record failed for domain: " + domain) + log.Println(e.Error()) + + return e + } + + return nil + }) + + return err +} + +func storeRecord(rr dns.RR) (err error) { + key, _ := getKey(rr.Header().Name, rr.Header().Rrtype) + err = bdb.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(rr_bucket)) + err := b.Put([]byte(key), []byte(rr.String())) + + if err != nil { + e := errors.New("Store record failed: " + rr.String()) + log.Println(e.Error()) + + return e + } + + return nil + }) + + return err +} + +func getRecord(domain string, rtype uint16) (rr dns.RR, err error) { + key, _ := getKey(domain, rtype) + var v []byte + + err = bdb.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(rr_bucket)) + v = b.Get([]byte(key)) + + if string(v) == "" { + e := errors.New("Record not found, key: " + key) + log.Println(e.Error()) + + return e + } + + return nil + }) + + if err == nil { + rr, err = dns.NewRR(string(v)) + } + + return rr, err +} + +func updateRecord(r dns.RR, q *dns.Question) { + var ( + rr dns.RR + name string + rtype uint16 + ttl uint32 + ip net.IP + ) + + header := r.Header() + name = header.Name + rtype = header.Rrtype + ttl = header.Ttl + + if _, ok := dns.IsDomainName(name); ok { + if header.Class == dns.ClassANY && header.Rdlength == 0 { // Delete record + deleteRecord(name, rtype) + } else { // Add record + rheader := dns.RR_Header{ + Name: name, + Rrtype: rtype, + Class: dns.ClassINET, + Ttl: ttl, + } + + if a, ok := r.(*dns.A); ok { + rrr, err := getRecord(name, rtype) + if err == nil { + rr = rrr.(*dns.A) + } else { + rr = new(dns.A) + } + + ip = a.A + rr.(*dns.A).Hdr = rheader + rr.(*dns.A).A = ip + } else if a, ok := r.(*dns.AAAA); ok { + rrr, err := getRecord(name, rtype) + if err == nil { + rr = rrr.(*dns.AAAA) + } else { + rr = new(dns.AAAA) + } + + ip = a.AAAA + rr.(*dns.AAAA).Hdr = rheader + rr.(*dns.AAAA).AAAA = ip + } + + storeRecord(rr) + } + } +} + +func parseQuery(m *dns.Msg) { + var rr dns.RR + + for _, q := range m.Question { + if read_rr, e := getRecord(q.Name, q.Qtype); e == nil { + rr = read_rr.(dns.RR) + if rr.Header().Name == q.Name { + m.Answer = append(m.Answer, rr) + } + } + } +} + +func handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + m.Compress = false + + switch r.Opcode { + case dns.OpcodeQuery: + parseQuery(m) + + case dns.OpcodeUpdate: + for _, question := range r.Question { + for _, rr := range r.Ns { + updateRecord(rr, &question) + } + } + } + + if r.IsTsig() != nil { + if w.TsigStatus() == nil { + m.SetTsig(r.Extra[len(r.Extra)-1].(*dns.TSIG).Hdr.Name, + dns.HmacMD5, 300, time.Now().Unix()) + } else { + log.Println("Status", w.TsigStatus().Error()) + } + } + + w.WriteMsg(m) +} + +func serve(name, secret string, ip string, port int) { + server := &dns.Server{Addr: "[" +ip + "]:" + strconv.Itoa(port), Net: "udp"} + + if name != "" { + server.TsigSecret = map[string]string{name: secret} + } + + err := server.ListenAndServe() + defer server.Shutdown() + + if err != nil { + log.Fatalf("Failed to setup the udp server: %sn", err.Error()) + } +} + +func main() { + var ( + name string // tsig keyname + secret string // tsig base64 + fh *os.File // logfile handle + ) + + // Parse flags + logfile = flag.String("logfile", "", "path to log file") + port = flag.Int("port", 53, "server port") + ip = flag.String("ip", "::", "ip to listen on") + tsig = flag.String("tsig", "", "use MD5 hmac tsig: keyname:base64") + db_path = flag.String("db_path", "/var/dyndns/dyndns.db", "location where db will be stored") + pid_file = flag.String("pid", "/var/dyndns/go-dyndns.pid", "pid file location") + + flag.Parse() + + // Open db + db, err := bolt.Open(*db_path, 0600, + &bolt.Options{Timeout: 10 * time.Second}) + + if err != nil { + log.Fatal(err) + } + defer db.Close() + bdb = db + + // Create dns bucket if doesn't exist + createBucket(rr_bucket) + + // Attach request handler func + dns.HandleFunc(".", handleDnsRequest) + + // Tsig extract + if *tsig != "" { + a := strings.SplitN(*tsig, ":", 2) + name, secret = dns.Fqdn(a[0]), a[1] + } + + // Logger setup + if *logfile != "" { + if _, err := os.Stat(*logfile); os.IsNotExist(err) { + if file, err := os.Create(*logfile); err != nil { + if err != nil { + log.Panic("Couldn't create log file: ", err) + } + + fh = file + } + } else { + fh, _ = os.OpenFile(*logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + } + defer fh.Close() + log.SetOutput(fh) + } + + // Pidfile + file, err := os.OpenFile(*pid_file, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Panic("Couldn't create pid file: ", err) + } else { + file.Write([]byte(strconv.Itoa(syscall.Getpid()))) + defer file.Close() + } + + // Start server + go serve(name, secret, *ip, *port) + + sig := make(chan os.Signal) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) +endless: + for { + select { + case s := <-sig: + log.Printf("Signal (%d) received, stopping", s) + break endless + } + } +} diff --git a/systemd/dyndns-server.service b/systemd/dyndns-server.service new file mode 100644 index 0000000..ec8f2b5 --- /dev/null +++ b/systemd/dyndns-server.service @@ -0,0 +1,16 @@ +[Unit] +Description=DynDNS server in Go +# Documentation=See git@git.ipng.nl:pim/dyndns-go.git + +[Service] +User=dyndns +Group=nogroup +ExecStart=/usr/local/sbin/dyndns-server -port=53 -ip=:: -tsig=key:bWFyaWVsbGU= +Restart=always +ProtectSystem=full +ProtectHome=true +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/update-dyndns.sh b/update-dyndns.sh new file mode 100755 index 0000000..20ced04 --- /dev/null +++ b/update-dyndns.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +SERVER="dyn.paphosting.net" +PORT="53" +TSIG="bWFyaWVsbGU=" +INTERFACE_A="eth0" +INTERFACE_AAAA="eth0" + +# Generated variables follow: +UPDATE_FILE="/tmp/go-dyndns-nsupdate.$$" +A_RECORD=$(ip addr show $INTERFACE_A | awk '/inet .*scope global/ { ip=$2; gsub("/.*","", ip); } END { print ip}') +AAAA_RECORD=$(ip addr show $INTERFACE_AAAA | awk '/inet6 .*scope global/ { ip=$2; gsub("/.*","", ip); } END { print ip}') +HOSTNAME_SHORT=$(hostname -s) + +cat << EOL > $UPDATE_FILE +server $SERVER $PORT +key key bWFyaWVsbGU= +; debug +zone dyn.ipng.nl. +; update delete ${HOSTNAME_SHORT}.dyn.ipng.nl. A +; update delete ${HOSTNAME_SHORT}.dyn.ipng.nl. AAAA +update add ${HOSTNAME_SHORT}.dyn.ipng.nl. 120 A ${A_RECORD} +update add ${HOSTNAME_SHORT}.dyn.ipng.nl. 120 AAAA ${AAAA_RECORD} +show +send +EOL + +nsupdate ${UPDATE_FILE} +rm -f ${UPDATE_FILE} + diff --git a/update.txt b/update.txt new file mode 100644 index 0000000..4a03e44 --- /dev/null +++ b/update.txt @@ -0,0 +1,10 @@ +server 194.1.163.24 53 +debug yes +key key bWFyaWVsbGU= +zone dyn.ipng.nl. +update delete test.dyn.ipng.nl. A +update delete test.dyn.ipng.nl. AAAA +update add test.dyn.ipng.nl. 120 A 192.0.2.1 +update add test.dyn.ipng.nl. 120 AAAA 2001:db8::1 +show +send