mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
e6fbc0cd54
More work towards removing the massive ipnserver.Run and ipnserver.Options and making composable pieces. Work remains. (The getEngine retry loop on Windows complicates things.) For now some duplicate code exists. Once the Windows side is fixed to either not need the retry loop or to move the retry loop into a custom wgengine.Engine wrapper, then we can unify tailscaled_windows.go too. Change-Id: If84d16e3cd15b54ead3c3bb301f27ae78d055f80 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
522 lines
15 KiB
Go
522 lines
15 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// The tailscaled program is the Tailscale client daemon. It's configured
|
|
// and controlled via the tailscale CLI program.
|
|
//
|
|
// It primarily supports Linux, though other systems will likely be
|
|
// supported in the future.
|
|
package main // import "tailscale.com/cmd/tailscaled"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnserver"
|
|
"tailscale.com/logpolicy"
|
|
"tailscale.com/net/dns"
|
|
"tailscale.com/net/netns"
|
|
"tailscale.com/net/socks5/tssocks"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/paths"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/types/flagtype"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/multierr"
|
|
"tailscale.com/util/osshare"
|
|
"tailscale.com/version"
|
|
"tailscale.com/version/distro"
|
|
"tailscale.com/wgengine"
|
|
"tailscale.com/wgengine/monitor"
|
|
"tailscale.com/wgengine/netstack"
|
|
"tailscale.com/wgengine/router"
|
|
)
|
|
|
|
// defaultTunName returns the default tun device name for the platform.
|
|
func defaultTunName() string {
|
|
switch runtime.GOOS {
|
|
case "openbsd":
|
|
return "tun"
|
|
case "windows":
|
|
return "Tailscale"
|
|
case "darwin":
|
|
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
|
|
// as a magic value that uses/creates any free number.
|
|
return "utun"
|
|
case "linux":
|
|
if distro.Get() == distro.Synology {
|
|
// Try TUN, but fall back to userspace networking if needed.
|
|
// See https://github.com/tailscale/tailscale-synology/issues/35
|
|
return "tailscale0,userspace-networking"
|
|
}
|
|
}
|
|
return "tailscale0"
|
|
}
|
|
|
|
var args struct {
|
|
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
|
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
|
// or comma-separated list thereof.
|
|
tunname string
|
|
|
|
cleanup bool
|
|
debug string
|
|
port uint16
|
|
statepath string
|
|
statedir string
|
|
socketpath string
|
|
birdSocketPath string
|
|
verbose int
|
|
socksAddr string // listen address for SOCKS5 server
|
|
httpProxyAddr string // listen address for HTTP proxy server
|
|
}
|
|
|
|
var (
|
|
installSystemDaemon func([]string) error // non-nil on some platforms
|
|
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
|
createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms
|
|
)
|
|
|
|
var subCommands = map[string]*func([]string) error{
|
|
"install-system-daemon": &installSystemDaemon,
|
|
"uninstall-system-daemon": &uninstallSystemDaemon,
|
|
"debug": &debugModeFunc,
|
|
}
|
|
|
|
func main() {
|
|
// We aren't very performance sensitive, and the parts that are
|
|
// performance sensitive (wireguard) try hard not to do any memory
|
|
// allocations. So let's be aggressive about garbage collection,
|
|
// unless the user specifically overrides it in the usual way.
|
|
if _, ok := os.LookupEnv("GOGC"); !ok {
|
|
debug.SetGCPercent(10)
|
|
}
|
|
|
|
printVersion := false
|
|
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
|
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
|
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
|
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
|
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
|
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
|
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
|
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
|
|
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
|
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
|
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
|
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
|
|
|
if len(os.Args) > 1 {
|
|
sub := os.Args[1]
|
|
if fp, ok := subCommands[sub]; ok {
|
|
if *fp == nil {
|
|
log.SetFlags(0)
|
|
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
|
|
}
|
|
if err := (*fp)(os.Args[2:]); err != nil {
|
|
log.SetFlags(0)
|
|
log.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if beWindowsSubprocess() {
|
|
return
|
|
}
|
|
|
|
flag.Parse()
|
|
if flag.NArg() > 0 {
|
|
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
|
}
|
|
|
|
if printVersion {
|
|
fmt.Println(version.String())
|
|
os.Exit(0)
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
|
|
log.SetFlags(0)
|
|
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
|
}
|
|
|
|
if args.socketpath == "" && runtime.GOOS != "windows" {
|
|
log.SetFlags(0)
|
|
log.Fatalf("--socket is required")
|
|
}
|
|
|
|
if args.birdSocketPath != "" && createBIRDClient == nil {
|
|
log.SetFlags(0)
|
|
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
|
|
}
|
|
|
|
err := run()
|
|
|
|
// Remove file sharing from Windows shell (noop in non-windows)
|
|
osshare.SetFileSharingEnabled(false, logger.Discard)
|
|
|
|
if err != nil {
|
|
// No need to log; the func already did
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func trySynologyMigration(p string) error {
|
|
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
|
return nil
|
|
}
|
|
|
|
fi, err := os.Stat(p)
|
|
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
// File is empty or doesn't exist, try reading from the old path.
|
|
|
|
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
|
|
if _, err := os.Stat(oldPath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(oldPath, p); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func statePathOrDefault() string {
|
|
if args.statepath != "" {
|
|
return args.statepath
|
|
}
|
|
if args.statedir != "" {
|
|
return filepath.Join(args.statedir, "tailscaled.state")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func ipnServerOpts() (o ipnserver.Options) {
|
|
// Allow changing the OS-specific IPN behavior for tests
|
|
// so we can e.g. test Windows-specific behaviors on Linux.
|
|
goos := os.Getenv("TS_DEBUG_TAILSCALED_IPN_GOOS")
|
|
if goos == "" {
|
|
goos = runtime.GOOS
|
|
}
|
|
|
|
o.VarRoot = args.statedir
|
|
|
|
// If an absolute --state is provided but not --statedir, try to derive
|
|
// a state directory.
|
|
if o.VarRoot == "" && filepath.IsAbs(args.statepath) {
|
|
if dir := filepath.Dir(args.statepath); strings.EqualFold(filepath.Base(dir), "tailscale") {
|
|
o.VarRoot = dir
|
|
}
|
|
}
|
|
|
|
switch goos {
|
|
default:
|
|
o.SurviveDisconnects = true
|
|
o.AutostartStateKey = ipn.GlobalDaemonStateKey
|
|
case "windows":
|
|
// Not those.
|
|
}
|
|
return o
|
|
}
|
|
|
|
func run() error {
|
|
var err error
|
|
|
|
pol := logpolicy.New("tailnode.log.tailscale.io")
|
|
pol.SetVerbosityLevel(args.verbose)
|
|
defer func() {
|
|
// Finish uploading logs after closing everything else.
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
pol.Shutdown(ctx)
|
|
}()
|
|
|
|
if isWindowsService() {
|
|
// Run the IPN server from the Windows service manager.
|
|
log.Printf("Running service...")
|
|
if err := runWindowsService(pol); err != nil {
|
|
log.Printf("runservice: %v", err)
|
|
}
|
|
log.Printf("Service ended.")
|
|
return nil
|
|
}
|
|
|
|
var logf logger.Logf = log.Printf
|
|
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
|
logf = logger.RusagePrefixLog(logf)
|
|
}
|
|
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
|
|
|
if args.cleanup {
|
|
if os.Getenv("TS_PLEASE_PANIC") != "" {
|
|
panic("TS_PLEASE_PANIC asked us to panic")
|
|
}
|
|
dns.Cleanup(logf, args.tunname)
|
|
router.Cleanup(logf, args.tunname)
|
|
return nil
|
|
}
|
|
|
|
if args.statepath == "" && args.statedir == "" {
|
|
log.Fatalf("--statedir (or at least --state) is required")
|
|
}
|
|
if err := trySynologyMigration(statePathOrDefault()); err != nil {
|
|
log.Printf("error in synology migration: %v", err)
|
|
}
|
|
|
|
var debugMux *http.ServeMux
|
|
if args.debug != "" {
|
|
debugMux = newDebugMux()
|
|
go runDebugServer(debugMux, args.debug)
|
|
}
|
|
|
|
linkMon, err := monitor.New(logf)
|
|
if err != nil {
|
|
log.Fatalf("creating link monitor: %v", err)
|
|
}
|
|
pol.Logtail.SetLinkMonitor(linkMon)
|
|
|
|
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
|
|
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
|
|
|
|
e, useNetstack, err := createEngine(logf, linkMon)
|
|
if err != nil {
|
|
logf("wgengine.New: %v", err)
|
|
return err
|
|
}
|
|
|
|
ns, err := newNetstack(logf, e)
|
|
if err != nil {
|
|
return fmt.Errorf("newNetstack: %w", err)
|
|
}
|
|
ns.ProcessLocalIPs = useNetstack
|
|
ns.ProcessSubnets = useNetstack || wrapNetstack
|
|
if err := ns.Start(); err != nil {
|
|
log.Fatalf("failed to start netstack: %v", err)
|
|
}
|
|
|
|
if socksListener != nil || httpProxyListener != nil {
|
|
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
|
if httpProxyListener != nil {
|
|
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
|
|
go func() {
|
|
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
|
}()
|
|
}
|
|
if socksListener != nil {
|
|
go func() {
|
|
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
|
}()
|
|
}
|
|
}
|
|
|
|
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.
|
|
interrupt := make(chan os.Signal, 1)
|
|
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
// SIGPIPE sometimes gets generated when CLIs disconnect from
|
|
// tailscaled. The default action is to terminate the process, we
|
|
// want to keep running.
|
|
signal.Ignore(syscall.SIGPIPE)
|
|
go func() {
|
|
select {
|
|
case s := <-interrupt:
|
|
logf("tailscaled got signal %v; shutting down", s)
|
|
cancel()
|
|
case <-ctx.Done():
|
|
// continue
|
|
}
|
|
}()
|
|
|
|
opts := ipnServerOpts()
|
|
|
|
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
|
|
if err != nil {
|
|
logf("ipnserver.New: %v", err)
|
|
return err
|
|
}
|
|
|
|
if debugMux != nil {
|
|
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
|
}
|
|
|
|
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
|
if err != nil {
|
|
return fmt.Errorf("safesocket.Listen: %v", err)
|
|
}
|
|
|
|
err = srv.Run(ctx, ln)
|
|
// Cancelation is not an error: it is the only way to stop ipnserver.
|
|
if err != nil && err != context.Canceled {
|
|
logf("ipnserver.Run: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
|
|
if args.tunname == "" {
|
|
return nil, false, errors.New("no --tun value specified")
|
|
}
|
|
var errs []error
|
|
for _, name := range strings.Split(args.tunname, ",") {
|
|
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
|
e, useNetstack, err = tryEngine(logf, linkMon, name)
|
|
if err == nil {
|
|
return e, useNetstack, nil
|
|
}
|
|
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
|
errs = append(errs, err)
|
|
}
|
|
return nil, false, multierr.New(errs...)
|
|
}
|
|
|
|
var wrapNetstack = shouldWrapNetstack()
|
|
|
|
func shouldWrapNetstack() bool {
|
|
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
|
|
v, err := strconv.ParseBool(e)
|
|
if err != nil {
|
|
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
|
|
}
|
|
return v
|
|
}
|
|
if distro.Get() == distro.Synology {
|
|
return true
|
|
}
|
|
switch runtime.GOOS {
|
|
case "windows", "darwin", "freebsd":
|
|
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
|
// affect the GUI clients), and on FreeBSD.
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
|
conf := wgengine.Config{
|
|
ListenPort: args.port,
|
|
LinkMonitor: linkMon,
|
|
}
|
|
|
|
useNetstack = name == "userspace-networking"
|
|
netns.SetEnabled(!useNetstack)
|
|
|
|
if args.birdSocketPath != "" && createBIRDClient != nil {
|
|
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
|
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
if !useNetstack {
|
|
dev, devName, err := tstun.New(logf, name)
|
|
if err != nil {
|
|
tstun.Diagnose(logf, name)
|
|
return nil, false, err
|
|
}
|
|
conf.Tun = dev
|
|
if strings.HasPrefix(name, "tap:") {
|
|
conf.IsTAP = true
|
|
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
|
return e, false, err
|
|
}
|
|
|
|
r, err := router.New(logf, dev, linkMon)
|
|
if err != nil {
|
|
dev.Close()
|
|
return nil, false, err
|
|
}
|
|
d, err := dns.NewOSConfigurator(logf, devName)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
conf.DNS = d
|
|
conf.Router = r
|
|
if wrapNetstack {
|
|
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
|
}
|
|
}
|
|
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
|
if err != nil {
|
|
return nil, useNetstack, err
|
|
}
|
|
return e, useNetstack, nil
|
|
}
|
|
|
|
func newDebugMux() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
return mux
|
|
}
|
|
|
|
func runDebugServer(mux *http.ServeMux, addr string) {
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
}
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
|
|
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
|
if !ok {
|
|
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
|
}
|
|
return netstack.Create(logf, tunDev, e, magicConn)
|
|
}
|
|
|
|
func mustStartTCPListener(name, addr string) net.Listener {
|
|
if addr == "" {
|
|
return nil
|
|
}
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
log.Fatalf("%v listener: %v", name, err)
|
|
}
|
|
if strings.HasSuffix(addr, ":0") {
|
|
// Log kernel-selected port number so integration tests
|
|
// can find it portably.
|
|
log.Printf("%v listening on %v", name, ln.Addr())
|
|
}
|
|
return ln
|
|
}
|