Skip to content

Commit

Permalink
Add support for dual stack
Browse files Browse the repository at this point in the history
When both IPv4 and IPv6 is available, race dial to all IPs and keep the
fastest one. Unlike happy eyeball, we don't give a head start to IPv6 as
we always want the absolute best.

Fixes #8
  • Loading branch information
rs committed Nov 18, 2019
1 parent 52756e9 commit 40eea6b
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 22 deletions.
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0 h1:QPlSTtPE2k6PZPasQUbzuK3p9JbS+vMXYVto8g/yrsg=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0=
Expand Down
1 change: 1 addition & 0 deletions proxy/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func replyNXDomain(q resolver.Query, buf []byte) (n int, err error) {
h.Response = true
h.RCode = dnsmessage.RCodeNameError
b := dnsmessage.NewBuilder(buf[:0], h)
_ = b.StartQuestions()
_ = b.Question(q1)
buf, err = b.Finish()
return len(buf), err
Expand Down
48 changes: 48 additions & 0 deletions resolver/endpoint/dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package endpoint

import (
"context"
"net"
)

type parallelDialer struct {
net.Dialer
}

func (d *parallelDialer) DialParallel(ctx context.Context, network string, addrs []string) (net.Conn, error) {
if len(addrs) == 1 {
return d.DialContext(ctx, network, addrs[0])
}
returned := make(chan struct{})
defer close(returned)

type dialResult struct {
net.Conn
error
}
results := make(chan dialResult)

racer := func(addr string) {
c, err := d.DialContext(ctx, network, addr)
select {
case results <- dialResult{Conn: c, error: err}:
case <-returned:
if c != nil {
c.Close()
}
}
}

for _, addr := range addrs {
go racer(addr)
}

var err error
for res := range results {
if res.error == nil {
return res.Conn, nil
}
err = res.error
}
return nil, err
}
23 changes: 12 additions & 11 deletions resolver/endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ type Endpoint struct {
// request by Transport is left untouched.
Path string

// Bootstrap is the IP to use to contact the DoH server. When provided, no
// DNS request is necessary to contact the DoH server.
Bootstrap string `json:"ip"`
// Bootstrap is the IPs to use to contact the DoH server. When provided, no
// DNS request is necessary to contact the DoH server. The fastest IP is
// used.
Bootstrap []string `json:"ips"`

once sync.Once
transport http.RoundTripper
Expand All @@ -58,7 +59,7 @@ func New(server string) (*Endpoint, error) {
Protocol: ProtocolDOH,
Hostname: u.Host,
Path: u.Path,
Bootstrap: u.Fragment,
Bootstrap: strings.Split(u.Fragment, ","),
}
return e, nil
}
Expand All @@ -84,16 +85,15 @@ func MustNew(server string) *Endpoint {
func (e *Endpoint) Equal(e2 *Endpoint) bool {
return e.Protocol == e2.Protocol &&
e.Hostname == e2.Hostname &&
e.Path == e2.Path &&
e.Bootstrap == e2.Bootstrap
e.Path == e2.Path
}

func (e *Endpoint) String() string {
if e.Protocol == ProtocolDNS {
return e.Hostname
}
if e.Bootstrap != "" {
return fmt.Sprintf("https://%s%s#%s", e.Hostname, e.Path, e.Bootstrap)
if len(e.Bootstrap) != 0 {
return fmt.Sprintf("https://%s%s#%s", e.Hostname, e.Path, strings.Join(e.Bootstrap, ","))
}
return fmt.Sprintf("https://%s%s", e.Hostname, e.Path)
}
Expand All @@ -107,15 +107,16 @@ func (e *Endpoint) RoundTrip(req *http.Request) (resp *http.Response, err error)
return e.transport.RoundTrip(req)
}

func (e *Endpoint) Test(ctx context.Context, testDomain string) error {
func (e *Endpoint) Test(ctx context.Context, testDomain string) (err error) {
switch e.Protocol {
case ProtocolDOH:
return testDOH(ctx, testDomain, e)
err = testDOH(ctx, testDomain, e)
case ProtocolDNS:
return testDNS(ctx, testDomain, e.Hostname)
err = testDNS(ctx, testDomain, e.Hostname)
default:
panic("unsupported protocol")
}
return err
}

// Provider is a type responsible for producing a list of Endpoint.
Expand Down
2 changes: 1 addition & 1 deletion resolver/endpoint/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (m *Manager) findBestEndpoint(ctx context.Context) (*activeEnpoint, error)
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := ae.Test(ctx, TestDomain); err != nil {
if err = ae.Test(ctx, TestDomain); err != nil {
if m.OnError != nil {
m.OnError(e, err)
}
Expand Down
16 changes: 14 additions & 2 deletions resolver/endpoint/transport.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package endpoint

import (
"context"
"crypto/tls"
"net"
"net/http"
Expand All @@ -16,15 +17,26 @@ type transport struct {

func newTransport(e *Endpoint) transport {
var addr string
if e.Bootstrap != "" {
addr = net.JoinHostPort(e.Bootstrap, "443")
var addrs []string
if len(e.Bootstrap) != 0 {
addr = net.JoinHostPort(e.Bootstrap[0], "443")
for _, addr := range e.Bootstrap {
addrs = append(addrs, net.JoinHostPort(addr, "443"))
}
} else {
addr = e.Hostname
}
d := &parallelDialer{}
t := &http.Transport{
TLSClientConfig: &tls.Config{
ServerName: e.Hostname,
},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
if addrs != nil {
return d.DialParallel(ctx, network, addrs)
}
return d.DialContext(ctx, network, addr)
},
}
runtime.SetFinalizer(t, func(t *http.Transport) {
t.CloseIdleConnections()
Expand Down
16 changes: 8 additions & 8 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"flag"
"fmt"
stdlog "log"
"math/rand"
"net/http"
"os"
"runtime"
Expand Down Expand Up @@ -41,13 +40,14 @@ func (p *proxySvc) Start(s service.Service) (err error) {
for _, f := range p.init {
go f(ctx)
}
_ = log.Infof("Starting NextDNS on %s", p.Addr)
if err = p.ListenAndServe(ctx); err != nil && err != context.Canceled {
errC <- err
}
}()
select {
case err := <-errC:
_ = log.Errorf("Start: %v", err)
_ = log.Errorf("Start error: %v", err)
return err
case <-time.After(5 * time.Second):
}
Expand Down Expand Up @@ -222,9 +222,9 @@ func svc(cmd string) error {
// nextdnsTransport returns a endpoint.Manager configured to connect to NextDNS
// using different steering techniques.
func nextdnsTransport(hpm bool) http.RoundTripper {
var qs string
qs := "?stack=dual"
if hpm {
qs = "?hardened_privacy=1"
qs = "&hardened_privacy=1"
}
return &endpoint.Manager{
Providers: []endpoint.Provider{
Expand All @@ -233,18 +233,18 @@ func nextdnsTransport(hpm bool) http.RoundTripper {
SourceURL: "https://router.nextdns.io" + qs,
Client: &http.Client{
// Trick to avoid depending on DNS to contact the router API.
Transport: endpoint.MustNew(fmt.Sprintf("https://router.nextdns.io#%s", []string{
Transport: &endpoint.Endpoint{Hostname: "router.nextdns.io", Bootstrap: []string{
"216.239.32.21",
"216.239.34.21",
"216.239.36.21",
"216.239.38.21",
}[rand.Intn(3)])),
}},
},
},
// Fallback on anycast.
endpoint.StaticProvider([]*endpoint.Endpoint{
endpoint.MustNew("https://dns1.nextdns.io#45.90.28.0"),
endpoint.MustNew("https://dns2.nextdns.io#45.90.30.0"),
endpoint.MustNew("https://dns1.nextdns.io#45.90.28.0,2a07:a8c0::"),
endpoint.MustNew("https://dns2.nextdns.io#45.90.30.0,2a07:a8c1::"),
}),
},
OnError: func(e *endpoint.Endpoint, err error) {
Expand Down

0 comments on commit 40eea6b

Please sign in to comment.