mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: use client preferred URL for the default DERP (#18911)
The agentsdk currently does a remap of the DERP map to change the EmbeddedRelay node's URL to match the agent's access URL. This PR makes changes to the `workspacesdk` (used by clients like the CLI) and `vpn` (used by Coder Desktop) to match this behavior. This enables us the ability to try Coder clients in dogfood over a VPN without changing the global access URL.
This commit is contained in:
+19
-8
@@ -568,13 +568,15 @@ type DERPMapSetter interface {
|
||||
}
|
||||
|
||||
type basicDERPController struct {
|
||||
logger slog.Logger
|
||||
setter DERPMapSetter
|
||||
logger slog.Logger
|
||||
rewriter DERPMapRewriter // optional
|
||||
setter DERPMapSetter
|
||||
}
|
||||
|
||||
func (b *basicDERPController) New(client DERPClient) CloserWaiter {
|
||||
l := &derpSetLoop{
|
||||
logger: b.logger,
|
||||
rewriter: b.rewriter,
|
||||
setter: b.setter,
|
||||
client: client,
|
||||
errChan: make(chan error, 1),
|
||||
@@ -584,17 +586,23 @@ func (b *basicDERPController) New(client DERPClient) CloserWaiter {
|
||||
return l
|
||||
}
|
||||
|
||||
func NewBasicDERPController(logger slog.Logger, setter DERPMapSetter) DERPController {
|
||||
// NewBasicDERPController creates a DERP controller that rewrites the DERP map
|
||||
// with the provided rewriter before setting it on the provided setter.
|
||||
//
|
||||
// The rewriter is optional and can be nil.
|
||||
func NewBasicDERPController(logger slog.Logger, rewriter DERPMapRewriter, setter DERPMapSetter) DERPController {
|
||||
return &basicDERPController{
|
||||
logger: logger,
|
||||
setter: setter,
|
||||
logger: logger,
|
||||
rewriter: rewriter,
|
||||
setter: setter,
|
||||
}
|
||||
}
|
||||
|
||||
type derpSetLoop struct {
|
||||
logger slog.Logger
|
||||
setter DERPMapSetter
|
||||
client DERPClient
|
||||
logger slog.Logger
|
||||
rewriter DERPMapRewriter // optional
|
||||
setter DERPMapSetter
|
||||
client DERPClient
|
||||
|
||||
sync.Mutex
|
||||
closed bool
|
||||
@@ -640,6 +648,9 @@ func (l *derpSetLoop) recvLoop() {
|
||||
return
|
||||
}
|
||||
l.logger.Debug(context.Background(), "got new DERP Map", slog.F("derp_map", dm))
|
||||
if l.rewriter != nil {
|
||||
l.rewriter.RewriteDERPMap(dm)
|
||||
}
|
||||
l.setter.SetDERPMap(dm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@ func TestNewBasicDERPController_Mainline(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := make(chan *tailcfg.DERPMap)
|
||||
logger := testutil.Logger(t)
|
||||
uut := tailnet.NewBasicDERPController(logger, fakeSetter(fs))
|
||||
uut := tailnet.NewBasicDERPController(logger, nil, fakeSetter(fs))
|
||||
fc := fakeDERPClient{
|
||||
ch: make(chan *tailcfg.DERPMap),
|
||||
}
|
||||
@@ -609,7 +609,7 @@ func TestNewBasicDERPController_RecvErr(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := make(chan *tailcfg.DERPMap)
|
||||
logger := testutil.Logger(t)
|
||||
uut := tailnet.NewBasicDERPController(logger, fakeSetter(fs))
|
||||
uut := tailnet.NewBasicDERPController(logger, nil, fakeSetter(fs))
|
||||
expectedErr := xerrors.New("a bad thing happened")
|
||||
fc := fakeDERPClient{
|
||||
ch: make(chan *tailcfg.DERPMap),
|
||||
@@ -1041,7 +1041,7 @@ func TestController_Disconnects(t *testing.T) {
|
||||
// darwin can be slow sometimes.
|
||||
tailnet.WithGracefulTimeout(5*time.Second))
|
||||
uut.CoordCtrl = tailnet.NewAgentCoordinationController(logger.Named("coord_ctrl"), fConn)
|
||||
uut.DERPCtrl = tailnet.NewBasicDERPController(logger.Named("derp_ctrl"), fConn)
|
||||
uut.DERPCtrl = tailnet.NewBasicDERPController(logger.Named("derp_ctrl"), nil, fConn)
|
||||
uut.Run(ctx)
|
||||
|
||||
call := testutil.TryReceive(testCtx, t, fCoord.CoordinateCalls)
|
||||
@@ -1945,6 +1945,52 @@ func TestTunnelAllWorkspaceUpdatesController_HandleErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicDERPController_RewriteDERPMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
testDERPMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
RegionID: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure the fake rewriter works as expected.
|
||||
rewriter := &fakeDERPMapRewriter{
|
||||
ctx: ctx,
|
||||
calls: make(chan rewriteDERPMapCall, 16),
|
||||
}
|
||||
rewriter.RewriteDERPMap(testDERPMap)
|
||||
rewriteCall := testutil.TryReceive(ctx, t, rewriter.calls)
|
||||
require.Same(t, testDERPMap, rewriteCall.derpMap)
|
||||
|
||||
derpClient := &fakeDERPClient{
|
||||
ch: make(chan *tailcfg.DERPMap),
|
||||
err: nil,
|
||||
}
|
||||
defer derpClient.Close()
|
||||
|
||||
derpSetter := &fakeDERPMapSetter{
|
||||
ctx: ctx,
|
||||
calls: make(chan *setDERPMapCall, 16),
|
||||
}
|
||||
|
||||
derpCtrl := tailnet.NewBasicDERPController(logger, rewriter, derpSetter)
|
||||
derpCtrl.New(derpClient)
|
||||
|
||||
// Simulate receiving a new DERP map from the server, which should be passed
|
||||
// to the rewriter and setter.
|
||||
testDERPMap = testDERPMap.Clone() // make a new pointer
|
||||
derpClient.ch <- testDERPMap
|
||||
rewriteCall = testutil.TryReceive(ctx, t, rewriter.calls)
|
||||
require.Same(t, testDERPMap, rewriteCall.derpMap)
|
||||
setterCall := testutil.TryReceive(ctx, t, derpSetter.calls)
|
||||
require.Same(t, testDERPMap, setterCall.derpMap)
|
||||
}
|
||||
|
||||
type fakeWorkspaceUpdatesController struct {
|
||||
ctx context.Context
|
||||
t testing.TB
|
||||
@@ -2040,3 +2086,45 @@ type fakeCloser struct{}
|
||||
func (fakeCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeDERPMapRewriter struct {
|
||||
ctx context.Context
|
||||
calls chan rewriteDERPMapCall
|
||||
}
|
||||
|
||||
var _ tailnet.DERPMapRewriter = &fakeDERPMapRewriter{}
|
||||
|
||||
type rewriteDERPMapCall struct {
|
||||
derpMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (f *fakeDERPMapRewriter) RewriteDERPMap(derpMap *tailcfg.DERPMap) {
|
||||
call := rewriteDERPMapCall{
|
||||
derpMap: derpMap,
|
||||
}
|
||||
select {
|
||||
case f.calls <- call:
|
||||
case <-f.ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
type fakeDERPMapSetter struct {
|
||||
ctx context.Context
|
||||
calls chan *setDERPMapCall
|
||||
}
|
||||
|
||||
var _ tailnet.DERPMapSetter = &fakeDERPMapSetter{}
|
||||
|
||||
type setDERPMapCall struct {
|
||||
derpMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (f *fakeDERPMapSetter) SetDERPMap(derpMap *tailcfg.DERPMap) {
|
||||
call := &setDERPMapCall{
|
||||
derpMap: derpMap,
|
||||
}
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
case f.calls <- call:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
@@ -70,3 +75,54 @@ func WithWebsocketSupport(s *derp.Server, base http.Handler) (http.Handler, func
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type DERPMapRewriter interface {
|
||||
RewriteDERPMap(derpMap *tailcfg.DERPMap)
|
||||
}
|
||||
|
||||
// RewriteDERPMapDefaultRelay rewrites the DERP map to use the given access URL
|
||||
// as the "embedded relay" access URL. The passed derp map is modified in place.
|
||||
//
|
||||
// This is used by clients and agents to rewrite the default DERP relay to use
|
||||
// their preferred access URL. Both of these clients can use a different access
|
||||
// URL than the deployment has configured (with `--access-url`), so we need to
|
||||
// accommodate that and respect the locally configured access URL.
|
||||
//
|
||||
// Note: passed context is only used for logging.
|
||||
func RewriteDERPMapDefaultRelay(ctx context.Context, logger slog.Logger, derpMap *tailcfg.DERPMap, accessURL *url.URL) {
|
||||
if derpMap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessPort := 80
|
||||
if accessURL.Scheme == "https" {
|
||||
accessPort = 443
|
||||
}
|
||||
if accessURL.Port() != "" {
|
||||
parsedAccessPort, err := strconv.Atoi(accessURL.Port())
|
||||
if err != nil {
|
||||
// This should never happen because URL.Port() returns the empty string
|
||||
// if the port is not valid.
|
||||
logger.Critical(ctx, "failed to parse URL port, using default port",
|
||||
slog.F("port", accessURL.Port()),
|
||||
slog.F("access_url", accessURL))
|
||||
} else {
|
||||
accessPort = parsedAccessPort
|
||||
}
|
||||
}
|
||||
|
||||
for _, region := range derpMap.Regions {
|
||||
if !region.EmbeddedRelay {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, node := range region.Nodes {
|
||||
if node.STUNOnly {
|
||||
continue
|
||||
}
|
||||
node.HostName = accessURL.Hostname()
|
||||
node.DERPPort = accessPort
|
||||
node.ForceHTTP = accessURL.Scheme == "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user