cmd/tailscale: define CLI tools to manipulate macOS network and system extensions (#14727)

Updates tailscale/corp#25278

Adds definitions for new CLI commands getting added in v1.80. Refactors some pre-existing CLI commands within the `configure` tree to clean up code.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
This commit is contained in:
Andrea Gottardo 2025-01-22 16:01:07 -08:00 committed by GitHub
parent 0fa7b4a236
commit 3dabea0fc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 213 additions and 68 deletions

View File

@ -190,7 +190,7 @@ change in the future.
loginCmd, loginCmd,
logoutCmd, logoutCmd,
switchCmd, switchCmd,
configureCmd, configureCmd(),
syspolicyCmd, syspolicyCmd,
netcheckCmd, netcheckCmd,
ipCmd, ipCmd,
@ -216,6 +216,7 @@ change in the future.
driveCmd, driveCmd,
idTokenCmd, idTokenCmd,
advertiseCmd(), advertiseCmd(),
configureHostCmd(),
), ),
FlagSet: rootfs, FlagSet: rootfs,
Exec: func(ctx context.Context, args []string) error { Exec: func(ctx context.Context, args []string) error {
@ -226,10 +227,6 @@ change in the future.
}, },
} }
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
walkCommands(rootCmd, func(w cmdWalk) bool { walkCommands(rootCmd, func(w cmdWalk) bool {
if w.UsageFunc == nil { if w.UsageFunc == nil {
w.UsageFunc = usageFunc w.UsageFunc = usageFunc

View File

@ -20,33 +20,31 @@ import (
"tailscale.com/version" "tailscale.com/version"
) )
func init() { func configureKubeconfigCmd() *ffcli.Command {
configureCmd.Subcommands = append(configureCmd.Subcommands, configureKubeconfigCmd) return &ffcli.Command{
} Name: "kubeconfig",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
var configureKubeconfigCmd = &ffcli.Command{ ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
Name: "kubeconfig", LongHelp: strings.TrimSpace(`
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(`
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale. Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster. The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
See: https://tailscale.com/s/k8s-auth-proxy See: https://tailscale.com/s/k8s-auth-proxy
`), `),
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("kubeconfig") fs := newFlagSet("kubeconfig")
return fs return fs
})(), })(),
Exec: runConfigureKubeconfig, Exec: runConfigureKubeconfig,
}
} }
// kubeconfigPath returns the path to the kubeconfig file for the current user. // kubeconfigPath returns the path to the kubeconfig file for the current user.
func kubeconfigPath() (string, error) { func kubeconfigPath() (string, error) {
if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" { if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
if version.IsSandboxedMacOS() { if version.IsSandboxedMacOS() {
return "", errors.New("$KUBECONFIG is incompatible with the App Store version") return "", errors.New("cannot read $KUBECONFIG on GUI builds of the macOS client: this requires the open-source tailscaled distribution")
} }
var out string var out string
for _, out = range filepath.SplitList(kubeconfig) { for _, out = range filepath.SplitList(kubeconfig) {

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ts_omit_kube
package cli
import "github.com/peterbourgon/ff/v3/ffcli"
func configureKubeconfigCmd() *ffcli.Command {
// omitted from the build when the ts_omit_kube build tag is set
return nil
}

View File

@ -22,22 +22,27 @@ import (
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
var synologyConfigureCertCmd = &ffcli.Command{ func synologyConfigureCertCmd() *ffcli.Command {
Name: "synology-cert", if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
Exec: runConfigureSynologyCert, return nil
ShortHelp: "Configure Synology with a TLS certificate for your tailnet", }
ShortUsage: "synology-cert [--domain <domain>]", return &ffcli.Command{
LongHelp: strings.TrimSpace(` Name: "synology-cert",
Exec: runConfigureSynologyCert,
ShortHelp: "Configure Synology with a TLS certificate for your tailnet",
ShortUsage: "synology-cert [--domain <domain>]",
LongHelp: strings.TrimSpace(`
This command is intended to run periodically as root on a Synology device to This command is intended to run periodically as root on a Synology device to
create or refresh the TLS certificate for the tailnet domain. create or refresh the TLS certificate for the tailnet domain.
See: https://tailscale.com/kb/1153/enabling-https See: https://tailscale.com/kb/1153/enabling-https
`), `),
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("synology-cert") fs := newFlagSet("synology-cert")
fs.StringVar(&synologyConfigureCertArgs.domain, "domain", "", "Tailnet domain to create or refresh certificates for. Ignored if only one domain exists.") fs.StringVar(&synologyConfigureCertArgs.domain, "domain", "", "Tailnet domain to create or refresh certificates for. Ignored if only one domain exists.")
return fs return fs
})(), })(),
}
} }
var synologyConfigureCertArgs struct { var synologyConfigureCertArgs struct {

View File

@ -21,34 +21,49 @@ import (
// configureHostCmd is the "tailscale configure-host" command which was once // configureHostCmd is the "tailscale configure-host" command which was once
// used to configure Synology devices, but is now a compatibility alias to // used to configure Synology devices, but is now a compatibility alias to
// "tailscale configure synology". // "tailscale configure synology".
var configureHostCmd = &ffcli.Command{ //
Name: "configure-host", // It returns nil if the actual "tailscale configure synology" command is not
Exec: runConfigureSynology, // available.
ShortUsage: "tailscale configure-host\n" + synologyConfigureCmd.ShortUsage, func configureHostCmd() *ffcli.Command {
ShortHelp: synologyConfigureCmd.ShortHelp, synologyConfigureCmd := synologyConfigureCmd()
LongHelp: hidden + synologyConfigureCmd.LongHelp, if synologyConfigureCmd == nil {
FlagSet: (func() *flag.FlagSet { // No need to offer this compatibility alias if the actual command is not available.
fs := newFlagSet("configure-host") return nil
return fs }
})(), return &ffcli.Command{
Name: "configure-host",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure-host\n" + synologyConfigureCmd.ShortUsage,
ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: hidden + synologyConfigureCmd.LongHelp,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host")
return fs
})(),
}
} }
var synologyConfigureCmd = &ffcli.Command{ func synologyConfigureCmd() *ffcli.Command {
Name: "synology", if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
Exec: runConfigureSynology, return nil
ShortUsage: "tailscale configure synology", }
ShortHelp: "Configure Synology to enable outbound connections", return &ffcli.Command{
LongHelp: strings.TrimSpace(` Name: "synology",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure synology",
ShortHelp: "Configure Synology to enable outbound connections",
LongHelp: strings.TrimSpace(`
This command is intended to run at boot as root on a Synology device to This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission create the /dev/net/tun device and give the tailscaled binary permission
to use it. to use it.
See: https://tailscale.com/s/synology-outbound See: https://tailscale.com/s/synology-outbound
`), `),
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("synology") fs := newFlagSet("synology")
return fs return fs
})(), })(),
}
} }
func runConfigureSynology(ctx context.Context, args []string) error { func runConfigureSynology(ctx context.Context, args []string) error {

View File

@ -5,32 +5,41 @@ package cli
import ( import (
"flag" "flag"
"runtime"
"strings" "strings"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/version/distro"
) )
var configureCmd = &ffcli.Command{ func configureCmd() *ffcli.Command {
Name: "configure", return &ffcli.Command{
ShortUsage: "tailscale configure <subcommand>", Name: "configure",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features", ShortUsage: "tailscale configure <subcommand>",
LongHelp: strings.TrimSpace(` ShortHelp: "Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure' set of commands are intended to provide a way to enable different The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways. services on the host to use Tailscale in more ways.
`), `),
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure") fs := newFlagSet("configure")
return fs return fs
})(), })(),
Subcommands: configureSubcommands(), Subcommands: nonNilCmds(
configureKubeconfigCmd(),
synologyConfigureCmd(),
synologyConfigureCertCmd(),
ccall(maybeSysExtCmd),
ccall(maybeVPNConfigCmd),
),
}
} }
func configureSubcommands() (out []*ffcli.Command) { // ccall calls the function f if it is non-nil, and returns its result.
if runtime.GOOS == "linux" && distro.Get() == distro.Synology { //
out = append(out, synologyConfigureCmd) // It returns the zero value of the type T if f is nil.
out = append(out, synologyConfigureCertCmd) func ccall[T any](f func() T) T {
var zero T
if f == nil {
return zero
} }
return out return f()
} }

View File

@ -0,0 +1,11 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import "github.com/peterbourgon/ff/v3/ffcli"
var (
maybeSysExtCmd func() *ffcli.Command // non-nil only on macOS, see configure_apple.go
maybeVPNConfigCmd func() *ffcli.Command // non-nil only on macOS, see configure_apple.go
)

View File

@ -0,0 +1,97 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin
package cli
import (
"context"
"errors"
"github.com/peterbourgon/ff/v3/ffcli"
)
func init() {
maybeSysExtCmd = sysExtCmd
maybeVPNConfigCmd = vpnConfigCmd
}
// Functions in this file provide a dummy Exec function that only prints an error message for users of the open-source
// tailscaled distribution. On GUI builds, the Swift code in the macOS client handles these commands by not passing the
// flow of execution to the CLI.
// sysExtCmd returns a command for managing the Tailscale system extension on macOS
// (for the Standalone variant of the client only).
func sysExtCmd() *ffcli.Command {
return &ffcli.Command{
Name: "sysext",
ShortUsage: "tailscale configure sysext [activate|deactivate|status]",
ShortHelp: "Manages the system extension for macOS (Standalone variant)",
LongHelp: "The sysext set of commands provides a way to activate, deactivate, or manage the state of the Tailscale system extension on macOS. " +
"This is only relevant if you are running the Standalone variant of the Tailscale client for macOS. " +
"To access more detailed information about system extensions installed on this Mac, run 'systemextensionsctl list'.",
Subcommands: []*ffcli.Command{
{
Name: "activate",
ShortUsage: "tailscale sysext activate",
ShortHelp: "Register the Tailscale system extension with macOS.",
LongHelp: "This command registers the Tailscale system extension with macOS. To run Tailscale, you'll also need to install the VPN configuration separately (run `tailscale configure vpn-config install`). After running this command, you need to approve the extension in System Settings > Login Items and Extensions > Network Extensions.",
Exec: requiresStandalone,
},
{
Name: "deactivate",
ShortUsage: "tailscale sysext deactivate",
ShortHelp: "Deactivate the Tailscale system extension on macOS",
LongHelp: "This command deactivates the Tailscale system extension on macOS. To completely remove Tailscale, you'll also need to delete the VPN configuration separately (use `tailscale configure vpn-config uninstall`).",
Exec: requiresStandalone,
},
{
Name: "status",
ShortUsage: "tailscale sysext status",
ShortHelp: "Print the enablement status of the Tailscale system extension",
LongHelp: "This command prints the enablement status of the Tailscale system extension. If the extension is not enabled, run `tailscale sysext activate` to enable it.",
Exec: requiresStandalone,
},
},
Exec: requiresStandalone,
}
}
// vpnConfigCmd returns a command for managing the Tailscale VPN configuration on macOS
// (the entry that appears in System Settings > VPN).
func vpnConfigCmd() *ffcli.Command {
return &ffcli.Command{
Name: "mac-vpn",
ShortUsage: "tailscale configure mac-vpn [install|uninstall]",
ShortHelp: "Manage the VPN configuration on macOS (App Store and Standalone variants)",
LongHelp: "The vpn-config set of commands provides a way to add or remove the Tailscale VPN configuration from the macOS settings. This is the entry that appears in System Settings > VPN.",
Subcommands: []*ffcli.Command{
{
Name: "install",
ShortUsage: "tailscale mac-vpn install",
ShortHelp: "Write the Tailscale VPN configuration to the macOS settings",
LongHelp: "This command writes the Tailscale VPN configuration to the macOS settings. This is the entry that appears in System Settings > VPN. If you are running the Standalone variant of the client, you'll also need to install the system extension separately (run `tailscale configure sysext activate`).",
Exec: requiresGUI,
},
{
Name: "uninstall",
ShortUsage: "tailscale mac-vpn uninstall",
ShortHelp: "Delete the Tailscale VPN configuration from the macOS settings",
LongHelp: "This command removes the Tailscale VPN configuration from the macOS settings. This is the entry that appears in System Settings > VPN. If you are running the Standalone variant of the client, you'll also need to deactivate the system extension separately (run `tailscale configure sysext deactivate`).",
Exec: requiresGUI,
},
},
Exec: func(ctx context.Context, args []string) error {
return errors.New("unsupported command: requires a GUI build of the macOS client")
},
}
}
func requiresStandalone(ctx context.Context, args []string) error {
return errors.New("unsupported command: requires the Standalone (.pkg installer) GUI build of the client")
}
func requiresGUI(ctx context.Context, args []string) error {
return errors.New("unsupported command: requires a GUI build of the macOS client")
}