diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 561848ba4a1bb..f37c030a18963 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11976,6 +11976,9 @@ const docTemplate = `{ "app_security_key": { "type": "string" }, + "derp_map": { + "$ref": "#/definitions/tailcfg.DERPMap" + }, "derp_mesh_key": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ee7c78c369698..8c2334acfea76 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10937,6 +10937,9 @@ "app_security_key": { "type": "string" }, + "derp_map": { + "$ref": "#/definitions/tailcfg.DERPMap" + }, "derp_mesh_key": { "type": "string" }, diff --git a/coderd/coderd.go b/coderd/coderd.go index e63c295423626..5f3bbab1a0360 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -404,7 +404,7 @@ func New(options *Options) *API { api.agentProvider, err = NewServerTailnet(api.ctx, options.Logger, options.DERPServer, - options.BaseDERPMap, + api.DERPMap, func(context.Context) (tailnet.MultiAgentConn, error) { return (*api.TailnetCoordinator.Load()).ServeMultiAgent(uuid.New()), nil }, diff --git a/coderd/tailnet.go b/coderd/tailnet.go index da9ccdff0ec3f..048fd9752f4e0 100644 --- a/coderd/tailnet.go +++ b/coderd/tailnet.go @@ -44,15 +44,16 @@ func NewServerTailnet( ctx context.Context, logger slog.Logger, derpServer *derp.Server, - derpMap *tailcfg.DERPMap, + derpMapFn func() *tailcfg.DERPMap, getMultiAgent func(context.Context) (tailnet.MultiAgentConn, error), cache *wsconncache.Cache, traceProvider trace.TracerProvider, ) (*ServerTailnet, error) { logger = logger.Named("servertailnet") + originalDerpMap := derpMapFn() conn, err := tailnet.NewConn(&tailnet.Options{ Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)}, - DERPMap: derpMap, + DERPMap: originalDerpMap, Logger: logger, }) if err != nil { @@ -60,9 +61,32 @@ func NewServerTailnet( } serverCtx, cancel := context.WithCancel(ctx) + derpMapUpdaterClosed := make(chan struct{}) + go func() { + defer close(derpMapUpdaterClosed) + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-serverCtx.Done(): + return + case <-ticker.C: + } + + newDerpMap := derpMapFn() + if !tailnet.CompareDERPMaps(originalDerpMap, newDerpMap) { + conn.SetDERPMap(newDerpMap) + originalDerpMap = newDerpMap + } + } + }() + tn := &ServerTailnet{ ctx: serverCtx, cancel: cancel, + derpMapUpdaterClosed: derpMapUpdaterClosed, logger: logger, tracer: traceProvider.Tracer(tracing.TracerName), conn: conn, @@ -237,8 +261,9 @@ func (s *ServerTailnet) reinitCoordinator() { } type ServerTailnet struct { - ctx context.Context - cancel func() + ctx context.Context + cancel func() + derpMapUpdaterClosed chan struct{} logger slog.Logger tracer trace.Tracer @@ -403,5 +428,6 @@ func (s *ServerTailnet) Close() error { _ = s.cache.Close() _ = s.conn.Close() s.transport.CloseIdleConnections() + <-s.derpMapUpdaterClosed return nil } diff --git a/coderd/tailnet_test.go b/coderd/tailnet_test.go index fcc21a1c9740c..634a715abfbd7 100644 --- a/coderd/tailnet_test.go +++ b/coderd/tailnet_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" + "tailscale.com/tailcfg" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" @@ -230,7 +231,7 @@ func setupAgent(t *testing.T, agentAddresses []netip.Prefix) (uuid.UUID, agent.A context.Background(), logger, derpServer, - manifest.DERPMap, + func() *tailcfg.DERPMap { return manifest.DERPMap }, func(context.Context) (tailnet.MultiAgentConn, error) { return coord.ServeMultiAgent(uuid.New()), nil }, cache, trace.NewNoopTracerProvider(), diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 019043cfda687..c3a133358bdb8 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -7724,6 +7724,65 @@ _None_ ```json { "app_security_key": "string", + "derp_map": { + "homeParams": { + "regionScore": { + "property1": 0, + "property2": 0 + } + }, + "omitDefaultRegions": true, + "regions": { + "property1": { + "avoid": true, + "embeddedRelay": true, + "nodes": [ + { + "canPort80": true, + "certName": "string", + "derpport": 0, + "forceHTTP": true, + "hostName": "string", + "insecureForTests": true, + "ipv4": "string", + "ipv6": "string", + "name": "string", + "regionID": 0, + "stunonly": true, + "stunport": 0, + "stuntestIP": "string" + } + ], + "regionCode": "string", + "regionID": 0, + "regionName": "string" + }, + "property2": { + "avoid": true, + "embeddedRelay": true, + "nodes": [ + { + "canPort80": true, + "certName": "string", + "derpport": 0, + "forceHTTP": true, + "hostName": "string", + "insecureForTests": true, + "ipv4": "string", + "ipv6": "string", + "name": "string", + "regionID": 0, + "stunonly": true, + "stunport": 0, + "stuntestIP": "string" + } + ], + "regionCode": "string", + "regionID": 0, + "regionName": "string" + } + } + }, "derp_mesh_key": "string", "derp_region_id": 0, "sibling_replicas": [ @@ -7745,6 +7804,7 @@ _None_ | Name | Type | Required | Restrictions | Description | | ------------------ | --------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------- | | `app_security_key` | string | false | | | +| `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | | `derp_mesh_key` | string | false | | | | `derp_region_id` | integer | false | | | | `sibling_replicas` | array of [codersdk.Replica](#codersdkreplica) | false | | Sibling replicas is a list of all other replicas of the proxy that have not timed out. | diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index a5c14b93d7760..faf665ffb8e5d 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -720,6 +720,7 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request) AppSecurityKey: api.AppSecurityKey.String(), DERPMeshKey: api.DERPServer.MeshKey(), DERPRegionID: regionID, + DERPMap: api.AGPL.DERPMap(), SiblingReplicas: siblingsRes, }) diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 1c561987bfd9d..9b3deab5624a2 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -21,6 +21,7 @@ import ( "golang.org/x/xerrors" "tailscale.com/derp" "tailscale.com/derp/derphttp" + "tailscale.com/tailcfg" "tailscale.com/types/key" "cdr.dev/slog" @@ -119,7 +120,8 @@ type Server struct { SDKClient *wsproxysdk.Client // DERP - derpMesh *derpmesh.Mesh + derpMesh *derpmesh.Mesh + latestDERPMap *tailcfg.DERPMap // Used for graceful shutdown. Required for the dialer. ctx context.Context @@ -239,17 +241,14 @@ func New(ctx context.Context, opts *Options) (*Server, error) { return nil, xerrors.Errorf("parse app security key: %w", err) } - connInfo, err := client.SDKClient.WorkspaceAgentConnectionInfoGeneric(ctx) - if err != nil { - return nil, xerrors.Errorf("get derpmap: %w", err) - } - var agentProvider workspaceapps.AgentProvider if opts.Experiments.Enabled(codersdk.ExperimentSingleTailnet) { stn, err := coderd.NewServerTailnet(ctx, s.Logger, nil, - connInfo.DERPMap, + func() *tailcfg.DERPMap { + return s.latestDERPMap + }, s.DialCoordinator, wsconncache.New(s.DialWorkspaceAgent, 0), s.TracerProvider, @@ -456,6 +455,8 @@ func (s *Server) handleRegister(_ context.Context, res wsproxysdk.RegisterWorksp } s.derpMesh.SetAddresses(addresses, false) + s.latestDERPMap = res.DERPMap + return nil } diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go index bd2c8001338ab..711614908d834 100644 --- a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go +++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "golang.org/x/xerrors" "nhooyr.io/websocket" + "tailscale.com/tailcfg" "tailscale.com/util/singleflight" "cdr.dev/slog" @@ -206,9 +207,10 @@ type RegisterWorkspaceProxyRequest struct { } type RegisterWorkspaceProxyResponse struct { - AppSecurityKey string `json:"app_security_key"` - DERPMeshKey string `json:"derp_mesh_key"` - DERPRegionID int32 `json:"derp_region_id"` + AppSecurityKey string `json:"app_security_key"` + DERPMeshKey string `json:"derp_mesh_key"` + DERPRegionID int32 `json:"derp_region_id"` + DERPMap *tailcfg.DERPMap `json:"derp_map"` // SiblingReplicas is a list of all other replicas of the proxy that have // not timed out. SiblingReplicas []codersdk.Replica `json:"sibling_replicas"`