mirror of https://github.com/coder/coder.git
feat: add flag to disable all direct connections (#7936)
This commit is contained in:
parent
96f9e61ca1
commit
a28d422c35
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/.",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -167,6 +167,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
|||
},
|
||||
"derp": {
|
||||
"config": {
|
||||
"block_direct": true,
|
||||
"path": "string",
|
||||
"url": "string"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
| | |
|
||||
|
|
|
@ -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/.
|
||||
|
|
|
@ -281,6 +281,7 @@ export interface DERP {
|
|||
|
||||
// From codersdk/deployment.go
|
||||
export interface DERPConfig {
|
||||
readonly block_direct: boolean
|
||||
readonly url: string
|
||||
readonly path: string
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue