summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Crawshaw <crawshaw@tailscale.com>2021-07-29 17:38:14 -0700
committerDavid Crawshaw <crawshaw@tailscale.com>2021-07-29 17:38:37 -0700
commit8b9e9c0786021c1cd02d86fffd3ba56b523f28ef (patch)
treed03ec0a7cbd83662dc094c92e17e1437b96c0a60
parentd37451bac6f38cc09b853b08b1dc8359ba767fa1 (diff)
downloadtailscale-crawshaw/peerdoh.tar.xz
tailscale-crawshaw/peerdoh.zip
ipnlocal, resolver, etc: add peer API DoHcrawshaw/peerdoh
-rw-r--r--ipn/ipnlocal/local.go5
-rw-r--r--ipn/ipnlocal/peerapi.go34
-rw-r--r--net/dns/manager.go5
-rw-r--r--net/dns/resolver/forwarder.go55
-rw-r--r--net/dns/resolver/tsdns.go10
-rw-r--r--wgengine/userspace.go4
-rw-r--r--wgengine/watchdog.go4
-rw-r--r--wgengine/wgengine.go3
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
}