Initial commit
authorvolpol <volpol@packet-gain.de>
Sat, 9 Jun 2018 14:34:58 +0000 (16:34 +0200)
committervolpol <volpol@packet-gain.de>
Sat, 9 Jun 2018 14:34:58 +0000 (16:34 +0200)
ddnset.go [new file with mode: 0644]

diff --git a/ddnset.go b/ddnset.go
new file mode 100644 (file)
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));
+}