// dnss is a tool for encapsulating DNS over HTTPS.
//
// It can act as a DNS-to-HTTPS proxy, exposing a traditional DNS server and
// resolving queries using any DNS-over-HTTP (DoH) server.
//
// It can also act as an HTTPS-to-DNS proxy, so you can use it as a DoH server
// if you want more control over the servers and the final DNS server used
// (for example if you are in an isolated environment, such as a test lab or a
// private network).
//
// See the README.md file for more details.
package main
import (
"flag"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"golang.org/x/net/http/httpproxy"
"blitiri.com.ar/go/dnss/internal/dnsserver"
"blitiri.com.ar/go/dnss/internal/httpresolver"
"blitiri.com.ar/go/dnss/internal/httpserver"
"blitiri.com.ar/go/log"
// Register pprof handlers for monitoring and debugging.
_ "net/http/pprof"
)
var (
dnsListenAddr = flag.String("dns_listen_addr", ":53",
"address to listen on for DNS")
dnsUnqualifiedUpstream = flag.String("dns_unqualified_upstream", "",
"DNS server to forward unqualified requests to")
fallbackUpstream = flag.String("fallback_upstream", "8.8.8.8:53",
"DNS server to resolve domains in --fallback_domains")
fallbackDomains = flag.String("fallback_domains", "dns.google.",
"Domains we resolve via DNS, using --fallback_upstream"+
" (space-separated list)")
enableDNStoHTTPS = flag.Bool("enable_dns_to_https", false,
"enable DNS-to-HTTPS proxy")
httpsUpstream = flag.String("https_upstream",
"https://dns.google/dns-query",
"URL of upstream DNS-to-HTTP server")
httpsClientCAFile = flag.String("https_client_cafile", "",
"CA file to use for the HTTPS client")
enableCache = flag.Bool("enable_cache", true, "enable the local cache")
enableHTTPStoDNS = flag.Bool("enable_https_to_dns", false,
"enable HTTPS-to-DNS proxy")
dnsUpstream = flag.String("dns_upstream",
"8.8.8.8:53",
"Address of the upstream DNS server (for the HTTPS-to-DNS proxy)")
httpsCertFile = flag.String("https_cert", "",
"certificate to use for the HTTPS server")
httpsKeyFile = flag.String("https_key", "",
"key to use for the HTTPS server")
httpsAddr = flag.String("https_server_addr", ":443",
"address to listen on for HTTPS-to-DNS requests")
insecureHTTPServer = flag.Bool("insecure_http_server", false,
"listen on plain HTTP, not HTTPS")
monitoringListenAddr = flag.String("monitoring_listen_addr", "",
"address to listen on for monitoring HTTP requests")
// Deprecated flags that no longer make sense; we keep them for backwards
// compatibility but may be removed in the future.
_ = flag.Duration("log_flush_every", 0, "deprecated, will be removed")
_ = flag.Bool("logtostderr", false, "deprecated, will be removed")
_ = flag.String("force_mode", "", "deprecated, will be removed")
)
func main() {
flag.Parse()
log.Init()
if *monitoringListenAddr != "" {
launchMonitoringServer(*monitoringListenAddr)
}
if !(*enableDNStoHTTPS || *enableHTTPStoDNS) {
log.Errorf("Need to set one of the following:")
log.Errorf(" --enable_dns_to_https")
log.Errorf(" --enable_https_to_dns")
log.Fatalf("")
}
var wg sync.WaitGroup
// DNS to HTTPS.
if *enableDNStoHTTPS {
upstream, err := url.Parse(*httpsUpstream)
if err != nil {
log.Fatalf("-https_upstream is not a valid URL: %v", err)
}
var resolver dnsserver.Resolver
resolver = httpresolver.NewDoH(upstream, *httpsClientCAFile)
if *enableCache {
cr := dnsserver.NewCachingResolver(resolver)
cr.RegisterDebugHandlers()
resolver = cr
}
dth := dnsserver.New(*dnsListenAddr, resolver, *dnsUnqualifiedUpstream)
// If we're using an HTTP proxy, add the name to the fallback domain
// so we don't have problems resolving it.
fallbackDoms := strings.Split(*fallbackDomains, " ")
if proxyDomain := proxyServerDomain(); proxyDomain != "" {
log.Infof("Adding proxy %q to fallback domains", proxyDomain)
fallbackDoms = append(fallbackDoms, proxyDomain)
}
dth.SetFallback(*fallbackUpstream, fallbackDoms)
wg.Add(1)
go func() {
defer wg.Done()
dth.ListenAndServe()
}()
}
// HTTPS to DNS.
if *enableHTTPStoDNS {
s := httpserver.Server{
Addr: *httpsAddr,
Upstream: *dnsUpstream,
CertFile: *httpsCertFile,
KeyFile: *httpsKeyFile,
Insecure: *insecureHTTPServer,
}
wg.Add(1)
go func() {
defer wg.Done()
s.ListenAndServe()
}()
}
wg.Wait()
}
// proxyServerDomain checks if we're using an HTTP proxy server, and if so
// returns its domain.
func proxyServerDomain() string {
url, err := url.Parse(*httpsUpstream)
if err != nil {
return ""
}
proxyFunc := httpproxy.FromEnvironment().ProxyFunc()
proxyURL, err := proxyFunc(url)
if err != nil || proxyURL == nil {
return ""
}
return proxyURL.Hostname()
}
func launchMonitoringServer(addr string) {
log.Infof("Monitoring HTTP server listening on %s", addr)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Write([]byte(monitoringHTMLIndex))
})
flags := dumpFlags()
http.HandleFunc("/debug/flags", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(flags))
})
go http.ListenAndServe(addr, nil)
}
// Static index for the monitoring website.
const monitoringHTMLIndex = `<!DOCTYPE html>
<html>
<head>
<title>dnss monitoring</title>
</head>
<body>
<h1>dnss monitoring</h1>
<ul>
<li><a href="/debug/requests">requests</a>
<small><a href="https://godoc.org/golang.org/x/net/trace">
(ref)</a></small>
<ul>
<li><a href="/debug/requests?fam=dnsserver&b=11">dns server latency</a>
<li><a href="/debug/requests?fam=dnsserver&b=0&exp=1">dns server trace</a>
</ul>
<li><a href="/debug/dnsserver/cache/dump">cache dump</a>
<li><a href="/debug/pprof">pprof</a>
<small><a href="https://golang.org/pkg/net/http/pprof/">
(ref)</a></small>
<ul>
<li><a href="/debug/pprof/goroutine?debug=1">goroutines</a>
</ul>
<li><a href="/debug/flags">flags</a>
<li><a href="/debug/vars">public variables</a>
</ul>
</body>
</html>
`
// dumpFlags to a string, for troubleshooting purposes.
func dumpFlags() string {
s := ""
visited := make(map[string]bool)
// Print set flags first, then the rest.
flag.Visit(func(f *flag.Flag) {
s += fmt.Sprintf("-%s=%s\n", f.Name, f.Value.String())
visited[f.Name] = true
})
s += "\n"
flag.VisitAll(func(f *flag.Flag) {
if !visited[f.Name] {
s += fmt.Sprintf("-%s=%s\n", f.Name, f.Value.String())
}
})
return s
}