diff --git a/README.md b/README.md index 43791ca..fb5d3cb 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,15 @@ make ``` 2) Generate the default config for your host ``` -./meshnamed genconf 200:6fc8:9220:f400:5cc2:305a:4ac6:967e | tee /tmp/meshnamed.conf +./meshnamed -genconf 200:6fc8:9220:f400:5cc2:305a:4ac6:967e | tee /tmp/meshnamed.conf ``` -3) Optionally, set the configuration with environment variables +3) Run the daemon ``` -export LISTEN_ADDR=[::1]:53535 -export MESH_SUBNET=200::/7 +./meshnamed -useconffile /tmp/meshnamed.conf ``` -4) Run the daemon +4) Optionally, set the configuration flags ``` -./meshnamed daemon /tmp/meshnamed.conf +./meshnamed -listenaddr [::1]:53535 -meshsubnet 200::/7 -debug -useconffile /tmp/meshnamed.conf ``` Add new DNS records to configuration file and restart the daemon to apply settings. A record can be of any valid string form parsed by [miekg/dns](https://godoc.org/github.com/miekg/dns#NewRR). @@ -61,9 +60,9 @@ In this example, meshnamed is configured as authoritative for two domain zones: ## Using meshnamed as a standalone DNS server -Set environment varialbe to listen on all interfaces and a standard DNS server port +Set the flag to listen on all interfaces and a standard DNS server port - export LISTEN_ADDR=[::]:53 + ./meshnamed -listenaddr [::]:53 -useconffile /tmp/meshnamed.conf -Allow incoming connections to port 53/UDP in firewall settings. +Run as root and allow incoming connections to port 53/UDP in firewall settings. diff --git a/cmd/meshname/main.go b/cmd/meshname/main.go index 07aa208..8a20a32 100644 --- a/cmd/meshname/main.go +++ b/cmd/meshname/main.go @@ -1,47 +1,17 @@ package main import ( - "encoding/base32" "fmt" "net" "strings" "os" - "errors" + + "github.com/zhoreeq/meshname/src/meshname" ) -var domainZone = ".meshname" - -func reverse_lookup(target string) (string, error) { - ip := net.ParseIP(target) - if ip == nil { - return "", errors.New("Invalid IP address") - } - str := base32.StdEncoding.EncodeToString(ip)[0:26] - return strings.ToLower(str) + domainZone, nil -} - -func lookup(target string) (string, error) { - labels := strings.Split(target, ".") - if len(labels) < 2 || strings.HasSuffix(domainZone, target) { - return "", errors.New("Invalid domain") - } - subDomain := labels[len(labels) - 2] - if len(subDomain) != 26 { - return "", errors.New("Invalid subdomain length") - } - name := strings.ToUpper(subDomain) + "======" - data, err := base32.StdEncoding.DecodeString(name) - if err != nil { - return "", err - } - s := net.IP(data) - if s == nil { - return "", errors.New("Invalid IP address") - } - return s.String(), nil -} - func main() { + domainZone := strings.TrimSuffix(meshname.DomainZone, ".") + usage := "Usage:\n\nmeshname lookup DOMAIN\nmeshname reverse_lookup IP" if len(os.Args) != 3 { fmt.Println(usage) @@ -53,20 +23,32 @@ func main() { switch action { case "lookup": - result, err := lookup(target) + labels := strings.Split(target, ".") + if len(labels) < 2 || !strings.HasSuffix(target, domainZone) { + fmt.Println("Invalid domain") + return + } + subDomain := labels[len(labels) - 2] + if len(subDomain) != 26 { + fmt.Println("Invalid subdomain length") + return + } + + result, err := meshname.IPFromDomain(subDomain) if err != nil { fmt.Println("Error:", err) return } - fmt.Println(result) + fmt.Println(result.String()) return case "reverse_lookup": - result, err := reverse_lookup(target) - if err != nil { - fmt.Println("Error:", err) + ip := net.ParseIP(target) + if ip == nil { + fmt.Println("Invalid IP address") return } - fmt.Println(result) + result := meshname.DomainFromIP(ip) + fmt.Println(result + "." + domainZone) return default: fmt.Println(usage) diff --git a/cmd/meshnamed/main.go b/cmd/meshnamed/main.go index a12194b..5d5063e 100644 --- a/cmd/meshnamed/main.go +++ b/cmd/meshnamed/main.go @@ -1,175 +1,55 @@ package main import ( - "encoding/base32" - "encoding/json" - "errors" - "fmt" - "io" "net" "os" - "strings" + "fmt" + "flag" - "github.com/miekg/dns" + "github.com/gologme/log" + + "github.com/zhoreeq/meshname/src/meshname" ) -const domainZone = "meshname." - -var _, validSubnet, _ = net.ParseCIDR("::/0") -var zoneConfigPath = "" -var zoneConfig = map[string][]dns.RR{} -var dnsClient = new(dns.Client) - -func loadConfig() { - if zoneConfigPath == "" { - return - } - - reader, err := os.Open(zoneConfigPath) - if err != nil { - fmt.Println("Can't open config:", err) - return - } - - type Zone struct { - Domain string - Records []string - } - - dec := json.NewDecoder(reader) - for { - var m Zone - if err := dec.Decode(&m); err == io.EOF { - break - } else if err != nil { - fmt.Println("Syntax error in config:", err) - return - } - for _, v := range m.Records { - rr, err := dns.NewRR(v) - if err != nil { - fmt.Println("Invalid DNS record:", v) - continue - } - zoneConfig[m.Domain] = append(zoneConfig[m.Domain], rr) - } - } - fmt.Println("Config loaded:", zoneConfigPath) -} - -func lookup(domain string) (net.IP, error) { - name := strings.ToUpper(domain) + "======" - data, err := base32.StdEncoding.DecodeString(name) - if err != nil { - return net.IP{}, err - } - if len(data) != 16 { - return net.IP{}, errors.New("Invalid subdomain") - } - ipAddr := net.IP(data) - if ipAddr == nil { - return net.IP{}, errors.New("Invalid IP address") - } - if !validSubnet.Contains(ipAddr) { - return net.IP{}, errors.New("Address from invalid subnet") - } - return ipAddr, nil -} - -func genConf(target string) (string, error) { - ip := net.ParseIP(target) - if ip == nil { - return "", errors.New("Invalid IP address") - } - zone := strings.ToLower(base32.StdEncoding.EncodeToString(ip)[0:26]) - selfRecord := fmt.Sprintf("\t\t\"%s.%s AAAA %s\"\n", zone, domainZone, target) - confString := fmt.Sprintf("{\n\t\"Domain\":\"%s\",\n\t\"Records\":[\n%s\t]\n}", zone, selfRecord) - - return confString, nil -} - -func handleRequest(w dns.ResponseWriter, r *dns.Msg) { - var remoteLookups = map[string][]dns.Question{} - m := new(dns.Msg) - m.SetReply(r) - - for _, q := range r.Question { - labels := dns.SplitDomainName(q.Name) - if len(labels) < 2 { - continue - } - subDomain := labels[len(labels)-2] - - resolvedAddr, err := lookup(subDomain) - if err != nil { - continue - } - if records, ok := zoneConfig[subDomain]; ok { - for _, rec := range records { - if h := rec.Header(); h.Name == q.Name && h.Rrtype == q.Qtype && h.Class == q.Qclass { - m.Answer = append(m.Answer, rec) - } - } - } else if ra := w.RemoteAddr().String(); strings.HasPrefix(ra, "[::1]:") || strings.HasPrefix(ra, "127.0.0.1:") { - // do remote lookups only for local clients - remoteLookups[resolvedAddr.String()] = append(remoteLookups[resolvedAddr.String()], q) - } - } - - for remoteServer, questions := range remoteLookups { - rm := new(dns.Msg) - rm.Question = questions - resp, _, err := dnsClient.Exchange(rm, "["+remoteServer+"]:53") // no retries - if err != nil { - continue - } - m.Answer = append(m.Answer, resp.Answer...) - } - w.WriteMsg(m) -} func main() { - helpMessage := "Usage:\nmeshnamed genconf [IP] > /etc/meshnamed.conf\nmeshnamed daemon /etc/meshnamed.conf" - if len(os.Args) < 2 { - fmt.Println(helpMessage) - return + genconf := flag.String("genconf", "", "generate a new config for IP address") + useconffile := flag.String("useconffile", "", "run daemon with a config file") + listenAddr := flag.String("listenaddr", "[::1]:53535", "address to listen on") + meshSubnetStr := flag.String("meshsubnet", "::/0", "valid IPv6 address space") + debug := flag.Bool("debug", false, "enable debug logging") + flag.Parse() + + var logger *log.Logger + logger = log.New(os.Stdout, "", log.Flags()) + + logger.EnableLevel("error") + logger.EnableLevel("warn") + logger.EnableLevel("info") + if *debug { + logger.EnableLevel("debug") } - action := os.Args[1] - if action == "genconf" && len(os.Args) == 3 { - confString, err := genConf(os.Args[2]) + switch { + case *genconf != "": + confString, err := meshname.GenConf(*genconf) if err != nil { - fmt.Println(err) + logger.Errorln(err) } else { fmt.Println(confString) } - } else if action == "daemon" { - if len(os.Args) == 3 { - zoneConfigPath = os.Args[2] - loadConfig() + case *useconffile != "": + s := new(meshname.MeshnameServer) + + _, validSubnet, err := net.ParseCIDR(*meshSubnetStr) + if err != nil { + logger.Errorln(err) + os.Exit(1) } - addr := "[::1]:53535" - if os.Getenv("LISTEN_ADDR") != "" { - addr = os.Getenv("LISTEN_ADDR") - } - - if os.Getenv("MESH_SUBNET") != "" { - _, meshSubnet, err := net.ParseCIDR(os.Getenv("MESH_SUBNET")) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - validSubnet = meshSubnet - } - - dnsClient.Timeout = 5000000000 // increased 5 seconds timeout - - dnsServer := &dns.Server{Addr: addr, Net: "udp"} - fmt.Println("Started meshnamed on:", addr) - dns.HandleFunc(domainZone, handleRequest) - dnsServer.ListenAndServe() - } else { - fmt.Println(helpMessage) + s.Init(logger, meshname.MeshnameOptions{ListenAddr: *listenAddr, ConfigPath: *useconffile, ValidSubnet: validSubnet}) + s.Start() + default: + flag.PrintDefaults() } } diff --git a/go.mod b/go.mod index 04189a1..8c93e32 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/zhoreeq/meshname go 1.13 -require github.com/miekg/dns v1.1.27 +require ( + github.com/gologme/log v1.2.0 + github.com/miekg/dns v1.1.27 +) diff --git a/go.sum b/go.sum index c27a619..8e8d3bb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= +github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/meshnamed.service b/meshnamed.service index 8c0e3fa..ea3c0c7 100644 --- a/meshnamed.service +++ b/meshnamed.service @@ -9,9 +9,7 @@ Group=nogroup ProtectHome=true ProtectSystem=true SyslogIdentifier=meshnamed -Environment="LISTEN_ADDR=[::1]:53535" -Environment="MESH_SUBNET=::/0" -ExecStart=/usr/local/bin/meshnamed daemon /etc/meshnamed.conf +ExecStart=/usr/local/bin/meshnamed -listenaddr [::1]:53535 -meshsubnet 200::/7 -useconffile /etc/meshnamed.conf Restart=always TimeoutStopSec=5 diff --git a/src/meshname/server.go b/src/meshname/server.go new file mode 100644 index 0000000..17a2b42 --- /dev/null +++ b/src/meshname/server.go @@ -0,0 +1,172 @@ +package meshname + +import ( + "encoding/base32" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + + "github.com/gologme/log" + "github.com/miekg/dns" +) + +const DomainZone = "meshname." + +func DomainFromIP(target net.IP) string { + return strings.ToLower(base32.StdEncoding.EncodeToString(target)[0:26]) +} + +func IPFromDomain(domain string) (net.IP, error) { + name := strings.ToUpper(domain) + "======" + data, err := base32.StdEncoding.DecodeString(name) + if err != nil { + return net.IP{}, err + } + if len(data) != 16 { + return net.IP{}, errors.New("Invalid subdomain") + } + ipAddr := net.IP(data) + if ipAddr == nil { + return net.IP{}, errors.New("Invalid IP address") + } + return ipAddr, nil +} + +func GenConf(target string) (string, error) { + ip := net.ParseIP(target) + if ip == nil { + return "", errors.New("Invalid IP address") + } + zone := DomainFromIP(ip) + selfRecord := fmt.Sprintf("\t\t\"%s.%s AAAA %s\"\n", zone, DomainZone, target) + confString := fmt.Sprintf("{\n\t\"Domain\":\"%s\",\n\t\"Records\":[\n%s\t]\n}", zone, selfRecord) + + return confString, nil +} + +type MeshnameServer struct { + validSubnet *net.IPNet + log *log.Logger + listenAddr, zoneConfigPath string + zoneConfig map[string][]dns.RR + dnsClient *dns.Client +} + +type MeshnameOptions struct { + ListenAddr, ConfigPath string + ValidSubnet *net.IPNet +} + +func (s *MeshnameServer) Init(log *log.Logger, options interface{}) { + mnoptions := options.(MeshnameOptions) + s.log = log + s.listenAddr = mnoptions.ListenAddr + s.validSubnet = mnoptions.ValidSubnet + s.zoneConfigPath = mnoptions.ConfigPath + s.zoneConfig = make(map[string][]dns.RR) + if s.dnsClient == nil { + s.dnsClient = new(dns.Client) + s.dnsClient.Timeout = 5000000000 // increased 5 seconds timeout + } + s.LoadConfig() +} + +func (s *MeshnameServer) LoadConfig() { + if s.zoneConfigPath == "" { + return + } + for k := range s.zoneConfig { + delete(s.zoneConfig, k) + } + + reader, err := os.Open(s.zoneConfigPath) + if err != nil { + s.log.Errorln("Can't open config:", err) + return + } + + type Zone struct { + Domain string + Records []string + } + + dec := json.NewDecoder(reader) + for { + var m Zone + if err := dec.Decode(&m); err == io.EOF { + break + } else if err != nil { + s.log.Errorln("Syntax error in config:", err) + return + } + for _, v := range m.Records { + rr, err := dns.NewRR(v) + if err != nil { + s.log.Errorln("Invalid DNS record:", v) + continue + } + s.zoneConfig[m.Domain] = append(s.zoneConfig[m.Domain], rr) + } + } + s.log.Infoln("Meshname config loaded:", s.zoneConfigPath) +} + +func (s *MeshnameServer) Start() { + dnsServer := &dns.Server{Addr: s.listenAddr, Net: "udp"} + s.log.Infoln("Started meshnamed on:", s.listenAddr) + dns.HandleFunc(DomainZone, s.handleRequest) + dnsServer.ListenAndServe() +} + +func (s *MeshnameServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) { + var remoteLookups = make(map[string][]dns.Question) + m := new(dns.Msg) + m.SetReply(r) + + for _, q := range r.Question { + labels := dns.SplitDomainName(q.Name) + if len(labels) < 2 { + s.log.Debugln("Error: invalid domain requested") + continue + } + subDomain := labels[len(labels)-2] + + resolvedAddr, err := IPFromDomain(subDomain) + if err != nil { + s.log.Debugln(err) + continue + } + if !s.validSubnet.Contains(resolvedAddr) { + s.log.Debugln("Error: subnet doesn't match") + continue + } + if records, ok := s.zoneConfig[subDomain]; ok { + for _, rec := range records { + if h := rec.Header(); h.Name == q.Name && h.Rrtype == q.Qtype && h.Class == q.Qclass { + m.Answer = append(m.Answer, rec) + } + } + } else if ra := w.RemoteAddr().String(); strings.HasPrefix(ra, "[::1]:") || strings.HasPrefix(ra, "127.0.0.1:") { + // TODO prefix whitelists ? + // do remote lookups only for local clients + remoteLookups[resolvedAddr.String()] = append(remoteLookups[resolvedAddr.String()], q) + } + } + + for remoteServer, questions := range remoteLookups { + rm := new(dns.Msg) + rm.Question = questions + resp, _, err := s.dnsClient.Exchange(rm, "["+remoteServer+"]:53") // no retries + if err != nil { + s.log.Debugln(err) + continue + } + m.Answer = append(m.Answer, resp.Answer...) + } + w.WriteMsg(m) +} +