net/socks5/tssocks, wgengine: permit SOCKS through subnet routers/exit nodes

Fixes #1970

Change-Id: Ibef45e8796e1d9625716d72539c96d1dbf7b1f76
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-11-30 10:30:44 -08:00 committed by Brad Fitzpatrick
parent 3181bbb8e4
commit bb91cfeae7
5 changed files with 62 additions and 51 deletions

View File

@ -12,7 +12,6 @@
"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 @@
//
// 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,
@ -38,6 +37,7 @@ 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
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
}

View File

@ -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

View File

@ -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()

View File

@ -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()
}

View File

@ -49,6 +49,20 @@ type someHandle struct{
// 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,