Skip to content

Commit 916df4d

Browse files
authored
feat: set DNS hostnames in workspace updates controller (coder#15507)
re: coder#14730 Adds support for the workspace updates protocol controller to also program DNS names for each agent. Right now, we only program names like `myagent.myworkspace.me.coder` and `myworkspace.coder.` (if there is exactly one agent in the workspace). We also want to support `myagent.myworkspace.username.coder.`, but for that we need to update WorkspaceUpdates RPC to also send the workspace owner's username, which will be in a separate PR.
1 parent 365ce67 commit 916df4d

File tree

5 files changed

+199
-91
lines changed

5 files changed

+199
-91
lines changed

tailnet/configmaps.go

-20
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,6 @@ func (c *configMaps) setAddresses(ips []netip.Prefix) {
277277
c.Broadcast()
278278
}
279279

280-
func (c *configMaps) addHosts(hosts map[dnsname.FQDN][]netip.Addr) {
281-
c.L.Lock()
282-
defer c.L.Unlock()
283-
for name, addrs := range hosts {
284-
c.hosts[name] = slices.Clone(addrs)
285-
}
286-
c.netmapDirty = true
287-
c.Broadcast()
288-
}
289-
290280
func (c *configMaps) setHosts(hosts map[dnsname.FQDN][]netip.Addr) {
291281
c.L.Lock()
292282
defer c.L.Unlock()
@@ -298,16 +288,6 @@ func (c *configMaps) setHosts(hosts map[dnsname.FQDN][]netip.Addr) {
298288
c.Broadcast()
299289
}
300290

301-
func (c *configMaps) removeHosts(names []dnsname.FQDN) {
302-
c.L.Lock()
303-
defer c.L.Unlock()
304-
for _, name := range names {
305-
delete(c.hosts, name)
306-
}
307-
c.netmapDirty = true
308-
c.Broadcast()
309-
}
310-
311291
// setBlockEndpoints sets whether we should block configuring endpoints we learn
312292
// from peers. It triggers a configuration of the engine if the value changes.
313293
// nolint: revive

tailnet/configmaps_internal_test.go

+4-35
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/google/uuid"
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13-
"golang.org/x/exp/maps"
1413
"tailscale.com/ipn/ipnstate"
1514
"tailscale.com/net/dns"
1615
"tailscale.com/tailcfg"
@@ -1177,8 +1176,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) {
11771176
addr3 := CoderServicePrefix.AddrFromUUID(uuid.New())
11781177
addr4 := CoderServicePrefix.AddrFromUUID(uuid.New())
11791178

1180-
// WHEN: we add two hosts
1181-
uut.addHosts(map[dnsname.FQDN][]netip.Addr{
1179+
// WHEN: we set two hosts
1180+
uut.setHosts(map[dnsname.FQDN][]netip.Addr{
11821181
"agent.myws.me.coder.": {
11831182
addr1,
11841183
},
@@ -1207,36 +1206,6 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) {
12071206
OnlyIPv6: true,
12081207
})
12091208

1210-
// WHEN: we add a new host
1211-
newHost := map[dnsname.FQDN][]netip.Addr{
1212-
"agent2.myws.me.coder.": {
1213-
addr4,
1214-
},
1215-
}
1216-
uut.addHosts(newHost)
1217-
1218-
// THEN: the engine is reconfigured with both the old and new hosts
1219-
_ = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap)
1220-
req = testutil.RequireRecvCtx(ctx, t, fEng.reconfig)
1221-
require.Equal(t, req.dnsCfg, &dns.Config{
1222-
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
1223-
CoderDNSSuffix: nil,
1224-
},
1225-
Hosts: map[dnsname.FQDN][]netip.Addr{
1226-
"agent.myws.me.coder.": {
1227-
addr1,
1228-
},
1229-
"dev.main.me.coder.": {
1230-
addr2,
1231-
addr3,
1232-
},
1233-
"agent2.myws.me.coder.": {
1234-
addr4,
1235-
},
1236-
},
1237-
OnlyIPv6: true,
1238-
})
1239-
12401209
// WHEN: We replace the hosts with a new set
12411210
uut.setHosts(map[dnsname.FQDN][]netip.Addr{
12421211
"newagent.myws.me.coder.": {
@@ -1265,8 +1234,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) {
12651234
OnlyIPv6: true,
12661235
})
12671236

1268-
// WHEN: we remove all the hosts, and a bad host
1269-
uut.removeHosts(append(maps.Keys(req.dnsCfg.Hosts), "badhostname"))
1237+
// WHEN: we remove all the hosts
1238+
uut.setHosts(map[dnsname.FQDN][]netip.Addr{})
12701239
_ = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap)
12711240
req = testutil.RequireRecvCtx(ctx, t, fEng.reconfig)
12721241

tailnet/conn.go

-16
Original file line numberDiff line numberDiff line change
@@ -451,22 +451,6 @@ func (c *Conn) SetAddresses(ips []netip.Prefix) error {
451451
return nil
452452
}
453453

454-
func (c *Conn) AddDNSHosts(hosts map[dnsname.FQDN][]netip.Addr) error {
455-
if c.dnsConfigurator == nil {
456-
return xerrors.New("no DNSConfigurator set")
457-
}
458-
c.configMaps.addHosts(hosts)
459-
return nil
460-
}
461-
462-
func (c *Conn) RemoveDNSHosts(names []dnsname.FQDN) error {
463-
if c.dnsConfigurator == nil {
464-
return xerrors.New("no DNSConfigurator set")
465-
}
466-
c.configMaps.removeHosts(names)
467-
return nil
468-
}
469-
470454
// SetDNSHosts replaces the map of DNS hosts for the connection.
471455
func (c *Conn) SetDNSHosts(hosts map[dnsname.FQDN][]netip.Addr) error {
472456
if c.dnsConfigurator == nil {

tailnet/controllers.go

+72-15
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

0 commit comments

Comments
 (0)