Skip to content

[pull] main from coder:main #110

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 2 commits into from
Jul 24, 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
4 changes: 2 additions & 2 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
}
api.derpMesh.SetAddresses(addresses, false)
}
_ = api.updateEntitlements(ctx)
_ = api.updateEntitlements(api.ctx)
})
} else {
coordinator = agpltailnet.NewCoordinator(api.Logger)
Expand All @@ -840,7 +840,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
api.replicaManager.SetCallback(func() {
// If the amount of replicas change, so should our entitlements.
// This is to display a warning in the UI if the user is unlicensed.
_ = api.updateEntitlements(ctx)
_ = api.updateEntitlements(api.ctx)
})
}

Expand Down
9 changes: 7 additions & 2 deletions enterprise/coderd/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,15 @@ func LicensesEntitlements(
if featureArguments.ManagedAgentCountFn != nil {
managedAgentCount, err = featureArguments.ManagedAgentCountFn(ctx, agentLimit.UsagePeriod.Start, agentLimit.UsagePeriod.End)
}
if err != nil {
switch {
case xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded):
// If the context is canceled, we want to bail the entire
// LicensesEntitlements call.
return entitlements, xerrors.Errorf("get managed agent count: %w", err)
case err != nil:
entitlements.Errors = append(entitlements.Errors,
fmt.Sprintf("Error getting managed agent count: %s", err.Error()))
} else {
default:
agentLimit.Actual = &managedAgentCount
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, agentLimit)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ replace github.com/tcnksm/go-httpstat => github.com/coder/go-httpstat v0.0.0-202

// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250724015444-494197765996

// This is replaced to include
// 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -926,8 +926,8 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c h1:d/qBIi3Ez7KkopRgNtfdvTMqvqBg47d36qVfkd3C5EQ=
github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c/go.mod h1:l7ml5uu7lFh5hY28lGYM4b/oFSmuPHYX6uk4RAu23Lc=
github.com/coder/tailscale v1.1.1-0.20250724015444-494197765996 h1:9x+ouDw9BKW1tdGzuQOWGMT2XkWLs+QQjeCrxYuU1lo=
github.com/coder/tailscale v1.1.1-0.20250724015444-494197765996/go.mod h1:l7ml5uu7lFh5hY28lGYM4b/oFSmuPHYX6uk4RAu23Lc=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/coder/terraform-provider-coder/v2 v2.7.1-0.20250623193313-e890833351e2 h1:vtGzECz5CyzuxMODexWdIRxhYLqyTcHafuJpH60PYhM=
Expand Down
22 changes: 20 additions & 2 deletions tailnet/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ const EnvMagicsockDebugLogging = "CODER_MAGICSOCK_DEBUG_LOGGING"

func init() {
// Globally disable network namespacing. All networking happens in
// userspace.
// userspace unless the connection is configured to use a TUN.
// NOTE: this exists in init() so it affects all connections (incl. DERP)
// made by tailscale packages by default.
netns.SetEnabled(false)
// Tailscale, by default, "trims" the set of peers down to ones that we are
// "actively" communicating with in an effort to save memory. Since
Expand Down Expand Up @@ -100,6 +102,18 @@ type Options struct {
BlockEndpoints bool
Logger slog.Logger
ListenPort uint16
// UseSoftNetIsolation enables our homemade soft isolation feature in the
// netns package. This option will only be considered if TUNDev is set.
//
// The Coder soft isolation mode is a workaround to allow Coder Connect to
// connect to Coder servers behind corporate VPNs, and relaxes some of the
// loop protections that come with Tailscale.
//
// When soft isolation is disabled, the netns package will function as
// normal and route all traffic through the default interface (and block all
// traffic to other VPN interfaces) on macOS and Windows.
UseSoftNetIsolation bool

// CaptureHook is a callback that captures Disco packets and packets sent
// into the tailnet tunnel.
CaptureHook capture.Callback
Expand Down Expand Up @@ -154,7 +168,11 @@ func NewConn(options *Options) (conn *Conn, err error) {
return nil, xerrors.New("At least one IP range must be provided")
}

netns.SetEnabled(options.TUNDev != nil)
useNetNS := options.TUNDev != nil
useSoftIsolation := useNetNS && options.UseSoftNetIsolation
options.Logger.Debug(context.Background(), "network isolation configuration", slog.F("use_netns", useNetNS), slog.F("use_soft_isolation", useSoftIsolation))
netns.SetEnabled(useNetNS)
netns.SetCoderSoftIsolation(useSoftIsolation)

var telemetryStore *TelemetryStore
if options.TelemetrySink != nil {
Expand Down
16 changes: 9 additions & 7 deletions vpn/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ func NewClient() Client {
}

type Options struct {
Headers http.Header
Logger slog.Logger
DNSConfigurator dns.OSConfigurator
Router router.Router
TUNDevice tun.Device
WireguardMonitor *netmon.Monitor
UpdateHandler tailnet.UpdatesHandler
Headers http.Header
Logger slog.Logger
UseSoftNetIsolation bool
DNSConfigurator dns.OSConfigurator
Router router.Router
TUNDevice tun.Device
WireguardMonitor *netmon.Monitor
UpdateHandler tailnet.UpdatesHandler
}

type derpMapRewriter struct {
Expand Down Expand Up @@ -163,6 +164,7 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
DERPForceWebSockets: connInfo.DERPForceWebSockets,
Logger: options.Logger,
BlockEndpoints: connInfo.DisableDirectConnections,
UseSoftNetIsolation: options.UseSoftNetIsolation,
DNSConfigurator: options.DNSConfigurator,
Router: options.Router,
TUNDev: options.TUNDevice,
Expand Down
2 changes: 1 addition & 1 deletion vpn/speaker_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestMain(m *testing.M) {
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
}

const expectedHandshake = "codervpn tunnel 1.2\n"
const expectedHandshake = "codervpn tunnel 1.3\n"

// TestSpeaker_RawPeer tests the speaker with a peer that we simulate by directly making reads and
// writes to the other end of the pipe. There should be at least one test that does this, rather
Expand Down
15 changes: 8 additions & 7 deletions vpn/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,14 @@ func (t *Tunnel) start(req *StartRequest) error {
svrURL,
apiToken,
&Options{
Headers: header,
Logger: t.clientLogger,
DNSConfigurator: networkingStack.DNSConfigurator,
Router: networkingStack.Router,
TUNDevice: networkingStack.TUNDevice,
WireguardMonitor: networkingStack.WireguardMonitor,
UpdateHandler: t,
Headers: header,
Logger: t.clientLogger,
UseSoftNetIsolation: req.GetTunnelUseSoftNetIsolation(),
DNSConfigurator: networkingStack.DNSConfigurator,
Router: networkingStack.Router,
TUNDevice: networkingStack.TUNDevice,
WireguardMonitor: networkingStack.WireguardMonitor,
UpdateHandler: t,
},
)
if err != nil {
Expand Down
99 changes: 71 additions & 28 deletions vpn/tunnel_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package vpn

import (
"context"
"encoding/json"
"maps"
"net"
"net/http"
"net/netip"
"net/url"
"slices"
Expand All @@ -22,32 +24,51 @@ import (
"github.com/coder/quartz"

maputil "github.com/coder/coder/v2/coderd/util/maps"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/testutil"
)

func newFakeClient(ctx context.Context, t *testing.T) *fakeClient {
return &fakeClient{
t: t,
ctx: ctx,
ch: make(chan *fakeConn, 1),
t: t,
ctx: ctx,
connCh: make(chan *fakeConn, 1),
}
}

func newFakeClientWithOptsCh(ctx context.Context, t *testing.T) *fakeClient {
return &fakeClient{
t: t,
ctx: ctx,
connCh: make(chan *fakeConn, 1),
optsCh: make(chan *Options, 1),
}
}

type fakeClient struct {
t *testing.T
ctx context.Context
ch chan *fakeConn
t *testing.T
ctx context.Context
connCh chan *fakeConn
optsCh chan *Options // options will be written to this channel if it's not nil
}

var _ Client = (*fakeClient)(nil)

func (f *fakeClient) NewConn(context.Context, *url.URL, string, *Options) (Conn, error) {
func (f *fakeClient) NewConn(_ context.Context, _ *url.URL, _ string, opts *Options) (Conn, error) {
if f.optsCh != nil {
select {
case <-f.ctx.Done():
return nil, f.ctx.Err()
case f.optsCh <- opts:
}
}

select {
case <-f.ctx.Done():
return nil, f.ctx.Err()
case conn := <-f.ch:
case conn := <-f.connCh:
return conn, nil
}
}
Expand Down Expand Up @@ -134,37 +155,53 @@ func TestTunnel_StartStop(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitShort)
client := newFakeClient(ctx, t)
client := newFakeClientWithOptsCh(ctx, t)
conn := newFakeConn(tailnet.WorkspaceUpdate{}, time.Time{})

_, mgr := setupTunnel(t, ctx, client, quartz.NewMock(t))

errCh := make(chan error, 1)
var resp *TunnelMessage
// When: we start the tunnel
telemetry := codersdk.CoderDesktopTelemetry{
DeviceID: "device001",
DeviceOS: "macOS",
CoderDesktopVersion: "0.24.8",
}
telemetryJSON, err := json.Marshal(telemetry)
require.NoError(t, err)
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Start{
Start: &StartRequest{
TunnelFileDescriptor: 2,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
// Use default value for TunnelUseSoftNetIsolation
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
Headers: []*StartRequest_Header{
{Name: "X-Test-Header", Value: "test"},
},
DeviceOs: "macOS",
DeviceId: "device001",
CoderDesktopVersion: "0.24.8",
DeviceOs: telemetry.DeviceOS,
DeviceId: telemetry.DeviceID,
CoderDesktopVersion: telemetry.CoderDesktopVersion,
},
},
})
resp = r
errCh <- err
}()
// Then: `NewConn` is called,
testutil.RequireSend(ctx, t, client.ch, conn)

// Then: `NewConn` is called
opts := testutil.RequireReceive(ctx, t, client.optsCh)
require.Equal(t, http.Header{
"X-Test-Header": {"test"},
codersdk.CoderDesktopTelemetryHeader: {string(telemetryJSON)},
}, opts.Headers)
require.False(t, opts.UseSoftNetIsolation) // the default is false
testutil.RequireSend(ctx, t, client.connCh, conn)

// And: a response is received
err := testutil.TryReceive(ctx, t, errCh)
err = testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
require.True(t, ok)
Expand Down Expand Up @@ -197,7 +234,7 @@ func TestTunnel_PeerUpdate(t *testing.T) {
wsID1 := uuid.UUID{1}
wsID2 := uuid.UUID{2}

client := newFakeClient(ctx, t)
client := newFakeClientWithOptsCh(ctx, t)
conn := newFakeConn(tailnet.WorkspaceUpdate{
UpsertedWorkspaces: []*tailnet.Workspace{
{
Expand All @@ -211,22 +248,28 @@ func TestTunnel_PeerUpdate(t *testing.T) {

tun, mgr := setupTunnel(t, ctx, client, quartz.NewMock(t))

// When: we start the tunnel
errCh := make(chan error, 1)
var resp *TunnelMessage
go func() {
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
Msg: &ManagerMessage_Start{
Start: &StartRequest{
TunnelFileDescriptor: 2,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
TunnelFileDescriptor: 2,
TunnelUseSoftNetIsolation: true,
CoderUrl: "https://coder.example.com",
ApiToken: "fakeToken",
},
},
})
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)

// Then: `NewConn` is called
opts := testutil.RequireReceive(ctx, t, client.optsCh)
require.True(t, opts.UseSoftNetIsolation)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -291,7 +334,7 @@ func TestTunnel_NetworkSettings(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -432,7 +475,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -603,7 +646,7 @@ func TestTunnel_sendAgentUpdateReconnect(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -703,7 +746,7 @@ func TestTunnel_sendAgentUpdateWorkspaceReconnect(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -806,7 +849,7 @@ func TestTunnel_slowPing(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down Expand Up @@ -895,7 +938,7 @@ func TestTunnel_stopMidPing(t *testing.T) {
resp = r
errCh <- err
}()
testutil.RequireSend(ctx, t, client.ch, conn)
testutil.RequireSend(ctx, t, client.connCh, conn)
err := testutil.TryReceive(ctx, t, errCh)
require.NoError(t, err)
_, ok := resp.Msg.(*TunnelMessage_Start)
Expand Down
Loading
Loading