Add a control knob to toggle writing RouteInfo to StateStore

To control the toggle in dev you can a) add a go workspace so that
control is using the new tailcfg from this commit and b) add the feature
flag to control.
This commit is contained in:
Fran Bull 2024-03-29 14:28:35 -07:00 committed by Kevin Liang
parent 43fbc0d588
commit c9eb5798c5
7 changed files with 370 additions and 322 deletions

View File

@ -69,13 +69,21 @@ type AppConnector struct {
// queue provides ordering for update operations // queue provides ordering for update operations
queue execqueue.ExecQueue queue execqueue.ExecQueue
// whether this tailscaled should persist routes. Storing RouteInfo enables the app connector
// to forget routes when appropriate and should make routes smaller. While we are verifying that
// writing the RouteInfo to StateStore is a good solution (and doesn't for example cause writes
// that are too frequent or too large) use a controlknob to manage this flag.
ShouldStoreRoutes bool
} }
// NewAppConnector creates a new AppConnector. // NewAppConnector creates a new AppConnector.
func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser) *AppConnector { func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, shouldStoreRoutes bool) *AppConnector {
// TODO(fran) if !shouldStoreRoutes we probably want to try and clean up any stored routes
return &AppConnector{ return &AppConnector{
logf: logger.WithPrefix(logf, "appc: "), logf: logger.WithPrefix(logf, "appc: "),
routeAdvertiser: routeAdvertiser, routeAdvertiser: routeAdvertiser,
ShouldStoreRoutes: shouldStoreRoutes,
} }
} }

View File

@ -18,193 +18,205 @@ import (
) )
func TestUpdateDomains(t *testing.T) { func TestUpdateDomains(t *testing.T) {
ctx := context.Background() for _, shouldStore := range []bool{true, false} {
a := NewAppConnector(t.Logf, nil) ctx := context.Background()
a.UpdateDomains([]string{"example.com"}) a := NewAppConnector(t.Logf, nil, shouldStore)
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{true, false} {
rc := &appctest.RouteCollector{} ctx := context.Background()
a := NewAppConnector(t.Logf, rc) rc := &appctest.RouteCollector{}
a.updateDomains([]string{"*.example.com"}) a := NewAppConnector(t.Logf, rc, shouldStore)
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{true, false} {
a := NewAppConnector(t.Logf, rc) rc := &appctest.RouteCollector{}
mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")}) a := NewAppConnector(t.Logf, rc, shouldStore)
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}) mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")} rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
a.updateRoutes(routes) routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
a.updateRoutes(routes)
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) { if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes) 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{true, false} {
a := NewAppConnector(t.Logf, rc) rc := &appctest.RouteCollector{}
a.updateDomains([]string{"example.com"}) a := NewAppConnector(t.Logf, rc, shouldStore)
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")) a.updateDomains([]string{"example.com"})
a.Wait(context.Background()) a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
a.Wait(context.Background())
want := map[string][]netip.Addr{ want := map[string][]netip.Addr{
"example.com": {netip.MustParseAddr("192.0.0.8")}, "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{true, false} {
rc := &appctest.RouteCollector{} ctx := context.Background()
a := NewAppConnector(t.Logf, rc) rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc, shouldStore)
// a has no domains configured, so it should not advertise any routes // a has no domains configured, so it should not advertise any routes
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")) a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
if got, want := rc.Routes(), ([]netip.Prefix)(nil); !slices.Equal(got, want) { if got, want := rc.Routes(), ([]netip.Prefix)(nil); !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want) t.Errorf("got %v; want %v", got, want)
} }
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")} wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
a.updateDomains([]string{"example.com"}) a.updateDomains([]string{"example.com"})
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")) a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
a.Wait(ctx) a.Wait(ctx)
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) { if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", 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
// matches a routed domain. // matches a routed domain.
a.updateDomains([]string{"www.example.com", "example.com"}) a.updateDomains([]string{"www.example.com", "example.com"})
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com.")) a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com."))
a.Wait(ctx) a.Wait(ctx)
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.9/32")) wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.9/32"))
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) { if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", 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 // 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.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.10", "outside.example.org.", "www.example.com.", "example.org."))
a.Wait(ctx) a.Wait(ctx)
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.10/32")) wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.10/32"))
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) { if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want) t.Errorf("got %v; want %v", got, want)
} }
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128")) wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
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 got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want) t.Errorf("got %v; want %v", got, want)
} }
// don't re-advertise routes that have already been advertised // 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 !slices.Equal(rc.Routes(), wantRoutes) { if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", 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 // don't advertise addresses that are already in a control provided route
pfx := netip.MustParsePrefix("192.0.2.0/24") pfx := netip.MustParsePrefix("192.0.2.0/24")
a.updateRoutes([]netip.Prefix{pfx}) a.updateRoutes([]netip.Prefix{pfx})
wantRoutes = append(wantRoutes, pfx) wantRoutes = append(wantRoutes, pfx)
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1")) a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1"))
a.Wait(ctx) a.Wait(ctx)
if !slices.Equal(rc.Routes(), wantRoutes) { if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", 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")) { 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"]) 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{true, false} {
rc := &appctest.RouteCollector{} ctx := context.Background()
a := NewAppConnector(t.Logf, rc) rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc, shouldStore)
a.updateDomains([]string{"*.example.com"}) a.updateDomains([]string{"*.example.com"})
a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8")) a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8"))
a.Wait(ctx) a.Wait(ctx)
if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) { 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) t.Errorf("routes: got %v; want %v", got, want)
} }
if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) { if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("wildcards: got %v; want %v", got, want) t.Errorf("wildcards: got %v; want %v", got, want)
} }
a.updateDomains([]string{"*.example.com", "example.com"}) a.updateDomains([]string{"*.example.com", "example.com"})
if _, ok := a.domains["foo.example.com"]; !ok { if _, ok := a.domains["foo.example.com"]; !ok {
t.Errorf("expected foo.example.com to be preserved in domains due to wildcard") 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) { if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("wildcards: got %v; want %v", 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)
}
} }
} }

View File

@ -72,6 +72,10 @@ type Knobs struct {
// ProbeUDPLifetime is whether the node should probe UDP path lifetime on // ProbeUDPLifetime is whether the node should probe UDP path lifetime on
// the tail end of an active direct connection in magicsock. // the tail end of an active direct connection in magicsock.
ProbeUDPLifetime atomic.Bool ProbeUDPLifetime atomic.Bool
// AppCStoreRoutes is whether the node should store RouteInfo to StateStore
// if it's an app connector.
AppCStoreRoutes atomic.Bool
} }
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@ -96,6 +100,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables) forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables)
seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal) seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal)
probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime) probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime)
appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes)
) )
if has(tailcfg.NodeAttrOneCGNATEnable) { if has(tailcfg.NodeAttrOneCGNATEnable) {
@ -118,6 +123,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
k.LinuxForceNfTables.Store(forceNfTables) k.LinuxForceNfTables.Store(forceNfTables)
k.SeamlessKeyRenewal.Store(seamlessKeyRenewal) k.SeamlessKeyRenewal.Store(seamlessKeyRenewal)
k.ProbeUDPLifetime.Store(probeUDPLifetime) k.ProbeUDPLifetime.Store(probeUDPLifetime)
k.AppCStoreRoutes.Store(appCStoreRoutes)
} }
// AsDebugJSON returns k as something that can be marshalled with json.Marshal // AsDebugJSON returns k as something that can be marshalled with json.Marshal
@ -141,5 +147,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
"LinuxForceNfTables": k.LinuxForceNfTables.Load(), "LinuxForceNfTables": k.LinuxForceNfTables.Load(),
"SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(), "SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(),
"ProbeUDPLifetime": k.ProbeUDPLifetime.Load(), "ProbeUDPLifetime": k.ProbeUDPLifetime.Load(),
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
} }
} }

View File

@ -3561,8 +3561,14 @@ func (b *LocalBackend) reconfigAppConnectorLocked(nm *netmap.NetworkMap, prefs i
return return
} }
if b.appConnector == nil { shouldAppCStoreRoutesHasChanged := false
b.appConnector = appc.NewAppConnector(b.logf, b) shouldAppCStoreRoutes := b.ControlKnobs().AppCStoreRoutes.Load()
if b.appConnector != nil {
shouldAppCStoreRoutesHasChanged = b.appConnector.ShouldStoreRoutes != shouldAppCStoreRoutes
}
if b.appConnector == nil || shouldAppCStoreRoutesHasChanged {
b.appConnector = appc.NewAppConnector(b.logf, b, shouldAppCStoreRoutes)
} }
if nm == nil { if nm == nil {
return return

View File

@ -1253,13 +1253,15 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
} }
func TestOfferingAppConnector(t *testing.T) { func TestOfferingAppConnector(t *testing.T) {
b := newTestBackend(t) for _, shouldStore := range []bool{true, false} {
if b.OfferingAppConnector() { b := newTestBackend(t)
t.Fatal("unexpected offering app connector") if b.OfferingAppConnector() {
} t.Fatal("unexpected offering app connector")
b.appConnector = appc.NewAppConnector(t.Logf, nil) }
if !b.OfferingAppConnector() { b.appConnector = appc.NewAppConnector(t.Logf, nil, shouldStore)
t.Fatal("unexpected not offering app connector") if !b.OfferingAppConnector() {
t.Fatal("unexpected not offering app connector")
}
} }
} }
@ -1304,21 +1306,23 @@ func TestRouterAdvertiserIgnoresContainedRoutes(t *testing.T) {
} }
func TestObserveDNSResponse(t *testing.T) { func TestObserveDNSResponse(t *testing.T) {
b := newTestBackend(t) for _, shouldStore := range []bool{true, false} {
b := newTestBackend(t)
// ensure no error when no app connector is configured // ensure no error when no app connector is configured
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")) b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
b.appConnector = appc.NewAppConnector(t.Logf, rc) b.appConnector = appc.NewAppConnector(t.Logf, rc, shouldStore)
b.appConnector.UpdateDomains([]string{"example.com"}) b.appConnector.UpdateDomains([]string{"example.com"})
b.appConnector.Wait(context.Background()) b.appConnector.Wait(context.Background())
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")) b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
b.appConnector.Wait(context.Background()) b.appConnector.Wait(context.Background())
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")} wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.Routes(), wantRoutes) { if !slices.Equal(rc.Routes(), wantRoutes) {
t.Fatalf("got routes %v, want %v", rc.Routes(), wantRoutes) t.Fatalf("got routes %v, want %v", rc.Routes(), wantRoutes)
}
} }
} }

View File

@ -687,185 +687,191 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
} }
func TestPeerAPIPrettyReplyCNAME(t *testing.T) { func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
var h peerAPIHandler for _, shouldStore := range []bool{true, false} {
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0) eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
h.ps = &peerAPIServer{ h.ps = &peerAPIServer{
b: &LocalBackend{ b: &LocalBackend{
e: eng, e: eng,
pm: pm, pm: pm,
store: pm.Store(), store: pm.Store(),
// configure as an app connector just to enable the API. // configure as an app connector just to enable the API.
appConnector: appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}), appConnector: appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}, shouldStore),
}, },
} }
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) { h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.CNAMEResource( b.CNAMEResource(
dnsmessage.ResourceHeader{ dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("www.example.com."), Name: dnsmessage.MustNewName("www.example.com."),
Type: dnsmessage.TypeCNAME, Type: dnsmessage.TypeCNAME,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
TTL: 0, TTL: 0,
}, },
dnsmessage.CNAMEResource{ dnsmessage.CNAMEResource{
CNAME: dnsmessage.MustNewName("example.com."), CNAME: dnsmessage.MustNewName("example.com."),
}, },
) )
b.AResource( b.AResource(
dnsmessage.ResourceHeader{ dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."), Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA, Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
TTL: 0, TTL: 0,
}, },
dnsmessage.AResource{ dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8}, A: [4]byte{192, 0, 0, 8},
}, },
) )
}} }}
f := filter.NewAllowAllForTest(logger.Discard) f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f) h.ps.b.setFilter(f)
if !h.replyToDNSQueries() { if !h.replyToDNSQueries() {
t.Errorf("unexpectedly deny; wanted to be a DNS server") t.Errorf("unexpectedly deny; wanted to be a DNS server")
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil)) h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code) t.Errorf("unexpected status code: %v", w.Code)
} }
var addrs []string var addrs []string
json.NewDecoder(w.Body).Decode(&addrs) json.NewDecoder(w.Body).Decode(&addrs)
if len(addrs) == 0 { if len(addrs) == 0 {
t.Fatalf("no addresses returned") t.Fatalf("no addresses returned")
} }
for _, addr := range addrs { for _, addr := range addrs {
netip.MustParseAddr(addr) netip.MustParseAddr(addr)
}
} }
} }
func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) { func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
ctx := context.Background() for _, shouldStore := range []bool{true, false} {
var h peerAPIHandler ctx := context.Background()
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0) eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
h.ps = &peerAPIServer{ h.ps = &peerAPIServer{
b: &LocalBackend{ b: &LocalBackend{
e: eng, e: eng,
pm: pm, pm: pm,
store: pm.Store(), store: pm.Store(),
appConnector: appc.NewAppConnector(t.Logf, rc), appConnector: appc.NewAppConnector(t.Logf, rc, shouldStore),
},
}
h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
h.ps.b.appConnector.Wait(ctx)
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
}, },
dnsmessage.AResource{ }
A: [4]byte{192, 0, 0, 8}, h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
}, h.ps.b.appConnector.Wait(ctx)
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.ps.b.OfferingAppConnector() { h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
t.Fatal("expecting to be offering app connector") b.AResource(
} dnsmessage.ResourceHeader{
if !h.replyToDNSQueries() { Name: dnsmessage.MustNewName("example.com."),
t.Errorf("unexpectedly deny; wanted to be a DNS server") Type: dnsmessage.TypeA,
} Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
w := httptest.NewRecorder() if !h.ps.b.OfferingAppConnector() {
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil)) t.Fatal("expecting to be offering app connector")
if w.Code != http.StatusOK { }
t.Errorf("unexpected status code: %v", w.Code) if !h.replyToDNSQueries() {
} t.Errorf("unexpectedly deny; wanted to be a DNS server")
h.ps.b.appConnector.Wait(ctx) }
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")} w := httptest.NewRecorder()
if !slices.Equal(rc.Routes(), wantRoutes) { h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes) if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
h.ps.b.appConnector.Wait(ctx)
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
}
} }
} }
func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) { func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
ctx := context.Background() for _, shouldStore := range []bool{true, false} {
var h peerAPIHandler ctx := context.Background()
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345") var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0) eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf)) pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
h.ps = &peerAPIServer{ h.ps = &peerAPIServer{
b: &LocalBackend{ b: &LocalBackend{
e: eng, e: eng,
pm: pm, pm: pm,
store: pm.Store(), store: pm.Store(),
appConnector: appc.NewAppConnector(t.Logf, rc), appConnector: appc.NewAppConnector(t.Logf, rc, shouldStore),
},
}
h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
h.ps.b.appConnector.Wait(ctx)
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
b.CNAMEResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("www.example.com."),
Type: dnsmessage.TypeCNAME,
Class: dnsmessage.ClassINET,
TTL: 0,
}, },
dnsmessage.CNAMEResource{ }
CNAME: dnsmessage.MustNewName("example.com."), h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
}, h.ps.b.appConnector.Wait(ctx)
)
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.ps.b.OfferingAppConnector() { h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
t.Fatal("expecting to be offering app connector") b.CNAMEResource(
} dnsmessage.ResourceHeader{
if !h.replyToDNSQueries() { Name: dnsmessage.MustNewName("www.example.com."),
t.Errorf("unexpectedly deny; wanted to be a DNS server") Type: dnsmessage.TypeCNAME,
} Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.CNAMEResource{
CNAME: dnsmessage.MustNewName("example.com."),
},
)
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: [4]byte{192, 0, 0, 8},
},
)
}}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
w := httptest.NewRecorder() if !h.ps.b.OfferingAppConnector() {
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil)) t.Fatal("expecting to be offering app connector")
if w.Code != http.StatusOK { }
t.Errorf("unexpected status code: %v", w.Code) if !h.replyToDNSQueries() {
} t.Errorf("unexpectedly deny; wanted to be a DNS server")
h.ps.b.appConnector.Wait(ctx) }
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")} w := httptest.NewRecorder()
if !slices.Equal(rc.Routes(), wantRoutes) { h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes) if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
h.ps.b.appConnector.Wait(ctx)
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
}
} }
} }

View File

@ -2232,8 +2232,13 @@ const (
// NodeAttrDisableWebClient disables using the web client. // NodeAttrDisableWebClient disables using the web client.
NodeAttrDisableWebClient NodeCapability = "disable-web-client" NodeAttrDisableWebClient NodeCapability = "disable-web-client"
<<<<<<< HEAD
// NodeAttrExitDstNetworkFlowLog enables exit node destinations in network flow logs. // NodeAttrExitDstNetworkFlowLog enables exit node destinations in network flow logs.
NodeAttrExitDstNetworkFlowLog NodeCapability = "exit-dst-network-flow-log" NodeAttrExitDstNetworkFlowLog NodeCapability = "exit-dst-network-flow-log"
=======
// NodeAttrStoreAppCRoutes enables storing app connector routes persistently.
NodeAttrStoreAppCRoutes NodeCapability = "store-appc-routes"
>>>>>>> 61f7b83bd (Add a control knob to toggle writing RouteInfo to StateStore)
) )
// SetDNSRequest is a request to add a DNS record. // SetDNSRequest is a request to add a DNS record.