From 660a4608d2ccb4508af71d888454b333e5eb501f Mon Sep 17 00:00:00 2001 From: George Jones Date: Fri, 13 Mar 2026 13:26:08 -0400 Subject: [PATCH] feature/conn25: Update ConnectorTransitIPRequest handling (#18979) Changed the mapping to store the transit IPs to be indexed by peer IP rather than NodeID because the data path only has access to the peer's IP. This change means that IPv4 transit IPs need to be indexed by the peer's IPv4 address, and IPv6 transit IPs need to be indexed by the peer's IPv6 address. It is an error if the peer does not have an address of the same family as the transit IP. It is also an error if the transit and destination IP families do not match. Added a check to ensure that the TransitIPRequest.App matches a configured app on the connector. Added additional TransitIPResponse codes to identify the new errors and change the exsting use of the Other code to use it's own specific code. Added logging for the error cases, since they generally indicate that a peer has constructed a bad request or that there is a config mismatch between the peer and the local netmap. Added a test framework for handleConnectorTransitIPRequest and moved the existing tests into the framework and added new tests. Fixes tailscale/corp#37143 Signed-off-by: George Jones --- feature/conn25/conn25.go | 97 +++++-- feature/conn25/conn25_test.go | 489 +++++++++++++++++++++++----------- 2 files changed, 409 insertions(+), 177 deletions(-) diff --git a/feature/conn25/conn25.go b/feature/conn25/conn25.go index b5d0dc9df..5318d2bdd 100644 --- a/feature/conn25/conn25.go +++ b/feature/conn25/conn25.go @@ -26,6 +26,7 @@ import ( "tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnlocal" "tailscale.com/net/dns" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/appctype" "tailscale.com/types/logger" @@ -131,7 +132,7 @@ func (e *extension) handleConnectorTransitIP(h ipnlocal.PeerAPIHandler, w http.R http.Error(w, "Error decoding JSON", http.StatusBadRequest) return } - resp := e.conn25.handleConnectorTransitIPRequest(h.Peer().ID(), req) + resp := e.conn25.handleConnectorTransitIPRequest(h.Peer(), req) bs, err := json.Marshal(resp) if err != nil { http.Error(w, "Error encoding JSON", http.StatusInternalServerError) @@ -248,47 +249,94 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte { } const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest" +const noMatchingPeerIPFamilyMessage = "No peer IP found with matching IP family" +const addrFamilyMismatchMessage = "Transit and Destination addresses must have matching IP family" +const unknownAppNameMessage = "The App name in the request does not match a configured App" + +// handleConnectorTransitIPRequest creates a ConnectorTransitIPResponse in response +// to a ConnectorTransitIPRequest. It updates the connectors mapping of +// TransitIP->DestinationIP per peer (using the Peer's IP that matches the address +// family of the transitIP). If a peer has stored this mapping in the connector, +// Conn25 will route traffic to TransitIPs to DestinationIPs for that peer. +func (c *Conn25) handleConnectorTransitIPRequest(n tailcfg.NodeView, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse { + var peerIPv4, peerIPv6 netip.Addr + for _, ip := range n.Addresses().All() { + if !ip.IsSingleIP() || !tsaddr.IsTailscaleIP(ip.Addr()) { + continue + } + if ip.Addr().Is4() && !peerIPv4.IsValid() { + peerIPv4 = ip.Addr() + } else if ip.Addr().Is6() && !peerIPv6.IsValid() { + peerIPv6 = ip.Addr() + } + } -// handleConnectorTransitIPRequest creates a ConnectorTransitIPResponse in response to a ConnectorTransitIPRequest. -// It updates the connectors mapping of TransitIP->DestinationIP per peer (tailcfg.NodeID). -// If a peer has stored this mapping in the connector Conn25 will route traffic to TransitIPs to DestinationIPs for that peer. -func (c *Conn25) handleConnectorTransitIPRequest(nid tailcfg.NodeID, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse { resp := ConnectorTransitIPResponse{} seen := map[netip.Addr]bool{} for _, each := range ctipr.TransitIPs { if seen[each.TransitIP] { resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{ - Code: OtherFailure, + Code: DuplicateTransitIP, Message: dupeTransitIPMessage, }) + c.connector.logf("[Unexpected] peer attempt to map a transit IP reused a transitIP: node: %s, IP: %v", + n.StableID(), each.TransitIP) continue } - tipresp := c.connector.handleTransitIPRequest(nid, each) + tipresp := c.connector.handleTransitIPRequest(n, peerIPv4, peerIPv6, each) seen[each.TransitIP] = true resp.TransitIPs = append(resp.TransitIPs, tipresp) } return resp } -func (s *connector) handleTransitIPRequest(nid tailcfg.NodeID, tipr TransitIPRequest) TransitIPResponse { +func (s *connector) handleTransitIPRequest(n tailcfg.NodeView, peerV4 netip.Addr, peerV6 netip.Addr, tipr TransitIPRequest) TransitIPResponse { + if tipr.TransitIP.Is4() != tipr.DestinationIP.Is4() { + s.logf("[Unexpected] peer attempt to map a transit IP to dest IP did not have matching families: node: %s, tIPv4: %v dIPv4: %v", + n.StableID(), tipr.TransitIP.Is4(), tipr.DestinationIP.Is4()) + return TransitIPResponse{Code: AddrFamilyMismatch, Message: addrFamilyMismatchMessage} + } + + // Datapath lookups only have access to the peer IP, and that will match the family + // of the transit IP, so we need to store v4 and v6 mappings separately. + var peerAddr netip.Addr + if tipr.TransitIP.Is4() { + peerAddr = peerV4 + } else { + peerAddr = peerV6 + } + + // If we couldn't find a matching family, return an error. + if !peerAddr.IsValid() { + s.logf("[Unexpected] peer attempt to map a transit IP did not have a matching address family: node: %s, IPv4: %v", + n.StableID(), tipr.TransitIP.Is4()) + return TransitIPResponse{NoMatchingPeerIPFamily, noMatchingPeerIPFamilyMessage} + } + s.mu.Lock() defer s.mu.Unlock() + if _, ok := s.config.appsByName[tipr.App]; !ok { + s.logf("[Unexpected] peer attempt to map a transit IP referenced unknown app: node: %s, app: %q", + n.StableID(), tipr.App) + return TransitIPResponse{Code: UnknownAppName, Message: unknownAppNameMessage} + } + if s.transitIPs == nil { - s.transitIPs = make(map[tailcfg.NodeID]map[netip.Addr]appAddr) + s.transitIPs = make(map[netip.Addr]map[netip.Addr]appAddr) } - peerMap, ok := s.transitIPs[nid] + peerMap, ok := s.transitIPs[peerAddr] if !ok { peerMap = make(map[netip.Addr]appAddr) - s.transitIPs[nid] = peerMap + s.transitIPs[peerAddr] = peerMap } peerMap[tipr.TransitIP] = appAddr{addr: tipr.DestinationIP, app: tipr.App} return TransitIPResponse{} } -func (s *connector) transitIPTarget(nid tailcfg.NodeID, tip netip.Addr) netip.Addr { +func (s *connector) transitIPTarget(peerIP, tip netip.Addr) netip.Addr { s.mu.Lock() defer s.mu.Unlock() - return s.transitIPs[nid][tip].addr + return s.transitIPs[peerIP][tip].addr } // TransitIPRequest details a single TransitIP allocation request from a client to a @@ -322,8 +370,24 @@ const ( OK TransitIPResponseCode = 0 // OtherFailure indicates that the mapping failed for a reason that does not have - // another relevant [TransitIPResponsecode]. + // another relevant [TransitIPResponseCode]. OtherFailure TransitIPResponseCode = 1 + + // DuplicateTransitIP indicates that the same transit address appeared more than + // once in a [ConnectorTransitIPRequest]. + DuplicateTransitIP TransitIPResponseCode = 2 + + // NoMatchingPeerIPFamily indicates that the peer did not have an associated + // IP with the same family as transit IP being registered. + NoMatchingPeerIPFamily = 3 + + // AddrFamilyMismatch indicates that the transit IP and destination IP addresses + // do not belong to the same IP family. + AddrFamilyMismatch = 4 + + // UnknownAppName indicates that the connector is not configured to handle requests + // for the App name that was specified in the request. + UnknownAppName = 5 ) // TransitIPResponse is the response to a TransitIPRequest @@ -651,8 +715,9 @@ type connector struct { logf logger.Logf mu sync.Mutex // protects the fields below - // transitIPs is a map of connector client peer NodeID -> client transitIPs that we update as connector client peers instruct us to, and then use to route traffic to its destination on behalf of connector clients. - transitIPs map[tailcfg.NodeID]map[netip.Addr]appAddr + // transitIPs is a map of connector client peer IP -> client transitIPs that we update as connector client peers instruct us to, and then use to route traffic to its destination on behalf of connector clients. + // Note that each peer could potentially have two maps: one for its IPv4 address, and one for its IPv6 address. The transit IPs map for a given peer IP will contain transit IPs of the same family as the peer's IP. + transitIPs map[netip.Addr]map[netip.Addr]appAddr config config } diff --git a/feature/conn25/conn25_test.go b/feature/conn25/conn25_test.go index 97a22c500..574320af8 100644 --- a/feature/conn25/conn25_test.go +++ b/feature/conn25/conn25_test.go @@ -38,180 +38,347 @@ func mustIPSetFromPrefix(s string) *netipx.IPSet { return set } -// TestHandleConnectorTransitIPRequestZeroLength tests that if sent a -// ConnectorTransitIPRequest with 0 TransitIPRequests, we respond with a -// ConnectorTransitIPResponse with 0 TransitIPResponses. -func TestHandleConnectorTransitIPRequestZeroLength(t *testing.T) { - c := newConn25(logger.Discard) - req := ConnectorTransitIPRequest{} - nid := tailcfg.NodeID(1) +// TestHandleConnectorTransitIPRequest tests that if sent a +// request with a transit addr and a destination addr we store that mapping +// and can retrieve it. +func TestHandleConnectorTransitIPRequest(t *testing.T) { - resp := c.handleConnectorTransitIPRequest(nid, req) - if len(resp.TransitIPs) != 0 { - t.Fatalf("n TransitIPs in response: %d, want 0", len(resp.TransitIPs)) - } -} + const appName = "TestApp" -// TestHandleConnectorTransitIPRequestStoresAddr tests that if sent a -// request with a transit addr and a destination addr we store that mapping -// and can retrieve it. If sent another req with a different dst for that transit addr -// we store that instead. -func TestHandleConnectorTransitIPRequestStoresAddr(t *testing.T) { - c := newConn25(logger.Discard) - nid := tailcfg.NodeID(1) - tip := netip.MustParseAddr("0.0.0.1") - dip := netip.MustParseAddr("1.2.3.4") - dip2 := netip.MustParseAddr("1.2.3.5") - mr := func(t, d netip.Addr) ConnectorTransitIPRequest { - return ConnectorTransitIPRequest{ - TransitIPs: []TransitIPRequest{ - {TransitIP: t, DestinationIP: d}, - }, - } - } + // Peer IPs + pipV4_1 := netip.MustParseAddr("100.101.101.101") + pipV4_2 := netip.MustParseAddr("100.101.101.102") - resp := c.handleConnectorTransitIPRequest(nid, mr(tip, dip)) - if len(resp.TransitIPs) != 1 { - t.Fatalf("n TransitIPs in response: %d, want 1", len(resp.TransitIPs)) - } - got := resp.TransitIPs[0].Code - if got != TransitIPResponseCode(0) { - t.Fatalf("TransitIP Code: %d, want 0", got) - } - gotAddr := c.connector.transitIPTarget(nid, tip) - if gotAddr != dip { - t.Fatalf("Connector stored destination for tip: %v, want %v", gotAddr, dip) - } + pipV6_1 := netip.MustParseAddr("fd7a:115c:a1e0::101") + pipV6_3 := netip.MustParseAddr("fd7a:115c:a1e0::103") - // mapping can be overwritten - resp2 := c.handleConnectorTransitIPRequest(nid, mr(tip, dip2)) - if len(resp2.TransitIPs) != 1 { - t.Fatalf("n TransitIPs in response: %d, want 1", len(resp2.TransitIPs)) - } - got2 := resp.TransitIPs[0].Code - if got2 != TransitIPResponseCode(0) { - t.Fatalf("TransitIP Code: %d, want 0", got2) - } - gotAddr2 := c.connector.transitIPTarget(nid, tip) - if gotAddr2 != dip2 { - t.Fatalf("Connector stored destination for tip: %v, want %v", gotAddr, dip2) - } -} + // Transit IPs + tipV4_1 := netip.MustParseAddr("0.0.0.1") + tipV4_2 := netip.MustParseAddr("0.0.0.2") -// TestHandleConnectorTransitIPRequestMultipleTIP tests that we can -// get a req with multiple mappings and we store them all. Including -// multiple transit addrs for the same destination. -func TestHandleConnectorTransitIPRequestMultipleTIP(t *testing.T) { - c := newConn25(logger.Discard) - nid := tailcfg.NodeID(1) - tip := netip.MustParseAddr("0.0.0.1") - tip2 := netip.MustParseAddr("0.0.0.2") - tip3 := netip.MustParseAddr("0.0.0.3") - dip := netip.MustParseAddr("1.2.3.4") - dip2 := netip.MustParseAddr("1.2.3.5") - req := ConnectorTransitIPRequest{ - TransitIPs: []TransitIPRequest{ - {TransitIP: tip, DestinationIP: dip}, - {TransitIP: tip2, DestinationIP: dip2}, - // can store same dst addr for multiple transit addrs - {TransitIP: tip3, DestinationIP: dip}, - }, - } - resp := c.handleConnectorTransitIPRequest(nid, req) - if len(resp.TransitIPs) != 3 { - t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs)) - } + tipV6_1 := netip.MustParseAddr("FE80::1") - for i := range 3 { - got := resp.TransitIPs[i].Code - if got != TransitIPResponseCode(0) { - t.Fatalf("i=%d TransitIP Code: %d, want 0", i, got) - } - } - gotAddr1 := c.connector.transitIPTarget(nid, tip) - if gotAddr1 != dip { - t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip) - } - gotAddr2 := c.connector.transitIPTarget(nid, tip2) - if gotAddr2 != dip2 { - t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip2) - } - gotAddr3 := c.connector.transitIPTarget(nid, tip3) - if gotAddr3 != dip { - t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip3, gotAddr3, dip) - } -} + // Destination IPs + dipV4_1 := netip.MustParseAddr("10.0.0.1") + dipV4_2 := netip.MustParseAddr("10.0.0.2") + dipV4_3 := netip.MustParseAddr("10.0.0.3") -// TestHandleConnectorTransitIPRequestSameTIP tests that if we get -// a req that has more than one TransitIPRequest for the same transit addr -// only the first is stored, and the subsequent ones get an error code and -// message in the response. -func TestHandleConnectorTransitIPRequestSameTIP(t *testing.T) { - c := newConn25(logger.Discard) - nid := tailcfg.NodeID(1) - tip := netip.MustParseAddr("0.0.0.1") - tip2 := netip.MustParseAddr("0.0.0.2") - dip := netip.MustParseAddr("1.2.3.4") - dip2 := netip.MustParseAddr("1.2.3.5") - dip3 := netip.MustParseAddr("1.2.3.6") - req := ConnectorTransitIPRequest{ - TransitIPs: []TransitIPRequest{ - {TransitIP: tip, DestinationIP: dip}, - // cannot have dupe TransitIPs in one ConnectorTransitIPRequest - {TransitIP: tip, DestinationIP: dip2}, - {TransitIP: tip2, DestinationIP: dip3}, + dipV6_1 := netip.MustParseAddr("fc00::1") + + // Peer nodes + peerV4V6 := (&tailcfg.Node{ + ID: tailcfg.NodeID(1), + Addresses: []netip.Prefix{netip.PrefixFrom(pipV4_1, 32), netip.PrefixFrom(pipV6_1, 128)}, + }).View() + + peerV4Only := (&tailcfg.Node{ + ID: tailcfg.NodeID(2), + Addresses: []netip.Prefix{netip.PrefixFrom(pipV4_2, 32)}, + }).View() + + peerV6Only := (&tailcfg.Node{ + ID: tailcfg.NodeID(3), + Addresses: []netip.Prefix{netip.PrefixFrom(pipV6_3, 128)}, + }).View() + + tests := []struct { + name string + ctipReqPeers []tailcfg.NodeView // One entry per request and the other + ctipReqs []ConnectorTransitIPRequest // arrays in this struct must have the same + wants []ConnectorTransitIPResponse // cardinality + // For checking lookups: + // The outer array needs to correspond to the number of requests, + // can be nil if no lookups need to be done after the request is processed. + // + // The middle array is the set of lookups for the corresponding request. + // + // The inner array is a tuple of (PeerIP, TransitIP, ExpectedDestinationIP) + wantLookups [][][]netip.Addr + }{ + // Single peer, single request with success ipV4 + { + name: "one-peer-one-req-ipv4", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}}, + }, + }, + // Single peer, single request with success ipV6 + { + name: "one-peer-one-req-ipv6", + ctipReqPeers: []tailcfg.NodeView{peerV6Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV6_1, DestinationIP: dipV6_1, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV6_3, tipV6_1, dipV6_1}}, + }, + }, + // Single peer, multi request with success, ipV4 + { + name: "one-peer-multi-req-ipv4", + ctipReqPeers: []tailcfg.NodeView{peerV4Only, peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}}, + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_2, DestinationIP: dipV4_2, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}}, + {{pipV4_2, tipV4_2, dipV4_2}}, + }, + }, + // Single peer, multi request remap tip, ipV4 + { + name: "one-peer-remap-tip", + ctipReqPeers: []tailcfg.NodeView{peerV4Only, peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}}, + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_2, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}}, + {{pipV4_2, tipV4_1, dipV4_2}}, + }, + }, + // Single peer, multi request with success, ipV4 and ipV6 + { + name: "one-peer-multi-req-ipv4-ipv6", + ctipReqPeers: []tailcfg.NodeView{peerV4V6, peerV4V6}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}}, + {TransitIPs: []TransitIPRequest{{TransitIP: tipV6_1, DestinationIP: dipV6_1, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_1, tipV4_1, dipV4_1}}, + {{pipV4_1, tipV4_1, dipV4_1}, {pipV6_1, tipV6_1, dipV6_1}, {pipV4_1, tipV6_1, netip.Addr{}}}, + }, + }, + // Single peer, multi map with success, ipV4 + { + name: "one-peer-multi-map-ipv4", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{ + {TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}, + {TransitIP: tipV4_2, DestinationIP: dipV4_2, App: appName}, + }}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}, {Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}, {pipV4_2, tipV4_2, dipV4_2}}, + }, + }, + // Single peer, error reuse same tip in one request, ensure all non-dup requests are processed + { + name: "one-peer-multi-map-duplicate-tip", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{ + {TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}, + {TransitIP: tipV4_1, DestinationIP: dipV4_2, App: appName}, + {TransitIP: tipV4_2, DestinationIP: dipV4_3, App: appName}, + }}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{ + {Code: OK, Message: ""}, + {Code: DuplicateTransitIP, Message: dupeTransitIPMessage}, + {Code: OK, Message: ""}}, + }, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}, {pipV4_2, tipV4_2, dipV4_3}}, + }, + }, + // Multi peer, success reuse same tip in one request + { + name: "multi-peer-duplicate-tip", + ctipReqPeers: []tailcfg.NodeView{peerV4V6, peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}}, + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_2, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_1, tipV4_1, dipV4_1}}, + {{pipV4_1, tipV4_1, dipV4_1}, {pipV4_2, tipV4_1, dipV4_2}}, + }, + }, + // Single peer, multi map, multiple tip to same dip + { + name: "one-peer-multi-map-multi-tip-to-dip", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{ + {TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}, + {TransitIP: tipV4_2, DestinationIP: dipV4_1, App: appName}, + }}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}, {Code: OK, Message: ""}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, dipV4_1}, {pipV4_2, tipV4_2, dipV4_1}}, + }, + }, + // Single peer, ipv4 tip, no ipv4 pip, but ipv6 tip works + { + name: "one-peer-missing-ipv4-family", + ctipReqPeers: []tailcfg.NodeView{peerV6Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{ + {TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}, + {TransitIP: tipV6_1, DestinationIP: dipV6_1, App: appName}, + }}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{ + {Code: NoMatchingPeerIPFamily, Message: noMatchingPeerIPFamilyMessage}, + {Code: OK, Message: ""}, + }}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV6_3, tipV4_1, netip.Addr{}}, {pipV6_3, tipV6_1, dipV6_1}}, + }, + }, + // Single peer, ipv6 tip, no ipv6 pip, but ipv4 tip works + { + name: "one-peer-missing-ipv6-family", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{ + {TransitIP: tipV6_1, DestinationIP: dipV6_1, App: appName}, + {TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}, + }}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{ + {Code: NoMatchingPeerIPFamily, Message: noMatchingPeerIPFamilyMessage}, + {Code: OK, Message: ""}, + }}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV6_1, netip.Addr{}}, {pipV4_2, tipV4_1, dipV4_1}}, + }, + }, + // Single peer, mismatched transit and destination ips + { + name: "one-peer-mismatched-tip-dip", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV6_1, App: appName}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: AddrFamilyMismatch, Message: addrFamilyMismatchMessage}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, netip.Addr{}}}, + }, + }, + // Single peer, invalid app name + { + name: "one-peer-invalid-app", + ctipReqPeers: []tailcfg.NodeView{peerV4Only}, + ctipReqs: []ConnectorTransitIPRequest{ + {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: "Unknown App"}}}, + }, + wants: []ConnectorTransitIPResponse{ + {TransitIPs: []TransitIPResponse{{Code: UnknownAppName, Message: unknownAppNameMessage}}}, + }, + wantLookups: [][][]netip.Addr{ + {{pipV4_2, tipV4_1, netip.Addr{}}}, + }, }, } - resp := c.handleConnectorTransitIPRequest(nid, req) - if len(resp.TransitIPs) != 3 { - t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs)) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch { + case len(tt.ctipReqPeers) != len(tt.ctipReqs): + t.Fatalf("error in test setup: ctipReqPeers has length %d does not match ctipReqs length %d", + len(tt.ctipReqPeers), len(tt.ctipReqs)) + case len(tt.ctipReqPeers) != len(tt.wants): + t.Fatalf("error in test setup: ctipReqPeers has length %d does not match wants length %d", + len(tt.ctipReqPeers), len(tt.wants)) + case len(tt.ctipReqPeers) != len(tt.wantLookups): + t.Fatalf("error in test setup: ctipReqPeers has length %d does not match wantLookups length %d", + len(tt.ctipReqPeers), len(tt.wantLookups)) + } - got := resp.TransitIPs[0].Code - if got != TransitIPResponseCode(0) { - t.Fatalf("i=0 TransitIP Code: %d, want 0", got) - } - msg := resp.TransitIPs[0].Message - if msg != "" { - t.Fatalf("i=0 TransitIP Message: \"%s\", want \"%s\"", msg, "") - } - got1 := resp.TransitIPs[1].Code - if got1 != TransitIPResponseCode(1) { - t.Fatalf("i=1 TransitIP Code: %d, want 1", got1) - } - msg1 := resp.TransitIPs[1].Message - if msg1 != dupeTransitIPMessage { - t.Fatalf("i=1 TransitIP Message: \"%s\", want \"%s\"", msg1, dupeTransitIPMessage) - } - got2 := resp.TransitIPs[2].Code - if got2 != TransitIPResponseCode(0) { - t.Fatalf("i=2 TransitIP Code: %d, want 0", got2) - } - msg2 := resp.TransitIPs[2].Message - if msg2 != "" { - t.Fatalf("i=2 TransitIP Message: \"%s\", want \"%s\"", msg, "") - } + // Use the same Conn25 for each request in the test and seed it with a test app name. + c := newConn25(logger.Discard) + c.connector.config = config{ + appsByName: map[string]appctype.Conn25Attr{appName: {}}, + } - gotAddr1 := c.connector.transitIPTarget(nid, tip) - if gotAddr1 != dip { - t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip) - } - gotAddr2 := c.connector.transitIPTarget(nid, tip2) - if gotAddr2 != dip3 { - t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip3) - } -} + for i, peer := range tt.ctipReqPeers { + req := tt.ctipReqs[i] + want := tt.wants[i] -// TestGetDstIPUnknownTIP tests that unknown transit addresses can be looked up without problem. -func TestTransitIPTargetUnknownTIP(t *testing.T) { - c := newConn25(logger.Discard) - nid := tailcfg.NodeID(1) - tip := netip.MustParseAddr("0.0.0.1") - got := c.connector.transitIPTarget(nid, tip) - want := netip.Addr{} - if got != want { - t.Fatalf("Unknown transit addr, want: %v, got %v", want, got) + resp := c.handleConnectorTransitIPRequest(peer, req) + + // Ensure that we have the expected number of responses + if len(resp.TransitIPs) != len(want.TransitIPs) { + t.Fatalf("wrong number of TransitIPs in response %d: got %d, want %d", + i, len(resp.TransitIPs), len(want.TransitIPs)) + } + + // Validate the contents of each response + for j, tipResp := range resp.TransitIPs { + wantResp := want.TransitIPs[j] + if tipResp.Code != wantResp.Code { + t.Errorf("transitIP.Code mismatch in response %d, tipresp %d: got %d, want %d", + i, j, tipResp.Code, wantResp.Code) + } + if tipResp.Message != wantResp.Message { + t.Errorf("transitIP.Message mismatch in response %d, tipresp %d: got %q, want %q", + i, j, tipResp.Message, wantResp.Message) + } + } + + // Validate the state of the transitIP map after each request + if tt.wantLookups[i] != nil { + for j, wantLookup := range tt.wantLookups[i] { + if len(wantLookup) != 3 { + t.Fatalf("test setup error: wantLookup for request %d lookup %d contains %d IPs, expected 3", + i, j, len(wantLookup)) + } + pip, tip, wantDip := wantLookup[0], wantLookup[1], wantLookup[2] + gotDip := c.connector.transitIPTarget(pip, tip) + if gotDip != wantDip { + t.Errorf("wrong result on lookup[%d][%d] ([%v], [%v]): got [%v] expected [%v]", + i, j, pip, tip, gotDip, wantDip) + } + } + } + } + }) } }