6
6
"io"
7
7
"maps"
8
8
"math"
9
+ "net/netip"
9
10
"strings"
10
11
"sync"
11
12
"time"
@@ -15,6 +16,7 @@ import (
15
16
"storj.io/drpc"
16
17
"storj.io/drpc/drpcerr"
17
18
"tailscale.com/tailcfg"
19
+ "tailscale.com/util/dnsname"
18
20
19
21
"cdr.dev/slog"
20
22
"github.com/coder/coder/v2/codersdk"
@@ -104,6 +106,12 @@ type WorkspaceUpdatesController interface {
104
106
New (WorkspaceUpdatesClient ) CloserWaiter
105
107
}
106
108
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
+
107
115
// ControlProtocolClients represents an abstract interface to the tailnet control plane via a set
108
116
// of protocol clients. The Closer should close all the clients (e.g. by closing the underlying
109
117
// connection).
@@ -835,8 +843,9 @@ func (r *basicResumeTokenRefresher) refresh() {
835
843
}
836
844
837
845
type tunnelAllWorkspaceUpdatesController struct {
838
- coordCtrl * TunnelSrcCoordController
839
- logger slog.Logger
846
+ coordCtrl * TunnelSrcCoordController
847
+ dnsHostSetter DNSHostsSetter
848
+ logger slog.Logger
840
849
}
841
850
842
851
type workspace struct {
@@ -845,30 +854,48 @@ type workspace struct {
845
854
agents map [uuid.UUID ]agent
846
855
}
847
856
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
+
848
873
type agent struct {
849
874
id uuid.UUID
850
875
name string
851
876
}
852
877
853
878
func (t * tunnelAllWorkspaceUpdatesController ) New (client WorkspaceUpdatesClient ) CloserWaiter {
854
879
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 ),
861
887
}
862
888
go updater .recvLoop ()
863
889
return updater
864
890
}
865
891
866
892
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 {}
872
899
873
900
// don't need the mutex since only manipulated by the recvLoop
874
901
workspaces map [uuid.UUID ]* workspace
@@ -991,6 +1018,16 @@ func (t *tunnelUpdater) handleUpdate(update *proto.WorkspaceUpdate) error {
991
1018
}
992
1019
allAgents := t .allAgentIDs ()
993
1020
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
+ }
994
1031
return nil
995
1032
}
996
1033
@@ -1035,10 +1072,30 @@ func (t *tunnelUpdater) allAgentIDs() []uuid.UUID {
1035
1072
return out
1036
1073
}
1037
1074
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.
1038
1095
func NewTunnelAllWorkspaceUpdatesController (
1039
- logger slog.Logger , c * TunnelSrcCoordController ,
1096
+ logger slog.Logger , c * TunnelSrcCoordController , d DNSHostsSetter ,
1040
1097
) WorkspaceUpdatesController {
1041
- return & tunnelAllWorkspaceUpdatesController {logger : logger , coordCtrl : c }
1098
+ return & tunnelAllWorkspaceUpdatesController {logger : logger , coordCtrl : c , dnsHostSetter : d }
1042
1099
}
1043
1100
1044
1101
// NewController creates a new Controller without running it
0 commit comments