mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1194 lines
36 KiB
Go
1194 lines
36 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
shellquote "github.com/kballard/go-shellquote"
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
qrcode "github.com/skip2/go-qrcode"
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/health/healthmsg"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/net/netutil"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/preftype"
|
|
"tailscale.com/types/views"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/version"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
var upCmd = &ffcli.Command{
|
|
Name: "up",
|
|
ShortUsage: "tailscale up [flags]",
|
|
ShortHelp: "Connect to Tailscale, logging in if needed",
|
|
|
|
LongHelp: strings.TrimSpace(`
|
|
"tailscale up" connects this machine to your Tailscale network,
|
|
triggering authentication if necessary.
|
|
|
|
With no flags, "tailscale up" brings the network online without
|
|
changing any settings. (That is, it's the opposite of "tailscale
|
|
down").
|
|
|
|
If flags are specified, the flags must be the complete set of desired
|
|
settings. An error is returned if any setting would be changed as a
|
|
result of an unspecified flag's default value, unless the --reset flag
|
|
is also used. (The flags --auth-key, --force-reauth, and --qr are not
|
|
considered settings that need to be re-specified when modifying
|
|
settings.)
|
|
`),
|
|
FlagSet: upFlagSet,
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
return runUp(ctx, "up", args, upArgsGlobal)
|
|
},
|
|
}
|
|
|
|
func effectiveGOOS() string {
|
|
if v := os.Getenv("TS_DEBUG_UP_FLAG_GOOS"); v != "" {
|
|
return v
|
|
}
|
|
return runtime.GOOS
|
|
}
|
|
|
|
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
|
// a function of the platform it's running on.
|
|
func acceptRouteDefault(goos string) bool {
|
|
switch goos {
|
|
case "windows":
|
|
return true
|
|
case "darwin":
|
|
return version.IsSandboxedMacOS()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgsGlobal, "up")
|
|
|
|
// newUpFlagSet returns a new flag set for the "up" and "login" commands.
|
|
func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
|
if cmd != "up" && cmd != "login" {
|
|
panic("cmd must be up or login")
|
|
}
|
|
upf := newFlagSet(cmd)
|
|
|
|
// When adding new flags, prefer to put them under "tailscale set" instead
|
|
// of here. Setting preferences via "tailscale up" is deprecated.
|
|
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
|
upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
|
|
|
|
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
|
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
|
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
|
upf.Var(notFalseVar{}, "host-routes", hidden+"install host routes to other Tailscale nodes (must be true as of Tailscale 1.67+)")
|
|
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
|
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
|
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
|
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
|
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
|
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
|
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
|
upf.BoolVar(&upArgs.advertiseConnector, "advertise-connector", false, "advertise this node as an app connector")
|
|
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
|
|
|
if safesocket.GOOSUsesPeerCreds(goos) {
|
|
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
|
}
|
|
switch goos {
|
|
case "linux":
|
|
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
|
upf.BoolVar(&upArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, etc.)")
|
|
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
|
case "windows":
|
|
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
|
}
|
|
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
|
|
|
|
if cmd == "login" {
|
|
upf.StringVar(&upArgs.profileName, "nickname", "", "short name for the account")
|
|
}
|
|
|
|
if cmd == "up" {
|
|
// Some flags are only for "up", not "login".
|
|
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
|
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
|
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
|
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
|
|
}
|
|
|
|
return upf
|
|
}
|
|
|
|
// notFalseVar is is a flag.Value that can only be "true", if set.
|
|
type notFalseVar struct{}
|
|
|
|
func (notFalseVar) IsBoolFlag() bool { return true }
|
|
func (notFalseVar) Set(v string) error {
|
|
if v != "true" {
|
|
return fmt.Errorf("unsupported value; only 'true' is allowed")
|
|
}
|
|
return nil
|
|
}
|
|
func (notFalseVar) String() string { return "true" }
|
|
|
|
func defaultNetfilterMode() string {
|
|
if distro.Get() == distro.Synology {
|
|
return "off"
|
|
}
|
|
return "on"
|
|
}
|
|
|
|
// upArgsT is the type of upArgs, the argument struct for `tailscale up`.
|
|
// As of 2024-10-08, upArgsT is frozen and no new arguments should be
|
|
// added to it. Add new arguments to setArgsT instead.
|
|
type upArgsT struct {
|
|
qr bool
|
|
reset bool
|
|
server string
|
|
acceptRoutes bool
|
|
acceptDNS bool
|
|
exitNodeIP string
|
|
exitNodeAllowLANAccess bool
|
|
shieldsUp bool
|
|
runSSH bool
|
|
runWebClient bool
|
|
forceReauth bool
|
|
forceDaemon bool
|
|
advertiseRoutes string
|
|
advertiseDefaultRoute bool
|
|
advertiseTags string
|
|
advertiseConnector bool
|
|
snat bool
|
|
statefulFiltering bool
|
|
netfilterMode string
|
|
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
|
hostname string
|
|
opUser string
|
|
json bool
|
|
timeout time.Duration
|
|
acceptedRisks string
|
|
profileName string
|
|
}
|
|
|
|
func (a upArgsT) getAuthKey() (string, error) {
|
|
v := a.authKeyOrFile
|
|
if file, ok := strings.CutPrefix(v, "file:"); ok {
|
|
b, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(b)), nil
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
var upArgsGlobal upArgsT
|
|
|
|
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
|
|
//
|
|
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
|
|
// providing the link for where to authenticate this client. BackendState would be
|
|
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
|
|
// populated if something goes badly wrong.
|
|
//
|
|
// When the client is authenticated by having someone visit the AuthURL, a second
|
|
// JSON block will be output. The AuthURL and QR fields will not be present, the
|
|
// BackendState and Error fields will give the result of the authentication.
|
|
// Ex:
|
|
//
|
|
// {
|
|
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
|
|
// "QR": "data:image/png;base64,0123...cdef"
|
|
// "BackendState": "NeedsLogin"
|
|
// }
|
|
//
|
|
// {
|
|
// "BackendState": "Running"
|
|
// }
|
|
type upOutputJSON struct {
|
|
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
|
|
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
|
|
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
|
|
Error string `json:",omitempty"` // description of an error
|
|
}
|
|
|
|
func warnf(format string, args ...any) {
|
|
printf("Warning: "+format+"\n", args...)
|
|
}
|
|
|
|
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
|
//
|
|
// Note that the parameters upArgs and warnf are named intentionally
|
|
// to shadow the globals to prevent accidental misuse of them. This
|
|
// function exists for testing and should have no side effects or
|
|
// outside interactions (e.g. no making Tailscale LocalAPI calls).
|
|
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
|
routes, err := netutil.CalcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if upArgs.exitNodeIP == "" && upArgs.exitNodeAllowLANAccess {
|
|
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
|
}
|
|
|
|
var tags []string
|
|
if upArgs.advertiseTags != "" {
|
|
tags = strings.Split(upArgs.advertiseTags, ",")
|
|
for _, tag := range tags {
|
|
err := tailcfg.CheckTag(tag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tag: %q: %s", tag, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prefs := ipn.NewPrefs()
|
|
prefs.ControlURL = upArgs.server
|
|
prefs.WantRunning = true
|
|
prefs.RouteAll = upArgs.acceptRoutes
|
|
if distro.Get() == distro.Synology {
|
|
// ipn.NewPrefs returns a non-zero Netfilter default. But Synology only
|
|
// supports "off" mode.
|
|
prefs.NetfilterMode = preftype.NetfilterOff
|
|
}
|
|
if upArgs.exitNodeIP != "" {
|
|
if err := prefs.SetExitNodeIP(upArgs.exitNodeIP, st); err != nil {
|
|
var e ipn.ExitNodeLocalIPError
|
|
if errors.As(err, &e) {
|
|
return nil, fmt.Errorf("%w; did you mean --advertise-exit-node?", err)
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
|
prefs.CorpDNS = upArgs.acceptDNS
|
|
prefs.ShieldsUp = upArgs.shieldsUp
|
|
prefs.RunSSH = upArgs.runSSH
|
|
prefs.RunWebClient = upArgs.runWebClient
|
|
prefs.AdvertiseRoutes = routes
|
|
prefs.AdvertiseTags = tags
|
|
prefs.Hostname = upArgs.hostname
|
|
prefs.ForceDaemon = upArgs.forceDaemon
|
|
prefs.OperatorUser = upArgs.opUser
|
|
prefs.ProfileName = upArgs.profileName
|
|
prefs.AppConnector.Advertise = upArgs.advertiseConnector
|
|
|
|
if goos == "linux" {
|
|
prefs.NoSNAT = !upArgs.snat
|
|
|
|
// Backfills for NoStatefulFiltering occur when loading a profile; just set it explicitly here.
|
|
prefs.NoStatefulFiltering.Set(!upArgs.statefulFiltering)
|
|
v, warning, err := netfilterModeFromFlag(upArgs.netfilterMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prefs.NetfilterMode = v
|
|
if warning != "" {
|
|
warnf(warning)
|
|
}
|
|
}
|
|
return prefs, nil
|
|
}
|
|
|
|
// netfilterModeFromFlag returns the preftype.NetfilterMode for the provided
|
|
// flag value. It returns a warning if there is something the user should know
|
|
// about the value.
|
|
func netfilterModeFromFlag(v string) (_ preftype.NetfilterMode, warning string, _ error) {
|
|
switch v {
|
|
case "on", "nodivert", "off":
|
|
default:
|
|
return preftype.NetfilterOn, "", fmt.Errorf("invalid value --netfilter-mode=%q", v)
|
|
}
|
|
m, err := preftype.ParseNetfilterMode(v)
|
|
if err != nil {
|
|
return preftype.NetfilterOn, "", err
|
|
}
|
|
switch m {
|
|
case preftype.NetfilterNoDivert:
|
|
warning = "netfilter=nodivert; add iptables calls to ts-* chains manually."
|
|
case preftype.NetfilterOff:
|
|
if defaultNetfilterMode() != "off" {
|
|
warning = "netfilter=off; configure iptables yourself."
|
|
}
|
|
}
|
|
return m, warning, nil
|
|
}
|
|
|
|
// updatePrefs returns how to edit preferences based on the
|
|
// flag-provided 'prefs' and the currently active 'curPrefs'.
|
|
//
|
|
// It returns a non-nil justEditMP if we're already running and none of
|
|
// the flags require a restart, so we can just do an EditPrefs call and
|
|
// change the prefs at runtime (e.g. changing hostname, changing
|
|
// advertised routes, etc).
|
|
//
|
|
// It returns simpleUp if we're running a simple "tailscale up" to
|
|
// transition to running from a previously-logged-in but down state,
|
|
// without changing any settings.
|
|
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
|
|
if !env.upArgs.reset {
|
|
applyImplicitPrefs(prefs, curPrefs, env)
|
|
|
|
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
|
|
return false, nil, err
|
|
}
|
|
}
|
|
|
|
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL &&
|
|
!(ipn.IsLoginServerSynonym(curPrefs.ControlURL) && ipn.IsLoginServerSynonym(prefs.ControlURL))
|
|
if controlURLChanged && env.backendState == ipn.Running.String() && !env.upArgs.forceReauth {
|
|
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
|
}
|
|
|
|
// Do this after validations to avoid the 5s delay if we're going to error
|
|
// out anyway.
|
|
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
|
if err := presentSSHToggleRisk(wantSSH, haveSSH, env.upArgs.acceptedRisks); err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
if env.upArgs.forceReauth && isSSHOverTailscale() {
|
|
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will result in your SSH session disconnecting.`, env.upArgs.acceptedRisks); err != nil {
|
|
return false, nil, err
|
|
}
|
|
}
|
|
|
|
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
|
|
|
simpleUp = env.flagSet.NFlag() == 0 &&
|
|
curPrefs.Persist != nil &&
|
|
curPrefs.Persist.UserProfile.LoginName != "" &&
|
|
env.backendState != ipn.NeedsLogin.String()
|
|
|
|
justEdit := env.backendState == ipn.Running.String() &&
|
|
!env.upArgs.forceReauth &&
|
|
env.upArgs.authKeyOrFile == "" &&
|
|
!controlURLChanged &&
|
|
!tagsChanged
|
|
|
|
if justEdit {
|
|
justEditMP = new(ipn.MaskedPrefs)
|
|
justEditMP.WantRunningSet = true
|
|
justEditMP.Prefs = *prefs
|
|
visitFlags := env.flagSet.Visit
|
|
if env.upArgs.reset {
|
|
visitFlags = env.flagSet.VisitAll
|
|
}
|
|
visitFlags(func(f *flag.Flag) {
|
|
updateMaskedPrefsFromUpOrSetFlag(justEditMP, f.Name)
|
|
})
|
|
}
|
|
|
|
return simpleUp, justEditMP, nil
|
|
}
|
|
|
|
func presentSSHToggleRisk(wantSSH, haveSSH bool, acceptedRisks string) error {
|
|
if !isSSHOverTailscale() || wantSSH == haveSSH {
|
|
return nil
|
|
}
|
|
if wantSSH {
|
|
return presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, acceptedRisks)
|
|
}
|
|
return presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, acceptedRisks)
|
|
}
|
|
|
|
func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retErr error) {
|
|
var egg bool
|
|
if len(args) > 0 {
|
|
egg = fmt.Sprint(args) == "[up down down left right left right b a]"
|
|
if !egg {
|
|
fatalf("too many non-flag arguments: %q", args)
|
|
}
|
|
}
|
|
|
|
st, err := localClient.Status(ctx)
|
|
if err != nil {
|
|
return fixTailscaledConnectError(err)
|
|
}
|
|
origAuthURL := st.AuthURL
|
|
|
|
// printAuthURL reports whether we should print out the
|
|
// provided auth URL from an IPN notify.
|
|
printAuthURL := func(url string) bool {
|
|
if url == "" {
|
|
// Probably unnecessary but we used to have a bug where tailscaled
|
|
// could send an empty URL over the IPN bus. ~Harmless to keep.
|
|
return false
|
|
}
|
|
if upArgs.authKeyOrFile != "" {
|
|
// Issue 1755: when using an authkey, don't
|
|
// show an authURL that might still be pending
|
|
// from a previous non-completed interactive
|
|
// login.
|
|
return false
|
|
}
|
|
if upArgs.forceReauth && url == origAuthURL {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
if distro.Get() == distro.Synology {
|
|
notSupported := "not supported on Synology; see https://github.com/tailscale/tailscale/issues/1995"
|
|
if upArgs.acceptRoutes {
|
|
return errors.New("--accept-routes is " + notSupported)
|
|
}
|
|
if upArgs.exitNodeIP != "" {
|
|
return errors.New("--exit-node is " + notSupported)
|
|
}
|
|
if upArgs.netfilterMode != "off" {
|
|
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
|
}
|
|
}
|
|
|
|
prefs, err := prefsFromUpArgs(upArgs, warnf, st, effectiveGOOS())
|
|
if err != nil {
|
|
fatalf("%s", err)
|
|
}
|
|
|
|
warnOnAdvertiseRouts(ctx, prefs)
|
|
|
|
curPrefs, err := localClient.GetPrefs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd == "up" {
|
|
// "tailscale up" should not be able to change the
|
|
// profile name.
|
|
prefs.ProfileName = curPrefs.ProfileName
|
|
}
|
|
|
|
env := upCheckEnv{
|
|
goos: effectiveGOOS(),
|
|
distro: distro.Get(),
|
|
user: os.Getenv("USER"),
|
|
flagSet: upFlagSet,
|
|
upArgs: upArgs,
|
|
backendState: st.BackendState,
|
|
curExitNodeIP: exitNodeIP(curPrefs, st),
|
|
}
|
|
|
|
defer func() {
|
|
if retErr == nil {
|
|
checkUpWarnings(ctx)
|
|
}
|
|
}()
|
|
|
|
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
|
if err != nil {
|
|
fatalf("%s", err)
|
|
}
|
|
if justEditMP != nil {
|
|
justEditMP.EggSet = egg
|
|
_, err := localClient.EditPrefs(ctx, justEditMP)
|
|
return err
|
|
}
|
|
|
|
watchCtx, cancelWatch := context.WithCancel(ctx)
|
|
defer cancelWatch()
|
|
|
|
go func() {
|
|
interrupt := make(chan os.Signal, 1)
|
|
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
select {
|
|
case <-interrupt:
|
|
cancelWatch()
|
|
case <-watchCtx.Done():
|
|
}
|
|
}()
|
|
|
|
running := make(chan bool, 1) // gets value once in state ipn.Running
|
|
watchErr := make(chan error, 1)
|
|
|
|
// Special case: bare "tailscale up" means to just start
|
|
// running, if there's ever been a login.
|
|
if simpleUp {
|
|
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
|
Prefs: ipn.Prefs{
|
|
WantRunning: true,
|
|
},
|
|
WantRunningSet: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := localClient.CheckPrefs(ctx, prefs); err != nil {
|
|
return err
|
|
}
|
|
|
|
authKey, err := upArgs.getAuthKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = localClient.Start(ctx, ipn.Options{
|
|
AuthKey: authKey,
|
|
UpdatePrefs: prefs,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if upArgs.forceReauth || !st.HaveNodeKey {
|
|
err := localClient.StartLoginInteractive(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
watcher, err := localClient.WatchIPNBus(watchCtx, ipn.NotifyInitialState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer watcher.Close()
|
|
|
|
go func() {
|
|
var printed bool // whether we've yet printed anything to stdout or stderr
|
|
var lastURLPrinted string
|
|
|
|
for {
|
|
n, err := watcher.Next()
|
|
if err != nil {
|
|
watchErr <- err
|
|
return
|
|
}
|
|
if n.ErrMessage != nil {
|
|
msg := *n.ErrMessage
|
|
fatalf("backend error: %v\n", msg)
|
|
}
|
|
if s := n.State; s != nil {
|
|
switch *s {
|
|
case ipn.NeedsMachineAuth:
|
|
printed = true
|
|
if env.upArgs.json {
|
|
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
|
} else {
|
|
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
|
}
|
|
case ipn.Running:
|
|
// Done full authentication process
|
|
if env.upArgs.json {
|
|
printUpDoneJSON(ipn.Running, "")
|
|
} else if printed {
|
|
// Only need to print an update if we printed the "please click" message earlier.
|
|
fmt.Fprintf(Stderr, "Success.\n")
|
|
}
|
|
select {
|
|
case running <- true:
|
|
default:
|
|
}
|
|
cancelWatch()
|
|
}
|
|
}
|
|
if url := n.BrowseToURL; url != nil {
|
|
authURL := *url
|
|
if !printAuthURL(authURL) || authURL == lastURLPrinted {
|
|
continue
|
|
}
|
|
printed = true
|
|
lastURLPrinted = authURL
|
|
if upArgs.json {
|
|
js := &upOutputJSON{AuthURL: authURL, BackendState: st.BackendState}
|
|
|
|
q, err := qrcode.New(authURL, qrcode.Medium)
|
|
if err == nil {
|
|
png, err := q.PNG(128)
|
|
if err == nil {
|
|
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
|
}
|
|
}
|
|
|
|
data, err := json.MarshalIndent(js, "", "\t")
|
|
if err != nil {
|
|
printf("upOutputJSON marshalling error: %v", err)
|
|
} else {
|
|
outln(string(data))
|
|
}
|
|
} else {
|
|
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", authURL)
|
|
if upArgs.qr {
|
|
q, err := qrcode.New(authURL, qrcode.Medium)
|
|
if err != nil {
|
|
log.Printf("QR code error: %v", err)
|
|
} else {
|
|
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// This whole 'up' mechanism is too complicated and results in
|
|
// hairy stuff like this select. We're ultimately waiting for
|
|
// 'running' to be done, but even in the case where
|
|
// it succeeds, other parts may shut down concurrently so we
|
|
// need to prioritize reads from 'running' if it's
|
|
// readable; its send does happen before the pump mechanism
|
|
// shuts down. (Issue 2333)
|
|
var timeoutCh <-chan time.Time
|
|
if upArgs.timeout > 0 {
|
|
timeoutTimer := time.NewTimer(upArgs.timeout)
|
|
defer timeoutTimer.Stop()
|
|
timeoutCh = timeoutTimer.C
|
|
}
|
|
select {
|
|
case <-running:
|
|
return nil
|
|
case <-watchCtx.Done():
|
|
select {
|
|
case <-running:
|
|
return nil
|
|
default:
|
|
}
|
|
return watchCtx.Err()
|
|
case err := <-watchErr:
|
|
select {
|
|
case <-running:
|
|
return nil
|
|
default:
|
|
}
|
|
return err
|
|
case <-timeoutCh:
|
|
return errors.New(`timeout waiting for Tailscale service to enter a Running state; check health with "tailscale status"`)
|
|
}
|
|
}
|
|
|
|
// upWorthWarning reports whether the health check message s is worth warning
|
|
// about during "tailscale up". Many of the health checks are noisy or confusing
|
|
// or very ephemeral and happen especially briefly at startup.
|
|
//
|
|
// TODO(bradfitz): change the server to send typed warnings with metadata about
|
|
// the health check, rather than just a string.
|
|
func upWorthyWarning(s string) bool {
|
|
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
|
|
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
|
|
strings.Contains(s, healthmsg.LockedOut) ||
|
|
strings.Contains(s, healthmsg.WarnExitNodeUsage) ||
|
|
strings.Contains(strings.ToLower(s), "update available: ")
|
|
}
|
|
|
|
func checkUpWarnings(ctx context.Context) {
|
|
st, err := localClient.StatusWithoutPeers(ctx)
|
|
if err != nil {
|
|
// Ignore. Don't spam more.
|
|
return
|
|
}
|
|
var warn []string
|
|
for _, w := range st.Health {
|
|
if upWorthyWarning(w) {
|
|
warn = append(warn, w)
|
|
}
|
|
}
|
|
if len(warn) == 0 {
|
|
return
|
|
}
|
|
if len(warn) == 1 {
|
|
printf("%s\n", warn[0])
|
|
return
|
|
}
|
|
printf("# Health check warnings:\n")
|
|
for _, m := range warn {
|
|
printf("# - %s\n", m)
|
|
}
|
|
}
|
|
|
|
func printUpDoneJSON(state ipn.State, errorString string) {
|
|
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
|
data, err := json.MarshalIndent(js, "", " ")
|
|
if err != nil {
|
|
log.Printf("printUpDoneJSON marshalling error: %v", err)
|
|
} else {
|
|
outln(string(data))
|
|
}
|
|
}
|
|
|
|
var (
|
|
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
|
)
|
|
|
|
func init() {
|
|
// Both these have the same ipn.Pref:
|
|
addPrefFlagMapping("advertise-exit-node", "AdvertiseRoutes")
|
|
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
|
|
|
// And this flag has two ipn.Prefs:
|
|
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
|
|
|
|
// The rest are 1:1:
|
|
addPrefFlagMapping("accept-dns", "CorpDNS")
|
|
addPrefFlagMapping("accept-routes", "RouteAll")
|
|
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
|
|
addPrefFlagMapping("hostname", "Hostname")
|
|
addPrefFlagMapping("login-server", "ControlURL")
|
|
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
|
addPrefFlagMapping("shields-up", "ShieldsUp")
|
|
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
|
addPrefFlagMapping("stateful-filtering", "NoStatefulFiltering")
|
|
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
|
addPrefFlagMapping("unattended", "ForceDaemon")
|
|
addPrefFlagMapping("operator", "OperatorUser")
|
|
addPrefFlagMapping("ssh", "RunSSH")
|
|
addPrefFlagMapping("webclient", "RunWebClient")
|
|
addPrefFlagMapping("nickname", "ProfileName")
|
|
addPrefFlagMapping("update-check", "AutoUpdate.Check")
|
|
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
|
|
addPrefFlagMapping("advertise-connector", "AppConnector")
|
|
addPrefFlagMapping("posture-checking", "PostureChecking")
|
|
}
|
|
|
|
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
|
prefsOfFlag[flagName] = prefNames
|
|
prefType := reflect.TypeFor[ipn.Prefs]()
|
|
for _, pref := range prefNames {
|
|
t := prefType
|
|
for _, name := range strings.Split(pref, ".") {
|
|
// Crash at runtime if there's a typo in the prefName.
|
|
f, ok := t.FieldByName(name)
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid ipn.Prefs field %q", pref))
|
|
}
|
|
t = f.Type
|
|
}
|
|
}
|
|
}
|
|
|
|
// preflessFlag reports whether flagName is a flag that doesn't
|
|
// correspond to an ipn.Pref.
|
|
func preflessFlag(flagName string) bool {
|
|
switch flagName {
|
|
case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk", "host-routes":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func updateMaskedPrefsFromUpOrSetFlag(mp *ipn.MaskedPrefs, flagName string) {
|
|
if preflessFlag(flagName) {
|
|
return
|
|
}
|
|
if prefs, ok := prefsOfFlag[flagName]; ok {
|
|
for _, pref := range prefs {
|
|
f := reflect.ValueOf(mp).Elem()
|
|
for _, name := range strings.Split(pref, ".") {
|
|
f = f.FieldByName(name + "Set")
|
|
}
|
|
f.SetBool(true)
|
|
}
|
|
return
|
|
}
|
|
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
|
}
|
|
|
|
const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires mentioning all\n" +
|
|
"non-default flags. To proceed, either re-run your command with --reset or\n" +
|
|
"use the command below to explicitly mention the current value of\n" +
|
|
"all non-default settings:\n\n" +
|
|
"\ttailscale up"
|
|
|
|
// upCheckEnv are extra parameters describing the environment as
|
|
// needed by checkForAccidentalSettingReverts and friends.
|
|
type upCheckEnv struct {
|
|
goos string
|
|
user string
|
|
flagSet *flag.FlagSet
|
|
upArgs upArgsT
|
|
backendState string
|
|
curExitNodeIP netip.Addr
|
|
distro distro.Distro
|
|
}
|
|
|
|
// checkForAccidentalSettingReverts (the "up checker") checks for
|
|
// people running "tailscale up" with a subset of the flags they
|
|
// originally ran it with.
|
|
//
|
|
// For example, in Tailscale 1.6 and prior, a user might've advertised
|
|
// a tag, but later tried to change just one other setting and forgot
|
|
// to mention the tag later and silently wiped it out. We now
|
|
// require --reset to change preferences to flag default values when
|
|
// the flag is not mentioned on the command line.
|
|
//
|
|
// curPrefs is what's currently active on the server.
|
|
//
|
|
// mp is the mask of settings actually set, where mp.Prefs is the new
|
|
// preferences to set, including any values set from implicit flags.
|
|
func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheckEnv) error {
|
|
if curPrefs.ControlURL == "" {
|
|
// Don't validate things on initial "up" before a control URL has been set.
|
|
return nil
|
|
}
|
|
|
|
flagIsSet := map[string]bool{}
|
|
env.flagSet.Visit(func(f *flag.Flag) {
|
|
flagIsSet[f.Name] = true
|
|
})
|
|
|
|
if len(flagIsSet) == 0 {
|
|
// A bare "tailscale up" is a special case to just
|
|
// mean bringing the network up without any changes.
|
|
return nil
|
|
}
|
|
|
|
// flagsCur is what flags we'd need to use to keep the exact
|
|
// settings as-is.
|
|
flagsCur := prefsToFlags(env, curPrefs)
|
|
flagsNew := prefsToFlags(env, newPrefs)
|
|
|
|
var missing []string
|
|
for flagName := range flagsCur {
|
|
valCur, valNew := flagsCur[flagName], flagsNew[flagName]
|
|
if flagIsSet[flagName] {
|
|
continue
|
|
}
|
|
if reflect.DeepEqual(valCur, valNew) {
|
|
continue
|
|
}
|
|
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
|
continue
|
|
}
|
|
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
|
|
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
|
continue
|
|
}
|
|
if flagName == "netfilter-mode" && valNew == preftype.NetfilterOn && env.goos == "linux" && env.distro == distro.Synology {
|
|
// Issue 6811. Ignore on Synology.
|
|
continue
|
|
}
|
|
if flagName == "stateful-filtering" && valCur == true && valNew == false && env.goos == "linux" {
|
|
// See https://github.com/tailscale/tailscale/issues/12307
|
|
// Stateful filtering was on by default in tailscale 1.66.0-1.66.3, then off in 1.66.4.
|
|
// This broke Tailscale installations in containerized
|
|
// environments that use the default containerboot
|
|
// configuration that configures tailscale using
|
|
// 'tailscale up' command, which requires that all
|
|
// previously set flags are explicitly provided on
|
|
// subsequent restarts.
|
|
continue
|
|
}
|
|
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
|
}
|
|
if len(missing) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Some previously provided flags are missing. This run of 'tailscale
|
|
// up' will error out.
|
|
|
|
sort.Strings(missing)
|
|
|
|
// Compute the stringification of the explicitly provided args in flagSet
|
|
// to prepend to the command to run.
|
|
var explicit []string
|
|
env.flagSet.Visit(func(f *flag.Flag) {
|
|
type isBool interface {
|
|
IsBoolFlag() bool
|
|
}
|
|
if ib, ok := f.Value.(isBool); ok && ib.IsBoolFlag() {
|
|
if f.Value.String() == "false" {
|
|
explicit = append(explicit, "--"+f.Name+"=false")
|
|
} else {
|
|
explicit = append(explicit, "--"+f.Name)
|
|
}
|
|
} else {
|
|
explicit = append(explicit, fmtFlagValueArg(f.Name, f.Value.String()))
|
|
}
|
|
})
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(accidentalUpPrefix)
|
|
|
|
for _, a := range append(explicit, missing...) {
|
|
fmt.Fprintf(&sb, " %s", a)
|
|
}
|
|
sb.WriteString("\n\n")
|
|
return errors.New(sb.String())
|
|
}
|
|
|
|
// applyImplicitPrefs mutates prefs to add implicit preferences for the user operator.
|
|
// If the operator flag is passed no action is taken, otherwise this only needs to be set if it doesn't
|
|
// match the current user.
|
|
//
|
|
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
|
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
|
|
explicitOperator := false
|
|
env.flagSet.Visit(func(f *flag.Flag) {
|
|
if f.Name == "operator" {
|
|
explicitOperator = true
|
|
}
|
|
})
|
|
|
|
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == env.user && !explicitOperator {
|
|
prefs.OperatorUser = oldPrefs.OperatorUser
|
|
}
|
|
}
|
|
|
|
func flagAppliesToOS(flag, goos string) bool {
|
|
switch flag {
|
|
case "netfilter-mode", "snat-subnet-routes", "stateful-filtering":
|
|
return goos == "linux"
|
|
case "unattended":
|
|
return goos == "windows"
|
|
}
|
|
return true
|
|
}
|
|
|
|
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
|
ret := make(map[string]any)
|
|
|
|
exitNodeIPStr := func() string {
|
|
if prefs.ExitNodeIP.IsValid() {
|
|
return prefs.ExitNodeIP.String()
|
|
}
|
|
if prefs.ExitNodeID.IsZero() || !env.curExitNodeIP.IsValid() {
|
|
return ""
|
|
}
|
|
return env.curExitNodeIP.String()
|
|
}
|
|
|
|
fs := newUpFlagSet(env.goos, new(upArgsT) /* dummy */, "up")
|
|
fs.VisitAll(func(f *flag.Flag) {
|
|
if preflessFlag(f.Name) {
|
|
return
|
|
}
|
|
set := func(v any) {
|
|
if flagAppliesToOS(f.Name, env.goos) {
|
|
ret[f.Name] = v
|
|
} else {
|
|
ret[f.Name] = nil
|
|
}
|
|
}
|
|
switch f.Name {
|
|
default:
|
|
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
|
case "ssh":
|
|
set(prefs.RunSSH)
|
|
case "webclient":
|
|
set(prefs.RunWebClient)
|
|
case "login-server":
|
|
set(prefs.ControlURL)
|
|
case "accept-routes":
|
|
set(prefs.RouteAll)
|
|
case "accept-dns":
|
|
set(prefs.CorpDNS)
|
|
case "shields-up":
|
|
set(prefs.ShieldsUp)
|
|
case "exit-node":
|
|
set(exitNodeIPStr())
|
|
case "exit-node-allow-lan-access":
|
|
set(prefs.ExitNodeAllowLANAccess)
|
|
case "advertise-tags":
|
|
set(strings.Join(prefs.AdvertiseTags, ","))
|
|
case "hostname":
|
|
set(prefs.Hostname)
|
|
case "operator":
|
|
set(prefs.OperatorUser)
|
|
case "advertise-routes":
|
|
var sb strings.Builder
|
|
for i, r := range tsaddr.WithoutExitRoutes(views.SliceOf(prefs.AdvertiseRoutes)).All() {
|
|
if i > 0 {
|
|
sb.WriteByte(',')
|
|
}
|
|
sb.WriteString(r.String())
|
|
}
|
|
set(sb.String())
|
|
case "advertise-exit-node":
|
|
set(tsaddr.ContainsExitRoutes(views.SliceOf(prefs.AdvertiseRoutes)))
|
|
case "advertise-connector":
|
|
set(prefs.AppConnector.Advertise)
|
|
case "snat-subnet-routes":
|
|
set(!prefs.NoSNAT)
|
|
case "stateful-filtering":
|
|
// We only set the stateful-filtering flag to false if
|
|
// the pref (negated!) is explicitly set to true; unset
|
|
// or false is treated as enabled.
|
|
val, ok := prefs.NoStatefulFiltering.Get()
|
|
if ok && val {
|
|
set(false)
|
|
} else {
|
|
set(true)
|
|
}
|
|
case "netfilter-mode":
|
|
set(prefs.NetfilterMode.String())
|
|
case "unattended":
|
|
set(prefs.ForceDaemon)
|
|
}
|
|
})
|
|
return ret
|
|
}
|
|
|
|
func fmtFlagValueArg(flagName string, val any) string {
|
|
if val == true {
|
|
return "--" + flagName
|
|
}
|
|
if val == "" {
|
|
return "--" + flagName + "="
|
|
}
|
|
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(fmt.Sprint(val)))
|
|
}
|
|
|
|
// exitNodeIP returns the exit node IP from p, using st to map
|
|
// it from its ID form to an IP address if needed.
|
|
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
if p.ExitNodeIP.IsValid() {
|
|
return p.ExitNodeIP
|
|
}
|
|
id := p.ExitNodeID
|
|
if id.IsZero() {
|
|
return
|
|
}
|
|
for _, p := range st.Peer {
|
|
if p.ID == id {
|
|
if len(p.TailscaleIPs) > 0 {
|
|
return p.TailscaleIPs[0]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func init() {
|
|
// Required to use our client API. We're fine with the instability since the
|
|
// client lives in the same repo as this code.
|
|
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
|
}
|
|
|
|
// resolveAuthKey either returns v unchanged (in the common case) or, if it
|
|
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
|
|
//
|
|
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
|
|
//
|
|
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
|
|
// property defaults to true if unspecified. The "preauthorized" defaults to
|
|
// false. The "baseURL" defaults to https://api.tailscale.com.
|
|
// The passed in tags are required, and must be non-empty. These will be
|
|
// set on the authkey generated by the OAuth2 dance.
|
|
func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
|
|
if !strings.HasPrefix(v, "tskey-client-") {
|
|
return v, nil
|
|
}
|
|
if tags == "" {
|
|
return "", errors.New("oauth authkeys require --advertise-tags")
|
|
}
|
|
|
|
clientSecret, named, _ := strings.Cut(v, "?")
|
|
attrs, err := url.ParseQuery(named)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for k := range attrs {
|
|
switch k {
|
|
case "ephemeral", "preauthorized", "baseURL":
|
|
default:
|
|
return "", fmt.Errorf("unknown attribute %q", k)
|
|
}
|
|
}
|
|
getBool := func(name string, def bool) (bool, error) {
|
|
v := attrs.Get(name)
|
|
if v == "" {
|
|
return def, nil
|
|
}
|
|
ret, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
|
|
}
|
|
return ret, nil
|
|
}
|
|
ephemeral, err := getBool("ephemeral", true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
preauth, err := getBool("preauthorized", false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
baseURL := "https://api.tailscale.com"
|
|
if v := attrs.Get("baseURL"); v != "" {
|
|
baseURL = v
|
|
}
|
|
|
|
credentials := clientcredentials.Config{
|
|
ClientID: "some-client-id", // ignored
|
|
ClientSecret: clientSecret,
|
|
TokenURL: baseURL + "/api/v2/oauth/token",
|
|
Scopes: []string{"device"},
|
|
}
|
|
|
|
tsClient := tailscale.NewClient("-", nil)
|
|
tsClient.UserAgent = "tailscale-cli"
|
|
tsClient.HTTPClient = credentials.Client(ctx)
|
|
tsClient.BaseURL = baseURL
|
|
|
|
caps := tailscale.KeyCapabilities{
|
|
Devices: tailscale.KeyDeviceCapabilities{
|
|
Create: tailscale.KeyDeviceCreateCapabilities{
|
|
Reusable: false,
|
|
Ephemeral: ephemeral,
|
|
Preauthorized: preauth,
|
|
Tags: strings.Split(tags, ","),
|
|
},
|
|
},
|
|
}
|
|
|
|
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return authkey, nil
|
|
}
|
|
|
|
func warnOnAdvertiseRouts(ctx context.Context, prefs *ipn.Prefs) {
|
|
if len(prefs.AdvertiseRoutes) > 0 || prefs.AppConnector.Advertise {
|
|
// TODO(jwhited): compress CheckIPForwarding and CheckUDPGROForwarding
|
|
// into a single HTTP req.
|
|
if err := localClient.CheckIPForwarding(ctx); err != nil {
|
|
warnf("%v", err)
|
|
}
|
|
if runtime.GOOS == "linux" {
|
|
if err := localClient.CheckUDPGROForwarding(ctx); err != nil {
|
|
warnf("%v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|