summaryrefslogtreecommitdiff
path: root/main.go
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 /main.go
parentb2e062c5de3fb233d34bf0c67c7e43cfd9969706 (diff)
downloadgo_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.bz2
go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.tar.xz
go_ddns_manager-473ec1c19bb9d8cad259481f5cd2096a47dfb40f.zip
Create updated service
Diffstat (limited to 'main.go')
-rw-r--r--main.go466
1 files changed, 302 insertions, 164 deletions
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}