cmd/tailscaled: graceful shutdown (#534)

Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
reviewable/pr536/r2
Dmytro Shynkevych 4 years ago committed by GitHub
parent 6255ce55df
commit 61abab999e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,8 +15,10 @@ import (
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"os" "os"
"os/signal"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"syscall"
"time" "time"
"github.com/apenwarr/fixconsole" "github.com/apenwarr/fixconsole"
@ -27,6 +29,7 @@ import (
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/wgengine" "tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
) )
// globalStateKey is the ipn.StateKey that tailscaled loads on // globalStateKey is the ipn.StateKey that tailscaled loads on
@ -52,6 +55,7 @@ func main() {
defaultTunName = "tun" defaultTunName = "tun"
} }
cleanup := getopt.BoolLong("cleanup", 0, "clean up system state and exit")
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap") fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
debug := getopt.StringLong("debug", 0, "", "Address of debug server") debug := getopt.StringLong("debug", 0, "", "Address of debug server")
tunname := getopt.StringLong("tun", 0, defaultTunName, "tunnel interface name") tunname := getopt.StringLong("tun", 0, defaultTunName, "tunnel interface name")
@ -73,6 +77,11 @@ func main() {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0]) log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
} }
if *cleanup {
router.Cleanup(logf, *tunname)
return
}
if *statepath == "" { if *statepath == "" {
log.Fatalf("--state is required") log.Fatalf("--state is required")
} }
@ -98,6 +107,20 @@ func main() {
} }
e = wgengine.NewWatchdog(e) e = wgengine.NewWatchdog(e)
ctx, cancel := context.WithCancel(context.Background())
// Exit gracefully by cancelling the ipnserver context in most common cases:
// interrupted from the TTY or killed by a service manager.
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
select {
case <-interrupt:
cancel()
case <-ctx.Done():
// continue
}
}()
opts := ipnserver.Options{ opts := ipnserver.Options{
SocketPath: *socketpath, SocketPath: *socketpath,
Port: 41112, Port: 41112,
@ -107,15 +130,13 @@ func main() {
SurviveDisconnects: true, SurviveDisconnects: true,
DebugMux: debugMux, DebugMux: debugMux,
} }
err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e) err = ipnserver.Run(ctx, logf, pol.PublicID.String(), opts, e)
if err != nil { if err != nil {
log.Fatalf("tailscaled: %v", err) log.Fatalf("tailscaled: %v", err)
} }
// TODO(crawshaw): It would be nice to start a timeout context the moment a signal // Finish uploading logs after closing everything else.
// is received and use that timeout to give us a moment to finish uploading logs ctx, cancel = context.WithTimeout(context.Background(), time.Second)
// here. But the signal is handled inside ipnserver.Run, so some plumbing is needed.
ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
pol.Shutdown(ctx) pol.Shutdown(ctx)
} }

@ -9,6 +9,7 @@ StartLimitBurst=0
[Service] [Service]
EnvironmentFile=/etc/default/tailscaled EnvironmentFile=/etc/default/tailscaled
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup
Restart=on-failure Restart=on-failure

@ -224,6 +224,7 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
} }
stopAll() stopAll()
b.Shutdown()
return rctx.Err() return rctx.Err()
} }

@ -49,6 +49,7 @@ type LocalBackend struct {
store StateStore store StateStore
backendLogID string backendLogID string
portpoll *portlist.Poller // may be nil portpoll *portlist.Poller // may be nil
portpollOnce sync.Once
newDecompressor func() (controlclient.Decompressor, error) newDecompressor func() (controlclient.Decompressor, error)
// TODO: these fields are accessed unsafely by concurrent // TODO: these fields are accessed unsafely by concurrent
@ -387,8 +388,10 @@ func (b *LocalBackend) Start(opts Options) error {
// At this point, we have finished using hostinfo without synchronization, // At this point, we have finished using hostinfo without synchronization,
// so it is safe to start readPoller which concurrently writes to it. // so it is safe to start readPoller which concurrently writes to it.
if b.portpoll != nil { if b.portpoll != nil {
go b.portpoll.Run(b.ctx) b.portpollOnce.Do(func() {
go b.readPoller() go b.portpoll.Run(b.ctx)
go b.readPoller()
})
} }
b.mu.Lock() b.mu.Lock()

@ -35,6 +35,13 @@ func New(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, err
return newUserspaceRouter(logf, wgdev, tundev) return newUserspaceRouter(logf, wgdev, tundev)
} }
// Cleanup restores the system network configuration to its original state
// in case the Tailscale daemon terminated without closing the router.
// No other state needs to be instantiated before this runs.
func Cleanup(logf logger.Logf, interfaceName string) {
// TODO(dmytro): implement this.
}
// NetfilterMode is the firewall management mode to use when // NetfilterMode is the firewall management mode to use when
// programming the Linux network stack. // programming the Linux network stack.
type NetfilterMode int type NetfilterMode int

Loading…
Cancel
Save