diff --git a/Makefile b/Makefile index 7bbfda2..b4b7c46 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ clean: install: doh-client/doh-client doh-server/doh-server install -Dm0755 doh-client/doh-client "$(DESTDIR)$(PREFIX)/bin/doh-client" install -Dm0755 doh-server/doh-server "$(DESTDIR)$(PREFIX)/bin/doh-server" + [ -e "$(DESTDIR)/etc/dns-over-https/doh-client.conf" ] || install -Dm0644 doh-client/doh-client.conf "$(DESTDIR)/etc/dns-over-https/doh-client.conf" + [ -e "$(DESTDIR)/etc/dns-over-https/doh-server.conf" ] || install -Dm0644 doh-server/doh-server.conf "$(DESTDIR)/etc/dns-over-https/doh-server.conf" $(MAKE) -C systemd install "DESTDIR=$(DESTDIR)" "PREFIX=$(PREFIX)" $(MAKE) -C NetworkManager install "DESTDIR=$(DESTDIR)" "PREFIX=$(PREFIX)" @@ -20,8 +22,8 @@ uninstall: $(MAKE) -C systemd uninstall "DESTDIR=$(DESTDIR)" "PREFIX=$(PREFIX)" $(MAKE) -C NetworkManager uninstall "DESTDIR=$(DESTDIR)" "PREFIX=$(PREFIX)" -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 +doh-client/doh-client: doh-client/client.go doh-client/config.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 && $(GOGET) && $(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 +doh-server/doh-server: doh-server/config.go 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 && $(GOGET) && $(GOBUILD) diff --git a/Readme.md b/Readme.md index c85f043..babceaa 100644 --- a/Readme.md +++ b/Readme.md @@ -24,8 +24,7 @@ By default, [Google DNS over HTTPS](https://dns.google.com) is used. It should work for most users (except for People's Republic of China). If you need to modify the default settings, type: - sudo cp /usr/lib/systemd/system/doh-client.service /etc/systemd/system/ - sudoedit /etc/systemd/system/doh-client.service + sudoedit /etc/dns-over-https/doh-client.conf To automatically start DNS-over-HTTPS client as a system service, type: diff --git a/doh-client/client.go b/doh-client/client.go index 3ee543a..41d4e9d 100644 --- a/doh-client/client.go +++ b/doh-client/client.go @@ -42,53 +42,45 @@ import ( ) type Client struct { - addr string - upstream string - bootstraps []string - timeout uint - noECS bool - verbose bool + conf *config + bootstrap []string udpServer *dns.Server tcpServer *dns.Server httpClient *http.Client } -func NewClient(addr, upstream string, bootstraps []string, timeout uint, noECS, verbose bool) (c *Client, err error) { +func NewClient(conf *config) (c *Client, err error) { c = &Client { - addr: addr, - upstream: upstream, - bootstraps: bootstraps, - timeout: timeout, - noECS: noECS, - verbose: verbose, + conf: conf, } c.udpServer = &dns.Server { - Addr: addr, + Addr: conf.Listen, Net: "udp", Handler: dns.HandlerFunc(c.udpHandlerFunc), UDPSize: 4096, } c.tcpServer = &dns.Server { - Addr: addr, + Addr: conf.Listen, Net: "tcp", Handler: dns.HandlerFunc(c.tcpHandlerFunc), } bootResolver := net.DefaultResolver - if len(c.bootstraps) != 0 { - for i, bootstrap := range c.bootstraps { + if len(conf.Bootstrap) != 0 { + c.bootstrap = make([]string, len(conf.Bootstrap)) + for i, bootstrap := range conf.Bootstrap { bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap) if err != nil { bootstrapAddr, err = net.ResolveUDPAddr("udp", "[" + bootstrap + "]:53") } if err != nil { return nil, err } - c.bootstraps[i] = bootstrapAddr.String() + c.bootstrap[i] = bootstrapAddr.String() } bootResolver = &net.Resolver { PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { var d net.Dialer - num_servers := len(c.bootstraps) - bootstrap := c.bootstraps[rand.Intn(num_servers)] + num_servers := len(c.bootstrap) + bootstrap := c.bootstrap[rand.Intn(num_servers)] conn, err := d.DialContext(ctx, network, bootstrap) return conn, err }, @@ -96,12 +88,12 @@ func NewClient(addr, upstream string, bootstraps []string, timeout uint, noECS, } httpTransport := *http.DefaultTransport.(*http.Transport) httpTransport.DialContext = (&net.Dialer { - Timeout: time.Duration(c.timeout) * time.Second, + Timeout: time.Duration(conf.Timeout) * time.Second, KeepAlive: 30 * time.Second, DualStack: true, Resolver: bootResolver, }).DialContext - httpTransport.ResponseHeaderTimeout = time.Duration(c.timeout) * time.Second + httpTransport.ResponseHeaderTimeout = time.Duration(conf.Timeout) * time.Second // Most CDNs require Cookie support to prevent DDoS attack cookieJar, err := cookiejar.New(nil) if err != nil { return nil, err } @@ -159,11 +151,13 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { questionType = strconv.Itoa(int(question.Qtype)) } - if c.verbose{ + 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) } - requestURL := fmt.Sprintf("%s?name=%s&type=%s", c.upstream, url.QueryEscape(questionName), url.QueryEscape(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" @@ -260,7 +254,7 @@ var ( func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) { ednsClientNetmask = 255 - if c.noECS { + if c.conf.NoECS { return net.IPv4(0, 0, 0, 0), 0 } if opt := r.IsEdns0(); opt != nil { diff --git a/doh-client/config.go b/doh-client/config.go new file mode 100644 index 0000000..47eb6af --- /dev/null +++ b/doh-client/config.go @@ -0,0 +1,69 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 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 ( + "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"` +} + +func loadConfig(path string) (*config, error) { + 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()) } + } + + if conf.Listen == "" { + conf.Listen = "127.0.0.1:53" + } + if len(conf.Upstream) == 0 { + conf.Upstream = []string { "https://dns.google.com/resolve" } + } + if conf.Timeout == 0 { + conf.Timeout = 10 + } + + return conf, nil +} + +type configError struct { + err string +} + +func (e *configError) Error() string { + return e.err +} diff --git a/doh-client/doh-client.conf b/doh-client/doh-client.conf new file mode 100644 index 0000000..0caf0a9 --- /dev/null +++ b/doh-client/doh-client.conf @@ -0,0 +1,25 @@ +# DNS listen port +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 = [ + "https://dns.google.com/resolve", +] + +# Bootstrap DNS server to resolve the address of the upstream resolver +# If multiple servers are specified, a random one will be chosen each time. +# If empty, use the system DNS settings. +bootstrap = [ + "8.8.8.8:53", + "8.8.4.4:53", +] + +# Timeout for upstream request +timeout = 10 + +# Disable EDNS0-Client-Subnet, do not send client's IP address +no_ecs = false + +# Enable logging +verbose = false diff --git a/doh-client/main.go b/doh-client/main.go index 5334c49..10aa4ae 100644 --- a/doh-client/main.go +++ b/doh-client/main.go @@ -26,23 +26,25 @@ package main import ( "flag" "log" - "strings" ) func main() { - addr := flag.String("addr", "127.0.0.1:53", "DNS listen port") - upstream := flag.String("upstream", "https://dns.google.com/resolve", "HTTP path for upstream resolver") - bootstrap := flag.String("bootstrap", "", "The bootstrap DNS server to resolve the address of the upstream resolver") - timeout := flag.Uint("timeout", 10, "Timeout for upstream request") - noECS := flag.Bool("no-ecs", false, "Disable EDNS0-Client-Subnet, do not send client's IP address") + confPath := flag.String("conf", "doh-client.conf", "Configuration file") verbose := flag.Bool("verbose", false, "Enable logging") flag.Parse() - bootstraps := []string {} - if *bootstrap != "" { - bootstraps = strings.Split(*bootstrap, ",") + conf, err := loadConfig(*confPath) + if err != nil { + log.Fatalln(err) + } + + if *verbose { + conf.Verbose = true + } + + client, err := NewClient(conf) + if err != nil { + log.Fatalln(err) } - client, err := NewClient(*addr, *upstream, bootstraps, *timeout, *noECS, *verbose) - if err != nil { log.Fatalln(err) } _ = client.Start() } diff --git a/doh-server/config.go b/doh-server/config.go new file mode 100644 index 0000000..6be4747 --- /dev/null +++ b/doh-server/config.go @@ -0,0 +1,78 @@ +/* + DNS-over-HTTPS + Copyright (C) 2017 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 ( + "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"` + Tries uint `toml:"tries"` + TCPOnly bool `toml:"tcp_only"` + Verbose bool `toml:"verbose"` +} + +func loadConfig(path string) (*config, error) { + 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()) } + } + + if conf.Listen == "" { + conf.Listen = "127.0.0.1:8053" + } + if conf.Path == "" { + conf.Path = "/resolve" + } + if len(conf.Upstream) == 0 { + conf.Upstream = []string { "8.8.8.8:53", "8.8.4.4:53" } + } + if conf.Tries == 0 { + conf.Tries = 3 + } + + if (conf.Cert != "") != (conf.Key != "") { + return nil, &configError { "You must specify both -cert and -key to enable TLS" } + } + + return conf, nil +} + +type configError struct { + err string +} + +func (e *configError) Error() string { + return e.err +} diff --git a/doh-server/doh-server.conf b/doh-server/doh-server.conf new file mode 100644 index 0000000..f1bec3f --- /dev/null +++ b/doh-server/doh-server.conf @@ -0,0 +1,27 @@ +# HTTP listen port +listen = "127.0.0.1:8053" + +# TLS certification file +cert = "" + +# TLS key file +key = "" + +# HTTP path for resolve application +path = "/resolve" + +# Upstream DNS resolver +# If multiple servers are specified, a random one will be chosen each time. +upstream = [ + "8.8.8.8:53", + "8.8.4.4:53", +] + +# Number of tries if upstream DNS fails +tries = 3 + +# Only use TCP for DNS query +tcp_only = false + +# Enable logging +verbose = false diff --git a/doh-server/main.go b/doh-server/main.go index 036d51a..e53cdb4 100644 --- a/doh-server/main.go +++ b/doh-server/main.go @@ -26,26 +26,24 @@ package main import ( "flag" "log" - "strings" ) func main() { - addr := flag.String("addr", "127.0.0.1:8053", "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") + confPath := flag.String("conf", "doh-server.conf", "Configuration file") verbose := flag.Bool("verbose", false, "Enable logging") flag.Parse() - if (*cert != "") != (*key != "") { - log.Fatalln("You must specify both -cert and -key to enable TLS") + conf, err := loadConfig(*confPath) + if err != nil { + log.Fatalln(err) } - upstreams := strings.Split(*upstream, ",") - server := NewServer(*addr, *cert, *key, *path, upstreams, *tcpOnly, *verbose) - err := server.Start() + if *verbose { + conf.Verbose = true + } + + server := NewServer(conf) + err = server.Start() if err != nil { log.Fatalln(err) } diff --git a/doh-server/server.go b/doh-server/server.go index 06027fd..d409445 100644 --- a/doh-server/server.go +++ b/doh-server/server.go @@ -41,29 +41,15 @@ import ( ) type Server struct { - addr string - cert string - key string - path string - upstreams []string - tcpOnly bool - verbose bool + conf *config udpClient *dns.Client tcpClient *dns.Client servemux *http.ServeMux } -func NewServer(addr, cert, key, path string, upstreams []string, tcpOnly, verbose bool) (s *Server) { - upstreamsCopy := make([]string, len(upstreams)) - copy(upstreamsCopy, upstreams) +func NewServer(conf *config) (s *Server) { s = &Server { - addr: addr, - cert: cert, - key: key, - path: path, - upstreams: upstreamsCopy, - tcpOnly: tcpOnly, - verbose: verbose, + conf: conf, udpClient: &dns.Client { Net: "udp", }, @@ -72,19 +58,19 @@ func NewServer(addr, cert, key, path string, upstreams []string, tcpOnly, verbos }, servemux: http.NewServeMux(), } - s.servemux.HandleFunc(path, s.handlerFunc) + s.servemux.HandleFunc(conf.Path, s.handlerFunc) return } func (s *Server) Start() error { servemux := http.Handler(s.servemux) - if s.verbose { + if s.conf.Verbose { servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux) } - if s.cert != "" || s.key != "" { - return http.ListenAndServeTLS(s.addr, s.cert, s.key, servemux) + 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.addr, servemux) + return http.ListenAndServe(s.conf.Listen, servemux) } } @@ -211,6 +197,7 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { 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 } @@ -259,31 +246,17 @@ 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.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 { + num_servers := len(s.conf.Upstream) + for i := uint(0); i < s.conf.Tries; i++ { + server := s.conf.Upstream[rand.Intn(num_servers)] + if !s.conf.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 diff --git a/systemd/doh-client.service b/systemd/doh-client.service index e217db6..e339969 100644 --- a/systemd/doh-client.service +++ b/systemd/doh-client.service @@ -6,7 +6,7 @@ Wants=nss-lookup.target [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE -ExecStart=/usr/local/bin/doh-client -addr 127.0.0.1:53 -upstream https://dns.google.com/resolve -bootstrap 8.8.8.8:53,8.8.4.4:53 +ExecStart=/usr/local/bin/doh-client -conf /etc/dns-over-https/doh-client.conf LimitNOFILE=1048576 Restart=always RestartSec=3 diff --git a/systemd/doh-server.service b/systemd/doh-server.service index d0a4f9c..e04c8d5 100644 --- a/systemd/doh-server.service +++ b/systemd/doh-server.service @@ -4,7 +4,7 @@ After=network.target [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE -ExecStart=/usr/local/bin/doh-server -addr 127.0.0.1:8053 -upstream 8.8.8.8:53,8.8.4.4:53 +ExecStart=/usr/local/bin/doh-server -conf /etc/dns-over-https/doh-server.conf LimitNOFILE=1048576 Restart=always RestartSec=3