Skip to content

Commit 916df4d

Browse files
authored
feat: set DNS hostnames in workspace updates controller (#15507)
re: #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

Lines changed: 0 additions & 20 deletions
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

Lines changed: 4 additions & 35 deletions
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

Lines changed: 0 additions & 16 deletions
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

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

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