@ -17,194 +17,238 @@ import (
"tailscale.com/util/must"
"tailscale.com/util/must"
)
)
func fakeStoreRoutes ( * RouteInfo ) error { return nil }
func TestUpdateDomains ( t * testing . T ) {
func TestUpdateDomains ( t * testing . T ) {
ctx := context . Background ( )
for _ , shouldStore := range [ ] bool { false , true } {
a := NewAppConnector ( t . Logf , nil )
ctx := context . Background ( )
a . UpdateDomains ( [ ] string { "example.com" } )
var a * AppConnector
if shouldStore {
a = NewAppConnector ( t . Logf , nil , & RouteInfo { } , fakeStoreRoutes )
} else {
a = NewAppConnector ( t . Logf , nil , nil , nil )
}
a . UpdateDomains ( [ ] string { "example.com" } )
a . Wait ( ctx )
a . Wait ( ctx )
if got , want := a . Domains ( ) . AsSlice ( ) , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
if got , want := a . Domains ( ) . AsSlice ( ) , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
t . Errorf ( "got %v; want %v" , got , want )
}
}
addr := netip . MustParseAddr ( "192.0.0.8" )
addr := netip . MustParseAddr ( "192.0.0.8" )
a . domains [ "example.com" ] = append ( a . domains [ "example.com" ] , addr )
a . domains [ "example.com" ] = append ( a . domains [ "example.com" ] , addr )
a . UpdateDomains ( [ ] string { "example.com" } )
a . UpdateDomains ( [ ] string { "example.com" } )
a . Wait ( ctx )
a . Wait ( ctx )
if got , want := a . domains [ "example.com" ] , [ ] netip . Addr { addr } ; ! slices . Equal ( got , want ) {
if got , want := a . domains [ "example.com" ] , [ ] netip . Addr { addr } ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
t . Errorf ( "got %v; want %v" , got , want )
}
}
// domains are explicitly downcased on set.
// domains are explicitly downcased on set.
a . UpdateDomains ( [ ] string { "UP.EXAMPLE.COM" } )
a . UpdateDomains ( [ ] string { "UP.EXAMPLE.COM" } )
a . Wait ( ctx )
a . Wait ( ctx )
if got , want := xmaps . Keys ( a . domains ) , [ ] string { "up.example.com" } ; ! slices . Equal ( got , want ) {
if got , want := xmaps . Keys ( a . domains ) , [ ] string { "up.example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
t . Errorf ( "got %v; want %v" , got , want )
}
}
}
}
}
func TestUpdateRoutes ( t * testing . T ) {
func TestUpdateRoutes ( t * testing . T ) {
ctx := context . Background ( )
for _ , shouldStore := range [ ] bool { false , true } {
rc := & appctest . RouteCollector { }
ctx := context . Background ( )
a := NewAppConnector ( t . Logf , rc )
rc := & appctest . RouteCollector { }
a . updateDomains ( [ ] string { "*.example.com" } )
var a * AppConnector
if shouldStore {
a = NewAppConnector ( t . Logf , rc , & RouteInfo { } , fakeStoreRoutes )
} else {
a = NewAppConnector ( t . Logf , rc , nil , nil )
}
a . updateDomains ( [ ] string { "*.example.com" } )
// This route should be collapsed into the range
// This route should be collapsed into the range
a . ObserveDNSResponse ( dnsResponse ( "a.example.com." , "192.0.2.1" ) )
a . ObserveDNSResponse ( dnsResponse ( "a.example.com." , "192.0.2.1" ) )
a . Wait ( ctx )
a . Wait ( ctx )
if ! slices . Equal ( rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } ) {
if ! slices . Equal ( rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } ) {
t . Fatalf ( "got %v, want %v" , rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } )
t . Fatalf ( "got %v, want %v" , rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } )
}
}
// This route should not be collapsed or removed
// This route should not be collapsed or removed
a . ObserveDNSResponse ( dnsResponse ( "b.example.com." , "192.0.0.1" ) )
a . ObserveDNSResponse ( dnsResponse ( "b.example.com." , "192.0.0.1" ) )
a . Wait ( ctx )
a . Wait ( ctx )
routes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.0/24" ) , netip . MustParsePrefix ( "192.0.0.1/32" ) }
routes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.0/24" ) , netip . MustParsePrefix ( "192.0.0.1/32" ) }
a . updateRoutes ( routes )
a . updateRoutes ( routes )
slices . SortFunc ( rc . Routes ( ) , prefixCompare )
slices . SortFunc ( rc . Routes ( ) , prefixCompare )
rc . SetRoutes ( slices . Compact ( rc . Routes ( ) ) )
rc . SetRoutes ( slices . Compact ( rc . Routes ( ) ) )
slices . SortFunc ( routes , prefixCompare )
slices . SortFunc ( routes , prefixCompare )
// Ensure that the non-matching /32 is preserved, even though it's in the domains table.
// Ensure that the non-matching /32 is preserved, even though it's in the domains table.
if ! slices . EqualFunc ( routes , rc . Routes ( ) , prefixEqual ) {
if ! slices . EqualFunc ( routes , rc . Routes ( ) , prefixEqual ) {
t . Errorf ( "added routes: got %v, want %v" , rc . Routes ( ) , routes )
t . Errorf ( "added routes: got %v, want %v" , rc . Routes ( ) , routes )
}
}
// Ensure that the contained /32 is removed, replaced by the /24.
// Ensure that the contained /32 is removed, replaced by the /24.
wantRemoved := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) }
wantRemoved := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) }
if ! slices . EqualFunc ( rc . RemovedRoutes ( ) , wantRemoved , prefixEqual ) {
if ! slices . EqualFunc ( rc . RemovedRoutes ( ) , wantRemoved , prefixEqual ) {
t . Fatalf ( "unexpected removed routes: %v" , rc . RemovedRoutes ( ) )
t . Fatalf ( "unexpected removed routes: %v" , rc . RemovedRoutes ( ) )
}
}
}
}
}
func TestUpdateRoutesUnadvertisesContainedRoutes ( t * testing . T ) {
func TestUpdateRoutesUnadvertisesContainedRoutes ( t * testing . T ) {
rc := & appctest . RouteCollector { }
for _ , shouldStore := range [ ] bool { false , true } {
a := NewAppConnector ( t . Logf , rc )
rc := & appctest . RouteCollector { }
mak . Set ( & a . domains , "example.com" , [ ] netip . Addr { netip . MustParseAddr ( "192.0.2.1" ) } )
var a * AppConnector
rc . SetRoutes ( [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } )
if shouldStore {
routes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.0/24" ) }
a = NewAppConnector ( t . Logf , rc , & RouteInfo { } , fakeStoreRoutes )
a . updateRoutes ( routes )
} else {
a = NewAppConnector ( t . Logf , rc , nil , nil )
if ! slices . EqualFunc ( routes , rc . Routes ( ) , prefixEqual ) {
}
t . Fatalf ( "got %v, want %v" , rc . Routes ( ) , routes )
mak . Set ( & a . domains , "example.com" , [ ] netip . Addr { netip . MustParseAddr ( "192.0.2.1" ) } )
rc . SetRoutes ( [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.1/32" ) } )
routes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.2.0/24" ) }
a . updateRoutes ( routes )
if ! slices . EqualFunc ( routes , rc . Routes ( ) , prefixEqual ) {
t . Fatalf ( "got %v, want %v" , rc . Routes ( ) , routes )
}
}
}
}
}
func TestDomainRoutes ( t * testing . T ) {
func TestDomainRoutes ( t * testing . T ) {
rc := & appctest . RouteCollector { }
for _ , shouldStore := range [ ] bool { false , true } {
a := NewAppConnector ( t . Logf , rc )
rc := & appctest . RouteCollector { }
a . updateDomains ( [ ] string { "example.com" } )
var a * AppConnector
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
if shouldStore {
a . Wait ( context . Background ( ) )
a = NewAppConnector ( t . Logf , rc , & RouteInfo { } , fakeStoreRoutes )
} else {
want := map [ string ] [ ] netip . Addr {
a = NewAppConnector ( t . Logf , rc , nil , nil )
"example.com" : { netip . MustParseAddr ( "192.0.0.8" ) } ,
}
}
a . updateDomains ( [ ] string { "example.com" } )
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
a . Wait ( context . Background ( ) )
want := map [ string ] [ ] netip . Addr {
"example.com" : { netip . MustParseAddr ( "192.0.0.8" ) } ,
}
if got := a . DomainRoutes ( ) ; ! reflect . DeepEqual ( got , want ) {
if got := a . DomainRoutes ( ) ; ! reflect . DeepEqual ( got , want ) {
t . Fatalf ( "DomainRoutes: got %v, want %v" , got , want )
t . Fatalf ( "DomainRoutes: got %v, want %v" , got , want )
}
}
}
}
}
func TestObserveDNSResponse ( t * testing . T ) {
func TestObserveDNSResponse ( t * testing . T ) {
ctx := context . Background ( )
for _ , shouldStore := range [ ] bool { false , true } {
rc := & appctest . RouteCollector { }
ctx := context . Background ( )
a := NewAppConnector ( t . Logf , rc )
rc := & appctest . RouteCollector { }
var a * AppConnector
// a has no domains configured, so it should not advertise any routes
if shouldStore {
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
a = NewAppConnector ( t . Logf , rc , & RouteInfo { } , fakeStoreRoutes )
if got , want := rc . Routes ( ) , ( [ ] netip . Prefix ) ( nil ) ; ! slices . Equal ( got , want ) {
} else {
t . Errorf ( "got %v; want %v" , got , want )
a = NewAppConnector ( t . Logf , rc , nil , nil )
}
}
wantRoutes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.0.8/32" ) }
// a has no domains configured, so it should not advertise any routes
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
if got , want := rc . Routes ( ) , ( [ ] netip . Prefix ) ( nil ) ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
}
a . updateDomains ( [ ] string { "example.com" } )
wantRoutes := [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.0.8/32" ) }
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
a . Wait ( ctx )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
}
// a CNAME record chain should result in a route being added if the chain
a . updateDomains ( [ ] string { "example.com" } )
// matches a routed domain.
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.0.8" ) )
a . updateDomains ( [ ] string { "www.example.com" , "example.com" } )
a . Wait ( ctx )
a . ObserveDNSResponse ( dnsCNAMEResponse ( "192.0.0.9" , "www.example.com." , "chain.example.com." , "example.com." ) )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
a . Wait ( ctx )
t . Errorf ( "got %v; want %v" , got , want )
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "192.0.0.9/32" ) )
}
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
}
// a CNAME record chain should result in a route being added if the chain
// a CNAME record chain should result in a route being added if the chain
// even if only found in the middle of the chain
// matches a routed domain.
a . ObserveDNSResponse ( dnsCNAMEResponse ( "192.0.0.10" , "outside.example.org." , "www.example.com." , "example.org." ) )
a . updateDomains ( [ ] string { "www.example.com" , "example.com" } )
a . Wait ( ctx )
a . ObserveDNSResponse ( dnsCNAMEResponse ( "192.0.0.9" , "www.example.com." , "chain.example.com." , "example.com." ) )
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "192.0.0.10/32" ) )
a . Wait ( ctx )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "192.0.0.9/32" ) )
t . Errorf ( "got %v; want %v" , got , want )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
}
t . Errorf ( "got %v; want %v" , got , want )
}
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "2001:db8::1/128" ) )
// a CNAME record chain should result in a route being added if the chain
// even if only found in the middle of the chain
a . ObserveDNSResponse ( dnsCNAMEResponse ( "192.0.0.10" , "outside.example.org." , "www.example.com." , "example.org." ) )
a . Wait ( ctx )
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "192.0.0.10/32" ) )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
}
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "2001:db8::1" ) )
wantRoutes = append ( wantRoutes , netip . MustParsePrefix ( "2001:db8::1/128" ) )
a . Wait ( ctx )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
t . Errorf ( "got %v; want %v" , got , want )
}
// don't re-advertise routes that have already been advertised
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "2001:db8::1" ) )
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "2001:db8::1" ) )
a . Wait ( ctx )
a . Wait ( ctx )
if got , want := rc . Routes ( ) , wantRoutes ; ! slices . Equal ( got , want ) {
if ! slices . Equal ( rc . Routes ( ) , wantRoutes ) {
t . Errorf ( "got %v; want %v" , got , want )
t . Errorf ( "rc.Routes(): got %v; want %v" , rc . Routes ( ) , wantRoutes )
}
}
// don't advertise addresses that are already in a control provided route
// don't re-advertise routes that have already been advertised
pfx := netip . MustParsePrefix ( "192.0.2.0/24" )
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "2001:db8::1" ) )
a . updateRoutes ( [ ] netip . Prefix { pfx } )
a . Wait ( ctx )
wantRoutes = append ( wantRoutes , pfx )
if ! slices . Equal ( rc . Routes ( ) , wantRoutes ) {
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.2.1" ) )
t . Errorf ( "rc.Routes(): got %v; want %v" , rc . Routes ( ) , wantRoutes )
a . Wait ( ctx )
}
if ! slices . Equal ( rc . Routes ( ) , wantRoutes ) {
t . Errorf ( "rc.Routes(): got %v; want %v" , rc . Routes ( ) , wantRoutes )
// don't advertise addresses that are already in a control provided route
}
pfx := netip . MustParsePrefix ( "192.0.2.0/24" )
if ! slices . Contains ( a . domains [ "example.com" ] , netip . MustParseAddr ( "192.0.2.1" ) ) {
a . updateRoutes ( [ ] netip . Prefix { pfx } )
t . Errorf ( "missing %v from %v" , "192.0.2.1" , a . domains [ "exmaple.com" ] )
wantRoutes = append ( wantRoutes , pfx )
a . ObserveDNSResponse ( dnsResponse ( "example.com." , "192.0.2.1" ) )
a . Wait ( ctx )
if ! slices . Equal ( rc . Routes ( ) , wantRoutes ) {
t . Errorf ( "rc.Routes(): got %v; want %v" , rc . Routes ( ) , wantRoutes )
}
if ! slices . Contains ( a . domains [ "example.com" ] , netip . MustParseAddr ( "192.0.2.1" ) ) {
t . Errorf ( "missing %v from %v" , "192.0.2.1" , a . domains [ "exmaple.com" ] )
}
}
}
}
}
func TestWildcardDomains ( t * testing . T ) {
func TestWildcardDomains ( t * testing . T ) {
ctx := context . Background ( )
for _ , shouldStore := range [ ] bool { false , true } {
rc := & appctest . RouteCollector { }
ctx := context . Background ( )
a := NewAppConnector ( t . Logf , rc )
rc := & appctest . RouteCollector { }
var a * AppConnector
a . updateDomains ( [ ] string { "*.example.com" } )
if shouldStore {
a . ObserveDNSResponse ( dnsResponse ( "foo.example.com." , "192.0.0.8" ) )
a = NewAppConnector ( t . Logf , rc , & RouteInfo { } , fakeStoreRoutes )
a . Wait ( ctx )
} else {
if got , want := rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.0.8/32" ) } ; ! slices . Equal ( got , want ) {
a = NewAppConnector ( t . Logf , rc , nil , nil )
t . Errorf ( "routes: got %v; want %v" , got , want )
}
}
if got , want := a . wildcards , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "wildcards: got %v; want %v" , got , want )
}
a . updateDomains ( [ ] string { "*.example.com" , "example.com" } )
a . updateDomains ( [ ] string { "*.example.com" } )
if _ , ok := a . domains [ "foo.example.com" ] ; ! ok {
a . ObserveDNSResponse ( dnsResponse ( "foo.example.com." , "192.0.0.8" ) )
t . Errorf ( "expected foo.example.com to be preserved in domains due to wildcard" )
a . Wait ( ctx )
}
if got , want := rc . Routes ( ) , [ ] netip . Prefix { netip . MustParsePrefix ( "192.0.0.8/32" ) } ; ! slices . Equal ( got , want ) {
if got , want := a . wildcards , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "routes: got %v; want %v" , got , want )
t . Errorf ( "wildcards: got %v; want %v" , got , want )
}
}
if got , want := a . wildcards , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "wildcards: got %v; want %v" , got , want )
}
a . updateDomains ( [ ] string { "*.example.com" , "example.com" } )
if _ , ok := a . domains [ "foo.example.com" ] ; ! ok {
t . Errorf ( "expected foo.example.com to be preserved in domains due to wildcard" )
}
if got , want := a . wildcards , [ ] string { "example.com" } ; ! slices . Equal ( got , want ) {
t . Errorf ( "wildcards: got %v; want %v" , got , want )
}
// There was an early regression where the wildcard domain was added repeatedly, this guards against that.
// There was an early regression where the wildcard domain was added repeatedly, this guards against that.
a . updateDomains ( [ ] string { "*.example.com" , "example.com" } )
a . updateDomains ( [ ] string { "*.example.com" , "example.com" } )
if len ( a . wildcards ) != 1 {
if len ( a . wildcards ) != 1 {
t . Errorf ( "expected only one wildcard domain, got %v" , a . wildcards )
t . Errorf ( "expected only one wildcard domain, got %v" , a . wildcards )
}
}
}
}
}