From a1623f112b61fe36524f873be5215ac2b16f041e Mon Sep 17 00:00:00 2001 From: volpol Date: Sat, 9 Jun 2018 16:34:58 +0200 Subject: [PATCH] Initial commit --- ddnset.go | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 ddnset.go diff --git a/ddnset.go b/ddnset.go new file mode 100644 index 0000000..1d1a04a --- /dev/null +++ b/ddnset.go @@ -0,0 +1,277 @@ +package main + +import ( + "net" + "net/http" + "log" + "strings" + "os" + "os/exec" + "io" + "io/ioutil" + "bufio" + "encoding/base64" + "crypto/md5" + "fmt" +) + +//TODO add parsing of cmdline +const ( + AccFile = "/usr/local/etc/ddnset/ddnset.acc" + ZoneFile = "/usr/local/etc/ddnset/ddnset.zon" + Logfile = "/var/log/ddnset/default.log" + ListenPort = "1980" +) + +const ( + StatusGood = "good" + StatusBadAuth = "badauth" + StatusBadDay = "911" + StatusNotFQHN = "notfqdn" + StatusBadHost = "nohost" + StatusBadZone = "dnserr" //??? + StatusBadIP = "badip" +) + + +func isPrivate (ip net.IP) bool { + if ip4 := ip.To4(); ip4 != nil { + return ( (10 == ip4[0] ) || ( 192 == ip4[0] && 168 == ip4[1] ) || ( 172 == ip4[0] && (ip4[1] >= 16 && ip4[1] <= 31) )) + } + //TODO what about them new v6 addresses!? + return false; +} + +// isDomainName checks if a string is a presentation-format domain name +// (currently restricted to hostname-compatible "preferred name" LDH labels and +// SRV-like "underscore labels"; see golang.org/issue/12421). +func isDomainName(s string) bool { + // See RFC 1035, RFC 3696. + // Presentation format has dots before every label except the first, and the + // terminal empty label is optional here because we assume fully-qualified + // (absolute) input. We must therefore reserve space for the first and last + // labels' length octets in wire format, where they are necessary and the + // maximum total length is 255. + // So our _effective_ maximum is 253, but 254 is not rejected if the last + // character is a dot. + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + + last := byte('.') + ok := false // Ok once we've seen a letter. + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + ok = true + partlen++ + case '0' <= c && c <= '9': + // fine + partlen++ + case c == '-': + // Byte before dash cannot be dot. + if last == '.' { + return false + } + partlen++ + case c == '.': + // Byte before dot cannot be dot, dash. + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + + return ok +} + +func checkIP (s string) (string, int, bool) { + valid := true + ipver := 4 + //parse the hell out of it + ip := net.ParseIP (s); + if nil == ip.To4() { ipver = 6 } + if isPrivate(ip) { valid = false } + if !ip.IsGlobalUnicast() { valid = false } + return ip.String(), ipver, valid; +} + +func isValidAuth (usr, psw string) (bool, string) { + pwentry := fmt.Sprintf("%s:%x", usr, md5.Sum([]byte(psw))) + acc,err := os.Open(AccFile) + if (nil == err){ + defer acc.Close() + scanner := bufio.NewScanner(acc) + for scanner.Scan(){ + line := scanner.Text() + if (strings.HasPrefix(line, pwentry)) { + //log.Print ("Irasshai!") + return true, strings.TrimPrefix (line, pwentry + ":") + } + } + } + //log.Print ("Okotowari yo!") + return false, "" +} + +func isValidZone (h string) (bool, string) { + zon,err := os.Open(ZoneFile) + if (nil == err){ + defer zon.Close() + scanner := bufio.NewScanner(zon) + for scanner.Scan(){ + line := scanner.Text() + if (strings.HasSuffix(h, line)) { +// log.Printf ("%s is in zone %s", h, line) + return true, line + } + } + } +// log.Printf ("No zone for the wicked %s", h) + return false, "" +} + +//TODO implement no temp file version +func knsupdate(host, ip, zone string, ver int) bool { + rectype := "A" + if (6 == ver) { rectype = "AAAA" } + tmpfile, err := ioutil.TempFile("", "knsupdate") + if err != nil { + log.Print(err) + return false + } + //log.Print ("Using ", tmpfile.Name()) + defer os.Remove(tmpfile.Name()) // clean up + fmt.Fprintf (tmpfile, "server 127.0.0.1\n"); + fmt.Fprintf (tmpfile, "zone %s\n", zone); + fmt.Fprintf (tmpfile, "del %s 300 IN %s\n", host, rectype); + fmt.Fprintf (tmpfile, "add %s 300 IN %s %s\n", host, rectype, ip); + fmt.Fprintf (tmpfile, "send\n"); + tmpfile.Close() + out, err := exec.Command("/usr/bin/knsupdate", tmpfile.Name()).Output() + + if err != nil { + log.Print(err) + return false + } + + if (strings.Contains(string(out), "Error")) { + log.Printf("%q", out); + return false; + } + + return true +} + + + +func myipHandle(w http.ResponseWriter, r *http.Request){ + log.Print(r.RemoteAddr, " ", r.Method, " ", r.URL.String()); + clientIP, _, err := net.SplitHostPort(r.RemoteAddr); + //TODO support more proxy headers and + //walk them until first valid address + //fallback to r.RemoteAddr if none are available + if nil != err { log.Print(err); return ;} + if prior, ok := r.Header["X-Forwarded-For"]; ok { + log.Print ("XFF header present") + clientIP = strings.Split(prior[0], ",")[0] + } + clientIP, _, _ = checkIP(clientIP) + log.Printf ("Client IP = %s", clientIP); + io.WriteString(w, clientIP) +} + +func rootHandle(w http.ResponseWriter, r *http.Request){ + + log.Print(r.RemoteAddr, " ", r.Method, " ", r.URL.String()); + //check auth + authok := false + hosts := "" + if hdr_auth, ok := r.Header["Authorization"]; ok { + log.Print ("Authorization header present") + for _, auth := range (hdr_auth) { + auth := strings.Split (auth, " ") + log.Print ("Method: ", auth[0]) + if ("Basic" == auth[0]){ + authdec, err := base64.StdEncoding.DecodeString(auth[1]) + if nil != err { continue } + usrpw := strings.Split (string(authdec), ":") + log.Printf ("User: %s", usrpw[0]); + authok, hosts = isValidAuth(usrpw[0], usrpw[1]) + } + } + } + if false == authok { + log.Print("StatusBadAuth"); + io.WriteString(w, StatusBadAuth + "\n"); + return; + } + + log.Print ("User hosts: ", hosts); + //check ip + q := r.URL.Query() + hostnames := q["hostname"] + myip, ver, valid := checkIP(q["myip"][0]) //one IP to rule them all + log.Printf("IP: %s (valid:%v, ver:%d)", myip, valid, ver); + if (!valid){ + log.Print("StatusBadIP"); + io.WriteString(w, StatusBadIP + "\n"); + return; + } + //update + for _, host := range (hostnames) { + if !isDomainName(host) { + log.Print("StatusNotFQHN"); + io.WriteString(w, StatusNotFQHN + "\n"); + continue + } + vz, zone := isValidZone(host) + if !vz { + log.Print("StatusBadZone"); + io.WriteString(w, StatusBadZone + "\n"); + continue + } + if (strings.Contains (hosts, host)) { + log.Printf ("Updating %s to IP %s", host, myip); + updated := knsupdate (host, myip, zone, ver) + if updated { + log.Print("StatusGood"); + io.WriteString(w, StatusGood + "\n"); + } else { + log.Print("StatusBadDay"); + io.WriteString(w, StatusBadDay + "\n"); + } + } else { + log.Print("StatusBadHost"); + io.WriteString(w, StatusBadHost + "\n"); + } + } +} + +func main() { + http.HandleFunc("/myip", myipHandle); + http.HandleFunc("/myip.php", myipHandle); + http.HandleFunc("/", rootHandle); + logf, err := os.OpenFile(Logfile, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644) + if nil == err{ + defer logf.Close() + log.SetOutput (logf) + } else { + log.Print (err) + } + + log.Fatal(http.ListenAndServe(":" + ListenPort, nil)); +} -- 2.30.2