diff options
| author | David Crawshaw <crawshaw@tailscale.com> | 2021-07-29 17:38:14 -0700 |
|---|---|---|
| committer | David Crawshaw <crawshaw@tailscale.com> | 2021-07-29 17:38:37 -0700 |
| commit | 8b9e9c0786021c1cd02d86fffd3ba56b523f28ef (patch) | |
| tree | d03ec0a7cbd83662dc094c92e17e1437b96c0a60 | |
| parent | d37451bac6f38cc09b853b08b1dc8359ba767fa1 (diff) | |
| download | tailscale-crawshaw/peerdoh.tar.xz tailscale-crawshaw/peerdoh.zip | |
ipnlocal, resolver, etc: add peer API DoHcrawshaw/peerdoh
| -rw-r--r-- | ipn/ipnlocal/local.go | 5 | ||||
| -rw-r--r-- | ipn/ipnlocal/peerapi.go | 34 | ||||
| -rw-r--r-- | net/dns/manager.go | 5 | ||||
| -rw-r--r-- | net/dns/resolver/forwarder.go | 55 | ||||
| -rw-r--r-- | net/dns/resolver/tsdns.go | 10 | ||||
| -rw-r--r-- | wgengine/userspace.go | 4 | ||||
| -rw-r--r-- | wgengine/watchdog.go | 4 | ||||
| -rw-r--r-- | wgengine/wgengine.go | 3 |
8 files changed, 104 insertions, 16 deletions
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index f83b38a53..a77d6c8bf 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2823,3 +2823,8 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap { } return b.netMap.DERPMap } + +// DNSManager returns the underlying DNSManager. +func (b *LocalBackend) DNSManager() *dns.Manager { + return b.e.DNSManager() +} diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 2e180afbf..75bb53ee7 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -12,6 +12,7 @@ import ( "html" "io" "io/fs" + "io/ioutil" "net" "net/http" "net/url" @@ -500,6 +501,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handlePeerPut(w, r) return } + if r.URL.Path == "/v0/doh" { + h.handleDoH(w, r) + return + } if r.URL.Path == "/v0/goroutines" { h.handleServeGoroutines(w, r) return @@ -710,3 +715,32 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re } w.Write(buf) } + +func (h *peerAPIHandler) handleDoH(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "only POST is support for DNS-over-HTTP", http.StatusMethodNotAllowed) + return + } + const dohType = "application/dns-message" + if r.Header.Get("Content-Type") != dohType { + http.Error(w, fmt.Sprintf("Content-Type=%q; want %q", r.Header.Get("Content-Type"), dohType), http.StatusBadRequest) + return + } + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + bs, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError) + return + } + + res, err := h.ps.b.DNSManager().Request(r.Context(), bs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", dohType) + w.Header().Set("Content-Length", strconv.FormatInt(int64(len(res)), 10)) + w.WriteHeader(200) + w.Write(res) +} diff --git a/net/dns/manager.go b/net/dns/manager.go index 121ee1732..786be41e4 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -6,6 +6,7 @@ package dns import ( "bufio" + "context" "runtime" "time" @@ -195,6 +196,10 @@ func (m *Manager) NextResponse() ([]byte, netaddr.IPPort, error) { return m.resolver.NextResponse() } +func (m *Manager) Request(ctx context.Context, bs []byte) ([]byte, error) { + return m.resolver.Request(ctx, bs) +} + func (m *Manager) Down() error { if err := m.os.Close(); err != nil { return err diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index 5d1904468..8f4e641f9 100644 --- a/net/dns/resolver/forwarder.go +++ b/net/dns/resolver/forwarder.go @@ -529,28 +529,56 @@ type forwardQuery struct { // forward forwards the query to all upstream nameservers and returns the first response. func (f *forwarder) forward(query packet) error { - domain, err := nameFromQuery(query.bs) + ctx, cancel := context.WithTimeout(f.ctx, responseTimeout) + defer cancel() + + v, err := f.forwardQuery(ctx, query.bs) if err != nil { return err } + select { + case <-ctx.Done(): + return ctx.Err() + case f.responses <- packet{v, query.addr}: + return nil + } +} - clampEDNSSize(query.bs, maxResponseBytes) +func (f *forwarder) Forward(ctx context.Context, bs []byte) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, responseTimeout) + defer cancel() + + go func() { + select { + case <-f.ctx.Done(): + cancel() + case <-ctx.Done(): + } + }() + + return f.forwardQuery(ctx, bs) +} + +func (f *forwarder) forwardQuery(ctx context.Context, bs []byte) ([]byte, error) { + domain, err := nameFromQuery(bs) + if err != nil { + return nil, err + } + + clampEDNSSize(bs, maxResponseBytes) resolvers := f.resolvers(domain) if len(resolvers) == 0 { - return errNoUpstreams + return nil, errNoUpstreams } fq := &forwardQuery{ - txid: getTxID(query.bs), - packet: query.bs, + txid: getTxID(bs), + packet: bs, closeOnCtxDone: new(closePool), } defer fq.closeOnCtxDone.Close() - ctx, cancel := context.WithTimeout(f.ctx, responseTimeout) - defer cancel() - resc := make(chan []byte, 1) var ( mu sync.Mutex @@ -586,19 +614,14 @@ func (f *forwarder) forward(query packet) error { select { case v := <-resc: - select { - case <-ctx.Done(): - return ctx.Err() - case f.responses <- packet{v, query.addr}: - return nil - } + return v, nil case <-ctx.Done(): mu.Lock() defer mu.Unlock() if firstErr != nil { - return firstErr + return nil, firstErr } - return ctx.Err() + return nil, ctx.Err() } } diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index bdd7b6318..04136ce46 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -8,6 +8,7 @@ package resolver import ( "bufio" + "context" "encoding/hex" "errors" "fmt" @@ -270,6 +271,15 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error) } } +// Request issues a DNS request and returns the result. +func (r *Resolver) Request(ctx context.Context, bs []byte) ([]byte, error) { + out, err := r.respond(bs) + if err == errNotOurName { + return r.forwarder.Forward(ctx, bs) + } + return out, err +} + // resolveLocal returns an IP for the given domain, if domain is in // the local hosts map and has an IP corresponding to the requested // typ (A, AAAA, ALL). diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 8c65ae0f9..ceae2d4d6 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -377,6 +377,10 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) return e, nil } +func (e *userspaceEngine) DNSManager() *dns.Manager { + return e.dns +} + // echoRespondToAll is an inbound post-filter responding to all echo requests. func echoRespondToAll(p *packet.Parsed, t *tstun.Wrapper) filter.Response { if p.IsEchoRequest() { diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index d040b2ae8..5465bd2fe 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -129,6 +129,10 @@ func (e *watchdogEngine) WhoIsIPPort(ipp netaddr.IPPort) (tsIP netaddr.IP, ok bo e.watchdog("UnregisterIPPortIdentity", func() { tsIP, ok = e.wrap.WhoIsIPPort(ipp) }) return tsIP, ok } +func (e *watchdogEngine) DNSManager() (m *dns.Manager) { + e.watchdog("DNSManager", func() { m = e.wrap.DNSManager() }) + return m +} func (e *watchdogEngine) Close() { e.watchdog("Close", e.wrap.Close) } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 22d891215..bacb31a29 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -149,4 +149,7 @@ type Engine interface { // WhoIsIPPort looks up an IP:port in the temporary registrations, // and returns a matching Tailscale IP, if it exists. WhoIsIPPort(netaddr.IPPort) (netaddr.IP, bool) + + // DNSManager returns the DNS manager for this engine. + DNSManager() *dns.Manager } |
