Skip to content

feat: vpn uses WorkspaceHostnameSuffix for DNS names #17335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codersdk/workspacesdk/workspacesdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ type AgentConnectionInfo struct {
DERPMap *tailcfg.DERPMap `json:"derp_map"`
DERPForceWebSockets bool `json:"derp_force_websockets"`
DisableDirectConnections bool `json:"disable_direct_connections"`
HostnameSuffix string `json:"hostname_suffix"`
HostnameSuffix string `json:"hostname_suffix,omitempty"`
}

func (c *Client) AgentConnectionInfoGeneric(ctx context.Context) (AgentConnectionInfo, error) {
Expand Down
4 changes: 1 addition & 3 deletions tailnet/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,7 @@ func NewConn(options *Options) (conn *Conn, err error) {
// A FQDN to be mapped to `tsaddr.CoderServiceIPv6`. This address can be used
// when you want to know if Coder Connect is running, but are not trying to
// connect to a specific known workspace.
const IsCoderConnectEnabledFQDNString = "is.coder--connect--enabled--right--now.coder."

var IsCoderConnectEnabledFQDN, _ = dnsname.ToFQDN(IsCoderConnectEnabledFQDNString)
const IsCoderConnectEnabledFmtString = "is.coder--connect--enabled--right--now.%s."

type ServicePrefix [6]byte

Expand Down
49 changes: 33 additions & 16 deletions tailnet/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,11 +864,12 @@ func (r *basicResumeTokenRefresher) refresh() {
}

type TunnelAllWorkspaceUpdatesController struct {
coordCtrl *TunnelSrcCoordController
dnsHostSetter DNSHostsSetter
updateHandler UpdatesHandler
ownerUsername string
logger slog.Logger
coordCtrl *TunnelSrcCoordController
dnsHostSetter DNSHostsSetter
dnsNameOptions DNSNameOptions
updateHandler UpdatesHandler
ownerUsername string
logger slog.Logger

mu sync.Mutex
updater *tunnelUpdater
Expand All @@ -883,37 +884,39 @@ type Workspace struct {
agents map[uuid.UUID]*Agent
}

type DNSNameOptions struct {
Suffix string
}

// updateDNSNames updates the DNS names for all agents in the workspace.
// DNS hosts must be all lowercase, or the resolver won't be able to find them.
// Usernames are globally unique & case-insensitive.
// Workspace names are unique per-user & case-insensitive.
// Agent names are unique per-workspace & case-insensitive.
func (w *Workspace) updateDNSNames() error {
func (w *Workspace) updateDNSNames(options DNSNameOptions) error {
wsName := strings.ToLower(w.Name)
username := strings.ToLower(w.ownerUsername)
for id, a := range w.agents {
agentName := strings.ToLower(a.Name)
names := make(map[dnsname.FQDN][]netip.Addr)
// TODO: technically, DNS labels cannot start with numbers, but the rules are often not
// strictly enforced.
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.coder.", agentName, wsName))
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.%s.", agentName, wsName, options.Suffix))
if err != nil {
return err
}
names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)}
fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.coder.", agentName, wsName, username))
fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.%s.", agentName, wsName, username, options.Suffix))
if err != nil {
return err
}
names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)}
if len(w.agents) == 1 {
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.coder.", wsName))
fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.", wsName, options.Suffix))
if err != nil {
return err
}
for _, a := range w.agents {
names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)}
}
names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)}
}
a.Hosts = names
w.agents[id] = a
Expand Down Expand Up @@ -950,6 +953,7 @@ func (t *TunnelAllWorkspaceUpdatesController) New(client WorkspaceUpdatesClient)
logger: t.logger,
coordCtrl: t.coordCtrl,
dnsHostsSetter: t.dnsHostSetter,
dnsNameOptions: t.dnsNameOptions,
updateHandler: t.updateHandler,
ownerUsername: t.ownerUsername,
recvLoopDone: make(chan struct{}),
Expand Down Expand Up @@ -996,6 +1000,7 @@ type tunnelUpdater struct {
updateHandler UpdatesHandler
ownerUsername string
recvLoopDone chan struct{}
dnsNameOptions DNSNameOptions

sync.Mutex
workspaces map[uuid.UUID]*Workspace
Expand Down Expand Up @@ -1250,14 +1255,15 @@ func (t *tunnelUpdater) allAgentIDsLocked() []uuid.UUID {
func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr {
names := make(map[dnsname.FQDN][]netip.Addr)
for _, w := range t.workspaces {
err := w.updateDNSNames()
err := w.updateDNSNames(t.dnsNameOptions)
if err != nil {
// This should never happen in production, because converting the FQDN only fails
// if names are too long, and we put strict length limits on agent, workspace, and user
// names.
t.logger.Critical(context.Background(),
"failed to include DNS name(s)",
slog.F("workspace_id", w.ID),
slog.F("suffix", t.dnsNameOptions.Suffix),
slog.Error(err))
}
for _, a := range w.agents {
Expand All @@ -1266,18 +1272,25 @@ func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr {
}
}
}
names[IsCoderConnectEnabledFQDN] = []netip.Addr{tsaddr.CoderServiceIPv6()}
isCoderConnectEnabledFQDN, err := dnsname.ToFQDN(fmt.Sprintf(IsCoderConnectEnabledFmtString, t.dnsNameOptions.Suffix))
if err != nil {
t.logger.Critical(context.Background(),
"failed to include Coder Connect enabled DNS name", slog.F("suffix", t.dnsNameOptions.Suffix))
} else {
names[isCoderConnectEnabledFQDN] = []netip.Addr{tsaddr.CoderServiceIPv6()}
}
return names
}

type TunnelAllOption func(t *TunnelAllWorkspaceUpdatesController)

// WithDNS configures the tunnelAllWorkspaceUpdatesController to set DNS names for all workspaces
// and agents it learns about.
func WithDNS(d DNSHostsSetter, ownerUsername string) TunnelAllOption {
func WithDNS(d DNSHostsSetter, ownerUsername string, options DNSNameOptions) TunnelAllOption {
return func(t *TunnelAllWorkspaceUpdatesController) {
t.dnsHostSetter = d
t.ownerUsername = ownerUsername
t.dnsNameOptions = options
}
}

Expand All @@ -1293,7 +1306,11 @@ func WithHandler(h UpdatesHandler) TunnelAllOption {
func NewTunnelAllWorkspaceUpdatesController(
logger slog.Logger, c *TunnelSrcCoordController, opts ...TunnelAllOption,
) *TunnelAllWorkspaceUpdatesController {
t := &TunnelAllWorkspaceUpdatesController{logger: logger, coordCtrl: c}
t := &TunnelAllWorkspaceUpdatesController{
logger: logger,
coordCtrl: c,
dnsNameOptions: DNSNameOptions{"coder"},
}
for _, opt := range opts {
opt(t)
}
Expand Down
71 changes: 40 additions & 31 deletions tailnet/controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,7 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) {
fUH := newFakeUpdateHandler(ctx, t)
fDNS := newFakeDNSSetter(ctx, t)
coordC, updateC, updateCtrl := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger,
tailnet.WithDNS(fDNS, "testy"),
tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "mctest"}),
tailnet.WithHandler(fUH),
)

Expand Down Expand Up @@ -1562,16 +1562,19 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) {
w2a1IP := netip.MustParseAddr("fd60:627a:a42b:0201::")
w2a2IP := netip.MustParseAddr("fd60:627a:a42b:0202::")

expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "mctest"))
require.NoError(t, err)

