diff options
author | Mike Crute <mike@crute.us> | 2020-01-02 03:05:21 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2020-01-02 03:05:21 +0000 |
commit | 473ec1c19bb9d8cad259481f5cd2096a47dfb40f (patch) | |
tree | b319656f708795e34fdecce45cb6aed28e7b4972 | |
parent | b2e062c5de3fb233d34bf0c67c7e43cfd9969706 (diff) | |
download | go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.bz2 go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.xz go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.zip |
Create updated service
-rw-r--r-- | bind/config.go | 227 | ||||
-rw-r--r-- | dns/cilent.go | 69 | ||||
-rw-r--r-- | dns/types.go | 294 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 55 | ||||
-rw-r--r-- | main.go | 466 |
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 @@ | |||
1 | package bind | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "io/ioutil" | ||
6 | "strings" | ||
7 | "time" | ||
8 | |||
9 | "github.com/miekg/dns" | ||
10 | ) | ||
11 | |||
12 | type Signable interface { | ||
13 | SetTsig(string, string, uint16, int64) *dns.Msg | ||
14 | } | ||
15 | |||
16 | func NewBINDConfig() *BINDConfig { | ||
17 | return &BINDConfig{ | ||
18 | zones: map[string]map[string]*Zone{}, | ||
19 | keys: map[string]*Key{}, | ||
20 | } | ||
21 | } | ||
22 | |||
23 | type BINDConfig struct { | ||
24 | zones map[string]map[string]*Zone | ||
25 | keys map[string]*Key | ||
26 | } | ||
27 | |||
28 | func (c *BINDConfig) Views() []string { | ||
29 | v := []string{} | ||
30 | for vn, _ := range c.zones { | ||
31 | v = append(v, vn) | ||
32 | } | ||
33 | return v | ||
34 | } | ||
35 | |||
36 | func (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 | |||
57 | func (c *BINDConfig) AddKey(k *Key) { | ||
58 | c.keys[k.Name] = k | ||
59 | } | ||
60 | |||
61 | func (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 | |||
69 | func 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 | |||
81 | type Zone struct { | ||
82 | Name string | ||
83 | Type string | ||
84 | InView string | ||
85 | keys []string | ||
86 | config *BINDConfig | ||
87 | } | ||
88 | |||
89 | func (z *Zone) AddKey(key string) { | ||
90 | z.keys = append(z.keys, key) | ||
91 | } | ||
92 | |||
93 | func (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 | |||
101 | type Key struct { | ||
102 | Name string | ||
103 | Algorithm string | ||
104 | Secret string | ||
105 | } | ||
106 | |||
107 | func (k *Key) CanonicalName() string { | ||
108 | if !strings.HasSuffix(k.Name, ".") { | ||
109 | return fmt.Sprintf("%s.", k.Name) | ||
110 | } | ||
111 | return k.Name | ||
112 | } | ||
113 | |||
114 | func (k *Key) CanonicalAlgorithm() string { | ||
115 | if !strings.HasSuffix(k.Algorithm, ".") { | ||
116 | return fmt.Sprintf("%s.", k.Algorithm) | ||
117 | } | ||
118 | return k.Name | ||
119 | } | ||
120 | |||
121 | func (k *Key) Sign(r Signable) { | ||
122 | r.SetTsig(k.CanonicalName(), k.CanonicalAlgorithm(), 300, time.Now().Unix()) | ||
123 | } | ||
124 | |||
125 | func (k *Key) AsMap() map[string]string { | ||
126 | return map[string]string{k.CanonicalName(): k.Secret} | ||
127 | } | ||
128 | |||
129 | func NewStringStack() *StringStack { | ||
130 | return &StringStack{[]string{}} | ||
131 | } | ||
132 | |||
133 | type StringStack struct { | ||
134 | items []string | ||
135 | } | ||
136 | |||
137 | func (s *StringStack) isEmpty() bool { | ||
138 | return len(s.items) == 0 | ||
139 | } | ||
140 | |||
141 | func (s *StringStack) Peek() string { | ||
142 | return s.items[len(s.items)-1] | ||
143 | } | ||
144 | |||
145 | func (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 | |||
154 | func (s *StringStack) Push(v string) { | ||
155 | s.items = append(s.items, v) | ||
156 | } | ||
157 | |||
158 | func cleanValue(s string) string { | ||
159 | return strings.Trim(strings.Trim(s, ";"), "\"") | ||
160 | } | ||
161 | |||
162 | func isContainerStart(line []string) bool { | ||
163 | return line[len(line)-1] == "{" | ||
164 | } | ||
165 | |||
166 | func 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 @@ | |||
1 | package dns | ||
2 | |||
3 | import ( | ||
4 | "code.crute.me/mcrute/go_ddns_manager/bind" | ||
5 | "github.com/miekg/dns" | ||
6 | ) | ||
7 | |||
8 | type DNSClient struct { | ||
9 | Server string | ||
10 | } | ||
11 | |||
12 | func (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 | |||
23 | func (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 | |||
39 | func (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 | |||
55 | func (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 @@ | |||
1 | package dns | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "net" | ||
6 | |||
7 | "code.crute.me/mcrute/go_ddns_manager/bind" | ||
8 | "github.com/miekg/dns" | ||
9 | ) | ||
10 | |||
11 | func 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 | |||
20 | func 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 | |||
28 | type RR interface { | ||
29 | ToDNS(*bind.Zone) dns.RR | ||
30 | } | ||
31 | |||
32 | type A struct { | ||
33 | Name string | ||
34 | Ttl int | ||
35 | A net.IP | ||
36 | } | ||
37 | |||
38 | func (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 | |||
45 | type AAAA struct { | ||
46 | Name string | ||
47 | Ttl int | ||
48 | AAAA net.IP | ||
49 | } | ||
50 | |||
51 | func (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 | |||
58 | type CAA struct { | ||
59 | Name string | ||
60 | Ttl int | ||
61 | Flag uint8 | ||
62 | Tag string | ||
63 | Value string | ||
64 | } | ||
65 | |||
66 | func (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 | |||
75 | type CERT struct { | ||
76 | Name string | ||
77 | Ttl int | ||
78 | Type uint16 | ||
79 | KeyTag uint16 | ||
80 | Algorithm uint8 | ||
81 | Certificate string | ||
82 | } | ||
83 | |||
84 | func (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 | |||
94 | type CNAME struct { | ||
95 | Name string | ||
96 | Ttl int | ||
97 | Target string | ||
98 | } | ||
99 | |||
100 | func (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 | |||
107 | type DNAME struct { | ||
108 | Name string | ||
109 | Ttl int | ||
110 | Target string | ||
111 | } | ||
112 | |||
113 | func (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 | |||
120 | type 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 | |||
132 | func (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 | |||
145 | type MX struct { | ||
146 | Name string | ||
147 | Ttl int | ||
148 | Preference uint16 | ||
149 | Mx string | ||
150 | } | ||
151 | |||
152 | func (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 | |||
160 | type 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 | |||
171 | func (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 | |||
183 | type NS struct { | ||
184 | Name string | ||
185 | Ttl int | ||
186 | Ns string | ||
187 | } | ||
188 | |||
189 | func (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 | |||
196 | type OPENPGPKEY struct { | ||
197 | Name string | ||
198 | Ttl int | ||
199 | PublicKey string | ||
200 | } | ||
201 | |||
202 | func (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 | |||
209 | type PTR struct { | ||
210 | Name string | ||
211 | Ttl int | ||
212 | Ptr string | ||
213 | } | ||
214 | |||
215 | func (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 | |||
222 | type 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 | |||
234 | func (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 | |||
247 | type SRV struct { | ||
248 | Name string | ||
249 | Ttl int | ||
250 | Priority uint16 | ||
251 | Weight uint16 | ||
252 | Port uint16 | ||
253 | Target string | ||
254 | } | ||
255 | |||
256 | func (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 | |||
266 | type SSHFP struct { | ||
267 | Name string | ||
268 | Ttl int | ||
269 | Algorithm uint8 | ||
270 | Type uint8 | ||
271 | FingerPrint string | ||
272 | } | ||
273 | |||
274 | func (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 | |||
283 | type TXT struct { | ||
284 | Name string | ||
285 | Ttl int | ||
286 | Txt []string | ||
287 | } | ||
288 | |||
289 | func (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 | } | ||
@@ -0,0 +1,8 @@ | |||
1 | module code.crute.me/mcrute/go_ddns_manager | ||
2 | |||
3 | go 1.13 | ||
4 | |||
5 | require ( | ||
6 | github.com/gin-gonic/gin v1.5.0 | ||
7 | github.com/miekg/dns v1.1.26 | ||
8 | ) | ||
@@ -0,0 +1,55 @@ | |||
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
3 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||
4 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||
5 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= | ||
6 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= | ||
7 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= | ||
8 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= | ||
9 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= | ||
10 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= | ||
11 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= | ||
12 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||
14 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||
15 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= | ||
16 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= | ||
17 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= | ||
18 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | ||
19 | github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= | ||
20 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= | ||
21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||
22 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||
23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
25 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||
27 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= | ||
28 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | ||
29 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= | ||
30 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= | ||
31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
32 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= | ||
33 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= | ||
34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||
35 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
36 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= | ||
37 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= | ||
39 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
41 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
42 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
43 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= | ||
44 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
46 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||
47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
48 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
49 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
51 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= | ||
52 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= | ||
53 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= | ||
54 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | ||
55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||
@@ -1,236 +1,374 @@ | |||
1 | package main | 1 | package main |
2 | 2 | ||
3 | import ( | 3 | import ( |
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 | |||
20 | const ( | ||
21 | ACME_AUTH_KEY = "ACMEAuthContext" | ||
22 | DDNS_AUTH_KEY = "DDNSAuthZone" | ||
14 | ) | 23 | ) |
15 | 24 | ||
16 | type Zone struct { | 25 | var ( |
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 | |||
31 | func 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 | |||
49 | type Secrets struct { | ||
50 | DDNS map[string]string | ||
51 | ACME map[string]map[string]int | ||
52 | } | ||
53 | |||
54 | type DDNSUpdateRequest struct { | ||
55 | Key string `form:"key" binding:"required"` | ||
19 | } | 56 | } |
20 | 57 | ||
21 | type TSIGSecrets struct { | 58 | type 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 | ||
26 | func NewTSIGSecrets() *TSIGSecrets { | 63 | type 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 | |||
69 | func 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 | ||
33 | func (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. |
83 | func 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 | ||
41 | func (t *TSIGSecrets) GetViewZones() map[string][]string { | 98 | func 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 | ||
45 | func (t *TSIGSecrets) UnmarshalJSON(d []byte) error { | 118 | func 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 | ||
94 | func (t *TSIGSecrets) GetSecret(zone, view string) (*TSIGSecret, error) { | 191 | func 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 | ||
111 | type 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 { |
116 | type 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 | ||
122 | func (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 | ||
126 | func (t *TSIGSecret) AsMap() map[string]string { | 242 | func 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 | ||
132 | func 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 | ||
167 | func 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 | ||
186 | func dnsClass(rrh *dns.RR_Header) string { | 293 | func 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 | ||
190 | func 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 | ||
194 | func dnsTTL(rrh *dns.RR_Header) string { | 316 | func 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 | ||
208 | func handler(w http.ResponseWriter, r *http.Request) { | 335 | func 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 | ||
219 | func main() { | 355 | func 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 | } |