From 5fa7189858e17d5e7b159c983a9c591c7aefdf74 Mon Sep 17 00:00:00 2001 From: George Date: Sun, 26 Jan 2020 14:20:22 -0500 Subject: [PATCH] Update the spec --- README.md | 37 ++++++----- dnsmasq.conf | 3 +- meshnamed.conf | 9 +++ meshnamed.go | 164 +++++++++++++++++++++++++++++++--------------- meshnamed.service | 4 +- 5 files changed, 144 insertions(+), 73 deletions(-) create mode 100644 meshnamed.conf diff --git a/README.md b/README.md index 6deb9c7..c4031c6 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ domain names for server to server communications. Self-organized and trustless networks like CJDNS and Yggdrasil Network are using public-key cryptography for IP address allocation. Every network node owns -a globally unique IPv6 address, and 16 bytes of that address can be -translated to a globally unique domain name. +a globally unique IPv6 address. Binary form of that address can be encoded with +base32 notation for deriving a globally unique name space managed by that node. Since there is no need for a global authority or consensus, such a naming system will reliably work in any network split scenarios. @@ -19,37 +19,40 @@ will reliably work in any network split scenarios. ".mesh.arpa" is ment to be used by machines, not by humans. A human-readable naming system would require a lot more engineering effort. -## How to resolve .mesh.arpa domains +## How .mesh.arpa domains work -Every third level domain in ".mesh.arpa" space represents a single IPv6 address. +Each mesh node can manage its own unique name space in "mesh.arpa." zone. +The name space is derived from its IPv6 address as follows: -Domain "aicrxoqgun7siwm42akzfsox7m.mesh.arpa" is resolved as follows: +1) IPv6 address is converted to its binary form of 16 bytes: -1) Append base32 padding "======" to the upper cased third level domain token; + IPv6Address('200:6fc8:9220:f400:5cc2:305a:4ac6:967e') - AICRXOQGUN7SIWM42AKZFSOX7M====== + b'\x02\x00o\xc8\x92 \xf4\x00\\\xc20ZJ\xc6\x96~' -2) Decode base32 string to a binary IPv6 representation; +2) The binary value is encoded to base32: - b'\x02\x05\x1b\xba\x06\xa3\x7f$Y\x9c\xd0\x15\x92\xc9\xd7\xfb' + AIAG7SESED2AAXGCGBNEVRUWPY====== -3) Convert the resulting 16 bytes to a IPv6 address structure. +3) Padding symbols "======" are removed from the end of the string. - IPv6Address('205:1bba:6a3:7f24:599c:d015:92c9:d7fb') +The resulting name space managed by '200:6fc8:9220:f400:5cc2:305a:4ac6:967e' +is "aiag7sesed2aaxgcgbnevruwpy.mesh.arpa." -If the server cannot translate a given domain name to IP address it should -return empty response. +In order to resolve a domain in "xxx.mesh.arpa." space, the client derives IPv6 +address from the third level domain "xxx" and use it as authoritative DNS server +for that zone. -Every additional subdomain, e.g. "mail.xxx.mesh.arpa, xmpp.xxx.mesh.arpa" -resolves to the same IPv6 address as "xxx.mesh.arpa". +"xxx.mesh.arpa" name is itself managed by the DNS server derived from "xxx" and +can point to any other IPv6 address. ## Why not .ip6.arpa There is a special domain for reverse DNS lookups, but it takes 72 characters to store a single value. The same value in .mesh.arpa takes 36 characters. -"7.c.4.9.0.d.8.f.8.d.2.a.6.4.6.7.8.e.2.d.4.b.1.a.d.4.7.8.0.0.2.0.ip6.arpa" -versus "aicrxoqgun7siwm42akzfsox7m.mesh.arpa" +"e.7.6.9.6.c.a.4.a.5.0.3.2.c.c.5.0.0.4.f.0.2.2.9.8.c.f.6.0.0.2.0.ip6.arpa" +versus "aiag7sesed2aaxgcgbnevruwpy.mesh.arpa." This saves twice amount of bandwidth and storage space. It is also arguably more aesthetically appealing, even though that's not a goal. diff --git a/dnsmasq.conf b/dnsmasq.conf index 71de853..06f7fd4 100644 --- a/dnsmasq.conf +++ b/dnsmasq.conf @@ -1,6 +1,5 @@ port=53 domain-needed bogus-priv -server=/mesh.arpa/127.0.0.1#53535 +server=/mesh.arpa/::1#53535 server=8.8.8.8 -interface=lo diff --git a/meshnamed.conf b/meshnamed.conf new file mode 100644 index 0000000..1b8e41c --- /dev/null +++ b/meshnamed.conf @@ -0,0 +1,9 @@ +{ + "Domain":"aiag7sesed2aaxgcgbnevruwpy", + "Records": [ + "aiag7sesed2aaxgcgbnevruwpy.mesh.arpa. AAAA 200:6fc8:9220:f400:5cc2:305a:4ac6:967e", + "_xmpp-client._tcp.aiag7sesed2aaxgcgbnevruwpy.mesh.arpa. SRV 5 0 5222 xmpp.aiag7sesed2aaxgcgbnevruwpy.mesh.arpa", + "_xmpp-server._tcp.aiag7sesed2aaxgcgbnevruwpy.mesh.arpa. SRV 5 0 5269 xmpp.aiag7sesed2aaxgcgbnevruwpy.mesh.arpa", + "xmpp.aiag7sesed2aaxgcgbnevruwpy.mesh.arpa. AAAA 300:6fc8:9220:f400::1" + ] +} diff --git a/meshnamed.go b/meshnamed.go index 8e2bfd4..3dfc89c 100644 --- a/meshnamed.go +++ b/meshnamed.go @@ -2,35 +2,61 @@ package main import ( "encoding/base32" - "fmt" - "net" - "strings" + "encoding/json" "errors" + "fmt" + "io" + "net" "os" + "strings" "github.com/miekg/dns" ) const domainZone = "mesh.arpa." -const maxTtl = 4294967295 var _, validSubnet, _ = net.ParseCIDR("::/0") +var zoneConfigPath = "" +var zoneConfig = map[string][]dns.RR{} +var dnsClient = new(dns.Client) -var srvPortMap = map[string]uint16{ - "_xmpp-client._tcp": 5222, - "_xmpp-server._tcp": 5269, - "_submission._tcp": 587, // rfc6186 - "_imap._tcp": 143, - "_imaps._tcp": 993, - "_pop3._tcp": 110, - "_pop3s._tcp": 995, - "_matrix._tcp": 8448, // https://matrix.org/docs/spec/server_server/unstable#server-discovery - "_sip._tcp": 5060, // rfc3263 - "_sip._udp": 5060, - "_sips._tcp": 5061, +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) @@ -50,15 +76,25 @@ func lookup(domain string) (net.IP, error) { 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 _, v := range r.Question { - if v.Qclass != dns.ClassINET { - continue - } - labels := dns.SplitDomainName(v.Name) + for _, q := range r.Question { + labels := dns.SplitDomainName(q.Name) if len(labels) < 3 { continue } @@ -68,48 +104,72 @@ func handleRequest(w dns.ResponseWriter, r *dns.Msg) { if err != nil { continue } - if v.Qtype == dns.TypeAAAA { - r := new(dns.AAAA) - r.Hdr = dns.RR_Header{Name: v.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: maxTtl} - r.AAAA = resolvedAddr - m.Answer = append(m.Answer, r) - } else if v.Qtype == dns.TypeSRV { - if len(labels) < 5 { - continue - } - - if srvRec := labels[0] + "." + labels[1]; srvPortMap[srvRec] != 0 { - r := new(dns.SRV) - r.Hdr = dns.RR_Header{Name: v.Name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: maxTtl} - r.Priority = 0 - r.Weight = 0 - r.Port = srvPortMap[srvRec] - r.Target = subDomain + "." + domainZone - m.Answer = append(m.Answer, r) + 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() { - addr := "127.0.0.1:53535" - if os.Getenv("LISTEN_ADDR") != "" { - addr = os.Getenv("LISTEN_ADDR") + helpMessage := "Usage:\nmeshnamed genconf [IP] > /etc/meshnamed.conf\nmeshnamed daemon /etc/meshnamed.conf" + if len(os.Args) < 2 { + fmt.Println(helpMessage) + return } - if os.Getenv("MESH_SUBNET") != "" { - _, meshSubnet, err := net.ParseCIDR(os.Getenv("MESH_SUBNET")) + action := os.Args[1] + if action == "genconf" && len(os.Args) == 3 { + confString, err := genConf(os.Args[2]) if err != nil { fmt.Println(err) - os.Exit(1) + } else { + fmt.Println(confString) + } + } else if action == "daemon" { + if len(os.Args) == 3 { + zoneConfigPath = os.Args[2] + loadConfig() } - validSubnet = meshSubnet - } - server := &dns.Server{Addr: addr, Net: "udp"} - fmt.Println("Started meshnamed on:", addr) - dns.HandleFunc(domainZone, handleRequest) - server.ListenAndServe() + 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) + } } diff --git a/meshnamed.service b/meshnamed.service index 5d6f7ea..8c0e3fa 100644 --- a/meshnamed.service +++ b/meshnamed.service @@ -9,9 +9,9 @@ Group=nogroup ProtectHome=true ProtectSystem=true SyslogIdentifier=meshnamed -Environment="LISTEN_ADDR=127.0.0.1:53535" +Environment="LISTEN_ADDR=[::1]:53535" Environment="MESH_SUBNET=::/0" -ExecStart=/usr/local/bin/meshnamed +ExecStart=/usr/local/bin/meshnamed daemon /etc/meshnamed.conf Restart=always TimeoutStopSec=5