Skip to content

Commit 2803ed7

Browse files
committed
feat: set DNS hostnames in workspace updates controller
1 parent e1ce08d commit 2803ed7

File tree

2 files changed

+195
-20
lines changed

2 files changed

+195
-20
lines changed

tailnet/controllers.go

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"maps"
88
"math"
9+
"net/netip"
910
"strings"
1011
"sync"
1112
"time"
@@ -15,6 +16,7 @@ import (
1516
"storj.io/drpc"
1617
"storj.io/drpc/drpcerr"
1718
"tailscale.com/tailcfg"
19+
"tailscale.com/util/dnsname"
1820

1921
"cdr.dev/slog"
2022
"github.com/coder/coder/v2/codersdk"
@@ -104,6 +106,12 @@ type WorkspaceUpdatesController interface {
104106
New(WorkspaceUpdatesClient) CloserWaiter
105107
}
106108

109+
// DNSHostsSetter is something that you can set a mapping of DNS names to IPs on. It's the subset
110+
// of the tailnet.Conn that we use to configure DNS records.
111+
type DNSHostsSetter interface {
112+
SetDNSHosts(hosts map[dnsname.FQDN][]netip.Addr) error
113+
}
114+
107115
// ControlProtocolClients represents an abstract interface to the tailnet control plane via a set
108116
// of protocol clients. The Closer should close all the clients (e.g. by closing the underlying
109117
// connection).
@@ -835,8 +843,9 @@ func (r *basicResumeTokenRefresher) refresh() {
835843
}
836844

837845
type tunnelAllWorkspaceUpdatesController struct {
838-
coordCtrl *TunnelSrcCoordController
839-
logger slog.Logger
846+
coordCtrl *TunnelSrcCoordController
847+
dnsHostSetter DNSHostsSetter
848+
logger slog.Logger
840849
}
841850

842851
type workspace struct {
@@ -845,30 +854,48 @@ type workspace struct {
845854
agents map[uuid.UUID]agent
846855
}
847856

857+
// addAllDNSNames adds names for all of its agents to the given map of names
858+
func (w workspace) addAllDNSNames(names map[dnsname.FQDN][]netip.Addr) error {
859+
for _, a := range w.agents {
860+
// TODO: technically, DNS labels cannot start with numbers, but the rules are often not
861+
// strictly enforced.
862+
// TODO: support <agent>.<workspace>.<username>.coder
863+
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.coder.", a.name, w.name))
864+
if err != nil {
865+
return err
866+
}
867+
names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.id)}
868+
}
869+
// TODO: Possibly support <workspace>.coder. alias if there is only one agent
870+
return nil
871+
}
872+
848873
type agent struct {
849874
id uuid.UUID
850875
name string
851876
}
852877

853878
func (t *tunnelAllWorkspaceUpdatesController) New(client WorkspaceUpdatesClient) CloserWaiter {
854879
updater := &tunnelUpdater{
855-
client: client,
856-
errChan: make(chan error, 1),
857-
logger: t.logger,
858-
coordCtrl: t.coordCtrl,
859-
recvLoopDone: make(chan struct{}),
860-
workspaces: make(map[uuid.UUID]*workspace),
880+
client: client,
881+
errChan: make(chan error, 1),
882+
logger: t.logger,
883+
coordCtrl: t.coordCtrl,
884+
dnsHostsSetter: t.dnsHostSetter,
885+
recvLoopDone: make(chan struct{}),
886+
workspaces: make(map[uuid.UUID]*workspace),
861887
}
862888
go updater.recvLoop()
863889
return updater
864890
}
865891

