Skip to content

Commit 147c202

Browse files
nbrownusJackDoan
andauthored
Swap back to a blocking udp socket, test shutdown(2) (#1806)
Co-authored-by: Jack Doan <me@jackdoan.com>
1 parent e290a68 commit 147c202

3 files changed

Lines changed: 437 additions & 161 deletions

File tree

cmd/nebula/close_on_timer_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//go:build linux && !android && !e2e_testing
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"net/netip"
8+
"os"
9+
"path/filepath"
10+
"runtime"
11+
"testing"
12+
"time"
13+
14+
"github.com/slackhq/nebula"
15+
"github.com/slackhq/nebula/cert"
16+
cert_test "github.com/slackhq/nebula/cert_test"
17+
"github.com/slackhq/nebula/config"
18+
"github.com/slackhq/nebula/test"
19+
"github.com/stretchr/testify/require"
20+
)
21+
22+
// TestControlStopClosesOnTimer reproduces the dnclient lifecycle: nebula runs as
23+
// a library, and on a config update dnclient calls Stop() in-process to tear the
24+
// old instance down before starting a new one. This boots a real nebula (real
25+
// blocking UDP sockets, tun disabled), lets it run, then Stop()s it on a timer
26+
// and asserts it actually closes. If the reader goroutines parked in recvmmsg
27+
// don't wake on Close(), Wait() blocks forever and this fails with a goroutine
28+
// dump instead of relying on a process signal to unstick them.
29+
func TestControlStopClosesOnTimer(t *testing.T) {
30+
l := test.NewLogger()
31+
dir := t.TempDir()
32+
33+
before := time.Now().Add(-time.Hour)
34+
after := time.Now().Add(time.Hour)
35+
ca, _, caKey, caPEM := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, before, after, nil, nil, nil)
36+
networks := []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")}
37+
_, _, keyPEM, certPEM := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, "close-on-timer", before, after, networks, nil, nil)
38+
39+
caPath := filepath.Join(dir, "ca.pem")
40+
certPath := filepath.Join(dir, "cert.pem")
41+
keyPath := filepath.Join(dir, "key.pem")
42+
require.NoError(t, os.WriteFile(caPath, caPEM, 0o600))
43+
require.NoError(t, os.WriteFile(certPath, certPEM, 0o600))
44+
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
45+
46+
// tun disabled so no device/root is needed; routines: 2 so we exercise the
47+
// multi-socket (SO_REUSEPORT) teardown, which is where dnclient runs.
48+
configBody := fmt.Sprintf(`
49+
pki:
50+
ca: %s
51+
cert: %s
52+
key: %s
53+
listen:
54+
host: 127.0.0.1
55+
port: 0
56+
tun:
57+
disabled: true
58+
firewall:
59+
outbound:
60+
- port: any
61+
proto: any
62+
host: any
63+
inbound:
64+
- port: any
65+
proto: any
66+
host: any
67+
routines: 2
68+
`, caPath, certPath, keyPath)
69+
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(configBody), 0o600))
70+
71+
c := config.NewC(l)
72+
require.NoError(t, c.Load(dir))
73+
74+
ctrl, err := nebula.Main(c, false, "close-on-timer", l, nil)
75+
require.NoError(t, err)
76+
require.NoError(t, ctrl.Start())
77+
78+
// Run like a live nebula, then close on a timer, exactly as dnclient does.
79+
<-time.NewTimer(5 * time.Second).C
80+
81+
stopped := make(chan struct{})
82+
go func() {
83+
ctrl.Stop() // closes the udp sockets (shutdown(2)) and the tun
84+
ctrl.Wait() // blocks until every reader goroutine has returned
85+
close(stopped)
86+
}()
87+
88+
select {
89+
case <-stopped:
90+
t.Log("nebula closed cleanly on timer")
91+
case <-time.After(10 * time.Second):
92+
buf := make([]byte, 1<<20)
93+
n := runtime.Stack(buf, true)
94+
t.Fatalf("nebula did NOT close within 10s of Stop(): a blocking reader never woke\n%s", buf[:n])
95+
}
96+
}

0 commit comments

Comments
 (0)