package main import ( "fmt" "log" "net" "net/http" "time" "github.com/kelseyhightower/envconfig" "github.com/miekg/dns" ) type App struct { DNSRecord string `envconfig:"DNS_RECORD" required:"true"` // must have trailing dot DNSServer string `envconfig:"DNS_SERVER" required:"true"` // must have :53 DNSZone string `envconfig:"DNS_ZONE" required:"true"` // must have trailing dot DNSKeyName string `envconfig:"DNS_KEY_NAME" required:"true"` // must have trailing dot DNSKeySecret string `envconfig:"DNS_KEY_Secret" required:"true"` // no trailing dot :) DNSKeyAlgo string `envconfig:"DNS_KEY_ALGO" default:"hmac-sha512."` // must have trailing dot Listen string `envconfig:"LISTEN" default:":8080"` Token string `envconfig:"TOKEN" required:"true"` } func main() { app := &App{} err := envconfig.Process("", app) if err != nil { log.Fatalf("unable to parse config: %s", err) } http.HandleFunc("/update", app.handler) err = http.ListenAndServe(app.Listen, nil) if err != nil { log.Fatalf("unable to listen: %s", err) } } func (a *App) handler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() token := query.Get("token") v4 := net.ParseIP(query.Get("v4")) v6 := net.ParseIP(query.Get("v6")) if token != a.Token { w.WriteHeader(http.StatusForbidden) fmt.Fprintf(w, "no :(") return } if v4 == nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "no :(") return } err := a.dnsUpdate(v4, v6) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("unable to update: %s", err) fmt.Fprintf(w, "error") return } log.Printf("updated to v4:%s v6:%s", v4, v6) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "updated") } func (a *App) dnsUpdate(v4, v6 net.IP) error { // dns key name as fqdn key := dns.Fqdn(a.DNSKeyName) // remove records (always includes v6) delRra, _ := dns.NewRR(fmt.Sprintf("%s IN A 127.0.0.1", a.DNSRecord)) delRraaaa, _ := dns.NewRR(fmt.Sprintf("%s IN AAAA ::1", a.DNSRecord)) delRrs := []dns.RR{delRra, delRraaaa} // create records rra := &dns.A{ Hdr: dns.RR_Header{Name: a.DNSRecord, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, A: v4, } rrs := []dns.RR{rra} // only v6 if set if v6 != nil { rraaaa := &dns.AAAA{ Hdr: dns.RR_Header{Name: a.DNSRecord, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 300}, AAAA: v6, } rrs = []dns.RR{rra, rraaaa} } // create message m := new(dns.Msg) m.SetUpdate(a.DNSZone) m.RemoveRRset(delRrs) m.Insert(rrs) m.SetTsig(key, a.DNSKeyAlgo, 300, time.Now().Unix()) // client c := &dns.Client{ SingleInflight: true, TsigSecret: map[string]string{key: a.DNSKeySecret}, } // send and handle message reply, _, err := c.Exchange(m, a.DNSServer) if err != nil { return fmt.Errorf("update failed: %w", err) } if reply != nil && reply.Rcode != dns.RcodeSuccess { return fmt.Errorf("dns update failed: %w", err) } return nil }