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 <george@tailscale.com>
This commit is contained in:
George Jones
2026-03-13 13:26:08 -04:00
committed by GitHub
parent 621f71981c
commit 660a4608d2
2 changed files with 414 additions and 182 deletions
+82 -17
View File
@@ -26,6 +26,7 @@ import (
"tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/appctype" "tailscale.com/types/appctype"
"tailscale.com/types/logger" "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) http.Error(w, "Error decoding JSON", http.StatusBadRequest)
return return
} }
resp := e.conn25.handleConnectorTransitIPRequest(h.Peer().ID(), req) resp := e.conn25.handleConnectorTransitIPRequest(h.Peer(), req)
bs, err := json.Marshal(resp) bs, err := json.Marshal(resp)
if err != nil { if err != nil {
http.Error(w, "Error encoding JSON", http.StatusInternalServerError) 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 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{} resp := ConnectorTransitIPResponse{}
seen := map[netip.Addr]bool{} seen := map[netip.Addr]bool{}
for _, each := range ctipr.TransitIPs { for _, each := range ctipr.TransitIPs {
if seen[each.TransitIP] { if seen[each.TransitIP] {
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{ resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
Code: OtherFailure, Code: DuplicateTransitIP,
Message: dupeTransitIPMessage, 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 continue
} }
tipresp := c.connector.handleTransitIPRequest(nid, each) tipresp := c.connector.handleTransitIPRequest(n, peerIPv4, peerIPv6, each)
seen[each.TransitIP] = true seen[each.TransitIP] = true
resp.TransitIPs = append(resp.TransitIPs, tipresp) resp.TransitIPs = append(resp.TransitIPs, tipresp)
} }
return resp 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() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if s.transitIPs == nil { if _, ok := s.config.appsByName[tipr.App]; !ok {
s.transitIPs = make(map[tailcfg.NodeID]map[netip.Addr]appAddr) 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}
} }
peerMap, ok := s.transitIPs[nid]
if s.transitIPs == nil {
s.transitIPs = make(map[netip.Addr]map[netip.Addr]appAddr)
}
peerMap, ok := s.transitIPs[peerAddr]
if !ok { if !ok {
peerMap = make(map[netip.Addr]appAddr) peerMap = make(map[netip.Addr]appAddr)
s.transitIPs[nid] = peerMap s.transitIPs[peerAddr] = peerMap
} }
peerMap[tipr.TransitIP] = appAddr{addr: tipr.DestinationIP, app: tipr.App} peerMap[tipr.TransitIP] = appAddr{addr: tipr.DestinationIP, app: tipr.App}
return TransitIPResponse{} 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() s.mu.Lock()
defer s.mu.Unlock() 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 // TransitIPRequest details a single TransitIP allocation request from a client to a
@@ -322,8 +370,24 @@ const (
OK TransitIPResponseCode = 0 OK TransitIPResponseCode = 0
// OtherFailure indicates that the mapping failed for a reason that does not have // OtherFailure indicates that the mapping failed for a reason that does not have
// another relevant [TransitIPResponsecode]. // another relevant [TransitIPResponseCode].
OtherFailure TransitIPResponseCode = 1 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 // TransitIPResponse is the response to a TransitIPRequest
@@ -651,8 +715,9 @@ type connector struct {
logf logger.Logf logf logger.Logf
mu sync.Mutex // protects the fields below 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 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.
transitIPs map[tailcfg.NodeID]map[netip.Addr]appAddr // 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 config config
} }
+332 -165
View File
@@ -38,180 +38,347 @@ func mustIPSetFromPrefix(s string) *netipx.IPSet {
return set return set
} }
// TestHandleConnectorTransitIPRequestZeroLength tests that if sent a // TestHandleConnectorTransitIPRequest 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)
resp := c.handleConnectorTransitIPRequest(nid, req)
if len(resp.TransitIPs) != 0 {
t.Fatalf("n TransitIPs in response: %d, want 0", len(resp.TransitIPs))
}
}
// TestHandleConnectorTransitIPRequestStoresAddr tests that if sent a
// request with a transit addr and a destination addr we store that mapping // 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 // and can retrieve it.
// we store that instead. func TestHandleConnectorTransitIPRequest(t *testing.T) {
func TestHandleConnectorTransitIPRequestStoresAddr(t *testing.T) {
c := newConn25(logger.Discard) const appName = "TestApp"
nid := tailcfg.NodeID(1)
tip := netip.MustParseAddr("0.0.0.1") // Peer IPs
dip := netip.MustParseAddr("1.2.3.4") pipV4_1 := netip.MustParseAddr("100.101.101.101")
dip2 := netip.MustParseAddr("1.2.3.5") pipV4_2 := netip.MustParseAddr("100.101.101.102")
mr := func(t, d netip.Addr) ConnectorTransitIPRequest {
return ConnectorTransitIPRequest{ pipV6_1 := netip.MustParseAddr("fd7a:115c:a1e0::101")
TransitIPs: []TransitIPRequest{ pipV6_3 := netip.MustParseAddr("fd7a:115c:a1e0::103")
{TransitIP: t, DestinationIP: d},
// Transit IPs
tipV4_1 := netip.MustParseAddr("0.0.0.1")
tipV4_2 := netip.MustParseAddr("0.0.0.2")
tipV6_1 := netip.MustParseAddr("FE80::1")
// 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")
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}},
}, },
}
}
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)
}
// 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)
}
}
// 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},
}, },
} // Single peer, single request with success ipV6
resp := c.handleConnectorTransitIPRequest(nid, req) {
if len(resp.TransitIPs) != 3 { name: "one-peer-one-req-ipv6",
t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs)) ctipReqPeers: []tailcfg.NodeView{peerV6Only},
} ctipReqs: []ConnectorTransitIPRequest{
{TransitIPs: []TransitIPRequest{{TransitIP: tipV6_1, DestinationIP: dipV6_1, App: appName}}},
for i := range 3 { },
got := resp.TransitIPs[i].Code wants: []ConnectorTransitIPResponse{
if got != TransitIPResponseCode(0) { {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}},
t.Fatalf("i=%d TransitIP Code: %d, want 0", i, got) },
} wantLookups: [][][]netip.Addr{
} {{pipV6_3, tipV6_1, dipV6_1}},
gotAddr1 := c.connector.transitIPTarget(nid, tip) },
if gotAddr1 != dip { },
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip) // Single peer, multi request with success, ipV4
} {
gotAddr2 := c.connector.transitIPTarget(nid, tip2) name: "one-peer-multi-req-ipv4",
if gotAddr2 != dip2 { ctipReqPeers: []tailcfg.NodeView{peerV4Only, peerV4Only},
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip2) ctipReqs: []ConnectorTransitIPRequest{
} {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}},
gotAddr3 := c.connector.transitIPTarget(nid, tip3) {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_2, DestinationIP: dipV4_2, App: appName}}},
if gotAddr3 != dip { },
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip3, gotAddr3, dip) wants: []ConnectorTransitIPResponse{
} {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}},
} {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}},
},
// TestHandleConnectorTransitIPRequestSameTIP tests that if we get wantLookups: [][][]netip.Addr{
// a req that has more than one TransitIPRequest for the same transit addr {{pipV4_2, tipV4_1, dipV4_1}},
// only the first is stored, and the subsequent ones get an error code and {{pipV4_2, tipV4_2, dipV4_2}},
// message in the response. },
func TestHandleConnectorTransitIPRequestSameTIP(t *testing.T) { },
c := newConn25(logger.Discard) // Single peer, multi request remap tip, ipV4
nid := tailcfg.NodeID(1) {
tip := netip.MustParseAddr("0.0.0.1") name: "one-peer-remap-tip",
tip2 := netip.MustParseAddr("0.0.0.2") ctipReqPeers: []tailcfg.NodeView{peerV4Only, peerV4Only},
dip := netip.MustParseAddr("1.2.3.4") ctipReqs: []ConnectorTransitIPRequest{
dip2 := netip.MustParseAddr("1.2.3.5") {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_1, App: appName}}},
dip3 := netip.MustParseAddr("1.2.3.6") {TransitIPs: []TransitIPRequest{{TransitIP: tipV4_1, DestinationIP: dipV4_2, App: appName}}},
req := ConnectorTransitIPRequest{ },
TransitIPs: []TransitIPRequest{ wants: []ConnectorTransitIPResponse{
{TransitIP: tip, DestinationIP: dip}, {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}},
// cannot have dupe TransitIPs in one ConnectorTransitIPRequest {TransitIPs: []TransitIPResponse{{Code: OK, Message: ""}}},
{TransitIP: tip, DestinationIP: dip2}, },
{TransitIP: tip2, DestinationIP: dip3}, 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) for _, tt := range tests {
if len(resp.TransitIPs) != 3 { t.Run(tt.name, func(t *testing.T) {
t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs)) 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 // Use the same Conn25 for each request in the test and seed it with a test app name.
if got != TransitIPResponseCode(0) { c := newConn25(logger.Discard)
t.Fatalf("i=0 TransitIP Code: %d, want 0", got) c.connector.config = config{
} appsByName: map[string]appctype.Conn25Attr{appName: {}},
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, "")
}
gotAddr1 := c.connector.transitIPTarget(nid, tip) for i, peer := range tt.ctipReqPeers {
if gotAddr1 != dip { req := tt.ctipReqs[i]
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip) want := tt.wants[i]
}
gotAddr2 := c.connector.transitIPTarget(nid, tip2)
if gotAddr2 != dip3 {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip3)
}
}
// TestGetDstIPUnknownTIP tests that unknown transit addresses can be looked up without problem. resp := c.handleConnectorTransitIPRequest(peer, req)
func TestTransitIPTargetUnknownTIP(t *testing.T) {
c := newConn25(logger.Discard) // Ensure that we have the expected number of responses
nid := tailcfg.NodeID(1) if len(resp.TransitIPs) != len(want.TransitIPs) {
tip := netip.MustParseAddr("0.0.0.1") t.Fatalf("wrong number of TransitIPs in response %d: got %d, want %d",
got := c.connector.transitIPTarget(nid, tip) i, len(resp.TransitIPs), len(want.TransitIPs))
want := netip.Addr{} }
if got != want {
t.Fatalf("Unknown transit addr, want: %v, got %v", want, got) // 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)
}
}
}
}
})
} }
} }