From 7f482b6defb921030bf24563b6c21962347931a9 Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 13 Aug 2017 11:15:39 +0200 Subject: [PATCH 1/8] Update to stretch #3 --- Dockerfile | 4 ++-- Makefile | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7569c8e..d07386f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM debian:jessie -MAINTAINER David Prandzioch +FROM debian:stretch +MAINTAINER David Prandzioch RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get install -q -y bind9 dnsutils golang git-core && \ diff --git a/Makefile b/Makefile index 9d2e79d..b91c868 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ image: - docker build -t davd/dyndns-server . + docker build -t davd/docker-ddns . console: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/dyndns-server bash + docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/docker-ddns bash server_test: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/dyndns-server + docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/docker-ddns api_test: curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" @@ -15,4 +15,4 @@ 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 \ No newline at end of file + docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/docker-ddns From 964a6941a8112072c5f37daa62e42bd771f6c3df Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 13 Aug 2017 11:25:12 +0200 Subject: [PATCH 2/8] Create multi-stage Dockerfile #4 --- Dockerfile | 22 +++++++++++++--------- Makefile | 8 ++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index d07386f..0988c1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,25 @@ +FROM debian:stretch as builder +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get install -q -y golang git-core && \ + apt-get clean + +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 + FROM debian:stretch MAINTAINER David Prandzioch RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ - apt-get install -q -y bind9 dnsutils golang git-core && \ + apt-get install -q -y bind9 dnsutils && \ 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 +COPY --from=builder /root/go/bin/dyndns /root/dyndns EXPOSE 53 8080 -CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/go/bin/dyndns"] +CMD ["sh", "-c", "/root/setup.sh ; service bind9 start ; /root/dyndns"] diff --git a/Makefile b/Makefile index b91c868..d1798ed 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ image: - docker build -t davd/docker-ddns . + docker build -t davd/docker-ddns:latest . console: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/docker-ddns bash + docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/docker-ddns:latest bash server_test: - docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/docker-ddns + docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/docker-ddns:latest api_test: curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" @@ -15,4 +15,4 @@ 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/docker-ddns + docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/docker-ddns:latest From 0069795d04e67ed67221ad1b6a95f6e5c82883ad Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 13 Aug 2017 11:26:26 +0200 Subject: [PATCH 3/8] Publish on docker hub #1 --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c1cd319..2f3ff42 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,30 @@ 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: +and all that stuff. ## Installation +You can either take the image from DockerHub or build it on your own. + +### Using DockerHub + +Just customize this to your needs and run: + +``` +docker run -it -d \ + -p 8080:8080 \ + -p 53:53 \ + -p 53:53/udp \ + -e SHARED_SECRET=changeme \ + -e ZONE=example.org \ + -e RECORD_TTL=3600 \ + --name=dyndns \ + davd/docker-ddns:latest +``` + +### Build from source / GitHub + ``` git clone https://github.com/dprandzioch/docker-ddns cd docker-ddns @@ -15,6 +35,8 @@ make deploy Make sure to change all environment variables in `envfile` to match your needs. Some more information can be found here: https://www.davd.eu/build-your-own-dynamic-dns-in-5-minutes/ +## Exposed ports + Afterwards you have a running docker container that exposes three ports: * 53/TCP -> DNS From 19a9092217b73c6f0214c2f2382a580524ff142b Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Fri, 18 Aug 2017 20:43:53 +0200 Subject: [PATCH 4/8] update readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 2f3ff42..d02ad48 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ docker run -it -d \ davd/docker-ddns:latest ``` +If you want to persist DNS configuration across container recreation, add `-v /somefolder:/var/cache/bind`. If you are experiencing any issues updating DNS configuration using the API +(`NOTAUTH` and `SERVFAIL`), make sure to add writing permissions for root (UID=0) to your persistent storage (e.g. `chmod -R a+w /somefolder`). + ### Build from source / GitHub ``` @@ -89,3 +92,7 @@ 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`. + +## Common pitfalls + +* If you're on a systemd-based distribution, the process `systemd-resolved` might occupy the DNS port 53. Therefore starting the container might fail. To fix this disable the DNSStubListener by adding `DNSStubListener=no` to `/etc/systemd/resolved.conf` and restart the service using `sudo systemctl restart systemd-resolved.service` but be aware of the implications... Read more here: https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html From a16d1635720aeb2dce4133558a24466cc6fac4df Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 12 Nov 2017 17:50:43 +0100 Subject: [PATCH 5/8] Create unit tests, fix index out of bounds error #9 --- Makefile | 16 ++++-- rest-api/ipparser/ipparser.go | 24 +++++++++ rest-api/ipparser_test.go | 30 +++++++++++ rest-api/main.go | 69 ++---------------------- rest-api/request_handler.go | 57 ++++++++++++++++++++ rest-api/request_handler_test.go | 90 ++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 68 deletions(-) create mode 100644 rest-api/ipparser/ipparser.go create mode 100644 rest-api/ipparser_test.go create mode 100644 rest-api/request_handler.go create mode 100644 rest-api/request_handler_test.go diff --git a/Makefile b/Makefile index d1798ed..d3af69e 100644 --- a/Makefile +++ b/Makefile @@ -4,15 +4,25 @@ image: console: docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --rm davd/docker-ddns:latest bash +devconsole: + docker run -it --rm -v ${PWD}/rest-api:/usr/src/app -w /usr/src/app golang:1.8.5 bash + server_test: docker run -it -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --rm davd/docker-ddns:latest +unit_tests: + docker run -it --rm -v ${PWD}/rest-api:/go/src/dyndns -w /go/src/dyndns golang:1.8.5 /bin/bash -c "go get && go test -v" + api_test: - curl "http://localhost:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" - dig @localhost foo.example.org + curl "http://docker.local:8080/update?secret=changeme&domain=foo&addr=1.2.3.4" + dig @docker.local foo.example.org + +api_test_invalid_params: + curl "http://docker.local:8080/update?secret=changeme&addr=1.2.3.4" + dig @docker.local foo.example.org api_test_recursion: - dig @localhost google.com + dig @docker.local google.com deploy: image docker run -it -d -p 8080:8080 -p 53:53 -p 53:53/udp --env-file envfile --name=dyndns davd/docker-ddns:latest diff --git a/rest-api/ipparser/ipparser.go b/rest-api/ipparser/ipparser.go new file mode 100644 index 0000000..44545d2 --- /dev/null +++ b/rest-api/ipparser/ipparser.go @@ -0,0 +1,24 @@ +package ipparser + +import ( + "net" +) + +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) +} + diff --git a/rest-api/ipparser_test.go b/rest-api/ipparser_test.go new file mode 100644 index 0000000..7fbfdf1 --- /dev/null +++ b/rest-api/ipparser_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + "dyndns/ipparser" +) + +func TestValidIP4ToReturnTrueOnValidAddress(t *testing.T) { + result := ipparser.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") + + if result == true { + t.Fatalf("Expected ValidIP(abcd) to be false but got true") + } +} + +func TestValidIP4ToReturnFalseOnEmptyAddress(t *testing.T) { + result := ipparser.ValidIP4("") + + if result == true { + t.Fatalf("Expected ValidIP() to be false but got true") + } +} diff --git a/rest-api/main.go b/rest-api/main.go index 44f483d..a09b3f4 100644 --- a/rest-api/main.go +++ b/rest-api/main.go @@ -10,18 +10,12 @@ import ( "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") @@ -32,70 +26,15 @@ func main() { 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; - } + response := BuildWebserviceResponseFromRequest(r, appConfig) - result := UpdateRecord(domain, address, addrType) + if response.Success { + result := UpdateRecord(response.Domain, response.Address, response.AddrType) if result == "" { response.Success = true - response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", addrType, domain, address) + response.Message = fmt.Sprintf("Updated %s record for %s to IP address %s", response.AddrType, response.Domain, response.Address) } else { response.Success = false response.Message = result diff --git a/rest-api/request_handler.go b/rest-api/request_handler.go new file mode 100644 index 0000000..7181614 --- /dev/null +++ b/rest-api/request_handler.go @@ -0,0 +1,57 @@ +package main + +import ( + "log" + "fmt" + "net/http" + + "dyndns/ipparser" +) + +type WebserviceResponse struct { + Success bool + Message string + Domain string + Address string + AddrType string +} + +func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config) WebserviceResponse { + response := WebserviceResponse{} + + var sharedSecret string + + vals := r.URL.Query() + sharedSecret = vals.Get("secret") + response.Domain = vals.Get("domain") + response.Address = vals.Get("addr") + + if sharedSecret != appConfig.SharedSecret { + log.Println(fmt.Sprintf("Invalid shared secret: %s", sharedSecret)) + response.Success = false + response.Message = "Invalid Credentials" + return response + } + + if response.Domain == "" { + response.Success = false + response.Message = fmt.Sprintf("Domain not set") + log.Println("Domain not set") + return response + } + + if ipparser.ValidIP4(response.Address) { + response.AddrType = "A" + } else if ipparser.ValidIP6(response.Address) { + response.AddrType = "AAAA" + } else { + response.Success = false + response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", response.Address) + log.Println(fmt.Sprintf("Invalid address: %s", response.Address)) + return response + } + + response.Success = true + + return response +} diff --git a/rest-api/request_handler_test.go b/rest-api/request_handler_test.go new file mode 100644 index 0000000..6e3b797 --- /dev/null +++ b/rest-api/request_handler_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "testing" + "net/http" +) + +func TestBuildWebserviceResponseFromRequestToReturnValidObject(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo&addr=1.2.3.4", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != true { + t.Fatalf("Expected WebserviceResponse.Success to be true") + } + + if result.Domain != "foo" { + t.Fatalf("Expected WebserviceResponse.Domain to be foo") + } + + if result.Address != "1.2.3.4" { + t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4") + } + + if result.AddrType != "A" { + t.Fatalf("Expected WebserviceResponse.AddrType to be A") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoSecretIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenInvalidSecretIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=foo", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoDomainIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoAddressIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} + +func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenInvalidAddressIsGiven(t *testing.T) { + var appConfig = &Config{} + appConfig.SharedSecret = "changeme" + + req, _ := http.NewRequest("POST", "/update?secret=changeme&domain=foo&addr=1.41:2", nil) + result := BuildWebserviceResponseFromRequest(req, appConfig) + + if result.Success != false { + t.Fatalf("Expected WebserviceResponse.Success to be false") + } +} From 2590cc96fc626689dcaadb690b4eeb65ed4bc58b Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 12 Nov 2017 17:53:30 +0100 Subject: [PATCH 6/8] Only build image if tests pass --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0988c1a..9b9ea54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ 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 +RUN cd /root/go/src/dyndns && go get && go test -v FROM debian:stretch MAINTAINER David Prandzioch From 906732d69f83d57388a56120f7953d6b873f817a Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 12 Nov 2017 17:58:45 +0100 Subject: [PATCH 7/8] Add changelog file --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b4b0b8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +[1.1.0] +* Update Debian Jessie to Debian Stretch +* Multistage Dockerfile resulting in smaller production image +* Code refactoring +* Basic unit test coverage +* Documentation on running from DockerHub + +[1.0.0] +* Initial release From df7caca1cdc4340cf5f0a63e51c6d9c6729013e4 Mon Sep 17 00:00:00 2001 From: David Prandzioch Date: Sun, 12 Nov 2017 18:04:08 +0100 Subject: [PATCH 8/8] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b0b8e..d1da3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Update Debian Jessie to Debian Stretch * Multistage Dockerfile resulting in smaller production image * Code refactoring +* Extended response * Basic unit test coverage * Documentation on running from DockerHub