Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
David Prandzioch committed Dec 18, 2016
1 parent 09dc112 commit 44e7b38
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 0 deletions.
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM debian:jessie
MAINTAINER David Prandzioch <[email protected]>

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -q -y bind9 dnsutils golang git-core && \
apt-get clean

RUN chmod 770 /var/cache/bind

COPY setup.sh /root/setup.sh
RUN chmod +x /root/setup.sh

ENV GOPATH=/root/go
RUN mkdir -p /root/go/src
COPY rest-api /root/go/src/dyndns
RUN cd /root/go/src/dyndns && go get

COPY named.conf.options /etc/bind/named.conf.options

EXPOSE 53 8080
CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/go/bin/dyndns"]
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
image:
docker build -t davd/dyndns-server .

console:
docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/dyndns-server bash

server_test:
docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/dyndns-server

api_test:
curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4"
dig @localhost foo.example.org

api_test_recursion:
dig @localhost google.com

deploy: image
docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/dyndns-server
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Dynamic DNS with Docker, Go and Bind9

This package allows you to set up a server for dynamic DNS using docker with a
few simple commands. You don't have to worry about nameserver setup, REST API
and all that stuff. Setup is as easy as that:

## Installation

```
git clone https://github.com/dprandzioch/docker-ddns
cd docker-ddns
$EDITOR envfile
make deploy
```

Make sure to change all environment variables in `envfile` to match your needs.

Afterwards you have a running docker container that exposes three ports:

* 53/TCP -> DNS
* 53/UDP -> DNS
* 8080/TCP -> Management REST API


## Using the API

That package features a simple REST API written in Go, that provides a simple
interface, that almost any router that supports Custom DDNS providers can
attach to (e.g. Fritz!Box). It is highly recommended to put a reverse proxy
before the API.

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`
* `domain`: The subdomain to your configured domain, in this example it would
result in `foo.example.org`
* `addr`: IPv4 or IPv6 address of the name record

## Accessing the REST API log

Just run

```
docker logs -f dyndns
```

## DNS setup

To provide a little help... To your "real" domain, like `domain.tld`, you
should add a subdomain that is delegated to this DDNS server like this:

```
dyndns IN NS ns
ns IN A <put ipv4 of dns server here>
ns IN AAAA <optional, put ipv6 of dns server here>
```

Your management API should then also be accessible through

```
http://ns.domain.tld:8080/update?...
```

If you provide `foo` as a domain when using the REST API, the resulting domain
will then be `foo.dyndns.domain.tld`.
3 changes: 3 additions & 0 deletions envfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SHARED_SECRET=changeme
ZONE=example.org
RECORD_TTL=3600
8 changes: 8 additions & 0 deletions named.conf.options
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
options {
directory "/var/cache/bind";
dnssec-validation auto;
recursion no;
allow-transfer { none; };
auth-nxdomain no;
listen-on-v6 { any; };
};
27 changes: 27 additions & 0 deletions rest-api/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"encoding/json"
"os"
)

type Config struct {
SharedSecret string
Server string
Zone string
Domain string
NsupdateBinary string
RecordTTL int
}

func (conf *Config) LoadConfig(path string) {
file, err := os.Open(path)
if err != nil {
panic(err)
}
decoder := json.NewDecoder(file)
err = decoder.Decode(&conf)
if err != nil {
panic(err)
}
}
140 changes: 140 additions & 0 deletions rest-api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package main

import (
"log"
"fmt"
"net/http"
"io/ioutil"
"os"
"bufio"
"os/exec"
"bytes"
"encoding/json"
"net"

"github.com/gorilla/mux"
)

var appConfig = &Config{}

type WebserviceResponse struct {
Success bool
Message string
}

func main() {
appConfig.LoadConfig("/etc/dyndns.json")

router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/update", Update).Methods("GET")

log.Println(fmt.Sprintf("Serving dyndns REST services on 0.0.0.0:8080..."))
log.Fatal(http.ListenAndServe(":8080", router))
}

func validIP4(ipAddress string) bool {
testInput := net.ParseIP(ipAddress)
if testInput == nil {
return false
}

return (testInput.To4() != nil)
}

func validIP6(ip6Address string) bool {
testInputIP6 := net.ParseIP(ip6Address)
if testInputIP6 == nil {
return false
}

return (testInputIP6.To16() != nil)
}

func Update(w http.ResponseWriter, r *http.Request) {
response := WebserviceResponse{}

var sharedSecret string
var domain string
var address string

vals := r.URL.Query()
sharedSecret = vals["secret"][0]
domain = vals["domain"][0]
address = vals["addr"][0]

if sharedSecret != appConfig.SharedSecret {
log.Println(fmt.Sprintf("Invalid shared secret: %s", sharedSecret))
response.Success = false
response.Message = "Invalid Credentials"
json.NewEncoder(w).Encode(response)
return;
}

w.Header().Set("Content-Type", "application/json")

var addrType string

if validIP4(address) {
addrType = "A"
} else if validIP6(address) {
addrType = "AAAA"
} else {
response.Success = false
response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", address)
}

if addrType != "" {
if domain == "" {
response.Success = false
response.Message = fmt.Sprintf("Domain not set", address)
log.Println(fmt.Sprintf("Domain not set"))
return;
}

result := UpdateRecord(domain, address, addrType)

if result == "" {
response.Success = true
response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", addrType, domain, address)
} else {
response.Success = false
response.Message = result
}
}

json.NewEncoder(w).Encode(response)
}

func UpdateRecord(domain string, ipaddr string, addrType string) string {
log.Println(fmt.Sprintf("%s record update request: %s -> %s", addrType, domain, ipaddr))

f, err := ioutil.TempFile(os.TempDir(), "dyndns")
if err != nil {
return err.Error()
}

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 A\n", domain, appConfig.Domain))
w.WriteString(fmt.Sprintf("update delete %s.%s AAAA\n", domain, appConfig.Domain))
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", domain, appConfig.Domain, appConfig.RecordTTL, addrType, ipaddr))
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
err = cmd.Run()
if err != nil {
return err.Error() + ": " + stderr.String()
}

return out.String()
}
50 changes: 50 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

[ -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;

if [ ! -f /var/cache/bind/$ZONE.zone ]
then
echo "creating zone...";
cat >> /etc/bind/named.conf <<EOF
zone "$ZONE" {
type master;
file "$ZONE.zone";
allow-query { any; };
allow-transfer { none; };
allow-update { localhost; };
};
EOF

echo "creating zone file..."
cat > /var/cache/bind/$ZONE.zone <<EOF
\$ORIGIN .
\$TTL 86400 ; 1 day
$ZONE IN SOA localhost. root.localhost. (
74 ; serial
3600 ; refresh (1 hour)
900 ; retry (15 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
NS localhost.
\$ORIGIN ${ZONE}.
\$TTL ${RECORD_TTL}
EOF
fi

if [ ! -f /etc/dyndns.json ]
then
echo "creating REST api config..."
cat > /etc/dyndns.json <<EOF
{
"SharedSecret": "${SHARED_SECRET}",
"Server": "localhost",
"Zone": "${ZONE}.",
"Domain": "${ZONE}",
"NsupdateBinary": "/usr/bin/nsupdate",
"RecordTTL": ${RECORD_TTL}
}
EOF
fi

0 comments on commit 44e7b38

Please sign in to comment.