From 473ec1c19bb9d8cad259481f5cd2096a47dfb40f Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Thu, 2 Jan 2020 03:05:21 +0000 Subject: Create updated service --- bind/config.go | 227 ++++++++++++++++++++++++++++ dns/cilent.go | 69 +++++++++ dns/types.go | 294 ++++++++++++++++++++++++++++++++++++ go.mod | 8 + go.sum | 55 +++++++ main.go | 466 +++++++++++++++++++++++++++++++++++++-------------------- 6 files changed, 955 insertions(+), 164 deletions(-) create mode 100644 bind/config.go create mode 100644 dns/cilent.go create mode 100644 dns/types.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/bind/config.go b/bind/config.go new file mode 100644 index 0000000..584b1ee --- /dev/null +++ b/bind/config.go @@ -0,0 +1,227 @@ +package bind + +import ( + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/miekg/dns" +) + +type Signable interface { + SetTsig(string, string, uint16, int64) *dns.Msg +} + +func NewBINDConfig() *BINDConfig { + return &BINDConfig{ + zones: map[string]map[string]*Zone{}, + keys: map[string]*Key{}, + } +} + +type BINDConfig struct { + zones map[string]map[string]*Zone + keys map[string]*Key +} + +func (c *BINDConfig) Views() []string { + v := []string{} + for vn, _ := range c.zones { + v = append(v, vn) + } + return v +} + +func (c *BINDConfig) Zone(view, name string) *Zone { + if !strings.HasSuffix(name, ".") { + name = fmt.Sprintf("%s.", name) + } + + if _, ok := c.zones[view]; !ok { + return nil + } + + z, ok := c.zones[view][name] + if !ok { + return nil + } + + if z.InView != "" { + return c.zones[z.InView][name] + } + + return z +} + +func (c *BINDConfig) AddKey(k *Key) { + c.keys[k.Name] = k +} + +func (c *BINDConfig) AddZone(z *Zone, view string) { + if c.zones[view] == nil { + c.zones[view] = map[string]*Zone{} + } + c.zones[view][z.Name] = z + z.config = c +} + +func NewZone(name string) *Zone { + // Canonicalize name + if !strings.HasSuffix(name, ".") { + name = fmt.Sprintf("%s.", name) + } + + return &Zone{ + Name: name, + keys: []string{}, + } +} + +type Zone struct { + Name string + Type string + InView string + keys []string + config *BINDConfig +} + +func (z *Zone) AddKey(key string) { + z.keys = append(z.keys, key) +} + +func (z *Zone) Keys() []*Key { + k := []*Key{} + for _, kn := range z.keys { + k = append(k, z.config.keys[kn]) + } + return k +} + +type Key struct { + Name string + Algorithm string + Secret string +} + +func (k *Key) CanonicalName() string { + if !strings.HasSuffix(k.Name, ".") { + return fmt.Sprintf("%s.", k.Name) + } + return k.Name +} + +func (k *Key) CanonicalAlgorithm() string { + if !strings.HasSuffix(k.Algorithm, ".") { + return fmt.Sprintf("%s.", k.Algorithm) + } + return k.Name +} + +func (k *Key) Sign(r Signable) { + r.SetTsig(k.CanonicalName(), k.CanonicalAlgorithm(), 300, time.Now().Unix()) +} + +func (k *Key) AsMap() map[string]string { + return map[string]string{k.CanonicalName(): k.Secret} +} + +func NewStringStack() *StringStack { + return &StringStack{[]string{}} +} + +type StringStack struct { + items []string +} + +func (s *StringStack) isEmpty() bool { + return len(s.items) == 0 +} + +func (s *StringStack) Peek() string { + return s.items[len(s.items)-1] +} + +func (s *StringStack) Pop() string { + if s.isEmpty() { + return "" + } + v := s.items[len(s.items)-1] + s.items = s.items[:len(s.items)-1] + return v +} + +func (s *StringStack) Push(v string) { + s.items = append(s.items, v) +} + +func cleanValue(s string) string { + return strings.Trim(strings.Trim(s, ";"), "\"") +} + +func isContainerStart(line []string) bool { + return line[len(line)-1] == "{" +} + +func ParseBINDConfig(filename string) (*BINDConfig, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var view string + var zone *Zone + var key *Key + + config := NewBINDConfig() + stack := NewStringStack() + + for _, line := range strings.Split(string(data), "\n") { + line := strings.Split(strings.TrimSpace(line), " ") + + switch line[0] { + case "};": + t := stack.Pop() + switch t { + case "view": + view = "" + case "key": + key = nil + case "zone": + zone = nil + } + case "view": + view = cleanValue(line[1]) + stack.Push("view") + case "zone": + zone = NewZone(cleanValue(line[1])) + config.AddZone(zone, view) + stack.Push("zone") + case "in-view": + zone.Type = "reference" + zone.InView = cleanValue(line[1]) + case "type": + zone.Type = cleanValue(line[1]) + case "grant": + zone.AddKey(cleanValue(line[1])) + case "key": + if !isContainerStart(line) { + continue + } + + key = &Key{Name: cleanValue(line[1])} + config.AddKey(key) + stack.Push("key") + case "algorithm": + key.Algorithm = cleanValue(line[1]) + case "secret": + key.Secret = cleanValue(line[1]) + default: + if isContainerStart(line) { + stack.Push(line[0]) + } + } + } + + return config, nil +} diff --git a/dns/cilent.go b/dns/cilent.go new file mode 100644 index 0000000..63e3734 --- /dev/null +++ b/dns/cilent.go @@ -0,0 +1,69 @@ +package dns + +import ( + "code.crute.me/mcrute/go_ddns_manager/bind" + "github.com/miekg/dns" +) + +type DNSClient struct { + Server string +} + +func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) { + k := zone.Keys()[0] + t := &dns.Transfer{TsigSecret: k.AsMap()} + + m := &dns.Msg{} + m.SetAxfr(zone.Name) + k.Sign(m) + + return t.In(m, c.Server) +} + +func (c *DNSClient) Insert(zone *bind.Zone, rrs ...RR) error { + k := zone.Keys()[0] + dc := &dns.Client{TsigSecret: k.AsMap()} + + m := &dns.Msg{} + m.SetUpdate(zone.Name) + m.Insert(toRRSet(zone, rrs...)) + k.Sign(m) + + if _, _, err := dc.Exchange(m, c.Server); err != nil { + return err + } + + return nil +} + +func (c *DNSClient) Remove(zone *bind.Zone, rrs ...RR) error { + k := zone.Keys()[0] + dc := &dns.Client{TsigSecret: k.AsMap()} + + m := &dns.Msg{} + m.SetUpdate(zone.Name) + m.Remove(toRRSet(zone, rrs...)) + k.Sign(m) + + if _, _, err := dc.Exchange(m, c.Server); err != nil { + return err + } + + return nil +} + +func (c *DNSClient) RemoveAll(zone *bind.Zone, rrs ...RR) error { + k := zone.Keys()[0] + dc := &dns.Client{TsigSecret: k.AsMap()} + + m := &dns.Msg{} + m.SetUpdate(zone.Name) + m.RemoveRRset(toRRSet(zone, rrs...)) + k.Sign(m) + + if _, _, err := dc.Exchange(m, c.Server); err != nil { + return err + } + + return nil +} diff --git a/dns/types.go b/dns/types.go new file mode 100644 index 0000000..8f840be --- /dev/null +++ b/dns/types.go @@ -0,0 +1,294 @@ +package dns + +import ( + "fmt" + "net" + + "code.crute.me/mcrute/go_ddns_manager/bind" + "github.com/miekg/dns" +) + +func makeHeader(name string, zone *bind.Zone, t uint16, ttl int) dns.RR_Header { + return dns.RR_Header{ + Name: fmt.Sprintf("%s.%s", name, zone.Name), + Rrtype: t, + Class: dns.ClassINET, + Ttl: uint32(ttl), + } +} + +func toRRSet(z *bind.Zone, rr ...RR) []dns.RR { + o := []dns.RR{} + for _, v := range rr { + o = append(o, v.ToDNS(z)) + } + return o +} + +type RR interface { + ToDNS(*bind.Zone) dns.RR +} + +type A struct { + Name string + Ttl int + A net.IP +} + +func (r *A) ToDNS(zone *bind.Zone) dns.RR { + return &dns.A{ + Hdr: makeHeader(r.Name, zone, dns.TypeA, r.Ttl), + A: r.A, + } +} + +type AAAA struct { + Name string + Ttl int + AAAA net.IP +} + +func (r *AAAA) ToDNS(zone *bind.Zone) dns.RR { + return &dns.AAAA{ + Hdr: makeHeader(r.Name, zone, dns.TypeAAAA, r.Ttl), + AAAA: r.AAAA, + } +} + +type CAA struct { + Name string + Ttl int + Flag uint8 + Tag string + Value string +} + +func (r *CAA) ToDNS(zone *bind.Zone) dns.RR { + return &dns.CAA{ + Hdr: makeHeader(r.Name, zone, dns.TypeCAA, r.Ttl), + Flag: r.Flag, + Tag: r.Tag, + Value: r.Value, + } +} + +type CERT struct { + Name string + Ttl int + Type uint16 + KeyTag uint16 + Algorithm uint8 + Certificate string +} + +func (r *CERT) ToDNS(zone *bind.Zone) dns.RR { + return &dns.CERT{ + Hdr: makeHeader(r.Name, zone, dns.TypeCERT, r.Ttl), + Type: r.Type, + KeyTag: r.KeyTag, + Algorithm: r.Algorithm, + Certificate: r.Certificate, + } +} + +type CNAME struct { + Name string + Ttl int + Target string +} + +func (r *CNAME) ToDNS(zone *bind.Zone) dns.RR { + return &dns.CNAME{ + Hdr: makeHeader(r.Name, zone, dns.TypeCNAME, r.Ttl), + Target: r.Target, + } +} + +type DNAME struct { + Name string + Ttl int + Target string +} + +func (r *DNAME) ToDNS(zone *bind.Zone) dns.RR { + return &dns.DNAME{ + Hdr: makeHeader(r.Name, zone, dns.TypeDNAME, r.Ttl), + Target: r.Target, + } +} + +type LOC struct { + Name string + Ttl int + Version uint8 + Size uint8 + HorizPre uint8 + VertPre uint8 + Latitude uint32 + Longitude uint32 + Altitude uint32 +} + +func (r *LOC) ToDNS(zone *bind.Zone) dns.RR { + return &dns.LOC{ + Hdr: makeHeader(r.Name, zone, dns.TypeLOC, r.Ttl), + Version: r.Version, + Size: r.Size, + HorizPre: r.HorizPre, + VertPre: r.VertPre, + Latitude: r.Latitude, + Longitude: r.Longitude, + Altitude: r.Altitude, + } +} + +type MX struct { + Name string + Ttl int + Preference uint16 + Mx string +} + +func (r *MX) ToDNS(zone *bind.Zone) dns.RR { + return &dns.MX{ + Hdr: makeHeader(r.Name, zone, dns.TypeMX, r.Ttl), + Preference: r.Preference, + Mx: r.Mx, + } +} + +type NAPTR struct { + Name string + Ttl int + Order uint16 + Preference uint16 + Flags string + Service string + Regexp string + Replacement string +} + +func (r *NAPTR) ToDNS(zone *bind.Zone) dns.RR { + return &dns.NAPTR{ + Hdr: makeHeader(r.Name, zone, dns.TypeNAPTR, r.Ttl), + Order: r.Order, + Preference: r.Preference, + Flags: r.Flags, + Service: r.Service, + Regexp: r.Regexp, + Replacement: r.Replacement, + } +} + +type NS struct { + Name string + Ttl int + Ns string +} + +func (r *NS) ToDNS(zone *bind.Zone) dns.RR { + return &dns.NS{ + Hdr: makeHeader(r.Name, zone, dns.TypeNS, r.Ttl), + Ns: r.Ns, + } +} + +type OPENPGPKEY struct { + Name string + Ttl int + PublicKey string +} + +func (r *OPENPGPKEY) ToDNS(zone *bind.Zone) dns.RR { + return &dns.OPENPGPKEY{ + Hdr: makeHeader(r.Name, zone, dns.TypeOPENPGPKEY, r.Ttl), + PublicKey: r.PublicKey, + } +} + +type PTR struct { + Name string + Ttl int + Ptr string +} + +func (r *PTR) ToDNS(zone *bind.Zone) dns.RR { + return &dns.PTR{ + Hdr: makeHeader(r.Name, zone, dns.TypePTR, r.Ttl), + Ptr: r.Ptr, + } +} + +type SOA struct { + Name string + Ttl int + Ns string + Mbox string + Serial uint32 + Refresh uint32 + Retry uint32 + Expire uint32 + Minttl uint32 +} + +func (r *SOA) ToDNS(zone *bind.Zone) dns.RR { + return &dns.SOA{ + Hdr: makeHeader(r.Name, zone, dns.TypeSOA, r.Ttl), + Ns: r.Ns, + Mbox: r.Mbox, + Serial: r.Serial, + Refresh: r.Refresh, + Retry: r.Retry, + Expire: r.Expire, + Minttl: r.Minttl, + } +} + +type SRV struct { + Name string + Ttl int + Priority uint16 + Weight uint16 + Port uint16 + Target string +} + +func (r *SRV) ToDNS(zone *bind.Zone) dns.RR { + return &dns.SRV{ + Hdr: makeHeader(r.Name, zone, dns.TypeSRV, r.Ttl), + Priority: r.Priority, + Weight: r.Weight, + Port: r.Port, + Target: r.Target, + } +} + +type SSHFP struct { + Name string + Ttl int + Algorithm uint8 + Type uint8 + FingerPrint string +} + +func (r *SSHFP) ToDNS(zone *bind.Zone) dns.RR { + return &dns.SSHFP{ + Hdr: makeHeader(r.Name, zone, dns.TypeSSHFP, r.Ttl), + Algorithm: r.Algorithm, + Type: r.Type, + FingerPrint: r.FingerPrint, + } +} + +type TXT struct { + Name string + Ttl int + Txt []string +} + +func (r *TXT) ToDNS(zone *bind.Zone) dns.RR { + return &dns.TXT{ + Hdr: makeHeader(r.Name, zone, dns.TypeTXT, r.Ttl), + Txt: r.Txt, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1fd977c --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module code.crute.me/mcrute/go_ddns_manager + +go 1.13 + +require ( + github.com/gin-gonic/gin v1.5.0 + github.com/miekg/dns v1.1.26 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f2c074 --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 2c99e0e..d883754 100644 --- a/main.go +++ b/main.go @@ -1,236 +1,374 @@ package main import ( - "bytes" + "encoding/base64" "encoding/json" - "errors" "fmt" - "github.com/miekg/dns" "io/ioutil" + "log" + "net" "net/http" + "net/url" + "regexp" "strings" - "text/template" - "time" + + "code.crute.me/mcrute/go_ddns_manager/bind" + "code.crute.me/mcrute/go_ddns_manager/dns" + "github.com/gin-gonic/gin" +) + +const ( + ACME_AUTH_KEY = "ACMEAuthContext" + DDNS_AUTH_KEY = "DDNSAuthZone" ) -type Zone struct { - Name string - View string +var ( + cfg *bind.BINDConfig + secrets *Secrets + ipRegexp = regexp.MustCompile(`(?:\[([0-9a-f:]+)\]|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})):\d+`) +) + +func init() { + var err error + cfg, err = bind.ParseBINDConfig("zones.conf") + if err != nil { + panic(err) + } + + fd, err := ioutil.ReadFile("secrets.json") + if err != nil { + panic(err) + } + + secrets = &Secrets{} + if err = json.Unmarshal(fd, secrets); err != nil { + panic(err) + } +} + +type Secrets struct { + DDNS map[string]string + ACME map[string]map[string]int +} + +type DDNSUpdateRequest struct { + Key string `form:"key" binding:"required"` } -type TSIGSecrets struct { - secrets map[string]map[string]TSIGSecret - viewzones map[string][]string +type ACMEChallenge struct { + Zone string `json:"zone" binding:"required"` + Challenge string `json:"challenge" binding:"required"` } -func NewTSIGSecrets() *TSIGSecrets { - return &TSIGSecrets{ - secrets: make(map[string]map[string]TSIGSecret), - viewzones: make(map[string][]string), +type ACMEChallengeID struct { + Zone string + Prefix string + Challenge string +} + +func joinDomainParts(parts ...string) string { + p := []string{} + for _, i := range parts { + if strings.TrimSpace(i) != "" { + p = append(p, i) + } } + return strings.Join(p, ".") } -func (t *TSIGSecrets) GetViews() []string { - r := make([]string, 0, len(t.viewzones)) - for k := range t.viewzones { - r = append(r, k) +// Find the closest zone that we manage by striping dotted components off the +// front of the domain until one matches. If there is a match return the zone +// that matched and any prefix components, if any, as a dotted string. If none +// match then return nil. +func findClosestZone(cfg *bind.BINDConfig, zoneIn, view string) (*bind.Zone, string) { + suffix := "" + prefix := []string{} + + zc := strings.Split(zoneIn, ".") + for i := 0; i <= len(zc)-2; i++ { + prefix, suffix = zc[:i], strings.Join(zc[i:], ".") + if zone := cfg.Zone(view, suffix); zone != nil { + return zone, strings.Join(prefix, ".") + } } - return r + + return nil, "" } -func (t *TSIGSecrets) GetViewZones() map[string][]string { - return t.viewzones +func makeURL(r *http.Request, path string, subs ...interface{}) *url.URL { + scheme := "https" + + if r.TLS == nil { + scheme = "http" + } + + // Always defer to whatever the proxy told us it was doing because this + // could be a mullet-VIP in either direction. + if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" { + scheme = fwProto + } + + return &url.URL{ + Scheme: scheme, + Host: r.Host, + Path: fmt.Sprintf(path, subs...), + } } -func (t *TSIGSecrets) UnmarshalJSON(d []byte) error { - v := make(map[string]map[string]string) +func createAcmeChallenge(c *gin.Context) { + dc := dns.DNSClient{Server: "172.16.18.52:53"} - if err := json.Unmarshal(d, &v); err != nil { - return err + var ch ACMEChallenge + if err := c.ShouldBindJSON(&ch); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return } - for k, v := range v { - o := strings.Split(k, "-") - view := o[len(o)-1] + zone, prefix := findClosestZone(cfg, ch.Zone, "external") + if zone == nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Zone not found", + }) + return + } - d := make([]string, len(o)) - copy(d, o[:len(o)-1]) - dn := strings.Join(d, ".") + if v, ok := c.Get(ACME_AUTH_KEY); !ok || v.(map[string]int)[zone.Name] != 1 { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Zone update not allowed", + }) + return + } - a, ok := v["algorithm"] - if !ok { - a = "hmac-sha256." - } + // Do this first, in-case it fails (even though it should never fail) + id, err := json.Marshal(ACMEChallengeID{ + Zone: zone.Name, + Prefix: prefix, + Challenge: ch.Challenge, + }) + if err != nil { + log.Printf("error: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "error encoding ID", + }) + return + } - if !strings.HasSuffix(a, ".") { - a = fmt.Sprintf("%s.", a) - } + url := makeURL(c.Request, "/acme/%s", base64.URLEncoding.EncodeToString(id)) - if _, ok := t.viewzones[view]; !ok { - t.viewzones[view] = make([]string, 1) - t.viewzones[view] = append(t.viewzones[view], dn) - } else { - t.viewzones[view] = append(t.viewzones[view], dn) - } + t := &dns.TXT{ + Name: joinDomainParts("_acme-challenge", prefix), + Ttl: 5, + Txt: []string{ch.Challenge}, + } - if _, ok := t.secrets[view]; !ok { - t.secrets[view] = make(map[string]TSIGSecret) - } + log.Printf("%+v %+v '%s'", zone, t, t.Name) - if !strings.HasSuffix(k, ".") { - k = fmt.Sprintf("%s.", k) - } + // Cleanup any old challenges before adding a new one + if err := dc.RemoveAll(zone, t); err != nil { + log.Printf("error RemoveAll: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } - t.secrets[view][dn] = TSIGSecret{ - KeyName: k, - Algorithm: a, - Secret: v["secret"], - } + if err := dc.Insert(zone, t); err != nil { + log.Printf("error Insert: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return } - return nil + c.Writer.Header()["Location"] = []string{url.String()} + c.JSON(http.StatusCreated, gin.H{ + "created": url.String(), + }) } -func (t *TSIGSecrets) GetSecret(zone, view string) (*TSIGSecret, error) { - if !strings.HasSuffix(zone, ".") { - zone = fmt.Sprintf("%s.", zone) +func deleteAcmeChallenge(c *gin.Context) { + dc := dns.DNSClient{Server: "172.16.18.52:53"} + + rid, err := base64.URLEncoding.DecodeString(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "unable to decode ID", + }) + return } - if _, ok := t.secrets[view]; !ok { - return nil, errors.New("No keys for requested zone") + var id ACMEChallengeID + if err = json.Unmarshal(rid, &id); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "unable to decode ID", + }) + return } - key, ok := t.secrets[view][zone] - if !ok { - return nil, errors.New("No keys for requested view of zone") + zone := cfg.Zone("external", id.Zone) + if zone == nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Zone not found", + }) + return } - return &key, nil -} + if v, ok := c.Get(ACME_AUTH_KEY); !ok || v.(map[string]int)[zone.Name] != 1 { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Zone update not allowed", + }) + return + } -type Signable interface { - SetTsig(string, string, uint16, int64) *dns.Msg -} + t := &dns.TXT{ + Name: joinDomainParts("_acme-challenge", id.Prefix), + Ttl: 5, + Txt: []string{id.Challenge}, + } -// TODO: Name and Algorithm end with dot (.) -type TSIGSecret struct { - KeyName string - Algorithm string `json:"algorithm"` - Secret string `json:"secret"` -} + if err := dc.Remove(zone, t); err != nil { + log.Printf("error Remove: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } -func (t *TSIGSecret) Sign(r Signable) { - r.SetTsig(t.KeyName, t.Algorithm, 300, time.Now().Unix()) + c.JSON(http.StatusNoContent, gin.H{}) } -func (t *TSIGSecret) AsMap() map[string]string { - return map[string]string{ - t.KeyName: t.Secret, +func updateDynamicDNS(c *gin.Context) { + dc := dns.DNSClient{Server: "172.16.18.52:53"} + + res, ok := c.GetString(DDNS_AUTH_KEY) + if !ok { + log.Println("ddns: Unable to get auth key") + c.AbortWithStatus(http.StatusForbidden) + return } -} -func getValue(v interface{}) string { - switch i := v.(type) { - case *dns.SOA: - return fmt.Sprintf("%s %s %d %d %d %d %d", i.Ns, i.Mbox, i.Serial, i.Refresh, i.Retry, i.Expire, i.Minttl) - case *dns.A: - return fmt.Sprintf("%s", i.A) - case *dns.CNAME: - return fmt.Sprintf("%s", i.Target) - case *dns.AAAA: - return fmt.Sprintf("%s", i.AAAA) - case *dns.MX: - return fmt.Sprintf("%d %s", i.Preference, i.Mx) - case *dns.TXT: - b := &bytes.Buffer{} - for _, t := range i.Txt { - fmt.Fprintf(b, "\"%s\"", t) // [] - } - return b.String() - case *dns.PTR: - return fmt.Sprintf("%s", i.Ptr) - case *dns.NS: - return fmt.Sprintf("%s", i.Ns) - case *dns.SRV: - return fmt.Sprintf("%d %d %d %s", i.Priority, i.Weight, i.Port, i.Target) - case *dns.SPF: - b := &bytes.Buffer{} - for _, t := range i.Txt { - fmt.Fprintf(b, "\"%s\"", t) // [] - } - return b.String() - default: - return "UNKNOWN" + zone, part := findClosestZone(cfg, res, "external") + if zone == nil { + log.Println("ddns: Unable to locate zone") + c.AbortWithStatus(http.StatusNotFound) + return + } + + inip := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) + xff := c.Request.Header.Get("X-Forwarded-For") + if xff != "" { + inip = net.ParseIP(xff) } -} -func getDns(sm *TSIGSecrets) chan *dns.Envelope { - s, _ := sm.GetSecret("crute.us", "external") + if inip == nil { + log.Println("ddns: Unable to parse IP") + c.AbortWithStatus(http.StatusInternalServerError) + return + } - c := &dns.Transfer{} - c.TsigSecret = s.AsMap() + t := &dns.A{ + Name: part, + Ttl: 60, + A: inip, + } - m := &dns.Msg{} - m.SetAxfr("crute.us.") - s.Sign(m) + // Cleanup any old records before adding the new one + if err := dc.RemoveAll(zone, t); err != nil { + log.Printf("ddns RemoveAll: %s", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } - in, err := c.In(m, "172.16.18.52:53") - if err != nil { - fmt.Printf("Error: %s\n", err.Error()) - return nil + if err := dc.Insert(zone, t); err != nil { + log.Printf("ddns Insert: %s", err) + c.AbortWithStatus(http.StatusInternalServerError) + return } - return in + c.String(http.StatusAccepted, "") } -func dnsClass(rrh *dns.RR_Header) string { - return dns.Class(rrh.Class).String() -} +func reflectIP(c *gin.Context) { + myIp := c.Request.RemoteAddr + xff := c.Request.Header.Get("X-Forwarded-For") + if xff != "" { + myIp = xff + } + + ips := ipRegexp.FindStringSubmatch(myIp) + if ips == nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } -func dnsType(rrh *dns.RR_Header) string { - return dns.Type(rrh.Rrtype).String() + v6, v4 := ips[1], ips[2] + if v6 != "" { + c.String(http.StatusOK, v6) + } else if v4 != "" { + c.String(http.StatusOK, v4) + } else { + c.AbortWithStatus(http.StatusInternalServerError) + } } -func dnsTTL(rrh *dns.RR_Header) string { - t := rrh.Ttl +func acmeAuth(c *gin.Context) { + _, pwd, ok := c.Request.BasicAuth() + if !ok { + c.Request.Header["WWW-Authenticate"] = []string{`Basic realm="closed site"`} + c.AbortWithStatus(http.StatusUnauthorized) + return + } - if t/86400 > 1 { - return fmt.Sprintf("%d days", t/86400) - } else if t/3600 > 1 { - return fmt.Sprintf("%d hours", t/3600) - } else if t/60 > 1 { - return fmt.Sprintf("%d minutes", t/60) + allowed, ok := secrets.ACME[pwd] + if !ok { + c.AbortWithStatus(http.StatusForbidden) + return } else { - return fmt.Sprintf("%d seconds", t) + c.Set(ACME_AUTH_KEY, allowed) } + + c.Next() } -func handler(w http.ResponseWriter, r *http.Request) { - fm := template.FuncMap{ - "getValue": getValue, - "dnsClass": dnsClass, - "dnsType": dnsType, - "dnsTTL": dnsTTL, +func ddnsAuth(c *gin.Context) { + var req DDNSUpdateRequest + if err := c.ShouldBind(&req); err != nil { + log.Println("ddnsAuth: No key in request") + c.AbortWithStatus(http.StatusNotFound) + return + } + + res, ok := secrets.DDNS[req.Key] + if !ok { + log.Println("ddnsAuth: Unknown secret") + c.AbortWithStatus(http.StatusNotFound) + return + } else { + c.Set(DDNS_AUTH_KEY, res) } - t, _ := template.New("").Funcs(fm).ParseFiles("dns.html") - t.ExecuteTemplate(w, "dns.html", getDns(nil)) + + c.Next() } func main() { - /* - http.HandleFunc("/", handler) - http.ListenAndServe(":8080", nil) - */ - - c, _ := ioutil.ReadFile("secrets.json") - k := NewTSIGSecrets() - json.Unmarshal(c, k) - - //fmt.Printf("%+v\n", k) - for r := range getDns(k) { - for _, rr := range r.RR { - /* hlen := len(rr.Header().String()) - fmt.Printf("%+v\n", rr.String()[hlen:]) */ - } + router := gin.Default() + + router.GET("/reflect-ip", reflectIP) + + ddns := router.Group("/dynamic-dns") + ddns.Use(ddnsAuth) + { + ddns.POST("", updateDynamicDNS) } + + acme := router.Group("/acme") + acme.Use(acmeAuth) + { + acme.POST("", createAcmeChallenge) + acme.DELETE("/:id", deleteAcmeChallenge) + } + + router.Run() } -- cgit v1.2.3