From 770aa71ffbfbb01f7c8bfc65ce5e0505c783efde Mon Sep 17 00:00:00 2001 From: Naman Sood Date: Mon, 15 Mar 2021 17:59:35 -0400 Subject: [PATCH] client, cmd/hello, ipn, wgengine: fix whois for netstack-forwarded connections Updates #504 Updates #707 Signed-off-by: Naman Sood --- client/tailscale/tailscale.go | 12 +----------- cmd/hello/hello.go | 22 +++++++++++++++++++--- ipn/ipnlocal/local.go | 19 +++++++++++++++---- ipn/localapi/localapi.go | 14 +++++++------- wgengine/netstack/netstack.go | 20 +++++++++++++++++--- wgengine/userspace.go | 26 ++++++++++++++++++++++++++ wgengine/watchdog.go | 10 ++++++++++ wgengine/wgengine.go | 13 +++++++++++++ 8 files changed, 108 insertions(+), 28 deletions(-) diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index 9ebbf8fcb..a6d304332 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -56,17 +56,7 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) { // WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port. func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) { - var ip string - if net.ParseIP(remoteAddr) != nil { - ip = remoteAddr - } else { - var err error - ip, _, err = net.SplitHostPort(remoteAddr) - if err != nil { - return nil, fmt.Errorf("invalid remoteAddr %q", remoteAddr) - } - } - req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil) + req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr), nil) if err != nil { return nil, err } diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index fc7343150..1c14fb71b 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -13,12 +13,12 @@ "html/template" "io/ioutil" "log" - "net" "net/http" "os" "strings" "tailscale.com/client/tailscale" + "tailscale.com/tailcfg" ) var ( @@ -107,6 +107,23 @@ type tmplData struct { IP string // "100.2.3.4" } +func tailscaleIP(who *tailcfg.WhoIsResponse) string { + if who == nil { + return "" + } + for _, nodeIP := range who.Node.Addresses { + if nodeIP.IP.Is4() && nodeIP.IsSingleIP() { + return nodeIP.IP.String() + } + } + for _, nodeIP := range who.Node.Addresses { + if nodeIP.IsSingleIP() { + return nodeIP.IP.String() + } + } + return "" +} + func root(w http.ResponseWriter, r *http.Request) { if r.TLS == nil && *httpsAddr != "" { host := r.Host @@ -146,14 +163,13 @@ func root(w http.ResponseWriter, r *http.Request) { return } } else { - ip, _, _ := net.SplitHostPort(r.RemoteAddr) data = tmplData{ DisplayName: who.UserProfile.DisplayName, LoginName: who.UserProfile.LoginName, ProfilePicURL: who.UserProfile.ProfilePicURL, MachineName: firstLabel(who.Node.ComputedName), MachineOS: who.Node.Hostinfo.OS, - IP: ip, + IP: tailscaleIP(who), } } w.Header().Set("Content-Type", "text/html; charset=utf-8") diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 89b2562a9..6036d5000 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -254,14 +254,25 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { } } -// WhoIs reports the node and user who owns the node with the given IP. +// WhoIs reports the node and user who owns the node with the given IP:port. +// If the IP address is a Tailscale IP, the provided port may be 0. // If ok == true, n and u are valid. -func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) { +func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) { b.mu.Lock() defer b.mu.Unlock() - n, ok = b.nodeByAddr[ip] + n, ok = b.nodeByAddr[ipp.IP] if !ok { - return nil, u, false + var ip netaddr.IP + if ipp.Port != 0 { + ip, ok = b.e.WhoIsIPPort(ipp) + } + if !ok { + return nil, u, false + } + n, ok = b.nodeByAddr[ip] + if !ok { + return nil, u, false + } } u, ok = b.netMap.UserProfiles[n.User] if !ok { diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 7162a65ee..4e0dba3da 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -67,21 +67,21 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { return } b := h.b - var ip netaddr.IP - if v := r.FormValue("ip"); v != "" { + var ipp netaddr.IPPort + if v := r.FormValue("addr"); v != "" { var err error - ip, err = netaddr.ParseIP(r.FormValue("ip")) + ipp, err = netaddr.ParseIPPort(v) if err != nil { - http.Error(w, "invalid 'ip' parameter", 400) + http.Error(w, "invalid 'addr' parameter", 400) return } } else { - http.Error(w, "missing 'ip' parameter", 400) + http.Error(w, "missing 'addr' parameter", 400) return } - n, u, ok := b.WhoIs(ip) + n, u, ok := b.WhoIs(ipp) if !ok { - http.Error(w, "no match for IP", 404) + http.Error(w, "no match for IP:port", 404) return } res := &tailcfg.WhoIsResponse{ diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 61b1cdb9b..1131490ec 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -367,6 +367,11 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, port uint16) return } defer server.Close() + backendLocalAddr := server.LocalAddr().(*net.TCPAddr) + backendLocalIPPort, _ := netaddr.FromStdAddr(backendLocalAddr.IP, backendLocalAddr.Port, backendLocalAddr.Zone) + clientRemoteIP, _ := netaddr.FromStdIP(client.RemoteAddr().(*net.TCPAddr).IP) + ns.e.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP) + defer ns.e.UnregisterIPPortIdentity(backendLocalIPPort) connClosed := make(chan error, 2) go func() { _, err := io.Copy(server, client) @@ -406,19 +411,28 @@ func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) { func (ns *Impl) forwardUDP(client *gonet.UDPConn, wq *waiter.Queue, clientLocalAddr, clientRemoteAddr tcpip.FullAddress) { port := clientLocalAddr.Port ns.logf("[v2] netstack: forwarding incoming UDP connection on port %v", port) - backendLocalAddr := &net.UDPAddr{Port: int(clientRemoteAddr.Port)} + backendListenAddr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(clientRemoteAddr.Port)} backendRemoteAddr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(port)} - backendConn, err := net.ListenUDP("udp4", backendLocalAddr) + backendConn, err := net.ListenUDP("udp4", backendListenAddr) if err != nil { ns.logf("netstack: could not bind local port %v: %v, trying again with random port", clientRemoteAddr.Port, err) - backendConn, err = net.ListenUDP("udp4", nil) + backendListenAddr.Port = 0 + backendConn, err = net.ListenUDP("udp4", backendListenAddr) if err != nil { ns.logf("netstack: could not connect to local UDP server on port %v: %v", port, err) return } } + backendLocalAddr := backendConn.LocalAddr().(*net.UDPAddr) + backendLocalIPPort, ok := netaddr.FromStdAddr(backendListenAddr.IP, backendLocalAddr.Port, backendLocalAddr.Zone) + if !ok { + ns.logf("could not get backend local IP:port from %v:%v", backendLocalAddr.IP, backendLocalAddr.Port) + } + clientRemoteIP, _ := netaddr.FromStdIP(net.ParseIP(clientRemoteAddr.Addr.String())) + ns.e.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP) ctx, cancel := context.WithCancel(context.Background()) timer := time.AfterFunc(2*time.Minute, func() { + ns.e.UnregisterIPPortIdentity(backendLocalIPPort) ns.logf("netstack: UDP session between %s and %s timed out", clientRemoteAddr, backendRemoteAddr) cancel() client.Close() diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 1baf7f040..fccca8c8b 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -125,6 +125,7 @@ type userspaceEngine struct { pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go networkMapCallbacks map[*someHandle]NetworkMapCallback + tsIPByIPPort map[netaddr.IPPort]netaddr.IP // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups // Lock ordering: magicsock.Conn.mu, wgLock, then mu. } @@ -1341,6 +1342,31 @@ func (e *userspaceEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { e.magicConn.Ping(ip, cb) } +func (e *userspaceEngine) RegisterIPPortIdentity(ipport netaddr.IPPort, tsIP netaddr.IP) { + e.mu.Lock() + defer e.mu.Unlock() + if e.tsIPByIPPort == nil { + e.tsIPByIPPort = make(map[netaddr.IPPort]netaddr.IP) + } + e.tsIPByIPPort[ipport] = tsIP +} + +func (e *userspaceEngine) UnregisterIPPortIdentity(ipport netaddr.IPPort) { + e.mu.Lock() + defer e.mu.Unlock() + if e.tsIPByIPPort == nil { + return + } + delete(e.tsIPByIPPort, ipport) +} + +func (e *userspaceEngine) WhoIsIPPort(ipport netaddr.IPPort) (tsIP netaddr.IP, ok bool) { + e.mu.Lock() + defer e.mu.Unlock() + tsIP, ok = e.tsIPByIPPort[ipport] + return tsIP, ok +} + // diagnoseTUNFailure is called if tun.CreateTUN fails, to poke around // the system and log some diagnostic info that might help debug why // TUN failed. Because TUN's already failed and things the program's diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index 3b96f2e8e..f4f7d3085 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -120,6 +120,16 @@ func (e *watchdogEngine) DiscoPublicKey() (k tailcfg.DiscoKey) { func (e *watchdogEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { e.watchdog("Ping", func() { e.wrap.Ping(ip, cb) }) } +func (e *watchdogEngine) RegisterIPPortIdentity(ipp netaddr.IPPort, tsIP netaddr.IP) { + e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) }) +} +func (e *watchdogEngine) UnregisterIPPortIdentity(ipp netaddr.IPPort) { + e.watchdog("UnregisterIPPortIdentity", func() { e.wrap.UnregisterIPPortIdentity(ipp) }) +} +func (e *watchdogEngine) WhoIsIPPort(ipp netaddr.IPPort) (tsIP netaddr.IP, ok bool) { + e.watchdog("UnregisterIPPortIdentity", func() { tsIP, ok = e.wrap.WhoIsIPPort(ipp) }) + return tsIP, ok +} func (e *watchdogEngine) Close() { e.watchdog("Close", e.wrap.Close) } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 57bc7cb77..c8e7963db 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -137,4 +137,17 @@ type Engine interface { // Ping is a request to start a discovery ping with the peer handling // the given IP and then call cb with its ping latency & method. Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) + + // 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. + RegisterIPPortIdentity(netaddr.IPPort, netaddr.IP) + + // UnregisterIPPortIdentity removes a temporary IP:port registration. + UnregisterIPPortIdentity(netaddr.IPPort) + + // 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) }