You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1855 lines
54 KiB
1855 lines
54 KiB
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package conn25
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"go4.org/mem"
|
|
"go4.org/netipx"
|
|
"golang.org/x/net/dns/dnsmessage"
|
|
"tailscale.com/ipn/ipnext"
|
|
"tailscale.com/net/dns"
|
|
"tailscale.com/net/packet"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tsd"
|
|
"tailscale.com/types/appctype"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/util/must"
|
|
"tailscale.com/util/set"
|
|
)
|
|
|
|
func mustIPSetFromPrefix(s string) *netipx.IPSet {
|
|
b := &netipx.IPSetBuilder{}
|
|
b.AddPrefix(netip.MustParsePrefix(s))
|
|
set, err := b.IPSet()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return set
|
|
}
|
|
|
|
// 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) {
|
|
|
|
const appName = "TestApp"
|
|
|
|
// Peer IPs
|
|
pipV4_1 := netip.MustParseAddr("100.101.101.101")
|
|
pipV4_2 := netip.MustParseAddr("100.101.101.102")
|
|
|
|
pipV6_1 := netip.MustParseAddr("fd7a:115c:a1e0::101")
|
|
pipV6_3 := netip.MustParseAddr("fd7a:115c:a1e0::103")
|
|
|
|
// 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}},
|
|
},
|
|
},
|
|
// 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{}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
// 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: {}},
|
|
}
|
|
|
|
for i, peer := range tt.ctipReqPeers {
|
|
req := tt.ctipReqs[i]
|
|
want := tt.wants[i]
|
|
|
|
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]
|
|
aa, _ := c.connector.lookupBySrcIPAndTransitIP(pip, tip)
|
|
gotDip := aa.addr
|
|
if gotDip != wantDip {
|
|
t.Errorf("wrong result on lookup[%d][%d] ([%v], [%v]): got [%v] expected [%v]",
|
|
i, j, pip, tip, gotDip, wantDip)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReserveIPs(t *testing.T) {
|
|
c := newConn25(logger.Discard)
|
|
c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
|
|
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
|
|
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
|
|
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
|
|
app := "a"
|
|
domainStr := "example.com."
|
|
mbd := map[dnsname.FQDN][]string{}
|
|
mbd["example.com."] = []string{app}
|
|
c.client.config.appNamesByDomain = mbd
|
|
domain := must.Get(dnsname.ToFQDN(domainStr))
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
dst netip.Addr
|
|
wantMagic netip.Addr
|
|
wantTransit netip.Addr
|
|
}{
|
|
{
|
|
name: "v4",
|
|
dst: netip.MustParseAddr("0.0.0.1"),
|
|
wantMagic: netip.MustParseAddr("100.64.0.0"), // first from magic pool
|
|
wantTransit: netip.MustParseAddr("169.254.0.0"), // first from transit pool
|
|
},
|
|
{
|
|
name: "v6",
|
|
dst: netip.MustParseAddr("::1"),
|
|
wantMagic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:100::"), // first from magic pool
|
|
wantTransit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:200::"), // first from transit pool
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
addrs, err := c.client.reserveAddresses(domain, tt.dst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tt.dst != addrs.dst {
|
|
t.Errorf("want %v, got %v", tt.dst, addrs.dst)
|
|
}
|
|
if tt.wantMagic != addrs.magic {
|
|
t.Errorf("want %v, got %v", tt.wantMagic, addrs.magic)
|
|
}
|
|
if tt.wantTransit != addrs.transit {
|
|
t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit)
|
|
}
|
|
if app != addrs.app {
|
|
t.Errorf("want %s, got %s", app, addrs.app)
|
|
}
|
|
if domain != addrs.domain {
|
|
t.Errorf("want %s, got %s", domain, addrs.domain)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReconfig(t *testing.T) {
|
|
rawCfg := `{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}`
|
|
capMap := tailcfg.NodeCapMap{
|
|
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{
|
|
tailcfg.RawMessage(rawCfg),
|
|
},
|
|
}
|
|
|
|
c := newConn25(logger.Discard)
|
|
sn := (&tailcfg.Node{
|
|
CapMap: capMap,
|
|
}).View()
|
|
|
|
err := c.reconfig(sn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(c.client.config.apps) != 1 || c.client.config.apps[0].Name != "app1" {
|
|
t.Fatalf("want apps to have one entry 'app1', got %v", c.client.config.apps)
|
|
}
|
|
}
|
|
|
|
func TestConfigReconfig(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
rawCfg string
|
|
cfg []appctype.Conn25Attr
|
|
tags []string
|
|
wantErr bool
|
|
wantAppsByDomain map[dnsname.FQDN][]string
|
|
wantSelfRoutedDomains set.Set[dnsname.FQDN]
|
|
}{
|
|
{
|
|
name: "bad-config",
|
|
rawCfg: `bad`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "simple",
|
|
cfg: []appctype.Conn25Attr{
|
|
{Name: "one", Domains: []string{"a.example.com"}, Connectors: []string{"tag:one"}},
|
|
{Name: "two", Domains: []string{"b.example.com"}, Connectors: []string{"tag:two"}},
|
|
},
|
|
tags: []string{"tag:one"},
|
|
wantAppsByDomain: map[dnsname.FQDN][]string{
|
|
"a.example.com.": {"one"},
|
|
"b.example.com.": {"two"},
|
|
},
|
|
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"a.example.com."}),
|
|
},
|
|
{
|
|
name: "more-complex",
|
|
cfg: []appctype.Conn25Attr{
|
|
{Name: "one", Domains: []string{"1.a.example.com", "1.b.example.com"}, Connectors: []string{"tag:one", "tag:onea"}},
|
|
{Name: "two", Domains: []string{"2.b.example.com", "2.c.example.com"}, Connectors: []string{"tag:two", "tag:twoa"}},
|
|
{Name: "three", Domains: []string{"1.b.example.com", "1.c.example.com"}, Connectors: []string{}},
|
|
{Name: "four", Domains: []string{"4.b.example.com", "4.d.example.com"}, Connectors: []string{"tag:four"}},
|
|
},
|
|
tags: []string{"tag:onea", "tag:four", "tag:unrelated"},
|
|
wantAppsByDomain: map[dnsname.FQDN][]string{
|
|
"1.a.example.com.": {"one"},
|
|
"1.b.example.com.": {"one", "three"},
|
|
"1.c.example.com.": {"three"},
|
|
"2.b.example.com.": {"two"},
|
|
"2.c.example.com.": {"two"},
|
|
"4.b.example.com.": {"four"},
|
|
"4.d.example.com.": {"four"},
|
|
},
|
|
wantSelfRoutedDomains: set.SetOf([]dnsname.FQDN{"1.a.example.com.", "1.b.example.com.", "4.b.example.com.", "4.d.example.com."}),
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := []tailcfg.RawMessage{tailcfg.RawMessage(tt.rawCfg)}
|
|
if tt.cfg != nil {
|
|
cfg = []tailcfg.RawMessage{}
|
|
for _, attr := range tt.cfg {
|
|
bs, err := json.Marshal(attr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error in test setup: %v", err)
|
|
}
|
|
cfg = append(cfg, tailcfg.RawMessage(bs))
|
|
}
|
|
}
|
|
capMap := tailcfg.NodeCapMap{
|
|
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
|
|
}
|
|
sn := (&tailcfg.Node{
|
|
CapMap: capMap,
|
|
Tags: tt.tags,
|
|
}).View()
|
|
c, err := configFromNodeView(sn)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("wantErr: %t, err: %v", tt.wantErr, err)
|
|
}
|
|
if diff := cmp.Diff(tt.wantAppsByDomain, c.appNamesByDomain); diff != "" {
|
|
t.Errorf("appsByDomain diff (-want, +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(tt.wantSelfRoutedDomains, c.selfRoutedDomains); diff != "" {
|
|
t.Errorf("selfRoutedDomains diff (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tailcfg.NodeView {
|
|
t.Helper()
|
|
cfg := make([]tailcfg.RawMessage, 0, len(attrs))
|
|
for i, attr := range attrs {
|
|
bs, err := json.Marshal(attr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error in test setup at index %d: %v", i, err)
|
|
}
|
|
cfg = append(cfg, tailcfg.RawMessage(bs))
|
|
}
|
|
capMap := tailcfg.NodeCapMap{
|
|
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg,
|
|
}
|
|
return (&tailcfg.Node{
|
|
CapMap: capMap,
|
|
Tags: tags,
|
|
}).View()
|
|
}
|
|
|
|
func v4RangeFrom(from, to string) netipx.IPRange {
|
|
return netipx.IPRangeFrom(
|
|
netip.MustParseAddr("100.64.0."+from),
|
|
netip.MustParseAddr("100.64.0."+to),
|
|
)
|
|
}
|
|
|
|
func v6RangeFrom(from, to string) netipx.IPRange {
|
|
return netipx.IPRangeFrom(
|
|
netip.MustParseAddr("fd7a:115c:a1e0:a99c:"+from+"::"),
|
|
netip.MustParseAddr("fd7a:115c:a1e0:a99c:"+to+"::"),
|
|
)
|
|
}
|
|
|
|
func makeDNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AResource) []byte {
|
|
t.Helper()
|
|
name := dnsmessage.MustNewName(domain)
|
|
questions := []dnsmessage.Question{
|
|
{
|
|
Name: name,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
}
|
|
var answers []dnsmessage.Resource
|
|
for _, addr := range addrs {
|
|
ans := dnsmessage.Resource{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: name,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: addr,
|
|
}
|
|
answers = append(answers, ans)
|
|
}
|
|
additional := []dnsmessage.Resource{
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: name,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AResource{A: [4]byte{9, 9, 9, 9}},
|
|
},
|
|
}
|
|
return makeDNSResponseForSections(t, questions, answers, additional)
|
|
}
|
|
|
|
func makeV6DNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AAAAResource) []byte {
|
|
t.Helper()
|
|
name := dnsmessage.MustNewName(domain)
|
|
questions := []dnsmessage.Question{
|
|
{
|
|
Name: name,
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
}
|
|
var answers []dnsmessage.Resource
|
|
for _, addr := range addrs {
|
|
ans := dnsmessage.Resource{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: name,
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: addr,
|
|
}
|
|
answers = append(answers, ans)
|
|
}
|
|
return makeDNSResponseForSections(t, questions, answers, nil)
|
|
}
|
|
|
|
func makeDNSResponseForSections(t *testing.T, questions []dnsmessage.Question, answers []dnsmessage.Resource, additional []dnsmessage.Resource) []byte {
|
|
t.Helper()
|
|
b := dnsmessage.NewBuilder(nil,
|
|
dnsmessage.Header{
|
|
ID: 1,
|
|
Response: true,
|
|
Authoritative: true,
|
|
RCode: dnsmessage.RCodeSuccess,
|
|
})
|
|
b.EnableCompression()
|
|
|
|
if err := b.StartQuestions(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, q := range questions {
|
|
if err := b.Question(q); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := b.StartAnswers(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, ans := range answers {
|
|
switch ans.Header.Type {
|
|
case dnsmessage.TypeA:
|
|
body, ok := (ans.Body).(*dnsmessage.AResource)
|
|
if !ok {
|
|
t.Fatalf("unexpected answer type, update test")
|
|
}
|
|
b.AResource(ans.Header, *body)
|
|
case dnsmessage.TypeAAAA:
|
|
body, ok := (ans.Body).(*dnsmessage.AAAAResource)
|
|
if !ok {
|
|
t.Fatalf("unexpected answer type, update test")
|
|
}
|
|
b.AAAAResource(ans.Header, *body)
|
|
default:
|
|
t.Fatalf("unhandled answer type, update test: %v", ans.Header.Type)
|
|
}
|
|
}
|
|
|
|
if err := b.StartAdditionals(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, add := range additional {
|
|
body, ok := (add.Body).(*dnsmessage.AResource)
|
|
if !ok {
|
|
t.Fatalf("unexpected additional type, update test")
|
|
}
|
|
b.AResource(add.Header, *body)
|
|
}
|
|
|
|
outbs, err := b.Finish()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return outbs
|
|
}
|
|
|
|
func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
domain string
|
|
v4Addrs []*dnsmessage.AResource
|
|
v6Addrs []*dnsmessage.AAAAResource
|
|
wantByMagicIP map[netip.Addr]addrs
|
|
}{
|
|
{
|
|
name: "one-ip-matches",
|
|
domain: "example.com.",
|
|
v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
|
|
// these are 'expected' because they are the beginning of the provided pools
|
|
wantByMagicIP: map[netip.Addr]addrs{
|
|
netip.MustParseAddr("100.64.0.0"): {
|
|
domain: "example.com.",
|
|
dst: netip.MustParseAddr("1.0.0.0"),
|
|
magic: netip.MustParseAddr("100.64.0.0"),
|
|
transit: netip.MustParseAddr("100.64.0.40"),
|
|
app: "app1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "v6-ip-matches",
|
|
domain: "example.com.",
|
|
v6Addrs: []*dnsmessage.AAAAResource{
|
|
{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
|
|
{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
|
|
},
|
|
wantByMagicIP: map[netip.Addr]addrs{
|
|
netip.MustParseAddr("fd7a:115c:a1e0:a99c::"): {
|
|
domain: "example.com.",
|
|
dst: netip.MustParseAddr("::1"),
|
|
magic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::"),
|
|
transit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:40::"),
|
|
app: "app1",
|
|
},
|
|
netip.MustParseAddr("fd7a:115c:a1e0:a99c::1"): {
|
|
domain: "example.com.",
|
|
dst: netip.MustParseAddr("::2"),
|
|
magic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::1"),
|
|
transit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:40::1"),
|
|
app: "app1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple-ip-matches",
|
|
domain: "example.com.",
|
|
v4Addrs: []*dnsmessage.AResource{
|
|
{A: [4]byte{1, 0, 0, 0}},
|
|
{A: [4]byte{2, 0, 0, 0}},
|
|
},
|
|
wantByMagicIP: map[netip.Addr]addrs{
|
|
netip.MustParseAddr("100.64.0.0"): {
|
|
domain: "example.com.",
|
|
dst: netip.MustParseAddr("1.0.0.0"),
|
|
magic: netip.MustParseAddr("100.64.0.0"),
|
|
transit: netip.MustParseAddr("100.64.0.40"),
|
|
app: "app1",
|
|
},
|
|
netip.MustParseAddr("100.64.0.1"): {
|
|
domain: "example.com.",
|
|
dst: netip.MustParseAddr("2.0.0.0"),
|
|
magic: netip.MustParseAddr("100.64.0.1"),
|
|
transit: netip.MustParseAddr("100.64.0.41"),
|
|
app: "app1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no-domain-match",
|
|
domain: "x.example.com.",
|
|
v4Addrs: []*dnsmessage.AResource{
|
|
{A: [4]byte{1, 0, 0, 0}},
|
|
{A: [4]byte{2, 0, 0, 0}},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var dnsResp []byte
|
|
if len(tt.v4Addrs) > 0 {
|
|
dnsResp = makeDNSResponse(t, tt.domain, tt.v4Addrs)
|
|
} else {
|
|
dnsResp = makeV6DNSResponse(t, tt.domain, tt.v6Addrs)
|
|
}
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
Name: "app1",
|
|
Connectors: []string{"tag:woo"},
|
|
Domains: []string{"example.com"},
|
|
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")},
|
|
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
|
|
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
|
|
V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")},
|
|
}}, []string{})
|
|
c := newConn25(logger.Discard)
|
|
c.reconfig(sn)
|
|
|
|
c.mapDNSResponse(dnsResp)
|
|
if diff := cmp.Diff(tt.wantByMagicIP, c.client.assignments.byMagicIP, cmpopts.EquateComparable(addrs{}, netip.Addr{})); diff != "" {
|
|
t.Errorf("byMagicIP diff (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReserveAddressesDeduplicated(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
dst netip.Addr
|
|
}{
|
|
{
|
|
name: "v4",
|
|
dst: netip.MustParseAddr("0.0.0.1"),
|
|
},
|
|
{
|
|
name: "v6",
|
|
dst: netip.MustParseAddr("::1"),
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := newConn25(logger.Discard)
|
|
c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
|
|
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
|
|
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
|
|
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
|
|
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
|
|
|
|
first, err := c.client.reserveAddresses("example.com.", tt.dst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
second, err := c.client.reserveAddresses("example.com.", tt.dst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if first != second {
|
|
t.Errorf("expected same addrs on repeated call, got first=%v second=%v", first, second)
|
|
}
|
|
if got := len(c.client.assignments.byMagicIP); got != 1 {
|
|
t.Errorf("want 1 entry in byMagicIP, got %d", got)
|
|
}
|
|
if got := len(c.client.assignments.byDomainDst); got != 1 {
|
|
t.Errorf("want 1 entry in byDomainDst, got %d", got)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
type testNodeBackend struct {
|
|
ipnext.NodeBackend
|
|
peers []tailcfg.NodeView
|
|
peerAPIURL string // should be per peer but there's only one peer in our test so this is ok for now
|
|
}
|
|
|
|
func (nb *testNodeBackend) AppendMatchingPeers(base []tailcfg.NodeView, pred func(tailcfg.NodeView) bool) []tailcfg.NodeView {
|
|
for _, p := range nb.peers {
|
|
if pred(p) {
|
|
base = append(base, p)
|
|
}
|
|
}
|
|
return base
|
|
}
|
|
|
|
func (nb *testNodeBackend) PeerHasPeerAPI(p tailcfg.NodeView) bool {
|
|
return true
|
|
}
|
|
|
|
func (nb *testNodeBackend) PeerAPIBase(p tailcfg.NodeView) string {
|
|
return nb.peerAPIURL
|
|
}
|
|
|
|
type testHost struct {
|
|
ipnext.Host
|
|
nb ipnext.NodeBackend
|
|
hooks ipnext.Hooks
|
|
authReconfigAsync func()
|
|
}
|
|
|
|
func (h *testHost) NodeBackend() ipnext.NodeBackend { return h.nb }
|
|
func (h *testHost) Hooks() *ipnext.Hooks { return &h.hooks }
|
|
func (h *testHost) AuthReconfigAsync() { h.authReconfigAsync() }
|
|
|
|
type testSafeBackend struct {
|
|
ipnext.SafeBackend
|
|
sys *tsd.System
|
|
}
|
|
|
|
func newTestSafeBackend() *testSafeBackend {
|
|
sb := &testSafeBackend{}
|
|
sys := &tsd.System{}
|
|
sys.Dialer.Set(&tsdial.Dialer{Logf: logger.Discard})
|
|
sys.DNSManager.Set(&dns.Manager{})
|
|
sys.Tun.Set(&tstun.Wrapper{})
|
|
sb.sys = sys
|
|
return sb
|
|
}
|
|
|
|
func (b *testSafeBackend) Sys() *tsd.System { return b.sys }
|
|
|
|
// TestAddressAssignmentIsHandled tests that after enqueueAddress has been called
|
|
// we handle the assignment asynchronously by:
|
|
// - making a peerapi request to a peer.
|
|
// - calling AuthReconfigAsync on the host.
|
|
func TestAddressAssignmentIsHandled(t *testing.T) {
|
|
// make a fake peer to test against
|
|
received := make(chan ConnectorTransitIPRequest, 1)
|
|
peersAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v0/connector/transit-ip" {
|
|
http.Error(w, "unexpected path", http.StatusNotFound)
|
|
return
|
|
}
|
|
var req ConnectorTransitIPRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
received <- req
|
|
resp := ConnectorTransitIPResponse{
|
|
TransitIPs: []TransitIPResponse{{Code: OK}},
|
|
}
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer peersAPI.Close()
|
|
|
|
connectorPeer := (&tailcfg.Node{
|
|
ID: tailcfg.NodeID(1),
|
|
Tags: []string{"tag:woo"},
|
|
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
|
|
Key: key.NodePublicFromRaw32(mem.B([]byte{0: 0xff, 1: 0xff, 31: 0x01})),
|
|
}).View()
|
|
|
|
ext := &extension{
|
|
conn25: newConn25(logger.Discard),
|
|
backend: newTestSafeBackend(),
|
|
}
|
|
authReconfigAsyncCalled := make(chan struct{}, 1)
|
|
if err := ext.Init(&testHost{
|
|
nb: &testNodeBackend{
|
|
peers: []tailcfg.NodeView{connectorPeer},
|
|
peerAPIURL: peersAPI.URL,
|
|
},
|
|
authReconfigAsync: func() {
|
|
authReconfigAsyncCalled <- struct{}{}
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ext.Shutdown()
|
|
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
Name: "app1",
|
|
Connectors: []string{"tag:woo"},
|
|
Domains: []string{"example.com"},
|
|
}}, []string{})
|
|
err := ext.conn25.reconfig(sn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
as := addrs{
|
|
dst: netip.MustParseAddr("1.2.3.4"),
|
|
magic: netip.MustParseAddr("100.64.0.0"),
|
|
transit: netip.MustParseAddr("169.254.0.1"),
|
|
domain: "example.com.",
|
|
app: "app1",
|
|
}
|
|
if err := ext.conn25.client.assignments.insert(as); err != nil {
|
|
t.Fatalf("error inserting address assignments: %v", err)
|
|
}
|
|
ext.conn25.client.enqueueAddressAssignment(as)
|
|
|
|
select {
|
|
case got := <-received:
|
|
if len(got.TransitIPs) != 1 {
|
|
t.Fatalf("want 1 TransitIP in request, got %d", len(got.TransitIPs))
|
|
}
|
|
tip := got.TransitIPs[0]
|
|
if tip.TransitIP != as.transit {
|
|
t.Errorf("TransitIP: got %v, want %v", tip.TransitIP, as.transit)
|
|
}
|
|
if tip.DestinationIP != as.dst {
|
|
t.Errorf("DestinationIP: got %v, want %v", tip.DestinationIP, as.dst)
|
|
}
|
|
if tip.App != as.app {
|
|
t.Errorf("App: got %q, want %q", tip.App, as.app)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for connector to receive request")
|
|
}
|
|
select {
|
|
case <-authReconfigAsyncCalled:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for AuthReconfigAsync to be called")
|
|
}
|
|
}
|
|
|
|
func parseResponse(t *testing.T, buf []byte) ([]dnsmessage.Resource, []dnsmessage.Resource) {
|
|
t.Helper()
|
|
var p dnsmessage.Parser
|
|
header, err := p.Start(buf)
|
|
if err != nil {
|
|
t.Fatalf("parsing DNS response: %v", err)
|
|
}
|
|
if header.RCode != dnsmessage.RCodeSuccess {
|
|
t.Fatalf("RCode want: %v, got: %v", dnsmessage.RCodeSuccess, header.RCode)
|
|
}
|
|
if err := p.SkipAllQuestions(); err != nil {
|
|
t.Fatalf("skipping questions: %v", err)
|
|
}
|
|
answers, err := p.AllAnswers()
|
|
if err != nil {
|
|
t.Fatalf("reading answers: %v", err)
|
|
}
|
|
if err := p.SkipAllAuthorities(); err != nil {
|
|
t.Fatalf("skipping questions: %v", err)
|
|
}
|
|
additionals, err := p.AllAdditionals()
|
|
if err != nil {
|
|
t.Fatalf("reading additionals: %v", err)
|
|
}
|
|
return answers, additionals
|
|
}
|
|
|
|
func TestMapDNSResponseRewritesResponses(t *testing.T) {
|
|
configuredDomain := "example.com"
|
|
domainName := configuredDomain + "."
|
|
dnsMessageName := dnsmessage.MustNewName(domainName)
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
Name: "app1",
|
|
Connectors: []string{"tag:connector"},
|
|
Domains: []string{configuredDomain},
|
|
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")},
|
|
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
|
|
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6812:100"), netip.MustParseAddr("2606:4700::6812:1ff"))},
|
|
V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))},
|
|
}}, []string{})
|
|
|
|
compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) {
|
|
t.Helper()
|
|
var got []netip.Addr
|
|
for _, r := range resources {
|
|
if b, ok := r.Body.(*dnsmessage.AResource); ok {
|
|
got = append(got, netip.AddrFrom4(b.A))
|
|
} else if b, ok := r.Body.(*dnsmessage.AAAAResource); ok {
|
|
got = append(got, netip.AddrFrom16(b.AAAA))
|
|
}
|
|
}
|
|
if diff := cmp.Diff(want, got, cmpopts.EquateComparable(netip.Addr{})); diff != "" {
|
|
t.Fatalf("A/AAAA records mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
assertParsesToAnswers := func(want []netip.Addr) func(t *testing.T, bs []byte) {
|
|
return func(t *testing.T, bs []byte) {
|
|
t.Helper()
|
|
answers, _ := parseResponse(t, bs)
|
|
compareToRecords(t, answers, want)
|
|
}
|
|
}
|
|
|
|
assertParsesToAdditionals := func(want []netip.Addr) func(t *testing.T, bs []byte) {
|
|
return func(t *testing.T, bs []byte) {
|
|
t.Helper()
|
|
_, additionals := parseResponse(t, bs)
|
|
compareToRecords(t, additionals, want)
|
|
}
|
|
}
|
|
|
|
assertBytes := func(want []byte) func(t *testing.T, bs []byte) {
|
|
return func(t *testing.T, bs []byte) {
|
|
t.Helper()
|
|
if diff := cmp.Diff(want, bs); diff != "" {
|
|
t.Fatalf("bytes mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
assertServFail := func(t *testing.T, bs []byte) {
|
|
var p dnsmessage.Parser
|
|
header, err := p.Start(bs)
|
|
if err != nil {
|
|
t.Fatalf("parsing DNS response: %v", err)
|
|
}
|
|
if header.RCode != dnsmessage.RCodeServerFailure {
|
|
t.Fatalf("RCode want: %v, got: %v", dnsmessage.RCodeServerFailure, header.RCode)
|
|
}
|
|
}
|
|
|
|
ipv6ResponseUnhandledDomain := makeV6DNSResponse(t, "tailscale.com.", []*dnsmessage.AAAAResource{
|
|
{AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()},
|
|
{AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()},
|
|
})
|
|
|
|
ipv4ResponseUnhandledDomain := makeDNSResponse(t, "tailscale.com.", []*dnsmessage.AResource{
|
|
{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
})
|
|
|
|
nonINETQuestionResp := makeDNSResponseForSections(t, []dnsmessage.Question{
|
|
{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassCHAOS,
|
|
},
|
|
}, nil, nil)
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
toMap []byte
|
|
assertFx func(*testing.T, []byte)
|
|
}{
|
|
{
|
|
name: "unparseable",
|
|
toMap: []byte{1, 2, 3, 4},
|
|
assertFx: assertBytes([]byte{1, 2, 3, 4}),
|
|
},
|
|
{
|
|
name: "maps-multi-typea-answers",
|
|
toMap: makeDNSResponse(t, domainName, []*dnsmessage.AResource{
|
|
{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
}),
|
|
assertFx: assertParsesToAnswers(
|
|
[]netip.Addr{
|
|
netip.MustParseAddr("100.64.0.0"),
|
|
netip.MustParseAddr("100.64.0.1"),
|
|
},
|
|
),
|
|
},
|
|
{
|
|
name: "ipv6-multiple",
|
|
toMap: makeV6DNSResponse(t, domainName, []*dnsmessage.AAAAResource{
|
|
{AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()},
|
|
{AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()},
|
|
}),
|
|
assertFx: assertParsesToAnswers(
|
|
[]netip.Addr{
|
|
netip.MustParseAddr("2606:4700::6812:100"),
|
|
netip.MustParseAddr("2606:4700::6812:101"),
|
|
},
|
|
),
|
|
},
|
|
{
|
|
name: "not-our-domain",
|
|
toMap: ipv4ResponseUnhandledDomain,
|
|
assertFx: assertBytes(ipv4ResponseUnhandledDomain),
|
|
},
|
|
{
|
|
name: "ipv6-not-our-domain",
|
|
toMap: ipv6ResponseUnhandledDomain,
|
|
assertFx: assertBytes(ipv6ResponseUnhandledDomain),
|
|
},
|
|
{
|
|
name: "case-insensitive",
|
|
toMap: makeDNSResponse(t, "eXample.com.", []*dnsmessage.AResource{
|
|
{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
}),
|
|
assertFx: assertParsesToAnswers(
|
|
[]netip.Addr{
|
|
netip.MustParseAddr("100.64.0.0"),
|
|
netip.MustParseAddr("100.64.0.1"),
|
|
},
|
|
),
|
|
},
|
|
{
|
|
name: "unhandled-keeps-additional-section",
|
|
toMap: makeDNSResponse(t, "tailscale.com.", []*dnsmessage.AResource{
|
|
{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
}),
|
|
assertFx: assertParsesToAdditionals(
|
|
// additionals are added in makeDNSResponse
|
|
[]netip.Addr{
|
|
netip.MustParseAddr("9.9.9.9"),
|
|
},
|
|
),
|
|
},
|
|
{
|
|
name: "handled-strips-additional-section",
|
|
toMap: makeDNSResponse(t, domainName, []*dnsmessage.AResource{
|
|
{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
}),
|
|
assertFx: assertParsesToAdditionals(nil),
|
|
},
|
|
{
|
|
name: "servfail-when-we-should-handle-but-cant",
|
|
// produced by
|
|
// makeDNSResponse(t, domainName, []*dnsmessage.AResource{{A: netip.MustParseAddr("1.2.3.4").As4()}})
|
|
// and then taking 17 bytes off the end. So that the parsing of it breaks after we have decided we should handle it.
|
|
// Frozen like this so that it doesn't depend on the implementation of dnsmessage.
|
|
toMap: []byte{0, 1, 132, 0, 0, 1, 0, 1, 0, 0, 0, 1, 7, 101, 120, 97, 109, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, 192, 12, 0, 1, 0, 1, 0, 0, 0, 0, 0, 4, 1, 2, 3},
|
|
assertFx: assertServFail,
|
|
},
|
|
{
|
|
name: "not-inet-question",
|
|
toMap: nonINETQuestionResp,
|
|
assertFx: assertBytes(nonINETQuestionResp),
|
|
},
|
|
{
|
|
name: "not-inet-answer",
|
|
toMap: makeDNSResponseForSections(t,
|
|
[]dnsmessage.Question{
|
|
{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
},
|
|
[]dnsmessage.Resource{
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassCHAOS,
|
|
},
|
|
Body: &dnsmessage.AResource{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
},
|
|
},
|
|
nil,
|
|
),
|
|
assertFx: assertParsesToAnswers(nil),
|
|
},
|
|
{
|
|
name: "answer-domain-mismatch",
|
|
toMap: makeDNSResponseForSections(t,
|
|
[]dnsmessage.Question{
|
|
{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
},
|
|
[]dnsmessage.Resource{
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName("tailscale.com."),
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AResource{A: netip.MustParseAddr("1.2.3.4").As4()},
|
|
},
|
|
},
|
|
nil,
|
|
),
|
|
assertFx: assertParsesToAnswers(nil),
|
|
},
|
|
{
|
|
name: "answer-type-mismatch-want-v4",
|
|
toMap: makeDNSResponseForSections(t,
|
|
[]dnsmessage.Question{
|
|
{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
},
|
|
[]dnsmessage.Resource{
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AAAAResource{AAAA: netip.MustParseAddr("1.2.3.4").As16()},
|
|
},
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AResource{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
},
|
|
},
|
|
nil,
|
|
),
|
|
assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("100.64.0.0")}),
|
|
},
|
|
{
|
|
name: "answer-type-mismatch-want-v6",
|
|
toMap: makeDNSResponseForSections(t,
|
|
[]dnsmessage.Question{
|
|
{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
},
|
|
[]dnsmessage.Resource{
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AAAAResource{AAAA: netip.MustParseAddr("1.2.3.4").As16()},
|
|
},
|
|
{
|
|
Header: dnsmessage.ResourceHeader{
|
|
Name: dnsMessageName,
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
},
|
|
Body: &dnsmessage.AResource{A: netip.MustParseAddr("5.6.7.8").As4()},
|
|
},
|
|
},
|
|
nil,
|
|
),
|
|
assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("2606:4700::6812:100")}),
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := newConn25(logger.Discard)
|
|
if err := c.reconfig(sn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bs := c.mapDNSResponse(tt.toMap)
|
|
tt.assertFx(t, bs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleAddressAssignmentStoresTransitIPs(t *testing.T) {
|
|
// make a fake peer API to test against, for all peers
|
|
peersAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v0/connector/transit-ip" {
|
|
http.Error(w, "unexpected path", http.StatusNotFound)
|
|
return
|
|
}
|
|
var req ConnectorTransitIPRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
resp := ConnectorTransitIPResponse{
|
|
TransitIPs: []TransitIPResponse{{Code: OK}},
|
|
}
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer peersAPI.Close()
|
|
|
|
connectorPeers := []tailcfg.NodeView{
|
|
(&tailcfg.Node{
|
|
ID: tailcfg.NodeID(1),
|
|
Tags: []string{"tag:woo"},
|
|
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
|
|
Key: key.NodePublicFromRaw32(mem.B([]byte{0: 0xff, 31: 0x01})),
|
|
}).View(),
|
|
(&tailcfg.Node{
|
|
ID: tailcfg.NodeID(2),
|
|
Tags: []string{"tag:hoo"},
|
|
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
|
|
Key: key.NodePublicFromRaw32(mem.B([]byte{0: 0xff, 31: 0x02})),
|
|
}).View(),
|
|
}
|
|
|
|
ext := &extension{
|
|
conn25: newConn25(logger.Discard),
|
|
backend: newTestSafeBackend(),
|
|
}
|
|
authReconfigAsyncCalled := make(chan struct{}, 1)
|
|
if err := ext.Init(&testHost{
|
|
nb: &testNodeBackend{
|
|
peers: connectorPeers,
|
|
peerAPIURL: peersAPI.URL,
|
|
},
|
|
authReconfigAsync: func() {
|
|
authReconfigAsyncCalled <- struct{}{}
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ext.Shutdown()
|
|
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{
|
|
{
|
|
Name: "app1",
|
|
Connectors: []string{"tag:woo"},
|
|
Domains: []string{"woo.example.com"},
|
|
},
|
|
{
|
|
Name: "app2",
|
|
Connectors: []string{"tag:hoo"},
|
|
Domains: []string{"hoo.example.com"},
|
|
},
|
|
}, []string{})
|
|
err := ext.conn25.reconfig(sn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
type lookup struct {
|
|
connKey key.NodePublic
|
|
expectedIPs []netip.Prefix
|
|
expectedOk bool
|
|
}
|
|
|
|
transitIPs := []netip.Prefix{
|
|
netip.MustParsePrefix("169.254.0.1/32"),
|
|
netip.MustParsePrefix("169.254.0.2/32"),
|
|
netip.MustParsePrefix("169.254.0.3/32"),
|
|
}
|
|
// Each step performs an insert on the provided addrs
|
|
// and then does the lookups.
|
|
steps := []struct {
|
|
name string
|
|
as addrs
|
|
lookups []lookup
|
|
}{
|
|
{
|
|
name: "step-1-conn1-tip1",
|
|
as: addrs{
|
|
dst: netip.MustParseAddr("1.2.3.1"),
|
|
magic: netip.MustParseAddr("100.64.0.1"),
|
|
transit: transitIPs[0].Addr(),
|
|
domain: "woo.example.com.",
|
|
app: "app1",
|
|
},
|
|
lookups: []lookup{
|
|
{
|
|
connKey: connectorPeers[0].Key(),
|
|
expectedIPs: []netip.Prefix{
|
|
transitIPs[0],
|
|
},
|
|
expectedOk: true,
|
|
},
|
|
{
|
|
connKey: connectorPeers[1].Key(),
|
|
expectedIPs: nil,
|
|
expectedOk: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "step-2-conn1-tip2",
|
|
as: addrs{
|
|
dst: netip.MustParseAddr("1.2.3.2"),
|
|
magic: netip.MustParseAddr("100.64.0.2"),
|
|
transit: transitIPs[1].Addr(),
|
|
domain: "woo.example.com.",
|
|
app: "app1",
|
|
},
|
|
lookups: []lookup{
|
|
{
|
|
connKey: connectorPeers[0].Key(),
|
|
expectedIPs: []netip.Prefix{
|
|
transitIPs[0],
|
|
transitIPs[1],
|
|
},
|
|
expectedOk: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "step-3-conn2-tip1",
|
|
as: addrs{
|
|
dst: netip.MustParseAddr("1.2.3.3"),
|
|
magic: netip.MustParseAddr("100.64.0.3"),
|
|
transit: transitIPs[2].Addr(),
|
|
domain: "hoo.example.com.",
|
|
app: "app2",
|
|
},
|
|
lookups: []lookup{
|
|
{
|
|
connKey: connectorPeers[0].Key(),
|
|
expectedIPs: []netip.Prefix{
|
|
transitIPs[0],
|
|
transitIPs[1],
|
|
},
|
|
expectedOk: true,
|
|
},
|
|
{
|
|
connKey: connectorPeers[1].Key(),
|
|
expectedIPs: []netip.Prefix{
|
|
transitIPs[2],
|
|
},
|
|
expectedOk: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range steps {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Add and enqueue the addrs, and then wait for the send to complete
|
|
// (as indicated by authReconfig being called).
|
|
if err := ext.conn25.client.assignments.insert(tt.as); err != nil {
|
|
t.Fatalf("error inserting address assignment: %v", err)
|
|
}
|
|
if err := ext.conn25.client.enqueueAddressAssignment(tt.as); err != nil {
|
|
t.Fatalf("error enqueuing address assignment: %v", err)
|
|
}
|
|
select {
|
|
case <-authReconfigAsyncCalled:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for AuthReconfigAsync to be called")
|
|
}
|
|
|
|
// Check that each of the lookups behaves as expected
|
|
for i, lu := range tt.lookups {
|
|
got, ok := ext.conn25.client.assignments.lookupTransitIPsByConnKey(lu.connKey)
|
|
if ok != lu.expectedOk {
|
|
t.Fatalf("unexpected ok result at index %d wanted %v, got %v", i, lu.expectedOk, ok)
|
|
}
|
|
slices.SortFunc(got, func(a, b netip.Prefix) int { return a.Compare(b) })
|
|
if diff := cmp.Diff(lu.expectedIPs, got, cmpopts.EquateComparable(netip.Prefix{})); diff != "" {
|
|
t.Fatalf("transit IPs mismatch at index %d, (-want +got):\n%s", i, diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTransitIPConnMapping(t *testing.T) {
|
|
conn25 := newConn25(t.Logf)
|
|
|
|
as := addrs{
|
|
dst: netip.MustParseAddr("1.2.3.1"),
|
|
magic: netip.MustParseAddr("100.64.0.1"),
|
|
transit: netip.MustParseAddr("169.254.0.1"),
|
|
domain: "woo.example.com.",
|
|
app: "app1",
|
|
}
|
|
|
|
connectorPeers := []tailcfg.NodeView{
|
|
(&tailcfg.Node{
|
|
ID: tailcfg.NodeID(0),
|
|
Tags: []string{"tag:woo"},
|
|
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
|
|
Key: key.NodePublic{},
|
|
}).View(),
|
|
(&tailcfg.Node{
|
|
ID: tailcfg.NodeID(2),
|
|
Tags: []string{"tag:hoo"},
|
|
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
|
|
Key: key.NodePublicFromRaw32(mem.B([]byte{0: 0xff, 31: 0x02})),
|
|
}).View(),
|
|
}
|
|
|
|
// Adding a transit IP that isn't known should fail
|
|
if err := conn25.client.addTransitIPForConnector(as.transit, connectorPeers[1]); err == nil {
|
|
t.Error("adding an unknown transit IP should fail")
|
|
}
|
|
|
|
// Insert the address assignments
|
|
conn25.client.assignments.insert(as)
|
|
|
|
// Adding a transit IP for a node with an unset key should fail
|
|
if err := conn25.client.addTransitIPForConnector(as.transit, connectorPeers[0]); err == nil {
|
|
t.Error("adding an transit IP mapping for a connector with a zero key should fail")
|
|
}
|
|
// Adding a transit IP that is known should succeed
|
|
if err := conn25.client.addTransitIPForConnector(as.transit, connectorPeers[1]); err != nil {
|
|
t.Errorf("unexpected error for first time add: %v", err)
|
|
}
|
|
// But doing it again should fail
|
|
if err := conn25.client.addTransitIPForConnector(as.transit, connectorPeers[1]); err == nil {
|
|
t.Error("adding a duplicate transitIP for a connector should fail")
|
|
}
|
|
}
|
|
|
|
func TestClientTransitIPForMagicIP(t *testing.T) {
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10
|
|
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10")},
|
|
}}, []string{})
|
|
|
|
mappedMip := netip.MustParseAddr("100.64.0.0")
|
|
mappedTip := netip.MustParseAddr("169.0.0.0")
|
|
unmappedMip := netip.MustParseAddr("100.64.0.1")
|
|
nonMip := netip.MustParseAddr("100.64.0.11")
|
|
dst := netip.MustParseAddr("0.0.0.1")
|
|
|
|
v6MappedMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::")
|
|
v6MappedTip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:100::")
|
|
v6UnmappedMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:1::")
|
|
v6NonMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:11::")
|
|
v6Dst := netip.MustParseAddr("::1")
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
mip netip.Addr
|
|
wantTip netip.Addr
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "not-a-magic-ip",
|
|
mip: nonMip,
|
|
wantTip: netip.Addr{},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "unmapped-magic-ip",
|
|
mip: unmappedMip,
|
|
wantTip: netip.Addr{},
|
|
wantErr: ErrUnmappedMagicIP,
|
|
},
|
|
{
|
|
name: "mapped-magic-ip",
|
|
mip: mappedMip,
|
|
wantTip: mappedTip,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "v6-not-magic",
|
|
mip: v6NonMip,
|
|
wantTip: netip.Addr{},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "v6-unmapped-magic-ip",
|
|
mip: v6UnmappedMip,
|
|
wantTip: netip.Addr{},
|
|
wantErr: ErrUnmappedMagicIP,
|
|
},
|
|
{
|
|
name: "v6-mapped-magic-ip",
|
|
mip: v6MappedMip,
|
|
wantTip: v6MappedTip,
|
|
wantErr: nil,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := newConn25(t.Logf)
|
|
if err := c.reconfig(sn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.client.assignments.insert(addrs{
|
|
magic: mappedMip,
|
|
transit: mappedTip,
|
|
dst: dst,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.client.assignments.insert(addrs{
|
|
magic: v6MappedMip,
|
|
transit: v6MappedTip,
|
|
dst: v6Dst,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tip, err := c.client.transitIPForMagicIP(tt.mip)
|
|
if tip != tt.wantTip {
|
|
t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip)
|
|
}
|
|
if err != tt.wantErr {
|
|
t.Fatalf("checking error: want %v, got %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50
|
|
}}, []string{})
|
|
mappedSrc := netip.MustParseAddr("100.0.0.1")
|
|
unmappedSrc := netip.MustParseAddr("100.0.0.2")
|
|
mappedTip := netip.MustParseAddr("100.64.0.41")
|
|
unmappedTip := netip.MustParseAddr("100.64.0.42")
|
|
nonTip := netip.MustParseAddr("100.0.0.3")
|
|
mappedMip := netip.MustParseAddr("100.64.0.1")
|
|
for _, tt := range []struct {
|
|
name string
|
|
src netip.Addr
|
|
tip netip.Addr
|
|
wantMip netip.Addr
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "not-a-transit-ip-unmapped-src",
|
|
src: unmappedSrc,
|
|
tip: nonTip,
|
|
wantMip: netip.Addr{},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "not-a-transit-ip-mapped-src",
|
|
src: mappedSrc,
|
|
tip: nonTip,
|
|
wantMip: netip.Addr{},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "unmapped-src-transit-ip",
|
|
src: unmappedSrc,
|
|
tip: unmappedTip,
|
|
wantMip: netip.Addr{},
|
|
wantErr: ErrUnmappedSrcAndTransitIP,
|
|
},
|
|
{
|
|
name: "unmapped-tip-transit-ip",
|
|
src: mappedSrc,
|
|
tip: unmappedTip,
|
|
wantMip: netip.Addr{},
|
|
wantErr: ErrUnmappedSrcAndTransitIP,
|
|
},
|
|
{
|
|
name: "mapped-src-and-transit-ip",
|
|
src: mappedSrc,
|
|
tip: mappedTip,
|
|
wantMip: mappedMip,
|
|
wantErr: nil,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := newConn25(t.Logf)
|
|
if err := c.reconfig(sn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{}
|
|
c.connector.transitIPs[mappedSrc] = map[netip.Addr]appAddr{}
|
|
c.connector.transitIPs[mappedSrc][mappedTip] = appAddr{addr: mappedMip}
|
|
mip, err := c.connector.realIPForTransitIPConnection(tt.src, tt.tip)
|
|
if mip != tt.wantMip {
|
|
t.Fatalf("checking magic ip: want %v, got %v", tt.wantMip, mip)
|
|
}
|
|
if err != tt.wantErr {
|
|
t.Fatalf("checking error: want %v, got %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsKnownTransitIP(t *testing.T) {
|
|
knownTip := netip.MustParseAddr("100.64.0.41")
|
|
unknownTip := netip.MustParseAddr("100.64.0.42")
|
|
|
|
c := newConn25(t.Logf)
|
|
c.client.assignments.insert(addrs{
|
|
transit: knownTip,
|
|
})
|
|
|
|
if !c.client.isKnownTransitIP(knownTip) {
|
|
t.Fatal("knownTip: should have been known")
|
|
}
|
|
if c.client.isKnownTransitIP(unknownTip) {
|
|
t.Fatal("unknownTip: should not have been known")
|
|
}
|
|
}
|
|
|
|
func TestLinkLocalAllow(t *testing.T) {
|
|
knownTip := netip.MustParseAddr("100.64.0.41")
|
|
|
|
c := newConn25(t.Logf)
|
|
c.client.assignments.insert(addrs{
|
|
transit: knownTip,
|
|
})
|
|
|
|
if allow, _ := c.client.linkLocalAllow(packet.Parsed{
|
|
Dst: netip.AddrPortFrom(knownTip, 1234),
|
|
}); !allow {
|
|
t.Fatal("knownTip: should have been allowed")
|
|
}
|
|
|
|
if allow, _ := c.client.linkLocalAllow(packet.Parsed{
|
|
Dst: netip.AddrPort{},
|
|
}); allow {
|
|
t.Fatal("unknownTip: should not have been allowed")
|
|
}
|
|
}
|
|
|
|
func TestConnectorPacketFilterAllow(t *testing.T) {
|
|
knownTip := netip.MustParseAddr("100.64.0.41")
|
|
knownSrc := netip.MustParseAddr("100.64.0.1")
|
|
unknownTip := netip.MustParseAddr("100.64.0.42")
|
|
unknownSrc := netip.MustParseAddr("100.64.0.42")
|
|
|
|
c := newConn25(t.Logf)
|
|
c.connector.transitIPs = map[netip.Addr]map[netip.Addr]appAddr{}
|
|
c.connector.transitIPs[knownSrc] = map[netip.Addr]appAddr{}
|
|
c.connector.transitIPs[knownSrc][knownTip] = appAddr{}
|
|
|
|
if allow, _ := c.connector.packetFilterAllow(packet.Parsed{
|
|
Src: netip.AddrPortFrom(knownSrc, 1234),
|
|
Dst: netip.AddrPortFrom(knownTip, 1234),
|
|
}); !allow {
|
|
t.Fatal("knownTip: should have been allowed")
|
|
}
|
|
|
|
if allow, _ := c.connector.packetFilterAllow(packet.Parsed{
|
|
Src: netip.AddrPortFrom(unknownSrc, 1234),
|
|
Dst: netip.AddrPortFrom(knownTip, 1234),
|
|
}); allow {
|
|
t.Fatal("unknownSrc: should not have been allowed")
|
|
}
|
|
if allow, _ := c.connector.packetFilterAllow(packet.Parsed{
|
|
Src: netip.AddrPortFrom(knownSrc, 1234),
|
|
Dst: netip.AddrPortFrom(unknownTip, 1234),
|
|
}); allow {
|
|
t.Fatal("unknownTip: should not have been allowed")
|
|
}
|
|
}
|
|
|
|
func TestGetMagicRange(t *testing.T) {
|
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
|
Name: "app1",
|
|
Connectors: []string{"tag:woo"},
|
|
Domains: []string{"example.com"},
|
|
V4MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3"))},
|
|
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("::1"), netip.MustParseAddr("::3"))},
|
|
}}, []string{})
|
|
c := newConn25(t.Logf)
|
|
if err := c.reconfig(sn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ext := &extension{
|
|
conn25: c,
|
|
}
|
|
mRange := ext.getMagicRange()
|
|
somePrefixCovers := func(a netip.Addr) bool {
|
|
for _, r := range mRange.All() {
|
|
if r.Contains(a) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
ins := []string{
|
|
"0.0.0.1",
|
|
"0.0.0.2",
|
|
"0.0.0.3",
|
|
"::1",
|
|
"::2",
|
|
"::3",
|
|
}
|
|
outs := []string{
|
|
"0.0.0.0",
|
|
"0.0.0.4",
|
|
"::",
|
|
"::4",
|
|
}
|
|
for _, s := range ins {
|
|
if !somePrefixCovers(netip.MustParseAddr(s)) {
|
|
t.Fatalf("expected addr to be covered but was not: %s", s)
|
|
}
|
|
}
|
|
for _, s := range outs {
|
|
if somePrefixCovers(netip.MustParseAddr(s)) {
|
|
t.Fatalf("expected addr to NOT be covered but WAS: %s", s)
|
|
}
|
|
}
|
|
}
|
|
|