@@ -7,12 +7,18 @@ import (
7
7
"context"
8
8
"fmt"
9
9
"io"
10
+ "net"
10
11
"net/http"
11
12
"net/netip"
12
13
"net/url"
14
+ "os"
15
+ "os/exec"
16
+ "path/filepath"
13
17
"strconv"
14
18
"strings"
19
+ "sync"
15
20
"sync/atomic"
21
+ "syscall"
16
22
"testing"
17
23
"time"
18
24
41
47
Client2ID = uuid .MustParse ("00000000-0000-0000-0000-000000000002" )
42
48
)
43
49
44
- type ServerOptions struct {
50
+ type TestTopology struct {
51
+ Name string
52
+ // SetupNetworking creates interfaces and network namespaces for the test.
53
+ // The most simple implementation is NetworkSetupDefault, which only creates
54
+ // a network namespace shared for all tests.
55
+ SetupNetworking func (t * testing.T , logger slog.Logger ) TestNetworking
56
+
57
+ // Server is the server starter for the test. It is executed in the server
58
+ // subprocess.
59
+ Server ServerStarter
60
+ // StartClient gets called in each client subprocess. It's expected to
61
+ // create the tailnet.Conn and ensure connectivity to it's peer.
62
+ StartClient func (t * testing.T , logger slog.Logger , serverURL * url.URL , myID uuid.UUID , peerID uuid.UUID ) * tailnet.Conn
63
+
64
+ // RunTests is the main test function. It's called in each of the client
65
+ // subprocesses. If tests can only run once, they should check the client ID
66
+ // and return early if it's not the expected one.
67
+ RunTests func (t * testing.T , logger slog.Logger , serverURL * url.URL , myID uuid.UUID , peerID uuid.UUID , conn * tailnet.Conn )
68
+ }
69
+
70
+ type ServerStarter interface {
71
+ // StartServer should start the server and return once it's listening. It
72
+ // should not block once it's listening. Cleanup should be handled by
73
+ // t.Cleanup.
74
+ StartServer (t * testing.T , logger slog.Logger , listenAddr string )
75
+ }
76
+
77
+ type SimpleServerOptions struct {
45
78
// FailUpgradeDERP will make the DERP server fail to handle the initial DERP
46
79
// upgrade in a way that causes the client to fallback to
47
80
// DERP-over-WebSocket fallback automatically.
@@ -54,8 +87,10 @@ type ServerOptions struct {
54
87
DERPWebsocketOnly bool
55
88
}
56
89
90
+ var _ ServerStarter = SimpleServerOptions {}
91
+
57
92
//nolint:revive
58
- func (o ServerOptions ) Router (t * testing.T , logger slog.Logger ) * chi.Mux {
93
+ func (o SimpleServerOptions ) Router (t * testing.T , logger slog.Logger ) * chi.Mux {
59
94
coord := tailnet .NewCoordinator (logger )
60
95
var coordPtr atomic.Pointer [tailnet.Coordinator ]
61
96
coordPtr .Store (& coord )
@@ -157,6 +192,76 @@ func (o ServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux {
157
192
return r
158
193
}
159
194
195
+ func (o SimpleServerOptions ) StartServer (t * testing.T , logger slog.Logger , listenAddr string ) {
196
+ srv := http.Server {
197
+ Addr : listenAddr ,
198
+ Handler : o .Router (t , logger ),
199
+ ReadTimeout : 10 * time .Second ,
200
+ }
201
+ serveDone := make (chan struct {})
202
+ go func () {
203
+ defer close (serveDone )
204
+ err := srv .ListenAndServe ()
205
+ if err != nil && ! xerrors .Is (err , http .ErrServerClosed ) {
206
+ t .Error ("HTTP server error:" , err )
207
+ }
208
+ }()
209
+ t .Cleanup (func () {
210
+ _ = srv .Close ()
211
+ <- serveDone
212
+ })
213
+ }
214
+
215
+ type NGINXServerOptions struct {
216
+ SimpleServerOptions
217
+ }
218
+
219
+ var _ ServerStarter = NGINXServerOptions {}
220
+
221
+ func (o NGINXServerOptions ) StartServer (t * testing.T , logger slog.Logger , listenAddr string ) {
222
+ host , nginxPortStr , err := net .SplitHostPort (listenAddr )
223
+ require .NoError (t , err )
224
+
225
+ nginxPort , err := strconv .Atoi (nginxPortStr )
226
+ require .NoError (t , err )
227
+
228
+ serverPort := nginxPort + 1
229
+ serverListenAddr := net .JoinHostPort (host , strconv .Itoa (serverPort ))
230
+
231
+ o .SimpleServerOptions .StartServer (t , logger , serverListenAddr )
232
+ startNginx (t , nginxPortStr , serverListenAddr )
233
+ }
234
+
235
+ func startNginx (t * testing.T , listenPort , serverAddr string ) {
236
+ cfg := `events {}
237
+ http {
238
+ server {
239
+ listen ` + listenPort + `;
240
+ server_name _;
241
+ location / {
242
+ proxy_pass http://` + serverAddr + `;
243
+ proxy_http_version 1.1;
244
+ proxy_set_header Upgrade $http_upgrade;
245
+ proxy_set_header Connection "upgrade";
246
+ proxy_set_header Host $host;
247
+ proxy_set_header X-Real-IP $remote_addr;
248
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
249
+ proxy_set_header X-Forwarded-Proto $scheme;
250
+ proxy_set_header X-Forwarded-Host $server_name;
251
+ }
252
+ }
253
+ }
254
+ `
255
+
256
+ dir := t .TempDir ()
257
+ cfgPath := filepath .Join (dir , "nginx.conf" )
258
+ err := os .WriteFile (cfgPath , []byte (cfg ), 0o600 )
259
+ require .NoError (t , err )
260
+
261
+ // ExecBackground will handle cleanup.
262
+ _ , _ = ExecBackground (t , "server.nginx" , nil , "nginx" , []string {"-c" , cfgPath })
263
+ }
264
+
160
265
// StartClientDERP creates a client connection to the server for coordination
161
266
// and creates a tailnet.Conn which will only use DERP to connect to the peer.
162
267
func StartClientDERP (t * testing.T , logger slog.Logger , serverURL * url.URL , myID , peerID uuid.UUID ) * tailnet.Conn {
@@ -296,3 +401,126 @@ func basicDERPMap(t *testing.T, serverURL *url.URL) *tailcfg.DERPMap {
296
401
},
297
402
}
298
403
}
404
+
405
+ // ExecBackground starts a subprocess with the given flags and returns a
406
+ // channel that will receive the error when the subprocess exits. The returned
407
+ // function can be used to close the subprocess.
408
+ //
409
+ // processName is used to identify the subprocess in logs.
410
+ //
411
+ // Optionally, a network namespace can be passed to run the subprocess in.
412
+ //
413
+ // Do not call close then wait on the channel. Use the returned value from the
414
+ // function instead in this case.
415
+ //
416
+ // Cleanup is handled automatically if you don't care about monitoring the
417
+ // process manually.
418
+ func ExecBackground (t * testing.T , processName string , netNS * os.File , name string , args []string ) (<- chan error , func () error ) {
419
+ if netNS != nil {
420
+ // We use nsenter to enter the namespace.
421
+ // We can't use `setns` easily from Golang in the parent process because
422
+ // you can't execute the syscall in the forked child thread before it
423
+ // execs.
424
+ // We can't use `setns` easily from Golang in the child process because
425
+ // by the time you call it, the process has already created multiple
426
+ // threads.
427
+ args = append ([]string {"--net=/proc/self/fd/3" , name }, args ... )
428
+ name = "nsenter"
429
+ }
430
+
431
+ cmd := exec .Command (name , args ... )
432
+ if netNS != nil {
433
+ cmd .ExtraFiles = []* os.File {netNS }
434
+ }
435
+
436
+ out := & testWriter {
437
+ name : processName ,
438
+ t : t ,
439
+ }
440
+ t .Cleanup (out .Flush )
441
+ cmd .Stdout = out
442
+ cmd .Stderr = out
443
+ cmd .SysProcAttr = & syscall.SysProcAttr {
444
+ Pdeathsig : syscall .SIGTERM ,
445
+ }
446
+ err := cmd .Start ()
447
+ require .NoError (t , err )
448
+
449
+ waitErr := make (chan error , 1 )
450
+ go func () {
451
+ err := cmd .Wait ()
452
+ waitErr <- err
453
+ close (waitErr )
454
+ }()
455
+
456
+ closeFn := func () error {
457
+ _ = cmd .Process .Signal (syscall .SIGTERM )
458
+ select {
459
+ case <- time .After (5 * time .Second ):
460
+ _ = cmd .Process .Kill ()
461
+ case err := <- waitErr :
462
+ return err
463
+ }
464
+ return <- waitErr
465
+ }
466
+
467
+ t .Cleanup (func () {
468
+ select {
469
+ case err := <- waitErr :
470
+ if err != nil {
471
+ t .Logf ("subprocess exited: " + err .Error ())
472
+ }
473
+ return
474
+ default :
475
+ }
476
+
477
+ _ = closeFn ()
478
+ })
479
+
480
+ return waitErr , closeFn
481
+ }
482
+
483
+ type testWriter struct {
484
+ mut sync.Mutex
485
+ name string
486
+ t * testing.T
487
+
488
+ capturedLines []string
489
+ }
490
+
491
+ func (w * testWriter ) Write (p []byte ) (n int , err error ) {
492
+ w .mut .Lock ()
493
+ defer w .mut .Unlock ()
494
+ str := string (p )
495
+ split := strings .Split (str , "\n " )
496
+ for _ , s := range split {
497
+ if s == "" {
498
+ continue
499
+ }
500
+
501
+ // If a line begins with "\s*--- (PASS|FAIL)" or is just PASS or FAIL,
502
+ // then it's a test result line. We want to capture it and log it later.
503
+ trimmed := strings .TrimSpace (s )
504
+ if strings .HasPrefix (trimmed , "--- PASS" ) || strings .HasPrefix (trimmed , "--- FAIL" ) || trimmed == "PASS" || trimmed == "FAIL" {
505
+ // Also fail the test if we see a FAIL line.
506
+ if strings .Contains (trimmed , "FAIL" ) {
507
+ w .t .Errorf ("subprocess logged test failure: %s: \t %s" , w .name , s )
508
+ }
509
+
510
+ w .capturedLines = append (w .capturedLines , s )
511
+ continue
512
+ }
513
+
514
+ w .t .Logf ("%s output: \t %s" , w .name , s )
515
+ }
516
+ return len (p ), nil
517
+ }
518
+
519
+ func (w * testWriter ) Flush () {
520
+ w .mut .Lock ()
521
+ defer w .mut .Unlock ()
522
+ for _ , s := range w .capturedLines {
523
+ w .t .Logf ("%s output: \t %s" , w .name , s )
524
+ }
525
+ w .capturedLines = nil
526
+ }
0 commit comments