feat: add flag to disable all direct connections (#7936)

This commit is contained in:
Dean Sheather 2023-06-22 08:02:05 +10:00 committed by GitHub
parent 96f9e61ca1
commit a28d422c35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 369 additions and 105 deletions

View File

@ -593,7 +593,7 @@ func (a *agent) run(ctx context.Context) error {
network := a.network
a.closeMutex.Unlock()
if network == nil {
network, err = a.createTailnet(ctx, manifest.DERPMap)
network, err = a.createTailnet(ctx, manifest.DERPMap, manifest.DisableDirectConnections)
if err != nil {
return xerrors.Errorf("create tailnet: %w", err)
}
@ -611,8 +611,9 @@ func (a *agent) run(ctx context.Context) error {
a.startReportingConnectionStats(ctx)
} else {
// Update the DERP map!
// Update the DERP map and allow/disallow direct connections.
network.SetDERPMap(manifest.DERPMap)
network.SetBlockEndpoints(manifest.DisableDirectConnections)
}
a.logger.Debug(ctx, "running tailnet connection coordinator")
@ -637,12 +638,13 @@ func (a *agent) trackConnGoroutine(fn func()) error {
return nil
}
func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ *tailnet.Conn, err error) {
func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap, disableDirectConnections bool) (_ *tailnet.Conn, err error) {
network, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)},
DERPMap: derpMap,
Logger: a.logger.Named("tailnet"),
ListenPort: a.tailnetListenPort,
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)},
DERPMap: derpMap,
Logger: a.logger.Named("tailnet"),
ListenPort: a.tailnetListenPort,
BlockEndpoints: disableDirectConnections,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet: %w", err)

View File

@ -25,10 +25,14 @@ func TestNetcheck(t *testing.T) {
clitest.StartWithWaiter(t, inv).RequireSuccess()
b := out.Bytes()
t.Log(string(b))
var report healthcheck.DERPReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report))
require.NoError(t, json.Unmarshal(b, &report))
assert.True(t, report.Healthy)
require.Len(t, report.Regions, 1)
require.Len(t, report.Regions[1].NodeReports, 1)
for _, v := range report.Regions {
require.Len(t, v.NodeReports, len(v.Region.Nodes))
}
}

View File

