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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy