ipn/{ipnlocal,localapi},client/local: add per-dst cap resolution for services

Adds two new cap resolution methods alongside the existing PeerCaps:

PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) resolves
the service name to its VIP addresses via the node's service IP mappings
and returns caps scoped to that service. Exposed on /v0/whois via the
svc_name query parameter and on client/local.Client as WhoIsForService.

PeerCapsForIP(src, dst netip.Addr) resolves caps against an arbitrary
destination IP. Exposed on /v0/whois via the svc_addr query parameter
and on client/local.Client as WhoIsForIP.

svc_name takes priority over svc_addr when both are present. Invalid
values for either return 400. The existing PeerCaps/WhoIs path is
unchanged: without a service parameter, WhoIs returns only host-level
caps.

Updates tailscale/corp#41632

Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
This commit is contained in:
Adriano Sela Aviles
2026-05-11 14:48:25 -07:00
committed by Adriano Sela Aviles
parent ad8ead9c94
commit 72578de033
5 changed files with 298 additions and 4 deletions
+21 -1
View File
@@ -545,6 +545,8 @@ type localBackendWhoIsMethods interface {
WhoIs(string, netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
PeerCaps(netip.Addr) tailcfg.PeerCapMap
PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap
PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap
}
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
@@ -592,7 +594,25 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
UserProfile: &u, // always non-nil per WhoIsResponse contract
}
if n.Addresses().Len() > 0 {
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
src := n.Addresses().At(0).Addr()
switch {
case r.FormValue("svc_name") != "":
svcName := tailcfg.AsServiceName(r.FormValue("svc_name"))
if svcName == "" {
http.Error(w, "invalid svc_name", http.StatusBadRequest)
return
}
res.CapMap = b.PeerCapsForService(src, svcName)
case r.FormValue("dst_ip") != "":
svcAddr, err := netip.ParseAddr(r.FormValue("dst_ip"))
if err != nil {
http.Error(w, "invalid dst_ip", http.StatusBadRequest)
return
}
res.CapMap = b.PeerCapsForIP(src, svcAddr)
default:
res.CapMap = b.PeerCaps(src)
}
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {