mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 21:27:31 +00:00
appc: add flag shouldStoreRoutes and controlknob for it
When an app connector is reconfigured and domains to route are removed, we would like to no longer advertise routes that were discovered for those domains. In order to do this we plan to store which routes were discovered for which domains. Add a controlknob so that we can enable/disable the new behavior. Updates #11008 Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
@@ -3516,8 +3516,22 @@ func (b *LocalBackend) reconfigAppConnectorLocked(nm *netmap.NetworkMap, prefs i
|
||||
return
|
||||
}
|
||||
|
||||
if b.appConnector == nil {
|
||||
b.appConnector = appc.NewAppConnector(b.logf, b)
|
||||
shouldAppCStoreRoutes := b.ControlKnobs().AppCStoreRoutes.Load()
|
||||
if b.appConnector == nil || b.appConnector.ShouldStoreRoutes() != shouldAppCStoreRoutes {
|
||||
var ri *appc.RouteInfo
|
||||
var storeFunc func(*appc.RouteInfo) error
|
||||
if shouldAppCStoreRoutes {
|
||||
var err error
|
||||
ri, err = b.readRouteInfoLocked()
|
||||
if err != nil {
|
||||
ri = &appc.RouteInfo{}
|
||||
if err != ipn.ErrStateNotExist {
|
||||
b.logf("Unsuccessful Read RouteInfo: ", err)
|
||||
}
|
||||
}
|
||||
storeFunc = b.storeRouteInfo
|
||||
}
|
||||
b.appConnector = appc.NewAppConnector(b.logf, b, ri, storeFunc)
|
||||
}
|
||||
if nm == nil {
|
||||
return
|
||||
|
@@ -55,6 +55,8 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func fakeStoreRoutes(*appc.RouteInfo) error { return nil }
|
||||
|
||||
func inRemove(ip netip.Addr) bool {
|
||||
for _, pfx := range removeFromDefaultRoute {
|
||||
if pfx.Contains(ip) {
|
||||
@@ -1291,13 +1293,19 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOfferingAppConnector(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
if b.OfferingAppConnector() {
|
||||
t.Fatal("unexpected offering app connector")
|
||||
}
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, nil)
|
||||
if !b.OfferingAppConnector() {
|
||||
t.Fatal("unexpected not offering app connector")
|
||||
for _, shouldStore := range []bool{false, true} {
|
||||
b := newTestBackend(t)
|
||||
if b.OfferingAppConnector() {
|
||||
t.Fatal("unexpected offering app connector")
|
||||
}
|
||||
if shouldStore {
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, nil, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
} else {
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, nil, nil, nil)
|
||||
}
|
||||
if !b.OfferingAppConnector() {
|
||||
t.Fatal("unexpected not offering app connector")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,21 +1350,27 @@ func TestRouterAdvertiserIgnoresContainedRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestObserveDNSResponse(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
for _, shouldStore := range []bool{false, true} {
|
||||
b := newTestBackend(t)
|
||||
|
||||
// ensure no error when no app connector is configured
|
||||
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
// ensure no error when no app connector is configured
|
||||
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
|
||||
rc := &appctest.RouteCollector{}
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, rc)
|
||||
b.appConnector.UpdateDomains([]string{"example.com"})
|
||||
b.appConnector.Wait(context.Background())
|
||||
rc := &appctest.RouteCollector{}
|
||||
if shouldStore {
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, rc, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
} else {
|
||||
b.appConnector = appc.NewAppConnector(t.Logf, rc, nil, nil)
|
||||
}
|
||||
b.appConnector.UpdateDomains([]string{"example.com"})
|
||||
b.appConnector.Wait(context.Background())
|
||||
|
||||
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
b.appConnector.Wait(context.Background())
|
||||
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
|
||||
if !slices.Equal(rc.Routes(), wantRoutes) {
|
||||
t.Fatalf("got routes %v, want %v", rc.Routes(), wantRoutes)
|
||||
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
b.appConnector.Wait(context.Background())
|
||||
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
|
||||
if !slices.Equal(rc.Routes(), wantRoutes) {
|
||||
t.Fatalf("got routes %v, want %v", rc.Routes(), wantRoutes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -687,185 +687,209 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
for _, shouldStore := range []bool{false, true} {
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
// configure as an app connector just to enable the API.
|
||||
appConnector: appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}),
|
||||
},
|
||||
}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
} else {
|
||||
a = appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}, nil, nil)
|
||||
}
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
// configure as an app connector just to enable the API.
|
||||
appConnector: a,
|
||||
},
|
||||
}
|
||||
|
||||
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."),
|
||||
},
|
||||
)
|
||||
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)
|
||||
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."),
|
||||
},
|
||||
)
|
||||
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.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
var addrs []string
|
||||
json.NewDecoder(w.Body).Decode(&addrs)
|
||||
if len(addrs) == 0 {
|
||||
t.Fatalf("no addresses returned")
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
netip.MustParseAddr(addr)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
var addrs []string
|
||||
json.NewDecoder(w.Body).Decode(&addrs)
|
||||
if len(addrs) == 0 {
|
||||
t.Fatalf("no addresses returned")
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
netip.MustParseAddr(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
for _, shouldStore := range []bool{false, true} {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
appConnector: appc.NewAppConnector(t.Logf, rc),
|
||||
},
|
||||
}
|
||||
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,
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, rc, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
} else {
|
||||
a = appc.NewAppConnector(t.Logf, rc, nil, nil)
|
||||
}
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
appConnector: a,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: [4]byte{192, 0, 0, 8},
|
||||
},
|
||||
)
|
||||
}}
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
h.ps.b.setFilter(f)
|
||||
}
|
||||
h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
|
||||
if !h.ps.b.OfferingAppConnector() {
|
||||
t.Fatal("expecting to be offering app connector")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
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},
|
||||
},
|
||||
)
|
||||
}}
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
h.ps.b.setFilter(f)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
if !h.ps.b.OfferingAppConnector() {
|
||||
t.Fatal("expecting to be offering app connector")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
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)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
for _, shouldStore := range []bool{false, true} {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
appConnector: appc.NewAppConnector(t.Logf, rc),
|
||||
},
|
||||
}
|
||||
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,
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, rc, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
} else {
|
||||
a = appc.NewAppConnector(t.Logf, rc, nil, nil)
|
||||
}
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
appConnector: a,
|
||||
},
|
||||
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)
|
||||
}
|
||||
h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
|
||||
if !h.ps.b.OfferingAppConnector() {
|
||||
t.Fatal("expecting to be offering app connector")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
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."),
|
||||
},
|
||||
)
|
||||
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()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
if !h.ps.b.OfferingAppConnector() {
|
||||
t.Fatal("expecting to be offering app connector")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
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)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user