3ec5be3f51
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
317 lines
12 KiB
Go
317 lines
12 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net/netip"
|
|
"os/exec"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
|
"tailscale.com/feature/buildfeatures"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/net/netutil"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/tsconst"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/types/ptr"
|
|
"tailscale.com/types/views"
|
|
"tailscale.com/util/set"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
var setCmd = &ffcli.Command{
|
|
Name: "set",
|
|
ShortUsage: "tailscale set [flags]",
|
|
ShortHelp: "Change specified preferences",
|
|
LongHelp: `"tailscale set" allows changing specific preferences.
|
|
|
|
Unlike "tailscale up", this command does not require the complete set of desired settings.
|
|
|
|
Only settings explicitly mentioned will be set. There are no default values.`,
|
|
FlagSet: setFlagSet,
|
|
Exec: runSet,
|
|
UsageFunc: usageFuncNoDefaultValues,
|
|
}
|
|
|
|
type setArgsT struct {
|
|
acceptRoutes bool
|
|
acceptDNS bool
|
|
exitNodeIP string
|
|
exitNodeAllowLANAccess bool
|
|
shieldsUp bool
|
|
runSSH bool
|
|
runWebClient bool
|
|
hostname string
|
|
advertiseRoutes string
|
|
advertiseDefaultRoute bool
|
|
advertiseConnector bool
|
|
opUser string
|
|
acceptedRisks string
|
|
profileName string
|
|
forceDaemon bool
|
|
updateCheck bool
|
|
updateApply bool
|
|
reportPosture bool
|
|
snat bool
|
|
statefulFiltering bool
|
|
sync bool
|
|
netfilterMode string
|
|
relayServerPort string
|
|
relayServerStaticEndpoints string
|
|
}
|
|
|
|
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
|
setf := newFlagSet("set")
|
|
|
|
setf.StringVar(&setArgs.profileName, "nickname", "", "nickname for the current account")
|
|
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
|
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
|
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP, base name, or auto:any) for internet traffic, or empty string to not use an exit node")
|
|
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
|
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
|
setf.BoolVar(&setArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
|
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
|
setf.StringVar(&setArgs.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")
|
|
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
|
setf.BoolVar(&setArgs.advertiseConnector, "advertise-connector", false, "offer to be an app connector for domain specific internet traffic for the tailnet")
|
|
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "notify about available Tailscale updates")
|
|
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
|
setf.BoolVar(&setArgs.reportPosture, "report-posture", false, "allow management plane to gather device posture information")
|
|
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
|
|
setf.BoolVar(&setArgs.sync, "sync", false, hidden+"actively sync configuration from the control plane (set to false only for network failure testing)")
|
|
setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "UDP port number (0 will pick a random unused port) for the relay server to bind to, on all interfaces, or empty string to disable relay server functionality")
|
|
setf.StringVar(&setArgs.relayServerStaticEndpoints, "relay-server-static-endpoints", "", "static IP:port endpoints to advertise as candidates for relay connections (comma-separated, e.g. \"[2001:db8::1]:40000,192.0.2.1:40000\") or empty string to not advertise any static endpoints")
|
|
|
|
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
|
st, err := localClient.Status(context.Background())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
nodes := make([]string, 0, len(st.Peer))
|
|
for _, node := range st.Peer {
|
|
if !node.ExitNodeOption {
|
|
continue
|
|
}
|
|
nodes = append(nodes, strings.TrimSuffix(node.DNSName, "."))
|
|
}
|
|
return nodes, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
|
})
|
|
|
|
if safesocket.GOOSUsesPeerCreds(goos) {
|
|
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
|
}
|
|
switch goos {
|
|
case "linux":
|
|
setf.BoolVar(&setArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
|
setf.BoolVar(&setArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, and so on)")
|
|
setf.StringVar(&setArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
|
case "windows":
|
|
setf.BoolVar(&setArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
|
}
|
|
|
|
registerAcceptRiskFlag(setf, &setArgs.acceptedRisks)
|
|
return setf
|
|
}
|
|
|
|
var (
|
|
setArgs setArgsT
|
|
setFlagSet = newSetFlagSet(effectiveGOOS(), &setArgs)
|
|
)
|
|
|
|
func runSet(ctx context.Context, args []string) (retErr error) {
|
|
if len(args) > 0 {
|
|
fatalf("too many non-flag arguments: %q", args)
|
|
}
|
|
|
|
st, err := localClient.Status(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Note that even though we set the values here regardless of whether the
|
|
// user passed the flag, the value is only used if the user passed the flag.
|
|
// See updateMaskedPrefsFromUpOrSetFlag.
|
|
maskedPrefs := &ipn.MaskedPrefs{
|
|
Prefs: ipn.Prefs{
|
|
ProfileName: setArgs.profileName,
|
|
RouteAll: setArgs.acceptRoutes,
|
|
CorpDNS: setArgs.acceptDNS,
|
|
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
|
|
ShieldsUp: setArgs.shieldsUp,
|
|
RunSSH: setArgs.runSSH,
|
|
RunWebClient: setArgs.runWebClient,
|
|
Hostname: setArgs.hostname,
|
|
OperatorUser: setArgs.opUser,
|
|
NoSNAT: !setArgs.snat,
|
|
ForceDaemon: setArgs.forceDaemon,
|
|
Sync: opt.NewBool(setArgs.sync),
|
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
|
Check: setArgs.updateCheck,
|
|
Apply: opt.NewBool(setArgs.updateApply),
|
|
},
|
|
AppConnector: ipn.AppConnectorPrefs{
|
|
Advertise: setArgs.advertiseConnector,
|
|
},
|
|
PostureChecking: setArgs.reportPosture,
|
|
NoStatefulFiltering: opt.NewBool(!setArgs.statefulFiltering),
|
|
},
|
|
}
|
|
|
|
if effectiveGOOS() == "linux" {
|
|
nfMode, warning, err := netfilterModeFromFlag(setArgs.netfilterMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if warning != "" {
|
|
warnf(warning)
|
|
}
|
|
maskedPrefs.Prefs.NetfilterMode = nfMode
|
|
}
|
|
|
|
if setArgs.exitNodeIP != "" {
|
|
if expr, useAutoExitNode := ipn.ParseAutoExitNodeString(setArgs.exitNodeIP); useAutoExitNode {
|
|
maskedPrefs.AutoExitNode = expr
|
|
maskedPrefs.AutoExitNodeSet = true
|
|
} else if err := maskedPrefs.Prefs.SetExitNodeIP(setArgs.exitNodeIP, st); err != nil {
|
|
var e ipn.ExitNodeLocalIPError
|
|
if errors.As(err, &e) {
|
|
return fmt.Errorf("%w; did you mean --advertise-exit-node?", err)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
warnOnAdvertiseRoutes(ctx, &maskedPrefs.Prefs)
|
|
if err := checkExitNodeRisk(ctx, &maskedPrefs.Prefs, setArgs.acceptedRisks); err != nil {
|
|
return err
|
|
}
|
|
var advertiseExitNodeSet, advertiseRoutesSet bool
|
|
setFlagSet.Visit(func(f *flag.Flag) {
|
|
updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name)
|
|
switch f.Name {
|
|
case "advertise-exit-node":
|
|
advertiseExitNodeSet = true
|
|
case "advertise-routes":
|
|
advertiseRoutesSet = true
|
|
}
|
|
})
|
|
if maskedPrefs.IsEmpty() {
|
|
return flag.ErrHelp
|
|
}
|
|
|
|
curPrefs, err := localClient.GetPrefs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if maskedPrefs.AdvertiseRoutesSet {
|
|
maskedPrefs.AdvertiseRoutes, err = calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet, curPrefs, setArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" && maskedPrefs.AppConnector.Advertise {
|
|
if err := presentRiskToUser(riskMacAppConnector, riskMacAppConnectorMessage, setArgs.acceptedRisks); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if maskedPrefs.RunSSHSet {
|
|
wantSSH, haveSSH := maskedPrefs.RunSSH, curPrefs.RunSSH
|
|
if err := presentSSHToggleRisk(wantSSH, haveSSH, setArgs.acceptedRisks); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if maskedPrefs.AutoUpdateSet.ApplySet && buildfeatures.HasClientUpdate && version.IsMacSysExt() {
|
|
apply := "0"
|
|
if maskedPrefs.AutoUpdate.Apply.EqualBool(true) {
|
|
apply = "1"
|
|
}
|
|
out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
|
}
|
|
}
|
|
|
|
if setArgs.relayServerPort != "" {
|
|
uport, err := strconv.ParseUint(setArgs.relayServerPort, 10, 16)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set relay server port: %v", err)
|
|
}
|
|
maskedPrefs.Prefs.RelayServerPort = ptr.To(uint16(uport))
|
|
}
|
|
|
|
if setArgs.relayServerStaticEndpoints != "" {
|
|
endpointsSet := make(set.Set[netip.AddrPort])
|
|
endpointsSplit := strings.Split(setArgs.relayServerStaticEndpoints, ",")
|
|
for _, s := range endpointsSplit {
|
|
ap, err := netip.ParseAddrPort(s)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set relay server static endpoints: %q is not a valid IP:port", s)
|
|
}
|
|
endpointsSet.Add(ap)
|
|
}
|
|
endpoints := endpointsSet.Slice()
|
|
slices.SortFunc(endpoints, netip.AddrPort.Compare)
|
|
maskedPrefs.Prefs.RelayServerStaticEndpoints = endpoints
|
|
}
|
|
|
|
checkPrefs := curPrefs.Clone()
|
|
checkPrefs.ApplyEdits(maskedPrefs)
|
|
if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = localClient.EditPrefs(ctx, maskedPrefs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if setArgs.runWebClient && len(st.TailscaleIPs) > 0 {
|
|
printf("\nWeb interface now running at %s:%d\n", st.TailscaleIPs[0], tsconst.WebListenPort)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// calcAdvertiseRoutesForSet returns the new value for Prefs.AdvertiseRoutes based on the
|
|
// current value, the flags passed to "tailscale set".
|
|
// advertiseExitNodeSet is whether the --advertise-exit-node flag was set.
|
|
// advertiseRoutesSet is whether the --advertise-routes flag was set.
|
|
// curPrefs is the current Prefs.
|
|
// setArgs is the parsed command-line arguments.
|
|
func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, curPrefs *ipn.Prefs, setArgs setArgsT) (routes []netip.Prefix, err error) {
|
|
if advertiseExitNodeSet && advertiseRoutesSet {
|
|
return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute)
|
|
|
|
}
|
|
if advertiseRoutesSet {
|
|
return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode())
|
|
}
|
|
if advertiseExitNodeSet {
|
|
alreadyAdvertisesExitNode := curPrefs.AdvertisesExitNode()
|
|
if alreadyAdvertisesExitNode == setArgs.advertiseDefaultRoute {
|
|
return curPrefs.AdvertiseRoutes, nil
|
|
}
|
|
routes = tsaddr.FilterPrefixesCopy(views.SliceOf(curPrefs.AdvertiseRoutes), func(p netip.Prefix) bool {
|
|
return p.Bits() != 0
|
|
})
|
|
if setArgs.advertiseDefaultRoute {
|
|
routes = append(routes, tsaddr.AllIPv4(), tsaddr.AllIPv6())
|
|
}
|
|
return routes, nil
|
|
}
|
|
return nil, nil
|
|
}
|