Initial checkin
This commit is contained in:
42
README.md
Normal file
42
README.md
Normal file
@ -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
|
322
dyndns-server.go
Normal file
322
dyndns-server.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
systemd/dyndns-server.service
Normal file
16
systemd/dyndns-server.service
Normal file
@ -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
|
30
update-dyndns.sh
Executable file
30
update-dyndns.sh
Executable file
@ -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}
|
||||||
|
|
10
update.txt
Normal file
10
update.txt
Normal file
@ -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
|
Reference in New Issue
Block a user