Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ docker run -it -d \
-e SHARED_SECRET=changeme \
-e ZONE=example.org \
-e RECORD_TTL=3600 \
-e RECORD_EXPIRY=86400 \
--name=dyndns \
davd/docker-ddns:latest
```
Expand Down Expand Up @@ -68,6 +69,7 @@ It provides one single GET request, that is used as follows:

http://myhost.mydomain.tld:8080/update?secret=changeme&domain=foo&addr=1.2.3.4


### Fields

* `secret`: The shared secret set in `envfile`
Expand Down Expand Up @@ -106,15 +108,19 @@ The handlers will listen on:

An example on the ddclient (Linux DDNS client) based Ubiquiti router line:

```
set service dns dynamic interface eth0 service dyndns host-name <your-ddns-hostname-to-be-updated>
set service dns dynamic interface eth0 service dyndns login <anything-as-username-is-not-validated>
set service dns dynamic interface eth0 service dyndns password <shared-secret>
set service dns dynamic interface eth0 service dyndns protocol dyndns2
set service dns dynamic interface eth0 service dyndns server <your-ddns-server>
```

Optional if you used this behind an HTTPS reverse proxy like I do:

```
set service dns dynamic interface eth0 service dyndns options ssl=true
```

This also means that DDCLIENT works out of the box and Linux based devices should work.

Expand Down
3 changes: 2 additions & 1 deletion envfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
SHARED_SECRET=changeme
ZONE=example.org
RECORD_TTL=3600
RECORD_TTL=3600
RECORD_EXPIRY=86400
1 change: 1 addition & 0 deletions rest-api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Config struct {
Domain string
NsupdateBinary string
RecordTTL int
RecordExpiry int
}

func (conf *Config) LoadConfig(path string) {
Expand Down
2 changes: 1 addition & 1 deletion rest-api/ipparser/ipparser.go → rest-api/ipparser.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ipparser
package main

import (
"net"
Expand Down
7 changes: 3 additions & 4 deletions rest-api/ipparser_test.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
package main

import (
"dyndns/ipparser"
"testing"
)

func TestValidIP4ToReturnTrueOnValidAddress(t *testing.T) {
result := ipparser.ValidIP4("1.2.3.4")
result := ValidIP4("1.2.3.4")

if result != true {
t.Fatalf("Expected ValidIP(1.2.3.4) to be true but got false")
}
}

func TestValidIP4ToReturnFalseOnInvalidAddress(t *testing.T) {
result := ipparser.ValidIP4("abcd")
result := ValidIP4("abcd")

if result == true {
t.Fatalf("Expected ValidIP(abcd) to be false but got true")
}
}

func TestValidIP4ToReturnFalseOnEmptyAddress(t *testing.T) {
result := ipparser.ValidIP4("")
result := ValidIP4("")

if result == true {
t.Fatalf("Expected ValidIP() to be false but got true")
Expand Down
95 changes: 95 additions & 0 deletions rest-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"net/http"
"os"
"os/exec"
"time"

"github.com/boltdb/bolt"
"github.com/gorilla/mux"
)

var appConfig = &Config{}
var db *bolt.DB = nil

func main() {
appConfig.LoadConfig("/etc/dyndns.json")
Expand All @@ -27,6 +30,11 @@ func main() {
router.HandleFunc("/v2/update", DynUpdate).Methods("GET")
router.HandleFunc("/v3/update", DynUpdate).Methods("GET")

db, _ = bolt.Open("dyndns.db", 0600, nil)
defer db.Close()

go databaseMaintenance(db)

log.Println(fmt.Sprintf("Serving dyndns REST services on 0.0.0.0:8080..."))
log.Fatal(http.ListenAndServe(":8080", router))
}
Expand Down Expand Up @@ -134,5 +142,92 @@ func UpdateRecord(domain string, ipaddr string, addrType string) string {
return err.Error() + ": " + stderr.String()
}

/* Create a resource record in the database */
if err := db.Update(func(tx *bolt.Tx) error {
rr, err := tx.CreateBucketIfNotExists([]byte(domain))
if err != nil {
return fmt.Errorf("create bucket: %s", err)
}
err = rr.Put([]byte("domain"), []byte(domain))
err = rr.Put([]byte("zone"), []byte(appConfig.Domain))
err = rr.Put([]byte("ttl"), []byte(fmt.Sprintf("%v", appConfig.RecordTTL)))
err = rr.Put([]byte("type"), []byte(addrType))
err = rr.Put([]byte("address"), []byte(ipaddr))

t := time.Now()
err = rr.Put([]byte("expiry"), []byte(t.Add(time.Second * time.Duration(appConfig.RecordExpiry)).Format(time.RFC3339)))
err = rr.Put([]byte("created"), []byte(t.Format(time.RFC3339)))

return nil
}); err != nil {
log.Print(err)
}

return out.String()
}

/* GO func to clean up expired entries */
func databaseMaintenance(db *bolt.DB) {
cleanupTicker := time.NewTicker(10 * time.Second)

for {
select {
case <-cleanupTicker.C:
now := []byte(time.Now().Format(time.RFC3339))
key := []byte("expiry")

if err := db.View(func(tx *bolt.Tx) error {
/* Iterate through all buckets (each is a resource record) */
err := tx.ForEach(func(name []byte, b *bolt.Bucket) error {
c := b.Cursor()

if k, v := c.Seek(key); k != nil && bytes.Equal(k, key) {
// Check for expiry
if bytes.Compare(v, now) < 0 {
if k, v := c.Seek([]byte("type")); k != nil {
log.Printf("Expired RR(%s): '%s'. Deleting.", string(v), string(name))
go deleteRecord(db, string(name), string(v))
}
}
}

return nil
})

if err != nil {
log.Print(err)
}
return nil
}); err != nil {
log.Print(err)
}
}
}
}

/* GO func to delete an entry once expired */
func deleteRecord(db *bolt.DB, name string, addrType string) {
db.Update(func(tx *bolt.Tx) error {
return tx.DeleteBucket([]byte(name))
})

f, _ := ioutil.TempFile(os.TempDir(), "dyndns_cleanup")

defer os.Remove(f.Name())
w := bufio.NewWriter(f)

w.WriteString(fmt.Sprintf("server %s\n", appConfig.Server))
w.WriteString(fmt.Sprintf("zone %s\n", appConfig.Zone))
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", name, appConfig.Domain, addrType))
w.WriteString("send\n")

w.Flush()
f.Close()

cmd := exec.Command(appConfig.NsupdateBinary, f.Name())
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
cmd.Run()
}
10 changes: 4 additions & 6 deletions rest-api/request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"net"
"net/http"
"strings"

"dyndns/ipparser"
)

type RequestDataExtractor struct {
Expand Down Expand Up @@ -51,9 +49,9 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr
// kept in the response for compatibility reasons
response.Domain = strings.Join(response.Domains, ",")

if ipparser.ValidIP4(response.Address) {
if ValidIP4(response.Address) {
response.AddrType = "A"
} else if ipparser.ValidIP6(response.Address) {
} else if ValidIP6(response.Address) {
response.AddrType = "AAAA"
} else {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
Expand All @@ -66,10 +64,10 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr
}

// @todo refactor this code to remove duplication
if ipparser.ValidIP4(ip) {
if ValidIP4(ip) {
response.AddrType = "A"
response.Address = ip
} else if ipparser.ValidIP6(ip) {
} else if ValidIP6(ip) {
response.AddrType = "AAAA"
response.Address = ip
} else {
Expand Down
4 changes: 3 additions & 1 deletion setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[ -z "$SHARED_SECRET" ] && echo "SHARED_SECRET not set" && exit 1;
[ -z "$ZONE" ] && echo "ZONE not set" && exit 1;
[ -z "$RECORD_TTL" ] && echo "RECORD_TTL not set" && exit 1;
[ -z "$RECORD_EXPIRY" ] && echo "RECORD_EXPIRY not set" && exit 1;

if ! grep 'zone "'$ZONE'"' /etc/bind/named.conf > /dev/null
then
Expand Down Expand Up @@ -53,7 +54,8 @@ then
"Zone": "${ZONE}.",
"Domain": "${ZONE}",
"NsupdateBinary": "/usr/bin/nsupdate",
"RecordTTL": ${RECORD_TTL}
"RecordTTL": ${RECORD_TTL},
"RecordExpiry": ${RECORD_EXPIRY}
}
EOF
fi