mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
89953b015b
Allows for serving the web interface from tailscaled, with the ability to start and stop the server via localapi endpoints (/web/start and /web/stop). This will be used to run the new full management web client, which will only be accessible over Tailscale (with an extra auth check step over noise) from the daemon. This switch also allows us to run the web interface as a long-lived service in environments where the CLI version is restricted to CGI, allowing us to manage certain auth state in memory. ipn/ipnlocal/web is stubbed out in ipn/ipnlocal/web_stub for ios builds to satisfy ios restriction from adding "text/template" and "html/template" dependencies. Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
814 lines
24 KiB
Go
814 lines
24 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build go1.21
|
|
|
|
// 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"
|
|
"net/netip"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/cmd/tailscaled/childproc"
|
|
"tailscale.com/control/controlclient"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/ipn/conffile"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/ipn/ipnserver"
|
|
"tailscale.com/ipn/store"
|
|
"tailscale.com/logpolicy"
|
|
"tailscale.com/logtail"
|
|
"tailscale.com/net/dns"
|
|
"tailscale.com/net/dnsfallback"
|
|
"tailscale.com/net/netmon"
|
|
"tailscale.com/net/netns"
|
|
"tailscale.com/net/proxymux"
|
|
"tailscale.com/net/socks5"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/net/tshttpproxy"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/paths"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/syncs"
|
|
"tailscale.com/tsd"
|
|
"tailscale.com/tsweb/varz"
|
|
"tailscale.com/types/flagtype"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/logid"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/multierr"
|
|
"tailscale.com/util/osshare"
|
|
"tailscale.com/version"
|
|
"tailscale.com/version/distro"
|
|
"tailscale.com/wgengine"
|
|
"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 "plan9":
|
|
return "userspace-networking"
|
|
case "linux":
|
|
switch distro.Get() {
|
|
case 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"
|
|
case distro.Gokrazy:
|
|
// Gokrazy doesn't yet work in tun mode because the whole
|
|
// Gokrazy thing is no C code, and Tailscale currently
|
|
// depends on the iptables binary for Linux's
|
|
// wgengine/router.
|
|
// But on Gokrazy there's no legacy iptables, so we could use netlink
|
|
// to program nft-iptables directly. It just isn't done yet;
|
|
// see https://github.com/tailscale/tailscale/issues/391
|
|
//
|
|
// But Gokrazy does have the tun module built-in, so users
|
|
// can still run --tun=tailscale0 if they wish, if they
|
|
// arrange for iptables to be present or run in "tailscale
|
|
// up --netfilter-mode=off" mode, perhaps. Untested.
|
|
return "userspace-networking"
|
|
}
|
|
|
|
}
|
|
return "tailscale0"
|
|
}
|
|
|
|
// defaultPort returns the default UDP port to listen on for disco+wireguard.
|
|
// By default it returns 0, to pick one randomly from the kernel.
|
|
// If the environment variable PORT is set, that's used instead.
|
|
// The PORT environment variable is chosen to match what the Linux systemd
|
|
// unit uses, to make documentation more consistent.
|
|
func defaultPort() uint16 {
|
|
if s := envknob.String("PORT"); s != "" {
|
|
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
|
|
return uint16(p)
|
|
}
|
|
}
|
|
if envknob.GOOS() == "windows" {
|
|
return 41641
|
|
}
|
|
return 0
|
|
}
|
|
|
|
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
|
|
confFile string
|
|
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
|
|
disableLogs bool
|
|
}
|
|
|
|
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,
|
|
"be-child": &beChildFunc,
|
|
}
|
|
|
|
var beCLI func() // non-nil if CLI is linked in
|
|
|
|
func main() {
|
|
envknob.PanicIfAnyEnvCheckedInInit()
|
|
envknob.ApplyDiskConfig()
|
|
|
|
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, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
|
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
|
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")
|
|
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
|
flag.StringVar(&args.confFile, "config", "", "path to config file")
|
|
|
|
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
|
beCLI()
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
flag.Parse()
|
|
if flag.NArg() > 0 {
|
|
// Windows subprocess is spawned with /subprocess, so we need to avoid this check there.
|
|
if runtime.GOOS != "windows" || (flag.Arg(0) != "/subproc" && flag.Arg(0) != "/firewall") {
|
|
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
|
}
|
|
}
|
|
|
|
if fd, ok := envknob.LookupInt("TS_PARENT_DEATH_FD"); ok && fd > 2 {
|
|
go dieOnPipeReadErrorOfFD(fd)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Only apply a default statepath when neither have been provided, so that a
|
|
// user may specify only --statedir if they wish.
|
|
if args.statepath == "" && args.statedir == "" {
|
|
args.statepath = paths.DefaultTailscaledStateFile()
|
|
}
|
|
|
|
if args.disableLogs {
|
|
envknob.SetNoLogsNoSupport()
|
|
}
|
|
|
|
if beWindowsSubprocess() {
|
|
return
|
|
}
|
|
|
|
err := run()
|
|
|
|
// Remove file sharing from Windows shell (noop in non-windows)
|
|
osshare.SetFileSharingEnabled(false, logger.Discard)
|
|
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
|
|
// serverOptions is the configuration of the Tailscale node agent.
|
|
type serverOptions struct {
|
|
// VarRoot is the Tailscale daemon's private writable
|
|
// directory (usually "/var/lib/tailscale" on Linux) that
|
|
// contains the "tailscaled.state" file, the "certs" directory
|
|
// for TLS certs, and the "files" directory for incoming
|
|
// Taildrop files before they're moved to a user directory.
|
|
// If empty, Taildrop and TLS certs don't function.
|
|
VarRoot string
|
|
|
|
// LoginFlags specifies the LoginFlags to pass to the client.
|
|
LoginFlags controlclient.LoginFlags
|
|
}
|
|
|
|
func ipnServerOpts() (o serverOptions) {
|
|
goos := envknob.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
|
|
}
|
|
}
|
|
if strings.HasPrefix(statePathOrDefault(), "mem:") {
|
|
// Register as an ephemeral node.
|
|
o.LoginFlags = controlclient.LoginEphemeral
|
|
}
|
|
|
|
switch goos {
|
|
case "js":
|
|
// The js/wasm client has no state storage so for now
|
|
// treat all interactive logins as ephemeral.
|
|
// TODO(bradfitz): if we start using browser LocalStorage
|
|
// or something, then rethink this.
|
|
o.LoginFlags = controlclient.LoginEphemeral
|
|
case "windows":
|
|
// Not those.
|
|
}
|
|
return o
|
|
}
|
|
|
|
var logPol *logpolicy.Policy
|
|
var debugMux *http.ServeMux
|
|
|
|
func run() error {
|
|
var logf logger.Logf = log.Printf
|
|
|
|
sys := new(tsd.System)
|
|
|
|
// Parse config, if specified, to fail early if it's invalid.
|
|
var conf *conffile.Config
|
|
if args.confFile != "" {
|
|
var err error
|
|
conf, err = conffile.Load(args.confFile)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading config file: %w", err)
|
|
}
|
|
sys.InitialConfig = conf
|
|
}
|
|
|
|
netMon, err := netmon.New(func(format string, args ...any) {
|
|
logf(format, args...)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("netmon.New: %w", err)
|
|
}
|
|
sys.Set(netMon)
|
|
|
|
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
|
|
pol.SetVerbosityLevel(args.verbose)
|
|
logPol = pol
|
|
defer func() {
|
|
// Finish uploading logs after closing everything else.
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
pol.Shutdown(ctx)
|
|
}()
|
|
|
|
if err := envknob.ApplyDiskConfigError(); err != nil {
|
|
log.Printf("Error reading environment config: %v", err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if envknob.Bool("TS_DEBUG_MEMORY") {
|
|
logf = logger.RusagePrefixLog(logf)
|
|
}
|
|
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
|
|
|
if args.cleanup {
|
|
if envknob.Bool("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)
|
|
}
|
|
|
|
if args.debug != "" {
|
|
debugMux = newDebugMux()
|
|
}
|
|
|
|
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
|
}
|
|
|
|
var sigPipe os.Signal // set by sigpipe.go
|
|
|
|
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
|
|
ln, err := safesocket.Listen(args.socketpath)
|
|
if err != nil {
|
|
return fmt.Errorf("safesocket.Listen: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
// 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.
|
|
if sigPipe != nil {
|
|
signal.Ignore(sigPipe)
|
|
}
|
|
go func() {
|
|
select {
|
|
case s := <-interrupt:
|
|
logf("tailscaled got signal %v; shutting down", s)
|
|
cancel()
|
|
case <-ctx.Done():
|
|
// continue
|
|
}
|
|
}()
|
|
|
|
srv := ipnserver.New(logf, logID, sys.NetMon.Get())
|
|
if debugMux != nil {
|
|
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
|
}
|
|
var lbErr syncs.AtomicValue[error]
|
|
|
|
go func() {
|
|
t0 := time.Now()
|
|
if s, ok := envknob.LookupInt("TS_DEBUG_BACKEND_DELAY_SEC"); ok {
|
|
d := time.Duration(s) * time.Second
|
|
logf("sleeping %v before starting backend...", d)
|
|
select {
|
|
case <-time.After(d):
|
|
logf("slept %v; starting backend...", d)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
lb, err := getLocalBackend(ctx, logf, logID, sys)
|
|
if err == nil {
|
|
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
|
srv.SetLocalBackend(lb)
|
|
return
|
|
}
|
|
lbErr.Store(err) // before the following cancel
|
|
cancel() // make srv.Run below complete
|
|
}()
|
|
|
|
err = srv.Run(ctx, ln)
|
|
|
|
if err != nil && lbErr.Load() != nil {
|
|
return fmt.Errorf("getLocalBackend error: %v", lbErr.Load())
|
|
}
|
|
|
|
// Cancelation is not an error: it is the only way to stop ipnserver.
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
return fmt.Errorf("ipnserver.Run: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
|
|
if logPol != nil {
|
|
logPol.Logtail.SetNetMon(sys.NetMon.Get())
|
|
}
|
|
|
|
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
|
|
|
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
|
sys.Set(dialer)
|
|
|
|
onlyNetstack, err := createEngine(logf, sys)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("createEngine: %w", err)
|
|
}
|
|
if debugMux != nil {
|
|
if ms, ok := sys.MagicSock.GetOK(); ok {
|
|
debugMux.HandleFunc("/debug/magicsock", ms.ServeHTTPDebug)
|
|
}
|
|
go runDebugServer(debugMux, args.debug)
|
|
}
|
|
|
|
ns, err := newNetstack(logf, sys)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("newNetstack: %w", err)
|
|
}
|
|
sys.Set(ns)
|
|
ns.ProcessLocalIPs = onlyNetstack
|
|
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
|
|
|
|
if onlyNetstack {
|
|
e := sys.Engine.Get()
|
|
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
|
_, ok := e.PeerForIP(ip)
|
|
return ok
|
|
}
|
|
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
|
return ns.DialContextTCP(ctx, dst)
|
|
}
|
|
}
|
|
if socksListener != nil || httpProxyListener != nil {
|
|
var addrs []string
|
|
if httpProxyListener != nil {
|
|
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
|
go func() {
|
|
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
|
}()
|
|
addrs = append(addrs, httpProxyListener.Addr().String())
|
|
}
|
|
if socksListener != nil {
|
|
ss := &socks5.Server{
|
|
Logf: logger.WithPrefix(logf, "socks5: "),
|
|
Dialer: dialer.UserDial,
|
|
}
|
|
go func() {
|
|
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
|
}()
|
|
addrs = append(addrs, socksListener.Addr().String())
|
|
}
|
|
tshttpproxy.SetSelfProxy(addrs...)
|
|
}
|
|
|
|
opts := ipnServerOpts()
|
|
|
|
store, err := store.New(logf, statePathOrDefault())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store.New: %w", err)
|
|
}
|
|
sys.Set(store)
|
|
|
|
if w, ok := sys.Tun.GetOK(); ok {
|
|
w.Start()
|
|
}
|
|
|
|
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
|
}
|
|
lb.SetVarRoot(opts.VarRoot)
|
|
if logPol != nil {
|
|
lb.SetLogFlusher(logPol.Logtail.StartFlush)
|
|
}
|
|
if root := lb.TailscaleVarRoot(); root != "" {
|
|
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
|
}
|
|
if envknob.Bool("TS_DEBUG_WEB_UI") {
|
|
lb.SetWebLocalClient(&tailscale.LocalClient{Socket: args.socketpath, UseSocketOnly: args.socketpath != ""})
|
|
}
|
|
configureTaildrop(logf, lb)
|
|
if err := ns.Start(lb); err != nil {
|
|
log.Fatalf("failed to start netstack: %v", err)
|
|
}
|
|
return lb, nil
|
|
}
|
|
|
|
// createEngine tries to the wgengine.Engine based on the order of tunnels
|
|
// specified in the command line flags.
|
|
//
|
|
// onlyNetstack is true if the user has explicitly requested that we use netstack
|
|
// for all networking.
|
|
func createEngine(logf logger.Logf, sys *tsd.System) (onlyNetstack bool, err error) {
|
|
if args.tunname == "" {
|
|
return false, errors.New("no --tun value specified")
|
|
}
|
|
var errs []error
|
|
for _, name := range strings.Split(args.tunname, ",") {
|
|
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
|
onlyNetstack, err = tryEngine(logf, sys, name)
|
|
if err == nil {
|
|
return onlyNetstack, nil
|
|
}
|
|
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
|
errs = append(errs, err)
|
|
}
|
|
return false, multierr.New(errs...)
|
|
}
|
|
|
|
// handleSubnetsInNetstack reports whether netstack should handle subnet routers
|
|
// as opposed to the OS. We do this if the OS doesn't support subnet routers
|
|
// (e.g. Windows) or if the user has explicitly requested it (e.g.
|
|
// --tun=userspace-networking).
|
|
func handleSubnetsInNetstack() bool {
|
|
if v, ok := envknob.LookupBool("TS_DEBUG_NETSTACK_SUBNETS"); ok {
|
|
return v
|
|
}
|
|
if distro.Get() == distro.Synology {
|
|
return true
|
|
}
|
|
switch runtime.GOOS {
|
|
case "windows", "darwin", "freebsd", "openbsd":
|
|
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
|
// affect the GUI clients), and on FreeBSD.
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var tstunNew = tstun.New
|
|
|
|
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
|
|
conf := wgengine.Config{
|
|
ListenPort: args.port,
|
|
NetMon: sys.NetMon.Get(),
|
|
Dialer: sys.Dialer.Get(),
|
|
SetSubsystem: sys.Set,
|
|
ControlKnobs: sys.ControlKnobs(),
|
|
}
|
|
|
|
onlyNetstack = name == "userspace-networking"
|
|
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
|
|
netns.SetEnabled(!onlyNetstack)
|
|
|
|
if args.birdSocketPath != "" && createBIRDClient != nil {
|
|
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
|
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("createBIRDClient: %w", err)
|
|
}
|
|
}
|
|
if onlyNetstack {
|
|
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
|
// On Synology in netstack mode, still init a DNS
|
|
// manager (directManager) to avoid the health check
|
|
// warnings in 'tailscale status' about DNS base
|
|
// configuration being unavailable (from the noop
|
|
// manager). More in Issue 4017.
|
|
// TODO(bradfitz): add a Synology-specific DNS manager.
|
|
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
|
|
if err != nil {
|
|
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
|
}
|
|
}
|
|
} else {
|
|
dev, devName, err := tstunNew(logf, name)
|
|
if err != nil {
|
|
tstun.Diagnose(logf, name, err)
|
|
return false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
|
}
|
|
conf.Tun = dev
|
|
if strings.HasPrefix(name, "tap:") {
|
|
conf.IsTAP = true
|
|
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
sys.Set(e)
|
|
return false, err
|
|
}
|
|
|
|
r, err := router.New(logf, dev, sys.NetMon.Get())
|
|
if err != nil {
|
|
dev.Close()
|
|
return false, fmt.Errorf("creating router: %w", err)
|
|
}
|
|
|
|
d, err := dns.NewOSConfigurator(logf, devName)
|
|
if err != nil {
|
|
dev.Close()
|
|
r.Close()
|
|
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
|
}
|
|
conf.DNS = d
|
|
conf.Router = r
|
|
if handleSubnetsInNetstack() {
|
|
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
|
netstackSubnetRouter = true
|
|
}
|
|
sys.Set(conf.Router)
|
|
}
|
|
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
|
if err != nil {
|
|
return onlyNetstack, err
|
|
}
|
|
e = wgengine.NewWatchdog(e)
|
|
sys.Set(e)
|
|
sys.NetstackRouter.Set(netstackSubnetRouter)
|
|
|
|
return onlyNetstack, nil
|
|
}
|
|
|
|
func newDebugMux() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/debug/metrics", servePrometheusMetrics)
|
|
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 servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
varz.Handler(w, r)
|
|
clientmetric.WritePrometheusExpositionFormat(w)
|
|
}
|
|
|
|
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, sys *tsd.System) (*netstack.Impl, error) {
|
|
return netstack.Create(logf,
|
|
sys.Tun.Get(),
|
|
sys.Engine.Get(),
|
|
sys.MagicSock.Get(),
|
|
sys.Dialer.Get(),
|
|
sys.DNSManager.Get(),
|
|
sys.ProxyMapper(),
|
|
)
|
|
}
|
|
|
|
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
|
|
// proxies, if the respective addresses are not empty. socksAddr and
|
|
// httpAddr can be the same, in which case socksListener will receive
|
|
// connections that look like they're speaking SOCKS and httpListener
|
|
// will receive everything else.
|
|
//
|
|
// socksListener and httpListener can be nil, if their respective
|
|
// addrs are empty.
|
|
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
|
|
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
|
|
ln, err := net.Listen("tcp", socksAddr)
|
|
if err != nil {
|
|
log.Fatalf("proxy listener: %v", err)
|
|
}
|
|
return proxymux.SplitSOCKSAndHTTP(ln)
|
|
}
|
|
|
|
var err error
|
|
if socksAddr != "" {
|
|
socksListener, err = net.Listen("tcp", socksAddr)
|
|
if err != nil {
|
|
log.Fatalf("SOCKS5 listener: %v", err)
|
|
}
|
|
if strings.HasSuffix(socksAddr, ":0") {
|
|
// Log kernel-selected port number so integration tests
|
|
// can find it portably.
|
|
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
|
}
|
|
}
|
|
if httpAddr != "" {
|
|
httpListener, err = net.Listen("tcp", httpAddr)
|
|
if err != nil {
|
|
log.Fatalf("HTTP proxy listener: %v", err)
|
|
}
|
|
if strings.HasSuffix(httpAddr, ":0") {
|
|
// Log kernel-selected port number so integration tests
|
|
// can find it portably.
|
|
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
|
|
}
|
|
}
|
|
|
|
return socksListener, httpListener
|
|
}
|
|
|
|
var beChildFunc = beChild
|
|
|
|
func beChild(args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("missing mode argument")
|
|
}
|
|
typ := args[0]
|
|
f, ok := childproc.Code[typ]
|
|
if !ok {
|
|
return fmt.Errorf("unknown be-child mode %q", typ)
|
|
}
|
|
return f(args[1:])
|
|
}
|
|
|
|
// dieOnPipeReadErrorOfFD reads from the pipe named by fd and exit the process
|
|
// when the pipe becomes readable. We use this in tests as a somewhat more
|
|
// portable mechanism for the Linux PR_SET_PDEATHSIG, which we wish existed on
|
|
// macOS. This helps us clean up straggler tailscaled processes when the parent
|
|
// test driver dies unexpectedly.
|
|
func dieOnPipeReadErrorOfFD(fd int) {
|
|
f := os.NewFile(uintptr(fd), "TS_PARENT_DEATH_FD")
|
|
f.Read(make([]byte, 1))
|
|
os.Exit(1)
|
|
}
|