ipn/ipnlocal, net/tsdial: make SOCKS/HTTP dials use ExitDNS

And simplify, unexport some tsdial/netstack stuff in the the process.

Fixes #3475

Change-Id: I186a5a5cbd8958e25c075b4676f7f6e70f3ff76e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-12-03 08:33:05 -08:00 committed by Brad Fitzpatrick
parent 9f6249b26d
commit 9c5c9d0a50
7 changed files with 181 additions and 27 deletions

View File

@ -333,7 +333,7 @@ func run() error {
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst.String())
return ns.DialContextTCP(ctx, dst)
}
}

View File

@ -1898,6 +1898,15 @@ func (b *LocalBackend) authReconfig() {
}
}
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
if err != nil {
b.logf("wgcfg: %v", err)

86
net/tsdial/dohclient.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdial
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
)
// dohConn is a net.PacketConn suitable for returning from
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
// ExitDNS DoH proxy service.
type dohConn struct {
ctx context.Context
baseURL string
hc *http.Client // if nil, default is used
rbuf bytes.Buffer
}
var (
_ net.Conn = (*dohConn)(nil)
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
)
func (*dohConn) Close() error { return nil }
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
func (*dohConn) SetDeadline(t time.Time) error { return nil }
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.Write(p)
}
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = c.Read(p)
return n, todoAddr{}, err
}
func (c *dohConn) Read(p []byte) (n int, err error) {
return c.rbuf.Read(p)
}
func (c *dohConn) Write(packet []byte) (n int, err error) {
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
if err != nil {
return 0, err
}
const dohType = "application/dns-message"
req.Header.Set("Content-Type", dohType)
hc := c.hc
if hc == nil {
hc = http.DefaultClient
}
hres, err := hc.Do(req)
if err != nil {
return 0, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
return 0, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
}
_, err = io.Copy(&c.rbuf, hres.Body)
if err != nil {
return 0, err
}
return len(packet), nil
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }

View File

@ -0,0 +1,32 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdial
import (
"context"
"flag"
"net"
"testing"
"time"
)
var dohBase = flag.String("doh-base", "", "DoH base URL for manual DoH tests; e.g. \"http://100.68.82.120:47830/dns-query\"")
func TestDoHResolve(t *testing.T) {
if *dohBase == "" {
t.Skip("skipping manual test without --doh-base= set")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var r net.Resolver
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{ctx: ctx, baseURL: *dohBase}, nil
}
addrs, err := r.LookupIP(ctx, "ip4", "google.com.")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %q", addrs)
}

View File

@ -11,6 +11,7 @@ import (
"fmt"
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"syscall"
@ -43,10 +44,11 @@ type Dialer struct {
peerDialerOnce sync.Once
peerDialer *net.Dialer
mu sync.Mutex
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
mu sync.Mutex
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
@ -66,6 +68,17 @@ func (d *Dialer) TUNName() string {
return d.tunName
}
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
// The doh URL should contain the scheme, authority, and path, but without
// a '?' and/or query parameters.
//
// For example, "http://100.68.82.120:47830/dns-query".
func (d *Dialer) SetExitDNSDoH(doh string) {
d.mu.Lock()
defer d.mu.Unlock()
d.exitDNSDoHBase = doh
}
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
d.mu.Lock()
defer d.mu.Unlock()
@ -113,21 +126,20 @@ func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
d.dns = m
}
func (d *Dialer) Resolve(ctx context.Context, network, addr string) (netaddr.IPPort, error) {
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
exitDNSDoH := d.exitDNSDoHBase
d.mu.Unlock()
// MagicDNS or otherwise baked in to the NetworkMap? Try that first.
ipp, err := dns.resolveMemory(ctx, network, addr)
if err != errUnresolved {
return ipp, err
}
// Otherwise, hit the network.
// TODO(bradfitz): use ExitDNS (Issue 3475)
// TODO(bradfitz): wire up net/dnscache too.
host, port, err := splitHostPort(addr)
@ -137,7 +149,17 @@ func (d *Dialer) Resolve(ctx context.Context, network, addr string) (netaddr.IPP
}
var r net.Resolver
ips, err := r.LookupIP(ctx, network, host)
if exitDNSDoH != "" {
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{
ctx: ctx,
baseURL: exitDNSDoH,
hc: d.PeerAPIHTTPClient(),
}, nil
}
}
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
if err != nil {
return netaddr.IPPort{}, err
}
@ -148,10 +170,23 @@ func (d *Dialer) Resolve(ctx context.Context, network, addr string) (netaddr.IPP
return netaddr.IPPortFrom(ip, port), nil
}
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
// to the input value of "tcp", "tcp4", "udp6" etc network
// names.
func ipNetOfNetwork(n string) string {
if strings.HasSuffix(n, "4") {
return "ip4"
}
if strings.HasSuffix(n, "6") {
return "ip6"
}
return "ip"
}
// UserDial connects to the provided network address as if a user were initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy)
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.Resolve(ctx, network, addr)
ipp, err := d.userDialResolve(ctx, network, addr)
if err != nil {
return nil, err
}

View File

@ -146,7 +146,7 @@ func (s *Server) start() error {
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst.String())
return ns.DialContextTCP(ctx, dst)
}
statePath := filepath.Join(s.dir, "tailscaled.state")

View File

@ -295,18 +295,14 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
}
}
func (ns *Impl) DialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn, error) {
remoteIPPort, err := ns.dialer.Resolve(ctx, "tcp", addr)
if err != nil {
return nil, err
}
func (ns *Impl) DialContextTCP(ctx context.Context, ipp netaddr.IPPort) (*gonet.TCPConn, error) {
remoteAddress := tcpip.FullAddress{
NIC: nicID,
Addr: tcpip.Address(remoteIPPort.IP().IPAddr().IP),
Port: remoteIPPort.Port(),
Addr: tcpip.Address(ipp.IP().IPAddr().IP),
Port: ipp.Port(),
}
var ipType tcpip.NetworkProtocolNumber
if remoteIPPort.IP().Is4() {
if ipp.IP().Is4() {
ipType = ipv4.ProtocolNumber
} else {
ipType = ipv6.ProtocolNumber
@ -315,18 +311,14 @@ func (ns *Impl) DialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn
return gonet.DialContextTCP(ctx, ns.ipstack, remoteAddress, ipType)
}
func (ns *Impl) DialContextUDP(ctx context.Context, addr string) (*gonet.UDPConn, error) {
remoteIPPort, err := ns.dialer.Resolve(ctx, "udp", addr)
if err != nil {
return nil, err
}
func (ns *Impl) DialContextUDP(ctx context.Context, ipp netaddr.IPPort) (*gonet.UDPConn, error) {
remoteAddress := &tcpip.FullAddress{
NIC: nicID,
Addr: tcpip.Address(remoteIPPort.IP().IPAddr().IP),
Port: remoteIPPort.Port(),
Addr: tcpip.Address(ipp.IP().IPAddr().IP),
Port: ipp.Port(),
}
var ipType tcpip.NetworkProtocolNumber
if remoteIPPort.IP().Is4() {
if ipp.IP().Is4() {
ipType = ipv4.ProtocolNumber
} else {
ipType = ipv6.ProtocolNumber