866892
type tunnelUpdater struct {
867-
errChan chan error
868-
logger slog.Logger
869-
client WorkspaceUpdatesClient
870-
coordCtrl *TunnelSrcCoordController
871-
recvLoopDone chan struct{}
893+
errChan chan error
894+
logger slog.Logger
895+
client WorkspaceUpdatesClient
896+
coordCtrl *TunnelSrcCoordController
897+
dnsHostsSetter DNSHostsSetter
898+
recvLoopDone chan struct{}
872899

873900
// don't need the mutex since only manipulated by the recvLoop
874901
workspaces map[uuid.UUID]*workspace
@@ -991,6 +1018,16 @@ func (t *tunnelUpdater) handleUpdate(update *proto.WorkspaceUpdate) error {
9911018
}
9921019
allAgents := t.allAgentIDs()
9931020
t.coordCtrl.SyncDestinations(allAgents)
1021+
if t.dnsHostsSetter != nil {
1022+
t.logger.Debug(context.Background(), "updating dns hosts")
1023+
dnsNames := t.allDNSNames()
1024+
err := t.dnsHostsSetter.SetDNSHosts(dnsNames)
1025+
if err != nil {
1026+
return xerrors.Errorf("failed to set DNS hosts: %w", err)
1027+
}
1028+
} else {
1029+
t.logger.Debug(context.Background(), "skipping setting DNS names because we have no setter")
1030+
}
9941031
return nil
9951032
}
9961033

@@ -1035,10 +1072,30 @@ func (t *tunnelUpdater) allAgentIDs() []uuid.UUID {
10351072
return out
10361073
}
10371074

1075+
func (t *tunnelUpdater) allDNSNames() map[dnsname.FQDN][]netip.Addr {
1076+
names := make(map[dnsname.FQDN][]netip.Addr)
1077+
for _, w := range t.workspaces {
1078+
err := w.addAllDNSNames(names)
1079+
if err != nil {
1080+
// This should never happen in production, because converting the FQDN only fails
1081+
// if names are too long, and we put strict length limits on agent, workspace, and user
1082+
// names.
1083+
t.logger.Critical(context.Background(),
1084+
"failed to include DNS name(s)",
1085+
slog.F("workspace_id", w.id),
1086+
slog.Error(err))
1087+
}
1088+
}
1089+
return names
1090+
}
1091+
1092+
// NewTunnelAllWorkspaceUpdatesController creates a WorkspaceUpdatesController that creates tunnels
1093+
// (via the TunnelSrcCoordController) to all agents received over the WorkspaceUpdates RPC. If a
1094+
// DNSHostSetter is provided, it also programs DNS hosts based on the agent and workspace names.
10381095
func NewTunnelAllWorkspaceUpdatesController(
1039-
logger slog.Logger, c *TunnelSrcCoordController,
1096+
logger slog.Logger, c *TunnelSrcCoordController, d DNSHostsSetter,
10401097
) WorkspaceUpdatesController {
1041-
return &tunnelAllWorkspaceUpdatesController{logger: logger, coordCtrl: c}
1098+
return &tunnelAllWorkspaceUpdatesController{logger: logger, coordCtrl: c, dnsHostSetter: d}
10421099
}
10431100

10441101
// NewController creates a new Controller without running it