@ -413,6 +413,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
derpMap, err := tailnet.NewDERPMap(
ctx, defaultRegion, cfg.DERP.Server.STUNAddresses,
cfg.DERP.Config.URL.String(), cfg.DERP.Config.Path.String(),
cfg.DERP.Config.BlockDirect.Value(),
)
if err != nil {
return xerrors.Errorf("create derp map: %w", err)

View File

@ -150,6 +150,14 @@ between workspaces and users are peer-to-peer. However, when Coder cannot
establish a peer to peer connection, Coder uses a distributed relay network
backed by Tailscale and WireGuard.
--block-direct-connections bool, $CODER_BLOCK_DIRECT
Block peer-to-peer (aka. direct) workspace connections. All workspace
connections from the CLI will be proxied through Coder (or custom
configured DERP servers) and will never be peer-to-peer when enabled.
Workspaces may still reach out to STUN servers to get their address
until they are restarted after this change has been made, but new
connections will still be proxied regardless.
--derp-config-path string, $CODER_DERP_CONFIG_PATH
Path to read a DERP mapping from. See:
https://tailscale.com/kb/1118/custom-derp-servers/.

View File

@ -117,6 +117,13 @@ networking:
# for high availability.
# (default: <unset>, type: url)
relayURL:
# Block peer-to-peer (aka. direct) workspace connections. All workspace
# connections from the CLI will be proxied through Coder (or custom configured
# DERP servers) and will never be peer-to-peer when enabled. Workspaces may still
# reach out to STUN servers to get their address until they are restarted after
# this change has been made, but new connections will still be proxied regardless.
# (default: <unset>, type: bool)
blockDirect: false
# URL to fetch a DERP mapping on startup. See:
# https://tailscale.com/kb/1118/custom-derp-servers/.
# (default: <unset>, type: string)

9
coderd/apidoc/docs.go generated
View File

@ -5784,6 +5784,9 @@ const docTemplate = `{
"directory": {
"type": "string"
},
"disable_direct_connections": {
"type": "boolean"
},
"environment_variables": {
"type": "object",
"additionalProperties": {
@ -7065,6 +7068,9 @@ const docTemplate = `{
"codersdk.DERPConfig": {
"type": "object",
"properties": {
"block_direct": {
"type": "boolean"
},
"path": {
"type": "string"
},
@ -9351,6 +9357,9 @@ const docTemplate = `{
"properties": {
"derp_map": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
"disable_direct_connections": {
"type": "boolean"
}
}
},

View File

@ -5095,6 +5095,9 @@
"directory": {
"type": "string"
},
"disable_direct_connections": {
"type": "boolean"
},
"environment_variables": {
"type": "object",
"additionalProperties": {
@ -6294,6 +6297,9 @@
"codersdk.DERPConfig": {
"type": "object",
"properties": {
"block_direct": {
"type": "boolean"
},
"path": {
"type": "string"
},
@ -8441,6 +8447,9 @@
"properties": {
"derp_map": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
"disable_direct_connections": {
"type": "boolean"
}
}
},

View File

@ -293,6 +293,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
}
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
stunAddr.IP = net.ParseIP("127.0.0.1")
t.Cleanup(stunCleanup)
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(slogtest.Make(t, nil).Named("derp").Leveled(slog.LevelDebug)))
@ -310,6 +311,29 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
require.NoError(t, err)
}
region := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
RegionCode: options.DeploymentValues.DERP.Server.RegionCode.String(),
RegionName: options.DeploymentValues.DERP.Server.RegionName.String(),
Nodes: []*tailcfg.DERPNode{{
Name: fmt.Sprintf("%db", options.DeploymentValues.DERP.Server.RegionID),
RegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
IPv4: "127.0.0.1",
DERPPort: derpPort,
// STUN port is added as a separate node by tailnet.NewDERPMap() if
// direct connections are enabled.
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: options.TLSCertificates == nil,
}},
}
if !options.DeploymentValues.DERP.Server.Enable.Value() {
region = nil
}
derpMap, err := tailnet.NewDERPMap(ctx, region, []string{stunAddr.String()}, "", "", options.DeploymentValues.DERP.Config.BlockDirect.Value())
require.NoError(t, err)
return func(h http.Handler) {
mutex.Lock()
defer mutex.Unlock()
@ -328,42 +352,24 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
Pubsub: options.Pubsub,
GitAuthConfigs: options.GitAuthConfigs,
Auditor: options.Auditor,
AWSCertificates: options.AWSCertificates,
AzureCertificates: options.AzureCertificates,
GithubOAuth2Config: options.GithubOAuth2Config,
RealIPConfig: options.RealIPConfig,
OIDCConfig: options.OIDCConfig,
GoogleTokenValidator: options.GoogleTokenValidator,
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
DERPServer: derpServer,
APIRateLimit: options.APIRateLimit,
LoginRateLimit: options.LoginRateLimit,
FilesRateLimit: options.FilesRateLimit,
Authorizer: options.Authorizer,
Telemetry: telemetry.NewNoop(),
TemplateScheduleStore: &templateScheduleStore,
TLSCertificates: options.TLSCertificates,
TrialGenerator: options.TrialGenerator,
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 1,
RegionCode: "coder",
RegionName: "Coder",
Nodes: []*tailcfg.DERPNode{{
Name: "1a",
RegionID: 1,
IPv4: "127.0.0.1",
DERPPort: derpPort,
STUNPort: stunAddr.Port,
InsecureForTests: true,
ForceHTTP: options.TLSCertificates == nil,
}},
},
},
},
Auditor: options.Auditor,
AWSCertificates: options.AWSCertificates,
AzureCertificates: options.AzureCertificates,
GithubOAuth2Config: options.GithubOAuth2Config,
RealIPConfig: options.RealIPConfig,
OIDCConfig: options.OIDCConfig,
GoogleTokenValidator: options.GoogleTokenValidator,
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
DERPServer: derpServer,
APIRateLimit: options.APIRateLimit,
LoginRateLimit: options.LoginRateLimit,
FilesRateLimit: options.FilesRateLimit,
Authorizer: options.Authorizer,
Telemetry: telemetry.NewNoop(),
TemplateScheduleStore: &templateScheduleStore,
TLSCertificates: options.TLSCertificates,
TrialGenerator: options.TrialGenerator,
DERPMap: derpMap,
MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
DeploymentValues: options.DeploymentValues,

View File

@ -161,18 +161,19 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
}
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
Apps: convertApps(dbApps),
DERPMap: api.DERPMap,
GitAuthConfigs: len(api.GitAuthConfigs),
EnvironmentVariables: apiAgent.EnvironmentVariables,
StartupScript: apiAgent.StartupScript,
Directory: apiAgent.Directory,
VSCodePortProxyURI: vscodeProxyURI,
MOTDFile: workspaceAgent.MOTDFile,
StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second,
ShutdownScript: apiAgent.ShutdownScript,
ShutdownScriptTimeout: time.Duration(apiAgent.ShutdownScriptTimeoutSeconds) * time.Second,
Metadata: convertWorkspaceAgentMetadataDesc(metadata),
Apps: convertApps(dbApps),
DERPMap: api.DERPMap,
GitAuthConfigs: len(api.GitAuthConfigs),
EnvironmentVariables: apiAgent.EnvironmentVariables,
StartupScript: apiAgent.StartupScript,
Directory: apiAgent.Directory,
VSCodePortProxyURI: vscodeProxyURI,
MOTDFile: workspaceAgent.MOTDFile,
StartupScriptTimeout: time.Duration(apiAgent.StartupScriptTimeoutSeconds) * time.Second,
ShutdownScript: apiAgent.ShutdownScript,
ShutdownScriptTimeout: time.Duration(apiAgent.ShutdownScriptTimeoutSeconds) * time.Second,
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
Metadata: convertWorkspaceAgentMetadataDesc(metadata),
})
}
@ -731,9 +732,10 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
func (api *API) dialWorkspaceAgentTailnet(agentID uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
clientConn, serverConn := net.Pipe()
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
DERPMap: api.DERPMap,
Logger: api.Logger.Named("tailnet"),
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
DERPMap: api.DERPMap,
Logger: api.Logger.Named("tailnet"),
BlockEndpoints: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
if err != nil {
_ = clientConn.Close()
@ -801,7 +803,8 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request
ctx := r.Context()
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{
DERPMap: api.DERPMap,
DERPMap: api.DERPMap,
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
}

View File

@ -2,6 +2,7 @@ package coderd_test
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
@ -575,6 +576,82 @@ func TestWorkspaceAgentTailnet(t *testing.T) {
require.Equal(t, "test", strings.TrimSpace(string(output)))
}
func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
err := dv.DERP.Config.BlockDirect.Set("true")
require.NoError(t, err)
require.True(t, dv.DERP.Config.BlockDirect.Value())
client, daemonCloser := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
DeploymentValues: dv,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
ctx := testutil.Context(t, testutil.WaitLong)
// Verify that the manifest has DisableDirectConnections set to true.
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
manifest, err := agentClient.Manifest(ctx)
require.NoError(t, err)
require.True(t, manifest.DisableDirectConnections)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
})
defer agentCloser.Close()
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
agentID := resources[0].Agents[0].ID
// Verify that the connection data has no STUN ports and
// DisableDirectConnections set to true.
res, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/connection", agentID), nil)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
var connInfo codersdk.WorkspaceAgentConnectionInfo
err = json.NewDecoder(res.Body).Decode(&connInfo)
require.NoError(t, err)
require.True(t, connInfo.DisableDirectConnections)
for _, region := range connInfo.DERPMap.Regions {
t.Logf("region %s (%v)", region.RegionCode, region.EmbeddedRelay)
for _, node := range region.Nodes {
t.Logf(" node %s (stun %d)", node.Name, node.STUNPort)
require.EqualValues(t, -1, node.STUNPort)
// tailnet.NewDERPMap() will create nodes with "stun" in the name,
// but not if direct is disabled.
require.NotContains(t, node.Name, "stun")
require.False(t, node.STUNOnly)
}
}
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug),
})
require.NoError(t, err)
defer conn.Close()
require.True(t, conn.BlockEndpoints())
require.True(t, conn.AwaitReachable(ctx))
_, p2p, _, err := conn.Ping(ctx)
require.NoError(t, err)
require.False(t, p2p)
}
func TestWorkspaceAgentListeningPorts(t *testing.T) {
t.Parallel()

View File

@ -87,18 +87,19 @@ type Manifest struct {
// GitAuthConfigs stores the number of Git configurations
// the Coder deployment has. If this number is >0, we
// set up special configuration in the workspace.
GitAuthConfigs int `json:"git_auth_configs"`
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
Apps []codersdk.WorkspaceApp `json:"apps"`
DERPMap *tailcfg.DERPMap `json:"derpmap"`
EnvironmentVariables map[string]string `json:"environment_variables"`
StartupScript string `json:"startup_script"`
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
Directory string `json:"directory"`
MOTDFile string `json:"motd_file"`
ShutdownScript string `json:"shutdown_script"`
ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"`
Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"`
GitAuthConfigs int `json:"git_auth_configs"`
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
Apps []codersdk.WorkspaceApp `json:"apps"`
DERPMap *tailcfg.DERPMap `json:"derpmap"`
EnvironmentVariables map[string]string `json:"environment_variables"`
StartupScript string `json:"startup_script"`
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
Directory string `json:"directory"`
MOTDFile string `json:"motd_file"`
ShutdownScript string `json:"shutdown_script"`
ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"`
DisableDirectConnections bool `json:"disable_direct_connections"`
Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"`
}
// Manifest fetches manifest for the currently authenticated workspace agent.

View File

@ -221,8 +221,9 @@ type DERPServerConfig struct {
}
type DERPConfig struct {
URL clibase.String `json:"url" typescript:",notnull"`
Path clibase.String `json:"path" typescript:",notnull"`
BlockDirect clibase.Bool `json:"block_direct" typescript:",notnull"`
URL clibase.String `json:"url" typescript:",notnull"`
Path clibase.String `json:"path" typescript:",notnull"`
}
type PrometheusConfig struct {
@ -711,6 +712,18 @@ when required by your organization's security policy.`,
Group: &deploymentGroupNetworkingDERP,
YAML: "relayURL",
},
{
Name: "Block Direct Connections",
Description: "Block peer-to-peer (aka. direct) workspace connections. All workspace connections from the CLI will be proxied through Coder (or custom configured DERP servers) and will never be peer-to-peer when enabled. Workspaces may still reach out to STUN servers to get their address until they are restarted after this change has been made, but new connections will still be proxied regardless.",
// This cannot be called `disable-direct-connections` because that's
// already a global CLI flag for CLI connections. This is a
// deployment-wide flag.
Flag: "block-direct-connections",
Env: "CODER_BLOCK_DIRECT",
Value: &c.DERP.Config.BlockDirect,
Group: &deploymentGroupNetworkingDERP,
YAML: "blockDirect",
},
{
Name: "DERP Config URL",
Description: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/.",

View File

@ -167,7 +167,8 @@ type DERPRegion struct {
// a connection with a workspace.
// @typescript-ignore WorkspaceAgentConnectionInfo
type WorkspaceAgentConnectionInfo struct {
DERPMap *tailcfg.DERPMap `json:"derp_map"`
DERPMap *tailcfg.DERPMap `json:"derp_map"`
DisableDirectConnections bool `json:"disable_direct_connections"`
}
func (c *Client) WorkspaceAgentConnectionInfo(ctx context.Context) (*WorkspaceAgentConnectionInfo, error) {
@ -215,6 +216,9 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
if err != nil {
return nil, xerrors.Errorf("decode conn info: %w", err)
}
if connInfo.DisableDirectConnections {
options.BlockEndpoints = true
}
ip := tailnet.IP()
var header http.Header

View File

@ -363,6 +363,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
}
},
"directory": "string",
"disable_direct_connections": true,
"environment_variables": {
"property1": "string",
"property2": "string"
@ -567,7 +568,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
"regionName": "string"
}
}
}
},
"disable_direct_connections": true
}
```

View File

@ -167,6 +167,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
},
"derp": {
"config": {
"block_direct": true,
"path": "string",
"url": "string"
},

View File

@ -232,6 +232,7 @@
}
},
"directory": "string",
"disable_direct_connections": true,
"environment_variables": {
"property1": "string",
"property2": "string"
@ -257,21 +258,22 @@
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | |
| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| `directory` | string | false | | |
| `environment_variables` | object | false | | |
| » `[any property]` | string | false | | |
| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. |
| `metadata` | array of [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | |
| `motd_file` | string | false | | |
| `shutdown_script` | string | false | | |
| `shutdown_script_timeout` | integer | false | | |
| `startup_script` | string | false | | |
| `startup_script_timeout` | integer | false | | |
| `vscode_port_proxy_uri` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | |
| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| `directory` | string | false | | |
| `disable_direct_connections` | boolean | false | | |
| `environment_variables` | object | false | | |
| » `[any property]` | string | false | | |
| `git_auth_configs` | integer | false | | Git auth configs stores the number of Git configurations the Coder deployment has. If this number is >0, we set up special configuration in the workspace. |
| `metadata` | array of [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | |
| `motd_file` | string | false | | |
| `shutdown_script` | string | false | | |
| `shutdown_script_timeout` | integer | false | | |
| `startup_script` | string | false | | |
| `startup_script_timeout` | integer | false | | |
| `vscode_port_proxy_uri` | string | false | | |
## agentsdk.PatchStartupLogs
@ -1669,6 +1671,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"config": {
"block_direct": true,
"path": "string",
"url": "string"
},
@ -1706,6 +1709,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"block_direct": true,
"path": "string",
"url": "string"
}
@ -1713,10 +1717,11 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
| Name | Type | Required | Restrictions | Description |
| ------ | ------ | -------- | ------------ | ----------- |
| `path` | string | false | | |
| `url` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| -------------- | ------- | -------- | ------------ | ----------- |
| `block_direct` | boolean | false | | |
| `path` | string | false | | |
| `url` | string | false | | |
## codersdk.DERPRegion
@ -1839,6 +1844,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
},
"derp": {
"config": {
"block_direct": true,
"path": "string",
"url": "string"
},
@ -2167,6 +2173,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
},
"derp": {
"config": {
"block_direct": true,
"path": "string",
"url": "string"
},
@ -4763,15 +4770,17 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"regionName": "string"
}
}
}
},
"disable_direct_connections": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ---------- | ---------------------------------- | -------- | ------------ | ----------- |
| `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| Name | Type | Required | Restrictions | Description |
| ---------------------------- | ---------------------------------- | -------- | ------------ | ----------- |
| `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| `disable_direct_connections` | boolean | false | | |
## codersdk.WorkspaceAgentLifecycle

View File

@ -30,6 +30,16 @@ coder server [flags]
The URL that users will use to access the Coder deployment.
### --block-direct-connections
| | |
| ----------- | ---------------------------------------- |
| Type | <code>bool</code> |
| Environment | <code>$CODER_BLOCK_DIRECT</code> |
| YAML | <code>networking.derp.blockDirect</code> |
Block peer-to-peer (aka. direct) workspace connections. All workspace connections from the CLI will be proxied through Coder (or custom configured DERP servers) and will never be peer-to-peer when enabled. Workspaces may still reach out to STUN servers to get their address until they are restarted after this change has been made, but new connections will still be proxied regardless.
### --browser-only
| | |

View File

@ -150,6 +150,14 @@ between workspaces and users are peer-to-peer. However, when Coder cannot
establish a peer to peer connection, Coder uses a distributed relay network
backed by Tailscale and WireGuard.
--block-direct-connections bool, $CODER_BLOCK_DIRECT
Block peer-to-peer (aka. direct) workspace connections. All workspace
connections from the CLI will be proxied through Coder (or custom
configured DERP servers) and will never be peer-to-peer when enabled.
Workspaces may still reach out to STUN servers to get their address
until they are restarted after this change has been made, but new
connections will still be proxied regardless.
--derp-config-path string, $CODER_DERP_CONFIG_PATH
Path to read a DERP mapping from. See:
https://tailscale.com/kb/1118/custom-derp-servers/.

View File

@ -281,6 +281,7 @@ export interface DERP {
// From codersdk/deployment.go
export interface DERPConfig {
readonly block_direct: boolean
readonly url: string
readonly path: string
}

View File

@ -353,6 +353,14 @@ func (c *Conn) SetDERPMap(derpMap *tailcfg.DERPMap) {
c.wireguardEngine.SetNetworkMap(&netMapCopy)
}
// SetBlockEndpoints sets whether or not to block P2P endpoints. This setting
// will only apply to new peers.
func (c *Conn) SetBlockEndpoints(blockEndpoints bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.blockEndpoints = blockEndpoints
}
// SetDERPRegionDialer updates the dialer to use for connecting to DERP regions.
func (c *Conn) SetDERPRegionDialer(dialer func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn) {
c.magicConn.SetDERPRegionDialer(dialer)
@ -516,6 +524,13 @@ func (c *Conn) DERPMap() *tailcfg.DERPMap {
return c.netMap.DERPMap
}
// BlockEndpoints returns whether or not P2P is blocked.
func (c *Conn) BlockEndpoints() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.blockEndpoints
}
// AwaitReachable pings the provided IP continually until the
// address is reachable. It's the callers responsibility to provide
// a timeout, otherwise this function will block forever.

View File

@ -15,10 +15,16 @@ import (
// NewDERPMap constructs a DERPMap from a set of STUN addresses and optionally a remote
// URL to fetch a mapping from e.g. https://controlplane.tailscale.com/derpmap/default.
func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []string, remoteURL, localPath string) (*tailcfg.DERPMap, error) {
//
//nolint:revive
func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []string, remoteURL, localPath string, disableSTUN bool) (*tailcfg.DERPMap, error) {
if remoteURL != "" && localPath != "" {
return nil, xerrors.New("a remote URL or local path must be specified, not both")
}
if disableSTUN {
stunAddrs = nil
}
if region != nil {
for index, stunAddr := range stunAddrs {
host, rawPort, err := net.SplitHostPort(stunAddr)
@ -74,5 +80,20 @@ func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []str
}
derpMap.Regions[region.RegionID] = region
}
// Remove all STUNPorts from DERPy nodes, and fully remove all STUNOnly
// nodes.
if disableSTUN {
for _, region := range derpMap.Regions {
newNodes := make([]*tailcfg.DERPNode, 0, len(region.Nodes))
for _, node := range region.Nodes {
node.STUNPort = -1
if !node.STUNOnly {
newNodes = append(newNodes, node)
}
}
region.Nodes = newNodes
}
}
return derpMap, nil
}

View File

@ -22,7 +22,7 @@ func TestNewDERPMap(t *testing.T) {
derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{
RegionID: 1,
Nodes: []*tailcfg.DERPNode{{}},
}, []string{"stun.google.com:2345"}, "", "")
}, []string{"stun.google.com:2345"}, "", "", false)
require.NoError(t, err)
require.Len(t, derpMap.Regions[1].Nodes, 2)
})
@ -39,7 +39,7 @@ func TestNewDERPMap(t *testing.T) {
t.Cleanup(server.Close)
derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{
RegionID: 2,
}, []string{}, server.URL, "")
}, []string{}, server.URL, "", false)
require.NoError(t, err)
require.Len(t, derpMap.Regions, 2)
})
@ -56,7 +56,7 @@ func TestNewDERPMap(t *testing.T) {
t.Cleanup(server.Close)
_, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{
RegionID: 1,
}, []string{}, server.URL, "")
}, []string{}, server.URL, "", false)
require.Error(t, err)
})
t.Run("LocalPath", func(t *testing.T) {
@ -72,8 +72,61 @@ func TestNewDERPMap(t *testing.T) {
require.NoError(t, err)
derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{
RegionID: 2,
}, []string{}, "", localPath)
}, []string{}, "", localPath, false)
require.NoError(t, err)
require.Len(t, derpMap.Regions, 2)
})
t.Run("DisableSTUN", func(t *testing.T) {
t.Parallel()
localPath := filepath.Join(t.TempDir(), "derp.json")
content, err := json.Marshal(&tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
Nodes: []*tailcfg.DERPNode{{
STUNPort: 1234,
}},
},
2: {
Nodes: []*tailcfg.DERPNode{
{
STUNPort: 1234,
},
{
STUNPort: 12345,
},
{
STUNOnly: true,
STUNPort: 54321,
},
},
},
},
})
require.NoError(t, err)
err = os.WriteFile(localPath, content, 0o600)
require.NoError(t, err)
region := &tailcfg.DERPRegion{
RegionID: 3,
Nodes: []*tailcfg.DERPNode{{
STUNPort: 1234,
}},
}
derpMap, err := tailnet.NewDERPMap(context.Background(), region, []string{"127.0.0.1:54321"}, "", localPath, true)
require.NoError(t, err)
require.Len(t, derpMap.Regions, 3)
require.Len(t, derpMap.Regions[1].Nodes, 1)
require.EqualValues(t, -1, derpMap.Regions[1].Nodes[0].STUNPort)
// The STUNOnly node should get removed.
require.Len(t, derpMap.Regions[2].Nodes, 2)
require.EqualValues(t, -1, derpMap.Regions[2].Nodes[0].STUNPort)
require.False(t, derpMap.Regions[2].Nodes[0].STUNOnly)
require.EqualValues(t, -1, derpMap.Regions[2].Nodes[1].STUNPort)
require.False(t, derpMap.Regions[2].Nodes[1].STUNOnly)
// We don't add any nodes ourselves if STUN is disabled.
require.Len(t, derpMap.Regions[3].Nodes, 1)
// ... but we still remove the STUN port from existing nodes in the
// region.
require.EqualValues(t, -1, derpMap.Regions[3].Nodes[0].STUNPort)
})
}