summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-01-02 03:05:21 +0000
committerMike Crute <mike@crute.us>2020-01-02 03:05:21 +0000
commit473ec1c19bb9d8cad259481f5cd2096a47dfb40f (patch)
treeb319656f708795e34fdecce45cb6aed28e7b4972
parentb2e062c5de3fb233d34bf0c67c7e43cfd9969706 (diff)
downloadgo_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.bz2
go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.xz
go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.zip
Create updated service
-rw-r--r--bind/config.go227
-rw-r--r--dns/cilent.go69
-rw-r--r--dns/types.go294
-rw-r--r--go.mod8
-rw-r--r--go.sum55
-rw-r--r--main.go466
6 files changed, 955 insertions, 164 deletions
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 @@
1package bind
2
3import (
4 "fmt"
5 "io/ioutil"
6 "strings"
7 "time"
8
9 "github.com/miekg/dns"
10)
11
12type Signable interface {
13 SetTsig(string, string, uint16, int64) *dns.Msg
14}
15
16func NewBINDConfig() *BINDConfig {
17 return &BINDConfig{
18 zones: map[string]map[string]*Zone{},
19 keys: map[string]*Key{},
20 }
21}
22
23type BINDConfig struct {
24 zones map[string]map[string]*Zone
25 keys map[string]*Key
26}
27
28func (c *BINDConfig) Views() []string {
29 v := []string{}
30 for vn, _ := range c.zones {
31 v = append(v, vn)
32 }
33 return v
34}
35
36func (c *BINDConfig) Zone(view, name string) *Zone {
37 if !strings.HasSuffix(name, ".") {
38 name = fmt.Sprintf("%s.", name)
39 }
40
41 if _, ok := c.zones[view]; !ok {
42 return nil
43 }
44
45 z, ok := c.zones[view][name]
46 if !ok {
47 return nil
48 }
49
50 if z.InView != "" {
51 return c.zones[z.InView][name]
52 }
53
54 return z
55}
56
57func (c *BINDConfig) AddKey(k *Key) {
58 c.keys[k.Name] = k
59}
60
61func (c *BINDConfig) AddZone(z *Zone, view string) {
62 if c.zones[view] == nil {
63 c.zones[view] = map[string]*Zone{}
64 }
65 c.zones[view][z.Name] = z
66 z.config = c
67}
68
69func NewZone(name string) *Zone {
70 // Canonicalize name
71 if !strings.HasSuffix(name, ".") {
72 name = fmt.Sprintf("%s.", name)
73 }
74
75 return &Zone{
76 Name: name,
77 keys: []string{},
78 }
79}
80
81type Zone struct {
82 Name string
83 Type string
84 InView string
85 keys []string
86 config *BINDConfig
87}
88
89func (z *Zone) AddKey(key string) {
90 z.keys = append(z.keys, key)
91}
92
93func (z *Zone) Keys() []*Key {
94 k := []*Key{}
95 for _, kn := range z.keys {
96 k = append(k, z.config.keys[kn])
97 }
98 return k
99}
100
101type Key struct {
102 Name string
103 Algorithm string
104 Secret string
105}
106
107func (k *Key) CanonicalName() string {
108 if !strings.HasSuffix(k.Name, ".") {
109 return fmt.Sprintf("%s.", k.Name)
110 }
111 return k.Name
112}
113
114func (k *Key) CanonicalAlgorithm() string {
115 if !strings.HasSuffix(k.Algorithm, ".") {
116 return fmt.Sprintf("%s.", k.Algorithm)
117 }
118 return k.Name
119}
120
121func (k *Key) Sign(r Signable) {
122 r.SetTsig(k.CanonicalName(), k.CanonicalAlgorithm(), 300, time.Now().Unix())
123}
124
125func (k *Key) AsMap() map[string]string {
126 return map[string]string{k.CanonicalName(): k.Secret}
127}
128
129func NewStringStack() *StringStack {
130 return &StringStack{[]string{}}
131}
132
133type StringStack struct {
134 items []string
135}
136
137func (s *StringStack) isEmpty() bool {
138 return len(s.items) == 0
139}
140
141func (s *StringStack) Peek() string {
142 return s.items[len(s.items)-1]
143}
144
145func (s *StringStack) Pop() string {
146 if s.isEmpty() {
147 return ""
148 }
149 v := s.items[len(s.items)-1]
150 s.items = s.items[:len(s.items)-1]
151 return v
152}
153
154func (s *StringStack) Push(v string) {
155 s.items = append(s.items, v)
156}
157
158func cleanValue(s string) string {
159 return strings.Trim(strings.Trim(s, ";"), "\"")
160}
161
162func isContainerStart(line []string) bool {
163 return line[len(line)-1] == "{"
164}
165
166func ParseBINDConfig(filename string) (*BINDConfig, error) {
167 data, err := ioutil.ReadFile(filename)
168 if err != nil {
169 return nil, err
170 }
171
172 var view string
173 var zone *Zone
174 var key *Key
175
176 config := NewBINDConfig()
177 stack := NewStringStack()
178
179 for _, line := range strings.Split(string(data), "\n") {
180 line := strings.Split(strings.TrimSpace(line), " ")
181
182 switch line[0] {
183 case "};":
184 t := stack.Pop()
185 switch t {
186 case "view":
187 view = ""
188 case "key":
189 key = nil
190 case "zone":
191 zone = nil
192 }
193 case "view":
194 view = cleanValue(line[1])
195 stack.Push("view")
196 case "zone":
197 zone = NewZone(cleanValue(line[1]))
198 config.AddZone(zone, view)
199 stack.Push("zone")
200 case "in-view":
201 zone.Type = "reference"
202 zone.InView = cleanValue(line[1])
203 case "type":
204 zone.Type = cleanValue(line[1])
205 case "grant":
206 zone.AddKey(cleanValue(line[1]))
207 case "key":
208 if !isContainerStart(line) {
209 continue
210 }
211
212 key = &Key{Name: cleanValue(line[1])}
213 config.AddKey(key)
214 stack.Push("key")
215 case "algorithm":
216 key.Algorithm = cleanValue(line[1])
217 case "secret":
218 key.Secret = cleanValue(line[1])
219 default:
220 if isContainerStart(line) {
221 stack.Push(line[0])
222 }
223 }
224 }
225
226 return config, nil
227}
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 @@
1package dns
2
3import (
4 "code.crute.me/mcrute/go_ddns_manager/bind"
5 "github.com/miekg/dns"
6)
7
8type DNSClient struct {
9 Server string
10}
11
12func (c *DNSClient) AXFR(zone *bind.Zone) (chan *dns.Envelope, error) {
13 k := zone.Keys()[0]
14 t := &dns.Transfer{TsigSecret: k.AsMap()}
15
16 m := &dns.Msg{}
17 m.SetAxfr(zone.Name)
18 k.Sign(m)
19
20 return t.In(m, c.Server)
21}
22
23func (c *DNSClient) Insert(zone *bind.Zone, rrs ...RR) error {
24 k := zone.Keys()[0]
25 dc := &dns.Client{TsigSecret: k.AsMap()}
26
27 m := &dns.Msg{}
28 m.SetUpdate(zone.Name)
29 m.Insert(toRRSet(zone, rrs...))
30 k.Sign(m)
31
32 if _, _, err := dc.Exchange(m, c.Server); err != nil {
33 return err
34 }
35
36 return nil
37}
38
39func (c *DNSClient) Remove(zone *bind.Zone, rrs ...RR) error {
40 k := zone.Keys()[0]
41 dc := &dns.Client{TsigSecret: k.AsMap()}
42
43 m := &dns.Msg{}
44 m.SetUpdate(zone.Name)
45 m.Remove(toRRSet(zone, rrs...))
46 k.Sign(m)
47
48 if _, _, err := dc.Exchange(m, c.Server); err != nil {
49 return err
50 }
51
52 return nil
53}
54
55func (c *DNSClient) RemoveAll(zone *bind.Zone, rrs ...RR) error {
56 k := zone.Keys()[0]
57 dc := &dns.Client{TsigSecret: k.AsMap()}
58
59 m := &dns.Msg{}
60 m.SetUpdate(zone.Name)
61 m.RemoveRRset(toRRSet(zone, rrs...))
62 k.Sign(m)
63
64 if _, _, err := dc.Exchange(m, c.Server); err != nil {
65 return err
66 }
67
68 return nil
69}
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 @@
1package dns
2
3import (
4 "fmt"
5 "net"
6
7 "code.crute.me/mcrute/go_ddns_manager/bind"
8 "github.com/miekg/dns"
9)
10
11func makeHeader(name string, zone *bind.Zone, t uint16, ttl int) dns.RR_Header {
12 return dns.RR_Header{
13 Name: fmt.Sprintf("%s.%s", name, zone.Name),
14 Rrtype: t,
15 Class: dns.ClassINET,
16 Ttl: uint32(ttl),
17 }
18}
19
20func toRRSet(z *bind.Zone, rr ...RR) []dns.RR {
21 o := []dns.RR{}
22 for _, v := range rr {
23 o = append(o, v.ToDNS(z))
24 }
25 return o
26}
27
28type RR interface {
29 ToDNS(*bind.Zone) dns.RR
30}
31
32type A struct {
33 Name string
34 Ttl int
35 A net.IP
36}
37
38func (r *A) ToDNS(zone *bind.Zone) dns.RR {
39 return &dns.A{
40 Hdr: makeHeader(r.Name, zone, dns.TypeA, r.Ttl),
41 A: r.A,
42 }
43}
44
45type AAAA struct {
46 Name string
47 Ttl int
48 AAAA net.IP
49}
50
51func (r *AAAA) ToDNS(zone *bind.Zone) dns.RR {
52 return &dns.AAAA{
53 Hdr: makeHeader(r.Name, zone, dns.TypeAAAA, r.Ttl),
54 AAAA: r.AAAA,
55 }
56}
57
58type CAA struct {
59 Name string
60 Ttl int
61 Flag uint8
62 Tag string
63 Value string
64}
65
66func (r *CAA) ToDNS(zone *bind.Zone) dns.RR {
67 return &dns.CAA{
68 Hdr: makeHeader(r.Name, zone, dns.TypeCAA, r.Ttl),
69 Flag: r.Flag,
70 Tag: r.Tag,
71 Value: r.Value,
72 }
73}
74
75type CERT struct {
76 Name string
77 Ttl int
78 Type uint16
79 KeyTag uint16
80 Algorithm uint8
81 Certificate string
82}
83
84func (r *CERT) ToDNS(zone *bind.Zone) dns.RR {
85 return &dns.CERT{
86 Hdr: makeHeader(r.Name, zone, dns.TypeCERT, r.Ttl),
87 Type: r.Type,
88 KeyTag: r.KeyTag,
89 Algorithm: r.Algorithm,
90 Certificate: r.Certificate,
91 }
92}
93
94type CNAME struct {
95 Name string
96 Ttl int
97 Target string
98}
99
100func (r *CNAME) ToDNS(zone *bind.Zone) dns.RR {
101 return &dns.CNAME{
102 Hdr: makeHeader(r.Name, zone, dns.TypeCNAME, r.Ttl),
103 Target: r.Target,
104 }
105}
106
107type DNAME struct {
108 Name string
109 Ttl int
110 Target string
111}
112
113func (r *DNAME) ToDNS(zone *bind.Zone) dns.RR {
114 return &dns.DNAME{
115 Hdr: makeHeader(r.Name, zone, dns.TypeDNAME, r.Ttl),
116 Target: r.Target,
117 }
118}
119
120type LOC struct {
121 Name string
122 Ttl int
123 Version uint8
124 Size uint8
125 HorizPre uint8
126 VertPre uint8
127 Latitude uint32
128 Longitude uint32
129 Altitude uint32
130}
131
132func (r *LOC) ToDNS(zone *bind.Zone) dns.RR {
133 return &dns.LOC{
134 Hdr: makeHeader(r.Name, zone, dns.TypeLOC, r.Ttl),
135 Version: r.Version,
136 Size: r.Size,
137 HorizPre: r.HorizPre,
138 VertPre: r.VertPre,
139 Latitude: r.Latitude,
140 Longitude: r.Longitude,
141 Altitude: r.Altitude,
142 }
143}
144
145type MX struct {
146 Name string
147 Ttl int
148 Preference uint16
149 Mx string
150}
151
152func (r *MX) ToDNS(zone *bind.Zone) dns.RR {
153 return &dns.MX{
154 Hdr: makeHeader(r.Name, zone, dns.TypeMX, r.Ttl),
155 Preference: r.Preference,
156 Mx: r.Mx,
157 }
158}
159
160type NAPTR struct {
161 Name string
162 Ttl int
163 Order uint16
164 Preference uint16
165 Flags string
166 Service string
167 Regexp string
168 Replacement string
169}
170
171func (r *NAPTR) ToDNS(zone *bind.Zone) dns.RR {
172 return &dns.NAPTR{
173 Hdr: makeHeader(r.Name, zone, dns.TypeNAPTR, r.Ttl),
174 Order: r.Order,
175 Preference: r.Preference,
176 Flags: r.Flags,
177 Service: r.Service,
178 Regexp: r.Regexp,
179 Replacement: r.Replacement,
180 }
181}
182
183type NS struct {
184 Name string
185 Ttl int
186 Ns string
187}
188
189func (r *NS) ToDNS(zone *bind.Zone) dns.RR {
190 return &dns.NS{
191 Hdr: makeHeader(r.Name, zone, dns.TypeNS, r.Ttl),
192 Ns: r.Ns,
193 }
194}
195
196type OPENPGPKEY struct {
197 Name string
198 Ttl int
199 PublicKey string
200}
201
202func (r *OPENPGPKEY) ToDNS(zone *bind.Zone) dns.RR {
203 return &dns.OPENPGPKEY{
204 Hdr: makeHeader(r.Name, zone, dns.TypeOPENPGPKEY, r.Ttl),
205 PublicKey: r.PublicKey,
206 }
207}
208
209type PTR struct {
210 Name string
211 Ttl int
212 Ptr string
213}
214
215func (r *PTR) ToDNS(zone *bind.Zone) dns.RR {
216 return &dns.PTR{
217 Hdr: makeHeader(r.Name, zone, dns.TypePTR, r.Ttl),
218 Ptr: r.Ptr,
219 }
220}
221
222type SOA struct {
223 Name string
224 Ttl int
225 Ns string
226 Mbox string
227 Serial uint32
228 Refresh uint32
229 Retry uint32
230 Expire uint32
231 Minttl uint32
232}
233
234func (r *SOA) ToDNS(zone *bind.Zone) dns.RR {
235 return &dns.SOA{
236 Hdr: makeHeader(r.Name, zone, dns.TypeSOA, r.Ttl),
237 Ns: r.Ns,
238 Mbox: r.Mbox,
239 Serial: r.Serial,
240 Refresh: r.Refresh,
241 Retry: r.Retry,
242 Expire: r.Expire,
243 Minttl: r.Minttl,
244 }
245}
246
247type SRV struct {
248 Name string
249 Ttl int
250 Priority uint16
251 Weight uint16
252 Port uint16
253 Target string
254}
255
256func (r *SRV) ToDNS(zone *bind.Zone) dns.RR {
257 return &dns.SRV{
258 Hdr: makeHeader(r.Name, zone, dns.TypeSRV, r.Ttl),
259 Priority: r.Priority,
260 Weight: r.Weight,
261 Port: r.Port,
262 Target: r.Target,
263 }
264}
265
266type SSHFP struct {
267 Name string
268 Ttl int
269 Algorithm uint8
270 Type uint8
271 FingerPrint string
272}
273
274func (r *SSHFP) ToDNS(zone *bind.Zone) dns.RR {
275 return &dns.SSHFP{
276 Hdr: makeHeader(r.Name, zone, dns.TypeSSHFP, r.Ttl),
277 Algorithm: r.Algorithm,
278 Type: r.Type,
279 FingerPrint: r.FingerPrint,
280 }
281}
282
283type TXT struct {
284 Name string
285 Ttl int
286 Txt []string
287}
288
289func (r *TXT) ToDNS(zone *bind.Zone) dns.RR {
290 return &dns.TXT{
291 Hdr: makeHeader(r.Name, zone, dns.TypeTXT, r.Ttl),
292 Txt: r.Txt,
293 }
294}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1fd977c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
1module code.crute.me/mcrute/go_ddns_manager
2
3go 1.13
4
5require (
6 github.com/gin-gonic/gin v1.5.0
7 github.com/miekg/dns v1.1.26
8)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..1f2c074
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,55 @@
1github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
4github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
5github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
6github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
7github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
8github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
9github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
10github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
11github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
12github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
13github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
14github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
15github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
16github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
17github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
18github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
19github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
20github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
21github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
22github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
23github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
25github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
26github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
27github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
28github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
29github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
30github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
31golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
32golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
33golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
34golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
35golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
36golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
37golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
38golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
39golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
40golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
41golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
43golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
44golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
46golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
47golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
48golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
49golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
50gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
52gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
53gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
54gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
55gopkg.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 @@
1package main 1package main
2 2
3import ( 3import (
4 "bytes" 4 "encoding/base64"
5 "encoding/json" 5 "encoding/json"
6 "errors"
7 "fmt" 6 "fmt"
8 "github.com/miekg/dns"
9 "io/ioutil" 7 "io/ioutil"
8 "log"
9 "net"
10 "net/http" 10 "net/http"
11 "net/url"
12 "regexp"
11 "strings" 13 "strings"
12 "text/template" 14
13 "time" 15 "code.crute.me/mcrute/go_ddns_manager/bind"
16 "code.crute.me/mcrute/go_ddns_manager/dns"
17 "github.com/gin-gonic/gin"
18)
19
20const (
21 ACME_AUTH_KEY = "ACMEAuthContext"
22 DDNS_AUTH_KEY = "DDNSAuthZone"
14) 23)
15 24
16type Zone struct { 25var (
17 Name string 26 cfg *bind.BINDConfig
18 View string 27 secrets *Secrets
28 ipRegexp = regexp.MustCompile(`(?:\[([0-9a-f:]+)\]|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})):\d+`)
29)
30
31func init() {
32 var err error
33 cfg, err = bind.ParseBINDConfig("zones.conf")
34 if err != nil {
35 panic(err)
36 }
37
38 fd, err := ioutil.ReadFile("secrets.json")
39 if err != nil {
40 panic(err)
41 }
42
43 secrets = &Secrets{}
44 if err = json.Unmarshal(fd, secrets); err != nil {
45 panic(err)
46 }
47}
48
49type Secrets struct {
50 DDNS map[string]string
51 ACME map[string]map[string]int
52}
53
54type DDNSUpdateRequest struct {
55 Key string `form:"key" binding:"required"`
19} 56}
20 57
21type TSIGSecrets struct { 58type ACMEChallenge struct {
22 secrets map[string]map[string]TSIGSecret 59 Zone string `json:"zone" binding:"required"`
23 viewzones map[string][]string 60 Challenge string `json:"challenge" binding:"required"`
24} 61}
25 62
26func NewTSIGSecrets() *TSIGSecrets { 63type ACMEChallengeID struct {
27 return &TSIGSecrets{ 64 Zone string
28 secrets: make(map[string]map[string]TSIGSecret), 65 Prefix string
29 viewzones: make(map[string][]string), 66 Challenge string
67}
68
69func joinDomainParts(parts ...string) string {
70 p := []string{}
71 for _, i := range parts {
72 if strings.TrimSpace(i) != "" {
73 p = append(p, i)
74 }
30 } 75 }
76 return strings.Join(p, ".")
31} 77}
32 78
33func (t *TSIGSecrets) GetViews() []string { 79// Find the closest zone that we manage by striping dotted components off the
34 r := make([]string, 0, len(t.viewzones)) 80// front of the domain until one matches. If there is a match return the zone
35 for k := range t.viewzones { 81// that matched and any prefix components, if any, as a dotted string. If none
36 r = append(r, k) 82// match then return nil.
83func findClosestZone(cfg *bind.BINDConfig, zoneIn, view string) (*bind.Zone, string) {
84 suffix := ""
85 prefix := []string{}
86
87 zc := strings.Split(zoneIn, ".")
88 for i := 0; i <= len(zc)-2; i++ {
89 prefix, suffix = zc[:i], strings.Join(zc[i:], ".")
90 if zone := cfg.Zone(view, suffix); zone != nil {
91 return zone, strings.Join(prefix, ".")
92 }
37 } 93 }
38 return r 94
95 return nil, ""
39} 96}
40 97
41func (t *TSIGSecrets) GetViewZones() map[string][]string { 98func makeURL(r *http.Request, path string, subs ...interface{}) *url.URL {
42 return t.viewzones 99 scheme := "https"
100
101 if r.TLS == nil {
102 scheme = "http"
103 }
104
105 // Always defer to whatever the proxy told us it was doing because this
106 // could be a mullet-VIP in either direction.
107 if fwProto := r.Header.Get("X-Forwarded-Proto"); fwProto != "" {
108 scheme = fwProto
109 }
110
111 return &url.URL{
112 Scheme: scheme,
113 Host: r.Host,
114 Path: fmt.Sprintf(path, subs...),
115 }
43} 116}
44 117
45func (t *TSIGSecrets) UnmarshalJSON(d []byte) error { 118func createAcmeChallenge(c *gin.Context) {
46 v := make(map[string]map[string]string) 119 dc := dns.DNSClient{Server: "172.16.18.52:53"}
47 120
48 if err := json.Unmarshal(d, &v); err != nil { 121 var ch ACMEChallenge
49 return err 122 if err := c.ShouldBindJSON(&ch); err != nil {
123 c.JSON(http.StatusBadRequest, gin.H{
124 "error": err.Error(),
125 })
126 return
50 } 127 }
51 128
52 for k, v := range v { 129 zone, prefix := findClosestZone(cfg, ch.Zone, "external")
53 o := strings.Split(k, "-") 130 if zone == nil {
54 view := o[len(o)-1] 131 c.JSON(http.StatusNotFound, gin.H{
132 "error": "Zone not found",
133 })
134 return
135 }
55 136
56 d := make([]string, len(o)) 137 if v, ok := c.Get(ACME_AUTH_KEY); !ok || v.(map[string]int)[zone.Name] != 1 {
57 copy(d, o[:len(o)-1]) 138 c.JSON(http.StatusForbidden, gin.H{
58 dn := strings.Join(d, ".") 139 "error": "Zone update not allowed",
140 })
141 return
142 }
59 143
60 a, ok := v["algorithm"] 144 // Do this first, in-case it fails (even though it should never fail)
61 if !ok { 145 id, err := json.Marshal(ACMEChallengeID{
62 a = "hmac-sha256." 146 Zone: zone.Name,
63 } 147 Prefix: prefix,
148 Challenge: ch.Challenge,
149 })
150 if err != nil {
151 log.Printf("error: %s", err)
152 c.JSON(http.StatusInternalServerError, gin.H{
153 "error": "error encoding ID",
154 })
155 return
156 }
64 157
65 if !strings.HasSuffix(a, ".") { 158 url := makeURL(c.Request, "/acme/%s", base64.URLEncoding.EncodeToString(id))
66 a = fmt.Sprintf("%s.", a)
67 }
68 159
69 if _, ok := t.viewzones[view]; !ok { 160 t := &dns.TXT{
70 t.viewzones[view] = make([]string, 1) 161 Name: joinDomainParts("_acme-challenge", prefix),
71 t.viewzones[view] = append(t.viewzones[view], dn) 162 Ttl: 5,
72 } else { 163 Txt: []string{ch.Challenge},
73 t.viewzones[view] = append(t.viewzones[view], dn) 164 }
74 }
75 165
76 if _, ok := t.secrets[view]; !ok { 166 log.Printf("%+v %+v '%s'", zone, t, t.Name)
77 t.secrets[view] = make(map[string]TSIGSecret)
78 }
79 167
80 if !strings.HasSuffix(k, ".") { 168 // Cleanup any old challenges before adding a new one
81 k = fmt.Sprintf("%s.", k) 169 if err := dc.RemoveAll(zone, t); err != nil {
82 } 170 log.Printf("error RemoveAll: %s", err)
171 c.JSON(http.StatusInternalServerError, gin.H{
172 "error": err.Error(),
173 })
174 return
175 }
83 176
84 t.secrets[view][dn] = TSIGSecret{ 177 if err := dc.Insert(zone, t); err != nil {
85 KeyName: k, 178 log.Printf("error Insert: %s", err)
86 Algorithm: a, 179 c.JSON(http.StatusInternalServerError, gin.H{
87 Secret: v["secret"], 180 "error": err.Error(),
88 } 181 })
182 return
89 } 183 }
90 184
91 return nil 185 c.Writer.Header()["Location"] = []string{url.String()}
186 c.JSON(http.StatusCreated, gin.H{
187 "created": url.String(),
188 })
92} 189}
93 190
94func (t *TSIGSecrets) GetSecret(zone, view string) (*TSIGSecret, error) { 191func deleteAcmeChallenge(c *gin.Context) {
95 if !strings.HasSuffix(zone, ".") { 192 dc := dns.DNSClient{Server: "172.16.18.52:53"}
96 zone = fmt.Sprintf("%s.", zone) 193
194 rid, err := base64.URLEncoding.DecodeString(c.Param("id"))
195 if err != nil {
196 c.JSON(http.StatusBadRequest, gin.H{
197 "error": "unable to decode ID",
198 })
199 return
97 } 200 }
98 201
99 if _, ok := t.secrets[view]; !ok { 202 var id ACMEChallengeID
100 return nil, errors.New("No keys for requested zone") 203 if err = json.Unmarshal(rid, &id); err != nil {
204 c.JSON(http.StatusBadRequest, gin.H{
205 "error": "unable to decode ID",
206 })
207 return
101 } 208 }
102 209
103 key, ok := t.secrets[view][zone] 210 zone := cfg.Zone("external", id.Zone)
104 if !ok { 211 if zone == nil {
105 return nil, errors.New("No keys for requested view of zone") 212 c.JSON(http.StatusNotFound, gin.H{
213 "error": "Zone not found",
214 })
215 return
106 } 216 }
107 217
108 return &key, nil 218 if v, ok := c.Get(ACME_AUTH_KEY); !ok || v.(map[string]int)[zone.Name] != 1 {
109} 219 c.JSON(http.StatusForbidden, gin.H{
220 "error": "Zone update not allowed",
221 })
222 return
223 }
110 224
111type Signable interface { 225 t := &dns.TXT{
112 SetTsig(string, string, uint16, int64) *dns.Msg 226 Name: joinDomainParts("_acme-challenge", id.Prefix),
113} 227 Ttl: 5,
228 Txt: []string{id.Challenge},
229 }
114 230
115// TODO: Name and Algorithm end with dot (.) 231 if err := dc.Remove(zone, t); err != nil {
116type TSIGSecret struct { 232 log.Printf("error Remove: %s", err)
117 KeyName string 233 c.JSON(http.StatusInternalServerError, gin.H{
118 Algorithm string `json:"algorithm"` 234 "error": err.Error(),
119 Secret string `json:"secret"` 235 })
120} 236 return
237 }
121 238
122func (t *TSIGSecret) Sign(r Signable) { 239 c.JSON(http.StatusNoContent, gin.H{})
123 r.SetTsig(t.KeyName, t.Algorithm, 300, time.Now().Unix())
124} 240}
125 241
126func (t *TSIGSecret) AsMap() map[string]string { 242func updateDynamicDNS(c *gin.Context) {
127 return map[string]string{ 243 dc := dns.DNSClient{Server: "172.16.18.52:53"}
128 t.KeyName: t.Secret, 244
245 res, ok := c.GetString(DDNS_AUTH_KEY)
246 if !ok {
247 log.Println("ddns: Unable to get auth key")
248 c.AbortWithStatus(http.StatusForbidden)
249 return
129 } 250 }
130}
131 251
132func getValue(v interface{}) string { 252 zone, part := findClosestZone(cfg, res, "external")
133 switch i := v.(type) { 253 if zone == nil {
134 case *dns.SOA: 254 log.Println("ddns: Unable to locate zone")
135 return fmt.Sprintf("%s %s %d %d %d %d %d", i.Ns, i.Mbox, i.Serial, i.Refresh, i.Retry, i.Expire, i.Minttl) 255 c.AbortWithStatus(http.StatusNotFound)
136 case *dns.A: 256 return
137 return fmt.Sprintf("%s", i.A) 257 }
138 case *dns.CNAME: 258
139 return fmt.Sprintf("%s", i.Target) 259 inip := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
140 case *dns.AAAA: 260 xff := c.Request.Header.Get("X-Forwarded-For")
141 return fmt.Sprintf("%s", i.AAAA) 261 if xff != "" {
142 case *dns.MX: 262 inip = net.ParseIP(xff)
143 return fmt.Sprintf("%d %s", i.Preference, i.Mx)
144 case *dns.TXT:
145 b := &bytes.Buffer{}
146 for _, t := range i.Txt {
147 fmt.Fprintf(b, "\"%s\"", t) // []
148 }
149 return b.String()
150 case *dns.PTR:
151 return fmt.Sprintf("%s", i.Ptr)
152 case *dns.NS:
153 return fmt.Sprintf("%s", i.Ns)
154 case *dns.SRV:
155 return fmt.Sprintf("%d %d %d %s", i.Priority, i.Weight, i.Port, i.Target)
156 case *dns.SPF:
157 b := &bytes.Buffer{}
158 for _, t := range i.Txt {
159 fmt.Fprintf(b, "\"%s\"", t) // []
160 }
161 return b.String()
162 default:
163 return "UNKNOWN"
164 } 263 }
165}
166 264
167func getDns(sm *TSIGSecrets) chan *dns.Envelope { 265 if inip == nil {
168 s, _ := sm.GetSecret("crute.us", "external") 266 log.Println("ddns: Unable to parse IP")
267 c.AbortWithStatus(http.StatusInternalServerError)
268 return
269 }
169 270
170 c := &dns.Transfer{} 271 t := &dns.A{
171 c.TsigSecret = s.AsMap() 272 Name: part,
273 Ttl: 60,
274 A: inip,
275 }
172 276
173 m := &dns.Msg{} 277 // Cleanup any old records before adding the new one
174 m.SetAxfr("crute.us.") 278 if err := dc.RemoveAll(zone, t); err != nil {
175 s.Sign(m) 279 log.Printf("ddns RemoveAll: %s", err)
280 c.AbortWithStatus(http.StatusInternalServerError)
281 return
282 }
176 283
177 in, err := c.In(m, "172.16.18.52:53") 284 if err := dc.Insert(zone, t); err != nil {
178 if err != nil { 285 log.Printf("ddns Insert: %s", err)
179 fmt.Printf("Error: %s\n", err.Error()) 286 c.AbortWithStatus(http.StatusInternalServerError)
180 return nil 287 return
181 } 288 }
182 289
183 return in 290 c.String(http.StatusAccepted, "")
184} 291}
185 292
186func dnsClass(rrh *dns.RR_Header) string { 293func reflectIP(c *gin.Context) {
187 return dns.Class(rrh.Class).String() 294 myIp := c.Request.RemoteAddr
188} 295 xff := c.Request.Header.Get("X-Forwarded-For")
296 if xff != "" {
297 myIp = xff
298 }
299
300 ips := ipRegexp.FindStringSubmatch(myIp)
301 if ips == nil {
302 c.AbortWithStatus(http.StatusInternalServerError)
303 return
304 }
189 305
190func dnsType(rrh *dns.RR_Header) string { 306 v6, v4 := ips[1], ips[2]
191 return dns.Type(rrh.Rrtype).String() 307 if v6 != "" {
308 c.String(http.StatusOK, v6)
309 } else if v4 != "" {
310 c.String(http.StatusOK, v4)
311 } else {
312 c.AbortWithStatus(http.StatusInternalServerError)
313 }
192} 314}
193 315
194func dnsTTL(rrh *dns.RR_Header) string { 316func acmeAuth(c *gin.Context) {
195 t := rrh.Ttl 317 _, pwd, ok := c.Request.BasicAuth()
318 if !ok {
319 c.Request.Header["WWW-Authenticate"] = []string{`Basic realm="closed site"`}
320 c.AbortWithStatus(http.StatusUnauthorized)
321 return
322 }
196 323
197 if t/86400 > 1 { 324 allowed, ok := secrets.ACME[pwd]
198 return fmt.Sprintf("%d days", t/86400) 325 if !ok {
199 } else if t/3600 > 1 { 326 c.AbortWithStatus(http.StatusForbidden)
200 return fmt.Sprintf("%d hours", t/3600) 327 return
201 } else if t/60 > 1 {
202 return fmt.Sprintf("%d minutes", t/60)
203 } else { 328 } else {
204 return fmt.Sprintf("%d seconds", t) 329 c.Set(ACME_AUTH_KEY, allowed)
205 } 330 }
331
332 c.Next()
206} 333}
207 334
208func handler(w http.ResponseWriter, r *http.Request) { 335func ddnsAuth(c *gin.Context) {
209 fm := template.FuncMap{ 336 var req DDNSUpdateRequest
210 "getValue": getValue, 337 if err := c.ShouldBind(&req); err != nil {
211 "dnsClass": dnsClass, 338 log.Println("ddnsAuth: No key in request")
212 "dnsType": dnsType, 339 c.AbortWithStatus(http.StatusNotFound)
213 "dnsTTL": dnsTTL, 340 return
341 }
342
343 res, ok := secrets.DDNS[req.Key]
344 if !ok {
345 log.Println("ddnsAuth: Unknown secret")
346 c.AbortWithStatus(http.StatusNotFound)
347 return
348 } else {
349 c.Set(DDNS_AUTH_KEY, res)
214 } 350 }
215 t, _ := template.New("").Funcs(fm).ParseFiles("dns.html") 351
216 t.ExecuteTemplate(w, "dns.html", getDns(nil)) 352 c.Next()
217} 353}
218 354
219func main() { 355func main() {
220 /* 356 router := gin.Default()
221 http.HandleFunc("/", handler) 357
222 http.ListenAndServe(":8080", nil) 358 router.GET("/reflect-ip", reflectIP)
223 */ 359
224 360 ddns := router.Group("/dynamic-dns")
225 c, _ := ioutil.ReadFile("secrets.json") 361 ddns.Use(ddnsAuth)
226 k := NewTSIGSecrets() 362 {
227 json.Unmarshal(c, k) 363 ddns.POST("", updateDynamicDNS)
228
229 //fmt.Printf("%+v\n", k)
230 for r := range getDns(k) {
231 for _, rr := range r.RR {
232 /* hlen := len(rr.Header().String())
233 fmt.Printf("%+v\n", rr.String()[hlen:]) */
234 }
235 } 364 }
365
366 acme := router.Group("/acme")
367 acme.Use(acmeAuth)
368 {
369 acme.POST("", createAcmeChallenge)
370 acme.DELETE("/:id", deleteAcmeChallenge)
371 }
372
373 router.Run()
236} 374}