From 1f8b1ea7ad9e552f6f61127531f7b2f5b7844427 Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Mon, 18 Sep 2017 00:55:31 +0800 Subject: [PATCH] Update --- Makefile | 24 ++++ doh-client/client.go | 209 ++++++++++++++++++++++++++++++++ doh-client/main.go | 32 +++++ doh-server/main.go | 46 +++++++ doh-server/server.go | 270 ++++++++++++++++++++++++++++++++++++++++++ json-dns/error.go | 42 +++++++ json-dns/globalip.go | 124 +++++++++++++++++++ json-dns/marshal.go | 109 +++++++++++++++++ json-dns/response.go | 67 +++++++++++ json-dns/unmarshal.go | 169 ++++++++++++++++++++++++++ 10 files changed, 1092 insertions(+) create mode 100644 Makefile create mode 100644 doh-client/client.go create mode 100644 doh-client/main.go create mode 100644 doh-server/main.go create mode 100644 doh-server/server.go create mode 100644 json-dns/error.go create mode 100644 json-dns/globalip.go create mode 100644 json-dns/marshal.go create mode 100644 json-dns/response.go create mode 100644 json-dns/unmarshal.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8e4af6a --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: all clean install uninstall + +GOBUILD=go build +PREFIX=/usr/local + +all: doh-client/doh-client doh-server/doh-server + +clean: + rm -f doh-client/doh-client doh-server/doh-server + +install: doh-client/doh-client doh-server/doh-server + install -Dm0755 doh-client/doh-client $(PREFIX)/bin/doh-client + install -Dm0755 doh-server/doh-server $(PREFIX)/bin/doh-server + setcap cap_net_bind_service=+ep $(PREFIX)/bin/doh-client + setcap cap_net_bind_service=+ep $(PREFIX)/bin/doh-server + +uninstall: + rm -f $(PREFIX)/bin/doh-client $(PREFIX)/bin/doh-server + +doh-client/doh-client: doh-client/client.go doh-client/main.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go + cd doh-client && $(GOBUILD) + +doh-server/doh-server: doh-server/main.go doh-server/server.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go + cd doh-server && $(GOBUILD) diff --git a/doh-client/client.go b/doh-client/client.go new file mode 100644 index 0000000..00566cc --- /dev/null +++ b/doh-client/client.go @@ -0,0 +1,209 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "strconv" + "time" + "github.com/miekg/dns" + "../json-dns" +) + +type Client struct { + addr string + upstream string + udpServer *dns.Server + tcpServer *dns.Server +} + +func NewClient(addr, upstream string) (c *Client) { + c = &Client { + addr: addr, + upstream: upstream, + } + c.udpServer = &dns.Server { + Addr: addr, + Net: "udp", + Handler: dns.HandlerFunc(c.udpHandlerFunc), + UDPSize: 4096, + } + c.tcpServer = &dns.Server { + Addr: addr, + Net: "tcp", + Handler: dns.HandlerFunc(c.tcpHandlerFunc), + } + return +} + +func (c *Client) Start() error { + result := make(chan error) + go func() { + err := c.udpServer.ListenAndServe() + if err != nil { + log.Println(err) + } + result <- err + } () + go func() { + err := c.tcpServer.ListenAndServe() + if err != nil { + log.Println(err) + } + result <- err + } () + err := <-result + if err != nil { + return err + } + err = <-result + return err +} + +func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { + if r.Response == true { + log.Println("Received a response packet") + return + } + + reply := jsonDNS.PrepareReply(r) + + if len(r.Question) != 1 { + log.Println("Number of questions is not 1") + reply.Rcode = dns.RcodeFormatError + w.WriteMsg(reply) + return + } + question := r.Question[0] + questionName := question.Name + questionType := "" + if qtype, ok := dns.TypeToString[question.Qtype]; ok { + questionType = qtype + } else { + questionType = strconv.Itoa(int(question.Qtype)) + } + + fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType) + + requestURL := fmt.Sprintf("%s?name=%s&type=%s", c.upstream, url.QueryEscape(questionName), url.QueryEscape(questionType)) + + if r.CheckingDisabled { + requestURL += "&cd=1" + } + + udpSize := uint16(512) + if opt := r.IsEdns0(); opt != nil { + udpSize = opt.UDPSize() + } + + ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r) + if ednsClientAddress != nil { + requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask) + } + + resp, err := http.Get(requestURL) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + + var respJson jsonDNS.Response + err = json.Unmarshal(body, &respJson) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + + fullReply := jsonDNS.Unmarshal(reply, &respJson, udpSize, ednsClientNetmask) + buf, err := fullReply.Pack() + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + if !isTCP && len(buf) > int(udpSize) { + fullReply.Truncated = true + buf, err = fullReply.Pack() + if err != nil { + log.Println(err) + return + } + buf = buf[:udpSize] + } + w.Write(buf) +} + +func (c *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { + c.handlerFunc(w, r, false) +} + +func (c *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { + c.handlerFunc(w, r, true) +} + +var ( + ipv4Mask24 net.IPMask = net.IPMask { 255, 255, 255, 0 } + ipv6Mask48 net.IPMask = net.CIDRMask(48, 128) +) + +func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) { + ednsClientNetmask = 255 + opt := r.IsEdns0() + for _, option := range opt.Option { + if option.Option() == dns.EDNS0SUBNET { + edns0Subnet := option.(*dns.EDNS0_SUBNET) + ednsClientAddress = edns0Subnet.Address + ednsClientNetmask = edns0Subnet.SourceNetmask + return + } + } + remoteAddr, err := net.ResolveUDPAddr("udp", w.RemoteAddr().String()) + if err != nil { + return + } + if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) { + if ipv4 := ip.To4(); ipv4 != nil { + ednsClientAddress = ipv4.Mask(ipv4Mask24) + ednsClientNetmask = 24 + } else { + ednsClientAddress = ip.Mask(ipv6Mask48) + ednsClientNetmask = 48 + } + } + return +} diff --git a/doh-client/main.go b/doh-client/main.go new file mode 100644 index 0000000..cf52026 --- /dev/null +++ b/doh-client/main.go @@ -0,0 +1,32 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "flag" +) + +func main() { + addr := flag.String("addr", ":5533", "DNS listen port") + upstream := flag.String("upstream", "http://localhost:8080/resolve", "HTTP path for upstream resolver") + flag.Parse() + + client := NewClient(*addr, *upstream) + _ = client.Start() +} diff --git a/doh-server/main.go b/doh-server/main.go new file mode 100644 index 0000000..d2f1a4f --- /dev/null +++ b/doh-server/main.go @@ -0,0 +1,46 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "flag" + "log" + "strings" +) + +func main() { + addr := flag.String("addr", ":8080", "HTTP listen port") + cert := flag.String("cert", "", "TLS certification file") + key := flag.String("key", "", "TLS key file") + path := flag.String("path", "/resolve", "HTTP path for resolve application") + upstream := flag.String("upstream", "8.8.8.8:53,8.8.4.4:53", "Upstream DNS resolver") + tcpOnly := flag.Bool("tcp", false, "Only use TCP for DNS query") + flag.Parse() + + if (*cert != "") != (*key != "") { + log.Fatalln("You must specify both -cert and -key to enable TLS") + } + + upstreams := strings.Split(*upstream, ",") + server := NewServer(*addr, *cert, *key, *path, upstreams, *tcpOnly) + err := server.Start() + if err != nil { + log.Fatalln(err) + } +} diff --git a/doh-server/server.go b/doh-server/server.go new file mode 100644 index 0000000..b45726f --- /dev/null +++ b/doh-server/server.go @@ -0,0 +1,270 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "encoding/json" + "fmt" + "math/rand" + "log" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" + "golang.org/x/net/idna" + "github.com/gorilla/handlers" + "github.com/miekg/dns" + "../json-dns" +) + +type Server struct { + addr string + cert string + key string + path string + upstreams []string + tcpOnly bool + udpClient *dns.Client + tcpClient *dns.Client + servemux *http.ServeMux +} + +func NewServer(addr, cert, key, path string, upstreams []string, tcpOnly bool) (s *Server) { + upstreamsCopy := make([]string, len(upstreams)) + copy(upstreamsCopy, upstreams) + s = &Server { + addr: addr, + cert: cert, + key: key, + path: path, + upstreams: upstreamsCopy, + tcpOnly: tcpOnly, + udpClient: &dns.Client { + Net: "udp", + }, + tcpClient: &dns.Client { + Net: "tcp", + }, + servemux: http.NewServeMux(), + } + s.servemux.HandleFunc(path, s.handlerFunc) + return +} + +func (s *Server) Start() error { + if s.cert != "" || s.key != "" { + return http.ListenAndServeTLS(s.addr, s.cert, s.key, handlers.CombinedLoggingHandler(os.Stdout, s.servemux)) + } else { + return http.ListenAndServe(s.addr, handlers.CombinedLoggingHandler(os.Stdout, s.servemux)) + } +} + +func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + name := r.FormValue("name") + if name == "" { + http.Error(w, jsonDNS.FormatError("Invalid argument value: \"name\""), 400) + return + } + if punycode, err := idna.ToASCII(name); err == nil { + name = punycode + } else { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error())), 400) + return + } + + rrTypeStr := r.FormValue("type") + rrType := uint16(1) + if rrTypeStr == "" { + } else if v, err := strconv.ParseUint(rrTypeStr, 10, 16); err == nil { + rrType = uint16(v) + } else if v, ok := dns.StringToType[strings.ToUpper(rrTypeStr)]; ok { + rrType = v + } else { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"type\" = %q", rrTypeStr)), 400) + return + } + + cdStr := r.FormValue("cd") + cd := false + if cdStr == "1" || cdStr == "true" { + cd = true + } else if cdStr == "0" || cdStr == "false" || cdStr == "" { + } else { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr)), 400) + return + } + + ednsClientSubnet := r.FormValue("edns_client_subnet") + ednsClientFamily := uint16(0) + ednsClientAddress := net.IP(nil) + ednsClientNetmask := uint8(255) + if ednsClientSubnet != "" { + slash := strings.IndexByte(ednsClientSubnet, '/') + if slash < 0 { + ednsClientAddress = net.ParseIP(ednsClientSubnet) + if ednsClientAddress == nil { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)), 400) + return + } + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + } else { + ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) + if ednsClientAddress == nil { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)), 400) + return + } + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + } else { + ednsClientFamily = 2 + } + netmask, err := strconv.ParseUint(ednsClientSubnet[slash + 1:], 10, 8) + if err != nil { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q (%s)", ednsClientSubnet, err.Error())), 400) + return + } + ednsClientNetmask = uint8(netmask) + } + } else { + ednsClientAddress = s.findClientIP(r) + if ednsClientAddress == nil { + ednsClientNetmask = 0 + } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + } + + msg := new(dns.Msg) + msg.SetQuestion(dns.Fqdn(name), rrType) + msg.CheckingDisabled = cd + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(4096) + opt.SetDo(true) + if ednsClientAddress != nil { + edns0Subnet := new(dns.EDNS0_SUBNET) + edns0Subnet.Code = dns.EDNS0SUBNET + edns0Subnet.Family = ednsClientFamily + edns0Subnet.SourceNetmask = ednsClientNetmask + edns0Subnet.SourceScope = 0 + edns0Subnet.Address = ednsClientAddress + opt.Option = append(opt.Option, edns0Subnet) + } + msg.Extra = append(msg.Extra, opt) + + resp, err := s.doDNSQuery(msg) + if err != nil { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("DNS query failure (%s)", err.Error())), 503) + return + } + respJson := jsonDNS.Marshal(resp) + respStr, err := json.Marshal(respJson) + if err != nil { + http.Error(w, jsonDNS.FormatError(fmt.Sprintf("DNS packet parse failure (%s)", err.Error())), 500) + return + } + + if respJson.HaveTTL { + w.Header().Set("Cache-Control", "max-age=" + strconv.Itoa(int(respJson.LeastTTL))) + w.Header().Set("Expires", respJson.EarliestExpires.Format(time.RFC1123)) + } + w.Write(respStr) +} + +func (s *Server) findClientIP(r *http.Request) net.IP { + XForwardedFor := r.Header.Get("X-Forwarded-For") + if XForwardedFor != "" { + for _, addr := range strings.Split(XForwardedFor, ",") { + addr = strings.TrimSpace(addr) + ip := net.ParseIP(addr) + if jsonDNS.IsGlobalIP(ip) { + return ip + } + } + } + XRealIP := r.Header.Get("X-Real-IP") + if XRealIP != "" { + addr := strings.TrimSpace(XRealIP) + ip := net.ParseIP(addr) + if jsonDNS.IsGlobalIP(ip) { + return ip + } + } + remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if err != nil { + return nil + } + if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) { + return ip + } + return nil +} + +func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) { + num_servers := len(s.upstreams) + init_server := rand.Intn(num_servers) + for i := 0; i < num_servers; i++ { + var server string + if init_server + i < num_servers { + server = s.upstreams[i + init_server] + } else { + server = s.upstreams[i + init_server - num_servers] + } + if !s.tcpOnly { + resp, _, err = s.udpClient.Exchange(msg, server) + if err == dns.ErrTruncated { + log.Println(err) + resp, _, err = s.tcpClient.Exchange(msg, server) + if err == dns.ErrTruncated { + log.Println(err) + return + } + } + } else { + resp, _, err = s.tcpClient.Exchange(msg, server) + if err == dns.ErrTruncated { + log.Println(err) + return + } + } + if err == nil { + return + } + log.Println(err) + } + return +} diff --git a/json-dns/error.go b/json-dns/error.go new file mode 100644 index 0000000..6f0a79f --- /dev/null +++ b/json-dns/error.go @@ -0,0 +1,42 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package jsonDNS + +import ( + "encoding/json" + "log" + "github.com/miekg/dns" +) + +type dnsError struct { + Status uint32 `json:"Status"` + Comment string `json:"Comment,omitempty"` +} + +func FormatError(comment string) string { + errJson := dnsError { + Status: dns.RcodeServerFailure, + Comment: comment, + } + errStr, err := json.Marshal(errJson) + if err != nil { + log.Fatalln(err) + } + return string(errStr) +} diff --git a/json-dns/globalip.go b/json-dns/globalip.go new file mode 100644 index 0000000..0e20895 --- /dev/null +++ b/json-dns/globalip.go @@ -0,0 +1,124 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package jsonDNS + +import ( + "net" +) + +// RFC6890 +var localIPv4Nets = []net.IPNet { + // This host on this network + net.IPNet { + net.IP { 0, 0, 0, 0 }, + net.IPMask { 255, 0, 0, 0 }, + }, + // Private-Use Networks + net.IPNet { + net.IP { 10, 0, 0, 0 }, + net.IPMask { 255, 0, 0, 0 }, + }, + // Shared Address Space + net.IPNet { + net.IP { 100, 64, 0, 0 }, + net.IPMask { 255, 192, 0, 0 }, + }, + // Loopback + net.IPNet { + net.IP { 127, 0, 0, 0 }, + net.IPMask { 255, 0, 0, 0 }, + }, + // Link Local + net.IPNet { + net.IP { 169, 254, 0, 0 }, + net.IPMask { 255, 255, 0, 0 }, + }, + // Private-Use Networks + net.IPNet { + net.IP { 172, 16, 0, 0 }, + net.IPMask { 255, 240, 0, 0 }, + }, + // DS-Lite + net.IPNet { + net.IP { 192, 0, 0, 0 }, + net.IPMask { 255, 255, 255, 248 }, + }, + // 6to4 Relay Anycast + net.IPNet { + net.IP { 192, 88, 99, 0 }, + net.IPMask { 255, 255, 255, 0 }, + }, + // Private-Use Networks + net.IPNet { + net.IP { 192, 168, 0, 0 }, + net.IPMask { 255, 255, 0, 0 }, + }, + // Reserved for Future Use & Limited Broadcast + net.IPNet { + net.IP { 240, 0, 0, 0 }, + net.IPMask { 240, 0, 0, 0 }, + }, +} + +// RFC6890 +var localIPv6Nets = []net.IPNet { + // Unspecified & Loopback Address + net.IPNet { + net.IP { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + net.IPMask { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe }, + }, + // Discard-Only Prefix + net.IPNet { + net.IP { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + net.IPMask { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + }, + // Unique-Local + net.IPNet { + net.IP { 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + net.IPMask { 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + }, + // Linked-Scoped Unicast + net.IPNet { + net.IP { 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + net.IPMask { 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + }, +} + +func IsGlobalIP(ip net.IP) bool { + if ip == nil { + return false + } + if ipv4 := ip.To4(); len(ipv4) == net.IPv4len { + for _, ipnet := range localIPv4Nets { + if ipnet.Contains(ip) { + return false + } + } + return true + } + if len(ip) == net.IPv6len { + for _, ipnet := range localIPv6Nets { + if ipnet.Contains(ip) { + return false + } + } + return true + } + return true +} diff --git a/json-dns/marshal.go b/json-dns/marshal.go new file mode 100644 index 0000000..a88c798 --- /dev/null +++ b/json-dns/marshal.go @@ -0,0 +1,109 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package jsonDNS + +import ( + "strconv" + "strings" + "time" + "github.com/miekg/dns" +) + +func Marshal(msg *dns.Msg) *Response { + now := time.Now().UTC() + + resp := new(Response) + resp.Status = uint32(msg.Rcode) + resp.TC = msg.Truncated + resp.RD = msg.RecursionDesired + resp.RA = msg.RecursionAvailable + resp.AD = msg.AuthenticatedData + resp.CD = msg.CheckingDisabled + + resp.Question = make([]Question, 0, len(msg.Question)) + for _, question := range msg.Question { + jsonQuestion := Question { + Name: question.Name, + Type: question.Qtype, + } + resp.Question = append(resp.Question, jsonQuestion) + } + + resp.Answer = make([]RR, 0, len(msg.Answer)) + for _, rr := range msg.Answer { + jsonAnswer := marshalRR(rr, now) + if !resp.HaveTTL || jsonAnswer.TTL < resp.LeastTTL { + resp.HaveTTL = true + resp.LeastTTL = jsonAnswer.TTL + resp.EarliestExpires = jsonAnswer.Expires + } + resp.Answer = append(resp.Answer, jsonAnswer) + } + + resp.Authority = make([]RR, 0, len(msg.Ns)) + for _, rr := range msg.Ns { + jsonAuthority := marshalRR(rr, now) + if !resp.HaveTTL || jsonAuthority.TTL < resp.LeastTTL { + resp.HaveTTL = true + resp.LeastTTL = jsonAuthority.TTL + resp.EarliestExpires = jsonAuthority.Expires + } + resp.Authority = append(resp.Authority, jsonAuthority) + } + + resp.Additional = make([]RR, 0, len(msg.Extra)) + for _, rr := range msg.Extra { + jsonAdditional := marshalRR(rr, now) + header := rr.Header() + if header.Rrtype == dns.TypeOPT { + opt := rr.(*dns.OPT) + resp.Status = ((opt.Hdr.Ttl & 0xff000000) >> 20) | (resp.Status & 0xff) + for _, option := range opt.Option { + if option.Option() == dns.EDNS0SUBNET { + edns0 := option.(*dns.EDNS0_SUBNET) + resp.EdnsClientSubnet = edns0.Address.String() + "/" + strconv.Itoa(int(edns0.SourceScope)) + } + } + continue + } + if !resp.HaveTTL || jsonAdditional.TTL < resp.LeastTTL { + resp.HaveTTL = true + resp.LeastTTL = jsonAdditional.TTL + resp.EarliestExpires = jsonAdditional.Expires + } + resp.Additional = append(resp.Additional, jsonAdditional) + } + + return resp +} + +func marshalRR(rr dns.RR, now time.Time) RR { + jsonRR := RR {} + rrHeader := rr.Header() + jsonRR.Name = rrHeader.Name + jsonRR.Type = rrHeader.Rrtype + jsonRR.TTL = rrHeader.Ttl + jsonRR.Expires = now.Add(time.Duration(jsonRR.TTL) * time.Second) + jsonRR.ExpiresStr = jsonRR.Expires.Format(time.RFC1123) + data := strings.SplitN(rr.String(), "\t", 5) + if len(data) >= 5 { + jsonRR.Data = data[4] + } + return jsonRR +} diff --git a/json-dns/response.go b/json-dns/response.go new file mode 100644 index 0000000..6819cb7 --- /dev/null +++ b/json-dns/response.go @@ -0,0 +1,67 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package jsonDNS + +import ( + "time" +) + +type Response struct { + // Standard DNS response code (32 bit integer) + Status uint32 `json:"Status"` + // Whether the response is truncated + TC bool `json:"TC"` + // Recursion desired + RD bool `json:"RD"` + // Recursion available + RA bool `json:"RA"` + // Whether all response data was validated with DNSSEC + // FIXME: We don't have DNSSEC yet! This bit is not reliable! + AD bool `json:"AD"` + // Whether the client asked to disable DNSSEC + CD bool `json:"CD"` + Question []Question `json:"Question"` + Answer []RR `json:"Answer,omitempty"` + Authority []RR `json:"Authority,omitempty"` + Additional []RR `json:"Additional,omitempty"` + Comment string `json:"Comment,omitempty"` + EdnsClientSubnet string `json:"edns_client_subnet,omitempty"` + // Least time-to-live + HaveTTL bool `json:"-"` + LeastTTL uint32 `json:"-"` + EarliestExpires time.Time `json:"-"` +} + +type Question struct { + // FQDN with trailing dot + Name string `json:"name"` + // Standard DNS RR type + Type uint16 `json:"type"` +} + +type RR struct { + Question + // Record's time-to-live in seconds + TTL uint32 `json:"TTL"` + // TTL in absolute time + Expires time.Time `json:"-"` + ExpiresStr string `json:"Expires"` + // Data + Data string `json:"data"` +} diff --git a/json-dns/unmarshal.go b/json-dns/unmarshal.go new file mode 100644 index 0000000..d500478 --- /dev/null +++ b/json-dns/unmarshal.go @@ -0,0 +1,169 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 Star Brilliant + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package jsonDNS + +import ( + "fmt" + "log" + "net" + "strconv" + "strings" + "time" + "github.com/miekg/dns" +) + +func PrepareReply(req *dns.Msg) *dns.Msg { + reply := new(dns.Msg) + reply.Id = req.Id + reply.Response = true + reply.Opcode = reply.Opcode + reply.RecursionDesired = req.RecursionDesired + reply.CheckingDisabled = req.CheckingDisabled + reply.Rcode = dns.RcodeServerFailure + reply.Compress = true + reply.Question = make([]dns.Question, len(req.Question)) + copy(reply.Question, req.Question) + return reply +} + +func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask uint8) *dns.Msg { + now := time.Now().UTC() + + reply := msg.Copy() + reply.Truncated = resp.TC + reply.RecursionDesired = resp.RD + reply.RecursionAvailable = resp.RA + reply.AuthenticatedData = resp.AD + reply.CheckingDisabled = resp.CD + reply.Rcode = dns.RcodeServerFailure + + reply.Answer = make([]dns.RR, 0, len(resp.Answer)) + for _, rr := range resp.Answer { + dnsRR, err := unmarshalRR(rr, now) + if err != nil { + log.Println(err) + } else { + reply.Answer = append(reply.Answer, dnsRR) + } + } + + reply.Ns = make([]dns.RR, 0, len(resp.Authority)) + for _, rr := range resp.Authority { + dnsRR, err := unmarshalRR(rr, now) + if err != nil { + log.Println(err) + } else { + reply.Ns = append(reply.Ns, dnsRR) + } + } + + reply.Extra = make([]dns.RR, 0, len(resp.Additional) + 1) + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + if udpSize >= 512 { + opt.SetUDPSize(udpSize) + } else { + opt.SetUDPSize(512) + } + opt.SetDo(false) + ednsClientSubnet := resp.EdnsClientSubnet + ednsClientFamily := uint16(0) + ednsClientAddress := net.IP(nil) + ednsClientScope := uint8(255) + if ednsClientSubnet != "" { + slash := strings.IndexByte(ednsClientSubnet, '/') + if slash < 0 { + log.Println(UnmarshalError { "Invalid client subnet" }) + } else { + ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) + if ednsClientAddress == nil { + log.Println(UnmarshalError { "Invalid client subnet address" }) + } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + } else { + ednsClientFamily = 2 + } + scope, err := strconv.ParseUint(ednsClientSubnet[slash + 1:], 10, 8) + if err != nil { + log.Println(UnmarshalError { "Invalid client subnet address" }) + } else { + ednsClientScope = uint8(scope) + } + } + } + if ednsClientAddress != nil { + edns0Subnet := new(dns.EDNS0_SUBNET) + edns0Subnet.Code = dns.EDNS0SUBNET + edns0Subnet.Family = ednsClientFamily + edns0Subnet.SourceNetmask = ednsClientNetmask + edns0Subnet.SourceScope = ednsClientScope + edns0Subnet.Address = ednsClientAddress + opt.Option = append(opt.Option, edns0Subnet) + } + reply.Extra = append(reply.Extra, opt) + for _, rr := range resp.Additional { + dnsRR, err := unmarshalRR(rr, now) + if err != nil { + log.Println(err) + } else { + reply.Extra = append(reply.Extra, dnsRR) + } + } + + reply.Rcode = int(resp.Status & 0xf) + opt.Hdr.Ttl = (opt.Hdr.Ttl & 0x00ffffff) | ((resp.Status & 0xff0) << 20) + reply.Extra[0] = opt + return reply +} + +func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) { + if strings.ContainsAny(rr.Name, "\t\r\n \"();\\") { + return nil, UnmarshalError { fmt.Sprintf("Record name contains space: %q", rr.Name) } + } + if rr.ExpiresStr != "" { + rr.Expires, err = time.Parse(time.RFC1123, rr.ExpiresStr) + if err != nil { + return nil, UnmarshalError { fmt.Sprintf("Invalid expire time: %q", rr.ExpiresStr) } + } + ttl := rr.Expires.Sub(now) / time.Second + if ttl >= 0 && ttl <= 0xffffffff { + rr.TTL = uint32(ttl) + } + } + rrType, ok := dns.TypeToString[rr.Type] + if !ok { + return nil, UnmarshalError { fmt.Sprintf("Unknown record type: %d", rr.Type) } + } + if strings.ContainsAny(rr.Data, "\r\n") { + return nil, UnmarshalError { fmt.Sprintf("Record data contains newline: %q", rr.Data) } + } + zone := fmt.Sprintf("%s %d IN %s %s", rr.Name, rr.TTL, rrType, rr.Data) + dnsRR, err = dns.NewRR(zone) + return +} + +type UnmarshalError struct { + err string +} + +func (e UnmarshalError) Error() string { + return "json-dns: " + e.err +}