From 521b4b6abc1fe99ebec4ab182dfb0e74f26740ef Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Wed, 21 Mar 2018 00:14:59 +0800 Subject: [PATCH 1/4] Implement IETF protocol --- LICENSE | 2 +- doh-client/client.go | 229 ++++++++++------------------------ doh-client/config.go | 58 ++++----- doh-client/doh-client.conf | 5 +- doh-client/google.go | 147 ++++++++++++++++++++++ doh-client/main.go | 34 +++--- doh-server/config.go | 65 +++++----- doh-server/doh-server.conf | 2 +- doh-server/google.go | 193 +++++++++++++++++++++++++++++ doh-server/ietf.go | 144 ++++++++++++++++++++++ doh-server/main.go | 34 +++--- doh-server/server.go | 243 +++++++++++++------------------------ json-dns/error.go | 44 +++---- json-dns/globalip.go | 122 +++++++++---------- json-dns/marshal.go | 41 ++++--- json-dns/response.go | 76 ++++++------ json-dns/unmarshal.go | 55 ++++----- 17 files changed, 912 insertions(+), 582 deletions(-) create mode 100644 doh-client/google.go create mode 100644 doh-server/google.go create mode 100644 doh-server/ietf.go diff --git a/LICENSE b/LICENSE index 105ed1c..bdc3121 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2017 Star Brilliant +Copyright (C) 2017-2018 Star Brilliant Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/doh-client/client.go b/doh-client/client.go index 192d1da..bd7c9b5 100644 --- a/doh-client/client.go +++ b/doh-client/client.go @@ -1,68 +1,64 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main import ( "context" - "encoding/json" - "fmt" - "math/rand" - "io/ioutil" "log" + "math/rand" "net" "net/http" "net/http/cookiejar" - "net/url" - "strconv" - "strings" "time" - "github.com/miekg/dns" + "../json-dns" + "github.com/miekg/dns" + "golang.org/x/net/http2" ) type Client struct { - conf *config - bootstrap []string - udpServer *dns.Server - tcpServer *dns.Server - httpTransport *http.Transport - httpClient *http.Client + conf *config + bootstrap []string + udpServer *dns.Server + tcpServer *dns.Server + httpTransport *http.Transport + httpClient *http.Client } func NewClient(conf *config) (c *Client, err error) { - c = &Client { + c = &Client{ conf: conf, } - c.udpServer = &dns.Server { - Addr: conf.Listen, - Net: "udp", + c.udpServer = &dns.Server{ + Addr: conf.Listen, + Net: "udp", Handler: dns.HandlerFunc(c.udpHandlerFunc), UDPSize: 4096, } - c.tcpServer = &dns.Server { - Addr: conf.Listen, - Net: "tcp", + c.tcpServer = &dns.Server{ + Addr: conf.Listen, + Net: "tcp", Handler: dns.HandlerFunc(c.tcpHandlerFunc), } bootResolver := net.DefaultResolver @@ -71,37 +67,49 @@ func NewClient(conf *config) (c *Client, err error) { for i, bootstrap := range conf.Bootstrap { bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap) if err != nil { - bootstrapAddr, err = net.ResolveUDPAddr("udp", "[" + bootstrap + "]:53") + bootstrapAddr, err = net.ResolveUDPAddr("udp", "["+bootstrap+"]:53") + } + if err != nil { + return nil, err } - if err != nil { return nil, err } c.bootstrap[i] = bootstrapAddr.String() } - bootResolver = &net.Resolver { + bootResolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { var d net.Dialer - num_servers := len(c.bootstrap) - bootstrap := c.bootstrap[rand.Intn(num_servers)] + numServers := len(c.bootstrap) + bootstrap := c.bootstrap[rand.Intn(numServers)] conn, err := d.DialContext(ctx, network, bootstrap) return conn, err }, } } c.httpTransport = new(http.Transport) - *c.httpTransport = *http.DefaultTransport.(*http.Transport) - c.httpTransport.DialContext = (&net.Dialer { - Timeout: time.Duration(conf.Timeout) * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - Resolver: bootResolver, - }).DialContext - c.httpTransport.ResponseHeaderTimeout = time.Duration(conf.Timeout) * time.Second + c.httpTransport = &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: time.Duration(conf.Timeout) * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + Resolver: bootResolver, + }).DialContext, + ExpectContinueTimeout: 1 * time.Second, + IdleConnTimeout: 90 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + Proxy: http.ProxyFromEnvironment, + ResponseHeaderTimeout: time.Duration(conf.Timeout) * time.Second, + TLSHandshakeTimeout: time.Duration(conf.Timeout) * time.Second, + } + http2.ConfigureTransport(c.httpTransport) // Most CDNs require Cookie support to prevent DDoS attack cookieJar, err := cookiejar.New(nil) - if err != nil { return nil, err } - c.httpClient = &http.Client { + if err != nil { + return nil, err + } + c.httpClient = &http.Client{ Transport: c.httpTransport, - Jar: cookieJar, + Jar: cookieJar, } return c, nil } @@ -114,14 +122,14 @@ func (c *Client) Start() error { 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 @@ -136,110 +144,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { 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 := strings.ToLower(question.Name) - questionType := "" - if qtype, ok := dns.TypeToString[question.Qtype]; ok { - questionType = qtype - } else { - questionType = strconv.Itoa(int(question.Qtype)) - } - - if c.conf.Verbose { - fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType) - } - - num_servers := len(c.conf.Upstream) - upstream := c.conf.Upstream[rand.Intn(num_servers)] - requestURL := fmt.Sprintf("%s?name=%s&type=%s", 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) - } - - req, err := http.NewRequest("GET", requestURL, nil) - if err != nil { - log.Println(err) - reply.Rcode = dns.RcodeServerFailure - w.WriteMsg(reply) - return - } - req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") - resp, err := c.httpClient.Do(req) - if err != nil { - log.Println(err) - reply.Rcode = dns.RcodeServerFailure - w.WriteMsg(reply) - c.httpTransport.CloseIdleConnections() - return - } - if resp.StatusCode != 200 { - log.Printf("HTTP error: %s\n", resp.Status) - reply.Rcode = dns.RcodeServerFailure - w.WriteMsg(reply) - contentType := resp.Header.Get("Content-Type") - if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") { - 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 - } - - if respJson.Status != dns.RcodeSuccess && respJson.Comment != "" { - log.Printf("DNS error: %s\n", respJson.Comment) - } - - 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) + c.handlerFuncGoogle(w, r, isTCP) } func (c *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { @@ -251,8 +156,8 @@ func (c *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { } var ( - ipv4Mask24 net.IPMask = net.IPMask { 255, 255, 255, 0 } - ipv6Mask48 net.IPMask = net.CIDRMask(48, 128) + ipv4Mask24 = net.IPMask{255, 255, 255, 0} + ipv6Mask48 = net.CIDRMask(48, 128) ) func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) { diff --git a/doh-client/config.go b/doh-client/config.go index 47eb6af..414832b 100644 --- a/doh-client/config.go +++ b/doh-client/config.go @@ -1,57 +1,59 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main import ( "fmt" + "github.com/BurntSushi/toml" ) type config struct { - Listen string `toml:"listen"` - Upstream []string `toml:"upstream"` - Bootstrap []string `toml:"bootstrap"` - Timeout uint `toml:"timeout"` - NoECS bool `toml:"no_ecs"` - Verbose bool `toml:"verbose"` + Listen string `toml:"listen"` + UpstreamGoogle []string `toml:"upstream_google"` + UpstreamIETF []string `toml:"upstream_ietf"` + Bootstrap []string `toml:"bootstrap"` + Timeout uint `toml:"timeout"` + NoECS bool `toml:"no_ecs"` + Verbose bool `toml:"verbose"` } func loadConfig(path string) (*config, error) { - conf := &config {} + conf := &config{} metaData, err := toml.DecodeFile(path, conf) if err != nil { return nil, err } for _, key := range metaData.Undecoded() { - return nil, &configError { fmt.Sprintf("unknown option %q", key.String()) } + return nil, &configError{fmt.Sprintf("unknown option %q", key.String())} } if conf.Listen == "" { conf.Listen = "127.0.0.1:53" } - if len(conf.Upstream) == 0 { - conf.Upstream = []string { "https://dns.google.com/resolve" } + if len(conf.UpstreamGoogle) == 0 && len(conf.UpstreamIETF) == 0 { + conf.UpstreamGoogle = []string{"https://dns.google.com/resolve"} } if conf.Timeout == 0 { conf.Timeout = 10 @@ -61,7 +63,7 @@ func loadConfig(path string) (*config, error) { } type configError struct { - err string + err string } func (e *configError) Error() string { diff --git a/doh-client/doh-client.conf b/doh-client/doh-client.conf index 0caf0a9..eecac7b 100644 --- a/doh-client/doh-client.conf +++ b/doh-client/doh-client.conf @@ -3,9 +3,12 @@ listen = "127.0.0.1:53" # HTTP path for upstream resolver # If multiple servers are specified, a random one will be chosen each time. -upstream = [ +upstream_google = [ "https://dns.google.com/resolve", ] +upstream_ietf = [ + "https://dns.google.com/experimental", +] # Bootstrap DNS server to resolve the address of the upstream resolver # If multiple servers are specified, a random one will be chosen each time. diff --git a/doh-client/google.go b/doh-client/google.go new file mode 100644 index 0000000..77aa088 --- /dev/null +++ b/doh-client/google.go @@ -0,0 +1,147 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "../json-dns" + "github.com/miekg/dns" +) + +func (c *Client) handlerFuncGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { + 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 := strings.ToLower(question.Name) + questionType := "" + if qtype, ok := dns.TypeToString[question.Qtype]; ok { + questionType = qtype + } else { + questionType = strconv.Itoa(int(question.Qtype)) + } + + if c.conf.Verbose { + fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType) + } + + numServers := len(c.conf.UpstreamGoogle) + upstream := c.conf.UpstreamGoogle[rand.Intn(numServers)] + requestURL := fmt.Sprintf("%s?name=%s&type=%s", 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) + } + + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") + resp, err := c.httpClient.Do(req) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + c.httpTransport.CloseIdleConnections() + return + } + if resp.StatusCode != 200 { + log.Printf("HTTP error: %s\n", resp.Status) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + contentType := resp.Header.Get("Content-Type") + if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") { + 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 + } + + if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" { + log.Printf("DNS error: %s\n", respJSON.Comment) + } + + 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) +} diff --git a/doh-client/main.go b/doh-client/main.go index 10aa4ae..83fb2d0 100644 --- a/doh-client/main.go +++ b/doh-client/main.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main diff --git a/doh-server/config.go b/doh-server/config.go index 1f5ccf5..e13aa2d 100644 --- a/doh-server/config.go +++ b/doh-server/config.go @@ -1,63 +1,64 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main import ( "fmt" + "github.com/BurntSushi/toml" ) type config struct { - Listen string `toml:"listen"` - Cert string `toml:"cert"` - Key string `toml:"key"` - Path string `toml:"path"` - Upstream []string `toml:"upstream"` - Timeout uint `toml:"timeout"` - Tries uint `toml:"tries"` - TCPOnly bool `toml:"tcp_only"` - Verbose bool `toml:"verbose"` + Listen string `toml:"listen"` + Cert string `toml:"cert"` + Key string `toml:"key"` + Path string `toml:"path"` + Upstream []string `toml:"upstream"` + Timeout uint `toml:"timeout"` + Tries uint `toml:"tries"` + TCPOnly bool `toml:"tcp_only"` + Verbose bool `toml:"verbose"` } func loadConfig(path string) (*config, error) { - conf := &config {} + conf := &config{} metaData, err := toml.DecodeFile(path, conf) if err != nil { return nil, err } for _, key := range metaData.Undecoded() { - return nil, &configError { fmt.Sprintf("unknown option %q", key.String()) } + return nil, &configError{fmt.Sprintf("unknown option %q", key.String())} } if conf.Listen == "" { conf.Listen = "127.0.0.1:8053" } if conf.Path == "" { - conf.Path = "/resolve" + conf.Path = "/dns-query" } if len(conf.Upstream) == 0 { - conf.Upstream = []string { "8.8.8.8:53", "8.8.4.4:53" } + conf.Upstream = []string{"8.8.8.8:53", "8.8.4.4:53"} } if conf.Timeout == 0 { conf.Timeout = 10 @@ -67,14 +68,14 @@ func loadConfig(path string) (*config, error) { } if (conf.Cert != "") != (conf.Key != "") { - return nil, &configError { "You must specify both -cert and -key to enable TLS" } + return nil, &configError{"You must specify both -cert and -key to enable TLS"} } return conf, nil } type configError struct { - err string + err string } func (e *configError) Error() string { diff --git a/doh-server/doh-server.conf b/doh-server/doh-server.conf index 768eaea..635277d 100644 --- a/doh-server/doh-server.conf +++ b/doh-server/doh-server.conf @@ -8,7 +8,7 @@ cert = "" key = "" # HTTP path for resolve application -path = "/resolve" +path = "/dns-query" # Upstream DNS resolver # If multiple servers are specified, a random one will be chosen each time. diff --git a/doh-server/google.go b/doh-server/google.go new file mode 100644 index 0000000..9ab6607 --- /dev/null +++ b/doh-server/google.go @@ -0,0 +1,193 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "strconv" + "strings" + + "../json-dns" + "github.com/miekg/dns" + "golang.org/x/net/idna" +) + +func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNSRequest { + name := r.FormValue("name") + if name == "" { + return &DNSRequest{ + errcode: 400, + errtext: "Invalid argument value: \"name\"", + } + } + name = strings.ToLower(name) + if punycode, err := idna.ToASCII(name); err == nil { + name = punycode + } else { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error()), + } + } + + 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 { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"type\" = %q", rrTypeStr), + } + } + + cdStr := r.FormValue("cd") + cd := false + if cdStr == "1" || cdStr == "true" { + cd = true + } else if cdStr == "0" || cdStr == "false" || cdStr == "" { + } else { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr), + } + } + + ednsClientSubnet := r.FormValue("edns_client_subnet") + ednsClientFamily := uint16(0) + ednsClientAddress := net.IP(nil) + ednsClientNetmask := uint8(255) + if ednsClientSubnet != "" { + if ednsClientSubnet == "0/0" { + ednsClientSubnet = "0.0.0.0/0" + } + slash := strings.IndexByte(ednsClientSubnet, '/') + if slash < 0 { + ednsClientAddress = net.ParseIP(ednsClientSubnet) + if ednsClientAddress == nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), + } + } + 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 { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), + } + } + 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 { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), + } + } + 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) + + return &DNSRequest{ + request: msg, + isTailored: ednsClientSubnet == "", + } +} + +func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request, req *DNSRequest) { + respJSON := jsonDNS.Marshal(req.response) + respStr, err := json.Marshal(respJSON) + if err != nil { + log.Println(err) + jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) + return + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + if respJSON.HaveTTL { + if req.isTailored { + w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) + } else { + w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) + } + w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) + } + if respJSON.Status == dns.RcodeServerFailure { + w.WriteHeader(503) + } + w.Write(respStr) +} diff --git a/doh-server/ietf.go b/doh-server/ietf.go new file mode 100644 index 0000000..6475989 --- /dev/null +++ b/doh-server/ietf.go @@ -0,0 +1,144 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package main + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "net/http" + "strconv" + "time" + + "../json-dns" + "github.com/miekg/dns" +) + +func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRequest { + requestBase64 := r.FormValue("dns") + requestBinary, err := base64.StdEncoding.DecodeString(requestBase64) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"dns\" = %q", requestBase64), + } + } + if len(requestBinary) == 0 && r.Header.Get("Content-Type") == "application/dns-udpwireformat" { + requestBinary, err = ioutil.ReadAll(r.Body) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Failed to read request body (%s)", err.Error()), + } + } + } + if len(requestBinary) == 0 { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("Invalid argument value: \"dns\""), + } + } + msg := new(dns.Msg) + err = msg.Unpack(requestBinary) + if err != nil { + return &DNSRequest{ + errcode: 400, + errtext: fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), + } + } + msg.Id = dns.Id() + opt := msg.IsEdns0() + if opt == nil { + opt = new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(4096) + opt.SetDo(false) + msg.Extra = append(msg.Extra, opt) + } + var edns0Subnet *dns.EDNS0_SUBNET + for _, option := range opt.Option { + if option.Option() == dns.EDNS0SUBNET { + edns0Subnet = option.(*dns.EDNS0_SUBNET) + break + } + } + isTailored := edns0Subnet == nil + if edns0Subnet == nil { + ednsClientFamily := uint16(0) + ednsClientAddress := s.findClientIP(r) + ednsClientNetmask := uint8(255) + if ednsClientAddress == nil { + ednsClientNetmask = 0 + } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + 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) + } + + return &DNSRequest{ + request: msg, + isTailored: isTailored, + } +} + +func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, req *DNSRequest) { + respJSON := jsonDNS.Marshal(req.response) + req.response.Id = 0 + respBytes, err := req.response.Pack() + if err != nil { + log.Println(err) + jsonDNS.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500) + return + } + + w.Header().Set("Content-Type", "application/dns-udpwireformat") + now := time.Now().Format(http.TimeFormat) + w.Header().Set("Date", now) + w.Header().Set("Last-Modified", now) + if respJSON.HaveTTL { + if req.isTailored { + w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) + } else { + w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) + } + w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) + } + if respJSON.Status == dns.RcodeServerFailure { + w.WriteHeader(503) + } + w.Write(respBytes) +} diff --git a/doh-server/main.go b/doh-server/main.go index e53cdb4..ac9bd75 100644 --- a/doh-server/main.go +++ b/doh-server/main.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main diff --git a/doh-server/server.go b/doh-server/server.go index 23de2ab..f814b8b 100644 --- a/doh-server/server.go +++ b/doh-server/server.go @@ -1,61 +1,67 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package main import ( - "encoding/json" "fmt" - "math/rand" "log" + "math/rand" "net" "net/http" "os" - "strconv" "strings" "time" - "golang.org/x/net/idna" + + "../json-dns" "github.com/gorilla/handlers" "github.com/miekg/dns" - "../json-dns" ) type Server struct { - conf *config - udpClient *dns.Client - tcpClient *dns.Client - servemux *http.ServeMux + conf *config + udpClient *dns.Client + tcpClient *dns.Client + servemux *http.ServeMux +} + +type DNSRequest struct { + request *dns.Msg + response *dns.Msg + isTailored bool + errcode int + errtext string } func NewServer(conf *config) (s *Server) { - s = &Server { + s = &Server{ conf: conf, - udpClient: &dns.Client { - Net: "udp", + udpClient: &dns.Client{ + Net: "udp", Timeout: time.Duration(conf.Timeout) * time.Second, }, - tcpClient: &dns.Client { - Net: "tcp", + tcpClient: &dns.Client{ + Net: "tcp", Timeout: time.Duration(conf.Timeout) * time.Second, }, servemux: http.NewServeMux(), @@ -71,151 +77,76 @@ func (s *Server) Start() error { } if s.conf.Cert != "" || s.conf.Key != "" { return http.ListenAndServeTLS(s.conf.Listen, s.conf.Cert, s.conf.Key, servemux) - } else { - return http.ListenAndServe(s.conf.Listen, servemux) } + return http.ListenAndServe(s.conf.Listen, servemux) } func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.Header().Set("Server", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") w.Header().Set("X-Powered-By", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") - name := r.FormValue("name") - if name == "" { - jsonDNS.FormatError(w, "Invalid argument value: \"name\"", 400) - return + if r.Form == nil { + const maxMemory = 32 << 20 // 32 MB + r.ParseMultipartForm(maxMemory) } - name = strings.ToLower(name) - if punycode, err := idna.ToASCII(name); err == nil { - name = punycode - } else { - jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error()), 400) - return + contentType := r.Header.Get("Content-Type") + if ct := r.FormValue("ct"); ct != "" { + contentType = ct } - - 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 { - jsonDNS.FormatError(w, 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 { - jsonDNS.FormatError(w, 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 != "" { - if ednsClientSubnet == "0/0" { - ednsClientSubnet = "0.0.0.0/0" + if contentType == "" { + // Guess request Content-Type based on other parameters + if r.FormValue("name") != "" { + contentType = "application/x-www-form-urlencoded" + } else if r.FormValue("dns") != "" { + contentType = "application/dns-udpwireformat" } - slash := strings.IndexByte(ednsClientSubnet, '/') - if slash < 0 { - ednsClientAddress = net.ParseIP(ednsClientSubnet) - if ednsClientAddress == nil { - jsonDNS.FormatError(w, 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 { - jsonDNS.FormatError(w, 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 { - jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q (%s)", ednsClientSubnet, err.Error()), 400) - return - } - ednsClientNetmask = uint8(netmask) + } + var responseType string + for _, responseCandidate := range strings.Split(r.Header.Get("Accept"), ",") { + responseCandidate = strings.ToLower(strings.SplitN(responseCandidate, ";", 2)[0]) + if responseCandidate == "application/json" { + responseType = "application/json" + break + } else if responseCandidate == "application/dns-udpwireformat" { + responseType = "application/dns-udpwireformat" + break } - } 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 + } + if responseType == "" { + // Guess response Content-Type based on request Content-Type + if contentType == "application/x-www-form-urlencoded" { + responseType = "application/json" + } else if contentType == "application/dns-udpwireformat" { + responseType = "application/dns-udpwireformat" } } - 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) + var req *DNSRequest + if contentType == "application/x-www-form-urlencoded" { + req = s.parseRequestGoogle(w, r) + } else if contentType == "application/dns-udpwireformat" { + req = s.parseRequestIETF(w, r) + } else { + jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 400) + return + } + if req.errcode != 0 { + jsonDNS.FormatError(w, req.errtext, req.errcode) + return } - msg.Extra = append(msg.Extra, opt) - resp, err := s.doDNSQuery(msg) + var err error + req.response, err = s.doDNSQuery(req.request) if err != nil { jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) return } - respJson := jsonDNS.Marshal(resp) - respStr, err := json.Marshal(respJson) - if err != nil { - log.Println(err) - jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) - return - } - if respJson.HaveTTL { - if ednsClientSubnet != "" { - w.Header().Set("Cache-Control", "public, max-age=" + strconv.Itoa(int(respJson.LeastTTL))) - } else { - w.Header().Set("Cache-Control", "private, max-age=" + strconv.Itoa(int(respJson.LeastTTL))) - } - w.Header().Set("Expires", respJson.EarliestExpires.Format(http.TimeFormat)) + if contentType == "application/x-www-form-urlencoded" { + s.generateResponseGoogle(w, r, req) + } else if contentType == "application/dns-udpwireformat" { + s.generateResponseIETF(w, r, req) } - if respJson.Status == dns.RcodeServerFailure { - w.WriteHeader(503) - } - w.Write(respStr) } func (s *Server) findClientIP(r *http.Request) net.IP { @@ -248,9 +179,9 @@ func (s *Server) findClientIP(r *http.Request) net.IP { } func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) { - num_servers := len(s.conf.Upstream) + numServers := len(s.conf.Upstream) for i := uint(0); i < s.conf.Tries; i++ { - server := s.conf.Upstream[rand.Intn(num_servers)] + server := s.conf.Upstream[rand.Intn(numServers)] if !s.conf.TCPOnly { resp, _, err = s.udpClient.Exchange(msg, server) if err == dns.ErrTruncated { diff --git a/json-dns/error.go b/json-dns/error.go index c3a3919..4f1cc0d 100644 --- a/json-dns/error.go +++ b/json-dns/error.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package jsonDNS @@ -27,17 +27,19 @@ import ( "encoding/json" "log" "net/http" + "github.com/miekg/dns" ) type dnsError struct { - Status uint32 `json:"Status"` - Comment string `json:"Comment,omitempty"` + Status uint32 `json:"Status"` + Comment string `json:"Comment,omitempty"` } func FormatError(w http.ResponseWriter, comment string, errcode int) { - errJson := dnsError { - Status: dns.RcodeServerFailure, + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + errJson := dnsError{ + Status: dns.RcodeServerFailure, Comment: comment, } errStr, err := json.Marshal(errJson) diff --git a/json-dns/globalip.go b/json-dns/globalip.go index 45ad8b9..0122bd3 100644 --- a/json-dns/globalip.go +++ b/json-dns/globalip.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package jsonDNS @@ -28,80 +28,80 @@ import ( ) // RFC6890 -var localIPv4Nets = []net.IPNet { +var localIPv4Nets = []net.IPNet{ // This host on this network - net.IPNet { - net.IP { 0, 0, 0, 0 }, - net.IPMask { 255, 0, 0, 0 }, + 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 }, + 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 }, + 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 }, + 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 }, + 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 }, + 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 }, + 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 }, + 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 }, + 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 }, + net.IPNet{ + net.IP{240, 0, 0, 0}, + net.IPMask{240, 0, 0, 0}, }, } // RFC6890 -var localIPv6Nets = []net.IPNet { +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 }, + 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 }, + 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 }, + 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 }, + 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}, }, } diff --git a/json-dns/marshal.go b/json-dns/marshal.go index 90a76c2..568c40d 100644 --- a/json-dns/marshal.go +++ b/json-dns/marshal.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package jsonDNS @@ -28,6 +28,7 @@ import ( "strconv" "strings" "time" + "github.com/miekg/dns" ) @@ -44,7 +45,7 @@ func Marshal(msg *dns.Msg) *Response { resp.Question = make([]Question, 0, len(msg.Question)) for _, question := range msg.Question { - jsonQuestion := Question { + jsonQuestion := Question{ Name: question.Name, Type: question.Qtype, } @@ -85,7 +86,7 @@ func Marshal(msg *dns.Msg) *Response { edns0 := option.(*dns.EDNS0_SUBNET) clientAddress := edns0.Address if clientAddress == nil { - clientAddress = net.IP { 0, 0, 0, 0 } + clientAddress = net.IP{0, 0, 0, 0} } else if ipv4 := clientAddress.To4(); ipv4 != nil { clientAddress = ipv4 } @@ -106,7 +107,7 @@ func Marshal(msg *dns.Msg) *Response { } func marshalRR(rr dns.RR, now time.Time) RR { - jsonRR := RR {} + jsonRR := RR{} rrHeader := rr.Header() jsonRR.Name = rrHeader.Name jsonRR.Type = rrHeader.Rrtype diff --git a/json-dns/response.go b/json-dns/response.go index a492ed7..c22c73f 100644 --- a/json-dns/response.go +++ b/json-dns/response.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package jsonDNS @@ -29,44 +29,44 @@ import ( type Response struct { // Standard DNS response code (32 bit integer) - Status uint32 `json:"Status"` + Status uint32 `json:"Status"` // Whether the response is truncated - TC bool `json:"TC"` + TC bool `json:"TC"` // Recursion desired - RD bool `json:"RD"` + RD bool `json:"RD"` // Recursion available - RA bool `json:"RA"` + 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"` + 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"` + 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:"-"` + HaveTTL bool `json:"-"` + LeastTTL uint32 `json:"-"` + EarliestExpires time.Time `json:"-"` } type Question struct { // FQDN with trailing dot - Name string `json:"name"` + Name string `json:"name"` // Standard DNS RR type - Type uint16 `json:"type"` + Type uint16 `json:"type"` } type RR struct { Question // Record's time-to-live in seconds - TTL uint32 `json:"TTL"` + TTL uint32 `json:"TTL"` // TTL in absolute time - Expires time.Time `json:"-"` - ExpiresStr string `json:"Expires"` + Expires time.Time `json:"-"` + ExpiresStr string `json:"Expires"` // Data - Data string `json:"data"` + Data string `json:"data"` } diff --git a/json-dns/unmarshal.go b/json-dns/unmarshal.go index 901d543..a42c6ab 100644 --- a/json-dns/unmarshal.go +++ b/json-dns/unmarshal.go @@ -1,24 +1,24 @@ /* - DNS-over-HTTPS - Copyright (C) 2017 Star Brilliant + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. */ package jsonDNS @@ -30,6 +30,7 @@ import ( "strconv" "strings" "time" + "github.com/miekg/dns" ) @@ -77,7 +78,7 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u } } - reply.Extra = make([]dns.RR, 0, len(resp.Additional) + 1) + reply.Extra = make([]dns.RR, 0, len(resp.Additional)+1) opt := new(dns.OPT) opt.Hdr.Name = "." opt.Hdr.Rrtype = dns.TypeOPT @@ -94,20 +95,20 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u if ednsClientSubnet != "" { slash := strings.IndexByte(ednsClientSubnet, '/') if slash < 0 { - log.Println(UnmarshalError { "Invalid client subnet" }) + log.Println(UnmarshalError{"Invalid client subnet"}) } else { ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) if ednsClientAddress == nil { - log.Println(UnmarshalError { "Invalid client subnet address" }) + 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) + scope, err := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8) if err != nil { - log.Println(UnmarshalError { "Invalid client subnet address" }) + log.Println(UnmarshalError{"Invalid client subnet address"}) } else { ednsClientScope = uint8(scope) } @@ -147,12 +148,12 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u 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) } + 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) } + return nil, UnmarshalError{fmt.Sprintf("Invalid expire time: %q", rr.ExpiresStr)} } ttl := rr.Expires.Sub(now) / time.Second if ttl >= 0 && ttl <= 0xffffffff { @@ -161,10 +162,10 @@ func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) { } rrType, ok := dns.TypeToString[rr.Type] if !ok { - return nil, UnmarshalError { fmt.Sprintf("Unknown record type: %d", rr.Type) } + 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) } + 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) @@ -172,7 +173,7 @@ func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) { } type UnmarshalError struct { - err string + err string } func (e UnmarshalError) Error() string { From 64664a59b8437fd5434093e806865d461aa58be6 Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Wed, 21 Mar 2018 01:38:15 +0800 Subject: [PATCH 2/4] Implement IETF protocol --- doh-client/client.go | 16 ++- doh-client/google.go | 4 +- doh-client/ietf.go | 235 +++++++++++++++++++++++++++++++++++++++++++ doh-server/ietf.go | 2 +- 4 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 doh-client/ietf.go diff --git a/doh-client/client.go b/doh-client/client.go index bd7c9b5..25948b3 100644 --- a/doh-client/client.go +++ b/doh-client/client.go @@ -144,7 +144,21 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { return } - c.handlerFuncGoogle(w, r, isTCP) + if len(c.conf.UpstreamIETF) == 0 { + c.handlerFuncGoogle(w, r, isTCP) + return + } + if len(c.conf.UpstreamGoogle) == 0 { + c.handlerFuncIETF(w, r, isTCP) + return + } + numServers := len(c.conf.UpstreamGoogle) + len(c.conf.UpstreamIETF) + random := rand.Intn(numServers) + if random <= len(c.conf.UpstreamGoogle) { + c.handlerFuncGoogle(w, r, isTCP) + } else { + c.handlerFuncIETF(w, r, isTCP) + } } func (c *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { diff --git a/doh-client/google.go b/doh-client/google.go index 77aa088..e1f0454 100644 --- a/doh-client/google.go +++ b/doh-client/google.go @@ -49,6 +49,7 @@ func (c *Client) handlerFuncGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) return } question := r.Question[0] + // knot-resolver scrambles capitalization, I think it is unfriendly to cache questionName := strings.ToLower(question.Name) questionType := "" if qtype, ok := dns.TypeToString[question.Qtype]; ok { @@ -86,6 +87,7 @@ func (c *Client) handlerFuncGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) w.WriteMsg(reply) return } + req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") resp, err := c.httpClient.Do(req) if err != nil { @@ -98,9 +100,9 @@ func (c *Client) handlerFuncGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) if resp.StatusCode != 200 { log.Printf("HTTP error: %s\n", resp.Status) reply.Rcode = dns.RcodeServerFailure - w.WriteMsg(reply) contentType := resp.Header.Get("Content-Type") if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") { + w.WriteMsg(reply) return } } diff --git a/doh-client/ietf.go b/doh-client/ietf.go new file mode 100644 index 0000000..ea61be7 --- /dev/null +++ b/doh-client/ietf.go @@ -0,0 +1,235 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017-2018 Star Brilliant + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "strconv" + "strings" + "time" + + "../json-dns" + "github.com/miekg/dns" +) + +func (c *Client) handlerFuncIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { + 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] + // knot-resolver scrambles capitalization, I think it is unfriendly to cache + questionName := strings.ToLower(question.Name) + questionType := "" + if qtype, ok := dns.TypeToString[question.Qtype]; ok { + questionType = qtype + } else { + questionType = strconv.Itoa(int(question.Qtype)) + } + + if c.conf.Verbose { + fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType) + } + + requestID := r.Id + r.Id = 0 + question.Name = questionName + opt := r.IsEdns0() + udpSize := uint16(512) + if opt == nil { + opt = new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + opt.SetUDPSize(4096) + opt.SetDo(false) + r.Extra = append(r.Extra, opt) + } else { + udpSize = opt.UDPSize() + } + var edns0Subnet *dns.EDNS0_SUBNET + for _, option := range opt.Option { + if option.Option() == dns.EDNS0SUBNET { + edns0Subnet = option.(*dns.EDNS0_SUBNET) + break + } + } + if edns0Subnet == nil { + ednsClientFamily := uint16(0) + ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r) + if ednsClientAddress == nil { + } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + 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) + } + + requestBinary, err := r.Pack() + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeFormatError + w.WriteMsg(reply) + return + } + requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary) + + numServers := len(c.conf.UpstreamIETF) + upstream := c.conf.UpstreamIETF[rand.Intn(numServers)] + requestURL := fmt.Sprintf("%s?ct=application/dns-udpwireformat&dns=%s", upstream, requestBase64) + + var req *http.Request + if len(requestURL) < 2048 { + req, err = http.NewRequest("GET", requestURL, nil) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + } else { + req, err = http.NewRequest("POST", upstream, bytes.NewReader(requestBinary)) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + req.Header.Set("Content-Type", "application/dns-udpwireformat") + } + req.Header.Set("Accept", "application/dns-udpwireformat") + req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") + resp, err := c.httpClient.Do(req) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + c.httpTransport.CloseIdleConnections() + return + } + if resp.StatusCode != 200 { + log.Printf("HTTP error: %s\n", resp.Status) + reply.Rcode = dns.RcodeServerFailure + contentType := resp.Header.Get("Content-Type") + if contentType != "application/dns-udpwireformat" && !strings.HasPrefix(contentType, "application/dns-udpwireformat;") { + w.WriteMsg(reply) + return + } + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + lastModified := resp.Header.Get("Last-Modified") + if lastModified == "" { + lastModified = resp.Header.Get("Date") + } + now := time.Now() + lastModifiedDate, err := time.Parse(http.TimeFormat, lastModified) + if err != nil { + log.Println(err) + lastModifiedDate = now + } + timeDelta := now.Sub(lastModifiedDate) + if timeDelta < 0 { + timeDelta = 0 + } + + fullReply := new(dns.Msg) + err = fullReply.Unpack(body) + if err != nil { + log.Println(err) + reply.Rcode = dns.RcodeServerFailure + w.WriteMsg(reply) + return + } + + fullReply.Id = requestID + for _, rr := range fullReply.Answer { + _ = fixRecordTTL(rr, timeDelta) + } + for _, rr := range fullReply.Ns { + _ = fixRecordTTL(rr, timeDelta) + } + for _, rr := range fullReply.Extra { + if rr.Header().Rrtype == dns.TypeOPT { + continue + } + _ = fixRecordTTL(rr, timeDelta) + } + + 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 fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR { + rrHeader := rr.Header() + oldTTL := time.Duration(rrHeader.Ttl) * time.Second + newTTL := oldTTL - delta + if newTTL >= 0 { + rrHeader.Ttl = uint32(newTTL / time.Second) + } else { + rrHeader.Ttl = 0 + } + return rr +} diff --git a/doh-server/ietf.go b/doh-server/ietf.go index 6475989..6fa08c1 100644 --- a/doh-server/ietf.go +++ b/doh-server/ietf.go @@ -38,7 +38,7 @@ import ( func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRequest { requestBase64 := r.FormValue("dns") - requestBinary, err := base64.StdEncoding.DecodeString(requestBase64) + requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64) if err != nil { return &DNSRequest{ errcode: 400, From 2ab2120a39a15c6fe36dbcc2ff3563266dc3a22c Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Wed, 21 Mar 2018 02:01:56 +0800 Subject: [PATCH 3/4] Implement IETF protocol --- doh-client/client.go | 2 +- doh-client/ietf.go | 31 ++++++++++++++++--------------- doh-server/ietf.go | 32 ++++++++++++++++---------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/doh-client/client.go b/doh-client/client.go index 25948b3..0a7ca93 100644 --- a/doh-client/client.go +++ b/doh-client/client.go @@ -154,7 +154,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { } numServers := len(c.conf.UpstreamGoogle) + len(c.conf.UpstreamIETF) random := rand.Intn(numServers) - if random <= len(c.conf.UpstreamGoogle) { + if random < len(c.conf.UpstreamGoogle) { c.handlerFuncGoogle(w, r, isTCP) } else { c.handlerFuncIETF(w, r, isTCP) diff --git a/doh-client/ietf.go b/doh-client/ietf.go index ea61be7..24e3efd 100644 --- a/doh-client/ietf.go +++ b/doh-client/ietf.go @@ -88,22 +88,23 @@ func (c *Client) handlerFuncIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { if edns0Subnet == nil { ednsClientFamily := uint16(0) ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r) - if ednsClientAddress == nil { - } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { - ednsClientFamily = 1 - ednsClientAddress = ipv4 - ednsClientNetmask = 24 - } else { - ednsClientFamily = 2 - ednsClientNetmask = 48 + if ednsClientAddress != nil { + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + 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) } - 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) } requestBinary, err := r.Pack() diff --git a/doh-server/ietf.go b/doh-server/ietf.go index 6fa08c1..96b9013 100644 --- a/doh-server/ietf.go +++ b/doh-server/ietf.go @@ -90,23 +90,23 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe ednsClientFamily := uint16(0) ednsClientAddress := s.findClientIP(r) ednsClientNetmask := uint8(255) - if ednsClientAddress == nil { - ednsClientNetmask = 0 - } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { - ednsClientFamily = 1 - ednsClientAddress = ipv4 - ednsClientNetmask = 24 - } else { - ednsClientFamily = 2 - ednsClientNetmask = 48 + if ednsClientAddress != nil { + if ipv4 := ednsClientAddress.To4(); ipv4 != nil { + ednsClientFamily = 1 + ednsClientAddress = ipv4 + ednsClientNetmask = 24 + } else { + ednsClientFamily = 2 + ednsClientNetmask = 48 + } + 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) } - 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) } return &DNSRequest{ From 47d706f1b57ebf342043ddc87e374b40800f50bd Mon Sep 17 00:00:00 2001 From: Star Brilliant Date: Wed, 21 Mar 2018 02:07:52 +0800 Subject: [PATCH 4/4] Update Readme --- Readme.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index b0622cc..9ac09a9 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,8 @@ DNS-over-HTTPS ============== -Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https). +Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https) +and [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https). ## Easy start @@ -71,11 +72,22 @@ Client Subnet during your configuring `unbound` or `bind`. ## Protocol compatibility -DNS-over-HTTPS use a protocol compatible to [Google DNS-over-HTTPS](https://developers.google.com/speed/public-dns/docs/dns-over-https), +### Google DNS-over-HTTPS + +DNS-over-HTTPS uses a protocol compatible to [Google DNS-over-HTTPS](https://developers.google.com/speed/public-dns/docs/dns-over-https), except for absolute expire time is preferred to relative TTL value. Refer to [json-dns/response.go](json-dns/response.go) for a complete description of the API. +### IETF DNS-over-HTTPS (Draft) + +DNS-over-HTTPS uses a protocol compatible to [draft-ietf-doh-dns-over-https +](https://github.com/dohwg/draft-ietf-doh-dns-over-https). +This protocol is in draft stage. Any incompatibility may be introduced before +it is finished. + +### Supported features + Currently supported features are: - [X] IPv4 / IPv6