// Also triggers setting DNS hosts
expectedDNS := map[dnsname.FQDN][]netip.Addr{
"w1a1.w1.me.coder.": {ws1a1IP},
"w2a1.w2.me.coder.": {w2a1IP},
"w2a2.w2.me.coder.": {w2a2IP},
"w1a1.w1.testy.coder.": {ws1a1IP},
"w2a1.w2.testy.coder.": {w2a1IP},
"w2a2.w2.testy.coder.": {w2a2IP},
"w1.coder.": {ws1a1IP},
tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()},
"w1a1.w1.me.mctest.": {ws1a1IP},
"w2a1.w2.me.mctest.": {w2a1IP},
"w2a2.w2.me.mctest.": {w2a2IP},
"w1a1.w1.testy.mctest.": {ws1a1IP},
"w2a1.w2.testy.mctest.": {w2a1IP},
"w2a2.w2.testy.mctest.": {w2a2IP},
"w1.mctest.": {ws1a1IP},
expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()},
}
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
require.Equal(t, expectedDNS, dnsCall.hosts)
Expand All @@ -1586,23 +1589,23 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) {
{
ID: w1a1ID, Name: "w1a1", WorkspaceID: w1ID,
Hosts: map[dnsname.FQDN][]netip.Addr{
"w1.coder.": {ws1a1IP},
"w1a1.w1.me.coder.": {ws1a1IP},
"w1a1.w1.testy.coder.": {ws1a1IP},
"w1.mctest.": {ws1a1IP},
"w1a1.w1.me.mctest.": {ws1a1IP},
"w1a1.w1.testy.mctest.": {ws1a1IP},
},
},
{
ID: w2a1ID, Name: "w2a1", WorkspaceID: w2ID,
Hosts: map[dnsname.FQDN][]netip.Addr{
"w2a1.w2.me.coder.": {w2a1IP},
"w2a1.w2.testy.coder.": {w2a1IP},
"w2a1.w2.me.mctest.": {w2a1IP},
"w2a1.w2.testy.mctest.": {w2a1IP},
},
},
{
ID: w2a2ID, Name: "w2a2", WorkspaceID: w2ID,
Hosts: map[dnsname.FQDN][]netip.Addr{
"w2a2.w2.me.coder.": {w2a2IP},
"w2a2.w2.testy.coder.": {w2a2IP},
"w2a2.w2.me.mctest.": {w2a2IP},
"w2a2.w2.testy.mctest.": {w2a2IP},
},
},
},
Expand Down Expand Up @@ -1634,7 +1637,7 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {
fUH := newFakeUpdateHandler(ctx, t)
fDNS := newFakeDNSSetter(ctx, t)
coordC, updateC, updateCtrl := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger,
tailnet.WithDNS(fDNS, "testy"),
tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}),
tailnet.WithHandler(fUH),
)

