From 913df7e6ea591b059b5681d130e68049559342c0 Mon Sep 17 00:00:00 2001 From: Adriano Sela Aviles Date: Fri, 5 Jun 2026 19:24:17 -0700 Subject: [PATCH] cmd/tailscale/cli: unit tests for tailscale ip Updates #20035 Signed-off-by: Adriano Sela Aviles --- cmd/tailscale/cli/ip.go | 11 +- cmd/tailscale/cli/ip_test.go | 199 +++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 cmd/tailscale/cli/ip_test.go diff --git a/cmd/tailscale/cli/ip.go b/cmd/tailscale/cli/ip.go index 4b24727dd..6ff1cf867 100644 --- a/cmd/tailscale/cli/ip.go +++ b/cmd/tailscale/cli/ip.go @@ -13,6 +13,7 @@ import ( "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" ) var ipCmd = &ffcli.Command{ @@ -130,12 +131,18 @@ func serviceAddrsMatchingIP(ctx context.Context, ipStr string) ([]netip.Addr, er if err != nil { return nil, err } + return allIPsForServiceWithIP(services, ip), nil +} + +// allIPsForServiceWithIP returns the Addrs of the service whose VIP addresses +// contain ip, or nil if no service matches. +func allIPsForServiceWithIP(services map[tailcfg.ServiceName]tailcfg.ServiceDetails, ip netip.Addr) []netip.Addr { for _, svc := range services { if slices.Contains(svc.Addrs, ip) { - return svc.Addrs, nil + return svc.Addrs } } - return nil, nil + return nil } func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) { diff --git a/cmd/tailscale/cli/ip_test.go b/cmd/tailscale/cli/ip_test.go new file mode 100644 index 000000000..9e5d31166 --- /dev/null +++ b/cmd/tailscale/cli/ip_test.go @@ -0,0 +1,199 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package cli + +import ( + "net/netip" + "testing" + + "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +func TestPeerMatchingIP(t *testing.T) { + st := &ipnstate.Status{ + Self: &ipnstate.PeerStatus{ + TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}, + }, + Peer: map[key.NodePublic]*ipnstate.PeerStatus{ + key.NewNode().Public(): { + TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("fd7a:115c:a1e0::2")}, + }, + key.NewNode().Public(): { + TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, + }, + }, + } + + tests := []struct { + name string + ipStr string + wantOK bool + wantIPs []netip.Addr + }{ + { + name: "match_self_v4", + ipStr: "100.64.0.1", + wantOK: true, + wantIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}, + }, + { + name: "match_self_v6", + ipStr: "fd7a:115c:a1e0::1", + wantOK: true, + wantIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}, + }, + { + name: "match_peer_v4", + ipStr: "100.64.0.2", + wantOK: true, + wantIPs: []netip.Addr{netip.MustParseAddr("100.64.0.2"), netip.MustParseAddr("fd7a:115c:a1e0::2")}, + }, + { + name: "match_peer_single_ip", + ipStr: "100.64.0.3", + wantOK: true, + wantIPs: []netip.Addr{netip.MustParseAddr("100.64.0.3")}, + }, + { + name: "no_match", + ipStr: "100.64.0.99", + wantOK: false, + }, + { + name: "invalid_ip", + ipStr: "not-an-ip", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ps, ok := peerMatchingIP(st, tt.ipStr) + if ok != tt.wantOK { + t.Fatalf("peerMatchingIP(%q) ok = %v, want %v", tt.ipStr, ok, tt.wantOK) + } + if ok { + if len(ps.TailscaleIPs) != len(tt.wantIPs) { + t.Fatalf("got %d IPs, want %d", len(ps.TailscaleIPs), len(tt.wantIPs)) + } + for i, ip := range ps.TailscaleIPs { + if ip != tt.wantIPs[i] { + t.Errorf("IP[%d] = %v, want %v", i, ip, tt.wantIPs[i]) + } + } + } + }) + } +} + +func TestAllIPsForServiceWithIP(t *testing.T) { + services := map[tailcfg.ServiceName]tailcfg.ServiceDetails{ + "svc:web": { + Name: "svc:web", + Addrs: []netip.Addr{ + netip.MustParseAddr("100.100.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::1"), + }, + }, + "svc:api": { + Name: "svc:api", + Addrs: []netip.Addr{netip.MustParseAddr("100.100.0.2")}, + }, + } + // Services should have at most 2 addrs (one v4, one v6), but + // we handle more gracefully if the server ever returns them. + multiAddr := map[tailcfg.ServiceName]tailcfg.ServiceDetails{ + "svc:multi": { + Name: "svc:multi", + Addrs: []netip.Addr{ + netip.MustParseAddr("100.100.0.3"), + netip.MustParseAddr("100.100.0.4"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::3"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::4"), + }, + }, + } + + tests := []struct { + name string + services map[tailcfg.ServiceName]tailcfg.ServiceDetails + ip netip.Addr + wantIPs []netip.Addr + }{ + { + name: "match_service_v4", + services: services, + ip: netip.MustParseAddr("100.100.0.1"), + wantIPs: []netip.Addr{ + netip.MustParseAddr("100.100.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::1"), + }, + }, + { + name: "match_service_v6", + services: services, + ip: netip.MustParseAddr("fd7a:115c:a1e0:ab12::1"), + wantIPs: []netip.Addr{ + netip.MustParseAddr("100.100.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::1"), + }, + }, + { + name: "match_single_addr_service", + services: services, + ip: netip.MustParseAddr("100.100.0.2"), + wantIPs: []netip.Addr{netip.MustParseAddr("100.100.0.2")}, + }, + { + name: "match_service_multiple_addrs_v4", + services: multiAddr, + ip: netip.MustParseAddr("100.100.0.3"), + wantIPs: []netip.Addr{ + netip.MustParseAddr("100.100.0.3"), + netip.MustParseAddr("100.100.0.4"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::3"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::4"), + }, + }, + { + name: "match_service_multiple_addrs_v6", + services: multiAddr, + ip: netip.MustParseAddr("fd7a:115c:a1e0:ab12::3"), + wantIPs: []netip.Addr{ + netip.MustParseAddr("100.100.0.3"), + netip.MustParseAddr("100.100.0.4"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::3"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12::4"), + }, + }, + { + name: "no_match", + services: services, + ip: netip.MustParseAddr("100.100.0.99"), + wantIPs: nil, + }, + { + name: "empty_services", + services: nil, + ip: netip.MustParseAddr("100.100.0.1"), + wantIPs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := allIPsForServiceWithIP(tt.services, tt.ip) + if len(got) != len(tt.wantIPs) { + t.Fatalf("allIPsForServiceWithIP(%v) returned %d IPs, want %d", tt.ip, len(got), len(tt.wantIPs)) + } + for i, ip := range got { + if ip != tt.wantIPs[i] { + t.Errorf("IP[%d] = %v, want %v", i, ip, tt.wantIPs[i]) + } + } + }) + } +}