From 6af394aa2f125cdb3f7b0db006f297b1b805d603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 29 Jan 2017 19:02:14 +0100 Subject: [PATCH 01/23] refactor ddns into several packages and add tests for most important components WIP restructure code into separate packages + add tests for request handling more documentation for methods remove useless comment --- backend.go | 99 ------------------ backend/backend.go | 170 +++++++++++++++++++++++++++++++ backend/backend_test.go | 177 +++++++++++++++++++++++++++++++++ config/config.go | 10 ++ ddns.go | 68 ++++++------- hosts/hosts.go | 36 +++++++ hosts/redis.go | 76 ++++++++++++++ redis.go | 100 ------------------- template.go => web/template.go | 2 +- web.go => web/web.go | 72 ++++++++++---- 10 files changed, 553 insertions(+), 257 deletions(-) delete mode 100644 backend.go create mode 100644 backend/backend.go create mode 100644 backend/backend_test.go create mode 100644 config/config.go create mode 100644 hosts/hosts.go create mode 100644 hosts/redis.go delete mode 100644 redis.go rename template.go => web/template.go (99%) rename web.go => web/web.go (57%) diff --git a/backend.go b/backend.go deleted file mode 100644 index b5b0620..0000000 --- a/backend.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "strings" - "time" -) - -type responder func() - -func respondWithFAIL() { - fmt.Printf("FAIL\n") -} - -func respondWithEND() { - fmt.Printf("END\n") -} - -// This function implements the PowerDNS-Pipe-Backend protocol and generates -// the response data it possible -func RunBackend(conn *RedisConnection) { - bio := bufio.NewReader(os.Stdin) - - // handshake with PowerDNS - _, _, _ = bio.ReadLine() - fmt.Printf("OK\tDDNS Go Backend\n") - - for { - line, _, err := bio.ReadLine() - if err != nil { - respondWithFAIL() - continue - } - - HandleRequest(string(line), conn)() - } -} - -func HandleRequest(line string, conn *RedisConnection) responder { - if Verbose { - fmt.Printf("LOG\t'%s'\n", line) - } - - parts := strings.Split(line, "\t") - if len(parts) != 6 { - return respondWithFAIL - } - - query_name := parts[1] - query_class := parts[2] - query_type := parts[3] - query_id := parts[4] - - var response, record string - record = query_type - - switch query_type { - case "SOA": - response = fmt.Sprintf("%s. hostmaster.example.com. %d 1800 3600 7200 5", - DdnsSoaFqdn, getSoaSerial()) - - case "NS": - response = fmt.Sprintf("%s.", DdnsSoaFqdn) - - case "A": - case "ANY": - // get the host part of the fqdn: pi.d.example.org -> pi - hostname := "" - if strings.HasSuffix(query_name, DdnsDomain) { - hostname = query_name[:len(query_name)-len(DdnsDomain)] - } - - if hostname == "" || !conn.HostExist(hostname) { - return respondWithFAIL - } - - host := conn.GetHost(hostname) - response = host.Ip - - record = "A" - if !host.IsIPv4() { - record = "AAAA" - } - - default: - return respondWithFAIL - } - - fmt.Printf("DATA\t%s\t%s\t%s\t10\t%s\t%s\n", - query_name, query_class, record, query_id, response) - return respondWithEND -} - -func getSoaSerial() int64 { - // return current time in milliseconds - return time.Now().UnixNano() -} diff --git a/backend/backend.go b/backend/backend.go new file mode 100644 index 0000000..9e1b340 --- /dev/null +++ b/backend/backend.go @@ -0,0 +1,170 @@ +package backend + +import ( + "../config" + "../hosts" + "bufio" + "errors" + "fmt" + "io" + "strings" + "time" +) + +// PowerDnsBackend implements the PowerDNS-Pipe-Backend protocol ABI-Version 1 +type PowerDnsBackend struct { + config *config.Config + hosts hosts.HostBackend + in io.Reader + out io.Writer +} + +// NewPowerDnsBackend creates an instance of a PowerDNS-Pipe-Backend using the supplied parameters +func NewPowerDnsBackend(config *config.Config, backend hosts.HostBackend, in io.Reader, out io.Writer) *PowerDnsBackend { + return &PowerDnsBackend{ + config: config, + hosts: backend, + in: in, + out: out, + } +} + +// Run reads requests from an input (normally STDIN) and prints response messages to an output (normally STDOUT) +func (b *PowerDnsBackend) Run() { + responses := make(chan backendResponse, 5) + + go func() { + for response := range responses { + fmt.Fprintln(b.out, strings.Join(response, "\t")) + } + }() + + // handshake with PowerDNS + bio := bufio.NewReader(b.in) + _, _, _ = bio.ReadLine() + responses <- handshakeResponse + + for { + request, err := b.parseRequest(bio) + if err != nil { + responses <- failResponse + continue + } + + if err = b.handleRequest(request, responses); err != nil { + responses <- newResponse("LOG", err.Error()) + } + } +} + +// handleRequest handles the supplied request by sending response messages on the supplied responses channel +func (b *PowerDnsBackend) handleRequest(request *backendRequest, responses chan backendResponse) error { + defer b.commitRequest(responses) + + responseRecord := request.queryType + var response string + + switch request.queryType { + case "SOA": + response = fmt.Sprintf("%s. hostmaster%s. %d 1800 3600 7200 5", + b.config.SOAFqdn, b.config.Domain, b.currentSOASerial()) + + case "NS": + response = fmt.Sprintf("%s.", b.config.SOAFqdn) + + case "A", "AAAA", "ANY": + hostname, err := b.extractHostname(request.queryName) + if err != nil { + return err + } + + var host *hosts.Host + if host, err = b.hosts.GetHost(hostname); err != nil { + return err + } + + response = host.Ip + + responseRecord = "A" + if !host.IsIPv4() { + responseRecord = "AAAA" + } + + if (request.queryType == "A" || request.queryType == "AAAA") && request.queryType != responseRecord { + return errors.New("IP address is not valid for requested record") + } + + default: + return errors.New("Unsupported query type") + } + + responses <- newResponse("DATA", request.queryName, request.queryClass, responseRecord, "10", request.queryId, response) + + return nil +} + +func (b *PowerDnsBackend) commitRequest(responses chan backendResponse) { + responses <- endResponse +} + +// parseRequest reads a line from input and tries to build a request structure from it +func (b *PowerDnsBackend) parseRequest(input *bufio.Reader) (*backendRequest, error) { + line, _, err := input.ReadLine() + if err != nil { + return nil, err + } + + parts := strings.Split(string(line), "\t") + if len(parts) != 6 { + return nil, errors.New("Invalid line") + } + + return &backendRequest{ + query: parts[0], + queryName: parts[1], + queryClass: parts[2], + queryType: parts[3], + queryId: parts[4], + }, nil +} + +// extractHostname extract the host part of the fqdn: pi.d.example.org -> pi +func (b *PowerDnsBackend) extractHostname(rawQueryName string) (string, error) { + queryName := strings.ToLower(rawQueryName) + + hostname := "" + if strings.HasSuffix(queryName, b.config.Domain) { + hostname = queryName[:len(queryName)-len(b.config.Domain)] + } + + if hostname == "" { + return "", errors.New("Query name does not correspond to our domain") + } + + return hostname, nil +} + +// currentSOASerial get the current SOA serial by returning the current time in seconds +func (b *PowerDnsBackend) currentSOASerial() int64 { + return time.Now().Unix() +} + +type backendRequest struct { + query string + queryName string + queryClass string + queryType string + queryId string +} + +type backendResponse []string + +func newResponse(values ...string) backendResponse { + return values +} + +var ( + handshakeResponse backendResponse = []string{"OK", "DDNS Backend"} + endResponse backendResponse = []string{"END"} + failResponse backendResponse = []string{"FAIL"} +) diff --git a/backend/backend_test.go b/backend/backend_test.go new file mode 100644 index 0000000..dfd2c55 --- /dev/null +++ b/backend/backend_test.go @@ -0,0 +1,177 @@ +package backend + +import ( + c "../config" + h "../hosts" + "bufio" + "bytes" + "errors" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +type testHostBackend struct { + hosts map[string]*h.Host +} + +func (b *testHostBackend) GetHost(hostname string) (*h.Host, error) { + host, ok := b.hosts[hostname] + if ok { + return host, nil + } else { + return nil, errors.New("Host not found") + } +} + +func (b *testHostBackend) SetHost(host *h.Host) error { + b.hosts[host.Hostname] = host + return nil +} + +func buildBackend(domain string) (*c.Config, *testHostBackend, *PowerDnsBackend) { + config := &c.Config{ + Verbose: false, + Domain: domain, + SOAFqdn: "dns" + domain, + } + + hosts := &testHostBackend{ + hosts: map[string]*h.Host{ + "www": { + Hostname: "www", + Ip: "10.11.12.13", + Token: "abcdef", + }, + "v4": { + Hostname: "v4", + Ip: "10.10.10.10", + Token: "ghijkl", + }, + "v6": { + Hostname: "v6", + Ip: "2001:db8:85a3::8a2e:370:7334", + Token: "ghijkl", + }, + }, + } + + return config, hosts, NewPowerDnsBackend(config, hosts, os.Stdin, os.Stdout) +} + +func buildRequest(queryName, queryType string) *backendRequest { + return &backendRequest{ + query: "Q", + queryName: queryName, + queryClass: "IN", + queryType: queryType, + queryId: "-1", + } +} + +func readResponse(t *testing.T, responses chan backendResponse) backendResponse { + select { + case response, ok := <-responses: + assert.True(t, ok) + return response + default: + assert.FailNow(t, "Couldn't read response because it is not available ...") + return nil + } +} + +func TestParseRequest(t *testing.T) { + _, _, backend := buildBackend(".example.org") + + reader := bufio.NewReader(bytes.NewBufferString("Q\twww.example.org\tIN\tCNAME\t-1\t203.0.113.210\n")) + request, err := backend.parseRequest(reader) + assert.Nil(t, err) + assert.Equal(t, buildRequest("www.example.org", "CNAME"), request) + + reader = bufio.NewReader(bytes.NewBufferString("Q\texample.org\tIN\tSOA\t-1\t203.0.113.210\n")) + request, err = backend.parseRequest(reader) + assert.Nil(t, err) + assert.Equal(t, buildRequest("example.org", "SOA"), request) + + reader = bufio.NewReader(bytes.NewBufferString("Q\texample.org\n")) + request, err = backend.parseRequest(reader) + assert.NotNil(t, err) + assert.Nil(t, request) +} + +func TestRequestHandling(t *testing.T) { + _, _, backend := buildBackend(".example.org") + + responses := make(chan backendResponse, 2) + err := backend.handleRequest(buildRequest("example.org", "SOA"), responses) + assert.Nil(t, err) + soaResponse := readResponse(t, responses) + assert.Equal(t, newResponse("DATA", "example.org", "IN", "SOA", "10", "-1"), soaResponse[0:6]) + assert.Regexp(t, "dns\\.example\\.org\\. hostmaster\\.example.org\\. \\d+ 1800 3600 7200 5", soaResponse[6]) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("example.org", "NS"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "example.org", "IN", "NS", "10", "-1", "dns.example.org."), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("www.example.org", "ANY"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "www.example.org", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("www.example.org", "A"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "www.example.org", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + // Allow hostname to be mixed case which is used by Let's Encrypt for a little bit more security + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("wWW.eXaMPlE.oRg", "A"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "wWW.eXaMPlE.oRg", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 1) + err = backend.handleRequest(buildRequest("notexisting.example.org", "A"), responses) + assert.NotNil(t, err) + assert.Equal(t, endResponse, readResponse(t, responses)) + + // Correct Handling of IPv4/IPv6 and ANY/A/AAAA + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("v4.example.org", "ANY"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "v4.example.org", "IN", "A", "10", "-1", "10.10.10.10"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("v4.example.org", "A"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "v4.example.org", "IN", "A", "10", "-1", "10.10.10.10"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 1) + err = backend.handleRequest(buildRequest("v4.example.org", "AAAA"), responses) + assert.NotNil(t, err) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("v6.example.org", "ANY"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "v6.example.org", "IN", "AAAA", "10", "-1", "2001:db8:85a3::8a2e:370:7334"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 2) + err = backend.handleRequest(buildRequest("v6.example.org", "AAAA"), responses) + assert.Nil(t, err) + assert.Equal(t, newResponse("DATA", "v6.example.org", "IN", "AAAA", "10", "-1", "2001:db8:85a3::8a2e:370:7334"), readResponse(t, responses)) + assert.Equal(t, endResponse, readResponse(t, responses)) + + responses = make(chan backendResponse, 1) + err = backend.handleRequest(buildRequest("v6.example.org", "A"), responses) + assert.NotNil(t, err) + assert.Equal(t, endResponse, readResponse(t, responses)) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..2532421 --- /dev/null +++ b/config/config.go @@ -0,0 +1,10 @@ +package config + +type Config struct { + Verbose bool + Domain string + SOAFqdn string + HostExpirationDays int + Listen string + RedisHost string +} diff --git a/ddns.go b/ddns.go index 515d64b..11e2e57 100644 --- a/ddns.go +++ b/ddns.go @@ -1,63 +1,63 @@ package main import ( + "./backend" + "./config" + "./hosts" + "./web" "flag" "log" + "os" "strings" ) -func HandleErr(err error) { - if err != nil { - log.Fatal(err) - } -} - const ( CmdBackend string = "backend" CmdWeb string = "web" ) -var ( - DdnsDomain string - DdnsWebListenSocket string - DdnsRedisHost string - DdnsSoaFqdn string - Verbose bool -) +var serviceConfig *config.Config func init() { - flag.StringVar(&DdnsDomain, "domain", "", + flag.StringVar(&serviceConfig.Domain, "domain", "", "The subdomain which should be handled by DDNS") - flag.StringVar(&DdnsWebListenSocket, "listen", ":8080", + flag.StringVar(&serviceConfig.Listen, "listen", ":8080", "Which socket should the web service use to bind itself") - flag.StringVar(&DdnsRedisHost, "redis", ":6379", + flag.StringVar(&serviceConfig.RedisHost, "redis", ":6379", "The Redis socket that should be used") - flag.StringVar(&DdnsSoaFqdn, "soa_fqdn", "", + flag.StringVar(&serviceConfig.SOAFqdn, "soa_fqdn", "", "The FQDN of the DNS server which is returned as a SOA record") - flag.BoolVar(&Verbose, "verbose", false, + flag.IntVar(&serviceConfig.HostExpirationDays, "expiration-days", 10, + "The number of days after a host is released when it is not updated") + + flag.BoolVar(&serviceConfig.Verbose, "verbose", false, "Be more verbose") } -func ValidateCommandArgs(cmd string) { - if DdnsDomain == "" { +func usage() { + log.Fatal("Usage: ./ddns [backend|web]") +} + +func validateCommandArgs(cmd string) { + if serviceConfig.Domain == "" { log.Fatal("You have to supply the domain via --domain=DOMAIN") - } else if !strings.HasPrefix(DdnsDomain, ".") { + } else if !strings.HasPrefix(serviceConfig.Domain, ".") { // get the domain in the right format - DdnsDomain = "." + DdnsDomain + serviceConfig.Domain = "." + serviceConfig.Domain } if cmd == CmdBackend { - if DdnsSoaFqdn == "" { + if serviceConfig.SOAFqdn == "" { log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") } } } -func PrepareForExecution() string { +func prepareForExecution() string { flag.Parse() if len(flag.Args()) != 1 { @@ -65,28 +65,24 @@ func PrepareForExecution() string { } cmd := flag.Args()[0] - ValidateCommandArgs(cmd) + validateCommandArgs(cmd) return cmd } func main() { - cmd := PrepareForExecution() + cmd := prepareForExecution() - conn := OpenConnection(DdnsRedisHost) - defer conn.Close() + redis := hosts.NewRedisBackend(serviceConfig) + defer redis.Close() switch cmd { case CmdBackend: - log.Printf("Starting PDNS Backend\n") - RunBackend(conn) + backend.NewPowerDnsBackend(serviceConfig, redis, os.Stdin, os.Stdout).Run() + case CmdWeb: - log.Printf("Starting Web Service\n") - RunWebService(conn) + web.NewWebService(serviceConfig, redis).Run() + default: usage() } } - -func usage() { - log.Fatal("Usage: ./ddns [backend|web]") -} diff --git a/hosts/hosts.go b/hosts/hosts.go new file mode 100644 index 0000000..6d55d91 --- /dev/null +++ b/hosts/hosts.go @@ -0,0 +1,36 @@ +package hosts + +import ( + "crypto/sha1" + "fmt" + "strings" + "time" +) + +type Host struct { + Hostname string `redis:"-"` + Ip string `redis:"ip"` + Token string `redis:"token"` +} + +func (h *Host) GenerateAndSetToken() { + hash := sha1.New() + hash.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + hash.Write([]byte(h.Hostname)) + + h.Token = fmt.Sprintf("%x", hash.Sum(nil)) +} + +func (h *Host) IsIPv4() bool { + if strings.Contains(h.Ip, ".") { + return true + } + + return false +} + +type HostBackend interface { + GetHost(string) (*Host, error) + + SetHost(*Host) error +} diff --git a/hosts/redis.go b/hosts/redis.go new file mode 100644 index 0000000..0ca1919 --- /dev/null +++ b/hosts/redis.go @@ -0,0 +1,76 @@ +package hosts + +import ( + "../config" + "github.com/garyburd/redigo/redis" + "time" +) + +type RedisBackend struct { + expirationSeconds int + pool *redis.Pool +} + +func NewRedisBackend(config *config.Config) *RedisBackend { + return &RedisBackend{ + expirationSeconds: config.HostExpirationDays * 24 * 60 * 60, + pool: &redis.Pool{ + MaxIdle: 3, + IdleTimeout: 240 * time.Second, + + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", config.RedisHost) + if err != nil { + return nil, err + } + return c, err + }, + + TestOnBorrow: func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + }, + }, + } +} + +func (r *RedisBackend) Close() { + r.pool.Close() +} + +func (r *RedisBackend) GetHost(name string) (*Host, error) { + conn := r.pool.Get() + defer conn.Close() + + host := Host{Hostname: name} + + var err error + var data []interface{} + + if data, err = redis.Values(conn.Do("HGETALL", host.Hostname)); err != nil { + return nil, err + } + + if err = redis.ScanStruct(data, &host); err != nil { + return nil, err + } + + return &host, nil +} + +func (r *RedisBackend) SetHost(host *Host) error { + conn := r.pool.Get() + defer conn.Close() + + var err error + + if _, err = conn.Do("HMSET", redis.Args{}.Add(host.Hostname).AddFlat(host)...); err != nil { + return err + } + + if _, err = conn.Do("EXPIRE", host.Hostname, r.expirationSeconds); err != nil { + return err + } + + return nil +} diff --git a/redis.go b/redis.go deleted file mode 100644 index 5f2c89f..0000000 --- a/redis.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "crypto/sha1" - "fmt" - "github.com/garyburd/redigo/redis" - "strings" - "time" -) - -const HostExpirationSeconds int = 10 * 24 * 60 * 60 // 10 Days - -type RedisConnection struct { - *redis.Pool -} - -func OpenConnection(server string) *RedisConnection { - return &RedisConnection{newPool(server)} -} - -func newPool(server string) *redis.Pool { - return &redis.Pool{ - MaxIdle: 3, - - IdleTimeout: 240 * time.Second, - - Dial: func() (redis.Conn, error) { - c, err := redis.Dial("tcp", server) - if err != nil { - return nil, err - } - return c, err - }, - - TestOnBorrow: func(c redis.Conn, t time.Time) error { - _, err := c.Do("PING") - return err - }, - } -} - -func (self *RedisConnection) GetHost(name string) *Host { - conn := self.Get() - defer conn.Close() - - host := Host{Hostname: name} - - if self.HostExist(name) { - data, err := redis.Values(conn.Do("HGETALL", host.Hostname)) - HandleErr(err) - - HandleErr(redis.ScanStruct(data, &host)) - } - - return &host -} - -func (self *RedisConnection) SaveHost(host *Host) { - conn := self.Get() - defer conn.Close() - - _, err := conn.Do("HMSET", redis.Args{}.Add(host.Hostname).AddFlat(host)...) - HandleErr(err) - - _, err = conn.Do("EXPIRE", host.Hostname, HostExpirationSeconds) - HandleErr(err) -} - -func (self *RedisConnection) HostExist(name string) bool { - conn := self.Get() - defer conn.Close() - - exists, err := redis.Bool(conn.Do("EXISTS", name)) - HandleErr(err) - - return exists -} - -type Host struct { - Hostname string `redis:"-"` - Ip string `redis:"ip"` - Token string `redis:"token"` -} - -func (self *Host) GenerateAndSetToken() { - hash := sha1.New() - hash.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) - hash.Write([]byte(self.Hostname)) - - self.Token = fmt.Sprintf("%x", hash.Sum(nil)) -} - -// Returns true when this host has a IPv4 Address and false if IPv6 -func (self *Host) IsIPv4() bool { - if strings.Contains(self.Ip, ".") { - return true - } - - return false -} diff --git a/template.go b/web/template.go similarity index 99% rename from template.go rename to web/template.go index 117402f..bb2b1cd 100644 --- a/template.go +++ b/web/template.go @@ -1,4 +1,4 @@ -package main +package web const indexTemplate string = ` diff --git a/web.go b/web/web.go similarity index 57% rename from web.go rename to web/web.go index fe9c18a..66df140 100644 --- a/web.go +++ b/web/web.go @@ -1,49 +1,74 @@ -package main +package web import ( + "../config" + "../hosts" "fmt" "github.com/gin-gonic/gin" "html/template" + "log" "net" "net/http" "regexp" ) -func RunWebService(conn *RedisConnection) { +type WebService struct { + config *config.Config + hosts hosts.HostBackend +} + +func NewWebService(config *config.Config, hosts hosts.HostBackend) *WebService { + return &WebService{ + config: config, + hosts: hosts, + } +} + +func (w *WebService) Run() { r := gin.Default() - r.SetHTMLTemplate(BuildTemplate()) + r.SetHTMLTemplate(buildTemplate()) r.GET("/", func(g *gin.Context) { - g.HTML(200, "index.html", gin.H{"domain": DdnsDomain}) + g.HTML(200, "index.html", gin.H{"domain": w.config.Domain}) }) r.GET("/available/:hostname", func(c *gin.Context) { - hostname, valid := ValidHostname(c.Params.ByName("hostname")) + hostname, valid := isValidHostname(c.Params.ByName("hostname")) + + if valid { + _, err := w.hosts.GetHost(hostname) + valid = err == nil + } c.JSON(200, gin.H{ - "available": valid && !conn.HostExist(hostname), + "available": valid, }) }) r.GET("/new/:hostname", func(c *gin.Context) { - hostname, valid := ValidHostname(c.Params.ByName("hostname")) + hostname, valid := isValidHostname(c.Params.ByName("hostname")) if !valid { c.JSON(404, gin.H{"error": "This hostname is not valid"}) return } - if conn.HostExist(hostname) { + var err error + + if _, err = w.hosts.GetHost(hostname); err == nil { c.JSON(403, gin.H{ "error": "This hostname has already been registered.", }) return } - host := &Host{Hostname: hostname, Ip: "127.0.0.1"} + host := &hosts.Host{Hostname: hostname, Ip: "127.0.0.1"} host.GenerateAndSetToken() - conn.SaveHost(host) + if err = w.hosts.SetHost(host); err != nil { + c.JSON(400, gin.H{"error": "Could not register host."}) + return + } c.JSON(200, gin.H{ "hostname": host.Hostname, @@ -53,7 +78,7 @@ func RunWebService(conn *RedisConnection) { }) r.GET("/update/:hostname/:token", func(c *gin.Context) { - hostname, valid := ValidHostname(c.Params.ByName("hostname")) + hostname, valid := isValidHostname(c.Params.ByName("hostname")) token := c.Params.ByName("token") if !valid { @@ -61,15 +86,14 @@ func RunWebService(conn *RedisConnection) { return } - if !conn.HostExist(hostname) { + host, err := w.hosts.GetHost(hostname) + if err != nil { c.JSON(404, gin.H{ "error": "This hostname has not been registered or is expired.", }) return } - host := conn.GetHost(hostname) - if host.Token != token { c.JSON(403, gin.H{ "error": "You have supplied the wrong token to manipulate this host", @@ -77,7 +101,7 @@ func RunWebService(conn *RedisConnection) { return } - ip, err := GetRemoteAddr(c.Request) + ip, err := extractRemoteAddr(c.Request) if err != nil { c.JSON(400, gin.H{ "error": "Your sender IP address is not in the right format", @@ -86,7 +110,11 @@ func RunWebService(conn *RedisConnection) { } host.Ip = ip - conn.SaveHost(host) + if err = w.hosts.SetHost(host); err != nil { + c.JSON(400, gin.H{ + "error": "Could not update registered IP address", + }) + } c.JSON(200, gin.H{ "current_ip": ip, @@ -94,13 +122,13 @@ func RunWebService(conn *RedisConnection) { }) }) - r.Run(DdnsWebListenSocket) + r.Run(w.config.Listen) } // Get the Remote Address of the client. At First we try to get the // X-Forwarded-For Header which holds the IP if we are behind a proxy, // otherwise the RemoteAddr is used -func GetRemoteAddr(req *http.Request) (string, error) { +func extractRemoteAddr(req *http.Request) (string, error) { header_data, ok := req.Header["X-Forwarded-For"] if ok { @@ -112,14 +140,16 @@ func GetRemoteAddr(req *http.Request) (string, error) { } // Get index template from bindata -func BuildTemplate() *template.Template { +func buildTemplate() *template.Template { html, err := template.New("index.html").Parse(indexTemplate) - HandleErr(err) + if err != nil { + log.Fatal(err) + } return html } -func ValidHostname(host string) (string, bool) { +func isValidHostname(host string) (string, bool) { valid, _ := regexp.Match("^[a-z0-9]{1,32}$", []byte(host)) return host, valid From e82621e1530ce08078baf6d21a9577cdb99f4bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 25 Dec 2016 23:09:05 +0100 Subject: [PATCH 02/23] First partial working docker setup add initial version of docker setup add configuration via environment variables remove override file support docker-compose.override.yml add up2date pdns container --- .gitignore | 5 ++-- docker/ddns/Dockerfile | 3 +++ docker/docker-compose.yml | 43 +++++++++++++++++++++++++++++++++++ docker/powerdns/Dockerfile | 20 ++++++++++++++++ docker/powerdns/entrypoint.sh | 9 ++++++++ docker/powerdns/pdns.conf | 8 +++++++ docker/web/Dockerfile | 6 +++++ web/web.go | 2 +- 8 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 docker/ddns/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 docker/powerdns/Dockerfile create mode 100644 docker/powerdns/entrypoint.sh create mode 100644 docker/powerdns/pdns.conf create mode 100644 docker/web/Dockerfile diff --git a/.gitignore b/.gitignore index 621dce7..00090a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.swp -pdns.conf -pdns_backend.py -ddns +/docker/docker-compose.override.yml +/ddns dump.rdb diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile new file mode 100644 index 0000000..936e5fd --- /dev/null +++ b/docker/ddns/Dockerfile @@ -0,0 +1,3 @@ +FROM golang:1.7-onbuild + +CMD ["/bin/true"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..de8e4aa --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,43 @@ +version: '2' + +volumes: + ddns: + +services: + ddns: + restart: unless-stopped + build: + context: .. + dockerfile: docker/ddns/Dockerfile + volumes: + - ddns:/go/bin/ + + ddns-web: + restart: unless-stopped + build: + context: web/ + dockerfile: Dockerfile + depends_on: + - redis + volumes_from: + - ddns + environment: + DDNS_DOMAIN: d.example.net + + ddns-powerdns: + restart: unless-stopped + build: + context: powerdns/ + dockerfile: Dockerfile + depends_on: + - redis + volumes_from: + - ddns + environment: + DDNS_DOMAIN: d.example.net + DDNS_SOA_DOMAIN: soa.example.net + DDNS_VERBOSE: --verbose + + redis: + restart: unless-stopped + image: redis:latest \ No newline at end of file diff --git a/docker/powerdns/Dockerfile b/docker/powerdns/Dockerfile new file mode 100644 index 0000000..af138e5 --- /dev/null +++ b/docker/powerdns/Dockerfile @@ -0,0 +1,20 @@ +FROM buildpack-deps:jessie-scm +MAINTAINER Philipp Böhm + +# the setup procedure according to https://repo.powerdns.com/ (Debian 8 Jessie) +RUN echo "deb http://repo.powerdns.com/debian jessie-auth-40 main" > /etc/apt/sources.list.d/pdns.list \ + && echo "Package: pdns-*\nPin: origin repo.powerdns.com\nPin-Priority: 600\n" >> /etc/apt/preferences.d/pdns \ + && curl https://repo.powerdns.com/FD380FBB-pub.asc | apt-key add - \ + && apt-get -y update \ + && apt-get install -y pdns-server pdns-backend-pipe \ + && rm -rf /var/lib/apt/lists/* + +COPY pdns.conf /etc/powerdns/pdns.conf + +COPY entrypoint.sh / +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +EXPOSE 53 + +CMD ["pdns_server", "--daemon=no"] \ No newline at end of file diff --git a/docker/powerdns/entrypoint.sh b/docker/powerdns/entrypoint.sh new file mode 100644 index 0000000..3070000 --- /dev/null +++ b/docker/powerdns/entrypoint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +CONFIG_FILE=/etc/powerdns/pdns.conf + +sed -i 's/{{DDNS_DOMAIN}}/'"${DDNS_DOMAIN}"'/g' ${CONFIG_FILE} +sed -i 's/{{DDNS_SOA_DOMAIN}}/'"${DDNS_SOA_DOMAIN}"'/g' ${CONFIG_FILE} +sed -i 's/{{DDNS_VERBOSE}}/'"${DDNS_VERBOSE}"'/g' ${CONFIG_FILE} + +exec "$@" \ No newline at end of file diff --git a/docker/powerdns/pdns.conf b/docker/powerdns/pdns.conf new file mode 100644 index 0000000..5e209f9 --- /dev/null +++ b/docker/powerdns/pdns.conf @@ -0,0 +1,8 @@ +disable-tcp=yes +cache-ttl=0 +loglevel=7 +log-dns-details=yes +disable-axfr=yes + +launch=pipe +pipe-command=/go/bin/app --domain={{DDNS_DOMAIN}} --soa_fqdn={{DDNS_SOA_DOMAIN}} --redis=redis:6379 {{DDNS_VERBOSE}} backend \ No newline at end of file diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 0000000..8c4e53c --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,6 @@ +FROM buildpack-deps:jessie-scm +MAINTAINER Philipp Böhm + +ENV GIN_MODE release + +CMD /go/bin/app --domain=${DDNS_DOMAIN} --redis=redis:6379 web diff --git a/web/web.go b/web/web.go index 66df140..a31bd0a 100644 --- a/web/web.go +++ b/web/web.go @@ -4,7 +4,7 @@ import ( "../config" "../hosts" "fmt" - "github.com/gin-gonic/gin" + "gopkg.in/gin-gonic/gin.v1" "html/template" "log" "net" From 95db447b7c7e9a09922aa5528d5595b613d5c463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 2 Jul 2017 22:27:41 +0200 Subject: [PATCH 03/23] use absolute imports instead of relative --- backend/backend.go | 4 ++-- backend/backend_test.go | 4 ++-- ddns.go | 10 +++++----- hosts/redis.go | 2 +- web/web.go | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 9e1b340..d956809 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -1,8 +1,8 @@ package backend import ( - "../config" - "../hosts" + "github.com/pboehm/ddns/config" + "github.com/pboehm/ddns/hosts" "bufio" "errors" "fmt" diff --git a/backend/backend_test.go b/backend/backend_test.go index dfd2c55..5fa307e 100644 --- a/backend/backend_test.go +++ b/backend/backend_test.go @@ -1,8 +1,8 @@ package backend import ( - c "../config" - h "../hosts" + c "github.com/pboehm/ddns/config" + h "github.com/pboehm/ddns/hosts" "bufio" "bytes" "errors" diff --git a/ddns.go b/ddns.go index 11e2e57..0dd5264 100644 --- a/ddns.go +++ b/ddns.go @@ -1,10 +1,10 @@ package main import ( - "./backend" - "./config" - "./hosts" - "./web" + "github.com/pboehm/ddns/backend" + "github.com/pboehm/ddns/config" + "github.com/pboehm/ddns/hosts" + "github.com/pboehm/ddns/web" "flag" "log" "os" @@ -16,7 +16,7 @@ const ( CmdWeb string = "web" ) -var serviceConfig *config.Config +var serviceConfig *config.Config = &config.Config{} func init() { flag.StringVar(&serviceConfig.Domain, "domain", "", diff --git a/hosts/redis.go b/hosts/redis.go index 0ca1919..f000a6e 100644 --- a/hosts/redis.go +++ b/hosts/redis.go @@ -1,7 +1,7 @@ package hosts import ( - "../config" + "github.com/pboehm/ddns/config" "github.com/garyburd/redigo/redis" "time" ) diff --git a/web/web.go b/web/web.go index a31bd0a..5a94905 100644 --- a/web/web.go +++ b/web/web.go @@ -1,8 +1,8 @@ package web import ( - "../config" - "../hosts" + "github.com/pboehm/ddns/config" + "github.com/pboehm/ddns/hosts" "fmt" "gopkg.in/gin-gonic/gin.v1" "html/template" From 9f4442db79a9287afb2fb7c8559a30e4aa33f9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 2 Jul 2017 22:58:07 +0200 Subject: [PATCH 04/23] go fmt --- backend/backend.go | 4 ++-- backend/backend_test.go | 4 ++-- ddns.go | 2 +- hosts/redis.go | 2 +- web/web.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index d956809..94f0772 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -1,11 +1,11 @@ package backend import ( - "github.com/pboehm/ddns/config" - "github.com/pboehm/ddns/hosts" "bufio" "errors" "fmt" + "github.com/pboehm/ddns/config" + "github.com/pboehm/ddns/hosts" "io" "strings" "time" diff --git a/backend/backend_test.go b/backend/backend_test.go index 5fa307e..2a155c1 100644 --- a/backend/backend_test.go +++ b/backend/backend_test.go @@ -1,11 +1,11 @@ package backend import ( - c "github.com/pboehm/ddns/config" - h "github.com/pboehm/ddns/hosts" "bufio" "bytes" "errors" + c "github.com/pboehm/ddns/config" + h "github.com/pboehm/ddns/hosts" "github.com/stretchr/testify/assert" "os" "testing" diff --git a/ddns.go b/ddns.go index 0dd5264..089b05b 100644 --- a/ddns.go +++ b/ddns.go @@ -1,11 +1,11 @@ package main import ( + "flag" "github.com/pboehm/ddns/backend" "github.com/pboehm/ddns/config" "github.com/pboehm/ddns/hosts" "github.com/pboehm/ddns/web" - "flag" "log" "os" "strings" diff --git a/hosts/redis.go b/hosts/redis.go index f000a6e..1d9dc0c 100644 --- a/hosts/redis.go +++ b/hosts/redis.go @@ -1,8 +1,8 @@ package hosts import ( - "github.com/pboehm/ddns/config" "github.com/garyburd/redigo/redis" + "github.com/pboehm/ddns/config" "time" ) diff --git a/web/web.go b/web/web.go index 5a94905..8a35e07 100644 --- a/web/web.go +++ b/web/web.go @@ -1,9 +1,9 @@ package web import ( + "fmt" "github.com/pboehm/ddns/config" "github.com/pboehm/ddns/hosts" - "fmt" "gopkg.in/gin-gonic/gin.v1" "html/template" "log" From abcdc9ecd310093a0ff0e61a7b211b5a191c3a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 2 Jul 2017 23:08:36 +0200 Subject: [PATCH 05/23] fix ddns build --- docker/ddns/Dockerfile | 8 +++++++- docker/docker-compose.yml | 2 +- docker/powerdns/pdns.conf | 3 ++- docker/web/Dockerfile | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile index 936e5fd..2b2bfaa 100644 --- a/docker/ddns/Dockerfile +++ b/docker/ddns/Dockerfile @@ -1,3 +1,9 @@ -FROM golang:1.7-onbuild +FROM golang:1.7 + +WORKDIR /go/src/github.com/pboehm/ddns +COPY . . + +RUN go-wrapper download # "go get -d -v ./..." +RUN go-wrapper install # "go install -v ." CMD ["/bin/true"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index de8e4aa..e6d1295 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,7 +5,7 @@ volumes: services: ddns: - restart: unless-stopped + restart: no build: context: .. dockerfile: docker/ddns/Dockerfile diff --git a/docker/powerdns/pdns.conf b/docker/powerdns/pdns.conf index 5e209f9..367baa3 100644 --- a/docker/powerdns/pdns.conf +++ b/docker/powerdns/pdns.conf @@ -3,6 +3,7 @@ cache-ttl=0 loglevel=7 log-dns-details=yes disable-axfr=yes +pipe-abi-version=1 launch=pipe -pipe-command=/go/bin/app --domain={{DDNS_DOMAIN}} --soa_fqdn={{DDNS_SOA_DOMAIN}} --redis=redis:6379 {{DDNS_VERBOSE}} backend \ No newline at end of file +pipe-command=/go/bin/ddns --domain={{DDNS_DOMAIN}} --soa_fqdn={{DDNS_SOA_DOMAIN}} --redis=redis:6379 {{DDNS_VERBOSE}} backend \ No newline at end of file diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index 8c4e53c..9ec1003 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -3,4 +3,4 @@ MAINTAINER Philipp Böhm ENV GIN_MODE release -CMD /go/bin/app --domain=${DDNS_DOMAIN} --redis=redis:6379 web +CMD /go/bin/ddns --domain=${DDNS_DOMAIN} --redis=redis:6379 web From 33ca0443ff3a2d3da985a1b77fbe1ef6c9292724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 2 Jul 2017 23:12:10 +0200 Subject: [PATCH 06/23] remove restart policy --- docker/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e6d1295..75e6606 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,7 +5,6 @@ volumes: services: ddns: - restart: no build: context: .. dockerfile: docker/ddns/Dockerfile From c2e7b6f2d282bd8b8eadd7b754ab09e936ffcdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Tue, 28 Nov 2017 22:26:59 +0100 Subject: [PATCH 07/23] prepare docker setup for http remote backend --- docker/ddns/Dockerfile | 6 ++++-- docker/docker-compose.yml | 25 +++++-------------------- docker/powerdns/Dockerfile | 4 ++-- docker/powerdns/entrypoint.sh | 4 +--- docker/powerdns/pdns.conf | 5 ++--- docker/web/Dockerfile | 6 ------ web/template.go | 4 ++-- 7 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 docker/web/Dockerfile diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile index 2b2bfaa..e51e786 100644 --- a/docker/ddns/Dockerfile +++ b/docker/ddns/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.7 +FROM golang:1.9 WORKDIR /go/src/github.com/pboehm/ddns COPY . . @@ -6,4 +6,6 @@ COPY . . RUN go-wrapper download # "go get -d -v ./..." RUN go-wrapper install # "go install -v ." -CMD ["/bin/true"] +ENV GIN_MODE release + +CMD /go/bin/ddns --domain=${DDNS_DOMAIN} --soa_fqdn=${DDNS_SOA_DOMAIN} --redis=redis:6379 web diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 75e6606..0685d5d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,42 +1,27 @@ version: '2' -volumes: - ddns: - services: ddns: + restart: unless-stopped build: context: .. dockerfile: docker/ddns/Dockerfile - volumes: - - ddns:/go/bin/ - - ddns-web: - restart: unless-stopped - build: - context: web/ - dockerfile: Dockerfile depends_on: - redis - volumes_from: - - ddns environment: DDNS_DOMAIN: d.example.net + DDNS_SOA_DOMAIN: soa.example.net - ddns-powerdns: + powerdns: restart: unless-stopped build: context: powerdns/ dockerfile: Dockerfile depends_on: - - redis - volumes_from: - ddns environment: - DDNS_DOMAIN: d.example.net - DDNS_SOA_DOMAIN: soa.example.net - DDNS_VERBOSE: --verbose + PDNS_REMOTE_HTTP_HOST: "ddns:8080" redis: restart: unless-stopped - image: redis:latest \ No newline at end of file + image: redis:4-alpine \ No newline at end of file diff --git a/docker/powerdns/Dockerfile b/docker/powerdns/Dockerfile index af138e5..e560109 100644 --- a/docker/powerdns/Dockerfile +++ b/docker/powerdns/Dockerfile @@ -2,11 +2,11 @@ FROM buildpack-deps:jessie-scm MAINTAINER Philipp Böhm # the setup procedure according to https://repo.powerdns.com/ (Debian 8 Jessie) -RUN echo "deb http://repo.powerdns.com/debian jessie-auth-40 main" > /etc/apt/sources.list.d/pdns.list \ +RUN echo "deb http://repo.powerdns.com/debian jessie-auth-41 main" > /etc/apt/sources.list.d/pdns.list \ && echo "Package: pdns-*\nPin: origin repo.powerdns.com\nPin-Priority: 600\n" >> /etc/apt/preferences.d/pdns \ && curl https://repo.powerdns.com/FD380FBB-pub.asc | apt-key add - \ && apt-get -y update \ - && apt-get install -y pdns-server pdns-backend-pipe \ + && apt-get install -y pdns-server pdns-backend-remote \ && rm -rf /var/lib/apt/lists/* COPY pdns.conf /etc/powerdns/pdns.conf diff --git a/docker/powerdns/entrypoint.sh b/docker/powerdns/entrypoint.sh index 3070000..da767ff 100644 --- a/docker/powerdns/entrypoint.sh +++ b/docker/powerdns/entrypoint.sh @@ -2,8 +2,6 @@ CONFIG_FILE=/etc/powerdns/pdns.conf -sed -i 's/{{DDNS_DOMAIN}}/'"${DDNS_DOMAIN}"'/g' ${CONFIG_FILE} -sed -i 's/{{DDNS_SOA_DOMAIN}}/'"${DDNS_SOA_DOMAIN}"'/g' ${CONFIG_FILE} -sed -i 's/{{DDNS_VERBOSE}}/'"${DDNS_VERBOSE}"'/g' ${CONFIG_FILE} +sed -i 's/{{PDNS_REMOTE_HTTP_HOST}}/'"${PDNS_REMOTE_HTTP_HOST}"'/g' ${CONFIG_FILE} exec "$@" \ No newline at end of file diff --git a/docker/powerdns/pdns.conf b/docker/powerdns/pdns.conf index 367baa3..0ddb6ec 100644 --- a/docker/powerdns/pdns.conf +++ b/docker/powerdns/pdns.conf @@ -3,7 +3,6 @@ cache-ttl=0 loglevel=7 log-dns-details=yes disable-axfr=yes -pipe-abi-version=1 -launch=pipe -pipe-command=/go/bin/ddns --domain={{DDNS_DOMAIN}} --soa_fqdn={{DDNS_SOA_DOMAIN}} --redis=redis:6379 {{DDNS_VERBOSE}} backend \ No newline at end of file +launch=remote +remote-connection-string=http:url=http://{{PDNS_REMOTE_HTTP_HOST}}/dnsapi \ No newline at end of file diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile deleted file mode 100644 index 9ec1003..0000000 --- a/docker/web/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM buildpack-deps:jessie-scm -MAINTAINER Philipp Böhm - -ENV GIN_MODE release - -CMD /go/bin/ddns --domain=${DDNS_DOMAIN} --redis=redis:6379 web diff --git a/web/template.go b/web/template.go index bb2b1cd..94424a9 100644 --- a/web/template.go +++ b/web/template.go @@ -168,7 +168,7 @@ const indexTemplate string = ` function validate() { var hostname = $('#hostname').val(); - $.getJSON("/available/" + hostname + "/", function( data ) { + $.getJSON("/available/" + hostname, function( data ) { if (data.available) { isValid(); } else { @@ -188,7 +188,7 @@ const indexTemplate string = ` $('#register').click(function() { var hostname = $("#hostname").val(); - $.getJSON("/new/" + hostname + "/", function( data ) { + $.getJSON("/new/" + hostname, function( data ) { console.log(data); var host = location.protocol + '//' + location.host; From 24b7ea605819cd901f9c4a1b65f52444021322b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Tue, 28 Nov 2017 23:54:05 +0100 Subject: [PATCH 08/23] Introduce HostLookup and corresponding tests --- backend/backend_test.go | 177 ---------------------------------------- backend/lookup.go | 93 +++++++++++++++++++++ backend/lookup_test.go | 154 ++++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 177 deletions(-) delete mode 100644 backend/backend_test.go create mode 100644 backend/lookup.go create mode 100644 backend/lookup_test.go diff --git a/backend/backend_test.go b/backend/backend_test.go deleted file mode 100644 index 2a155c1..0000000 --- a/backend/backend_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package backend - -import ( - "bufio" - "bytes" - "errors" - c "github.com/pboehm/ddns/config" - h "github.com/pboehm/ddns/hosts" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -type testHostBackend struct { - hosts map[string]*h.Host -} - -func (b *testHostBackend) GetHost(hostname string) (*h.Host, error) { - host, ok := b.hosts[hostname] - if ok { - return host, nil - } else { - return nil, errors.New("Host not found") - } -} - -func (b *testHostBackend) SetHost(host *h.Host) error { - b.hosts[host.Hostname] = host - return nil -} - -func buildBackend(domain string) (*c.Config, *testHostBackend, *PowerDnsBackend) { - config := &c.Config{ - Verbose: false, - Domain: domain, - SOAFqdn: "dns" + domain, - } - - hosts := &testHostBackend{ - hosts: map[string]*h.Host{ - "www": { - Hostname: "www", - Ip: "10.11.12.13", - Token: "abcdef", - }, - "v4": { - Hostname: "v4", - Ip: "10.10.10.10", - Token: "ghijkl", - }, - "v6": { - Hostname: "v6", - Ip: "2001:db8:85a3::8a2e:370:7334", - Token: "ghijkl", - }, - }, - } - - return config, hosts, NewPowerDnsBackend(config, hosts, os.Stdin, os.Stdout) -} - -func buildRequest(queryName, queryType string) *backendRequest { - return &backendRequest{ - query: "Q", - queryName: queryName, - queryClass: "IN", - queryType: queryType, - queryId: "-1", - } -} - -func readResponse(t *testing.T, responses chan backendResponse) backendResponse { - select { - case response, ok := <-responses: - assert.True(t, ok) - return response - default: - assert.FailNow(t, "Couldn't read response because it is not available ...") - return nil - } -} - -func TestParseRequest(t *testing.T) { - _, _, backend := buildBackend(".example.org") - - reader := bufio.NewReader(bytes.NewBufferString("Q\twww.example.org\tIN\tCNAME\t-1\t203.0.113.210\n")) - request, err := backend.parseRequest(reader) - assert.Nil(t, err) - assert.Equal(t, buildRequest("www.example.org", "CNAME"), request) - - reader = bufio.NewReader(bytes.NewBufferString("Q\texample.org\tIN\tSOA\t-1\t203.0.113.210\n")) - request, err = backend.parseRequest(reader) - assert.Nil(t, err) - assert.Equal(t, buildRequest("example.org", "SOA"), request) - - reader = bufio.NewReader(bytes.NewBufferString("Q\texample.org\n")) - request, err = backend.parseRequest(reader) - assert.NotNil(t, err) - assert.Nil(t, request) -} - -func TestRequestHandling(t *testing.T) { - _, _, backend := buildBackend(".example.org") - - responses := make(chan backendResponse, 2) - err := backend.handleRequest(buildRequest("example.org", "SOA"), responses) - assert.Nil(t, err) - soaResponse := readResponse(t, responses) - assert.Equal(t, newResponse("DATA", "example.org", "IN", "SOA", "10", "-1"), soaResponse[0:6]) - assert.Regexp(t, "dns\\.example\\.org\\. hostmaster\\.example.org\\. \\d+ 1800 3600 7200 5", soaResponse[6]) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("example.org", "NS"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "example.org", "IN", "NS", "10", "-1", "dns.example.org."), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("www.example.org", "ANY"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "www.example.org", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("www.example.org", "A"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "www.example.org", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - // Allow hostname to be mixed case which is used by Let's Encrypt for a little bit more security - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("wWW.eXaMPlE.oRg", "A"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "wWW.eXaMPlE.oRg", "IN", "A", "10", "-1", "10.11.12.13"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 1) - err = backend.handleRequest(buildRequest("notexisting.example.org", "A"), responses) - assert.NotNil(t, err) - assert.Equal(t, endResponse, readResponse(t, responses)) - - // Correct Handling of IPv4/IPv6 and ANY/A/AAAA - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("v4.example.org", "ANY"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "v4.example.org", "IN", "A", "10", "-1", "10.10.10.10"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("v4.example.org", "A"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "v4.example.org", "IN", "A", "10", "-1", "10.10.10.10"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 1) - err = backend.handleRequest(buildRequest("v4.example.org", "AAAA"), responses) - assert.NotNil(t, err) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("v6.example.org", "ANY"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "v6.example.org", "IN", "AAAA", "10", "-1", "2001:db8:85a3::8a2e:370:7334"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 2) - err = backend.handleRequest(buildRequest("v6.example.org", "AAAA"), responses) - assert.Nil(t, err) - assert.Equal(t, newResponse("DATA", "v6.example.org", "IN", "AAAA", "10", "-1", "2001:db8:85a3::8a2e:370:7334"), readResponse(t, responses)) - assert.Equal(t, endResponse, readResponse(t, responses)) - - responses = make(chan backendResponse, 1) - err = backend.handleRequest(buildRequest("v6.example.org", "A"), responses) - assert.NotNil(t, err) - assert.Equal(t, endResponse, readResponse(t, responses)) -} diff --git a/backend/lookup.go b/backend/lookup.go new file mode 100644 index 0000000..51d8c20 --- /dev/null +++ b/backend/lookup.go @@ -0,0 +1,93 @@ +package backend + +import ( + "errors" + "fmt" + "github.com/pboehm/ddns/config" + "github.com/pboehm/ddns/hosts" + "strings" + "time" +) + +type Request struct { + QType string + QName string + Remote string + Local string + RealRemote string + ZoneId string +} + +type Response struct { + QType string + QName string + Content string + TTL int +} + +type HostLookup struct { + config *config.Config + hosts hosts.HostBackend +} + +func (l *HostLookup) Lookup(request *Request) (*Response, error) { + responseRecord := request.QType + responseContent := "" + + switch request.QType { + case "SOA": + responseContent = fmt.Sprintf("%s. hostmaster%s. %d 1800 3600 7200 5", + l.config.SOAFqdn, l.config.Domain, l.currentSOASerial()) + + case "NS": + responseContent = l.config.SOAFqdn + + case "A", "AAAA", "ANY": + hostname, err := l.extractHostname(request.QName) + if err != nil { + return nil, err + } + + var host *hosts.Host + if host, err = l.hosts.GetHost(hostname); err != nil { + return nil, err + } + + responseContent = host.Ip + + responseRecord = "A" + if !host.IsIPv4() { + responseRecord = "AAAA" + } + + if (request.QType == "A" || request.QType == "AAAA") && request.QType != responseRecord { + return nil, errors.New("IP address is not valid for requested record") + } + + default: + return nil, errors.New("Invalid request") + } + + return &Response{QType: responseRecord, QName: request.QName, Content: responseContent, TTL: 5}, nil +} + +// extractHostname extract the host part of the fqdn: pi.d.example.org -> pi +func (l *HostLookup) extractHostname(rawQueryName string) (string, error) { + queryName := strings.ToLower(rawQueryName) + + hostname := "" + if strings.HasSuffix(queryName, l.config.Domain) { + hostname = queryName[:len(queryName)-len(l.config.Domain)] + } + + if hostname == "" { + return "", errors.New("Query name does not correspond to our domain") + } + + return hostname, nil +} + +// currentSOASerial get the current SOA serial by returning the current time in seconds +func (l *HostLookup) currentSOASerial() int64 { + return time.Now().Unix() +} diff --git a/backend/lookup_test.go b/backend/lookup_test.go new file mode 100644 index 0000000..0ec7ed7 --- /dev/null +++ b/backend/lookup_test.go @@ -0,0 +1,154 @@ +package backend + +import ( + "errors" + c "github.com/pboehm/ddns/config" + h "github.com/pboehm/ddns/hosts" + "github.com/stretchr/testify/assert" + "testing" +) + +type testHostBackend struct { + hosts map[string]*h.Host +} + +func (b *testHostBackend) GetHost(hostname string) (*h.Host, error) { + host, ok := b.hosts[hostname] + if ok { + return host, nil + } else { + return nil, errors.New("Host not found") + } +} + +func (b *testHostBackend) SetHost(host *h.Host) error { + b.hosts[host.Hostname] = host + return nil +} + +func buildLookup(domain string) (*c.Config, *testHostBackend, *HostLookup) { + config := &c.Config{ + Verbose: false, + Domain: domain, + SOAFqdn: "dns" + domain, + } + + hosts := &testHostBackend{ + hosts: map[string]*h.Host{ + "www": { + Hostname: "www", + Ip: "10.11.12.13", + Token: "abcdef", + }, + "v4": { + Hostname: "v4", + Ip: "10.10.10.10", + Token: "ghijkl", + }, + "v6": { + Hostname: "v6", + Ip: "2001:db8:85a3::8a2e:370:7334", + Token: "ghijkl", + }, + }, + } + + return config, hosts, &HostLookup{config, hosts} +} + +func buildRequest(queryName, queryType string) *Request { + return &Request{ + QType: queryType, + QName: queryName, + } +} + +func TestRequestHandling(t *testing.T) { + _, _, lookup := buildLookup(".example.org") + + response, err := lookup.Lookup(buildRequest("example.org", "SOA")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "SOA", response.QType) + assert.Equal(t, "example.org", response.QName) + assert.Regexp(t, "dns\\.example\\.org\\. hostmaster\\.example.org\\. \\d+ 1800 3600 7200 5", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("example.org", "NS")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "NS", response.QType) + assert.Equal(t, "example.org", response.QName) + assert.Equal(t, "dns.example.org", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("www.example.org", "ANY")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "A", response.QType) + assert.Equal(t, "www.example.org", response.QName) + assert.Equal(t, "10.11.12.13", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("www.example.org", "A")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "A", response.QType) + assert.Equal(t, "www.example.org", response.QName) + assert.Equal(t, "10.11.12.13", response.Content) + assert.Equal(t, 5, response.TTL) + + // Allow hostname to be mixed case which is used by Let's Encrypt for a little bit more security + response, err = lookup.Lookup(buildRequest("wWW.eXaMPlE.oRg", "A")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "A", response.QType) + assert.Equal(t, "wWW.eXaMPlE.oRg", response.QName) + assert.Equal(t, "10.11.12.13", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("notexisting.example.org", "A")) + assert.NotNil(t, err) + assert.Nil(t, response) + + // Correct Handling of IPv4/IPv6 and ANY/A/AAAA + response, err = lookup.Lookup(buildRequest("v4.example.org", "ANY")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "A", response.QType) + assert.Equal(t, "v4.example.org", response.QName) + assert.Equal(t, "10.10.10.10", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("v4.example.org", "A")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "A", response.QType) + assert.Equal(t, "v4.example.org", response.QName) + assert.Equal(t, "10.10.10.10", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("v4.example.org", "AAAA")) + assert.NotNil(t, err) + assert.Nil(t, response) + + response, err = lookup.Lookup(buildRequest("v6.example.org", "ANY")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "AAAA", response.QType) + assert.Equal(t, "v6.example.org", response.QName) + assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("v6.example.org", "AAAA")) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "AAAA", response.QType) + assert.Equal(t, "v6.example.org", response.QName) + assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", response.Content) + assert.Equal(t, 5, response.TTL) + + response, err = lookup.Lookup(buildRequest("v6.example.org", "A")) + assert.NotNil(t, err) + assert.Nil(t, response) +} From b17528ab9abd4a43658f15dbcfc0e2b506394808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Wed, 29 Nov 2017 00:43:00 +0100 Subject: [PATCH 09/23] add partially working implementation of /dnsapi endpoint --- backend/backend.go | 170 --------------------------------------------- backend/lookup.go | 12 ++-- ddns.go | 44 ++---------- hosts/redis.go | 5 ++ web/web.go | 31 +++++++-- 5 files changed, 47 insertions(+), 215 deletions(-) delete mode 100644 backend/backend.go diff --git a/backend/backend.go b/backend/backend.go deleted file mode 100644 index 94f0772..0000000 --- a/backend/backend.go +++ /dev/null @@ -1,170 +0,0 @@ -package backend - -import ( - "bufio" - "errors" - "fmt" - "github.com/pboehm/ddns/config" - "github.com/pboehm/ddns/hosts" - "io" - "strings" - "time" -) - -// PowerDnsBackend implements the PowerDNS-Pipe-Backend protocol ABI-Version 1 -type PowerDnsBackend struct { - config *config.Config - hosts hosts.HostBackend - in io.Reader - out io.Writer -} - -// NewPowerDnsBackend creates an instance of a PowerDNS-Pipe-Backend using the supplied parameters -func NewPowerDnsBackend(config *config.Config, backend hosts.HostBackend, in io.Reader, out io.Writer) *PowerDnsBackend { - return &PowerDnsBackend{ - config: config, - hosts: backend, - in: in, - out: out, - } -} - -// Run reads requests from an input (normally STDIN) and prints response messages to an output (normally STDOUT) -func (b *PowerDnsBackend) Run() { - responses := make(chan backendResponse, 5) - - go func() { - for response := range responses { - fmt.Fprintln(b.out, strings.Join(response, "\t")) - } - }() - - // handshake with PowerDNS - bio := bufio.NewReader(b.in) - _, _, _ = bio.ReadLine() - responses <- handshakeResponse - - for { - request, err := b.parseRequest(bio) - if err != nil { - responses <- failResponse - continue - } - - if err = b.handleRequest(request, responses); err != nil { - responses <- newResponse("LOG", err.Error()) - } - } -} - -// handleRequest handles the supplied request by sending response messages on the supplied responses channel -func (b *PowerDnsBackend) handleRequest(request *backendRequest, responses chan backendResponse) error { - defer b.commitRequest(responses) - - responseRecord := request.queryType - var response string - - switch request.queryType { - case "SOA": - response = fmt.Sprintf("%s. hostmaster%s. %d 1800 3600 7200 5", - b.config.SOAFqdn, b.config.Domain, b.currentSOASerial()) - - case "NS": - response = fmt.Sprintf("%s.", b.config.SOAFqdn) - - case "A", "AAAA", "ANY": - hostname, err := b.extractHostname(request.queryName) - if err != nil { - return err - } - - var host *hosts.Host - if host, err = b.hosts.GetHost(hostname); err != nil { - return err - } - - response = host.Ip - - responseRecord = "A" - if !host.IsIPv4() { - responseRecord = "AAAA" - } - - if (request.queryType == "A" || request.queryType == "AAAA") && request.queryType != responseRecord { - return errors.New("IP address is not valid for requested record") - } - - default: - return errors.New("Unsupported query type") - } - - responses <- newResponse("DATA", request.queryName, request.queryClass, responseRecord, "10", request.queryId, response) - - return nil -} - -func (b *PowerDnsBackend) commitRequest(responses chan backendResponse) { - responses <- endResponse -} - -// parseRequest reads a line from input and tries to build a request structure from it -func (b *PowerDnsBackend) parseRequest(input *bufio.Reader) (*backendRequest, error) { - line, _, err := input.ReadLine() - if err != nil { - return nil, err - } - - parts := strings.Split(string(line), "\t") - if len(parts) != 6 { - return nil, errors.New("Invalid line") - } - - return &backendRequest{ - query: parts[0], - queryName: parts[1], - queryClass: parts[2], - queryType: parts[3], - queryId: parts[4], - }, nil -} - -// extractHostname extract the host part of the fqdn: pi.d.example.org -> pi -func (b *PowerDnsBackend) extractHostname(rawQueryName string) (string, error) { - queryName := strings.ToLower(rawQueryName) - - hostname := "" - if strings.HasSuffix(queryName, b.config.Domain) { - hostname = queryName[:len(queryName)-len(b.config.Domain)] - } - - if hostname == "" { - return "", errors.New("Query name does not correspond to our domain") - } - - return hostname, nil -} - -// currentSOASerial get the current SOA serial by returning the current time in seconds -func (b *PowerDnsBackend) currentSOASerial() int64 { - return time.Now().Unix() -} - -type backendRequest struct { - query string - queryName string - queryClass string - queryType string - queryId string -} - -type backendResponse []string - -func newResponse(values ...string) backendResponse { - return values -} - -var ( - handshakeResponse backendResponse = []string{"OK", "DDNS Backend"} - endResponse backendResponse = []string{"END"} - failResponse backendResponse = []string{"FAIL"} -) diff --git a/backend/lookup.go b/backend/lookup.go index 51d8c20..10c5903 100644 --- a/backend/lookup.go +++ b/backend/lookup.go @@ -19,10 +19,10 @@ type Request struct { } type Response struct { - QType string - QName string - Content string - TTL int + QType string `json:"qtype"` + QName string `json:"qname"` + Content string `json:"content"` + TTL int `json:"ttl"` } type HostLookup struct { @@ -30,6 +30,10 @@ type HostLookup struct { hosts hosts.HostBackend } +func NewHostLookup(config *config.Config, hostsBackend hosts.HostBackend) *HostLookup { + return &HostLookup{config, hostsBackend} +} + func (l *HostLookup) Lookup(request *Request) (*Response, error) { responseRecord := request.QType responseContent := "" diff --git a/ddns.go b/ddns.go index 089b05b..10f0abc 100644 --- a/ddns.go +++ b/ddns.go @@ -7,15 +7,9 @@ import ( "github.com/pboehm/ddns/hosts" "github.com/pboehm/ddns/web" "log" - "os" "strings" ) -const ( - CmdBackend string = "backend" - CmdWeb string = "web" -) - var serviceConfig *config.Config = &config.Config{} func init() { @@ -38,11 +32,7 @@ func init() { "Be more verbose") } -func usage() { - log.Fatal("Usage: ./ddns [backend|web]") -} - -func validateCommandArgs(cmd string) { +func validateCommandArgs() { if serviceConfig.Domain == "" { log.Fatal("You have to supply the domain via --domain=DOMAIN") } else if !strings.HasPrefix(serviceConfig.Domain, ".") { @@ -50,39 +40,19 @@ func validateCommandArgs(cmd string) { serviceConfig.Domain = "." + serviceConfig.Domain } - if cmd == CmdBackend { - if serviceConfig.SOAFqdn == "" { - log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") - } + if serviceConfig.SOAFqdn == "" { + log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") } } -func prepareForExecution() string { - flag.Parse() - - if len(flag.Args()) != 1 { - usage() - } - cmd := flag.Args()[0] - - validateCommandArgs(cmd) - return cmd -} - func main() { - cmd := prepareForExecution() + flag.Parse() + validateCommandArgs() redis := hosts.NewRedisBackend(serviceConfig) defer redis.Close() - switch cmd { - case CmdBackend: - backend.NewPowerDnsBackend(serviceConfig, redis, os.Stdin, os.Stdout).Run() + lookup := backend.NewHostLookup(serviceConfig, redis) - case CmdWeb: - web.NewWebService(serviceConfig, redis).Run() - - default: - usage() - } + web.NewWebService(serviceConfig, redis, lookup).Run() } diff --git a/hosts/redis.go b/hosts/redis.go index 1d9dc0c..89d91ab 100644 --- a/hosts/redis.go +++ b/hosts/redis.go @@ -1,6 +1,7 @@ package hosts import ( + "errors" "github.com/garyburd/redigo/redis" "github.com/pboehm/ddns/config" "time" @@ -51,6 +52,10 @@ func (r *RedisBackend) GetHost(name string) (*Host, error) { return nil, err } + if len(data) == 0 { + return nil, errors.New("Host does not exist") + } + if err = redis.ScanStruct(data, &host); err != nil { return nil, err } diff --git a/web/web.go b/web/web.go index 8a35e07..d5e8bb0 100644 --- a/web/web.go +++ b/web/web.go @@ -2,6 +2,7 @@ package web import ( "fmt" + "github.com/pboehm/ddns/backend" "github.com/pboehm/ddns/config" "github.com/pboehm/ddns/hosts" "gopkg.in/gin-gonic/gin.v1" @@ -10,17 +11,20 @@ import ( "net" "net/http" "regexp" + "strings" ) type WebService struct { config *config.Config hosts hosts.HostBackend + lookup *backend.HostLookup } -func NewWebService(config *config.Config, hosts hosts.HostBackend) *WebService { +func NewWebService(config *config.Config, hosts hosts.HostBackend, lookup *backend.HostLookup) *WebService { return &WebService{ config: config, hosts: hosts, + lookup: lookup, } } @@ -37,7 +41,7 @@ func (w *WebService) Run() { if valid { _, err := w.hosts.GetHost(hostname) - valid = err == nil + valid = err != nil } c.JSON(200, gin.H{ @@ -55,9 +59,9 @@ func (w *WebService) Run() { var err error - if _, err = w.hosts.GetHost(hostname); err == nil { + if h, err := w.hosts.GetHost(hostname); err == nil { c.JSON(403, gin.H{ - "error": "This hostname has already been registered.", + "error": fmt.Sprintf("This hostname has already been registered. %v", h), }) return } @@ -122,6 +126,25 @@ func (w *WebService) Run() { }) }) + r.GET("/dnsapi/lookup/:qname/:qtype", func(c *gin.Context) { + request := &backend.Request{ + QName: strings.TrimRight(c.Param("qname"), "."), + QType: c.Param("qtype"), + } + + response, err := w.lookup.Lookup(request) + if err == nil { + c.JSON(200, gin.H{ + "result": []*backend.Response{response}, + }) + } else { + log.Printf("Error during lookup: %v", err) + c.JSON(200, gin.H{ + "result": []string{}, + }) + } + }) + r.Run(w.config.Listen) } From 0acc5bb1e3eb6d98a49a687cc7924a1336ac0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Wed, 29 Nov 2017 22:45:55 +0100 Subject: [PATCH 10/23] signal error when host lookup was not successful --- web/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/web.go b/web/web.go index d5e8bb0..f1f6ff7 100644 --- a/web/web.go +++ b/web/web.go @@ -140,7 +140,7 @@ func (w *WebService) Run() { } else { log.Printf("Error during lookup: %v", err) c.JSON(200, gin.H{ - "result": []string{}, + "result": false, }) } }) From 887600f964510c2a598315d396a43e92b2cd5a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 3 Dec 2017 22:42:25 +0100 Subject: [PATCH 11/23] restructure code into 3 subpackages for backend frontend and shared --- backend/lookup.go | 11 +++++------ backend/lookup_test.go | 15 +++++++-------- ddns.go | 11 +++++------ {web => frontend}/template.go | 2 +- {web => frontend}/web.go | 13 ++++++------- {config => shared}/config.go | 2 +- {hosts => shared}/hosts.go | 2 +- {hosts => shared}/redis.go | 5 ++--- 8 files changed, 28 insertions(+), 33 deletions(-) rename {web => frontend}/template.go (99%) rename {web => frontend}/web.go (92%) rename {config => shared}/config.go (92%) rename {hosts => shared}/hosts.go (97%) rename {hosts => shared}/redis.go (92%) diff --git a/backend/lookup.go b/backend/lookup.go index 10c5903..8c3dfe8 100644 --- a/backend/lookup.go +++ b/backend/lookup.go @@ -3,8 +3,7 @@ package backend import ( "errors" "fmt" - "github.com/pboehm/ddns/config" - "github.com/pboehm/ddns/hosts" + "github.com/pboehm/ddns/shared" "strings" "time" ) @@ -26,11 +25,11 @@ type Response struct { } type HostLookup struct { - config *config.Config - hosts hosts.HostBackend + config *shared.Config + hosts shared.HostBackend } -func NewHostLookup(config *config.Config, hostsBackend hosts.HostBackend) *HostLookup { +func NewHostLookup(config *shared.Config, hostsBackend shared.HostBackend) *HostLookup { return &HostLookup{config, hostsBackend} } @@ -52,7 +51,7 @@ func (l *HostLookup) Lookup(request *Request) (*Response, error) { return nil, err } - var host *hosts.Host + var host *shared.Host if host, err = l.hosts.GetHost(hostname); err != nil { return nil, err } diff --git a/backend/lookup_test.go b/backend/lookup_test.go index 0ec7ed7..7fb52ce 100644 --- a/backend/lookup_test.go +++ b/backend/lookup_test.go @@ -2,17 +2,16 @@ package backend import ( "errors" - c "github.com/pboehm/ddns/config" - h "github.com/pboehm/ddns/hosts" + "github.com/pboehm/ddns/shared" "github.com/stretchr/testify/assert" "testing" ) type testHostBackend struct { - hosts map[string]*h.Host + hosts map[string]*shared.Host } -func (b *testHostBackend) GetHost(hostname string) (*h.Host, error) { +func (b *testHostBackend) GetHost(hostname string) (*shared.Host, error) { host, ok := b.hosts[hostname] if ok { return host, nil @@ -21,20 +20,20 @@ func (b *testHostBackend) GetHost(hostname string) (*h.Host, error) { } } -func (b *testHostBackend) SetHost(host *h.Host) error { +func (b *testHostBackend) SetHost(host *shared.Host) error { b.hosts[host.Hostname] = host return nil } -func buildLookup(domain string) (*c.Config, *testHostBackend, *HostLookup) { - config := &c.Config{ +func buildLookup(domain string) (*shared.Config, *testHostBackend, *HostLookup) { + config := &shared.Config{ Verbose: false, Domain: domain, SOAFqdn: "dns" + domain, } hosts := &testHostBackend{ - hosts: map[string]*h.Host{ + hosts: map[string]*shared.Host{ "www": { Hostname: "www", Ip: "10.11.12.13", diff --git a/ddns.go b/ddns.go index 10f0abc..8d5969c 100644 --- a/ddns.go +++ b/ddns.go @@ -3,14 +3,13 @@ package main import ( "flag" "github.com/pboehm/ddns/backend" - "github.com/pboehm/ddns/config" - "github.com/pboehm/ddns/hosts" - "github.com/pboehm/ddns/web" + "github.com/pboehm/ddns/frontend" + "github.com/pboehm/ddns/shared" "log" "strings" ) -var serviceConfig *config.Config = &config.Config{} +var serviceConfig *shared.Config = &shared.Config{} func init() { flag.StringVar(&serviceConfig.Domain, "domain", "", @@ -49,10 +48,10 @@ func main() { flag.Parse() validateCommandArgs() - redis := hosts.NewRedisBackend(serviceConfig) + redis := shared.NewRedisBackend(serviceConfig) defer redis.Close() lookup := backend.NewHostLookup(serviceConfig, redis) - web.NewWebService(serviceConfig, redis, lookup).Run() + frontend.NewWebService(serviceConfig, redis, lookup).Run() } diff --git a/web/template.go b/frontend/template.go similarity index 99% rename from web/template.go rename to frontend/template.go index 94424a9..8c38d57 100644 --- a/web/template.go +++ b/frontend/template.go @@ -1,4 +1,4 @@ -package web +package frontend const indexTemplate string = ` diff --git a/web/web.go b/frontend/web.go similarity index 92% rename from web/web.go rename to frontend/web.go index f1f6ff7..65c9303 100644 --- a/web/web.go +++ b/frontend/web.go @@ -1,10 +1,9 @@ -package web +package frontend import ( "fmt" "github.com/pboehm/ddns/backend" - "github.com/pboehm/ddns/config" - "github.com/pboehm/ddns/hosts" + "github.com/pboehm/ddns/shared" "gopkg.in/gin-gonic/gin.v1" "html/template" "log" @@ -15,12 +14,12 @@ import ( ) type WebService struct { - config *config.Config - hosts hosts.HostBackend + config *shared.Config + hosts shared.HostBackend lookup *backend.HostLookup } -func NewWebService(config *config.Config, hosts hosts.HostBackend, lookup *backend.HostLookup) *WebService { +func NewWebService(config *shared.Config, hosts shared.HostBackend, lookup *backend.HostLookup) *WebService { return &WebService{ config: config, hosts: hosts, @@ -66,7 +65,7 @@ func (w *WebService) Run() { return } - host := &hosts.Host{Hostname: hostname, Ip: "127.0.0.1"} + host := &shared.Host{Hostname: hostname, Ip: "127.0.0.1"} host.GenerateAndSetToken() if err = w.hosts.SetHost(host); err != nil { diff --git a/config/config.go b/shared/config.go similarity index 92% rename from config/config.go rename to shared/config.go index 2532421..664829a 100644 --- a/config/config.go +++ b/shared/config.go @@ -1,4 +1,4 @@ -package config +package shared type Config struct { Verbose bool diff --git a/hosts/hosts.go b/shared/hosts.go similarity index 97% rename from hosts/hosts.go rename to shared/hosts.go index 6d55d91..bfc3bf3 100644 --- a/hosts/hosts.go +++ b/shared/hosts.go @@ -1,4 +1,4 @@ -package hosts +package shared import ( "crypto/sha1" diff --git a/hosts/redis.go b/shared/redis.go similarity index 92% rename from hosts/redis.go rename to shared/redis.go index 89d91ab..22badd6 100644 --- a/hosts/redis.go +++ b/shared/redis.go @@ -1,9 +1,8 @@ -package hosts +package shared import ( "errors" "github.com/garyburd/redigo/redis" - "github.com/pboehm/ddns/config" "time" ) @@ -12,7 +11,7 @@ type RedisBackend struct { pool *redis.Pool } -func NewRedisBackend(config *config.Config) *RedisBackend { +func NewRedisBackend(config *Config) *RedisBackend { return &RedisBackend{ expirationSeconds: config.HostExpirationDays * 24 * 60 * 60, pool: &redis.Pool{ From b7c07b86a09b4378e35b72b47e91c15a3c2d2aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 3 Dec 2017 23:07:13 +0100 Subject: [PATCH 12/23] move dnsapi from frontend to backend --- backend/backend.go | 45 ++++++++++++++++++++++++++++++++ ddns.go | 29 +++++++++++++++----- frontend/{web.go => frontend.go} | 45 ++++++++------------------------ shared/config.go | 3 ++- 4 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 backend/backend.go rename frontend/{web.go => frontend.go} (73%) diff --git a/backend/backend.go b/backend/backend.go new file mode 100644 index 0000000..dbf9f73 --- /dev/null +++ b/backend/backend.go @@ -0,0 +1,45 @@ +package backend + +import ( + "github.com/pboehm/ddns/shared" + "gopkg.in/gin-gonic/gin.v1" + "log" + "strings" +) + +type Backend struct { + config *shared.Config + lookup *HostLookup +} + +func NewBackend(config *shared.Config, lookup *HostLookup) *Backend { + return &Backend{ + config: config, + lookup: lookup, + } +} + +func (b *Backend) Run() error { + r := gin.Default() + + r.GET("/dnsapi/lookup/:qname/:qtype", func(c *gin.Context) { + request := &Request{ + QName: strings.TrimRight(c.Param("qname"), "."), + QType: c.Param("qtype"), + } + + response, err := b.lookup.Lookup(request) + if err == nil { + c.JSON(200, gin.H{ + "result": []*Response{response}, + }) + } else { + log.Printf("Error during lookup: %v", err) + c.JSON(200, gin.H{ + "result": false, + }) + } + }) + + return r.Run(b.config.BackendListen) +} diff --git a/ddns.go b/ddns.go index 8d5969c..15983aa 100644 --- a/ddns.go +++ b/ddns.go @@ -5,6 +5,7 @@ import ( "github.com/pboehm/ddns/backend" "github.com/pboehm/ddns/frontend" "github.com/pboehm/ddns/shared" + "golang.org/x/sync/errgroup" "log" "strings" ) @@ -15,15 +16,18 @@ func init() { flag.StringVar(&serviceConfig.Domain, "domain", "", "The subdomain which should be handled by DDNS") - flag.StringVar(&serviceConfig.Listen, "listen", ":8080", - "Which socket should the web service use to bind itself") + flag.StringVar(&serviceConfig.SOAFqdn, "soa_fqdn", "", + "The FQDN of the DNS server which is returned as a SOA record") + + flag.StringVar(&serviceConfig.BackendListen, "listen-backend", ":8057", + "Which socket should the backend web service use to bind itself") + + flag.StringVar(&serviceConfig.FrontendListen, "listen-frontend", ":8080", + "Which socket should the frontend web service use to bind itself") flag.StringVar(&serviceConfig.RedisHost, "redis", ":6379", "The Redis socket that should be used") - flag.StringVar(&serviceConfig.SOAFqdn, "soa_fqdn", "", - "The FQDN of the DNS server which is returned as a SOA record") - flag.IntVar(&serviceConfig.HostExpirationDays, "expiration-days", 10, "The number of days after a host is released when it is not updated") @@ -51,7 +55,18 @@ func main() { redis := shared.NewRedisBackend(serviceConfig) defer redis.Close() - lookup := backend.NewHostLookup(serviceConfig, redis) + var group errgroup.Group - frontend.NewWebService(serviceConfig, redis, lookup).Run() + group.Go(func() error { + lookup := backend.NewHostLookup(serviceConfig, redis) + return backend.NewBackend(serviceConfig, lookup).Run() + }) + + group.Go(func() error { + return frontend.NewFrontend(serviceConfig, redis).Run() + }) + + if err := group.Wait(); err != nil { + log.Fatal(err) + } } diff --git a/frontend/web.go b/frontend/frontend.go similarity index 73% rename from frontend/web.go rename to frontend/frontend.go index 65c9303..976948e 100644 --- a/frontend/web.go +++ b/frontend/frontend.go @@ -2,7 +2,6 @@ package frontend import ( "fmt" - "github.com/pboehm/ddns/backend" "github.com/pboehm/ddns/shared" "gopkg.in/gin-gonic/gin.v1" "html/template" @@ -10,36 +9,33 @@ import ( "net" "net/http" "regexp" - "strings" ) -type WebService struct { +type Frontend struct { config *shared.Config hosts shared.HostBackend - lookup *backend.HostLookup } -func NewWebService(config *shared.Config, hosts shared.HostBackend, lookup *backend.HostLookup) *WebService { - return &WebService{ +func NewFrontend(config *shared.Config, hosts shared.HostBackend) *Frontend { + return &Frontend{ config: config, hosts: hosts, - lookup: lookup, } } -func (w *WebService) Run() { +func (f *Frontend) Run() error { r := gin.Default() r.SetHTMLTemplate(buildTemplate()) r.GET("/", func(g *gin.Context) { - g.HTML(200, "index.html", gin.H{"domain": w.config.Domain}) + g.HTML(200, "index.html", gin.H{"domain": f.config.Domain}) }) r.GET("/available/:hostname", func(c *gin.Context) { hostname, valid := isValidHostname(c.Params.ByName("hostname")) if valid { - _, err := w.hosts.GetHost(hostname) + _, err := f.hosts.GetHost(hostname) valid = err != nil } @@ -58,7 +54,7 @@ func (w *WebService) Run() { var err error - if h, err := w.hosts.GetHost(hostname); err == nil { + if h, err := f.hosts.GetHost(hostname); err == nil { c.JSON(403, gin.H{ "error": fmt.Sprintf("This hostname has already been registered. %v", h), }) @@ -68,7 +64,7 @@ func (w *WebService) Run() { host := &shared.Host{Hostname: hostname, Ip: "127.0.0.1"} host.GenerateAndSetToken() - if err = w.hosts.SetHost(host); err != nil { + if err = f.hosts.SetHost(host); err != nil { c.JSON(400, gin.H{"error": "Could not register host."}) return } @@ -89,7 +85,7 @@ func (w *WebService) Run() { return } - host, err := w.hosts.GetHost(hostname) + host, err := f.hosts.GetHost(hostname) if err != nil { c.JSON(404, gin.H{ "error": "This hostname has not been registered or is expired.", @@ -113,7 +109,7 @@ func (w *WebService) Run() { } host.Ip = ip - if err = w.hosts.SetHost(host); err != nil { + if err = f.hosts.SetHost(host); err != nil { c.JSON(400, gin.H{ "error": "Could not update registered IP address", }) @@ -125,26 +121,7 @@ func (w *WebService) Run() { }) }) - r.GET("/dnsapi/lookup/:qname/:qtype", func(c *gin.Context) { - request := &backend.Request{ - QName: strings.TrimRight(c.Param("qname"), "."), - QType: c.Param("qtype"), - } - - response, err := w.lookup.Lookup(request) - if err == nil { - c.JSON(200, gin.H{ - "result": []*backend.Response{response}, - }) - } else { - log.Printf("Error during lookup: %v", err) - c.JSON(200, gin.H{ - "result": false, - }) - } - }) - - r.Run(w.config.Listen) + return r.Run(f.config.FrontendListen) } // Get the Remote Address of the client. At First we try to get the diff --git a/shared/config.go b/shared/config.go index 664829a..42326d6 100644 --- a/shared/config.go +++ b/shared/config.go @@ -5,6 +5,7 @@ type Config struct { Domain string SOAFqdn string HostExpirationDays int - Listen string + FrontendListen string + BackendListen string RedisHost string } From ce032237a8d04d1200e5aabb262b9cf6ad197ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 3 Dec 2017 23:18:47 +0100 Subject: [PATCH 13/23] move config parsing into the shared package --- backend/backend.go | 2 +- ddns.go | 39 ++--------------------------------- frontend/frontend.go | 2 +- shared/config.go | 48 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 50 insertions(+), 41 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index dbf9f73..65c6e10 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -41,5 +41,5 @@ func (b *Backend) Run() error { } }) - return r.Run(b.config.BackendListen) + return r.Run(b.config.ListenBackend) } diff --git a/ddns.go b/ddns.go index 15983aa..ea125a8 100644 --- a/ddns.go +++ b/ddns.go @@ -1,56 +1,21 @@ package main import ( - "flag" "github.com/pboehm/ddns/backend" "github.com/pboehm/ddns/frontend" "github.com/pboehm/ddns/shared" "golang.org/x/sync/errgroup" "log" - "strings" ) var serviceConfig *shared.Config = &shared.Config{} func init() { - flag.StringVar(&serviceConfig.Domain, "domain", "", - "The subdomain which should be handled by DDNS") - - flag.StringVar(&serviceConfig.SOAFqdn, "soa_fqdn", "", - "The FQDN of the DNS server which is returned as a SOA record") - - flag.StringVar(&serviceConfig.BackendListen, "listen-backend", ":8057", - "Which socket should the backend web service use to bind itself") - - flag.StringVar(&serviceConfig.FrontendListen, "listen-frontend", ":8080", - "Which socket should the frontend web service use to bind itself") - - flag.StringVar(&serviceConfig.RedisHost, "redis", ":6379", - "The Redis socket that should be used") - - flag.IntVar(&serviceConfig.HostExpirationDays, "expiration-days", 10, - "The number of days after a host is released when it is not updated") - - flag.BoolVar(&serviceConfig.Verbose, "verbose", false, - "Be more verbose") -} - -func validateCommandArgs() { - if serviceConfig.Domain == "" { - log.Fatal("You have to supply the domain via --domain=DOMAIN") - } else if !strings.HasPrefix(serviceConfig.Domain, ".") { - // get the domain in the right format - serviceConfig.Domain = "." + serviceConfig.Domain - } - - if serviceConfig.SOAFqdn == "" { - log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") - } + serviceConfig.Initialize() } func main() { - flag.Parse() - validateCommandArgs() + serviceConfig.Validate() redis := shared.NewRedisBackend(serviceConfig) defer redis.Close() diff --git a/frontend/frontend.go b/frontend/frontend.go index 976948e..d868e57 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -121,7 +121,7 @@ func (f *Frontend) Run() error { }) }) - return r.Run(f.config.FrontendListen) + return r.Run(f.config.ListenFrontend) } // Get the Remote Address of the client. At First we try to get the diff --git a/shared/config.go b/shared/config.go index 42326d6..72debf1 100644 --- a/shared/config.go +++ b/shared/config.go @@ -1,11 +1,55 @@ package shared +import ( + "flag" + "log" + "strings" +) + type Config struct { Verbose bool Domain string SOAFqdn string HostExpirationDays int - FrontendListen string - BackendListen string + ListenFrontend string + ListenBackend string RedisHost string } + +func (c *Config) Initialize() { + flag.StringVar(&c.Domain, "domain", "", + "The subdomain which should be handled by DDNS") + + flag.StringVar(&c.SOAFqdn, "soa_fqdn", "", + "The FQDN of the DNS server which is returned as a SOA record") + + flag.StringVar(&c.ListenBackend, "listen-backend", ":8057", + "Which socket should the backend web service use to bind itself") + + flag.StringVar(&c.ListenFrontend, "listen-frontend", ":8080", + "Which socket should the frontend web service use to bind itself") + + flag.StringVar(&c.RedisHost, "redis", ":6379", + "The Redis socket that should be used") + + flag.IntVar(&c.HostExpirationDays, "expiration-days", 10, + "The number of days after a host is released when it is not updated") + + flag.BoolVar(&c.Verbose, "verbose", false, + "Be more verbose") +} + +func (c *Config) Validate() { + flag.Parse() + + if c.Domain == "" { + log.Fatal("You have to supply the domain via --domain=DOMAIN") + } else if !strings.HasPrefix(c.Domain, ".") { + // get the domain in the right format + c.Domain = "." + c.Domain + } + + if c.SOAFqdn == "" { + log.Fatal("You have to supply the server FQDN via --soa_fqdn=FQDN") + } +} From e7985a4619aa90e49b49e0a262e4d3297a4e4fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 3 Dec 2017 23:22:29 +0100 Subject: [PATCH 14/23] adjust docker-compose to use seperate backend service --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0685d5d..6e8a41e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: depends_on: - ddns environment: - PDNS_REMOTE_HTTP_HOST: "ddns:8080" + PDNS_REMOTE_HTTP_HOST: "ddns:8057" redis: restart: unless-stopped From de87bee0483bf2cbd977acbe0b4163373767fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sat, 27 Jan 2018 14:38:58 +0100 Subject: [PATCH 15/23] disable dnssec for remote backend --- docker/powerdns/pdns.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/powerdns/pdns.conf b/docker/powerdns/pdns.conf index 0ddb6ec..9d519cc 100644 --- a/docker/powerdns/pdns.conf +++ b/docker/powerdns/pdns.conf @@ -5,4 +5,5 @@ log-dns-details=yes disable-axfr=yes launch=remote +remote-dnssec=no remote-connection-string=http:url=http://{{PDNS_REMOTE_HTTP_HOST}}/dnsapi \ No newline at end of file From fd82de0eaeeba19c93485e7600f7e830816521f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sat, 27 Jan 2018 21:03:46 +0100 Subject: [PATCH 16/23] change dns api listen port to 8053 which is more like the real dns port --- docker/docker-compose.yml | 2 +- shared/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6e8a41e..baf7b45 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: depends_on: - ddns environment: - PDNS_REMOTE_HTTP_HOST: "ddns:8057" + PDNS_REMOTE_HTTP_HOST: "ddns:8053" redis: restart: unless-stopped diff --git a/shared/config.go b/shared/config.go index 72debf1..807e7f7 100644 --- a/shared/config.go +++ b/shared/config.go @@ -23,7 +23,7 @@ func (c *Config) Initialize() { flag.StringVar(&c.SOAFqdn, "soa_fqdn", "", "The FQDN of the DNS server which is returned as a SOA record") - flag.StringVar(&c.ListenBackend, "listen-backend", ":8057", + flag.StringVar(&c.ListenBackend, "listen-backend", ":8053", "Which socket should the backend web service use to bind itself") flag.StringVar(&c.ListenFrontend, "listen-frontend", ":8080", From c842ce6ff7456cea8e4befb95ee82263195dcdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sat, 27 Jan 2018 21:27:50 +0100 Subject: [PATCH 17/23] make redis host configurable through env variable --- docker/ddns/Dockerfile | 2 +- docker/docker-compose.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile index e51e786..d15d6cb 100644 --- a/docker/ddns/Dockerfile +++ b/docker/ddns/Dockerfile @@ -8,4 +8,4 @@ RUN go-wrapper install # "go install -v ." ENV GIN_MODE release -CMD /go/bin/ddns --domain=${DDNS_DOMAIN} --soa_fqdn=${DDNS_SOA_DOMAIN} --redis=redis:6379 web +CMD /go/bin/ddns --domain=${DDNS_DOMAIN} --soa_fqdn=${DDNS_SOA_DOMAIN} --redis=${DDNS_REDIS_HOST} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index baf7b45..0969eb1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,7 +10,8 @@ services: - redis environment: DDNS_DOMAIN: d.example.net - DDNS_SOA_DOMAIN: soa.example.net + DDNS_SOA_DOMAIN: ns.example.net + DDNS_REDIS_HOST: redis:6379 powerdns: restart: unless-stopped From 145a1bd3c5a8e5ff195e88aa04af0d8e95faddc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sat, 27 Jan 2018 22:56:03 +0100 Subject: [PATCH 18/23] add sample for docker-compose.override.yml which also contains caddy for tls --- .gitignore | 2 +- docker/caddy/Caddyfile | 6 +++++ docker/docker-compose.override.yml.sample | 31 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docker/caddy/Caddyfile create mode 100644 docker/docker-compose.override.yml.sample diff --git a/.gitignore b/.gitignore index 00090a6..5ef7cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.swp -/docker/docker-compose.override.yml +/docker/docker-compose.*.yml /ddns dump.rdb diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile new file mode 100644 index 0000000..d25a6c0 --- /dev/null +++ b/docker/caddy/Caddyfile @@ -0,0 +1,6 @@ +{$DDNS_CADDY_DOMAIN} { + tls {$DDNS_CADDY_TLS_EMAIL} + proxy / {$DDNS_FRONTEND_HOST} { + transparent + } +} \ No newline at end of file diff --git a/docker/docker-compose.override.yml.sample b/docker/docker-compose.override.yml.sample new file mode 100644 index 0000000..524ee87 --- /dev/null +++ b/docker/docker-compose.override.yml.sample @@ -0,0 +1,31 @@ +version: '2' + +services: + ddns: + environment: + DDNS_DOMAIN: d.example.net # <<< CHANGE THIS + DDNS_SOA_DOMAIN: ns.example.net # <<< CHANGE THIS + + powerdns: + ports: + - "53/udp:53/udp" + + redis: + volumes: + - "${HOME}/ddns-redis:/data" + + caddy: + restart: unless-stopped + image: abiosoft/caddy:latest + depends_on: + - ddns + environment: + DDNS_FRONTEND_HOST: ddns:8080 + DDNS_CADDY_DOMAIN: ddns.example.net # <<< CHANGE THIS + DDNS_CADDY_TLS_EMAIL: changeme@example.net # <<< CHANGE THIS + volumes: + - "${PWD}/caddy/Caddyfile:/etc/Caddyfile" + - "${HOME}/ddns-caddy:/root/.caddy" + ports: + - "80:80" + - "443:443" From a9167f5d8a91964b0fcb2bffdf86a103f5b48064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 28 Jan 2018 01:49:41 +0100 Subject: [PATCH 19/23] disable logging if not started with --verbose --- backend/backend.go | 12 ++++++++++-- frontend/frontend.go | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 65c6e10..f647838 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -20,7 +20,12 @@ func NewBackend(config *shared.Config, lookup *HostLookup) *Backend { } func (b *Backend) Run() error { - r := gin.Default() + r := gin.New() + r.Use(gin.Recovery()) + + if b.config.Verbose { + r.Use(gin.Logger()) + } r.GET("/dnsapi/lookup/:qname/:qtype", func(c *gin.Context) { request := &Request{ @@ -34,7 +39,10 @@ func (b *Backend) Run() error { "result": []*Response{response}, }) } else { - log.Printf("Error during lookup: %v", err) + if b.config.Verbose { + log.Printf("Error during lookup: %v", err) + } + c.JSON(200, gin.H{ "result": false, }) diff --git a/frontend/frontend.go b/frontend/frontend.go index d868e57..c00af83 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -24,7 +24,13 @@ func NewFrontend(config *shared.Config, hosts shared.HostBackend) *Frontend { } func (f *Frontend) Run() error { - r := gin.Default() + r := gin.New() + r.Use(gin.Recovery()) + + if f.config.Verbose { + r.Use(gin.Logger()) + } + r.SetHTMLTemplate(buildTemplate()) r.GET("/", func(g *gin.Context) { From 25d13229023ef04ef982e263a0042d9aa3545e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 28 Jan 2018 01:50:07 +0100 Subject: [PATCH 20/23] add getDomainMetadata endpoint which returns an error result --- backend/backend.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/backend.go b/backend/backend.go index f647838..15457ab 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -49,5 +49,11 @@ func (b *Backend) Run() error { } }) + r.GET("/dnsapi/getDomainMetadata/:name/:kind", func(c *gin.Context) { + c.JSON(200, gin.H{ + "result": false, + }) + }) + return r.Run(b.config.ListenBackend) } From 365d07dba018c7ae7735b47540a59ef3617cd8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 28 Jan 2018 11:50:24 +0100 Subject: [PATCH 21/23] use a smaller base image for building ddns --- docker/ddns/Dockerfile | 4 +++- docker/powerdns/Dockerfile | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile index d15d6cb..8105f09 100644 --- a/docker/ddns/Dockerfile +++ b/docker/ddns/Dockerfile @@ -1,4 +1,6 @@ -FROM golang:1.9 +FROM golang:1.9-alpine3.7 + +RUN apk add --no-cache git WORKDIR /go/src/github.com/pboehm/ddns COPY . . diff --git a/docker/powerdns/Dockerfile b/docker/powerdns/Dockerfile index e560109..48777e1 100644 --- a/docker/powerdns/Dockerfile +++ b/docker/powerdns/Dockerfile @@ -1,5 +1,4 @@ FROM buildpack-deps:jessie-scm -MAINTAINER Philipp Böhm # the setup procedure according to https://repo.powerdns.com/ (Debian 8 Jessie) RUN echo "deb http://repo.powerdns.com/debian jessie-auth-41 main" > /etc/apt/sources.list.d/pdns.list \ From e4232a31182cb322626d2ecb5f332d9fc374740a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 28 Jan 2018 11:57:56 +0100 Subject: [PATCH 22/23] update year in LICENSE file --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8ad7ff4..38c0e80 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Philipp Böhm +Copyright (c) 2018 Philipp Böhm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 5fdaff0f0e954044544e74777019d69a554bd66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20B=C3=B6hm?= Date: Sun, 28 Jan 2018 15:31:31 +0100 Subject: [PATCH 23/23] change README --- README.md | 143 +++++++++------------- docker/docker-compose.override.yml.sample | 12 +- 2 files changed, 66 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index b24c227..6f6aced 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,86 @@ -ddns -==== +# `ddns` - Dynamic DNS A self-hosted Dynamic DNS solution similar to DynDNS or NO-IP. -You can use a hosted version at [ddns.pboehm.org](http://ddns.pboehm.org/) where you can register a host under the `d.pboehm.de` domain (e.g `test.d.pboehm.de`). +You can use a hosted version at [ddns.pboehm.de](https://ddns.pboehm.de/) where you can register a +host under the `d.pboehm.de` domain (e.g `test.d.pboehm.de`). + +**Recent Changes** + +`ddns` has been massively restructured and refactored and now uses the PowerDNS +[Remote Backend](https://doc.powerdns.com/md/authoritative/backend-remote/) instead +of the [Pipe Backend](https://doc.powerdns.com/md/authoritative/backend-pipe/), which +is far easier to deploy. It now serves both the frontend and the backend other HTTP using different ports. + +The old `ddns` source code can be found at the [legacy](https://github.com/pboehm/ddns/tree/legacy) tag. ## How can I update my IP if it changes? -`ddns` is built around a small webservice, so that you can update your IP address, simply by calling an URL periodically through `curl` (using `cron`). Hosts that haven't been updated for 10 days will be automatically removed. This can be configured in your own instance. +`ddns` is built around a small webservice, so that you can update your IP address simply by calling +an URL periodically through `curl`. Hosts that haven't been updated for 10 days will +be automatically removed. This can be configured in your own instance. -An API similar to DynDNS/NO-IP hasn't been implemented yet. +An API similar to DynDNS/NO-IP has not been implemented yet. ## Self-Hosting ### Requirements -* A custom domain where the registrar allows NS-Records for subdomains -* A global accessible Server running an OS which is supported by the tools listed below -* A running [Redis](http://redis.io) instance for data storage -* An installation of [PowerDNS](https://www.powerdns.com/) with the Pipe-Backend included -* [Go](http://golang.org/) 1.3 +* A custom domain where the registrar allows setting `NS` records for subdomains. This is important because not all + DNS providers support this. +* A server with [docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed +* The following ports should be opened in the firewall: + * `53/udp` + * `80/tcp` + * `443/tcp` -### Installation +### DNS-Setup -The following instructions are valid for Ubuntu/Debian. Some files/packages -could have other names/locations, please search for it. +For the domain you want to use with `ddns` (`example.net` in the following sections, please adjust this to your domain) +you have to create the following two DNS records: -You should have a working Go environment (correct `$GOPATH`). +* `ddns.example.net` as a `CNAME` or `A`/`AAAA` record pointing to the server `ddns` will be running on. This record + will be used for accessing the `ddns` frontend in your browser or via `curl`. It is also the target for the + corresponding `NS` record. +* `d.example.net` as an `NS` record pointing to the previously created `ddns.example.net` record. This will delegate + all subdomains under `d.example.net` to the PowerDNS server running on `ddns.example.net`. - $ go version # check that you have go 1.3 installed - go version go1.3 linux/amd64 +### `ddns`-Setup -Then install `ddns` via: +Setting up `ddns` was kind of a hassle in the legacy version, because there are multiple components that have to +work together: - $ go get github.com/pboehm/ddns - $ ls $GOPATH/bin/ddns # the displayed path will be used later - /home/user/gocode/bin/ddns +* `ddns` that runs the frontend and provides and provides an API compatible with the + [Remote Backend](https://doc.powerdns.com/md/authoritative/backend-remote/) +* Redis as storage backend for `ddns` +* PowerDNS as DNS server, which uses the `ddns` backend API on Port `8053` +* A web server that makes the `ddns` frontend accessible to the Internet through HTTPS -#### Backend +The setup is now automated using [docker-compose](https://docs.docker.com/compose/) and only some customization has +to be made in a `docker-compose.override.yml` file. -Install `pdns` and `redis-server`: +#### Configuring the Setup - $ sudo apt-get install redis-server pdns-server pdns-backend-pipe +The setup included in this repository contains all the components described above and uses +[caddy](https://caddyserver.com/) as a web server, because it provides automatic HTTPS using Lets Encrypt. -Both services should start at boot automatically. You should open `udp/53` and -`tcp/53` on your Firewall so that `pdns` can be be used from outside of your -host. +``` +git clone git@github.com:pboehm/ddns.git +cd ddns/docker +cp docker-compose.override.yml.sample docker-compose.override.yml +``` - $ sudo vim /etc/powerdns/pdns.d/pipe.conf +Please adjust the settings in `docker-compose.override.yml` marked with the `#<<< ....` comments as follows: -`pipe.conf` should have the following content. Please adjust the path of `ddns` -and the values supplied to `--domain` and `--soa_fqdn`: +* adjust the domain part in lines marked with `# <<< ADJUST DOMAIN` according to your DNS-Setup +* insert your email address in lines marked with `# <<< INSERT EMAIL` which is required for getting certificates + from Lets Encrypt +* adjust the path component before the `:` in lines marked with `# <<< ADJUST LOCAL PATH` if the shown path + does not meet your requirements - launch=pipe - pipebackend-abi-version=1 - pipe-command=/home/user/gocode/bin/ddns --soa_fqdn=dns.example.com --domain=sub.example.com backend +Finally execute the following `docker-compose` command, which creates 4 containers in detached mode which are also +started automatically after reboot. -Then restart `pdns`: - - $ sudo service pdns restart - -#### Frontend - -`ddns` includes a webservice which is used for creating new hosts and updating -ip addresses. I prefer using `nginx` as a reverse proxy and not running `ddns` -on port 80. As a process manager, I prefer using `supervisord` so it is -described here. - - $ sudo apt-get install nginx supervisor - -Create a supervisor config file for ddns: - - $ sudo vim /etc/supervisor/conf.d/ddns.conf - $ cat /etc/supervisor/conf.d/ddns.conf - [program:ddns] - directory = /tmp/ - user = user - command = /home/user/gocode/bin/ddns --domain=sub.example.com web - autostart = True - autorestart = True - redirect_stderr = True - -Restart the `supervisor` daemon and `ddns` listens on Port 8080 (can be -changed by adding `--listen=:1234`). - - $ sudo service supervisor restart - -Now you have to add a nginx virtual host for `ddns`: - - $ sudo vim /etc/nginx/sites-enabled/default - $ cat /etc/nginx/sites-enabled/default - server { - listen 80; - server_name ddns.example.com; - - location / { - proxy_pass http://127.0.0.1:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; - proxy_connect_timeout 300; - } - } - -Please adjust the `server_name` with a valid FQDN. Now we only have to restart -`nginx`: - - $ sudo service nginx restart +``` +docker-compose --project-name ddns up -d +``` diff --git a/docker/docker-compose.override.yml.sample b/docker/docker-compose.override.yml.sample index 524ee87..6124c58 100644 --- a/docker/docker-compose.override.yml.sample +++ b/docker/docker-compose.override.yml.sample @@ -3,8 +3,8 @@ version: '2' services: ddns: environment: - DDNS_DOMAIN: d.example.net # <<< CHANGE THIS - DDNS_SOA_DOMAIN: ns.example.net # <<< CHANGE THIS + DDNS_DOMAIN: d.example.net # <<< ADJUST DOMAIN + DDNS_SOA_DOMAIN: ddns.example.net # <<< ADJUST DOMAIN powerdns: ports: @@ -12,7 +12,7 @@ services: redis: volumes: - - "${HOME}/ddns-redis:/data" + - "/root/ddns-redis:/data" # <<< ADJUST LOCAL PATH caddy: restart: unless-stopped @@ -21,11 +21,11 @@ services: - ddns environment: DDNS_FRONTEND_HOST: ddns:8080 - DDNS_CADDY_DOMAIN: ddns.example.net # <<< CHANGE THIS - DDNS_CADDY_TLS_EMAIL: changeme@example.net # <<< CHANGE THIS + DDNS_CADDY_DOMAIN: ddns.example.net # <<< ADJUST DOMAIN + DDNS_CADDY_TLS_EMAIL: changeme@example.net # <<< INSERT EMAIL volumes: - "${PWD}/caddy/Caddyfile:/etc/Caddyfile" - - "${HOME}/ddns-caddy:/root/.caddy" + - "/root/ddns-caddy:/root/.caddy" # <<< ADJUST LOCAL PATH ports: - "80:80" - "443:443"