tailnet/controllers_test.go

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net"
8+
"net/netip"
89
"slices"
910
"sync"
1011
"sync/atomic"
@@ -23,6 +24,7 @@ import (
2324
"storj.io/drpc/drpcerr"
2425
"tailscale.com/tailcfg"
2526
"tailscale.com/types/key"
27+
"tailscale.com/util/dnsname"
2628

2729
"cdr.dev/slog"
2830
"cdr.dev/slog/sloggers/slogtest"
@@ -1344,14 +1346,56 @@ func testUUID(b ...byte) uuid.UUID {
13441346
return o
13451347
}
13461348

1349+
type fakeDNSSetter struct {
1350+
ctx context.Context
1351+
t testing.TB
1352+
calls chan *setDNSCall
1353+
}
1354+
1355+
type setDNSCall struct {
1356+
hosts map[dnsname.FQDN][]netip.Addr
1357+
err chan<- error
1358+
}
1359+
1360+
func newFakeDNSSetter(ctx context.Context, t testing.TB) *fakeDNSSetter {
1361+
return &fakeDNSSetter{
1362+
ctx: ctx,
1363+
t: t,
1364+
calls: make(chan *setDNSCall),
1365+
}
1366+
}
1367+
1368+
func (f *fakeDNSSetter) SetDNSHosts(hosts map[dnsname.FQDN][]netip.Addr) error {
1369+
f.t.Helper()
1370+
errs := make(chan error)
1371+
call := &setDNSCall{
1372+
hosts: hosts,
1373+
err: errs,
1374+
}
1375+
select {
1376+
case <-f.ctx.Done():
1377+
f.t.Error("timed out waiting to send SetDNSHosts() call")
1378+
return f.ctx.Err()
1379+
case f.calls <- call:
1380+
// OK
1381+
}
1382+
select {
1383+
case <-f.ctx.Done():
1384+
f.t.Error("timed out waiting for SetDNSHosts() call response")
1385+
return f.ctx.Err()
1386+
case err := <-errs:
1387+
return err
1388+
}
1389+
}
1390+
13471391
func setupConnectedAllWorkspaceUpdatesController(
1348-
ctx context.Context, t testing.TB, logger slog.Logger,
1392+
ctx context.Context, t testing.TB, logger slog.Logger, dnsSetter tailnet.DNSHostsSetter,
13491393
) (
13501394
*fakeCoordinatorClient, *fakeWorkspaceUpdateClient,
13511395
) {
13521396
fConn := &fakeCoordinatee{}
13531397
tsc := tailnet.NewTunnelSrcCoordController(logger, fConn)
1354-
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc)
1398+
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc, dnsSetter)
13551399

13561400
// connect up a coordinator client, to track adding and removing tunnels
13571401
coordC := newFakeCoordinatorClient(ctx, t)
@@ -1385,7 +1429,8 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) {
13851429
ctx := testutil.Context(t, testutil.WaitShort)
13861430
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
13871431

1388-
coordC, updateC := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger)
1432+
fDNS := newFakeDNSSetter(ctx, t)
1433+
coordC, updateC := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger, fDNS)
13891434

13901435
// Initial update contains 2 workspaces with 1 & 2 agents, respectively
13911436
w1ID := testUUID(1)
@@ -1418,14 +1463,25 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) {
14181463
require.Contains(t, adds, w1a1ID)
14191464
require.Contains(t, adds, w2a1ID)
14201465
require.Contains(t, adds, w2a2ID)
1466+
1467+
// Also triggers setting DNS hosts
1468+
expectedDNS := map[dnsname.FQDN][]netip.Addr{
1469+
"w1a1.w1.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0101::")},
1470+
"w2a1.w2.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0201::")},
1471+
"w2a2.w2.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0202::")},
1472+
}
1473+
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
1474+
require.Equal(t, expectedDNS, dnsCall.hosts)
1475+
testutil.RequireSendCtx(ctx, t, dnsCall.err, nil)
14211476
}
14221477

14231478
func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {
14241479
t.Parallel()
14251480
ctx := testutil.Context(t, testutil.WaitShort)
14261481
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
14271482

1428-
coordC, updateC := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger)
1483+
fDNS := newFakeDNSSetter(ctx, t)
1484+
coordC, updateC := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger, fDNS)
14291485

