Skip to content

Commit b48b7d8

Browse files
committed
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>
1 parent e7482f0 commit b48b7d8

File tree

9 files changed

+518
-15
lines changed

9 files changed

+518
-15
lines changed

appc/appc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type target struct {
3131
Matching tailcfg.ProtoPortRange
3232
}
3333

34-
// Server implements an App Connector.
34+
// Server implements an App Connector as expressed in sniproxy.
3535
type Server struct {
3636
mu sync.RWMutex // mu guards following fields
3737
connectors map[appctype.ConfigID]connector

appc/embedded.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
// Package appc implements App Connectors. An AppConnector provides domain
5+
// oriented routing of traffic.
6+
package appc
7+
8+
import (
9+
"net/netip"
10+
"slices"
11+
"strings"
12+
"sync"
13+
14+
"golang.org/x/net/dns/dnsmessage"
15+
"tailscale.com/types/logger"
16+
)
17+
18+
/*
19+
* TODO(raggi): the sniproxy servicing portions of this package will be moved
20+
* into the sniproxy or deprecated at some point, when doing so is not
21+
* disruptive. At that time EmbeddedAppConnector can be renamed to AppConnector.
22+
*/
23+
24+
// RouteAdvertiser is an interface that allows the AppConnector to advertise
25+
// newly discovered routes that need to be served through the AppConnector.
26+
type RouteAdvertiser interface {
27+
// AdvertiseRoute adds a new route advertisement if the route is not already
28+
// being advertised.
29+
AdvertiseRoute(netip.Prefix) error
30+
}
31+
32+
// EmbeddedAppConnector is an implementation of an AppConnector that performs
33+
// its function as a subsystem inside of a tailscale node. At the control plane
34+
// side App Connector routing is configured in terms of domains rather than IP
35+
// addresses.
36+
// The AppConnectors responsibility inside tailscaled is to apply the routing
37+
// and domain configuration as supplied in the map response.
38+
// DNS requests for configured domains are observed. If the domains resolve to
39+
// routes not yet served by the AppConnector the local node configuration is
40+
// updated to advertise the new route.
41+
type EmbeddedAppConnector struct {
42+
logf logger.Logf
43+
routeAdvertiser RouteAdvertiser
44+
45+
// mu guards the fields that follow
46+
mu sync.Mutex
47+
// domains is a map of lower case domain names with no trailing dot, to a
48+
// list of resolved IP addresses.
49+
domains map[string][]netip.Addr
50+
}
51+
52+
// NewEmbeddedAppConnector creates a new EmbeddedAppConnector.
53+
func NewEmbeddedAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser) *EmbeddedAppConnector {
54+
return &EmbeddedAppConnector{
55+
logf: logger.WithPrefix(logf, "appc: "),
56+
routeAdvertiser: routeAdvertiser,
57+
}
58+
}
59+
60+
// UpdateDomains replaces the current set of configured domains with the
61+
// supplied set of domains. Domains must not contain a trailing dot, and should
62+
// be lower case.
63+
func (e *EmbeddedAppConnector) UpdateDomains(domains []string) {
64+
e.mu.Lock()
65+
defer e.mu.Unlock()
66+
67+
var old map[string][]netip.Addr
68+
old, e.domains = e.domains, make(map[string][]netip.Addr, len(domains))
69+
for _, d := range domains {
70+
d = strings.ToLower(d)
71+
e.domains[d] = old[d]
72+
}
73+
}
74+
75+
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
76+
// response is being returned over the PeerAPI. The response is parsed and
77+
// matched against the configured domains, if matched the routeAdvertiser is
78+
// advised to advertise the discovered route.
79+
func (e *EmbeddedAppConnector) ObserveDNSResponse(res []byte) {
80+
var p dnsmessage.Parser
81+
if _, err := p.Start(res); err != nil {
82+
return
83+
}
84+
if err := p.SkipAllQuestions(); err != nil {
85+
return
86+
}
87+
88+
for {
89+
h, err := p.AnswerHeader()
90+
if err == dnsmessage.ErrSectionDone {
91+
break
92+
}
93+
if err != nil {
94+
return
95+
}
96+
97+
if h.Class != dnsmessage.ClassINET {
98+
if err := p.SkipAnswer(); err != nil {
99+
return
100+
}
101+
continue
102+
}
103+
if h.Type != dnsmessage.TypeA && h.Type != dnsmessage.TypeAAAA {
104+
if err := p.SkipAnswer(); err != nil {
105+
return
106+
}
107+
continue
108+
}
109+
110+
domain := h.Name.String()
111+
if len(domain) == 0 {
112+
return
113+
}
114+
if domain[len(domain)-1] == '.' {
115+
domain = domain[:len(domain)-1]
116+
}
117+
domain = strings.ToLower(domain)
118+
e.logf("[v2] observed DNS response for %s", domain)
119+
120+
e.mu.Lock()
121+
addrs, ok := e.domains[domain]
122+
e.mu.Unlock()
123+
if !ok {
124+
if err := p.SkipAnswer(); err != nil {
125+
return
126+
}
127+
continue
128+
}
129+
130+
var addr netip.Addr
131+
switch h.Type {
132+
case dnsmessage.TypeA:
133+
r, err := p.AResource()
134+
if err != nil {
135+
return
136+
}
137+
addr = netip.AddrFrom4(r.A)
138+
case dnsmessage.TypeAAAA:
139+
r, err := p.AAAAResource()
140+
if err != nil {
141+
return
142+
}
143+
addr = netip.AddrFrom16(r.AAAA)
144+
default:
145+
if err := p.SkipAnswer(); err != nil {
146+
return
147+
}
148+
continue
149+
}
150+
if slices.Contains(addrs, addr) {
151+
continue
152+
}
153+
// TODO(raggi): check for existing prefixes
154+
if err := e.routeAdvertiser.AdvertiseRoute(netip.PrefixFrom(addr, addr.BitLen())); err != nil {
155+
e.logf("failed to advertise route for %v: %v", addr, err)
156+
continue
157+
}
158+
e.logf("[v2] advertised route for %v: %v", domain, addr)
159+
160+
e.mu.Lock()
161+
e.domains[domain] = append(addrs, addr)
162+
e.mu.Unlock()
163+
}
164+
165+
}

