From bb91cfeae79aae1a41d0ab938f8131c4e8ea041c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 30 Nov 2021 10:30:44 -0800 Subject: [PATCH] net/socks5/tssocks, wgengine: permit SOCKS through subnet routers/exit nodes Fixes #1970 Change-Id: Ibef45e8796e1d9625716d72539c96d1dbf7b1f76 Signed-off-by: Brad Fitzpatrick --- net/socks5/tssocks/tssocks.go | 27 ++++++++++++++------ wgengine/pendopen.go | 11 +++------ wgengine/userspace.go | 46 +++++++++-------------------------- wgengine/watchdog.go | 5 ++++ wgengine/wgengine.go | 24 ++++++++++++++++-- 5 files changed, 62 insertions(+), 51 deletions(-) diff --git a/net/socks5/tssocks/tssocks.go b/net/socks5/tssocks/tssocks.go index 589364fa7..c8d021ebe 100644 --- a/net/socks5/tssocks/tssocks.go +++ b/net/socks5/tssocks/tssocks.go @@ -12,7 +12,6 @@ import ( "inet.af/netaddr" "tailscale.com/net/socks5" - "tailscale.com/net/tsaddr" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/wgengine" @@ -27,7 +26,7 @@ import ( // // If ns is non-nil, it is used for dialing when needed. func NewServer(logf logger.Logf, e wgengine.Engine, ns *netstack.Impl) *socks5.Server { - d := &dialer{ns: ns} + d := &dialer{ns: ns, eng: e} e.AddNetworkMapCallback(d.onNewNetmap) return &socks5.Server{ Logf: logf, @@ -37,7 +36,8 @@ func NewServer(logf logger.Logf, e wgengine.Engine, ns *netstack.Impl) *socks5.S // dialer is the Tailscale SOCKS5 dialer. type dialer struct { - ns *netstack.Impl + ns *netstack.Impl + eng wgengine.Engine mu sync.Mutex dns netstack.DNSMap @@ -69,11 +69,22 @@ func (d *dialer) DialContext(ctx context.Context, network, addr string) (net.Con } func (d *dialer) useNetstackForIP(ip netaddr.IP) bool { - if d.ns == nil { + if d.ns == nil || !d.ns.ProcessLocalIPs { + // If netstack isn't used at all (nil), then obviously don't use it. + // + // But the ProcessLocalIPs check is more subtle: it really means + // whether we should use netstack for incoming traffic to ourselves. + // It's only ever true if we're running in full netstack mode (no TUN), + // so we can also use it as a proxy here for whether TUN is available. + // If it's false, there's tun and OS routes to things we need, + // so we don't want to dial with netstack. return false } - // TODO(bradfitz): this isn't exactly right. - // We should also support subnets when the - // prefs are configured as such. - return tsaddr.IsTailscaleIP(ip) + // Otherwise, we're in netstack mode, so dial via netstack if there's + // any peer handling that IP (including exit nodes). + // + // Otherwise assume it's something else (e.g. dialing + // google.com:443 via SOCKS) that the caller can dial directly. + _, ok := d.eng.PeerForIP(ip) + return ok } diff --git a/wgengine/pendopen.go b/wgengine/pendopen.go index 574c73435..ae25d449d 100644 --- a/wgengine/pendopen.go +++ b/wgengine/pendopen.go @@ -117,7 +117,7 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wra // like: // open-conn-track: timeout opening (100.115.73.60:52501 => 17.125.252.5:443); no associated peer node if runtime.GOOS == "ios" && flow.Dst.Port() == 443 && !tsaddr.IsTailscaleIP(flow.Dst.IP()) { - if _, _, err := e.peerForIP(flow.Dst.IP()); err != nil { + if _, ok := e.PeerForIP(flow.Dst.IP()); !ok { return } } @@ -157,15 +157,12 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { } // Diagnose why it might've timed out. - n, _, err := e.peerForIP(flow.Dst.IP()) - if err != nil { - e.logf("open-conn-track: timeout opening %v; peerForIP: %v", flow, err) - return - } - if n == nil { + pip, ok := e.PeerForIP(flow.Dst.IP()) + if !ok { e.logf("open-conn-track: timeout opening %v; no associated peer node", flow) return } + n := pip.Node if n.DiscoKey.IsZero() { e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key.ShortString()) return diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 2071b3757..ee1994453 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1271,25 +1271,20 @@ func (e *userspaceEngine) UpdateStatus(sb *ipnstate.StatusBuilder) { func (e *userspaceEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) { res := &ipnstate.PingResult{IP: ip.String()} - peer, self, err := e.peerForIP(ip) - if err != nil { - e.logf("ping(%v): %v", ip, err) - res.Err = err.Error() - cb(res) - return - } - if peer == nil { + pip, ok := e.PeerForIP(ip) + if !ok { e.logf("ping(%v): no matching peer", ip) res.Err = "no matching peer" cb(res) return } - if self { + if pip.IsSelf { res.Err = fmt.Sprintf("%v is local Tailscale IP", ip) res.IsLocalIP = true cb(res) return } + peer := pip.Node pingType := "disco" if useTSMP { @@ -1424,46 +1419,35 @@ func (e *userspaceEngine) WhoIsIPPort(ipport netaddr.IPPort) (tsIP netaddr.IP, o return tsIP, false } -// peerForIP returns the Node in the wireguard config +// PeerForIP returns the Node in the wireguard config // that's responsible for handling the given IP address. // // If none is found in the wireguard config but one is found in // the netmap, it's described in an error. // -// If none is found in either place, (nil, nil) is returned. // // peerForIP acquires both e.mu and e.wgLock, but neither at the same // time. -func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, isSelf bool, err error) { +func (e *userspaceEngine) PeerForIP(ip netaddr.IP) (ret PeerForIP, ok bool) { e.mu.Lock() nm := e.netMap e.mu.Unlock() if nm == nil { - return nil, false, errors.New("no network map") + return ret, false } // Check for exact matches before looking for subnet matches. - var bestInNMPrefix netaddr.IPPrefix - var bestInNM *tailcfg.Node + // TODO(bradfitz): add maps for these. on NetworkMap? for _, p := range nm.Peers { for _, a := range p.Addresses { if a.IP() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { - return p, false, nil - } - } - for _, cidr := range p.AllowedIPs { - if !cidr.Contains(ip) { - continue - } - if bestInNMPrefix.IsZero() || cidr.Bits() > bestInNMPrefix.Bits() { - bestInNMPrefix = cidr - bestInNM = p + return PeerForIP{Node: p, Route: a}, true } } } for _, a := range nm.Addresses { if a.IP() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { - return nm.SelfNode, true, nil + return PeerForIP{Node: nm.SelfNode, IsSelf: true, Route: a}, true } } @@ -1489,17 +1473,11 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, isSelf bool if !bestKey.IsZero() { for _, p := range nm.Peers { if p.Key == bestKey { - return p, false, nil + return PeerForIP{Node: p, Route: best}, true } } } - if bestInNM == nil { - return nil, false, nil - } - if bestInNMPrefix.Bits() == 0 { - return nil, false, errors.New("exit node found but not enabled") - } - return nil, false, fmt.Errorf("node %q found, but not using its %v route", bestInNM.ComputedNameWithHost, bestInNMPrefix) + return ret, false } type closeOnErrorPool []func() diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index dec2858de..baa84ca95 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -146,6 +146,11 @@ func (e *watchdogEngine) GetResolver() (r *resolver.Resolver, ok bool) { } return nil, false } +func (e *watchdogEngine) PeerForIP(ip netaddr.IP) (ret PeerForIP, ok bool) { + e.watchdog("PeerForIP", func() { ret, ok = e.wrap.PeerForIP(ip) }) + return ret, ok +} + func (e *watchdogEngine) Wait() { e.wrap.Wait() } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 44e51e639..bdfa21a3f 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -49,6 +49,20 @@ type someHandle struct{ _ byte } // ErrNoChanges is returned by Engine.Reconfig if no changes were made. var ErrNoChanges = errors.New("no changes made to Engine config") +// PeerForIP is the type returned by Engine.PeerForIP. +type PeerForIP struct { + // Node is the matched node. It's always non-nil when + // Engine.PeerForIP returns ok==true. + Node *tailcfg.Node + + // IsSelf is whether the Node is the local process. + IsSelf bool + + // Route is the route that matched the IP provided + // to Engine.PeerForIP. + Route netaddr.IPPrefix +} + // Engine is the Tailscale WireGuard engine interface. type Engine interface { // Reconfig reconfigures WireGuard and makes sure it's running. @@ -62,6 +76,10 @@ type Engine interface { // The returned error is ErrNoChanges if no changes were made. Reconfig(*wgcfg.Config, *router.Config, *dns.Config, *tailcfg.Debug) error + // PeerForIP returns the node to which the provided IP routes, + // if any. If none is found, (nil, nil) is returned. + PeerForIP(netaddr.IP) (_ PeerForIP, ok bool) + // GetFilter returns the current packet filter, if any. GetFilter() *filter.Filter @@ -141,10 +159,12 @@ type Engine interface { // RegisterIPPortIdentity registers a given node (identified by its // Tailscale IP) as temporarily having the given IP:port for whois lookups. // The IP:port is generally a localhost IP and an ephemeral port, used - // while proxying connections to localhost. + // while proxying connections to localhost when tailscaled is running + // in netstack mode. RegisterIPPortIdentity(netaddr.IPPort, netaddr.IP) - // UnregisterIPPortIdentity removes a temporary IP:port registration. + // UnregisterIPPortIdentity removes a temporary IP:port registration + // made previously by RegisterIPPortIdentity. UnregisterIPPortIdentity(netaddr.IPPort) // WhoIsIPPort looks up an IP:port in the temporary registrations,