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:
committed by
Adriano Sela Aviles
parent
ad8ead9c94
commit
72578de033
@@ -327,6 +327,35 @@ func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsR
|
|||||||
return decodeJSON[*apitype.WhoIsResponse](body)
|
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WhoIsForService is like [Client.WhoIs] but scopes the returned CapMap to
|
||||||
|
// capabilities that apply to the named VIP service. This enables per-service
|
||||||
|
// capability resolution on hosts that advertise multiple VIP services.
|
||||||
|
func (lc *Client) WhoIsForService(ctx context.Context, remoteAddr string, svcName tailcfg.ServiceName) (*apitype.WhoIsResponse, error) {
|
||||||
|
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)+"&svc_name="+url.QueryEscape(string(svcName)))
|
||||||
|
if err != nil {
|
||||||
|
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
|
||||||
|
return nil, ErrPeerNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhoIsForIP is like [Client.WhoIs] but scopes the returned CapMap to
|
||||||
|
// capabilities that apply to the given destination IP. The IP may be a
|
||||||
|
// VIP service address, the node's own tailnet address, or any other
|
||||||
|
// routable IP the node handles.
|
||||||
|
func (lc *Client) WhoIsForIP(ctx context.Context, remoteAddr string, dst netip.Addr) (*apitype.WhoIsResponse, error) {
|
||||||
|
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)+"&dst_ip="+url.QueryEscape(dst.String()))
|
||||||
|
if err != nil {
|
||||||
|
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
|
||||||
|
return nil, ErrPeerNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrPeerNotFound is returned by [Client.WhoIs], [Client.WhoIsNodeKey] and
|
// ErrPeerNotFound is returned by [Client.WhoIs], [Client.WhoIsNodeKey] and
|
||||||
// [Client.WhoIsProto] when a peer is not found.
|
// [Client.WhoIsProto] when a peer is not found.
|
||||||
var ErrPeerNotFound = errors.New("peer not found")
|
var ErrPeerNotFound = errors.New("peer not found")
|
||||||
|
|||||||
@@ -1621,6 +1621,18 @@ func (b *LocalBackend) PeerCaps(src netip.Addr) tailcfg.PeerCapMap {
|
|||||||
return b.currentNode().PeerCaps(src)
|
return b.currentNode().PeerCaps(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerCapsForIP returns the capabilities that remote src IP has when
|
||||||
|
// talking to the given destination IP on this node.
|
||||||
|
func (b *LocalBackend) PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap {
|
||||||
|
return b.currentNode().PeerCapsForIP(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerCapsForService returns the capabilities that remote src IP has when
|
||||||
|
// talking to the named VIP service on this node.
|
||||||
|
func (b *LocalBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap {
|
||||||
|
return b.currentNode().PeerCapsForService(src, svcName)
|
||||||
|
}
|
||||||
|
|
||||||
// PeerByID returns the current full [tailcfg.Node] for the peer with the
|
// PeerByID returns the current full [tailcfg.Node] for the peer with the
|
||||||
// given NodeID, in O(1) time. It returns ok=false if no such peer is in
|
// given NodeID, in O(1) time. It returns ok=false if no such peer is in
|
||||||
// the current netmap.
|
// the current netmap.
|
||||||
|
|||||||
@@ -331,6 +331,46 @@ func (nb *nodeBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
|
|||||||
return nil
|
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,
|
// PeerHasCap reports whether the peer contains the given capability string,
|
||||||
// with any value(s).
|
// with any value(s).
|
||||||
func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool {
|
func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool {
|
||||||
|
|||||||
@@ -545,6 +545,8 @@ type localBackendWhoIsMethods interface {
|
|||||||
WhoIs(string, netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
WhoIs(string, netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||||
WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||||
PeerCaps(netip.Addr) tailcfg.PeerCapMap
|
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) {
|
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
|
UserProfile: &u, // always non-nil per WhoIsResponse contract
|
||||||
}
|
}
|
||||||
if n.Addresses().Len() > 0 {
|
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")
|
j, err := json.MarshalIndent(res, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -116,9 +116,11 @@ func TestSetPushDeviceToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type whoIsBackend struct {
|
type whoIsBackend struct {
|
||||||
whoIs func(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
whoIs func(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||||
whoIsNodeKey func(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
whoIsNodeKey func(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||||
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
||||||
|
peerCapsForIP func(src, dst netip.Addr) tailcfg.PeerCapMap
|
||||||
|
peerCapsForSvcName func(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b whoIsBackend) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
func (b whoIsBackend) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||||
@@ -133,6 +135,20 @@ func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
|
|||||||
return b.peerCaps[ip]
|
return b.peerCaps[ip]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b whoIsBackend) PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap {
|
||||||
|
if b.peerCapsForIP != nil {
|
||||||
|
return b.peerCapsForIP(src, dst)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b whoIsBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap {
|
||||||
|
if b.peerCapsForSvcName != nil {
|
||||||
|
return b.peerCapsForSvcName(src, svcName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that the WhoIs handler accepts IPs, IP:ports, or nodekeys.
|
// Tests that the WhoIs handler accepts IPs, IP:ports, or nodekeys.
|
||||||
//
|
//
|
||||||
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
|
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
|
||||||
@@ -202,6 +218,183 @@ func TestWhoIsArgTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWhoIsServiceParams(t *testing.T) {
|
||||||
|
h := handlerForTest(t, &Handler{
|
||||||
|
PermitRead: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
peerAddr := netip.MustParseAddr("100.101.102.103")
|
||||||
|
vipA := netip.MustParseAddr("100.100.0.1")
|
||||||
|
vipB := netip.MustParseAddr("100.100.0.2")
|
||||||
|
|
||||||
|
nodeCapsForAddr := tailcfg.PeerCapMap{"host-cap": {`"host-val"`}}
|
||||||
|
vipACaps := tailcfg.PeerCapMap{"svc-a-cap": {`"a-val"`}}
|
||||||
|
vipBCaps := tailcfg.PeerCapMap{"svc-b-cap": {`"b-val"`}}
|
||||||
|
|
||||||
|
match := func() (tailcfg.NodeView, tailcfg.UserProfile, bool) {
|
||||||
|
return (&tailcfg.Node{
|
||||||
|
ID: 123,
|
||||||
|
Addresses: []netip.Prefix{netip.PrefixFrom(peerAddr, 32)},
|
||||||
|
}).View(), tailcfg.UserProfile{ID: 456}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := whoIsBackend{
|
||||||
|
whoIs: func(proto string, ipp netip.AddrPort) (tailcfg.NodeView, tailcfg.UserProfile, bool) {
|
||||||
|
return match()
|
||||||
|
},
|
||||||
|
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
|
||||||
|
peerAddr: nodeCapsForAddr,
|
||||||
|
},
|
||||||
|
peerCapsForIP: func(src, dst netip.Addr) tailcfg.PeerCapMap {
|
||||||
|
switch dst {
|
||||||
|
case vipA:
|
||||||
|
return vipACaps
|
||||||
|
case vipB:
|
||||||
|
return vipBCaps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
peerCapsForSvcName: func(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap {
|
||||||
|
switch svcName {
|
||||||
|
case "svc:db":
|
||||||
|
return vipACaps
|
||||||
|
case "svc:cache":
|
||||||
|
return vipBCaps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
doWhoIs := func(t *testing.T, query string) apitype.WhoIsResponse {
|
||||||
|
t.Helper()
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?"+query, nil), backend)
|
||||||
|
if rec.Code != 200 {
|
||||||
|
t.Fatalf("response code %d; body: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var res apitype.WhoIsResponse
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
|
||||||
|
t.Fatalf("parsing response: %v", err)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
doWhoIsStatus := func(t *testing.T, query string) int {
|
||||||
|
t.Helper()
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?"+query, nil), backend)
|
||||||
|
return rec.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
// No service params — uses PeerCaps (host-level).
|
||||||
|
t.Run("no_service_params_uses_PeerCaps", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String())
|
||||||
|
if _, ok := res.CapMap["host-cap"]; !ok {
|
||||||
|
t.Errorf("expected host-cap from PeerCaps; got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := res.CapMap["svc-a-cap"]; ok {
|
||||||
|
t.Error("VIP cap should not appear without service param")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// dst_ip tests — PeerCapsForIP path.
|
||||||
|
t.Run("dst_ip_uses_PeerCapsForIP", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String()+"&dst_ip="+vipA.String())
|
||||||
|
if _, ok := res.CapMap["svc-a-cap"]; !ok {
|
||||||
|
t.Errorf("expected svc-a-cap; got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := res.CapMap["host-cap"]; ok {
|
||||||
|
t.Error("host-cap should not appear when dst_ip is specified")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dst_ip_scopes_to_specific_service", func(t *testing.T) {
|
||||||
|
resA := doWhoIs(t, "addr="+peerAddr.String()+"&dst_ip="+vipA.String())
|
||||||
|
resB := doWhoIs(t, "addr="+peerAddr.String()+"&dst_ip="+vipB.String())
|
||||||
|
|
||||||
|
if _, ok := resA.CapMap["svc-a-cap"]; !ok {
|
||||||
|
t.Errorf("dst_ip=vipA: expected svc-a-cap; got %v", resA.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := resA.CapMap["svc-b-cap"]; ok {
|
||||||
|
t.Error("dst_ip=vipA: svc-b-cap should not appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resB.CapMap["svc-b-cap"]; !ok {
|
||||||
|
t.Errorf("dst_ip=vipB: expected svc-b-cap; got %v", resB.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := resB.CapMap["svc-a-cap"]; ok {
|
||||||
|
t.Error("dst_ip=vipB: svc-a-cap should not appear")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dst_ip_unrelated_ip_returns_empty", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String()+"&dst_ip=10.0.0.99")
|
||||||
|
if len(res.CapMap) != 0 {
|
||||||
|
t.Errorf("expected empty CapMap for unrelated dst_ip; got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dst_ip_invalid_returns_400", func(t *testing.T) {
|
||||||
|
if code := doWhoIsStatus(t, "addr="+peerAddr.String()+"&dst_ip=not-an-ip"); code != 400 {
|
||||||
|
t.Errorf("expected 400 for invalid dst_ip; got %d", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// svc_name tests — PeerCapsForService path.
|
||||||
|
t.Run("svc_name_uses_PeerCapsForService", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String()+"&svc_name=svc:db")
|
||||||
|
if _, ok := res.CapMap["svc-a-cap"]; !ok {
|
||||||
|
t.Errorf("expected svc-a-cap; got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := res.CapMap["host-cap"]; ok {
|
||||||
|
t.Error("host-cap should not appear when svc_name is specified")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("svc_name_scopes_to_specific_service", func(t *testing.T) {
|
||||||
|
resA := doWhoIs(t, "addr="+peerAddr.String()+"&svc_name=svc:db")
|
||||||
|
resB := doWhoIs(t, "addr="+peerAddr.String()+"&svc_name=svc:cache")
|
||||||
|
|
||||||
|
if _, ok := resA.CapMap["svc-a-cap"]; !ok {
|
||||||
|
t.Errorf("svc_name=svc:db: expected svc-a-cap; got %v", resA.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := resA.CapMap["svc-b-cap"]; ok {
|
||||||
|
t.Error("svc_name=svc:db: svc-b-cap should not appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := resB.CapMap["svc-b-cap"]; !ok {
|
||||||
|
t.Errorf("svc_name=svc:cache: expected svc-b-cap; got %v", resB.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := resB.CapMap["svc-a-cap"]; ok {
|
||||||
|
t.Error("svc_name=svc:cache: svc-a-cap should not appear")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("svc_name_unknown_service_returns_empty", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String()+"&svc_name=svc:unknown")
|
||||||
|
if len(res.CapMap) != 0 {
|
||||||
|
t.Errorf("expected empty CapMap for unknown service; got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("svc_name_invalid_returns_400", func(t *testing.T) {
|
||||||
|
if code := doWhoIsStatus(t, "addr="+peerAddr.String()+"&svc_name=not-a-service-name"); code != 400 {
|
||||||
|
t.Errorf("expected 400 for invalid svc_name; got %d", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// svc_name takes priority over dst_ip when both are specified.
|
||||||
|
t.Run("svc_name_takes_priority_over_dst_ip", func(t *testing.T) {
|
||||||
|
res := doWhoIs(t, "addr="+peerAddr.String()+"&svc_name=svc:cache&dst_ip="+vipA.String())
|
||||||
|
if _, ok := res.CapMap["svc-b-cap"]; !ok {
|
||||||
|
t.Errorf("svc_name should take priority; expected svc-b-cap (cache); got %v", res.CapMap)
|
||||||
|
}
|
||||||
|
if _, ok := res.CapMap["svc-a-cap"]; ok {
|
||||||
|
t.Error("dst_ip result should not appear when svc_name is also specified")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type fakePeerByIDBackend map[tailcfg.NodeID]*tailcfg.Node
|
type fakePeerByIDBackend map[tailcfg.NodeID]*tailcfg.Node
|
||||||
|
|
||||||
func (f fakePeerByIDBackend) PeerByID(id tailcfg.NodeID) (tailcfg.NodeView, bool) {
|
func (f fakePeerByIDBackend) PeerByID(id tailcfg.NodeID) (tailcfg.NodeView, bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user