@@ -2,6 +2,8 @@ package wsproxy
2
2
3
3
import (
4
4
"context"
5
+ "crypto/tls"
6
+ "crypto/x509"
5
7
"fmt"
6
8
"net/http"
7
9
"net/url"
@@ -13,6 +15,7 @@ import (
13
15
14
16
"github.com/go-chi/chi/v5"
15
17
"github.com/google/uuid"
18
+ "github.com/hashicorp/go-multierror"
16
19
"github.com/prometheus/client_golang/prometheus"
17
20
"go.opentelemetry.io/otel/trace"
18
21
"golang.org/x/xerrors"
@@ -28,6 +31,7 @@ import (
28
31
"github.com/coder/coder/coderd/workspaceapps"
29
32
"github.com/coder/coder/coderd/wsconncache"
30
33
"github.com/coder/coder/codersdk"
34
+ "github.com/coder/coder/enterprise/derpmesh"
31
35
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
32
36
"github.com/coder/coder/site"
33
37
"github.com/coder/coder/tailnet"
@@ -57,11 +61,13 @@ type Options struct {
57
61
RealIPConfig * httpmw.RealIPConfig
58
62
Tracing trace.TracerProvider
59
63
PrometheusRegistry * prometheus.Registry
64
+ TLSCertificates []tls.Certificate
60
65
61
- APIRateLimit int
62
- SecureAuthCookie bool
63
- DisablePathApps bool
64
- DERPEnabled bool
66
+ APIRateLimit int
67
+ SecureAuthCookie bool
68
+ DisablePathApps bool
69
+ DERPEnabled bool
70
+ DERPServerRelayAddress string
65
71
66
72
ProxySessionToken string
67
73
}
@@ -101,10 +107,14 @@ type Server struct {
101
107
// the moon's token.
102
108
SDKClient * wsproxysdk.Client
103
109
110
+ // DERP
111
+ derpMesh * derpmesh.Mesh
112
+
104
113
// Used for graceful shutdown. Required for the dialer.
105
114
ctx context.Context
106
115
cancel context.CancelFunc
107
116
derpCloseFunc func ()
117
+ registerDone <- chan struct {}
108
118
}
109
119
110
120
// New creates a new workspace proxy server. This requires a primary coderd
@@ -139,35 +149,33 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
139
149
return nil , xerrors .Errorf ("%q is a workspace proxy, not a primary coderd instance" , opts .DashboardURL )
140
150
}
141
151
142
- // TODO: registering logic need to be moved to a struct that calls it
143
- // periodically
144
- replicaID := uuid .New ()
145
- osHostname , err := os .Hostname ()
146
- if err != nil {
147
- return nil , xerrors .Errorf ("get OS hostname: %w" , err )
152
+ meshRootCA := x509 .NewCertPool ()
153
+ for _ , certificate := range opts .TLSCertificates {
154
+ for _ , certificatePart := range certificate .Certificate {
155
+ certificate , err := x509 .ParseCertificate (certificatePart )
156
+ if err != nil {
157
+ return nil , xerrors .Errorf ("parse certificate %s: %w" , certificate .Subject .CommonName , err )
158
+ }
159
+ meshRootCA .AddCert (certificate )
160
+ }
148
161
}
149
- regResp , err := client .RegisterWorkspaceProxy (ctx , wsproxysdk.RegisterWorkspaceProxyRequest {
150
- AccessURL : opts .AccessURL .String (),
151
- WildcardHostname : opts .AppHostname ,
152
- DerpEnabled : opts .DERPEnabled ,
153
- ReplicaID : replicaID ,
154
- ReplicaHostname : osHostname ,
155
- ReplicaError : "" ,
156
- // TODO: replica relay address
157
- ReplicaRelayAddress : "" ,
158
- Version : buildinfo .Version (),
159
- })
160
- if err != nil {
161
- return nil , xerrors .Errorf ("register proxy: %w" , err )
162
+ // This TLS configuration spoofs access from the access URL hostname
163
+ // assuming that the certificates provided will cover that hostname.
164
+ //
165
+ // Replica sync and DERP meshing require accessing replicas via their
166
+ // internal IP addresses, and if TLS is configured we use the same
167
+ // certificates.
168
+ meshTLSConfig := & tls.Config {
169
+ MinVersion : tls .VersionTLS12 ,
170
+ Certificates : opts .TLSCertificates ,
171
+ RootCAs : meshRootCA ,
172
+ ServerName : opts .AccessURL .Hostname (),
162
173
}
163
174
164
- secKey , err := workspaceapps .KeyFromString (regResp .AppSecurityKey )
165
- if err != nil {
166
- return nil , xerrors .Errorf ("parse app security key: %w" , err )
167
- }
175
+ derpServer := derp .NewServer (key .NewNode (), tailnet .Logger (opts .Logger .Named ("derp" )))
168
176
169
- r := chi .NewRouter ()
170
177
ctx , cancel := context .WithCancel (context .Background ())
178
+ r := chi .NewRouter ()
171
179
s := & Server {
172
180
Options : opts ,
173
181
Handler : r ,
@@ -176,10 +184,48 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
176
184
TracerProvider : opts .Tracing ,
177
185
PrometheusRegistry : opts .PrometheusRegistry ,
178
186
SDKClient : client ,
187
+ derpMesh : derpmesh .New (opts .Logger .Named ("derpmesh" ), derpServer , meshTLSConfig ),
179
188
ctx : ctx ,
180
189
cancel : cancel ,
181
190
}
182
191
192
+ // Register the workspace proxy with the primary coderd instance and start a
193
+ // goroutine to periodically re-register.
194
+ replicaID := uuid .New ()
195
+ osHostname , err := os .Hostname ()
196
+ if err != nil {
197
+ return nil , xerrors .Errorf ("get OS hostname: %w" , err )
198
+ }
199
+ regResp , registerDone , err := client .RegisterWorkspaceProxyLoop (ctx , wsproxysdk.RegisterWorkspaceProxyLoopOpts {
200
+ Logger : opts .Logger ,
201
+ Request : wsproxysdk.RegisterWorkspaceProxyRequest {
202
+ AccessURL : opts .AccessURL .String (),
203
+ WildcardHostname : opts .AppHostname ,
204
+ DerpEnabled : opts .DERPEnabled ,
205
+ ReplicaID : replicaID ,
206
+ ReplicaHostname : osHostname ,
207
+ ReplicaError : "" ,
208
+ ReplicaRelayAddress : opts .DERPServerRelayAddress ,
209
+ Version : buildinfo .Version (),
210
+ },
211
+ MutateFn : s .mutateRegister ,
212
+ CallbackFn : s .handleRegister ,
213
+ FailureFn : s .handleRegisterFailure ,
214
+ })
215
+ if err != nil {
216
+ return nil , xerrors .Errorf ("register proxy: %w" , err )
217
+ }
218
+ s .registerDone = registerDone
219
+ err = s .handleRegister (ctx , regResp )
220
+ if err != nil {
221
+ return nil , xerrors .Errorf ("handle register: %w" , err )
222
+ }
223
+ derpServer .SetMeshKey (regResp .DERPMeshKey )
224
+
225
+ secKey , err := workspaceapps .KeyFromString (regResp .AppSecurityKey )
226
+ if err != nil {
227
+ return nil , xerrors .Errorf ("parse app security key: %w" , err )
228
+ }
183
229
s .AppServer = & workspaceapps.Server {
184
230
Logger : opts .Logger .Named ("workspaceapps" ),
185
231
DashboardURL : opts .DashboardURL ,
@@ -202,9 +248,6 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
202
248
SecureAuthCookie : opts .SecureAuthCookie ,
203
249
}
204
250
205
- derpServer := derp .NewServer (key .NewNode (), tailnet .Logger (opts .Logger .Named ("derp" )))
206
- // TODO: mesh and derpmesh package stuff
207
- // derpServer.SetMeshKey(regResp.DERPMeshKey)
208
251
derpHandler := derphttp .Handler (derpServer )
209
252
derpHandler , s .derpCloseFunc = tailnet .WithWebsocketSupport (derpServer , derpHandler )
210
253
@@ -279,14 +322,51 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
279
322
280
323
func (s * Server ) Close () error {
281
324
s .cancel ()
325
+
326
+ var err error
327
+ registerDoneWaitTicker := time .NewTicker (3 * time .Second )
328
+ select {
329
+ case <- registerDoneWaitTicker .C :
330
+ err = multierror .Append (err , xerrors .New ("timed out waiting for registerDone" ))
331
+ case <- s .registerDone :
332
+ }
282
333
s .derpCloseFunc ()
283
- return s .AppServer .Close ()
334
+ appServerErr := s .AppServer .Close ()
335
+ if appServerErr != nil {
336
+ err = multierror .Append (err , appServerErr )
337
+ }
338
+ return err
284
339
}
285
340
286
341
func (s * Server ) DialWorkspaceAgent (id uuid.UUID ) (* codersdk.WorkspaceAgentConn , error ) {
287
342
return s .SDKClient .DialWorkspaceAgent (s .ctx , id , nil )
288
343
}
289
344
345
+ func (* Server ) mutateRegister (_ * wsproxysdk.RegisterWorkspaceProxyRequest ) {
346
+ // TODO: we should probably ping replicas similarly to the replicasync
347
+ // package in the primary and update req.ReplicaError accordingly.
348
+ }
349
+
350
+ func (s * Server ) handleRegister (_ context.Context , res wsproxysdk.RegisterWorkspaceProxyResponse ) error {
351
+ addresses := make ([]string , len (res .SiblingReplicas ))
352
+ for i , replica := range res .SiblingReplicas {
353
+ addresses [i ] = replica .RelayAddress
354
+ }
355
+ s .derpMesh .SetAddresses (addresses , false )
356
+
357
+ return nil
358
+ }
359
+
360
+ func (s * Server ) handleRegisterFailure (err error ) {
361
+ if s .ctx .Err () != nil {
362
+ return
363
+ }
364
+ s .Logger .Fatal (s .ctx ,
365
+ "failed to periodically re-register workspace proxy with primary Coder deployment" ,
366
+ slog .Error (err ),
367
+ )
368
+ }
369
+
290
370
func (s * Server ) buildInfo (rw http.ResponseWriter , r * http.Request ) {
291
371
httpapi .Write (r .Context (), rw , http .StatusOK , codersdk.BuildInfoResponse {
292
372
ExternalURL : buildinfo .ExternalURL (),
0 commit comments