diff --git a/upstream/helpers.go b/upstream/helpers.go new file mode 100644 index 00000000..6e9cc30a --- /dev/null +++ b/upstream/helpers.go @@ -0,0 +1,23 @@ +package upstream + +import "github.com/miekg/dns" + +// Performs a simple health-check of the specified upstream +func IsAlive(u Upstream) (bool, error) { + + // Using ipv4only.arpa. domain as it is a part of DNS64 RFC and it should exist everywhere + ping := new(dns.Msg) + ping.SetQuestion("ipv4only.arpa.", dns.TypeA) + + resp, err := u.Exchange(nil, ping) + + // If we got a header, we're alright, basically only care about I/O errors 'n stuff. + if err != nil && resp != nil { + // Silly check, something sane came back. + if resp.Response || resp.Opcode == dns.OpcodeQuery { + err = nil + } + } + + return err == nil, err +} diff --git a/upstream/https_upstream.go b/upstream/https_upstream.go index 7daab106..906eadf2 100644 --- a/upstream/https_upstream.go +++ b/upstream/https_upstream.go @@ -10,16 +10,17 @@ import ( "golang.org/x/net/http2" "io/ioutil" "log" + "net" "net/http" "net/url" + "time" ) const ( dnsMessageContentType = "application/dns-message" + defaultKeepAlive = 30 * time.Second ) -// TODO: Add bootstrap DNS resolver field - // HttpsUpstream is the upstream implementation for DNS-over-HTTPS type HttpsUpstream struct { client *http.Client @@ -27,18 +28,39 @@ type HttpsUpstream struct { } // NewHttpsUpstream creates a new DNS-over-HTTPS upstream from hostname -func NewHttpsUpstream(endpoint string) (Upstream, error) { +func NewHttpsUpstream(endpoint string, bootstrap string) (Upstream, error) { u, err := url.Parse(endpoint) if err != nil { return nil, err } + // Initialize bootstrap resolver + bootstrapResolver := net.DefaultResolver + if bootstrap != "" { + bootstrapResolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + var d net.Dialer + conn, err := d.DialContext(ctx, network, bootstrap) + return conn, err + }, + } + } + + dialer := &net.Dialer{ + Timeout: defaultTimeout, + KeepAlive: defaultKeepAlive, + DualStack: true, + Resolver: bootstrapResolver, + } + // Update TLS and HTTP client configuration tlsConfig := &tls.Config{ServerName: u.Hostname()} transport := &http.Transport{ TLSClientConfig: tlsConfig, DisableCompression: true, MaxIdleConns: 1, + DialContext: dialer.DialContext, } http2.ConfigureTransport(transport) diff --git a/upstream/upstream.go b/upstream/upstream.go index 44d4e389..9d2222dc 100644 --- a/upstream/upstream.go +++ b/upstream/upstream.go @@ -42,8 +42,8 @@ func (p *UpstreamPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r * var reply *dns.Msg var backendErr error - // TODO: Change the way we call upstreams - for _, upstream := range p.Upstreams { + for i := range p.Upstreams { + upstream := p.Upstreams[i] reply, backendErr = upstream.Exchange(ctx, r) if backendErr == nil { w.WriteMsg(reply) @@ -67,4 +67,4 @@ func (p *UpstreamPlugin) finalizer() { log.Printf("Error while closing the upstream: %s", err) } } -} +} \ No newline at end of file diff --git a/upstream/upstream_test.go b/upstream/upstream_test.go index 5e60b63d..f612fc6e 100644 --- a/upstream/upstream_test.go +++ b/upstream/upstream_test.go @@ -6,27 +6,107 @@ import ( "testing" ) -func TestDnsUpstream(t *testing.T) { +func TestDnsUpstreamIsAlive(t *testing.T) { - u, err := NewDnsUpstream("8.8.8.8:53", "udp", "") - - if err != nil { - t.Errorf("cannot create a DNS upstream") + var tests = []struct { + endpoint string + proto string + }{ + {"8.8.8.8:53", "udp"}, + {"8.8.8.8:53", "tcp"}, + {"1.1.1.1:53", "udp"}, } - testUpstream(t, u) + for _, test := range tests { + u, err := NewDnsUpstream(test.endpoint, test.proto, "") + + if err != nil { + t.Errorf("cannot create a DNS upstream") + } + + testUpstreamIsAlive(t, u) + } +} + +func TestHttpsUpstreamIsAlive(t *testing.T) { + + var tests = []struct { + url string + bootstrap string + }{ + {"https://cloudflare-dns.com/dns-query", "8.8.8.8:53"}, + {"https://dns.google.com/experimental", "8.8.8.8:53"}, + {"https://doh.cleanbrowsing.org/doh/security-filter/", ""}, // TODO: status 201?? + } + + for _, test := range tests { + u, err := NewHttpsUpstream(test.url, test.bootstrap) + + if err != nil { + t.Errorf("cannot create a DNS-over-HTTPS upstream") + } + + testUpstreamIsAlive(t, u) + } +} + +func TestDnsOverTlsIsAlive(t *testing.T) { + + var tests = []struct { + endpoint string + tlsServerName string + }{ + {"1.1.1.1:853", ""}, + {"9.9.9.9:853", ""}, + {"185.228.168.10:853", "security-filter-dns.cleanbrowsing.org"}, + } + + for _, test := range tests { + u, err := NewDnsUpstream(test.endpoint, "tcp-tls", test.tlsServerName) + + if err != nil { + t.Errorf("cannot create a DNS-over-TLS upstream") + } + + testUpstreamIsAlive(t, u) + } +} + +func TestDnsUpstream(t *testing.T) { + + var tests = []struct { + endpoint string + proto string + }{ + {"8.8.8.8:53", "udp"}, + {"8.8.8.8:53", "tcp"}, + {"1.1.1.1:53", "udp"}, + } + + for _, test := range tests { + u, err := NewDnsUpstream(test.endpoint, test.proto, "") + + if err != nil { + t.Errorf("cannot create a DNS upstream") + } + + testUpstream(t, u) + } } func TestHttpsUpstream(t *testing.T) { - testCases := []string{ - "https://cloudflare-dns.com/dns-query", - "https://dns.google.com/experimental", - "https://doh.cleanbrowsing.org/doh/security-filter/", + var tests = []struct { + url string + bootstrap string + }{ + {"https://cloudflare-dns.com/dns-query", "8.8.8.8:53"}, + {"https://dns.google.com/experimental", "8.8.8.8:53"}, + {"https://doh.cleanbrowsing.org/doh/security-filter/", ""}, } - for _, url := range testCases { - u, err := NewHttpsUpstream(url) + for _, test := range tests { + u, err := NewHttpsUpstream(test.url, test.bootstrap) if err != nil { t.Errorf("cannot create a DNS-over-HTTPS upstream") @@ -58,6 +138,15 @@ func TestDnsOverTlsUpstream(t *testing.T) { } } +func testUpstreamIsAlive(t *testing.T, u Upstream) { + alive, err := IsAlive(u) + if !alive || err != nil { + t.Errorf("Upstream is not alive") + } + + u.Close() +} + func testUpstream(t *testing.T, u Upstream) { var tests = []struct {