Expand All @@ -1661,12 +1664,15 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {
require.Equal(t, w1a1ID[:], coordCall.req.GetAddTunnel().GetId())
testutil.RequireSendCtx(ctx, t, coordCall.err, nil)

expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder"))
require.NoError(t, err)

// DNS for w1a1
expectedDNS := map[dnsname.FQDN][]netip.Addr{
"w1a1.w1.testy.coder.": {ws1a1IP},
"w1a1.w1.me.coder.": {ws1a1IP},
"w1.coder.": {ws1a1IP},
tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()},
"w1a1.w1.testy.coder.": {ws1a1IP},
"w1a1.w1.me.coder.": {ws1a1IP},
"w1.coder.": {ws1a1IP},
expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()},
}
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
require.Equal(t, expectedDNS, dnsCall.hosts)
Expand Down Expand Up @@ -1719,10 +1725,10 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) {

// DNS contains only w1a2
expectedDNS = map[dnsname.FQDN][]netip.Addr{
"w1a2.w1.testy.coder.": {ws1a2IP},
"w1a2.w1.me.coder.": {ws1a2IP},
"w1.coder.": {ws1a2IP},
tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()},
"w1a2.w1.testy.coder.": {ws1a2IP},
"w1a2.w1.me.coder.": {ws1a2IP},
"w1.coder.": {ws1a2IP},
expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()},
}
dnsCall = testutil.RequireRecvCtx(ctx, t, fDNS.calls)
require.Equal(t, expectedDNS, dnsCall.hosts)
Expand Down Expand Up @@ -1779,7 +1785,7 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) {
fConn := &fakeCoordinatee{}
tsc := tailnet.NewTunnelSrcCoordController(logger, fConn)
uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc,
tailnet.WithDNS(fDNS, "testy"),
tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}),
)

updateC := newFakeWorkspaceUpdateClient(ctx, t)
Expand All @@ -1800,12 +1806,15 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) {
upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv)
testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp)

expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder"))
require.NoError(t, err)

// DNS for w1a1
expectedDNS := map[dnsname.FQDN][]netip.Addr{
"w1a1.w1.me.coder.": {ws1a1IP},
"w1a1.w1.testy.coder.": {ws1a1IP},
"w1.coder.": {ws1a1IP},
tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()},
"w1a1.w1.me.coder.": {ws1a1IP},
"w1a1.w1.testy.coder.": {ws1a1IP},
"w1.coder.": {ws1a1IP},
expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()},
}
dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls)
require.Equal(t, expectedDNS, dnsCall.hosts)
Expand All @@ -1816,7 +1825,7 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) {
testutil.RequireSendCtx(ctx, t, closeCall, io.EOF)

// error should be our initial DNS error
err := testutil.RequireRecvCtx(ctx, t, updateCW.Wait())
err = testutil.RequireRecvCtx(ctx, t, updateCW.Wait())
require.ErrorIs(t, err, dnsError)
}

Expand Down
7 changes: 6 additions & 1 deletion vpn/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
if err != nil {
return nil, xerrors.Errorf("get connection info: %w", err)
}
// default to DNS suffix of "coder" if the server hasn't set it (might be too old).
dnsNameOptions := tailnet.DNSNameOptions{Suffix: "coder"}
if connInfo.HostnameSuffix != "" {
dnsNameOptions.Suffix = connInfo.HostnameSuffix
}

headers.Set(codersdk.SessionTokenHeader, token)
dialer := workspacesdk.NewWebsocketDialer(options.Logger, rpcURL, &websocket.DialOptions{
Expand Down Expand Up @@ -148,7 +153,7 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
updatesCtrl := tailnet.NewTunnelAllWorkspaceUpdatesController(
options.Logger,
coordCtrl,
tailnet.WithDNS(conn, me.Username),
tailnet.WithDNS(conn, me.Username, dnsNameOptions),
tailnet.WithHandler(options.UpdateHandler),
)
controller.WorkspaceUpdatesCtrl = updatesCtrl
Expand Down
Loading
Loading