appc,ipn/ipnlocal,net/dns/resolver: add App Connector wiring when enabled in prefs

An EmbeddedAppConnector is added that when configured observes DNS
responses from the PeerAPI. If a response is found matching a configured
domain, routes are advertised when necessary.

The wiring from a configuration in the netmap capmap is not yet done, so
while the connector can be enabled, no domains can yet be added.

Updates tailscale/corp#15437

Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
James Tucker
2023-10-27 14:20:10 -07:00
committed by James Tucker
parent e7482f0df0
commit b48b7d82d0
9 changed files with 518 additions and 15 deletions

View File

@@ -10,10 +10,13 @@ import (
"net/http"
"net/netip"
"reflect"
"slices"
"testing"
"time"
"go4.org/netipx"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
@@ -30,6 +33,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/util/set"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
@@ -1142,6 +1146,47 @@ 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.NewEmbeddedAppConnector(t.Logf, nil)
if !b.OfferingAppConnector() {
t.Fatal("unexpected not offering app connector")
}
}
func TestRouteAdvertiser(t *testing.T) {
b := newTestBackend(t)
testPrefix := netip.MustParsePrefix("192.0.0.8/32")
ra := appc.RouteAdvertiser(b)
must.Do(ra.AdvertiseRoute(testPrefix))
routes := b.Prefs().AdvertiseRoutes()
if routes.Len() != 1 || routes.At(0) != testPrefix {
t.Fatalf("got routes %v, want %v", routes, []netip.Prefix{testPrefix})
}
}
func TestObserveDNSResponse(t *testing.T) {
b := newTestBackend(t)
// ensure no error when no app connector is configured
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
rc := &routeCollector{}
b.appConnector = appc.NewEmbeddedAppConnector(t.Logf, rc)
b.appConnector.UpdateDomains([]string{"example.com"})
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
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)
}
}
func resolversEqual(t *testing.T, a, b []*dnstype.Resolver) bool {
if a == nil && b == nil {
return true
@@ -1176,3 +1221,50 @@ func routesEqual(t *testing.T, a, b map[dnsname.FQDN][]*dnstype.Resolver) bool {
}
return true
}
// 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())
}
// routeCollector is a test helper that collects the list of routes advertised
type routeCollector struct {
routes []netip.Prefix
}
func (rc *routeCollector) AdvertiseRoute(pfx netip.Prefix) error {
rc.routes = append(rc.routes, pfx)
return nil
}