appc/embedded_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package appc
5+
6+
import (
7+
"net/netip"
8+
"slices"
9+
"testing"
10+
11+
xmaps "golang.org/x/exp/maps"
12+
"golang.org/x/net/dns/dnsmessage"
13+
"tailscale.com/util/must"
14+
)
15+
16+
func TestUpdateDomains(t *testing.T) {
17+
a := NewEmbeddedAppConnector(t.Logf, nil)
18+
a.UpdateDomains([]string{"example.com"})
19+
if got, want := xmaps.Keys(a.domains), []string{"example.com"}; !slices.Equal(got, want) {
20+
t.Errorf("got %v; want %v", got, want)
21+
}
22+
23+
addr := netip.MustParseAddr("192.0.0.8")
24+
a.domains["example.com"] = append(a.domains["example.com"], addr)
25+
a.UpdateDomains([]string{"example.com"})
26+
27+
if got, want := a.domains["example.com"], []netip.Addr{addr}; !slices.Equal(got, want) {
28+
t.Errorf("got %v; want %v", got, want)
29+
}
30+
31+
// domains are explicitly downcased on set.
32+
a.UpdateDomains([]string{"UP.EXAMPLE.COM"})
33+
if got, want := xmaps.Keys(a.domains), []string{"up.example.com"}; !slices.Equal(got, want) {
34+
t.Errorf("got %v; want %v", got, want)
35+
}
36+
}
37+
38+
func TestObserveDNSResponse(t *testing.T) {
39+
rc := &routeCollector{}
40+
a := NewEmbeddedAppConnector(t.Logf, rc)
41+
42+
// a has no domains configured, so it should not advertise any routes
43+
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
44+
if got, want := rc.routes, ([]netip.Prefix)(nil); !slices.Equal(got, want) {
45+
t.Errorf("got %v; want %v", got, want)
46+
}
47+
48+
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
49+
50+
a.UpdateDomains([]string{"example.com"})
51+
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
52+
if got, want := rc.routes, wantRoutes; !slices.Equal(got, want) {
53+
t.Errorf("got %v; want %v", got, want)
54+
}
55+
56+
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
57+
58+
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
59+
if got, want := rc.routes, wantRoutes; !slices.Equal(got, want) {
60+
t.Errorf("got %v; want %v", got, want)
61+
}
62+
63+
// don't re-advertise routes that have already been advertised
64+
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
65+
if !slices.Equal(rc.routes, wantRoutes) {
66+
t.Errorf("got %v; want %v", rc.routes, wantRoutes)
67+
}
68+
}
69+
70+
// dnsResponse is a test helper that creates a DNS response buffer for the given domain and address
71+
func dnsResponse(domain, address string) []byte {
72+
addr := netip.MustParseAddr(address)
73+
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
74+
b.EnableCompression()
75+
b.StartAnswers()
76+
switch addr.BitLen() {
77+
case 32:
78+
b.AResource(
79+
dnsmessage.ResourceHeader{
80+
Name: dnsmessage.MustNewName(domain),
81+
Type: dnsmessage.TypeA,
82+
Class: dnsmessage.ClassINET,
83+
TTL: 0,
84+
},
85+
dnsmessage.AResource{
86+
A: addr.As4(),
87+
},
88+
)
89+
case 128:
90+
b.AAAAResource(
91+
dnsmessage.ResourceHeader{
92+
Name: dnsmessage.MustNewName(domain),
93+
Type: dnsmessage.TypeAAAA,
94+
Class: dnsmessage.ClassINET,
95+
TTL: 0,
96+
},
97+
dnsmessage.AAAAResource{
98+
AAAA: addr.As16(),
99+
},
100+
)
101+
default:
102+
panic("invalid address length")
103+
}
104+
return must.Get(b.Finish())
105+
}
106+
107+
// routeCollector is a test helper that collects the list of routes advertised
108+
type routeCollector struct {
109+
routes []netip.Prefix
110+
}
111+
112+
// routeCollector implements RouteAdvertiser
113+
var _ RouteAdvertiser = (*routeCollector)(nil)
114+
115+
func (rc *routeCollector) AdvertiseRoute(pfx netip.Prefix) error {
116+
rc.routes = append(rc.routes, pfx)
117+
return nil
118+
}

cmd/tailscaled/depaware.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
216216
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
217217
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
218218
inet.af/peercred from tailscale.com/ipn/ipnauth
219+
inet.af/tcpproxy from tailscale.com/appc
219220
W 💣 inet.af/wf from tailscale.com/wf
220221
nhooyr.io/websocket from tailscale.com/derp/derphttp+
221222
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
222223
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
223224
tailscale.com from tailscale.com/version
225+
tailscale.com/appc from tailscale.com/ipn/ipnlocal
224226
tailscale.com/atomicfile from tailscale.com/ipn+
225227
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
226228
tailscale.com/client/tailscale from tailscale.com/derp+
@@ -269,7 +271,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
269271
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
270272
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
271273
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
272-
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
274+
tailscale.com/net/dns/resolver from tailscale.com/net/dns
273275
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
274276
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
275277
tailscale.com/net/flowtrack from tailscale.com/net/packet+
@@ -319,6 +321,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
319321
tailscale.com/tstime/mono from tailscale.com/net/tstun+
320322
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
321323
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
324+
tailscale.com/types/appctype from tailscale.com/appc
322325
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
323326
tailscale.com/types/empty from tailscale.com/ipn+
324327
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled

0 commit comments

Comments
 (0)