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 <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-12-14 14:34:15 -05:00 committed by Sonia Appasamy
parent 685b853763
commit c6a274611e
1 changed files with 56 additions and 22 deletions

View File

@ -314,24 +314,63 @@ func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (han
return true return true
} }
var ipv4 string // store the first IPv4 address we see for redirect later ipv4, ipv6 := s.selfNodeAddresses(r, st)
for _, ip := range st.Self.TailscaleIPs { if r.Host == fmt.Sprintf("%s:%d", ipv4.String(), ListenPort) {
if ip.Is4() { return false // already accessing over Tailscale IP
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
}
} }
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 := *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) http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently)
return true 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 // authorizeRequest reports whether the request from the web client
// is authorized to be completed. // is authorized to be completed.
// It reports true if the request is authorized, and false otherwise. // 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), 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 == "" { if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn && data.URLPrefix == "" {
// X-Ingress-Path is the path prefix in use for Home Assistant // X-Ingress-Path is the path prefix in use for Home Assistant
// https://developers.home-assistant.io/docs/add-ons/presentation#ingress // 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 { } else {
data.ClientVersion = cv 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 { if st.CurrentTailnet != nil {
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
data.DomainName = st.CurrentTailnet.Name data.DomainName = st.CurrentTailnet.Name