14301486
w1ID := testUUID(1)
14311487
w1a1ID := testUUID(1, 1)
@@ -1447,6 +1503,14 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {
14471503
require.Equal(t, w1a1ID[:], coordCall.req.GetAddTunnel().GetId())
14481504
testutil.RequireSendCtx(ctx, t, coordCall.err, nil)
14491505

1506+
// DNS for w1a1
1507+
expectedDNS := map[dnsname.FQDN][]netip.Addr{
1508+
"w1a1.w1.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0101::")},
1509+
}
1510+
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
1511+
require.Equal(t, expectedDNS, dnsCall.hosts)
1512+
testutil.RequireSendCtx(ctx, t, dnsCall.err, nil)
1513+
14501514
// Send update that removes w1a1 and adds w1a2
14511515
agentUpdate := &proto.WorkspaceUpdate{
14521516
UpsertedAgents: []*proto.Agent{
@@ -1468,6 +1532,60 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {
14681532
coordCall = testutil.RequireRecvCtx(ctx, t, coordC.reqs)
14691533
require.Equal(t, w1a1ID[:], coordCall.req.GetRemoveTunnel().GetId())
14701534
testutil.RequireSendCtx(ctx, t, coordCall.err, nil)
1535+
1536+
// DNS contains only w1a2
1537+
expectedDNS = map[dnsname.FQDN][]netip.Addr{
1538+
"w1a2.w1.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0102::")},
1539+
}
1540+
dnsCall = testutil.RequireRecvCtx(ctx, t, fDNS.calls)
1541+
require.Equal(t, expectedDNS, dnsCall.hosts)
1542+
testutil.RequireSendCtx(ctx, t, dnsCall.err, nil)
1543+
}
1544+
1545+
func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) {
1546+
t.Parallel()
1547+
ctx := testutil.Context(t, testutil.WaitShort)
1548+
dnsError := xerrors.New("a bad thing happened")
1549+
logger := slogtest.Make(t,
1550+
&slogtest.Options{IgnoredErrorIs: []error{dnsError}}).
1551+
Leveled(slog.LevelDebug)
1552+
1553+
fDNS := newFakeDNSSetter(ctx, t)
1554+
fConn := &fakeCoordinatee{}
1555+
tsc := tailnet.NewTunnelSrcCoordController(logger, fConn)
1556+
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc, fDNS)
1557+
1558+
updateC := newFakeWorkspaceUpdateClient(ctx, t)
1559+
updateCW := uut.New(updateC)
1560+
1561+
w1ID := testUUID(1)
1562+
w1a1ID := testUUID(1, 1)
1563+
initUp := &proto.WorkspaceUpdate{
1564+
UpsertedWorkspaces: []*proto.Workspace{
1565+
{Id: w1ID[:], Name: "w1"},
1566+
},
1567+
UpsertedAgents: []*proto.Agent{
1568+
{Id: w1a1ID[:], Name: "w1a1", WorkspaceId: w1ID[:]},
1569+
},
1570+
}
1571+
upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv)
1572+
testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp)
1573+
1574+
// DNS for w1a1
1575+
expectedDNS := map[dnsname.FQDN][]netip.Addr{
1576+
"w1a1.w1.me.coder.": {netip.MustParseAddr("fd60:627a:a42b:0101::")},
1577+
}
1578+
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
1579+
require.Equal(t, expectedDNS, dnsCall.hosts)
1580+
testutil.RequireSendCtx(ctx, t, dnsCall.err, dnsError)
1581+
1582+
// should trigger a close on the client
1583+
closeCall := testutil.RequireRecvCtx(ctx, t, updateC.close)
1584+
testutil.RequireSendCtx(ctx, t, closeCall, io.EOF)
1585+
1586+
// error should be our initial DNS error
1587+
err := testutil.RequireRecvCtx(ctx, t, updateCW.Wait())
1588+
require.ErrorIs(t, err, dnsError)
14711589
}
14721590

14731591
func TestTunnelAllWorkspaceUpdatesController_HandleErrors(t *testing.T) {
@@ -1562,7 +1680,7 @@ func TestTunnelAllWorkspaceUpdatesController_HandleErrors(t *testing.T) {
15621680

15631681
fConn := &fakeCoordinatee{}
15641682
tsc := tailnet.NewTunnelSrcCoordController(logger, fConn)
1565-
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc)
1683+
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc, nil)
15661684
updateC := newFakeWorkspaceUpdateClient(ctx, t)
15671685
updateCW := uut.New(updateC)
15681686

0 commit comments

Comments
 (0)