From c6a274611e01768f2d77201436f11a45dbc0c75f Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Thu, 14 Dec 2023 14:34:15 -0500 Subject: [PATCH] client/web: use Tailscale IP known by peer node Throughout the web UI, we present the tailscale addresses for the self node. In the case of the node being shared out with a user from another tailnet, the peer viewer may actually know the node by a different IP than the node knows itself as (Tailscale IPs can be configured as desired on a tailnet level). This change includes two fixes: 1. Present the self node's addresses in the frontend as the addresses the viewing node knows it as (i.e. the addresses the viewing node uses to access the web client). 2. We currently redirect the viewer to the Tailscale IPv4 address if viewing it by MagicDNS name, or any other name that maps to the Tailscale node. When doing this redirect, which is primarily added for DNS rebinding protection, we now check the address the peer knows this node as, and redirect to specifically that IP. Fixes tailscale/corp#16402 Signed-off-by: Sonia Appasamy --- client/web/web.go | 78 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/client/web/web.go b/client/web/web.go index a3310bbfe..93aeb68e8 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -314,24 +314,63 @@ func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (han return true } - var ipv4 string // store the first IPv4 address we see for redirect later - for _, ip := range st.Self.TailscaleIPs { - if ip.Is4() { - if r.Host == fmt.Sprintf("%s:%d", ip, ListenPort) { - return false - } - ipv4 = ip.String() - } - if ip.Is6() && r.Host == fmt.Sprintf("[%s]:%d", ip, ListenPort) { - return false - } + ipv4, ipv6 := s.selfNodeAddresses(r, st) + if r.Host == fmt.Sprintf("%s:%d", ipv4.String(), ListenPort) { + return false // already accessing over Tailscale IP } + if r.Host == fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort) { + return false // already accessing over Tailscale IP + } + + // Not currently accessing via Tailscale IP, + // redirect them. + + var preferV6 bool + if ap, err := netip.ParseAddrPort(r.Host); err == nil { + // If Host was already ipv6, keep them on same protocol. + preferV6 = ap.Addr().Is6() + } + newURL := *r.URL - newURL.Host = fmt.Sprintf("%s:%d", ipv4, ListenPort) + if (preferV6 && ipv6.IsValid()) || !ipv4.IsValid() { + newURL.Host = fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort) + } else { + newURL.Host = fmt.Sprintf("%s:%d", ipv4.String(), ListenPort) + } http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently) return true } +// selfNodeAddresses return the Tailscale IPv4 and IPv6 addresses for the self node. +// st is expected to be a status with peers included. +func (s *Server) selfNodeAddresses(r *http.Request, st *ipnstate.Status) (ipv4, ipv6 netip.Addr) { + for _, ip := range st.Self.TailscaleIPs { + if ip.Is4() { + ipv4 = ip + } else if ip.Is6() { + ipv6 = ip + } + if ipv4.IsValid() && ipv6.IsValid() { + break // found both IPs + } + } + if whois, err := s.lc.WhoIs(r.Context(), r.RemoteAddr); err == nil { + // The source peer connecting to this node may know it by a different + // IP than the node knows itself as. Specifically, this may be the case + // if the peer is coming from a different tailnet (sharee node), as IPs + // are specific to each tailnet. + // Here, we check if the source peer knows the node by a different IP, + // and return the peer's version if so. + if knownIPv4 := whois.Node.SelfNodeV4MasqAddrForThisPeer; knownIPv4 != nil { + ipv4 = *knownIPv4 + } + if knownIPv6 := whois.Node.SelfNodeV6MasqAddrForThisPeer; knownIPv6 != nil { + ipv6 = *knownIPv6 + } + } + return ipv4, ipv6 +} + // authorizeRequest reports whether the request from the web client // is authorized to be completed. // It reports true if the request is authorized, and false otherwise. @@ -674,6 +713,10 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { ACLAllowsAnyIncomingTraffic: s.aclsAllowAccess(filterRules), } + ipv4, ipv6 := s.selfNodeAddresses(r, st) + data.IPv4 = ipv4.String() + data.IPv6 = ipv6.String() + if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn && data.URLPrefix == "" { // X-Ingress-Path is the path prefix in use for Home Assistant // https://developers.home-assistant.io/docs/add-ons/presentation#ingress @@ -686,16 +729,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { } else { data.ClientVersion = cv } - for _, ip := range st.TailscaleIPs { - if ip.Is4() { - data.IPv4 = ip.String() - } else if ip.Is6() { - data.IPv6 = ip.String() - } - if data.IPv4 != "" && data.IPv6 != "" { - break - } - } + if st.CurrentTailnet != nil { data.TailnetName = st.CurrentTailnet.MagicDNSSuffix data.DomainName = st.CurrentTailnet.Name