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
+40
View File
@@ -331,6 +331,46 @@ func (nb *nodeBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
return nil
}
// PeerCapsForIP returns the capabilities that remote src IP has when
// talking to the given destination IP on this node. The destination may
// be any IP the node handles: its own tailnet address, a VIP service
// address, or any future routable IP.
func (nb *nodeBackend) PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap {
nb.mu.Lock()
defer nb.mu.Unlock()
if nb.netMap == nil {
return nil
}
filt := nb.filterAtomic.Load()
if filt == nil {
return nil
}
return filt.CapsWithValues(src, dst)
}
// PeerCapsForService returns the capabilities that remote src IP has when
// talking to the named VIP service on this node. The service name is
// resolved to its VIP addresses via the node's service IP mappings, and
// the first address matching the src IP family is used for cap lookup.
func (nb *nodeBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap {
nb.mu.Lock()
defer nb.mu.Unlock()
if nb.netMap == nil {
return nil
}
filt := nb.filterAtomic.Load()
if filt == nil {
return nil
}
addrs := nb.netMap.GetVIPServiceIPMap()[svcName]
for _, ip := range addrs {
if ip.BitLen() == src.BitLen() {
return filt.CapsWithValues(src, ip)
}
}
return nil
}
// PeerHasCap reports whether the peer contains the given capability string,
// with any value(s).
func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool {