Skip to content

Commit ab736e4

Browse files
authored
Make Control safe to stop and wait on from any lifecycle state (#1794)
1 parent 5ecdd4e commit ab736e4

10 files changed

Lines changed: 441 additions & 58 deletions

File tree

cmd/nebula-service/main.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ func main() {
5353
l := logging.NewLogger(os.Stdout)
5454

5555
if *serviceFlag != "" {
56-
if err := doService(configPath, configTest, Build, serviceFlag); err != nil {
56+
if *configTest {
57+
fmt.Println("-test is not supported with -service, run the config test without -service")
58+
os.Exit(1)
59+
}
60+
61+
if err := doService(configPath, Build, serviceFlag); err != nil {
5762
l.Error("Service command failed", "error", err)
5863
os.Exit(1)
5964
}
@@ -93,15 +98,14 @@ func main() {
9398
}
9499

95100
if !*configTest {
96-
wait, err := ctrl.Start()
97-
if err != nil {
101+
if err := ctrl.Start(); err != nil {
98102
util.LogWithContextIfNeeded("Error while running", err, l)
99103
os.Exit(1)
100104
}
101105

102106
go ctrl.ShutdownBlock()
103107

104-
if err := wait(); err != nil {
108+
if err := ctrl.Wait(); err != nil {
105109
l.Error("Nebula stopped due to fatal error", "error", err)
106110
os.Exit(2)
107111
}

cmd/nebula-service/service.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"log"
6+
"os"
67

78
"github.com/kardianos/service"
89
"github.com/slackhq/nebula"
@@ -14,7 +15,6 @@ var logger service.Logger
1415

1516
type program struct {
1617
configPath *string
17-
configTest *bool
1818
build string
1919
control *nebula.Control
2020
}
@@ -40,22 +40,41 @@ func (p *program) Start(s service.Service) error {
4040
}
4141
})
4242

43-
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
43+
p.control, err = nebula.Main(c, false, Build, l, nil)
4444
if err != nil {
4545
return err
4646
}
4747

48-
p.control.Start()
48+
if err := p.control.Start(); err != nil {
49+
return err
50+
}
51+
52+
// Nebula can stop itself on a fatal packet reader error, make sure to log it if it happens.
53+
go func() {
54+
if err := p.control.Wait(); err != nil {
55+
logger.Error(fmt.Sprintf("Nebula stopped due to fatal error: %v", err))
56+
os.Exit(2)
57+
}
58+
}()
59+
4960
return nil
5061
}
5162

5263
func (p *program) Stop(s service.Service) error {
5364
logger.Info("Nebula service stopping.")
65+
if p.control == nil {
66+
return nil
67+
}
68+
5469
p.control.Stop()
70+
71+
// block until nebula has fully drained before reporting stopped.
72+
// error logging is handled by Start.
73+
_ = p.control.Wait()
5574
return nil
5675
}
5776

58-
func doService(configPath *string, configTest *bool, build string, serviceFlag *string) error {
77+
func doService(configPath *string, build string, serviceFlag *string) error {
5978
if *configPath == "" {
6079
p, err := config.DefaultPath()
6180
if err != nil {
@@ -73,7 +92,6 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
7392

7493
prg := &program{
7594
configPath: configPath,
76-
configTest: configTest,
7795
build: build,
7896
}
7997

@@ -105,8 +123,9 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
105123
switch *serviceFlag {
106124
case "run":
107125
if err := s.Run(); err != nil {
108-
// Route any errors to the system logger
126+
// Route any errors to the system logger and report the failure
109127
logger.Error(err)
128+
return err
110129
}
111130
default:
112131
if err := service.Control(s, *serviceFlag); err != nil {

cmd/nebula/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,15 @@ func main() {
8484
}
8585

8686
if !*configTest {
87-
wait, err := ctrl.Start()
88-
if err != nil {
87+
if err := ctrl.Start(); err != nil {
8988
util.LogWithContextIfNeeded("Error while running", err, l)
9089
os.Exit(1)
9190
}
9291

9392
go ctrl.ShutdownBlock()
9493
notifyReady(l)
9594

96-
if err := wait(); err != nil {
95+
if err := ctrl.Wait(); err != nil {
9796
l.Error("Nebula stopped due to fatal error", "error", err)
9897
os.Exit(2)
9998
}

control.go

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,29 +69,29 @@ type ControlHostInfo struct {
6969
}
7070

7171
// Start actually runs nebula, this is a nonblocking call.
72-
// The returned function blocks until nebula has fully stopped and returns the
73-
// first fatal reader error (if any). A nil error means nebula shut down
74-
// gracefully; a non-nil error means a reader hit an unexpected failure that
75-
// triggered the shutdown.
76-
func (c *Control) Start() (func() error, error) {
72+
// Use Wait to block until nebula has fully stopped and to learn whether a fatal reader error caused the shutdown.
73+
func (c *Control) Start() error {
7774
c.stateLock.Lock()
7875
defer c.stateLock.Unlock()
7976
switch c.state {
8077
case StateReady:
8178
//yay!
8279
case StateStopped, StateStopping:
83-
return nil, ErrAlreadyStopped
80+
return ErrAlreadyStopped
8481
case StateStarted:
85-
return nil, ErrAlreadyStarted
82+
return ErrAlreadyStarted
8683
default:
87-
return nil, ErrUnknownState
84+
return ErrUnknownState
8885
}
8986

9087
// Activate the interface
9188
err := c.f.activate()
9289
if err != nil {
90+
// Cancel before Close so a caller returning from Wait always observes a dead Context
91+
c.cancel()
92+
_ = c.f.Close()
9393
c.state = StateStopped
94-
return nil, err
94+
return err
9595
}
9696

9797
// Call all the delayed funcs that waited patiently for the interface to be created.
@@ -114,13 +114,9 @@ func (c *Control) Start() (func() error, error) {
114114
c.f.triggerShutdown = c.Stop
115115

116116
// Start reading packets.
117-
out, err := c.f.run()
118-
if err != nil {
119-
c.state = StateStopped
120-
return nil, err
121-
}
117+
c.f.run()
122118
c.state = StateStarted
123-
return out, nil
119+
return nil
124120
}
125121

126122
func (c *Control) State() RunState {
@@ -133,10 +129,26 @@ func (c *Control) Context() context.Context {
133129
return c.ctx
134130
}
135131

136-
// Stop is a non-blocking call that signals nebula to close all tunnels and shut down
132+
// Stop tears nebula down, closing all tunnels and releasing everything it holds.
133+
// Use Wait to block until the shutdown has completed.
134+
// A Control that has been stopped cannot be started again, Start will return ErrAlreadyStopped.
137135
func (c *Control) Stop() {
138136
c.stateLock.Lock()
139-
if c.state != StateStarted {
137+
switch c.state {
138+
case StateStarted:
139+
// Fall through to the full teardown below
140+
141+
case StateReady:
142+
// Never started
143+
c.cancel()
144+
c.state = StateStopped
145+
if err := c.f.Close(); err != nil {
146+
c.l.Error("Close interface failed", "error", err)
147+
}
148+
c.stateLock.Unlock()
149+
return
150+
151+
default:
140152
c.stateLock.Unlock()
141153
// We are stopping or stopped already
142154
return
@@ -145,19 +157,26 @@ func (c *Control) Stop() {
145157
c.state = StateStopping
146158
c.stateLock.Unlock()
147159

148-
// Stop the handshakeManager (and other services), to prevent new tunnels from
149-
// being created while we're shutting them all down.
160+
// Closing tunnels can be slow with a large hostmap, don't hold the lock for it
150161
c.cancel()
151-
152162
c.CloseAllTunnels(false)
163+
164+
c.stateLock.Lock()
165+
c.state = StateStopped
153166
if err := c.f.Close(); err != nil {
154167
c.l.Error("Close interface failed", "error", err)
155168
}
156-
c.stateLock.Lock()
157-
c.state = StateStopped
158169
c.stateLock.Unlock()
159170
}
160171

172+
// Wait blocks until nebula has fully stopped, either via Stop or an internal fatal error,
173+
// and returns the first fatal packet reader error if there was one.
174+
// It is safe to call from multiple goroutines and at any point in the lifecycle,
175+
// but a Wait on a Control that is never started and never stopped will block forever.
176+
func (c *Control) Wait() error {
177+
return c.f.wait()
178+
}
179+
161180
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
162181
func (c *Control) ShutdownBlock() {
163182
sigChan := make(chan os.Signal, 1)
@@ -170,8 +189,15 @@ func (c *Control) ShutdownBlock() {
170189
c.Stop()
171190
}
172191

173-
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change
192+
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change.
174193
func (c *Control) RebindUDPServer() {
194+
c.stateLock.Lock()
195+
defer c.stateLock.Unlock()
196+
197+
if c.state != StateStarted {
198+
return
199+
}
200+
175201
_ = c.f.outside.Rebind()
176202

177203
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0

0 commit comments

Comments
 (0)