mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-27 10:47:35 +00:00

Before the fix in 7eb8a77ac80b78a2917f0a78a6f4a9189d739797 we were writing route info on every DNS request. Since that fix we have only been writing when we advertise new routes. This means we haven't been writing domain->IP Address associations if the IP Address is covered by a control route. We do want to persist the info associating the domain and IP Address so make sure we also store routes in that case. Fixes #12673 Signed-off-by: Fran Bull <fran@tailscale.com>
604 lines
20 KiB
Go
604 lines
20 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package appc
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
xmaps "golang.org/x/exp/maps"
|
|
"golang.org/x/net/dns/dnsmessage"
|
|
"tailscale.com/appc/appctest"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/util/mak"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
func fakeStoreRoutes(*RouteInfo) error { return nil }
|
|
|
|
func TestUpdateDomains(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, &appctest.RouteCollector{}, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, &appctest.RouteCollector{}, nil, nil)
|
|
}
|
|
a.UpdateDomains([]string{"example.com"})
|
|
|
|
a.Wait(ctx)
|
|
if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) {
|
|
t.Errorf("got %v; want %v", got, want)
|
|
}
|
|
|
|
addr := netip.MustParseAddr("192.0.0.8")
|
|
a.domains["example.com"] = append(a.domains["example.com"], addr)
|
|
a.UpdateDomains([]string{"example.com"})
|
|
a.Wait(ctx)
|
|
|
|
if got, want := a.domains["example.com"], []netip.Addr{addr}; !slices.Equal(got, want) {
|
|
t.Errorf("got %v; want %v", got, want)
|
|
}
|
|
|
|
// domains are explicitly downcased on set.
|
|
a.UpdateDomains([]string{"UP.EXAMPLE.COM"})
|
|
a.Wait(ctx)
|
|
if got, want := xmaps.Keys(a.domains), []string{"up.example.com"}; !slices.Equal(got, want) {
|
|
t.Errorf("got %v; want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateRoutes(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
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
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.0.2.1"))
|
|
a.Wait(ctx)
|
|
|
|
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")})
|
|
}
|
|
|
|
// This route should not be collapsed or removed
|
|
a.ObserveDNSResponse(dnsResponse("b.example.com.", "192.0.0.1"))
|
|
a.Wait(ctx)
|
|
|
|
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
|
|
a.updateRoutes(routes)
|
|
|
|
slices.SortFunc(rc.Routes(), prefixCompare)
|
|
rc.SetRoutes(slices.Compact(rc.Routes()))
|
|
slices.SortFunc(routes, prefixCompare)
|
|
|
|
// Ensure that the non-matching /32 is preserved, even though it's in the domains table.
|
|
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
|
|
t.Errorf("added routes: got %v, want %v", rc.Routes(), routes)
|
|
}
|
|
|
|
// Ensure that the contained /32 is removed, replaced by the /24.
|
|
wantRemoved := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}
|
|
if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) {
|
|
t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
rc := &appctest.RouteCollector{}
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, rc, nil, nil)
|
|
}
|
|
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) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
rc := &appctest.RouteCollector{}
|
|
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"})
|
|
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) {
|
|
t.Fatalf("DomainRoutes: got %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestObserveDNSResponse(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, rc, nil, nil)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
|
|
|
|
a.updateDomains([]string{"example.com"})
|
|
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
|
|
// matches a routed domain.
|
|
a.updateDomains([]string{"www.example.com", "example.com"})
|
|
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com."))
|
|
a.Wait(ctx)
|
|
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
|
|
// 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)
|
|
}
|
|
|
|
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
|
|
|
|
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
|
|
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.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")
|
|
a.updateRoutes([]netip.Prefix{pfx})
|
|
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) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
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"})
|
|
a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8"))
|
|
a.Wait(ctx)
|
|
if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
|
|
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"})
|
|
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.
|
|
a.updateDomains([]string{"*.example.com", "example.com"})
|
|
if len(a.wildcards) != 1 {
|
|
t.Errorf("expected only one wildcard domain, got %v", a.wildcards)
|
|
}
|
|
}
|
|
}
|
|
|
|
// dnsResponse is a test helper that creates a DNS response buffer for the given domain and address
|
|
func dnsResponse(domain, address string) []byte {
|
|
addr := netip.MustParseAddr(address)
|
|
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
|
|
b.EnableCompression()
|
|
b.StartAnswers()
|
|
switch addr.BitLen() {
|
|
case 32:
|
|
b.AResource(
|
|
dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName(domain),
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
TTL: 0,
|
|
},
|
|
dnsmessage.AResource{
|
|
A: addr.As4(),
|
|
},
|
|
)
|
|
case 128:
|
|
b.AAAAResource(
|
|
dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName(domain),
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
TTL: 0,
|
|
},
|
|
dnsmessage.AAAAResource{
|
|
AAAA: addr.As16(),
|
|
},
|
|
)
|
|
default:
|
|
panic("invalid address length")
|
|
}
|
|
return must.Get(b.Finish())
|
|
}
|
|
|
|
func dnsCNAMEResponse(address string, domains ...string) []byte {
|
|
addr := netip.MustParseAddr(address)
|
|
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
|
|
b.EnableCompression()
|
|
b.StartAnswers()
|
|
|
|
if len(domains) >= 2 {
|
|
for i, domain := range domains[:len(domains)-1] {
|
|
b.CNAMEResource(
|
|
dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName(domain),
|
|
Type: dnsmessage.TypeCNAME,
|
|
Class: dnsmessage.ClassINET,
|
|
TTL: 0,
|
|
},
|
|
dnsmessage.CNAMEResource{
|
|
CNAME: dnsmessage.MustNewName(domains[i+1]),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
domain := domains[len(domains)-1]
|
|
|
|
switch addr.BitLen() {
|
|
case 32:
|
|
b.AResource(
|
|
dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName(domain),
|
|
Type: dnsmessage.TypeA,
|
|
Class: dnsmessage.ClassINET,
|
|
TTL: 0,
|
|
},
|
|
dnsmessage.AResource{
|
|
A: addr.As4(),
|
|
},
|
|
)
|
|
case 128:
|
|
b.AAAAResource(
|
|
dnsmessage.ResourceHeader{
|
|
Name: dnsmessage.MustNewName(domain),
|
|
Type: dnsmessage.TypeAAAA,
|
|
Class: dnsmessage.ClassINET,
|
|
TTL: 0,
|
|
},
|
|
dnsmessage.AAAAResource{
|
|
AAAA: addr.As16(),
|
|
},
|
|
)
|
|
default:
|
|
panic("invalid address length")
|
|
}
|
|
return must.Get(b.Finish())
|
|
}
|
|
|
|
func prefixEqual(a, b netip.Prefix) bool {
|
|
return a == b
|
|
}
|
|
|
|
func prefixCompare(a, b netip.Prefix) int {
|
|
if a.Addr().Compare(b.Addr()) == 0 {
|
|
return a.Bits() - b.Bits()
|
|
}
|
|
return a.Addr().Compare(b.Addr())
|
|
}
|
|
|
|
func prefixes(in ...string) []netip.Prefix {
|
|
toRet := make([]netip.Prefix, len(in))
|
|
for i, s := range in {
|
|
toRet[i] = netip.MustParsePrefix(s)
|
|
}
|
|
return toRet
|
|
}
|
|
|
|
func TestUpdateRouteRouteRemoval(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
|
|
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
|
|
if !slices.Equal(routes, rc.Routes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
|
|
}
|
|
if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
|
|
}
|
|
}
|
|
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, rc, nil, nil)
|
|
}
|
|
// nothing has yet been advertised
|
|
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
|
|
|
|
a.UpdateDomainsAndRoutes([]string{}, prefixes("1.2.3.1/32", "1.2.3.2/32"))
|
|
a.Wait(ctx)
|
|
// the routes passed to UpdateDomainsAndRoutes have been advertised
|
|
assertRoutes("simple update", prefixes("1.2.3.1/32", "1.2.3.2/32"), []netip.Prefix{})
|
|
|
|
// one route the same, one different
|
|
a.UpdateDomainsAndRoutes([]string{}, prefixes("1.2.3.1/32", "1.2.3.3/32"))
|
|
a.Wait(ctx)
|
|
// old behavior: routes are not removed, resulting routes are both old and new
|
|
// (we have dupe 1.2.3.1 routes because the test RouteAdvertiser doesn't have the deduplication
|
|
// the real one does)
|
|
wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.1/32", "1.2.3.3/32")
|
|
wantRemovedRoutes := []netip.Prefix{}
|
|
if shouldStore {
|
|
// new behavior: routes are removed, resulting routes are new only
|
|
wantRoutes = prefixes("1.2.3.1/32", "1.2.3.1/32", "1.2.3.3/32")
|
|
wantRemovedRoutes = prefixes("1.2.3.2/32")
|
|
}
|
|
assertRoutes("removal", wantRoutes, wantRemovedRoutes)
|
|
}
|
|
}
|
|
|
|
func TestUpdateDomainRouteRemoval(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
|
|
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
|
|
if !slices.Equal(routes, rc.Routes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
|
|
}
|
|
if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
|
|
}
|
|
}
|
|
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, rc, nil, nil)
|
|
}
|
|
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
|
|
|
|
a.UpdateDomainsAndRoutes([]string{"a.example.com", "b.example.com"}, []netip.Prefix{})
|
|
a.Wait(ctx)
|
|
// adding domains doesn't immediately cause any routes to be advertised
|
|
assertRoutes("update domains", []netip.Prefix{}, []netip.Prefix{})
|
|
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "1.2.3.1"))
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "1.2.3.2"))
|
|
a.ObserveDNSResponse(dnsResponse("b.example.com.", "1.2.3.3"))
|
|
a.ObserveDNSResponse(dnsResponse("b.example.com.", "1.2.3.4"))
|
|
a.Wait(ctx)
|
|
// observing dns responses causes routes to be advertised
|
|
assertRoutes("observed dns", prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32"), []netip.Prefix{})
|
|
|
|
a.UpdateDomainsAndRoutes([]string{"a.example.com"}, []netip.Prefix{})
|
|
a.Wait(ctx)
|
|
// old behavior, routes are not removed
|
|
wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32")
|
|
wantRemovedRoutes := []netip.Prefix{}
|
|
if shouldStore {
|
|
// new behavior, routes are removed for b.example.com
|
|
wantRoutes = prefixes("1.2.3.1/32", "1.2.3.2/32")
|
|
wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
|
|
}
|
|
assertRoutes("removal", wantRoutes, wantRemovedRoutes)
|
|
}
|
|
}
|
|
|
|
func TestUpdateWildcardRouteRemoval(t *testing.T) {
|
|
for _, shouldStore := range []bool{false, true} {
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
|
|
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
|
|
if !slices.Equal(routes, rc.Routes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
|
|
}
|
|
if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
|
|
t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
|
|
}
|
|
}
|
|
|
|
var a *AppConnector
|
|
if shouldStore {
|
|
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
|
|
} else {
|
|
a = NewAppConnector(t.Logf, rc, nil, nil)
|
|
}
|
|
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
|
|
|
|
a.UpdateDomainsAndRoutes([]string{"a.example.com", "*.b.example.com"}, []netip.Prefix{})
|
|
a.Wait(ctx)
|
|
// adding domains doesn't immediately cause any routes to be advertised
|
|
assertRoutes("update domains", []netip.Prefix{}, []netip.Prefix{})
|
|
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "1.2.3.1"))
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "1.2.3.2"))
|
|
a.ObserveDNSResponse(dnsResponse("1.b.example.com.", "1.2.3.3"))
|
|
a.ObserveDNSResponse(dnsResponse("2.b.example.com.", "1.2.3.4"))
|
|
a.Wait(ctx)
|
|
// observing dns responses causes routes to be advertised
|
|
assertRoutes("observed dns", prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32"), []netip.Prefix{})
|
|
|
|
a.UpdateDomainsAndRoutes([]string{"a.example.com"}, []netip.Prefix{})
|
|
a.Wait(ctx)
|
|
// old behavior, routes are not removed
|
|
wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32")
|
|
wantRemovedRoutes := []netip.Prefix{}
|
|
if shouldStore {
|
|
// new behavior, routes are removed for *.b.example.com
|
|
wantRoutes = prefixes("1.2.3.1/32", "1.2.3.2/32")
|
|
wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
|
|
}
|
|
assertRoutes("removal", wantRoutes, wantRemovedRoutes)
|
|
}
|
|
}
|
|
|
|
func TestRoutesWithout(t *testing.T) {
|
|
assert := func(msg string, got, want []netip.Prefix) {
|
|
if !slices.Equal(want, got) {
|
|
t.Errorf("%s: want %v, got %v", msg, want, got)
|
|
}
|
|
}
|
|
|
|
assert("empty routes", routesWithout([]netip.Prefix{}, []netip.Prefix{}), []netip.Prefix{})
|
|
assert("a empty", routesWithout([]netip.Prefix{}, prefixes("1.1.1.1/32", "1.1.1.2/32")), []netip.Prefix{})
|
|
assert("b empty", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), []netip.Prefix{}), prefixes("1.1.1.1/32", "1.1.1.2/32"))
|
|
assert("no overlap", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), prefixes("1.1.1.3/32", "1.1.1.4/32")), prefixes("1.1.1.1/32", "1.1.1.2/32"))
|
|
assert("a has fewer", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32")), []netip.Prefix{})
|
|
assert("a has more", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32"), prefixes("1.1.1.1/32", "1.1.1.3/32")), prefixes("1.1.1.2/32", "1.1.1.4/32"))
|
|
}
|
|
|
|
func TestRateLogger(t *testing.T) {
|
|
clock := tstest.Clock{}
|
|
wasCalled := false
|
|
rl := newRateLogger(func() time.Time { return clock.Now() }, 1*time.Second, func(count int64, _ time.Time, _ int64) {
|
|
if count != 3 {
|
|
t.Fatalf("count for prev period: got %d, want 3", count)
|
|
}
|
|
wasCalled = true
|
|
})
|
|
|
|
for i := 0; i < 3; i++ {
|
|
clock.Advance(1 * time.Millisecond)
|
|
rl.update(0)
|
|
if wasCalled {
|
|
t.Fatalf("wasCalled: got true, want false")
|
|
}
|
|
}
|
|
|
|
clock.Advance(1 * time.Second)
|
|
rl.update(0)
|
|
if !wasCalled {
|
|
t.Fatalf("wasCalled: got false, want true")
|
|
}
|
|
|
|
wasCalled = false
|
|
rl = newRateLogger(func() time.Time { return clock.Now() }, 1*time.Hour, func(count int64, _ time.Time, _ int64) {
|
|
if count != 3 {
|
|
t.Fatalf("count for prev period: got %d, want 3", count)
|
|
}
|
|
wasCalled = true
|
|
})
|
|
|
|
for i := 0; i < 3; i++ {
|
|
clock.Advance(1 * time.Minute)
|
|
rl.update(0)
|
|
if wasCalled {
|
|
t.Fatalf("wasCalled: got true, want false")
|
|
}
|
|
}
|
|
|
|
clock.Advance(1 * time.Hour)
|
|
rl.update(0)
|
|
if !wasCalled {
|
|
t.Fatalf("wasCalled: got false, want true")
|
|
}
|
|
}
|
|
|
|
func TestWriteRoutesCoveredByControlRoutes(t *testing.T) {
|
|
var writeCount int
|
|
write := func(*RouteInfo) error {
|
|
writeCount++
|
|
return nil
|
|
}
|
|
// even if there are routes that cover a domain's ip, if we haven't see the domain before we should write
|
|
ctx := context.Background()
|
|
rc := &appctest.RouteCollector{}
|
|
a := NewAppConnector(t.Logf, rc, &RouteInfo{}, write)
|
|
a.UpdateDomainsAndRoutes([]string{"*.example.com"}, []netip.Prefix{netip.MustParsePrefix("192.1.1.0/31")})
|
|
a.Wait(ctx)
|
|
// now we have an app connector that is wanting to learn routes for *.example.com, and is preconfigured with
|
|
// a route range, when it observes a dns response within the route range it should not publish a new route, but
|
|
// it should write it's RouteInfo, so that it remembers the domain<->ip addr association.
|
|
writeCount = 0
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.1.1.1"))
|
|
a.Wait(ctx)
|
|
want := 1
|
|
if writeCount != want {
|
|
t.Fatalf("writeCount new ip: got %d, want %d", writeCount, want)
|
|
}
|
|
// we should NOT write, if we are observing the same ip address again
|
|
writeCount = 0
|
|
a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.1.1.1"))
|
|
a.Wait(ctx)
|
|
want = 0
|
|
if writeCount != want {
|
|
t.Fatalf("writeCount old ip: got %d, want %d", writeCount, want)
|
|
}
|
|
}
|