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