Compare commits
78 Commits
licenses/c
...
v1.64.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede81e2669 | ||
|
|
02a96c8d7c | ||
|
|
ab4f9d2514 | ||
|
|
78dc8622d7 | ||
|
|
a2eb1c22b0 | ||
|
|
7f4cda23ac | ||
|
|
8fa3026614 | ||
|
|
d0f3fa7d7e | ||
|
|
db760d0bac | ||
|
|
8d83adde07 | ||
|
|
da4e92bf01 | ||
|
|
9da135dd64 | ||
|
|
1e0ebc6c6d | ||
|
|
b4ba492701 | ||
|
|
231e44e742 | ||
|
|
0001237253 | ||
|
|
b27238b654 | ||
|
|
e6983baa73 | ||
|
|
0f3a292ebd | ||
|
|
c71e8db058 | ||
|
|
5336362e64 | ||
|
|
21671ca374 | ||
|
|
b0fbd85592 | ||
|
|
a5e1f7d703 | ||
|
|
3f4c5daa15 | ||
|
|
fe22032fb3 | ||
|
|
aa084a29c6 | ||
|
|
5e7c0b025c | ||
|
|
efb710d0e5 | ||
|
|
38377c37b5 | ||
|
|
21b32b467e | ||
|
|
ac2522092d | ||
|
|
6e334e64a1 | ||
|
|
1fbaf26106 | ||
|
|
8c75da27fc | ||
|
|
306bacc669 | ||
|
|
9699bb0a20 | ||
|
|
fe0cfec4ad | ||
|
|
4bbac72868 | ||
|
|
98cf71cd73 | ||
|
|
853e3e29a0 | ||
|
|
1a38d2a3b4 | ||
|
|
7d7d159824 | ||
|
|
ac574d875c | ||
|
|
8d7894c68e | ||
|
|
92d3f64e95 | ||
|
|
93618a3518 | ||
|
|
2409661a0d | ||
|
|
b9611461e5 | ||
|
|
262fa8a01e | ||
|
|
9eaa56df93 | ||
|
|
14683371ee | ||
|
|
1c259100b0 | ||
|
|
1535d0feca | ||
|
|
f384742375 | ||
|
|
92ca770b8d | ||
|
|
27038ee3c2 | ||
|
|
ec87e219ae | ||
|
|
e2586bc674 | ||
|
|
7558a1d594 | ||
|
|
e20ce7bf0c | ||
|
|
1d2af801fa | ||
|
|
e80b99cdd1 | ||
|
|
5aa4cfad06 | ||
|
|
e7599c1f7e | ||
|
|
5fb721d4ad | ||
|
|
af61179c2f | ||
|
|
b0941b79d6 | ||
|
|
354cac74a9 | ||
|
|
9401b09028 | ||
|
|
9b5176c4d9 | ||
|
|
9e2f58f846 | ||
|
|
b60c4664c7 | ||
|
|
3e6306a782 | ||
|
|
8f27520633 | ||
|
|
008676f76e | ||
|
|
66e4d843c1 | ||
|
|
bed818a978 |
64
.github/workflows/go-licenses.yml
vendored
64
.github/workflows/go-licenses.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: go-licenses
|
||||
|
||||
on:
|
||||
# run action when a change lands in the main branch which updates go.mod or
|
||||
# our license template file. Also allow manual triggering.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- go.mod
|
||||
- .github/licenses.tmpl
|
||||
- .github/workflows/go-licenses.yml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install go-licenses
|
||||
run: |
|
||||
go install github.com/google/go-licenses@v1.2.2-0.20220825154955-5eedde1c6584
|
||||
|
||||
- name: Run go-licenses
|
||||
env:
|
||||
# include all build tags to include platform-specific dependencies
|
||||
GOFLAGS: "-tags=android,cgo,darwin,freebsd,ios,js,linux,openbsd,wasm,windows"
|
||||
run: |
|
||||
[ -d licenses ] || mkdir licenses
|
||||
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply+license-updater@tailscale.com>
|
||||
committer: License Updater <noreply+license-updater@tailscale.com>
|
||||
branch: licenses/cli
|
||||
commit-message: "licenses: update tailscale{,d} licenses"
|
||||
title: "licenses: update tailscale{,d} licenses"
|
||||
body: Triggered by ${{ github.repository }}@${{ github.sha }}
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
team-reviewers: opensource-license-reviewers
|
||||
@@ -1 +1 @@
|
||||
1.63.0
|
||||
1.64.2
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
@@ -1418,53 +1418,62 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
|
||||
return &cv, nil
|
||||
}
|
||||
|
||||
// TailFSSetFileServerAddr instructs TailFS to use the server at addr to access
|
||||
// SetUseExitNode toggles the use of an exit node on or off.
|
||||
// To turn it on, there must have been a previously used exit node.
|
||||
// The most previously used one is reused.
|
||||
// This is a convenience method for GUIs. To select an actual one, update the prefs.
|
||||
func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
|
||||
// the filesystem. This is used on platforms like Windows and MacOS to let
|
||||
// TailFS know to use the file server running in the GUI app.
|
||||
func (lc *LocalClient) TailFSSetFileServerAddr(ctx context.Context, addr string) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
|
||||
// Taildrive know to use the file server running in the GUI app.
|
||||
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareSet adds or updates the given share in the list of shares that
|
||||
// TailFS will serve to remote nodes. If a share with the same name already
|
||||
// DriveShareSet adds or updates the given share in the list of shares that
|
||||
// Taildrive will serve to remote nodes. If a share with the same name already
|
||||
// exists, the existing share is replaced/updated.
|
||||
func (lc *LocalClient) TailFSShareSet(ctx context.Context, share *tailfs.Share) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
|
||||
func (lc *LocalClient) DriveShareSet(ctx context.Context, share *drive.Share) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareRemove removes the share with the given name from the list of
|
||||
// shares that TailFS will serve to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
|
||||
// DriveShareRemove removes the share with the given name from the list of
|
||||
// shares that Taildrive will serve to remote nodes.
|
||||
func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
"DELETE",
|
||||
"/localapi/v0/tailfs/shares",
|
||||
"/localapi/v0/drive/shares",
|
||||
http.StatusNoContent,
|
||||
strings.NewReader(name))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareRename renames the share from old to new name.
|
||||
func (lc *LocalClient) TailFSShareRename(ctx context.Context, oldName, newName string) error {
|
||||
// DriveShareRename renames the share from old to new name.
|
||||
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
"POST",
|
||||
"/localapi/v0/tailfs/shares",
|
||||
"/localapi/v0/drive/shares",
|
||||
http.StatusNoContent,
|
||||
jsonBody([2]string{oldName, newName}))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareList returns the list of shares that TailFS is currently serving
|
||||
// DriveShareList returns the list of shares that drive is currently serving
|
||||
// to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareList(ctx context.Context) ([]*tailfs.Share, error) {
|
||||
result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
|
||||
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
|
||||
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
err = json.Unmarshal(result, &shares)
|
||||
return shares, err
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
// Make sure we don't again accidentally bring in a dependency on
|
||||
// TailFS or its transitive dependencies
|
||||
"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
// drive or its transitive dependencies
|
||||
"tailscale.com/drive/driveimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func restartSystemdUnit(ctx context.Context) error {
|
||||
}
|
||||
defer c.Close()
|
||||
if err := c.ReloadContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to reload tailsacled.service: %w", err)
|
||||
return fmt.Errorf("failed to reload tailscaled.service: %w", err)
|
||||
}
|
||||
ch := make(chan string, 1)
|
||||
if _, err := c.RestartUnitContext(ctx, "tailscaled.service", "replace", ch); err != nil {
|
||||
return fmt.Errorf("failed to restart tailsacled.service: %w", err)
|
||||
return fmt.Errorf("failed to restart tailscaled.service: %w", err)
|
||||
}
|
||||
select {
|
||||
case res := <-ch:
|
||||
|
||||
@@ -17,7 +17,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/tsweb
|
||||
github.com/google/uuid from tailscale.com/util/fastuuid
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
@@ -86,6 +86,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/derp from tailscale.com/cmd/derper+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/derper
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/drive from tailscale.com/client/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
@@ -114,7 +115,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
@@ -143,6 +143,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/fastuuid from tailscale.com/tsweb
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
@@ -250,6 +251,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand/v2 from tailscale.com/util/fastuuid
|
||||
mime from github.com/prometheus/common/expfmt+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
|
||||
@@ -36,19 +36,21 @@ import (
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
|
||||
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
|
||||
versionFlag = flag.Bool("version", false, "print version and exit")
|
||||
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
@@ -129,6 +131,10 @@ func writeNewConfig() config {
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.Long())
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
|
||||
"tailscale.com/prober"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
versionFlag = flag.Bool("version", false, "print version and exit")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
||||
spread = flag.Bool("spread", true, "whether to spread probing over time")
|
||||
@@ -33,6 +35,10 @@ var (
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.Long())
|
||||
return
|
||||
}
|
||||
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
|
||||
opts := []prober.DERPOpt{
|
||||
|
||||
@@ -2,7 +2,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
github.com/google/uuid from tailscale.com/tsweb
|
||||
github.com/google/uuid from tailscale.com/util/fastuuid
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
|
||||
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
|
||||
@@ -65,6 +65,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/tailcfg
|
||||
tailscale.com/util/fastuuid from tailscale.com/tsweb
|
||||
tailscale.com/util/lineread from tailscale.com/version/distro
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/slicesx from tailscale.com/tailcfg
|
||||
@@ -151,6 +152,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from math/big+
|
||||
math/rand/v2 from tailscale.com/util/fastuuid
|
||||
mime from github.com/prometheus/common/expfmt+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
|
||||
@@ -17,7 +17,7 @@ var bugReportCmd = &ffcli.Command{
|
||||
Name: "bugreport",
|
||||
Exec: runBugReport,
|
||||
ShortHelp: "Print a shareable identifier to help diagnose issues",
|
||||
ShortUsage: "bugreport [note]",
|
||||
ShortUsage: "tailscale bugreport [note]",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("bugreport")
|
||||
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
|
||||
|
||||
@@ -28,7 +28,7 @@ var certCmd = &ffcli.Command{
|
||||
Name: "cert",
|
||||
Exec: runCert,
|
||||
ShortHelp: "Get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
ShortUsage: "tailscale cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("cert")
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
||||
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
@@ -93,6 +94,49 @@ func Run(args []string) (err error) {
|
||||
})
|
||||
})
|
||||
|
||||
rootCmd := newRootCmd()
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if envknob.Bool("TS_DUMP_HELP") {
|
||||
walkCommands(rootCmd, func(c *ffcli.Command) {
|
||||
fmt.Println("===")
|
||||
// UsageFuncs are typically called during Command.Run which ensures
|
||||
// FlagSet is not nil.
|
||||
if c.FlagSet == nil {
|
||||
c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
|
||||
}
|
||||
if c.UsageFunc != nil {
|
||||
fmt.Println(c.UsageFunc(c))
|
||||
} else {
|
||||
fmt.Println(ffcli.DefaultUsageFunc(c))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
localClient.Socket = rootArgs.socket
|
||||
rootCmd.FlagSet.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "socket" {
|
||||
localClient.UseSocketOnly = true
|
||||
}
|
||||
})
|
||||
|
||||
err = rootCmd.Run(context.Background())
|
||||
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
|
||||
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
|
||||
}
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newRootCmd() *ffcli.Command {
|
||||
rootfs := newFlagSet("tailscale")
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
|
||||
|
||||
@@ -129,13 +173,14 @@ change in the future.
|
||||
certCmd,
|
||||
netlockCmd,
|
||||
licensesCmd,
|
||||
exitNodeCmd,
|
||||
exitNodeCmd(),
|
||||
updateCmd,
|
||||
whoisCmd,
|
||||
debugCmd,
|
||||
driveCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
if envknob.UseWIPCode() {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands,
|
||||
@@ -143,45 +188,16 @@ change in the future.
|
||||
)
|
||||
}
|
||||
|
||||
// Don't advertise these commands, but they're still explicitly available.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "share"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, shareCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
}
|
||||
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
walkCommands(rootCmd, func(c *ffcli.Command) {
|
||||
if c.UsageFunc == nil {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
localClient.Socket = rootArgs.socket
|
||||
rootfs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "socket" {
|
||||
localClient.UseSocketOnly = true
|
||||
}
|
||||
})
|
||||
|
||||
err = rootCmd.Run(context.Background())
|
||||
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
|
||||
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
|
||||
}
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...any) {
|
||||
@@ -200,6 +216,13 @@ var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
func walkCommands(cmd *ffcli.Command, f func(*ffcli.Command)) {
|
||||
f(cmd)
|
||||
for _, sub := range cmd.Subcommands {
|
||||
walkCommands(sub, f)
|
||||
}
|
||||
}
|
||||
|
||||
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
|
||||
func usageFuncNoDefaultValues(c *ffcli.Command) string {
|
||||
return usageFuncOpt(c, false)
|
||||
@@ -211,23 +234,32 @@ func usageFunc(c *ffcli.Command) string {
|
||||
|
||||
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
var b strings.Builder
|
||||
const hiddenPrefix = "HIDDEN: "
|
||||
|
||||
if c.ShortHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "USAGE\n")
|
||||
if c.ShortUsage != "" {
|
||||
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
|
||||
fmt.Fprintf(&b, " %s\n", strings.ReplaceAll(c.ShortUsage, "\n", "\n "))
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s\n", c.Name)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
if c.LongHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
|
||||
help, _ := strings.CutPrefix(c.LongHelp, hiddenPrefix)
|
||||
fmt.Fprintf(&b, "%s\n\n", help)
|
||||
}
|
||||
|
||||
if len(c.Subcommands) > 0 {
|
||||
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
for _, subcommand := range c.Subcommands {
|
||||
if strings.HasPrefix(subcommand.LongHelp, hiddenPrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
||||
}
|
||||
tw.Flush()
|
||||
@@ -240,7 +272,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
var s string
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
if strings.HasPrefix(usage, "HIDDEN: ") {
|
||||
if strings.HasPrefix(usage, hiddenPrefix) {
|
||||
return
|
||||
}
|
||||
if isBoolFlag(f) {
|
||||
@@ -287,3 +319,17 @@ func countFlags(fs *flag.FlagSet) (n int) {
|
||||
fs.VisitAll(func(*flag.Flag) { n++ })
|
||||
return n
|
||||
}
|
||||
|
||||
// colorableOutput returns a colorable writer if stdout is a terminal (not, say,
|
||||
// redirected to a file or pipe), the Stdout writer is os.Stdout (we're not
|
||||
// embedding the CLI in wasm or a mobile app), and NO_COLOR is not set (see
|
||||
// https://no-color.org/). If any of those is not the case, ok is false
|
||||
// and w is Stdout.
|
||||
func colorableOutput() (w io.Writer, ok bool) {
|
||||
if Stdout != os.Stdout ||
|
||||
os.Getenv("NO_COLOR") != "" ||
|
||||
!isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
return Stdout, false
|
||||
}
|
||||
return colorable.NewColorableStdout(), true
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -28,6 +30,32 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
|
||||
envknob.PanicIfAnyEnvCheckedInInit()
|
||||
}
|
||||
|
||||
func TestShortUsage_FullCmd(t *testing.T) {
|
||||
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
if !envknob.UseWIPCode() {
|
||||
t.Fatal("expected envknob.UseWIPCode() to be true")
|
||||
}
|
||||
|
||||
// Some commands have more than one path from the root, so investigate all
|
||||
// paths before we report errors.
|
||||
ok := make(map[*ffcli.Command]bool)
|
||||
root := newRootCmd()
|
||||
walkCommands(root, func(c *ffcli.Command) {
|
||||
if !ok[c] {
|
||||
ok[c] = strings.HasPrefix(c.ShortUsage, "tailscale ") && (c.Name == "tailscale" || strings.Contains(c.ShortUsage, " "+c.Name+" ") || strings.HasSuffix(c.ShortUsage, " "+c.Name))
|
||||
}
|
||||
})
|
||||
walkCommands(root, func(c *ffcli.Command) {
|
||||
if !ok[c] {
|
||||
t.Errorf("subcommand %s should show full usage ('tailscale ... %s ...') in ShortUsage (%q)", c.Name, c.Name, c.ShortUsage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
// But it should include anything handled specially (e.g. linux, windows)
|
||||
// and at least one thing that's not (darwin, freebsd).
|
||||
@@ -829,10 +857,14 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
// Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
|
||||
// a CLI flag for this. The Pref is used by c2n.
|
||||
continue
|
||||
case "TailFSShares":
|
||||
case "DriveShares":
|
||||
// Handled by the tailscale share subcommand, we don't want a CLI
|
||||
// flag for this.
|
||||
continue
|
||||
case "InternalExitNodePrior":
|
||||
// Used internally by LocalBackend as part of exit node usage toggling.
|
||||
// No CLI flag for this.
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func init() {
|
||||
var configureKubeconfigCmd = &ffcli.Command{
|
||||
Name: "kubeconfig",
|
||||
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
|
||||
ShortUsage: "kubeconfig <hostname-or-fqdn>",
|
||||
ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
|
||||
|
||||
@@ -43,7 +43,20 @@ See: https://tailscale.com/s/k8s-auth-proxy
|
||||
}
|
||||
|
||||
// kubeconfigPath returns the path to the kubeconfig file for the current user.
|
||||
func kubeconfigPath() string {
|
||||
func kubeconfigPath() (string, error) {
|
||||
if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
|
||||
if version.IsSandboxedMacOS() {
|
||||
return "", errors.New("$KUBECONFIG is incompatible with the App Store version")
|
||||
}
|
||||
var out string
|
||||
for _, out = range filepath.SplitList(kubeconfig) {
|
||||
if info, err := os.Stat(out); !os.IsNotExist(err) && !info.IsDir() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
var dir string
|
||||
if version.IsSandboxedMacOS() {
|
||||
// The HOME environment variable in macOS sandboxed apps is set to
|
||||
@@ -55,7 +68,7 @@ func kubeconfigPath() string {
|
||||
} else {
|
||||
dir = homedir.HomeDir()
|
||||
}
|
||||
return filepath.Join(dir, ".kube", "config")
|
||||
return filepath.Join(dir, ".kube", "config"), nil
|
||||
}
|
||||
|
||||
func runConfigureKubeconfig(ctx context.Context, args []string) error {
|
||||
@@ -76,7 +89,11 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("no peer found with hostname %q", hostOrFQDN)
|
||||
}
|
||||
targetFQDN = strings.TrimSuffix(targetFQDN, ".")
|
||||
if err := setKubeconfigForPeer(targetFQDN, kubeconfigPath()); err != nil {
|
||||
var kubeconfig string
|
||||
if kubeconfig, err = kubeconfigPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setKubeconfigForPeer(targetFQDN, kubeconfig); err != nil {
|
||||
return err
|
||||
}
|
||||
printf("kubeconfig configured for %q\n", hostOrFQDN)
|
||||
|
||||
220
cmd/tailscale/cli/configure-synology-cert.go
Normal file
220
cmd/tailscale/cli/configure-synology-cert.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var synologyConfigureCertCmd = &ffcli.Command{
|
||||
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
|
||||
create or refresh the TLS certificate for the tailnet domain.
|
||||
|
||||
See: https://tailscale.com/kb/1153/enabling-https
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("synology-cert")
|
||||
fs.StringVar(&synologyConfigureCertArgs.domain, "domain", "", "Tailnet domain to create or refresh certificates for. Ignored if only one domain exists.")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var synologyConfigureCertArgs struct {
|
||||
domain string
|
||||
}
|
||||
|
||||
func runConfigureSynologyCert(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return errors.New("only implemented on Synology")
|
||||
}
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
hi := hostinfo.New()
|
||||
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
|
||||
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
|
||||
}
|
||||
|
||||
domain := synologyConfigureCertArgs.domain
|
||||
if st, err := localClient.Status(ctx); err == nil {
|
||||
if st.BackendState != ipn.Running.String() {
|
||||
return fmt.Errorf("Tailscale is not running.")
|
||||
} else if len(st.CertDomains) == 0 {
|
||||
return fmt.Errorf("TLS certificate support is not enabled/configured for your tailnet.")
|
||||
} else if len(st.CertDomains) == 1 {
|
||||
if domain != "" && domain != st.CertDomains[0] {
|
||||
log.Printf("Ignoring supplied domain %q, TLS certificate will be created for %q.\n", domain, st.CertDomains[0])
|
||||
}
|
||||
domain = st.CertDomains[0]
|
||||
} else {
|
||||
var found bool
|
||||
for _, d := range st.CertDomains {
|
||||
if d == domain {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("Domain %q was not one of the valid domain options: %q.", domain, st.CertDomains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for an existing certificate, and replace it if it already exists
|
||||
var id string
|
||||
certs, err := listCerts(ctx, synowebapiCommand{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range certs {
|
||||
if c.Subject.CommonName == domain {
|
||||
id = c.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Certs have to be written to file for the upload command to work.
|
||||
tmpDir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
keyFile := path.Join(tmpDir, "key.pem")
|
||||
os.WriteFile(keyFile, keyPEM, 0600)
|
||||
certFile := path.Join(tmpDir, "cert.pem")
|
||||
os.WriteFile(certFile, certPEM, 0600)
|
||||
|
||||
if err := uploadCert(ctx, synowebapiCommand{}, certFile, keyFile, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type subject struct {
|
||||
CommonName string `json:"common_name"`
|
||||
}
|
||||
|
||||
type certificateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Desc string `json:"desc"`
|
||||
Subject subject `json:"subject"`
|
||||
}
|
||||
|
||||
// listCerts fetches a list of the certificates that DSM knows about
|
||||
func listCerts(ctx context.Context, c synoAPICaller) ([]certificateInfo, error) {
|
||||
rawData, err := c.Call(ctx, "SYNO.Core.Certificate.CRT", "list", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Certificates []certificateInfo `json:"certificates"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &payload); err != nil {
|
||||
return nil, fmt.Errorf("decoding certificate list response payload: %w", err)
|
||||
}
|
||||
|
||||
return payload.Certificates, nil
|
||||
}
|
||||
|
||||
// uploadCert creates or replaces a certificate. If id is given, it will attempt to replace the certificate with that ID.
|
||||
func uploadCert(ctx context.Context, c synoAPICaller, certFile, keyFile string, id string) error {
|
||||
params := map[string]string{
|
||||
"key_tmp": keyFile,
|
||||
"cert_tmp": certFile,
|
||||
"desc": "Tailnet Certificate",
|
||||
}
|
||||
if id != "" {
|
||||
params["id"] = id
|
||||
}
|
||||
|
||||
rawData, err := c.Call(ctx, "SYNO.Core.Certificate", "import", params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
NewID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &payload); err != nil {
|
||||
return fmt.Errorf("decoding certificate upload response payload: %w", err)
|
||||
}
|
||||
log.Printf("Tailnet Certificate uploaded with ID %q.", payload.NewID)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
type synoAPICaller interface {
|
||||
Call(context.Context, string, string, map[string]string) (json.RawMessage, error)
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error *apiError `json:"error,omitempty"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Code int64 `json:"code"`
|
||||
Errors string `json:"errors"`
|
||||
}
|
||||
|
||||
// synowebapiCommand implements synoAPICaller using the /usr/syno/bin/synowebapi binary. Must be run as root.
|
||||
type synowebapiCommand struct{}
|
||||
|
||||
func (s synowebapiCommand) Call(ctx context.Context, api, method string, params map[string]string) (json.RawMessage, error) {
|
||||
args := []string{"--exec", fmt.Sprintf("api=%s", api), fmt.Sprintf("method=%s", method)}
|
||||
|
||||
for k, v := range params {
|
||||
args = append(args, fmt.Sprintf("%s=%q", k, v))
|
||||
}
|
||||
|
||||
out, err := exec.CommandContext(ctx, "/usr/syno/bin/synowebapi", args...).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calling %q method of %q API: %v, %s", method, api, err, out)
|
||||
}
|
||||
|
||||
var payload apiResponse
|
||||
if err := json.Unmarshal(out, &payload); err != nil {
|
||||
return nil, fmt.Errorf("decoding response json from %q method of %q API: %w", method, api, err)
|
||||
}
|
||||
|
||||
if payload.Error != nil {
|
||||
return nil, fmt.Errorf("error response from %q method of %q API: %v", method, api, payload.Error)
|
||||
}
|
||||
|
||||
return payload.Data, nil
|
||||
}
|
||||
140
cmd/tailscale/cli/configure-synology-cert_test.go
Normal file
140
cmd/tailscale/cli/configure-synology-cert_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeAPICaller struct {
|
||||
Data json.RawMessage
|
||||
Error error
|
||||
}
|
||||
|
||||
func (c fakeAPICaller) Call(_ context.Context, _, _ string, _ map[string]string) (json.RawMessage, error) {
|
||||
return c.Data, c.Error
|
||||
}
|
||||
|
||||
func Test_listCerts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
caller synoAPICaller
|
||||
want []certificateInfo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal response",
|
||||
caller: fakeAPICaller{
|
||||
Data: json.RawMessage(`{
|
||||
"certificates" : [
|
||||
{
|
||||
"desc" : "Tailnet Certificate",
|
||||
"id" : "cG2XBt",
|
||||
"is_broken" : false,
|
||||
"is_default" : false,
|
||||
"issuer" : {
|
||||
"common_name" : "R3",
|
||||
"country" : "US",
|
||||
"organization" : "Let's Encrypt"
|
||||
},
|
||||
"key_types" : "ECC",
|
||||
"renewable" : false,
|
||||
"services" : [
|
||||
{
|
||||
"display_name" : "DSM Desktop Service",
|
||||
"display_name_i18n" : "common:web_desktop",
|
||||
"isPkg" : false,
|
||||
"multiple_cert" : true,
|
||||
"owner" : "root",
|
||||
"service" : "default",
|
||||
"subscriber" : "system",
|
||||
"user_setable" : true
|
||||
}
|
||||
],
|
||||
"signature_algorithm" : "sha256WithRSAEncryption",
|
||||
"subject" : {
|
||||
"common_name" : "foo.tailscale.ts.net",
|
||||
"sub_alt_name" : [ "foo.tailscale.ts.net" ]
|
||||
},
|
||||
"user_deletable" : true,
|
||||
"valid_from" : "Sep 26 11:39:43 2023 GMT",
|
||||
"valid_till" : "Dec 25 11:39:42 2023 GMT"
|
||||
},
|
||||
{
|
||||
"desc" : "",
|
||||
"id" : "sgmnpb",
|
||||
"is_broken" : false,
|
||||
"is_default" : false,
|
||||
"issuer" : {
|
||||
"city" : "Taipei",
|
||||
"common_name" : "Synology Inc. CA",
|
||||
"country" : "TW",
|
||||
"organization" : "Synology Inc."
|
||||
},
|
||||
"key_types" : "",
|
||||
"renewable" : false,
|
||||
"self_signed_cacrt_info" : {
|
||||
"issuer" : {
|
||||
"city" : "Taipei",
|
||||
"common_name" : "Synology Inc. CA",
|
||||
"country" : "TW",
|
||||
"organization" : "Synology Inc."
|
||||
},
|
||||
"subject" : {
|
||||
"city" : "Taipei",
|
||||
"common_name" : "Synology Inc. CA",
|
||||
"country" : "TW",
|
||||
"organization" : "Synology Inc."
|
||||
}
|
||||
},
|
||||
"services" : [],
|
||||
"signature_algorithm" : "sha256WithRSAEncryption",
|
||||
"subject" : {
|
||||
"city" : "Taipei",
|
||||
"common_name" : "synology.com",
|
||||
"country" : "TW",
|
||||
"organization" : "Synology Inc.",
|
||||
"sub_alt_name" : []
|
||||
},
|
||||
"user_deletable" : true,
|
||||
"valid_from" : "May 27 00:23:19 2019 GMT",
|
||||
"valid_till" : "Feb 11 00:23:19 2039 GMT"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
Error: nil,
|
||||
},
|
||||
want: []certificateInfo{
|
||||
{Desc: "Tailnet Certificate", ID: "cG2XBt", Subject: subject{CommonName: "foo.tailscale.ts.net"}},
|
||||
{Desc: "", ID: "sgmnpb", Subject: subject{CommonName: "synology.com"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call error",
|
||||
caller: fakeAPICaller{nil, fmt.Errorf("caller failed")},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "payload decode error",
|
||||
caller: fakeAPICaller{json.RawMessage("This isn't JSON!"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := listCerts(context.Background(), tt.caller)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("listCerts() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("listCerts() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,11 @@ import (
|
||||
// used to configure Synology devices, but is now a compatibility alias to
|
||||
// "tailscale configure synology".
|
||||
var configureHostCmd = &ffcli.Command{
|
||||
Name: "configure-host",
|
||||
Exec: runConfigureSynology,
|
||||
ShortHelp: synologyConfigureCmd.ShortHelp,
|
||||
LongHelp: synologyConfigureCmd.LongHelp,
|
||||
Name: "configure-host",
|
||||
Exec: runConfigureSynology,
|
||||
ShortUsage: "tailscale configure-host",
|
||||
ShortHelp: synologyConfigureCmd.ShortHelp,
|
||||
LongHelp: synologyConfigureCmd.LongHelp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure-host")
|
||||
return fs
|
||||
@@ -33,9 +34,10 @@ var configureHostCmd = &ffcli.Command{
|
||||
}
|
||||
|
||||
var synologyConfigureCmd = &ffcli.Command{
|
||||
Name: "synology",
|
||||
Exec: runConfigureSynology,
|
||||
ShortHelp: "Configure Synology to enable outbound connections",
|
||||
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
|
||||
create the /dev/net/tun device and give the tailscaled binary permission
|
||||
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
var configureCmd = &ffcli.Command{
|
||||
Name: "configure",
|
||||
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
|
||||
Name: "configure",
|
||||
ShortUsage: "tailscale configure <subcommand>",
|
||||
ShortHelp: "[ALPHA] 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
|
||||
services on the host to use Tailscale in more ways.
|
||||
@@ -33,6 +34,7 @@ services on the host to use Tailscale in more ways.
|
||||
func configureSubcommands() (out []*ffcli.Command) {
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
out = append(out, synologyConfigureCmd)
|
||||
out = append(out, synologyConfigureCertCmd)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ import (
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
ShortUsage: "tailscale debug <debug-flags | subcommand>",
|
||||
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
@@ -58,15 +59,16 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "derp-map",
|
||||
Exec: runDERPMap,
|
||||
ShortHelp: "print DERP map",
|
||||
Name: "derp-map",
|
||||
ShortUsage: "tailscale debug derp-map",
|
||||
Exec: runDERPMap,
|
||||
ShortHelp: "Print DERP map",
|
||||
},
|
||||
{
|
||||
Name: "component-logs",
|
||||
Exec: runDebugComponentLogs,
|
||||
ShortHelp: "enable/disable debug logs for a component",
|
||||
ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]",
|
||||
Exec: runDebugComponentLogs,
|
||||
ShortHelp: "Enable/disable debug logs for a component",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("component-logs")
|
||||
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
|
||||
@@ -74,14 +76,16 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "daemon-goroutines",
|
||||
Exec: runDaemonGoroutines,
|
||||
ShortHelp: "print tailscaled's goroutines",
|
||||
Name: "daemon-goroutines",
|
||||
ShortUsage: "tailscale debug daemon-goroutines",
|
||||
Exec: runDaemonGoroutines,
|
||||
ShortHelp: "Print tailscaled's goroutines",
|
||||
},
|
||||
{
|
||||
Name: "daemon-logs",
|
||||
Exec: runDaemonLogs,
|
||||
ShortHelp: "watch tailscaled's server logs",
|
||||
Name: "daemon-logs",
|
||||
ShortUsage: "tailscale debug daemon-logs",
|
||||
Exec: runDaemonLogs,
|
||||
ShortHelp: "Watch tailscaled's server logs",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("daemon-logs")
|
||||
fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level")
|
||||
@@ -90,9 +94,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Exec: runDaemonMetrics,
|
||||
ShortHelp: "print tailscaled's metrics",
|
||||
Name: "metrics",
|
||||
ShortUsage: "tailscale debug metrics",
|
||||
Exec: runDaemonMetrics,
|
||||
ShortHelp: "Print tailscaled's metrics",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("metrics")
|
||||
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
|
||||
@@ -100,80 +105,95 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "env",
|
||||
Exec: runEnv,
|
||||
ShortHelp: "print cmd/tailscale environment",
|
||||
Name: "env",
|
||||
ShortUsage: "tailscale debug env",
|
||||
Exec: runEnv,
|
||||
ShortHelp: "Print cmd/tailscale environment",
|
||||
},
|
||||
{
|
||||
Name: "stat",
|
||||
Exec: runStat,
|
||||
ShortHelp: "stat a file",
|
||||
Name: "stat",
|
||||
ShortUsage: "tailscale debug stat <files...>",
|
||||
Exec: runStat,
|
||||
ShortHelp: "Stat a file",
|
||||
},
|
||||
{
|
||||
Name: "hostinfo",
|
||||
Exec: runHostinfo,
|
||||
ShortHelp: "print hostinfo",
|
||||
Name: "hostinfo",
|
||||
ShortUsage: "tailscale debug hostinfo",
|
||||
Exec: runHostinfo,
|
||||
ShortHelp: "Print hostinfo",
|
||||
},
|
||||
{
|
||||
Name: "local-creds",
|
||||
Exec: runLocalCreds,
|
||||
ShortHelp: "print how to access Tailscale LocalAPI",
|
||||
Name: "local-creds",
|
||||
ShortUsage: "tailscale debug local-creds",
|
||||
Exec: runLocalCreds,
|
||||
ShortHelp: "Print how to access Tailscale LocalAPI",
|
||||
},
|
||||
{
|
||||
Name: "restun",
|
||||
Exec: localAPIAction("restun"),
|
||||
ShortHelp: "force a magicsock restun",
|
||||
Name: "restun",
|
||||
ShortUsage: "tailscale debug restun",
|
||||
Exec: localAPIAction("restun"),
|
||||
ShortHelp: "Force a magicsock restun",
|
||||
},
|
||||
{
|
||||
Name: "rebind",
|
||||
Exec: localAPIAction("rebind"),
|
||||
ShortHelp: "force a magicsock rebind",
|
||||
Name: "rebind",
|
||||
ShortUsage: "tailscale debug rebind",
|
||||
Exec: localAPIAction("rebind"),
|
||||
ShortHelp: "Force a magicsock rebind",
|
||||
},
|
||||
{
|
||||
Name: "derp-set-on-demand",
|
||||
Exec: localAPIAction("derp-set-homeless"),
|
||||
ShortHelp: "enable DERP on-demand mode (breaks reachability)",
|
||||
Name: "derp-set-on-demand",
|
||||
ShortUsage: "tailscale debug derp-set-on-demand",
|
||||
Exec: localAPIAction("derp-set-homeless"),
|
||||
ShortHelp: "Enable DERP on-demand mode (breaks reachability)",
|
||||
},
|
||||
{
|
||||
Name: "derp-unset-on-demand",
|
||||
Exec: localAPIAction("derp-unset-homeless"),
|
||||
ShortHelp: "disable DERP on-demand mode",
|
||||
Name: "derp-unset-on-demand",
|
||||
ShortUsage: "tailscale debug derp-unset-on-demand",
|
||||
Exec: localAPIAction("derp-unset-homeless"),
|
||||
ShortHelp: "Disable DERP on-demand mode",
|
||||
},
|
||||
{
|
||||
Name: "break-tcp-conns",
|
||||
Exec: localAPIAction("break-tcp-conns"),
|
||||
ShortHelp: "break any open TCP connections from the daemon",
|
||||
Name: "break-tcp-conns",
|
||||
ShortUsage: "tailscale debug break-tcp-conns",
|
||||
Exec: localAPIAction("break-tcp-conns"),
|
||||
ShortHelp: "Break any open TCP connections from the daemon",
|
||||
},
|
||||
{
|
||||
Name: "break-derp-conns",
|
||||
Exec: localAPIAction("break-derp-conns"),
|
||||
ShortHelp: "break any open DERP connections from the daemon",
|
||||
Name: "break-derp-conns",
|
||||
ShortUsage: "tailscale debug break-derp-conns",
|
||||
Exec: localAPIAction("break-derp-conns"),
|
||||
ShortHelp: "Break any open DERP connections from the daemon",
|
||||
},
|
||||
{
|
||||
Name: "pick-new-derp",
|
||||
Exec: localAPIAction("pick-new-derp"),
|
||||
ShortHelp: "switch to some other random DERP home region for a short time",
|
||||
Name: "pick-new-derp",
|
||||
ShortUsage: "tailscale debug pick-new-derp",
|
||||
Exec: localAPIAction("pick-new-derp"),
|
||||
ShortHelp: "Switch to some other random DERP home region for a short time",
|
||||
},
|
||||
{
|
||||
Name: "force-netmap-update",
|
||||
Exec: localAPIAction("force-netmap-update"),
|
||||
ShortHelp: "force a full no-op netmap update (for load testing)",
|
||||
Name: "force-netmap-update",
|
||||
ShortUsage: "tailscale debug force-netmap-update",
|
||||
Exec: localAPIAction("force-netmap-update"),
|
||||
ShortHelp: "Force a full no-op netmap update (for load testing)",
|
||||
},
|
||||
{
|
||||
// TODO(bradfitz,maisem): eventually promote this out of debug
|
||||
Name: "reload-config",
|
||||
Exec: reloadConfig,
|
||||
ShortHelp: "reload config",
|
||||
Name: "reload-config",
|
||||
ShortUsage: "tailscale debug reload-config",
|
||||
Exec: reloadConfig,
|
||||
ShortHelp: "Reload config",
|
||||
},
|
||||
{
|
||||
Name: "control-knobs",
|
||||
Exec: debugControlKnobs,
|
||||
ShortHelp: "see current control knobs",
|
||||
Name: "control-knobs",
|
||||
ShortUsage: "tailscale debug control-knobs",
|
||||
Exec: debugControlKnobs,
|
||||
ShortHelp: "See current control knobs",
|
||||
},
|
||||
{
|
||||
Name: "prefs",
|
||||
Exec: runPrefs,
|
||||
ShortHelp: "print prefs",
|
||||
Name: "prefs",
|
||||
ShortUsage: "tailscale debug prefs",
|
||||
Exec: runPrefs,
|
||||
ShortHelp: "Print prefs",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("prefs")
|
||||
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
|
||||
@@ -181,9 +201,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "watch-ipn",
|
||||
Exec: runWatchIPN,
|
||||
ShortHelp: "subscribe to IPN message bus",
|
||||
Name: "watch-ipn",
|
||||
ShortUsage: "tailscale debug watch-ipn",
|
||||
Exec: runWatchIPN,
|
||||
ShortHelp: "Subscribe to IPN message bus",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("watch-ipn")
|
||||
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
|
||||
@@ -194,9 +215,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "netmap",
|
||||
Exec: runNetmap,
|
||||
ShortHelp: "print the current network map",
|
||||
Name: "netmap",
|
||||
ShortUsage: "tailscale debug netmap",
|
||||
Exec: runNetmap,
|
||||
ShortHelp: "Print the current network map",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("netmap")
|
||||
fs.BoolVar(&netmapArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
|
||||
@@ -204,14 +226,17 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "via",
|
||||
Name: "via",
|
||||
ShortUsage: "tailscale via <site-id> <v4-cidr>\n" +
|
||||
"tailscale via <v6-route>",
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
ShortHelp: "Convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
{
|
||||
Name: "ts2021",
|
||||
Exec: runTS2021,
|
||||
ShortHelp: "debug ts2021 protocol connectivity",
|
||||
Name: "ts2021",
|
||||
ShortUsage: "tailscale debug ts2021",
|
||||
Exec: runTS2021,
|
||||
ShortHelp: "Debug ts2021 protocol connectivity",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ts2021")
|
||||
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
|
||||
@@ -221,9 +246,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "set-expire",
|
||||
Exec: runSetExpire,
|
||||
ShortHelp: "manipulate node key expiry for testing",
|
||||
Name: "set-expire",
|
||||
ShortUsage: "tailscale debug set-expire --in=1m",
|
||||
Exec: runSetExpire,
|
||||
ShortHelp: "Manipulate node key expiry for testing",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("set-expire")
|
||||
fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now")
|
||||
@@ -231,9 +257,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "dev-store-set",
|
||||
Exec: runDevStoreSet,
|
||||
ShortHelp: "set a key/value pair during development",
|
||||
Name: "dev-store-set",
|
||||
ShortUsage: "tailscale debug dev-store-set",
|
||||
Exec: runDevStoreSet,
|
||||
ShortHelp: "Set a key/value pair during development",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("store-set")
|
||||
fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger")
|
||||
@@ -241,14 +268,16 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "derp",
|
||||
Exec: runDebugDERP,
|
||||
ShortHelp: "test a DERP configuration",
|
||||
Name: "derp",
|
||||
ShortUsage: "tailscale debug derp",
|
||||
Exec: runDebugDERP,
|
||||
ShortHelp: "Test a DERP configuration",
|
||||
},
|
||||
{
|
||||
Name: "capture",
|
||||
Exec: runCapture,
|
||||
ShortHelp: "streams pcaps for debugging",
|
||||
Name: "capture",
|
||||
ShortUsage: "tailscale debug capture",
|
||||
Exec: runCapture,
|
||||
ShortHelp: "Streams pcaps for debugging",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("capture")
|
||||
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
|
||||
@@ -256,9 +285,10 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "portmap",
|
||||
Exec: debugPortmap,
|
||||
ShortHelp: "run portmap debugging",
|
||||
Name: "portmap",
|
||||
ShortUsage: "tailscale debug portmap",
|
||||
Exec: debugPortmap,
|
||||
ShortHelp: "Run portmap debugging",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("portmap")
|
||||
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
|
||||
@@ -270,14 +300,16 @@ var debugCmd = &ffcli.Command{
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "peer-endpoint-changes",
|
||||
Exec: runPeerEndpointChanges,
|
||||
ShortHelp: "prints debug information about a peer's endpoint changes",
|
||||
Name: "peer-endpoint-changes",
|
||||
ShortUsage: "tailscale debug peer-endpoint-changes <hostname-or-IP>",
|
||||
Exec: runPeerEndpointChanges,
|
||||
ShortHelp: "Prints debug information about a peer's endpoint changes",
|
||||
},
|
||||
{
|
||||
Name: "dial-types",
|
||||
Exec: runDebugDialTypes,
|
||||
ShortHelp: "prints debug information about connecting to a given host or IP",
|
||||
Name: "dial-types",
|
||||
ShortUsage: "tailscale debug dial-types <hostname-or-IP> <port>",
|
||||
Exec: runDebugDialTypes,
|
||||
ShortHelp: "Prints debug information about connecting to a given host or IP",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("dial-types")
|
||||
fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`)
|
||||
@@ -453,7 +485,7 @@ func runWatchIPN(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
fmt.Fprintf(os.Stderr, "Connected.\n")
|
||||
fmt.Fprintf(Stderr, "Connected.\n")
|
||||
for seen := 0; watchIPNArgs.count == 0 || seen < watchIPNArgs.count; seen++ {
|
||||
n, err := watcher.Next()
|
||||
if err != nil {
|
||||
@@ -563,7 +595,7 @@ func runStat(ctx context.Context, args []string) error {
|
||||
func runHostinfo(ctx context.Context, args []string) error {
|
||||
hi := hostinfo.New()
|
||||
j, _ := json.MarshalIndent(hi, "", " ")
|
||||
os.Stdout.Write(j)
|
||||
Stdout.Write(j)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -716,7 +748,7 @@ var ts2021Args struct {
|
||||
}
|
||||
|
||||
func runTS2021(ctx context.Context, args []string) error {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetOutput(Stdout)
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
|
||||
@@ -867,7 +899,7 @@ var setExpireArgs struct {
|
||||
|
||||
func runSetExpire(ctx context.Context, args []string) error {
|
||||
if len(args) != 0 || setExpireArgs.in == 0 {
|
||||
return errors.New("usage --in=<duration>")
|
||||
return errors.New("usage: tailscale debug set-expire --in=<duration>")
|
||||
}
|
||||
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
|
||||
}
|
||||
@@ -885,7 +917,7 @@ func runCapture(ctx context.Context, args []string) error {
|
||||
|
||||
switch captureArgs.outFile {
|
||||
case "-":
|
||||
fmt.Fprintln(os.Stderr, "Press Ctrl-C to stop the capture.")
|
||||
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
|
||||
_, err = io.Copy(os.Stdout, stream)
|
||||
return err
|
||||
case "":
|
||||
@@ -911,7 +943,7 @@ func runCapture(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintln(os.Stderr, "Press Ctrl-C to stop the capture.")
|
||||
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
|
||||
_, err = io.Copy(f, stream)
|
||||
return err
|
||||
}
|
||||
@@ -966,7 +998,7 @@ func runPeerEndpointChanges(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: peer-status <hostname-or-IP>")
|
||||
return errors.New("usage: tailscale debug peer-endpoint-changes <hostname-or-IP>")
|
||||
}
|
||||
var ip string
|
||||
|
||||
@@ -1042,7 +1074,7 @@ func runDebugDialTypes(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if len(args) != 2 || args[0] == "" || args[1] == "" {
|
||||
return errors.New("usage: dial-types <hostname-or-IP> <port>")
|
||||
return errors.New("usage: tailscale debug dial-types <hostname-or-IP> <port>")
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(args[1], 10, 16)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortUsage: "tailscale down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
|
||||
@@ -10,111 +10,116 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/drive"
|
||||
)
|
||||
|
||||
const (
|
||||
shareSetUsage = "share set <name> <path>"
|
||||
shareRenameUsage = "share rename <oldname> <newname>"
|
||||
shareRemoveUsage = "share remove <name>"
|
||||
shareListUsage = "share list"
|
||||
driveShareUsage = "tailscale drive share <name> <path>"
|
||||
driveRenameUsage = "tailscale drive rename <oldname> <newname>"
|
||||
driveUnshareUsage = "tailscale drive unshare <name>"
|
||||
driveListUsage = "tailscale drive list"
|
||||
)
|
||||
|
||||
var shareCmd = &ffcli.Command{
|
||||
Name: "share",
|
||||
var driveCmd = &ffcli.Command{
|
||||
Name: "drive",
|
||||
ShortHelp: "Share a directory with your tailnet",
|
||||
ShortUsage: strings.Join([]string{
|
||||
shareSetUsage,
|
||||
shareRemoveUsage,
|
||||
shareListUsage,
|
||||
driveShareUsage,
|
||||
driveRenameUsage,
|
||||
driveUnshareUsage,
|
||||
driveListUsage,
|
||||
}, "\n "),
|
||||
LongHelp: buildShareLongHelp(),
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "set",
|
||||
Exec: runShareSet,
|
||||
ShortHelp: "[ALPHA] set a share",
|
||||
UsageFunc: usageFunc,
|
||||
Name: "share",
|
||||
ShortUsage: driveShareUsage,
|
||||
Exec: runDriveShare,
|
||||
ShortHelp: "[ALPHA] create or modify a share",
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
ShortHelp: "[ALPHA] rename a share",
|
||||
Exec: runShareRename,
|
||||
UsageFunc: usageFunc,
|
||||
Name: "rename",
|
||||
ShortUsage: driveRenameUsage,
|
||||
ShortHelp: "[ALPHA] rename a share",
|
||||
Exec: runDriveRename,
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
ShortHelp: "[ALPHA] remove a share",
|
||||
Exec: runShareRemove,
|
||||
UsageFunc: usageFunc,
|
||||
Name: "unshare",
|
||||
ShortUsage: driveUnshareUsage,
|
||||
ShortHelp: "[ALPHA] remove a share",
|
||||
Exec: runDriveUnshare,
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
ShortHelp: "[ALPHA] list current shares",
|
||||
Exec: runShareList,
|
||||
UsageFunc: usageFunc,
|
||||
Name: "list",
|
||||
ShortUsage: driveListUsage,
|
||||
ShortHelp: "[ALPHA] list current shares",
|
||||
Exec: runDriveList,
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
return errors.New("share subcommand required; run 'tailscale share -h' for details")
|
||||
return errors.New("drive subcommand required; run 'tailscale drive -h' for details")
|
||||
},
|
||||
}
|
||||
|
||||
// runShareSet is the entry point for the "tailscale share set" command.
|
||||
func runShareSet(ctx context.Context, args []string) error {
|
||||
// runDriveShare is the entry point for the "tailscale drive share" command.
|
||||
func runDriveShare(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareSetUsage)
|
||||
return fmt.Errorf("usage: tailscale %v", driveShareUsage)
|
||||
}
|
||||
|
||||
name, path := args[0], args[1]
|
||||
|
||||
err := localClient.TailFSShareSet(ctx, &tailfs.Share{
|
||||
err := localClient.DriveShareSet(ctx, &drive.Share{
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
if err == nil {
|
||||
fmt.Printf("Set share %q at %q\n", name, path)
|
||||
fmt.Printf("Sharing %q as %q\n", path, name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// runShareRemove is the entry point for the "tailscale share remove" command.
|
||||
func runShareRemove(ctx context.Context, args []string) error {
|
||||
// runDriveUnshare is the entry point for the "tailscale drive unshare" command.
|
||||
func runDriveUnshare(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareRemoveUsage)
|
||||
return fmt.Errorf("usage: tailscale %v", driveUnshareUsage)
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
err := localClient.TailFSShareRemove(ctx, name)
|
||||
err := localClient.DriveShareRemove(ctx, name)
|
||||
if err == nil {
|
||||
fmt.Printf("Removed share %q\n", name)
|
||||
fmt.Printf("No longer sharing %q\n", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// runShareRename is the entry point for the "tailscale share rename" command.
|
||||
func runShareRename(ctx context.Context, args []string) error {
|
||||
// runDriveRename is the entry point for the "tailscale drive rename" command.
|
||||
func runDriveRename(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareRenameUsage)
|
||||
return fmt.Errorf("usage: tailscale %v", driveRenameUsage)
|
||||
}
|
||||
oldName := args[0]
|
||||
newName := args[1]
|
||||
|
||||
err := localClient.TailFSShareRename(ctx, oldName, newName)
|
||||
err := localClient.DriveShareRename(ctx, oldName, newName)
|
||||
if err == nil {
|
||||
fmt.Printf("Renamed share %q to %q\n", oldName, newName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// runShareList is the entry point for the "tailscale share list" command.
|
||||
func runShareList(ctx context.Context, args []string) error {
|
||||
// runDriveList is the entry point for the "tailscale drive list" command.
|
||||
func runDriveList(ctx context.Context, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareListUsage)
|
||||
return fmt.Errorf("usage: tailscale %v", driveListUsage)
|
||||
}
|
||||
|
||||
shares, err := localClient.TailFSShareList(ctx)
|
||||
shares, err := localClient.DriveShareList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,17 +150,17 @@ func runShareList(ctx context.Context, args []string) error {
|
||||
|
||||
func buildShareLongHelp() string {
|
||||
longHelpAs := ""
|
||||
if tailfs.AllowShareAs() {
|
||||
if drive.AllowShareAs() {
|
||||
longHelpAs = shareLongHelpAs
|
||||
}
|
||||
return fmt.Sprintf(shareLongHelpBase, longHelpAs)
|
||||
}
|
||||
|
||||
var shareLongHelpBase = `Tailscale share allows you to share directories with other machines on your tailnet.
|
||||
var shareLongHelpBase = `Taildrive allows you to share directories with other machines on your tailnet.
|
||||
|
||||
In order to share folders, your node needs to have the node attribute "tailfs:share".
|
||||
In order to share folders, your node needs to have the node attribute "drive:share".
|
||||
|
||||
In order to access shares, your node needs to have the node attribute "tailfs:access".
|
||||
In order to access shares, your node needs to have the node attribute "drive:access".
|
||||
|
||||
For example, to enable sharing and accessing shares for all member nodes:
|
||||
|
||||
@@ -163,14 +168,14 @@ For example, to enable sharing and accessing shares for all member nodes:
|
||||
{
|
||||
"target": ["autogroup:member"],
|
||||
"attr": [
|
||||
"tailfs:share",
|
||||
"tailfs:access",
|
||||
"drive:share",
|
||||
"drive:access",
|
||||
],
|
||||
}]
|
||||
|
||||
Each share is identified by a name and points to a directory at a specific path. For example, to share the path /Users/me/Documents under the name "docs", you would run:
|
||||
|
||||
$ tailscale share set docs /Users/me/Documents
|
||||
$ tailscale drive share docs /Users/me/Documents
|
||||
|
||||
Note that the system forces share names to lowercase to avoid problems with clients that don't support case-sensitive filenames.
|
||||
|
||||
@@ -191,7 +196,7 @@ Permissions to access shares are controlled via ACLs. For example, to give yours
|
||||
"src": ["mylogin@domain.com"],
|
||||
"dst": ["mylaptop's ip address"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"tailscale.com/cap/drive": [{
|
||||
"shares": ["docs"],
|
||||
"access": "rw"
|
||||
}]
|
||||
@@ -201,7 +206,7 @@ Permissions to access shares are controlled via ACLs. For example, to give yours
|
||||
"src": ["group:home"],
|
||||
"dst": ["mylaptop"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"tailscale.com/cap/drive": [{
|
||||
"shares": ["docs"],
|
||||
"access": "ro"
|
||||
}]
|
||||
@@ -215,7 +220,7 @@ To categorically give yourself access to all your shares, you can use the below
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"tailscale.com/cap/drive": [{
|
||||
"shares": ["*"],
|
||||
"access": "rw"
|
||||
}]
|
||||
@@ -226,18 +231,18 @@ Whenever either you or anyone in the group "home" connects to the share, they co
|
||||
|
||||
You can rename shares, for example you could rename the above share by running:
|
||||
|
||||
$ tailscale share rename docs newdocs
|
||||
$ tailscale drive rename docs newdocs
|
||||
|
||||
You can remove shares by name, for example you could remove the above share by running:
|
||||
|
||||
$ tailscale share remove newdocs
|
||||
$ tailscale drive unshare newdocs
|
||||
|
||||
You can get a list of currently published shares by running:
|
||||
|
||||
$ tailscale share list`
|
||||
$ tailscale drive list`
|
||||
|
||||
var shareLongHelpAs = `
|
||||
const shareLongHelpAs = `
|
||||
|
||||
If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:
|
||||
|
||||
$ sudo -u theuser tailscale share set docs /Users/theuser/Documents`
|
||||
$ sudo -u theuser tailscale drive share docs /Users/theuser/Documents`
|
||||
@@ -9,44 +9,83 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var exitNodeCmd = &ffcli.Command{
|
||||
Name: "exit-node",
|
||||
ShortUsage: "exit-node [flags]",
|
||||
ShortHelp: "Show machines on your tailnet configured as exit nodes",
|
||||
LongHelp: "Show machines on your tailnet configured as exit nodes",
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ShortUsage: "exit-node list [flags]",
|
||||
ShortHelp: "Show exit nodes",
|
||||
Exec: runExitNodeList,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("list")
|
||||
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
|
||||
return fs
|
||||
})(),
|
||||
func exitNodeCmd() *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "exit-node",
|
||||
ShortUsage: "tailscale exit-node [flags]",
|
||||
ShortHelp: "Show machines on your tailnet configured as exit nodes",
|
||||
LongHelp: "Show machines on your tailnet configured as exit nodes",
|
||||
Exec: func(context.Context, []string) error {
|
||||
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
|
||||
},
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
|
||||
},
|
||||
Subcommands: append([]*ffcli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ShortUsage: "tailscale exit-node list [flags]",
|
||||
ShortHelp: "Show exit nodes",
|
||||
Exec: runExitNodeList,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("list")
|
||||
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
|
||||
return fs
|
||||
})(),
|
||||
}},
|
||||
(func() []*ffcli.Command {
|
||||
if !envknob.UseWIPCode() {
|
||||
return nil
|
||||
}
|
||||
return []*ffcli.Command{
|
||||
{
|
||||
Name: "connect",
|
||||
ShortUsage: "tailscale exit-node connect",
|
||||
ShortHelp: "connect to most recently used exit node",
|
||||
Exec: exitNodeSetUse(true),
|
||||
},
|
||||
{
|
||||
Name: "disconnect",
|
||||
ShortUsage: "tailscale exit-node disconnect",
|
||||
ShortHelp: "disconnect from current exit node, if any",
|
||||
Exec: exitNodeSetUse(false),
|
||||
},
|
||||
}
|
||||
})()...),
|
||||
}
|
||||
}
|
||||
|
||||
var exitNodeArgs struct {
|
||||
filter string
|
||||
}
|
||||
|
||||
func exitNodeSetUse(wantOn bool) func(ctx context.Context, args []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected non-flag arguments")
|
||||
}
|
||||
err := localClient.SetUseExitNode(ctx, wantOn)
|
||||
if err != nil {
|
||||
if !wantOn {
|
||||
pref, err := localClient.GetPrefs(ctx)
|
||||
if err == nil && pref.ExitNodeID == "" {
|
||||
// Two processes concurrently turned it off.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
|
||||
// If the exit node has location and priority data, only the highest
|
||||
// priority node for each city location is shown to the user.
|
||||
@@ -70,7 +109,6 @@ func runExitNodeList(ctx context.Context, args []string) error {
|
||||
// We only show exit nodes under the exit-node subcommand.
|
||||
continue
|
||||
}
|
||||
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
|
||||
@@ -84,13 +122,12 @@ func runExitNodeList(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("no exit nodes found for %q", exitNodeArgs.filter)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
|
||||
w := tabwriter.NewWriter(Stdout, 10, 5, 5, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", "IP", "HOSTNAME", "COUNTRY", "CITY", "STATUS")
|
||||
for _, country := range filteredPeers.Countries {
|
||||
for _, city := range country.Cities {
|
||||
for _, peer := range city.Peers {
|
||||
|
||||
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer))
|
||||
}
|
||||
}
|
||||
@@ -137,46 +174,51 @@ type filteredCity struct {
|
||||
|
||||
const noLocationData = "-"
|
||||
|
||||
var noLocation = &tailcfg.Location{
|
||||
Country: noLocationData,
|
||||
CountryCode: noLocationData,
|
||||
City: noLocationData,
|
||||
CityCode: noLocationData,
|
||||
}
|
||||
|
||||
// filterFormatAndSortExitNodes filters and sorts exit nodes into
|
||||
// alphabetical order, by country, city and then by priority if
|
||||
// present.
|
||||
// If an exit node has location data, and the country has more than
|
||||
// once city, an `Any` city is added to the country that contains the
|
||||
// one city, an `Any` city is added to the country that contains the
|
||||
// highest priority exit node within that country.
|
||||
// For exit nodes without location data, their country fields are
|
||||
// defined as '-' to indicate that the data is not available.
|
||||
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
|
||||
// first get peers into some fixed order, as code below doesn't break ties
|
||||
// and our input comes from a random range-over-map.
|
||||
slices.SortFunc(peers, func(a, b *ipnstate.PeerStatus) int {
|
||||
return strings.Compare(a.DNSName, b.DNSName)
|
||||
})
|
||||
|
||||
countries := make(map[string]*filteredCountry)
|
||||
cities := make(map[string]*filteredCity)
|
||||
for _, ps := range peers {
|
||||
if ps.Location == nil {
|
||||
ps.Location = &tailcfg.Location{
|
||||
Country: noLocationData,
|
||||
CountryCode: noLocationData,
|
||||
City: noLocationData,
|
||||
CityCode: noLocationData,
|
||||
}
|
||||
}
|
||||
loc := cmp.Or(ps.Location, noLocation)
|
||||
|
||||
if filterBy != "" && ps.Location.Country != filterBy {
|
||||
if filterBy != "" && loc.Country != filterBy {
|
||||
continue
|
||||
}
|
||||
|
||||
co, coOK := countries[ps.Location.CountryCode]
|
||||
if !coOK {
|
||||
co, ok := countries[loc.CountryCode]
|
||||
if !ok {
|
||||
co = &filteredCountry{
|
||||
Name: ps.Location.Country,
|
||||
Name: loc.Country,
|
||||
}
|
||||
countries[ps.Location.CountryCode] = co
|
||||
|
||||
countries[loc.CountryCode] = co
|
||||
}
|
||||
|
||||
ci, ciOK := cities[ps.Location.CityCode]
|
||||
if !ciOK {
|
||||
ci, ok := cities[loc.CityCode]
|
||||
if !ok {
|
||||
ci = &filteredCity{
|
||||
Name: ps.Location.City,
|
||||
Name: loc.City,
|
||||
}
|
||||
cities[ps.Location.CityCode] = ci
|
||||
cities[loc.CityCode] = ci
|
||||
co.Cities = append(co.Cities, ci)
|
||||
}
|
||||
ci.Peers = append(ci.Peers, ps)
|
||||
@@ -193,10 +235,10 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
continue
|
||||
}
|
||||
|
||||
var countryANYPeer []*ipnstate.PeerStatus
|
||||
var countryAnyPeer []*ipnstate.PeerStatus
|
||||
for _, city := range country.Cities {
|
||||
sortPeersByPriority(city.Peers)
|
||||
countryANYPeer = append(countryANYPeer, city.Peers...)
|
||||
countryAnyPeer = append(countryAnyPeer, city.Peers...)
|
||||
var reducedCityPeers []*ipnstate.PeerStatus
|
||||
for i, peer := range city.Peers {
|
||||
if i == 0 || peer.ExitNode {
|
||||
@@ -208,7 +250,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
city.Peers = reducedCityPeers
|
||||
}
|
||||
sortByCityName(country.Cities)
|
||||
sortPeersByPriority(countryANYPeer)
|
||||
sortPeersByPriority(countryAnyPeer)
|
||||
|
||||
if len(country.Cities) > 1 {
|
||||
// For countries with more than one city, we want to return the
|
||||
@@ -216,7 +258,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
country.Cities = append([]*filteredCity{
|
||||
{
|
||||
Name: "Any",
|
||||
Peers: []*ipnstate.PeerStatus{countryANYPeer[0]},
|
||||
Peers: []*ipnstate.PeerStatus{countryAnyPeer[0]},
|
||||
},
|
||||
}, country.Cities...)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
|
||||
var fileCmd = &ffcli.Command{
|
||||
Name: "file",
|
||||
ShortUsage: "file <cp|get> ...",
|
||||
ShortUsage: "tailscale file <cp|get> ...",
|
||||
ShortHelp: "Send or receive files",
|
||||
Subcommands: []*ffcli.Command{
|
||||
fileCpCmd,
|
||||
@@ -65,7 +65,7 @@ func (c *countingReader) Read(buf []byte) (int, error) {
|
||||
|
||||
var fileCpCmd = &ffcli.Command{
|
||||
Name: "cp",
|
||||
ShortUsage: "file cp <files...> <target>:",
|
||||
ShortUsage: "tailscale file cp <files...> <target>:",
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
@@ -412,7 +412,7 @@ func (v *onConflict) Set(s string) error {
|
||||
|
||||
var fileGetCmd = &ffcli.Command{
|
||||
Name: "get",
|
||||
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
|
||||
ShortUsage: "tailscale file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
@@ -420,7 +420,7 @@ var fileGetCmd = &ffcli.Command{
|
||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
|
||||
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory.
|
||||
fs.Var(&getArgs.conflict, "conflict", "`behavior`"+` when a conflicting (same-named) file already exists in the target directory.
|
||||
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
|
||||
overwrite: overwrite existing file
|
||||
rename: write to a new number-suffixed filename`)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -37,9 +36,9 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
Name: "funnel",
|
||||
ShortHelp: "Turn on/off Funnel service",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"funnel <serve-port> {on|off}",
|
||||
"funnel status [--json]",
|
||||
}, "\n "),
|
||||
"tailscale funnel <serve-port> {on|off}",
|
||||
"tailscale funnel status [--json]",
|
||||
}, "\n"),
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
@@ -47,17 +46,16 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
"Turning off Funnel only turns off serving to the internet.",
|
||||
"It does not affect serving to your tailnet.",
|
||||
}, "\n"),
|
||||
Exec: e.runFunnel,
|
||||
UsageFunc: usageFunc,
|
||||
Exec: e.runFunnel,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortUsage: "tailscale funnel status [--json]",
|
||||
ShortHelp: "Show current serve/funnel status",
|
||||
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -169,10 +167,10 @@ func printFunnelWarning(sc *ipn.ServeConfig) {
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||
warn = true
|
||||
fmt.Fprintf(os.Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
|
||||
fmt.Fprintf(Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
|
||||
}
|
||||
}
|
||||
if warn {
|
||||
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||
fmt.Fprintf(Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
var idTokenCmd = &ffcli.Command{
|
||||
Name: "id-token",
|
||||
ShortUsage: "id-token <aud>",
|
||||
ShortHelp: "fetch an OIDC id-token for the Tailscale machine",
|
||||
ShortUsage: "tailscale id-token <aud>",
|
||||
ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
|
||||
Exec: runIDToken,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
var ipCmd = &ffcli.Command{
|
||||
Name: "ip",
|
||||
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
|
||||
ShortUsage: "tailscale ip [-1] [-4] [-6] [peer hostname or ip address]",
|
||||
ShortHelp: "Show Tailscale IP addresses",
|
||||
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
|
||||
Exec: runIP,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
var licensesCmd = &ffcli.Command{
|
||||
Name: "licenses",
|
||||
ShortUsage: "licenses",
|
||||
ShortUsage: "tailscale licenses",
|
||||
ShortHelp: "Get open source license information",
|
||||
LongHelp: "Get open source license information",
|
||||
Exec: runLicenses,
|
||||
|
||||
@@ -14,11 +14,10 @@ var loginArgs upArgsT
|
||||
|
||||
var loginCmd = &ffcli.Command{
|
||||
Name: "login",
|
||||
ShortUsage: "login [flags]",
|
||||
ShortUsage: "tailscale login [flags]",
|
||||
ShortHelp: "Log in to a Tailscale account",
|
||||
LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
|
||||
This command is currently in alpha and may change in the future.`,
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
return newUpFlagSet(effectiveGOOS(), &loginArgs, "login")
|
||||
}(),
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
var logoutCmd = &ffcli.Command{
|
||||
Name: "logout",
|
||||
ShortUsage: "logout [flags]",
|
||||
ShortUsage: "tailscale logout",
|
||||
ShortHelp: "Disconnect from Tailscale and expire current node key",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
var ncCmd = &ffcli.Command{
|
||||
Name: "nc",
|
||||
ShortUsage: "nc <hostname-or-IP> <port>",
|
||||
ShortUsage: "tailscale nc <hostname-or-IP> <port>",
|
||||
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
|
||||
Exec: runNC,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
var netcheckCmd = &ffcli.Command{
|
||||
Name: "netcheck",
|
||||
ShortUsage: "netcheck",
|
||||
ShortUsage: "tailscale netcheck",
|
||||
ShortHelp: "Print an analysis of local network conditions",
|
||||
Exec: runNetcheck,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
@@ -28,7 +26,7 @@ import (
|
||||
|
||||
var netlockCmd = &ffcli.Command{
|
||||
Name: "lock",
|
||||
ShortUsage: "lock <sub-command> <arguments>",
|
||||
ShortUsage: "tailscale lock <sub-command> <arguments>",
|
||||
ShortHelp: "Manage tailnet lock",
|
||||
LongHelp: "Manage tailnet lock",
|
||||
Subcommands: []*ffcli.Command{
|
||||
@@ -63,7 +61,7 @@ var nlInitArgs struct {
|
||||
|
||||
var nlInitCmd = &ffcli.Command{
|
||||
Name: "init",
|
||||
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
|
||||
ShortUsage: "tailscale lock init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
|
||||
ShortHelp: "Initialize tailnet lock",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -185,7 +183,7 @@ var nlStatusArgs struct {
|
||||
|
||||
var nlStatusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status",
|
||||
ShortUsage: "tailscale lock status",
|
||||
ShortHelp: "Outputs the state of tailnet lock",
|
||||
LongHelp: "Outputs the state of tailnet lock",
|
||||
Exec: runNetworkLockStatus,
|
||||
@@ -282,7 +280,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
|
||||
var nlAddCmd = &ffcli.Command{
|
||||
Name: "add",
|
||||
ShortUsage: "add <public-key>...",
|
||||
ShortUsage: "tailscale lock add <public-key>...",
|
||||
ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
|
||||
LongHelp: "Adds one or more trusted signing keys to tailnet lock",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
@@ -296,7 +294,7 @@ var nlRemoveArgs struct {
|
||||
|
||||
var nlRemoveCmd = &ffcli.Command{
|
||||
Name: "remove",
|
||||
ShortUsage: "remove [--re-sign=false] <public-key>...",
|
||||
ShortUsage: "tailscale lock remove [--re-sign=false] <public-key>...",
|
||||
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
Exec: runNetworkLockRemove,
|
||||
@@ -437,7 +435,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
|
||||
var nlSignCmd = &ffcli.Command{
|
||||
Name: "sign",
|
||||
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
|
||||
ShortUsage: "tailscale lock sign <node-key> [<rotation-key>] or sign <auth-key>",
|
||||
ShortHelp: "Signs a node or pre-approved auth key",
|
||||
LongHelp: `Either:
|
||||
- signs a node key and transmits the signature to the coordination server, or
|
||||
@@ -471,17 +469,17 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
// Provide a better help message for when someone clicks through the signing flow
|
||||
// on the wrong device.
|
||||
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
|
||||
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
|
||||
fmt.Fprintln(Stderr)
|
||||
fmt.Fprintln(Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
|
||||
fmt.Fprintln(Stderr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var nlDisableCmd = &ffcli.Command{
|
||||
Name: "disable",
|
||||
ShortUsage: "disable <disablement-secret>",
|
||||
ShortUsage: "tailscale lock disable <disablement-secret>",
|
||||
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -510,7 +508,7 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
|
||||
|
||||
var nlLocalDisableCmd = &ffcli.Command{
|
||||
Name: "local-disable",
|
||||
ShortUsage: "local-disable",
|
||||
ShortUsage: "tailscale lock local-disable",
|
||||
ShortHelp: "Disables tailnet lock for this node only",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -532,7 +530,7 @@ func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
|
||||
|
||||
var nlDisablementKDFCmd = &ffcli.Command{
|
||||
Name: "disablement-kdf",
|
||||
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
|
||||
ShortUsage: "tailscale lock disablement-kdf <hex-encoded-disablement-secret>",
|
||||
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
|
||||
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
|
||||
Exec: runNetworkLockDisablementKDF,
|
||||
@@ -557,7 +555,7 @@ var nlLogArgs struct {
|
||||
|
||||
var nlLogCmd = &ffcli.Command{
|
||||
Name: "log",
|
||||
ShortUsage: "log [--limit N]",
|
||||
ShortUsage: "tailscale lock log [--limit N]",
|
||||
ShortHelp: "List changes applied to tailnet lock",
|
||||
LongHelp: "List changes applied to tailnet lock",
|
||||
Exec: runNetworkLockLog,
|
||||
@@ -643,20 +641,19 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if nlLogArgs.json {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(updates)
|
||||
}
|
||||
|
||||
useColor := isatty.IsTerminal(os.Stdout.Fd())
|
||||
out, useColor := colorableOutput()
|
||||
|
||||
stdOut := colorable.NewColorableStdout()
|
||||
for _, update := range updates {
|
||||
stanza, err := nlDescribeUpdate(update, useColor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(stdOut, stanza)
|
||||
fmt.Fprintln(out, stanza)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -722,7 +719,7 @@ var nlRevokeKeysArgs struct {
|
||||
|
||||
var nlRevokeKeysCmd = &ffcli.Command{
|
||||
Name: "revoke-keys",
|
||||
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
|
||||
ShortUsage: "tailscale lock revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
|
||||
ShortHelp: "Revoke compromised tailnet-lock keys",
|
||||
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
var pingCmd = &ffcli.Command{
|
||||
Name: "ping",
|
||||
ShortUsage: "ping <hostname-or-IP>",
|
||||
ShortUsage: "tailscale ping <hostname-or-IP>",
|
||||
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ func newServeLegacyCommand(e *serveEnv) *ffcli.Command {
|
||||
Name: "serve",
|
||||
ShortHelp: "Serve content and local servers",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"serve http:<port> <mount-point> <source> [off]",
|
||||
"serve https:<port> <mount-point> <source> [off]",
|
||||
"serve tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve status [--json]",
|
||||
"serve reset",
|
||||
}, "\n "),
|
||||
"tailscale serve http:<port> <mount-point> <source> [off]",
|
||||
"tailscale serve https:<port> <mount-point> <source> [off]",
|
||||
"tailscale serve tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"tailscale serve status [--json]",
|
||||
"tailscale serve reset",
|
||||
}, "\n"),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** BETA; all of this is subject to change ***
|
||||
|
||||
@@ -91,24 +91,21 @@ EXAMPLES
|
||||
local plaintext server on port 80:
|
||||
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||
`),
|
||||
Exec: e.runServe,
|
||||
UsageFunc: usageFunc,
|
||||
Exec: e.runServe,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
ShortHelp: "Show current serve/funnel status",
|
||||
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "reset",
|
||||
Exec: e.runServeReset,
|
||||
ShortHelp: "reset current serve/funnel config",
|
||||
ShortHelp: "Reset current serve/funnel config",
|
||||
FlagSet: e.newFlags("serve-reset", nil),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -197,7 +194,7 @@ func (e *serveEnv) getLocalClientStatusWithoutPeers(ctx context.Context) (*ipnst
|
||||
}
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", description)
|
||||
fmt.Fprintf(Stderr, "%s\n", description)
|
||||
os.Exit(1)
|
||||
}
|
||||
if st.Self == nil {
|
||||
@@ -251,7 +248,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
|
||||
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
fmt.Fprintf(Stderr, "error: invalid number of arguments\n\n")
|
||||
return errHelp
|
||||
}
|
||||
|
||||
@@ -290,8 +287,8 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
}
|
||||
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
|
||||
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
fmt.Fprintf(Stderr, "error: invalid serve type %q\n", srcType)
|
||||
fmt.Fprint(Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
return errHelp
|
||||
}
|
||||
}
|
||||
@@ -327,13 +324,13 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
|
||||
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
||||
}
|
||||
if !filepath.IsAbs(source) {
|
||||
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
||||
fmt.Fprintf(Stderr, "error: path must be absolute\n\n")
|
||||
return errHelp
|
||||
}
|
||||
source = filepath.Clean(source)
|
||||
fi, err := os.Stat(source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
||||
fmt.Fprintf(Stderr, "error: invalid path: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
if fi.IsDir() && !strings.HasSuffix(mount, "/") {
|
||||
@@ -357,7 +354,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
|
||||
return err
|
||||
}
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
||||
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
|
||||
return errHelp
|
||||
}
|
||||
|
||||
@@ -512,18 +509,18 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
|
||||
case "tls-terminated-tcp":
|
||||
terminateTLS = true
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
|
||||
fmt.Fprintf(Stderr, "error: invalid TCP source %q\n\n", dest)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
dstURL, err := url.Parse(dest)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
fmt.Fprintf(Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return errHelp
|
||||
}
|
||||
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
fmt.Fprintf(Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
@@ -531,13 +528,13 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
|
||||
case "localhost", "127.0.0.1":
|
||||
// ok
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
|
||||
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
|
||||
fmt.Fprintf(Stderr, "error: invalid TCP source %q\n", dest)
|
||||
fmt.Fprint(Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
|
||||
fmt.Fprintf(Stderr, "error: invalid port %q\n\n", dstPortStr)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
@@ -804,10 +801,10 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
|
||||
return nil // already enabled
|
||||
}
|
||||
if info.Text != "" {
|
||||
fmt.Fprintln(os.Stdout, "\n"+info.Text)
|
||||
fmt.Fprintln(Stdout, "\n"+info.Text)
|
||||
}
|
||||
if info.URL != "" {
|
||||
fmt.Fprintln(os.Stdout, "\n "+info.URL+"\n")
|
||||
fmt.Fprintln(Stdout, "\n "+info.URL+"\n")
|
||||
}
|
||||
if !info.ShouldWait {
|
||||
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_not_awaiting_enablement", feature), 1)
|
||||
@@ -852,7 +849,7 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
|
||||
}
|
||||
if gotAll {
|
||||
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_enabled", feature), 1)
|
||||
fmt.Fprintln(os.Stdout, "Success.")
|
||||
fmt.Fprintln(Stdout, "Success.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -54,6 +55,9 @@ func TestCleanMountPoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServeConfigMutations(t *testing.T) {
|
||||
tstest.Replace(t, &Stderr, io.Discard)
|
||||
tstest.Replace(t, &Stdout, io.Discard)
|
||||
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
command []string // serve args; nil means no command to run (only reset)
|
||||
@@ -706,6 +710,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
lc: lc,
|
||||
testFlagOut: &flagOut,
|
||||
testStdout: &stdout,
|
||||
testStderr: io.Discard,
|
||||
}
|
||||
lastCount := lc.setCount
|
||||
var cmd *ffcli.Command
|
||||
@@ -717,6 +722,10 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
cmd = newServeLegacyCommand(e)
|
||||
args = st.command
|
||||
}
|
||||
if cmd.FlagSet == nil {
|
||||
cmd.FlagSet = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
|
||||
cmd.FlagSet.SetOutput(Stdout)
|
||||
}
|
||||
err := cmd.ParseAndRun(context.Background(), args)
|
||||
if flagOut.Len() > 0 {
|
||||
t.Logf("flag package output: %q", flagOut.Bytes())
|
||||
@@ -750,6 +759,9 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
tstest.Replace(t, &Stderr, io.Discard)
|
||||
tstest.Replace(t, &Stdout, io.Discard)
|
||||
|
||||
lc := &fakeLocalServeClient{}
|
||||
var stdout bytes.Buffer
|
||||
var flagOut bytes.Buffer
|
||||
@@ -757,6 +769,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
lc: lc,
|
||||
testFlagOut: &flagOut,
|
||||
testStdout: &stdout,
|
||||
testStderr: io.Discard,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -110,10 +110,10 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
Name: info.Name,
|
||||
ShortHelp: info.ShortHelp,
|
||||
ShortUsage: strings.Join([]string{
|
||||
fmt.Sprintf("%s <target>", info.Name),
|
||||
fmt.Sprintf("%s status [--json]", info.Name),
|
||||
fmt.Sprintf("%s reset", info.Name),
|
||||
}, "\n "),
|
||||
fmt.Sprintf("tailscale %s <target>", info.Name),
|
||||
fmt.Sprintf("tailscale %s status [--json]", info.Name),
|
||||
fmt.Sprintf("tailscale %s reset", info.Name),
|
||||
}, "\n"),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
@@ -131,20 +131,20 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "view current proxy configuration",
|
||||
Name: "status",
|
||||
ShortUsage: "tailscale " + info.Name + " status [--json]",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "View current " + info.Name + " configuration",
|
||||
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "reset",
|
||||
ShortHelp: "reset current serve/funnel config",
|
||||
Exec: e.runServeReset,
|
||||
FlagSet: e.newFlags("serve-reset", nil),
|
||||
UsageFunc: usageFunc,
|
||||
Name: "reset",
|
||||
ShortUsage: "tailscale " + info.Name + " reset",
|
||||
ShortHelp: "Reset current " + info.Name + " config",
|
||||
Exec: e.runServeReset,
|
||||
FlagSet: e.newFlags("serve-reset", nil),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -497,9 +497,9 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
}
|
||||
h.Text = text
|
||||
case filepath.IsAbs(target):
|
||||
if version.IsSandboxedMacOS() {
|
||||
// don't allow path serving for now on macOS (2022-11-15)
|
||||
return errors.New("path serving is not supported if sandboxed on macOS")
|
||||
if version.IsMacAppStore() || version.IsMacSys() {
|
||||
// The Tailscale network extension cannot serve arbitrary paths on macOS due to sandbox restrictions (2024-03-26)
|
||||
return errors.New("Path serving is not supported on macOS due to sandbox restrictions. To use Tailscale Serve on macOS, switch to the open-source tailscaled distribution. See https://tailscale.com/kb/1065/macos-variants for more information.")
|
||||
}
|
||||
|
||||
target = filepath.Clean(target)
|
||||
@@ -823,12 +823,12 @@ func (e *serveEnv) stdout() io.Writer {
|
||||
if e.testStdout != nil {
|
||||
return e.testStdout
|
||||
}
|
||||
return os.Stdout
|
||||
return Stdout
|
||||
}
|
||||
|
||||
func (e *serveEnv) stderr() io.Writer {
|
||||
if e.testStderr != nil {
|
||||
return e.testStderr
|
||||
}
|
||||
return os.Stderr
|
||||
return Stderr
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
var setCmd = &ffcli.Command{
|
||||
Name: "set",
|
||||
ShortUsage: "set [flags]",
|
||||
ShortUsage: "tailscale set [flags]",
|
||||
ShortHelp: "Change specified preferences",
|
||||
LongHelp: `"tailscale set" allows changing specific preferences.
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
var sshCmd = &ffcli.Command{
|
||||
Name: "ssh",
|
||||
ShortUsage: "ssh [user@]<host> [args...]",
|
||||
ShortUsage: "tailscale ssh [user@]<host> [args...]",
|
||||
ShortHelp: "SSH to a Tailscale machine",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -106,10 +106,8 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
"-o", "CanonicalizeHostname no", // https://github.com/tailscale/tailscale/issues/10348
|
||||
)
|
||||
|
||||
// TODO(bradfitz): nc is currently broken on macOS:
|
||||
// https://github.com/tailscale/tailscale/issues/4529
|
||||
// So don't use it for now. MagicDNS is usually working on macOS anyway
|
||||
// and they're not in userspace mode, so 'nc' isn't very useful.
|
||||
// MagicDNS is usually working on macOS anyway and they're not in userspace
|
||||
// mode, so 'nc' isn't very useful.
|
||||
if runtime.GOOS != "darwin" {
|
||||
socketArg := ""
|
||||
if rootArgs.socket != "" && rootArgs.socket != paths.DefaultTailscaledSocket() {
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortUsage: "tailscale status [--active] [--web] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
|
||||
@@ -17,26 +17,22 @@ import (
|
||||
)
|
||||
|
||||
var switchCmd = &ffcli.Command{
|
||||
Name: "switch",
|
||||
ShortHelp: "Switches to a different Tailscale account",
|
||||
Name: "switch",
|
||||
ShortUsage: "tailscale switch <id>",
|
||||
ShortHelp: "Switches to a different Tailscale account",
|
||||
LongHelp: `"tailscale switch" switches between logged in accounts. You can
|
||||
use the ID that's returned from 'tailnet switch -list'
|
||||
to pick which profile you want to switch to. Alternatively, you
|
||||
can use the Tailnet or the account names to switch as well.
|
||||
|
||||
This command is currently in alpha and may change in the future.`,
|
||||
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("switch", flag.ExitOnError)
|
||||
fs.BoolVar(&switchArgs.list, "list", false, "list available accounts")
|
||||
return fs
|
||||
}(),
|
||||
Exec: switchProfile,
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return `USAGE
|
||||
switch <id>
|
||||
switch --list
|
||||
|
||||
"tailscale switch" switches between logged in accounts. You can
|
||||
use the ID that's returned from 'tailnet switch -list'
|
||||
to pick which profile you want to switch to. Alternatively, you
|
||||
can use the Tailnet or the account names to switch as well.
|
||||
|
||||
This command is currently in alpha and may change in the future.`
|
||||
},
|
||||
}
|
||||
|
||||
var switchArgs struct {
|
||||
@@ -48,7 +44,7 @@ func listProfiles(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tw := tabwriter.NewWriter(os.Stdout, 2, 2, 2, ' ', 0)
|
||||
tw := tabwriter.NewWriter(Stdout, 2, 2, 2, ' ', 0)
|
||||
defer tw.Flush()
|
||||
printRow := func(vals ...string) {
|
||||
fmt.Fprintln(tw, strings.Join(vals, "\t"))
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
Name: "up",
|
||||
ShortUsage: "up [flags]",
|
||||
ShortUsage: "tailscale up [flags]",
|
||||
ShortHelp: "Connect to Tailscale, logging in if needed",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortUsage: "tailscale update",
|
||||
ShortHelp: "Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/clientupdate"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
|
||||
var versionCmd = &ffcli.Command{
|
||||
Name: "version",
|
||||
ShortUsage: "version [flags]",
|
||||
ShortUsage: "tailscale version [flags]",
|
||||
ShortHelp: "Print Tailscale version",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("version")
|
||||
@@ -70,7 +69,7 @@ func runVersion(ctx context.Context, args []string) error {
|
||||
Meta: m,
|
||||
Upstream: upstreamVer,
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e := json.NewEncoder(Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
return e.Encode(out)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
Name: "web",
|
||||
ShortUsage: "web [flags]",
|
||||
ShortUsage: "tailscale web [flags]",
|
||||
ShortHelp: "Run a web server for controlling Tailscale",
|
||||
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -18,13 +17,12 @@ import (
|
||||
|
||||
var whoisCmd = &ffcli.Command{
|
||||
Name: "whois",
|
||||
ShortUsage: "whois [--json] ip[:port]",
|
||||
ShortUsage: "tailscale whois [--json] ip[:port]",
|
||||
ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6).
|
||||
`),
|
||||
UsageFunc: usageFunc,
|
||||
Exec: runWhoIs,
|
||||
Exec: runWhoIs,
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := newFlagSet("whois")
|
||||
fs.BoolVar(&whoIsArgs.json, "json", false, "output in JSON format")
|
||||
@@ -53,7 +51,7 @@ func runWhoIs(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
|
||||
w := tabwriter.NewWriter(Stdout, 10, 5, 5, ' ', 0)
|
||||
fmt.Fprintf(w, "Machine:\n")
|
||||
fmt.Fprintf(w, " Name:\t%s\n", strings.TrimSuffix(who.Node.Name, "."))
|
||||
fmt.Fprintf(w, " ID:\t%s\n", who.Node.StableID)
|
||||
|
||||
@@ -27,7 +27,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
💣 github.com/mattn/go-isatty from tailscale.com/cmd/tailscale/cli+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
@@ -84,6 +84,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/drive from tailscale.com/client/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
@@ -118,7 +119,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
|
||||
@@ -88,7 +88,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
💣 github.com/djherbis/times from tailscale.com/tailfs/tailfsimpl
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
@@ -102,7 +102,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/clientupdate
|
||||
github.com/google/uuid from tailscale.com/clientupdate+
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
@@ -111,7 +111,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/tailfsimpl/compositedav
|
||||
github.com/jellydator/ttlcache/v3 from tailscale.com/drive/driveimpl/compositedav
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
@@ -172,7 +172,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/xnet/webdav from tailscale.com/tailfs/tailfsimpl+
|
||||
github.com/tailscale/xnet/webdav from tailscale.com/drive/driveimpl+
|
||||
github.com/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
@@ -251,6 +251,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/drive from tailscale.com/client/tailscale+
|
||||
tailscale.com/drive/driveimpl from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/drive/driveimpl/compositedav from tailscale.com/drive/driveimpl
|
||||
tailscale.com/drive/driveimpl/dirfs from tailscale.com/drive/driveimpl+
|
||||
tailscale.com/drive/driveimpl/shared from tailscale.com/drive/driveimpl+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
@@ -320,11 +325,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/tailfs/tailfsimpl/compositedav from tailscale.com/tailfs/tailfsimpl
|
||||
tailscale.com/tailfs/tailfsimpl/dirfs from tailscale.com/tailfs/tailfsimpl+
|
||||
tailscale.com/tailfs/tailfsimpl/shared from tailscale.com/tailfs/tailfsimpl+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
@@ -376,6 +376,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osuser from tailscale.com/ipn/localapi+
|
||||
tailscale.com/util/progresstracking from tailscale.com/ipn/localapi
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
@@ -522,7 +523,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand/v2 from tailscale.com/util/rands
|
||||
mime from github.com/tailscale/xnet/webdav+
|
||||
mime/multipart from net/http
|
||||
mime/multipart from net/http+
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
@@ -52,7 +53,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
@@ -116,7 +116,7 @@ var args struct {
|
||||
// or comma-separated list thereof.
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
cleanUp bool
|
||||
confFile string
|
||||
debug string
|
||||
port uint16
|
||||
@@ -145,7 +145,7 @@ var subCommands = map[string]*func([]string) error{
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
"be-child": &beChildFunc,
|
||||
"serve-tailfs": &serveTailFSFunc,
|
||||
"serve-taildrive": &serveDriveFunc,
|
||||
}
|
||||
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
@@ -156,7 +156,7 @@ func main() {
|
||||
|
||||
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.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")`)
|
||||
@@ -207,7 +207,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
|
||||
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)")
|
||||
}
|
||||
@@ -387,12 +387,16 @@ func run() (err error) {
|
||||
}
|
||||
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)
|
||||
if envknob.Bool("TS_PLEASE_PANIC") {
|
||||
panic("TS_PLEASE_PANIC asked us to panic")
|
||||
}
|
||||
// Always clean up, even if we're going to run the server. This covers cases
|
||||
// such as when a system was rebooted without shutting down, or tailscaled
|
||||
// crashed, and would for example restore system DNS configuration.
|
||||
dns.CleanUp(logf, args.tunname)
|
||||
router.CleanUp(logf, args.tunname)
|
||||
// If the cleanUp flag was passed, then exit.
|
||||
if args.cleanUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -407,7 +411,7 @@ func run() (err error) {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(logf))
|
||||
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||
}
|
||||
@@ -645,12 +649,12 @@ 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(),
|
||||
TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
|
||||
}
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
@@ -709,7 +713,6 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if handleSubnetsInNetstack() {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
netstackSubnetRouter = true
|
||||
}
|
||||
sys.Set(conf.Router)
|
||||
@@ -753,7 +756,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
tfs, _ := sys.TailFSForLocal.GetOK()
|
||||
tfs, _ := sys.DriveForLocal.GetOK()
|
||||
ret, err := netstack.Create(logf,
|
||||
sys.Tun.Get(),
|
||||
sys.Engine.Get(),
|
||||
@@ -831,25 +834,25 @@ func beChild(args []string) error {
|
||||
return f(args[1:])
|
||||
}
|
||||
|
||||
var serveTailFSFunc = serveTailFS
|
||||
var serveDriveFunc = serveDrive
|
||||
|
||||
// serveTailFS serves one or more tailfs on localhost using the WebDAV
|
||||
// protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
|
||||
// tailscaled processes in serve-tailfs mode in order to access the fliesystem
|
||||
// serveDrive serves one or more Taildrives on localhost using the WebDAV
|
||||
// protocol. On UNIX and MacOS tailscaled environment, Taildrive spawns child
|
||||
// tailscaled processes in serve-taildrive mode in order to access the fliesystem
|
||||
// as specific (usually unprivileged) users.
|
||||
//
|
||||
// serveTailFS prints the address on which it's listening to stdout so that the
|
||||
// serveDrive prints the address on which it's listening to stdout so that the
|
||||
// parent process knows where to connect to.
|
||||
func serveTailFS(args []string) error {
|
||||
func serveDrive(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing shares")
|
||||
}
|
||||
if len(args)%2 != 0 {
|
||||
return errors.New("need <sharename> <path> pairs")
|
||||
}
|
||||
s, err := tailfsimpl.NewFileServer()
|
||||
s, err := driveimpl.NewFileServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start tailfs FileServer: %v", err)
|
||||
return fmt.Errorf("unable to start Taildrive file server: %v", err)
|
||||
}
|
||||
shares := make(map[string]string)
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
|
||||
@@ -6,7 +6,6 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
|
||||
@@ -42,13 +42,13 @@ import (
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
@@ -316,7 +316,7 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
sys.Set(netMon)
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(log.Printf))
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(log.Printf))
|
||||
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
|
||||
|
||||
@@ -90,8 +90,11 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
c := logtail.Config{
|
||||
Collection: lpc.Collection,
|
||||
PrivateID: lpc.PrivateID,
|
||||
// NewZstdEncoder is intentionally not passed in, compressed requests
|
||||
// set HTTP headers that are not supported by the no-cors fetching mode.
|
||||
|
||||
// Compressed requests set HTTP headers that are not supported by the
|
||||
// no-cors fetching mode:
|
||||
CompressLogs: false,
|
||||
|
||||
HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
|
||||
}
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
|
||||
@@ -82,9 +82,9 @@ type Direct struct {
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverLegacyKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key; only used for signRegisterRequest on Windows now
|
||||
serverNoiseKey key.MachinePublic
|
||||
|
||||
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
|
||||
noiseClient *NoiseClient
|
||||
@@ -436,12 +436,6 @@ type loginOpt struct {
|
||||
OldNodeKeySignature tkatype.MarshaledSignature
|
||||
}
|
||||
|
||||
// httpClient provides a common interface for the noiseClient and
|
||||
// the NaCl box http.Client.
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// hostInfoLocked returns a Clone of c.hostinfo and c.netinfo.
|
||||
// It must only be called with c.mu held.
|
||||
func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo {
|
||||
@@ -454,7 +448,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.mu.Lock()
|
||||
persist := c.persist.AsStruct()
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverKey
|
||||
serverKey := c.serverLegacyKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
|
||||
hi := c.hostInfoLocked()
|
||||
@@ -494,20 +488,22 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
||||
|
||||
c.mu.Lock()
|
||||
c.serverKey = keys.LegacyPublicKey
|
||||
c.serverLegacyKey = keys.LegacyPublicKey
|
||||
c.serverNoiseKey = keys.PublicKey
|
||||
c.mu.Unlock()
|
||||
serverKey = keys.LegacyPublicKey
|
||||
serverNoiseKey = keys.PublicKey
|
||||
|
||||
// For servers supporting the Noise transport,
|
||||
// proactively shut down our TLS TCP connection.
|
||||
// Proactively shut down our TLS TCP connection.
|
||||
// We're not going to need it and it's nicer to the
|
||||
// server.
|
||||
if !serverNoiseKey.IsZero() {
|
||||
c.httpc.CloseIdleConnections()
|
||||
}
|
||||
c.httpc.CloseIdleConnections()
|
||||
}
|
||||
|
||||
if serverNoiseKey.IsZero() {
|
||||
return false, "", nil, errors.New("control server is too old; no noise key")
|
||||
}
|
||||
|
||||
var oldNodeKey key.NodePublic
|
||||
switch {
|
||||
case opt.Logout:
|
||||
@@ -594,7 +590,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
request.Auth.Provider = persist.Provider
|
||||
request.Auth.LoginName = persist.UserProfile.LoginName
|
||||
request.Auth.AuthKey = authKey
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverKey, machinePrivKey.Public())
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public())
|
||||
if err != nil {
|
||||
// If signing failed, clear all related fields
|
||||
request.SignatureType = tailcfg.SignatureNone
|
||||
@@ -614,21 +610,16 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
}
|
||||
|
||||
// URL and httpc are protocol specific.
|
||||
var url string
|
||||
var httpc httpClient
|
||||
if serverNoiseKey.IsZero() {
|
||||
httpc = c.httpc
|
||||
url = fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
} else {
|
||||
request.Version = tailcfg.CurrentCapabilityVersion
|
||||
httpc, err = c.getNoiseClient()
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url = fmt.Sprintf("%s/machine/register", c.serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
request.Version = tailcfg.CurrentCapabilityVersion
|
||||
httpc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
url := fmt.Sprintf("%s/machine/register", c.serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
bodyData, err := encode(request)
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
@@ -650,7 +641,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, serverKey, serverNoiseKey, machinePrivKey); err != nil {
|
||||
if err := decode(res, &resp); err != nil {
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
@@ -844,7 +835,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverKey := c.serverKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
@@ -858,6 +848,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if serverNoiseKey.IsZero() {
|
||||
return errors.New("control server is too old; no noise key")
|
||||
}
|
||||
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
@@ -914,7 +908,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
request.Compress = "zstd"
|
||||
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
bodyData, err := encode(request)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
@@ -926,20 +920,36 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
machinePubKey := machinePrivKey.Public()
|
||||
t0 := c.clock.Now()
|
||||
|
||||
// Url and httpc are protocol specific.
|
||||
var url string
|
||||
var httpc httpClient
|
||||
if serverNoiseKey.IsZero() {
|
||||
httpc = c.httpc
|
||||
url = fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.UntypedHexString())
|
||||
} else {
|
||||
httpc, err = c.getNoiseClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url = fmt.Sprintf("%s/machine/map", serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
httpc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url := fmt.Sprintf("%s/machine/map", serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
// Create a watchdog timer that breaks the connection if we don't receive a
|
||||
// MapResponse from the network at least once every two minutes. The
|
||||
// watchdog timer is stopped every time we receive a MapResponse (so it
|
||||
// doesn't run when we're processing a MapResponse message, including any
|
||||
// long-running requested operations like Debug.Sleep) and is reset whenever
|
||||
// we go back to blocking on network reads.
|
||||
// The watchdog timer also covers the initial request (effectively the
|
||||
// pre-body and initial-body read timeouts) as we do not have any other
|
||||
// keep-alive mechanism for the initial request.
|
||||
watchdogTimer, watchdogTimedOut := c.clock.NewTimer(watchdogTimeout)
|
||||
defer watchdogTimer.Stop()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
case <-watchdogTimedOut:
|
||||
c.logf("map response long-poll timed out!")
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
@@ -962,6 +972,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
defer res.Body.Close()
|
||||
|
||||
health.NoteMapRequestHeard(request)
|
||||
watchdogTimer.Reset(watchdogTimeout)
|
||||
|
||||
if nu == nil {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
@@ -993,27 +1004,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.expiry = nm.Expiry
|
||||
}
|
||||
|
||||
// Create a watchdog timer that breaks the connection if we don't receive a
|
||||
// MapResponse from the network at least once every two minutes. The
|
||||
// watchdog timer is stopped every time we receive a MapResponse (so it
|
||||
// doesn't run when we're processing a MapResponse message, including any
|
||||
// long-running requested operations like Debug.Sleep) and is reset whenever
|
||||
// we go back to blocking on network reads.
|
||||
watchdogTimer, watchdogTimedOut := c.clock.NewTimer(watchdogTimeout)
|
||||
defer watchdogTimer.Stop()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
case <-watchdogTimedOut:
|
||||
c.logf("map response long-poll timed out!")
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// gotNonKeepAliveMessage is whether we've yet received a MapResponse message without
|
||||
// KeepAlive set.
|
||||
var gotNonKeepAliveMessage bool
|
||||
@@ -1043,7 +1033,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
|
||||
if err := c.decodeMsg(msg, &resp); err != nil {
|
||||
vlogf("netmap: decode error: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -1160,9 +1150,8 @@ func initDisplayNames(selfNode tailcfg.NodeView, resp *tailcfg.MapResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
// decode JSON decodes the res.Body into v. If serverNoiseKey is not specified,
|
||||
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
|
||||
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
|
||||
// decode JSON decodes the res.Body into v.
|
||||
func decode(res *http.Response, v any) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -1171,10 +1160,7 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("%d: %v", res.StatusCode, string(msg))
|
||||
}
|
||||
if !serverNoiseKey.IsZero() {
|
||||
return json.Unmarshal(msg, v)
|
||||
}
|
||||
return decodeMsg(msg, v, serverKey, mkey)
|
||||
return json.Unmarshal(msg, v)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1185,25 +1171,8 @@ var (
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
|
||||
// If c.serverNoiseKey is not specified, it uses the c.serverKey and mkey
|
||||
// to first the decrypt msg from the NaCl-crypto-box.
|
||||
func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
c.mu.Unlock()
|
||||
|
||||
var decrypted []byte
|
||||
if serverNoiseKey.IsZero() {
|
||||
var ok bool
|
||||
decrypted, ok = mkey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
} else {
|
||||
decrypted = msg
|
||||
}
|
||||
b, err := zstdframe.AppendDecode(nil, decrypted)
|
||||
func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
|
||||
b, err := zstdframe.AppendDecode(nil, compressedMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1220,26 +1189,11 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v any, serverKey key.MachinePublic, machinePrivKey key.MachinePrivate) error {
|
||||
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
}
|
||||
if err := json.Unmarshal(decrypted, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encode JSON encodes v. If serverNoiseKey is not specified, it uses the serverKey and mkey to
|
||||
// seal the message into a NaCl-crypto-box.
|
||||
func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) ([]byte, error) {
|
||||
// encode JSON encodes v as JSON, logging tailcfg.MapRequest values if
|
||||
// debugMap is set.
|
||||
func encode(v any) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1249,10 +1203,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
if !serverNoiseKey.IsZero() {
|
||||
return b, nil
|
||||
}
|
||||
return mkey.SealTo(serverKey, b), nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string) (*tailcfg.OverTLSPublicKeyResponse, error) {
|
||||
@@ -1349,7 +1300,7 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
|
||||
func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
httpc := c.httpc
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n" && c.noiseConfigured()
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n"
|
||||
if useNoise {
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
@@ -1550,14 +1501,6 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// noiseConfigured reports whether the client can communicate with Control
|
||||
// over Noise.
|
||||
func (c *Direct) noiseConfigured() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return !c.serverNoiseKey.IsZero()
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
|
||||
@@ -1567,53 +1510,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
||||
metricSetDNSError.Add(1)
|
||||
}
|
||||
}()
|
||||
if c.noiseConfigured() {
|
||||
return c.setDNSNoise(ctx, req)
|
||||
}
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
if serverKey.IsZero() {
|
||||
return errors.New("zero serverKey")
|
||||
}
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
}
|
||||
if machinePrivKey.IsZero() {
|
||||
return errors.New("getMachinePrivKey returned zero key")
|
||||
}
|
||||
|
||||
// TODO(maisem): dedupe this codepath from SetDNSNoise.
|
||||
var serverNoiseKey key.MachinePublic
|
||||
bodyData, err := encode(req, serverKey, serverNoiseKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := c.httpc.Do(hreq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
if err := decode(res, &setDNSRes, serverKey, serverNoiseKey, machinePrivKey); err != nil {
|
||||
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return fmt.Errorf("set-dns-response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.setDNSNoise(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -86,7 +86,8 @@ type Client struct {
|
||||
addrFamSelAtomic syncs.AtomicValue[AddressFamilySelector]
|
||||
|
||||
mu sync.Mutex
|
||||
started bool // true upon first connect, never transitions to false
|
||||
atomicState syncs.AtomicValue[ConnectedState] // hold mu to write
|
||||
started bool // true upon first connect, never transitions to false
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
closed bool
|
||||
@@ -99,6 +100,14 @@ type Client struct {
|
||||
clock tstime.Clock
|
||||
}
|
||||
|
||||
// ConnectedState describes the state of a derphttp Client.
|
||||
type ConnectedState struct {
|
||||
Connected bool
|
||||
Connecting bool
|
||||
Closed bool
|
||||
LocalAddr netip.AddrPort // if Connected
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
return fmt.Sprintf("<derphttp_client.Client %s url=%s>", c.ServerPublicKey().ShortString(), c.url)
|
||||
}
|
||||
@@ -307,6 +316,12 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
if c.client != nil {
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
c.atomicState.Store(ConnectedState{Connecting: true})
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.atomicState.Store(ConnectedState{Connecting: false})
|
||||
}
|
||||
}()
|
||||
|
||||
// timeout is the fallback maximum time (if ctx doesn't limit
|
||||
// it further) to do all of: DNS + TCP + TLS + HTTP Upgrade +
|
||||
@@ -524,6 +539,12 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
c.netConn = tcpConn
|
||||
c.tlsState = tlsState
|
||||
c.connGen++
|
||||
|
||||
localAddr, _ := c.client.LocalAddr()
|
||||
c.atomicState.Store(ConnectedState{
|
||||
Connected: true,
|
||||
LocalAddr: localAddr,
|
||||
})
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
@@ -795,7 +816,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, pu.Hostname(), authHeader); err != nil {
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, target, authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
@@ -906,16 +927,15 @@ func (c *Client) SendPing(data [8]byte) error {
|
||||
// LocalAddr reports c's local TCP address, without any implicit
|
||||
// connect or reconnect.
|
||||
func (c *Client) LocalAddr() (netip.AddrPort, error) {
|
||||
c.mu.Lock()
|
||||
closed, client := c.closed, c.client
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
st := c.atomicState.Load()
|
||||
if st.Closed {
|
||||
return netip.AddrPort{}, ErrClientClosed
|
||||
}
|
||||
if client == nil {
|
||||
la := st.LocalAddr
|
||||
if !st.Connected && !la.IsValid() {
|
||||
return netip.AddrPort{}, errors.New("client not connected")
|
||||
}
|
||||
return client.LocalAddr()
|
||||
return la, nil
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
|
||||
@@ -1049,6 +1069,7 @@ func (c *Client) Close() error {
|
||||
if c.netConn != nil {
|
||||
c.netConn.Close()
|
||||
}
|
||||
c.atomicState.Store(ConnectedState{Closed: true})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -447,3 +448,16 @@ func TestRunWatchConnectionLoopServeConnect(t *testing.T) {
|
||||
}
|
||||
watcher.RunWatchConnectionLoop(ctx, key.NodePublic{}, t.Logf, noopAdd, noopRemove)
|
||||
}
|
||||
|
||||
// verify that the LocalAddr method doesn't acquire the mutex.
|
||||
// See https://github.com/tailscale/tailscale/issues/11519
|
||||
func TestLocalAddrNoMutex(t *testing.T) {
|
||||
var c Client
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock() // not needed in test but for symmetry
|
||||
|
||||
_, err := c.LocalAddr()
|
||||
if got, want := fmt.Sprint(err), "client not connected"; got != want {
|
||||
t.Errorf("got error %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<string id="SINCE_V1_56">Tailscale version 1.56.0 and later</string>
|
||||
<string id="PARTIAL_FULL_SINCE_V1_56">Tailscale version 1.56.0 and later (full support), some earlier versions (partial support)</string>
|
||||
<string id="SINCE_V1_58">Tailscale version 1.58.0 and later</string>
|
||||
<string id="SINCE_V1_62">Tailscale version 1.62.0 and later</string>
|
||||
<string id="Tailscale_Category">Tailscale</string>
|
||||
<string id="UI_Category">UI customization</string>
|
||||
<string id="Settings_Category">Settings</string>
|
||||
@@ -195,6 +196,14 @@ If you enable this policy, then data collection is always enabled.
|
||||
If you disable this policy, then data collection is always disabled.
|
||||
|
||||
If you do not configure this policy, then data collection depends on if it has been enabled from the CLI (as of Tailscale 1.56), it may be present in the GUI in later versions.]]></string>
|
||||
<string id="ManagedBy">Show the "Managed By {Organization}" menu item</string>
|
||||
<string id="ManagedBy_Help"><![CDATA[Use this policy to configure the “Managed By {Organization}” item in the Tailscale Menu.
|
||||
|
||||
If you enable this policy, the menu item will be displayed indicating the organization name. For instance, “Managed By XYZ Corp, Inc.”. Optionally, you can provide a custom message to be displayed when a user clicks on the “Managed By” menu item, and a URL pointing to a help desk webpage or other helpful resources for users in the organization.
|
||||
|
||||
If you disable this policy or do not configure it, the corresponding menu item will be hidden.
|
||||
|
||||
See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more details.]]></string>
|
||||
</stringTable>
|
||||
<presentationTable>
|
||||
<presentation id="LoginURL">
|
||||
@@ -217,6 +226,17 @@ If you do not configure this policy, then data collection depends on if it has b
|
||||
<label>Exit Node</label>
|
||||
</textBox>
|
||||
</presentation>
|
||||
<presentation id="ManagedBy">
|
||||
<textBox refId="ManagedByOrganization">
|
||||
<label>Organization Name:</label>
|
||||
</textBox>
|
||||
<textBox refId="ManagedByCustomMessage">
|
||||
<label>Custom Message:</label>
|
||||
</textBox>
|
||||
<textBox refId="ManagedBySupportURL">
|
||||
<label>Support URL:</label>
|
||||
</textBox>
|
||||
</presentation>
|
||||
</presentationTable>
|
||||
</resources>
|
||||
</policyDefinitionResources>
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
displayName="$(string.SINCE_V1_58)">
|
||||
<and><reference ref="TAILSCALE_PRODUCT"/></and>
|
||||
</definition>
|
||||
<definition name="SINCE_V1_62"
|
||||
displayName="$(string.SINCE_V1_62)">
|
||||
<and><reference ref="TAILSCALE_PRODUCT"/></and>
|
||||
</definition>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
<categories>
|
||||
@@ -252,5 +256,14 @@
|
||||
<string>hide</string>
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ManagedBy" class="Machine" displayName="$(string.ManagedBy)" explainText="$(string.ManagedBy_Help)" presentation="$(presentation.ManagedBy)" key="Software\Policies\Tailscale">
|
||||
<parentCategory ref="UI_Category" />
|
||||
<supportedOn ref="SINCE_V1_62" />
|
||||
<elements>
|
||||
<text id="ManagedByOrganization" valueName="ManagedByOrganizationName" required="true" />
|
||||
<text id="ManagedByCustomMessage" valueName="ManagedByCaption" />
|
||||
<text id="ManagedBySupportURL" valueName="ManagedByURL" />
|
||||
</elements>
|
||||
</policy>
|
||||
</policies>
|
||||
</policyDefinitions>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
// Clone makes a deep copy of Share.
|
||||
// The result aliases no memory with the original.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
//go:build windows || darwin
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// Mkdir implements webdav.FileSystem. All attempts to Mkdir a directory that
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// OpenFile implements interface webdav.Filesystem.
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// Stat implements webdav.FileSystem.
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -38,10 +38,10 @@ const (
|
||||
func init() {
|
||||
// set AllowShareAs() to false so that we don't try to use sub-processes
|
||||
// for access files on disk.
|
||||
tailfs.DisallowShareAs = true
|
||||
drive.DisallowShareAs = true
|
||||
}
|
||||
|
||||
// The tests in this file simulate real-life TailFS scenarios, but without
|
||||
// The tests in this file simulate real-life Taildrive scenarios, but without
|
||||
// going over the Tailscale network stack.
|
||||
func TestDirectoryListing(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
@@ -51,9 +51,9 @@ func TestDirectoryListing(t *testing.T) {
|
||||
s.checkDirList("domain should contain its only remote", shared.Join(domain), remote1)
|
||||
s.checkDirList("remote with no shares should be empty", shared.Join(domain, remote1))
|
||||
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.checkDirList("remote with one share should contain that share", shared.Join(domain, remote1), share11)
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.addShare(remote1, share12, drive.PermissionReadOnly)
|
||||
s.checkDirList("remote with two shares should contain both in lexicographical order", shared.Join(domain, remote1), share12, share11)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkDirList("remote share should contain file", shared.Join(domain, remote1, share11), file111)
|
||||
@@ -76,12 +76,12 @@ func TestFileManipulation(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkFileStatus(remote1, share11, file111)
|
||||
s.checkFileContents(remote1, share11, file111)
|
||||
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.addShare(remote1, share12, drive.PermissionReadOnly)
|
||||
s.writeFile("writing file to read-only remote should fail", remote1, share12, file111, "hello world", false)
|
||||
|
||||
s.writeFile("writing file to non-existent remote should fail", "non-existent", share11, file111, "hello world", false)
|
||||
@@ -98,7 +98,7 @@ type remote struct {
|
||||
fs *FileSystemForRemote
|
||||
fileServer *FileServer
|
||||
shares map[string]string
|
||||
permissions map[string]tailfs.Permission
|
||||
permissions map[string]drive.Permission
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -175,15 +175,15 @@ func (s *system) addRemote(name string) {
|
||||
fileServer: fileServer,
|
||||
fs: NewFileSystemForRemote(log.Printf),
|
||||
shares: make(map[string]string),
|
||||
permissions: make(map[string]tailfs.Permission),
|
||||
permissions: make(map[string]drive.Permission),
|
||||
}
|
||||
r.fs.SetFileServerAddr(fileServer.Addr())
|
||||
go http.Serve(l, r)
|
||||
s.remotes[name] = r
|
||||
|
||||
remotes := make([]*tailfs.Remote, 0, len(s.remotes))
|
||||
remotes := make([]*drive.Remote, 0, len(s.remotes))
|
||||
for name, r := range s.remotes {
|
||||
remotes = append(remotes, &tailfs.Remote{
|
||||
remotes = append(remotes, &drive.Remote{
|
||||
Name: name,
|
||||
URL: fmt.Sprintf("http://%s", r.l.Addr()),
|
||||
})
|
||||
@@ -197,7 +197,7 @@ func (s *system) addRemote(name string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *system) addShare(remoteName, shareName string, permission tailfs.Permission) {
|
||||
func (s *system) addShare(remoteName, shareName string, permission drive.Permission) {
|
||||
r, ok := s.remotes[remoteName]
|
||||
if !ok {
|
||||
s.t.Fatalf("unknown remote %q", remoteName)
|
||||
@@ -207,14 +207,14 @@ func (s *system) addShare(remoteName, shareName string, permission tailfs.Permis
|
||||
r.shares[shareName] = f
|
||||
r.permissions[shareName] = permission
|
||||
|
||||
shares := make([]*tailfs.Share, 0, len(r.shares))
|
||||
shares := make([]*drive.Share, 0, len(r.shares))
|
||||
for shareName, folder := range r.shares {
|
||||
shares = append(shares, &tailfs.Share{
|
||||
shares = append(shares, &drive.Share{
|
||||
Name: shareName,
|
||||
Path: folder,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(shares, tailfs.CompareShares)
|
||||
slices.SortFunc(shares, drive.CompareShares)
|
||||
r.fs.SetShares(shares)
|
||||
r.fileServer.SetShares(r.shares)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// FileServer is a standalone WebDAV server that dynamically serves up shares.
|
||||
// It's typically used in a separate process from the actual TailFS server to
|
||||
// It's typically used in a separate process from the actual Taildrive server to
|
||||
// serve up files as an unprivileged user.
|
||||
type FileServer struct {
|
||||
l net.Listener
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfsimpl provides an implementation of package tailfs.
|
||||
package tailfsimpl
|
||||
// Package driveimpl provides an implementation of package drive.
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositedav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/compositedav"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
// FileSystemForLocal is the Taildrive filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote Taildrive shares on other nodes.
|
||||
type FileSystemForLocal struct {
|
||||
logf logger.Logf
|
||||
h *compositedav.Handler
|
||||
@@ -69,7 +69,7 @@ func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) erro
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*tailfs.Remote, transport http.RoundTripper) {
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*drive.Remote, transport http.RoundTripper) {
|
||||
children := make([]*compositedav.Child, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
children = append(children, &compositedav.Child{
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/compositedav"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositedav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote implements tailfs.FileSystemForRemote.
|
||||
// FileSystemForRemote implements drive.FileSystemForRemote.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
@@ -53,23 +53,23 @@ type FileSystemForRemote struct {
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
fileServerAddr string
|
||||
shares []*tailfs.Share
|
||||
shares []*drive.Share
|
||||
children map[string]*compositedav.Child
|
||||
userServers map[string]*userServer
|
||||
}
|
||||
|
||||
// SetFileServerAddr implements tailfs.FileSystemForRemote.
|
||||
// SetFileServerAddr implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
||||
s.mu.Lock()
|
||||
s.fileServerAddr = addr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetShares implements tailfs.FileSystemForRemote. Shares must be sorted
|
||||
// according to tailfs.CompareShares.
|
||||
func (s *FileSystemForRemote) SetShares(shares []*tailfs.Share) {
|
||||
// SetShares implements drive.FileSystemForRemote. Shares must be sorted
|
||||
// according to drive.CompareShares.
|
||||
func (s *FileSystemForRemote) SetShares(shares []*drive.Share) {
|
||||
userServers := make(map[string]*userServer)
|
||||
if tailfs.AllowShareAs() {
|
||||
if drive.AllowShareAs() {
|
||||
// Set up per-user server by running the current executable as an
|
||||
// unprivileged user in order to avoid privilege escalation.
|
||||
executable, err := os.Executable()
|
||||
@@ -112,7 +112,7 @@ func (s *FileSystemForRemote) SetShares(shares []*tailfs.Share) {
|
||||
s.closeChildren(oldChildren)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Child {
|
||||
func (s *FileSystemForRemote) buildChild(share *drive.Share) *compositedav.Child {
|
||||
return &compositedav.Child{
|
||||
Child: &dirfs.Child{
|
||||
Name: share.Name,
|
||||
@@ -133,8 +133,8 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
shareName := string(shareNameBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
var share *tailfs.Share
|
||||
i, shareFound := slices.BinarySearchFunc(s.shares, shareName, func(s *tailfs.Share, name string) int {
|
||||
var share *drive.Share
|
||||
i, shareFound := slices.BinarySearchFunc(s.shares, shareName, func(s *drive.Share, name string) int {
|
||||
return strings.Compare(s.Name, name)
|
||||
})
|
||||
if shareFound {
|
||||
@@ -149,7 +149,7 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
}
|
||||
|
||||
var addr string
|
||||
if !tailfs.AllowShareAs() {
|
||||
if !drive.AllowShareAs() {
|
||||
addr = fileServerAddr
|
||||
} else {
|
||||
userServer, found := userServers[share.As]
|
||||
@@ -176,18 +176,18 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
// ServeHTTPWithPerms implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions drive.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
isWrite := writeMethods[r.Method]
|
||||
if isWrite {
|
||||
share := shared.CleanAndSplit(r.URL.Path)[0]
|
||||
switch permissions.For(share) {
|
||||
case tailfs.PermissionNone:
|
||||
case drive.PermissionNone:
|
||||
// If we have no permissions to this share, treat it as not found
|
||||
// to avoid leaking any information about the share's existence.
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
case tailfs.PermissionReadOnly:
|
||||
case drive.PermissionReadOnly:
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions,
|
||||
children := make([]*compositedav.Child, 0, len(childrenMap))
|
||||
// filter out shares to which the connecting principal has no access
|
||||
for name, child := range childrenMap {
|
||||
if permissions.For(name) == tailfs.PermissionNone {
|
||||
if permissions.For(name) == drive.PermissionNone {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions,
|
||||
func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
|
||||
for _, server := range userServers {
|
||||
if err := server.Close(); err != nil {
|
||||
s.logf("error closing tailfs user server: %v", err)
|
||||
s.logf("error closing taildrive user server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (s *FileSystemForRemote) closeChildren(children map[string]*compositedav.Ch
|
||||
}
|
||||
}
|
||||
|
||||
// Close() implements tailfs.FileSystemForRemote.
|
||||
// Close() implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) Close() error {
|
||||
s.mu.Lock()
|
||||
userServers := s.userServers
|
||||
@@ -242,12 +242,12 @@ func (s *FileSystemForRemote) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// userServer runs tailscaled serve-tailfs to serve webdav content for the
|
||||
// userServer runs tailscaled serve-taildrive to serve webdav content for the
|
||||
// given Shares. All Shares are assumed to have the same Share.As, and the
|
||||
// content is served as that Share.As user.
|
||||
type userServer struct {
|
||||
logf logger.Logf
|
||||
shares []*tailfs.Share
|
||||
shares []*drive.Share
|
||||
username string
|
||||
executable string
|
||||
|
||||
@@ -306,13 +306,13 @@ func (s *userServer) runLoop() {
|
||||
// userServers anyway.
|
||||
func (s *userServer) run() error {
|
||||
// set up the command
|
||||
args := []string{"serve-tailfs"}
|
||||
args := []string{"serve-taildrive"}
|
||||
for _, s := range s.shares {
|
||||
args = append(args, s.Name, s.Path)
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
if s.canSudo() {
|
||||
s.logf("starting TailFS file server as user %q", s.username)
|
||||
s.logf("starting taildrive file server as user %q", s.username)
|
||||
allArgs := []string{"-n", "-u", s.username, s.executable}
|
||||
allArgs = append(allArgs, args...)
|
||||
cmd = exec.Command("sudo", allArgs...)
|
||||
@@ -324,7 +324,7 @@ func (s *userServer) run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logf("starting TailFS file server as ourselves")
|
||||
s.logf("starting taildrive file server as ourselves")
|
||||
cmd = exec.Command(s.executable, args...)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
@@ -356,13 +356,13 @@ func (s *userServer) run() error {
|
||||
// send the rest of stdout and stderr to logger to avoid blocking
|
||||
go func() {
|
||||
for stdoutScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
|
||||
s.logf("tailscaled serve-taildrive stdout: %v", stdoutScanner.Text())
|
||||
}
|
||||
}()
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
go func() {
|
||||
for stderrScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
|
||||
s.logf("tailscaled serve-taildrive stderr: %v", stderrScanner.Text())
|
||||
}
|
||||
}()
|
||||
s.mu.Lock()
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package shared contains types and functions shared by different tailfs
|
||||
// Package shared contains types and functions shared by different drive
|
||||
// packages.
|
||||
package shared
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfs provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV. The actual implementation of the core TailFS
|
||||
// functionality lives in package tailfsimpl. These packages are separated to
|
||||
// allow users of tailfs to refer to the interfaces without having a hard
|
||||
// dependency on tailfs, so that programs which don't actually use tailfs can
|
||||
// Package drive provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV. The actual implementation of the core Taildrive
|
||||
// functionality lives in package driveimpl. These packages are separated to
|
||||
// allow users of Taildrive to refer to the interfaces without having a hard
|
||||
// dependency on Taildrive, so that programs which don't actually use Taildrive can
|
||||
// avoid its transitive dependencies.
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Remote represents a remote TailFS node.
|
||||
// Remote represents a remote Taildrive node.
|
||||
type Remote struct {
|
||||
Name string
|
||||
URL string
|
||||
Available func() bool
|
||||
}
|
||||
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
// FileSystemForLocal is the Taildrive filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote Taildrive shares on other nodes.
|
||||
type FileSystemForLocal interface {
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
HandleConn(conn net.Conn, remoteAddr net.Addr) error
|
||||
@@ -1,20 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=Share --clonefunc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// DisallowShareAs forcibly disables sharing as a specific user, only used
|
||||
// for testing.
|
||||
DisallowShareAs = false
|
||||
DisallowShareAs = false
|
||||
ErrDriveNotEnabled = errors.New("Taildrive not enabled")
|
||||
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
||||
)
|
||||
|
||||
var (
|
||||
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
||||
)
|
||||
|
||||
// AllowShareAs reports whether sharing files as a specific user is allowed.
|
||||
@@ -22,7 +30,7 @@ func AllowShareAs() bool {
|
||||
return !DisallowShareAs && doAllowShareAs()
|
||||
}
|
||||
|
||||
// Share configures a folder to be shared through TailFS.
|
||||
// Share configures a folder to be shared through drive.
|
||||
type Share struct {
|
||||
// Name is how this share appears on remote nodes.
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -78,7 +86,7 @@ func CompareShares(a, b *Share) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
|
||||
// FileSystemForRemote is the TailFS filesystem exposed to remote nodes. It
|
||||
// FileSystemForRemote is the drive filesystem exposed to remote nodes. It
|
||||
// provides a unified WebDAV interface to local directories that have been
|
||||
// shared.
|
||||
type FileSystemForRemote interface {
|
||||
@@ -103,3 +111,20 @@ type FileSystemForRemote interface {
|
||||
// Close() stops serving the WebDAV content
|
||||
Close() error
|
||||
}
|
||||
|
||||
// NormalizeShareName normalizes the given share name and returns an error if
|
||||
// it contains any disallowed characters.
|
||||
func NormalizeShareName(name string) (string, error) {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// Trim whitespace
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
if !shareNameRegex.MatchString(name) {
|
||||
return "", ErrInvalidShareName
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !unix
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
func doAllowShareAs() bool {
|
||||
// On non-UNIX platforms, we use the GUI application (e.g. Windows taskbar
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
package drive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -29,7 +29,7 @@ func TestNormalizeShareName(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("name %q", tt.name), func(t *testing.T) {
|
||||
got, err := normalizeShareName(tt.name)
|
||||
got, err := NormalizeShareName(tt.name)
|
||||
if tt.err != nil && err != tt.err {
|
||||
t.Errorf("wanted error %v, got %v", tt.err, err)
|
||||
} else if got != tt.want {
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build unix
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import "tailscale.com/version"
|
||||
|
||||
@@ -120,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-pFnWnB9jiI+gm9g+bzuYH9cm1n3t9jLOyoXYgfWAlc4=
|
||||
# nix-direnv cache busting line: sha256-pYeHqYd2cCOVQlD1r2lh//KC+732H0lj1fPDBr+W8qA=
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-pFnWnB9jiI+gm9g+bzuYH9cm1n3t9jLOyoXYgfWAlc4=
|
||||
sha256-pYeHqYd2cCOVQlD1r2lh//KC+732H0lj1fPDBr+W8qA=
|
||||
|
||||
@@ -1 +1 @@
|
||||
f86d7c8ef64a0f8a2516fc23652eee28abc8d8e0
|
||||
48d71857bf5352daaa10b61dd3e9b1c0dd51e27a
|
||||
|
||||
@@ -52,7 +52,7 @@ func New() *tailcfg.Hostinfo {
|
||||
GoArchVar: lazyGoArchVar.Get(),
|
||||
GoVersion: runtime.Version(),
|
||||
Machine: condCall(unameMachine),
|
||||
DeviceModel: deviceModel(),
|
||||
DeviceModel: deviceModelCached(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
NoLogsNoSupport: envknob.NoLogsNoSupport(),
|
||||
AllowsUpdate: envknob.AllowsRemoteUpdate(),
|
||||
@@ -68,6 +68,7 @@ var (
|
||||
distroVersion func() string
|
||||
distroCodeName func() string
|
||||
unameMachine func() string
|
||||
deviceModel func() string
|
||||
)
|
||||
|
||||
func condCall[T any](fn func() T) T {
|
||||
@@ -176,6 +177,20 @@ var (
|
||||
// SetDeviceModel sets the device model for use in Hostinfo updates.
|
||||
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
|
||||
|
||||
func deviceModelCached() string {
|
||||
if v, _ := deviceModelAtomic.Load().(string); v != "" {
|
||||
return v
|
||||
}
|
||||
if deviceModel == nil {
|
||||
return ""
|
||||
}
|
||||
v := deviceModel()
|
||||
if v != "" {
|
||||
deviceModelAtomic.Store(v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// SetOSVersion sets the OS version.
|
||||
func SetOSVersion(v string) { osVersionAtomic.Store(v) }
|
||||
|
||||
@@ -193,11 +208,6 @@ func SetPackage(v string) { packagingType.Store(v) }
|
||||
// and "k8s-operator".
|
||||
func SetApp(v string) { appType.Store(v) }
|
||||
|
||||
func deviceModel() string {
|
||||
s, _ := deviceModelAtomic.Load().(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// FirewallMode returns the firewall mode for the app.
|
||||
// It is empty if unset.
|
||||
func FirewallMode() string {
|
||||
|
||||
@@ -22,9 +22,7 @@ func init() {
|
||||
distroName = distroNameLinux
|
||||
distroVersion = distroVersionLinux
|
||||
distroCodeName = distroCodeNameLinux
|
||||
if v := linuxDeviceModel(); v != "" {
|
||||
SetDeviceModel(v)
|
||||
}
|
||||
deviceModel = deviceModelLinux
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -50,7 +48,7 @@ func distroCodeNameLinux() string {
|
||||
return lazyVersionMeta.Get().DistroCodeName
|
||||
}
|
||||
|
||||
func linuxDeviceModel() string {
|
||||
func deviceModelLinux() string {
|
||||
for _, path := range []string{
|
||||
// First try the Synology-specific location.
|
||||
// Example: "DS916+-j"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -67,8 +67,9 @@ const (
|
||||
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
|
||||
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
|
||||
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyInitialTailFSShares // if set, the first Notify message (sent immediately) will contain the current TailFS Shares
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyInitialDriveShares // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
|
||||
NotifyInitialOutgoingFiles // if set, the first Notify message (sent immediately) will contain the current Taildrop OutgoingFiles
|
||||
)
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -114,6 +115,11 @@ type Notify struct {
|
||||
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
|
||||
IncomingFiles []PartialFile `json:",omitempty"`
|
||||
|
||||
// OutgoingFiles, if non-nil, tracks which files are in the process of
|
||||
// being sent via TailDrop, including files that finished, whether
|
||||
// successful or failed. This slice is sorted by Started time, then Name.
|
||||
OutgoingFiles []*OutgoingFile `json:",omitempty"`
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
// This is currently only used by Tailscale when run in the
|
||||
@@ -124,13 +130,13 @@ type Notify struct {
|
||||
// is available.
|
||||
ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
|
||||
|
||||
// TailFSShares tracks the full set of current TailFSShares that we're
|
||||
// DriveShares tracks the full set of current DriveShares that we're
|
||||
// publishing. Some client applications, like the MacOS and Windows clients,
|
||||
// will listen for updates to this and handle serving these shares under
|
||||
// the identity of the unprivileged user that is running the application. A
|
||||
// nil value here means that we're not broadcasting shares information, an
|
||||
// empty value means that there are no shares.
|
||||
TailFSShares views.SliceView[*tailfs.Share, tailfs.ShareView]
|
||||
DriveShares views.SliceView[*drive.Share, drive.ShareView]
|
||||
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
@@ -175,7 +181,7 @@ func (n Notify) String() string {
|
||||
return s[0:len(s)-1] + "}"
|
||||
}
|
||||
|
||||
// PartialFile represents an in-progress file transfer.
|
||||
// PartialFile represents an in-progress incoming file transfer.
|
||||
type PartialFile struct {
|
||||
Name string // e.g. "foo.jpg"
|
||||
Started time.Time // time transfer started
|
||||
@@ -194,6 +200,18 @@ type PartialFile struct {
|
||||
Done bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// OutgoingFile represents an in-progress outgoing file transfer.
|
||||
type OutgoingFile struct {
|
||||
ID string `json:",omitempty"` // unique identifier for this transfer (a type 4 UUID)
|
||||
PeerID tailcfg.StableNodeID `json:",omitempty"` // identifier for the peer to which this is being transferred
|
||||
Name string `json:",omitempty"` // e.g. "foo.jpg"
|
||||
Started time.Time // time transfer started
|
||||
DeclaredSize int64 // or -1 if unknown
|
||||
Sent int64 // bytes copied thus far
|
||||
Finished bool // indicates whether or not the transfer finished
|
||||
Succeeded bool // for a finished transfer, indicates whether or not it was successful
|
||||
}
|
||||
|
||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||
// (preferences, private keys, etc.). It is also used as a key for
|
||||
// the various LoginProfiles that the instance may be signed into.
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
@@ -25,10 +25,10 @@ func (src *Prefs) Clone() *Prefs {
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if src.TailFSShares != nil {
|
||||
dst.TailFSShares = make([]*tailfs.Share, len(src.TailFSShares))
|
||||
for i := range dst.TailFSShares {
|
||||
dst.TailFSShares[i] = src.TailFSShares[i].Clone()
|
||||
if src.DriveShares != nil {
|
||||
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
||||
for i := range dst.DriveShares {
|
||||
dst.DriveShares[i] = src.DriveShares[i].Clone()
|
||||
}
|
||||
}
|
||||
dst.Persist = src.Persist.Clone()
|
||||
@@ -42,6 +42,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netip.Addr
|
||||
InternalExitNodePrior string
|
||||
ExitNodeAllowLANAccess bool
|
||||
CorpDNS bool
|
||||
RunSSH bool
|
||||
@@ -63,7 +64,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
TailFSShares []*tailfs.Share
|
||||
DriveShares []*drive.Share
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
@@ -69,6 +69,7 @@ func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
|
||||
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
|
||||
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
|
||||
func (v PrefsView) InternalExitNodePrior() string { return v.ж.InternalExitNodePrior }
|
||||
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
|
||||
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
|
||||
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
|
||||
@@ -92,8 +93,8 @@ func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpda
|
||||
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
|
||||
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
|
||||
func (v PrefsView) TailFSShares() views.SliceView[*tailfs.Share, tailfs.ShareView] {
|
||||
return views.SliceOfViews[*tailfs.Share, tailfs.ShareView](v.ж.TailFSShares)
|
||||
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
|
||||
}
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
@@ -104,6 +105,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netip.Addr
|
||||
InternalExitNodePrior string
|
||||
ExitNodeAllowLANAccess bool
|
||||
CorpDNS bool
|
||||
RunSSH bool
|
||||
@@ -125,7 +127,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
TailFSShares []*tailfs.Share
|
||||
DriveShares []*drive.Share
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
360
ipn/ipnlocal/drive.go
Normal file
360
ipn/ipnlocal/drive.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
const (
|
||||
// DriveLocalPort is the port on which the Taildrive listens for location
|
||||
// connections on quad 100.
|
||||
DriveLocalPort = 8080
|
||||
)
|
||||
|
||||
// DriveSharingEnabled reports whether sharing to remote nodes via Taildrive is
|
||||
// enabled. This is currently based on checking for the drive:share node
|
||||
// attribute.
|
||||
func (b *LocalBackend) DriveSharingEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.driveSharingEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSharingEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveShare)
|
||||
}
|
||||
|
||||
// DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes
|
||||
// is enabled. This is currently based on checking for the drive:access node
|
||||
// attribute.
|
||||
func (b *LocalBackend) DriveAccessEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.driveAccessEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveAccessEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTaildriveAccess)
|
||||
}
|
||||
|
||||
// DriveSetServerAddr tells Taildrive to use the given address for connecting
|
||||
// to the drive.FileServer that's exposing local files as an unprivileged
|
||||
// user.
|
||||
func (b *LocalBackend) DriveSetServerAddr(addr string) error {
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
fs.SetFileServerAddr(addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DriveSetShare adds the given share if no share with that name exists, or
|
||||
// replaces the existing share if one with the same name already exists. To
|
||||
// avoid potential incompatibilities across file systems, share names are
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
func (b *LocalBackend) DriveSetShare(share *drive.Share) error {
|
||||
var err error
|
||||
share.Name, err = drive.NormalizeShareName(share.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveSetShareLocked(share)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSetShareLocked(share *drive.Share) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
addedShare := false
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() != share.Name {
|
||||
if !addedShare && existing.Name() > share.Name {
|
||||
// Add share in order
|
||||
shares = append(shares, share)
|
||||
addedShare = true
|
||||
}
|
||||
shares = append(shares, existing.AsStruct())
|
||||
}
|
||||
}
|
||||
if !addedShare {
|
||||
shares = append(shares, share)
|
||||
}
|
||||
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// DriveRenameShare renames the share at old name to new name. To avoid
|
||||
// potential incompatibilities across file systems, the new share name is
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
// Any of the following will result in an error.
|
||||
// - no share found under old name
|
||||
// - new share name contains disallowed characters
|
||||
// - share already exists under new name
|
||||
func (b *LocalBackend) DriveRenameShare(oldName, newName string) error {
|
||||
var err error
|
||||
newName, err = drive.NormalizeShareName(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveRenameShareLocked(oldName, newName)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRenameShareLocked(oldName, newName string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() == newName {
|
||||
return existingShares, os.ErrExist
|
||||
}
|
||||
if existing.Name() == oldName {
|
||||
share := existing.AsStruct()
|
||||
share.Name = newName
|
||||
shares = append(shares, share)
|
||||
found = true
|
||||
} else {
|
||||
shares = append(shares, existing.AsStruct())
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
slices.SortFunc(shares, drive.CompareShares)
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// DriveRemoveShare removes the named share. Share names are forced to
|
||||
// lowercase.
|
||||
func (b *LocalBackend) DriveRemoveShare(name string) error {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
var err error
|
||||
name, err = drive.NormalizeShareName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.driveRemoveShareLocked(name)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRemoveShareLocked(name string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, drive.ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() != name {
|
||||
shares = append(shares, existing.AsStruct())
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveSetSharesLocked(shares []*drive.Share) error {
|
||||
prefs := b.pm.prefs.AsStruct()
|
||||
prefs.ApplyEdits(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
DriveShares: shares,
|
||||
},
|
||||
DriveSharesSet: true,
|
||||
})
|
||||
return b.pm.setPrefsLocked(prefs.View())
|
||||
}
|
||||
|
||||
// driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||||
// about the latest list of shares.
|
||||
func (b *LocalBackend) driveNotifyShares(shares views.SliceView[*drive.Share, drive.ShareView]) {
|
||||
// Ensures shares is not nil to distinguish "no shares" from "not notifying shares"
|
||||
if shares.IsNil() {
|
||||
shares = views.SliceOfViews(make([]*drive.Share, 0))
|
||||
}
|
||||
b.send(ipn.Notify{DriveShares: shares})
|
||||
}
|
||||
|
||||
// driveNotifyCurrentSharesLocked sends an ipn.Notify if the current set of
|
||||
// shares has changed since the last notification.
|
||||
func (b *LocalBackend) driveNotifyCurrentSharesLocked() {
|
||||
var shares views.SliceView[*drive.Share, drive.ShareView]
|
||||
if b.driveSharingEnabledLocked() {
|
||||
// Only populate shares if sharing is enabled.
|
||||
shares = b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
lastNotified := b.lastNotifiedDriveShares.Load()
|
||||
if lastNotified == nil || !driveShareViewsEqual(lastNotified, shares) {
|
||||
// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
|
||||
go b.driveNotifyShares(shares)
|
||||
}
|
||||
}
|
||||
|
||||
func driveShareViewsEqual(a *views.SliceView[*drive.Share, drive.ShareView], b views.SliceView[*drive.Share, drive.ShareView]) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Len() != b.Len() {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < a.Len(); i++ {
|
||||
if !drive.ShareViewsEqual(a.At(i), b.At(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DriveGetShares gets the current list of Taildrive shares, sorted by name.
|
||||
func (b *LocalBackend) DriveGetShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
// updateDrivePeersLocked sets all applicable peers from the netmap as Taildrive
|
||||
// remotes.
|
||||
func (b *LocalBackend) updateDrivePeersLocked(nm *netmap.NetworkMap) {
|
||||
fs, ok := b.sys.DriveForLocal.GetOK()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var driveRemotes []*drive.Remote
|
||||
if b.driveAccessEnabledLocked() {
|
||||
// Only populate peers if access is enabled, otherwise leave blank.
|
||||
driveRemotes = b.driveRemotesFromPeers(nm)
|
||||
}
|
||||
|
||||
fs.SetRemotes(b.netMap.Domain, driveRemotes, &driveTransport{b: b})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Remote {
|
||||
driveRemotes := make([]*drive.Remote, 0, len(nm.Peers))
|
||||
for _, p := range nm.Peers {
|
||||
// Exclude mullvad exit nodes from list of Taildrive peers
|
||||
// TODO(oxtoacart) - once we have a better mechanism for finding only accessible sharers
|
||||
// (see below) we can remove this logic.
|
||||
if strings.HasSuffix(p.Name(), ".mullvad.ts.net.") {
|
||||
continue
|
||||
}
|
||||
|
||||
peerID := p.ID()
|
||||
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), taildrivePrefix[1:])
|
||||
driveRemotes = append(driveRemotes, &drive.Remote{
|
||||
Name: p.DisplayName(false),
|
||||
URL: url,
|
||||
Available: func() bool {
|
||||
// TODO(oxtoacart): need to figure out a performant and reliable way to only
|
||||
// show the peers that have shares to which we have access
|
||||
// This will require work on the control server to transmit the inverse
|
||||
// of the "tailscale.com/cap/drive" capability.
|
||||
// For now, at least limit it only to nodes that are online.
|
||||
// Note, we have to iterate the latest netmap because the peer we got from the first iteration may not be it
|
||||
b.mu.Lock()
|
||||
latestNetMap := b.netMap
|
||||
b.mu.Unlock()
|
||||
|
||||
for _, candidate := range latestNetMap.Peers {
|
||||
if candidate.ID() == peerID {
|
||||
online := candidate.Online()
|
||||
// TODO(oxtoacart): for some reason, this correctly
|
||||
// catches when a node goes from offline to online,
|
||||
// but not the other way around...
|
||||
return online != nil && *online
|
||||
}
|
||||
}
|
||||
|
||||
// peer not found, must not be available
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
return driveRemotes
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"tailscale.com/doctor/ethtool"
|
||||
"tailscale.com/doctor/permissions"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/health/healthmsg"
|
||||
@@ -69,7 +70,6 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstime"
|
||||
@@ -316,9 +316,12 @@ type LocalBackend struct {
|
||||
// Last ClientVersion received in MapResponse, guarded by mu.
|
||||
lastClientVersion *tailcfg.ClientVersion
|
||||
|
||||
// lastNotifiedTailFSShares keeps track of the last set of shares that we
|
||||
// lastNotifiedDriveShares keeps track of the last set of shares that we
|
||||
// notified about.
|
||||
lastNotifiedTailFSShares atomic.Pointer[views.SliceView[*tailfs.Share, tailfs.ShareView]]
|
||||
lastNotifiedDriveShares atomic.Pointer[views.SliceView[*drive.Share, drive.ShareView]]
|
||||
|
||||
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
||||
outgoingFiles map[string]*ipn.OutgoingFile
|
||||
}
|
||||
|
||||
type updateStatus struct {
|
||||
@@ -402,8 +405,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
if err != nil {
|
||||
log.Printf("error setting up sockstat logger: %v", err)
|
||||
}
|
||||
// Enable sockstats logs only on unstable builds
|
||||
if version.IsUnstableBuild() && b.sockstatLogger != nil {
|
||||
// Enable sockstats logs only on non-mobile unstable builds
|
||||
if version.IsUnstableBuild() && !version.IsMobile() && b.sockstatLogger != nil {
|
||||
b.sockstatLogger.SetLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@@ -439,12 +442,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
}
|
||||
|
||||
// initialize TailFS shares from saved state
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
// initialize Taildrive shares from saved state
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if ok {
|
||||
currentShares := b.pm.prefs.TailFSShares()
|
||||
currentShares := b.pm.prefs.DriveShares()
|
||||
if currentShares.Len() > 0 {
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < currentShares.Len(); i++ {
|
||||
shares = append(shares, currentShares.At(i).AsStruct())
|
||||
}
|
||||
@@ -891,6 +894,14 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
ExitNode: p.StableID() != "" && p.StableID() == exitNodeID,
|
||||
SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(),
|
||||
Location: p.Hostinfo().Location(),
|
||||
Capabilities: p.Capabilities().AsSlice(),
|
||||
}
|
||||
if cm := p.CapMap(); cm.Len() > 0 {
|
||||
ps.CapMap = make(tailcfg.NodeCapMap, cm.Len())
|
||||
cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
|
||||
ps.CapMap[k] = v.AsSlice()
|
||||
return true
|
||||
})
|
||||
}
|
||||
peerStatusFromNode(ps, p)
|
||||
|
||||
@@ -1007,15 +1018,16 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
|
||||
// SetControlClientStatus is the callback invoked by the control client whenever it posts a new status.
|
||||
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
|
||||
func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st controlclient.Status) {
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if b.cc != c {
|
||||
b.logf("Ignoring SetControlClientStatus from old client")
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
// The following do not depend on any data for which we need to lock b.
|
||||
if st.Err != nil {
|
||||
b.mu.Unlock()
|
||||
// The following do not depend on any data for which we need b locked.
|
||||
unlock.UnlockEarly()
|
||||
if errors.Is(st.Err, io.EOF) {
|
||||
b.logf("[v1] Received error: EOF")
|
||||
return
|
||||
@@ -1082,7 +1094,8 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
}
|
||||
b.keyExpired = isExpired
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
unlock.UnlockEarly()
|
||||
|
||||
if keyExpiryExtended && wasBlocked {
|
||||
// Key extended, unblock the engine
|
||||
@@ -1098,7 +1111,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
|
||||
}
|
||||
|
||||
// Lock b once and do only the things that require locking.
|
||||
// Lock b again and do only the things that require locking.
|
||||
b.mu.Lock()
|
||||
|
||||
prefsChanged := false
|
||||
@@ -2281,7 +2294,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialTailFSShares
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares
|
||||
if mask&initialBits != 0 {
|
||||
ini = &ipn.Notify{Version: version.Long()}
|
||||
if mask&ipn.NotifyInitialState != 0 {
|
||||
@@ -2297,8 +2310,8 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
if mask&ipn.NotifyInitialNetMap != 0 {
|
||||
ini.NetMap = b.netMap
|
||||
}
|
||||
if mask&ipn.NotifyInitialTailFSShares != 0 && b.tailFSSharingEnabledLocked() {
|
||||
ini.TailFSShares = b.pm.prefs.TailFSShares()
|
||||
if mask&ipn.NotifyInitialDriveShares != 0 && b.driveSharingEnabledLocked() {
|
||||
ini.DriveShares = b.pm.prefs.DriveShares()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3002,20 +3015,20 @@ func (b *LocalBackend) SetCurrentUser(token ipnauth.WindowsToken) (ipn.WindowsUs
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if b.pm.CurrentUserID() == uid {
|
||||
b.mu.Unlock()
|
||||
return uid, nil
|
||||
}
|
||||
if err := b.pm.SetCurrentUserID(uid); err != nil {
|
||||
b.mu.Unlock()
|
||||
return uid, nil
|
||||
}
|
||||
if b.currentUser != nil {
|
||||
b.currentUser.Close()
|
||||
}
|
||||
b.currentUser = token
|
||||
b.resetForProfileChangeLockedOnEntry()
|
||||
b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
@@ -3145,8 +3158,60 @@ func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUseExitNodeEnabled turns on or off the most recently selected exit node.
|
||||
//
|
||||
// On success, it returns the resulting prefs (or current prefs, in the case of no change).
|
||||
// Setting the value to false when use of an exit node is already false is not an error,
|
||||
// nor is true when the exit node is already in use.
|
||||
func (b *LocalBackend) SetUseExitNodeEnabled(v bool) (ipn.PrefsView, error) {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
p0 := b.pm.CurrentPrefs()
|
||||
if v && p0.ExitNodeID() != "" {
|
||||
// Already on.
|
||||
return p0, nil
|
||||
}
|
||||
if !v && p0.ExitNodeID() == "" {
|
||||
// Already off.
|
||||
return p0, nil
|
||||
}
|
||||
|
||||
var zero ipn.PrefsView
|
||||
if v && p0.InternalExitNodePrior() == "" {
|
||||
if !p0.ExitNodeIP().IsValid() {
|
||||
return zero, errors.New("no exit node IP to enable & prior exit node IP was never resolved an a node")
|
||||
}
|
||||
return zero, errors.New("no prior exit node to enable")
|
||||
}
|
||||
|
||||
mp := &ipn.MaskedPrefs{}
|
||||
if v {
|
||||
mp.ExitNodeIDSet = true
|
||||
mp.ExitNodeID = tailcfg.StableNodeID(p0.InternalExitNodePrior())
|
||||
} else {
|
||||
mp.ExitNodeIDSet = true
|
||||
mp.ExitNodeID = ""
|
||||
mp.InternalExitNodePriorSet = true
|
||||
mp.InternalExitNodePrior = string(p0.ExitNodeID())
|
||||
}
|
||||
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
b.mu.Lock()
|
||||
if mp.SetsInternal() {
|
||||
return ipn.PrefsView{}, errors.New("can't set Internal fields")
|
||||
}
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||
}
|
||||
|
||||
// Warning: b.mu must be held on entry, but it unlocks it on the way out.
|
||||
// TODO(bradfitz): redo the locking on all these weird methods like this.
|
||||
func (b *LocalBackend) editPrefsLockedOnEntry(mp *ipn.MaskedPrefs, unlock unlockOnce) (ipn.PrefsView, error) {
|
||||
defer unlock() // for error paths
|
||||
|
||||
if mp.EggSet {
|
||||
mp.EggSet = false
|
||||
b.egg = true
|
||||
@@ -3156,21 +3221,18 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
p1 := b.pm.CurrentPrefs().AsStruct()
|
||||
p1.ApplyEdits(mp)
|
||||
if err := b.checkPrefsLocked(p1); err != nil {
|
||||
b.mu.Unlock()
|
||||
b.logf("EditPrefs check error: %v", err)
|
||||
return ipn.PrefsView{}, err
|
||||
}
|
||||
if p1.RunSSH && !envknob.CanSSHD() {
|
||||
b.mu.Unlock()
|
||||
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
|
||||
return ipn.PrefsView{}, errors.New("Tailscale SSH server administratively disabled.")
|
||||
}
|
||||
if p1.View().Equals(p0) {
|
||||
b.mu.Unlock()
|
||||
return stripKeysFromPrefs(p0), nil
|
||||
}
|
||||
b.logf("EditPrefs: %v", mp.Pretty())
|
||||
newPrefs := b.setPrefsLockedOnEntry("EditPrefs", p1) // does a b.mu.Unlock
|
||||
newPrefs := b.setPrefsLockedOnEntry("EditPrefs", p1, unlock)
|
||||
|
||||
// Note: don't perform any actions for the new prefs here. Not
|
||||
// every prefs change goes through EditPrefs. Put your actions
|
||||
@@ -3203,8 +3265,9 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
if newp == nil {
|
||||
panic("SetPrefs got nil prefs")
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.setPrefsLockedOnEntry("SetPrefs", newp)
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
b.setPrefsLockedOnEntry("SetPrefs", newp, unlock)
|
||||
}
|
||||
|
||||
// wantIngressLocked reports whether this node has ingress configured. This bool
|
||||
@@ -3224,7 +3287,9 @@ func (b *LocalBackend) wantIngressLocked() bool {
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
// It returns a readonly copy of the new prefs.
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn.PrefsView {
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs, unlock unlockOnce) ipn.PrefsView {
|
||||
defer unlock()
|
||||
|
||||
netMap := b.netMap
|
||||
b.setAtomicValuesFromPrefsLocked(newp.View())
|
||||
|
||||
@@ -3285,7 +3350,8 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||
b.logf("failed to save new controlclient state: %v", err)
|
||||
}
|
||||
b.lastProfileID = b.pm.CurrentProfile().ID
|
||||
b.mu.Unlock()
|
||||
|
||||
unlock.UnlockEarly()
|
||||
|
||||
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
|
||||
b.doSetHostinfoFilterServices()
|
||||
@@ -3371,8 +3437,8 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
return b.handleWebClientConn, opts
|
||||
}
|
||||
return b.HandleQuad100Port80Conn, opts
|
||||
case TailFSLocalPort:
|
||||
return b.handleTailFSConn, opts
|
||||
case DriveLocalPort:
|
||||
return b.handleDriveConn, opts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3406,9 +3472,9 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleTailFSConn(conn net.Conn) error {
|
||||
fs, ok := b.sys.TailFSForLocal.GetOK()
|
||||
if !ok || !b.TailFSAccessEnabled() {
|
||||
func (b *LocalBackend) handleDriveConn(conn net.Conn) error {
|
||||
fs, ok := b.sys.DriveForLocal.GetOK()
|
||||
if !ok || !b.DriveAccessEnabled() {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
@@ -3444,15 +3510,15 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
// TODO(danderson): we shouldn't be mangling hostinfo here after
|
||||
// painstakingly constructing it in twelvety other places.
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices() {
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
cc := b.cc
|
||||
if cc == nil {
|
||||
// Control client isn't up yet.
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if b.hostinfo == nil {
|
||||
b.mu.Unlock()
|
||||
b.logf("[unexpected] doSetHostinfoFilterServices with nil hostinfo")
|
||||
return
|
||||
}
|
||||
@@ -3463,7 +3529,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices() {
|
||||
|
||||
// TODO(maisem,bradfitz): store hostinfo as a view, not as a mutable struct.
|
||||
hi := *b.hostinfo // shallow copy
|
||||
b.mu.Unlock()
|
||||
unlock.UnlockEarly()
|
||||
|
||||
// Make a shallow copy of hostinfo so we can mutate
|
||||
// at the Service field.
|
||||
@@ -4247,13 +4313,13 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
||||
// really this is more "one of several places in which random things
|
||||
// happen".
|
||||
func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
b.mu.Lock()
|
||||
b.enterStateLockedOnEntry(newState)
|
||||
unlock := b.lockAndGetUnlock()
|
||||
b.enterStateLockedOnEntry(newState, unlock)
|
||||
}
|
||||
|
||||
// enterStateLockedOnEntry is like enterState but requires b.mu be held to call
|
||||
// it, but it unlocks b.mu when done.
|
||||
func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
|
||||
// it, but it unlocks b.mu when done (via unlock, a once func).
|
||||
func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlockOnce) {
|
||||
oldState := b.state
|
||||
b.state = newState
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
@@ -4269,7 +4335,8 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
|
||||
b.closePeerAPIListenersLocked()
|
||||
}
|
||||
b.pauseOrResumeControlClientLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
unlock.UnlockEarly()
|
||||
|
||||
// prefs may change irrespective of state; WantRunning should be explicitly
|
||||
// set before potential early return even if the state is unchanged.
|
||||
@@ -4425,8 +4492,63 @@ func (b *LocalBackend) RequestEngineStatus() {
|
||||
// TODO(apenwarr): use a channel or something to prevent reentrancy?
|
||||
// Or maybe just call the state machine from fewer places.
|
||||
func (b *LocalBackend) stateMachine() {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
b.enterStateLockedOnEntry(b.nextStateLocked(), unlock)
|
||||
}
|
||||
|
||||
// lockAndGetUnlock locks b.mu and returns a sync.OnceFunc function that will
|
||||
// unlock it at most once.
|
||||
//
|
||||
// This is all very unfortunate but exists as a guardrail against the
|
||||
// unfortunate "lockedOnEntry" methods in this package (primarily
|
||||
// enterStateLockedOnEntry) that require b.mu held to be locked on entry to the
|
||||
// function but unlock the mutex on their way out. As a stepping stone to
|
||||
// cleaning things up (as of 2024-04-06), we at least pass the unlock func
|
||||
// around now and defer unlock in the caller to avoid missing unlocks and double
|
||||
// unlocks. TODO(bradfitz,maisem): make the locking in this package more
|
||||
// traditional (simple). See https://github.com/tailscale/tailscale/issues/11649
|
||||
func (b *LocalBackend) lockAndGetUnlock() (unlock unlockOnce) {
|
||||
b.mu.Lock()
|
||||
b.enterStateLockedOnEntry(b.nextStateLocked())
|
||||
var unlocked atomic.Bool
|
||||
return func() bool {
|
||||
if unlocked.CompareAndSwap(false, true) {
|
||||
b.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// unlockOnce is a func that unlocks only b.mu the first time it's called.
|
||||
// Therefore it can be safely deferred to catch error paths, without worrying
|
||||
// about double unlocks if a different point in the code later needs to explicitly
|
||||
// unlock it first as well. It reports whether it was unlocked.
|
||||
type unlockOnce func() bool
|
||||
|
||||
// UnlockEarly unlocks the LocalBackend.mu. It panics if u returns false,
|
||||
// indicating that this unlocker was already used.
|
||||
//
|
||||
// We're using this method to help us document & find the places that have
|
||||
// atypical locking patterns. See
|
||||
// https://github.com/tailscale/tailscale/issues/11649 for background.
|
||||
//
|
||||
// A normal unlock is a deferred one or an explicit b.mu.Unlock a few lines
|
||||
// after the lock, without lots of control flow in-between. An "early" unlock is
|
||||
// one that happens in weird places, like in various "LockedOnEntry" methods in
|
||||
// this package that require the mutex to be locked on entry but unlock it
|
||||
// somewhere in the middle (maybe several calls away) and then sometimes proceed
|
||||
// to lock it again.
|
||||
//
|
||||
// The reason UnlockeEarly panics if already called is because these are the
|
||||
// points at which it's assumed that the mutex is already held and it now needs
|
||||
// to be released. If somebody already released it, that invariant was violated.
|
||||
// On the other hand, simply calling u only returns false instead of panicking
|
||||
// so you can defer it without care, confident you got all the error return
|
||||
// paths which were previously done by hand.
|
||||
func (u unlockOnce) UnlockEarly() {
|
||||
if !u() {
|
||||
panic("Unlock on already-called unlockOnce")
|
||||
}
|
||||
}
|
||||
|
||||
// stopEngineAndWait deconfigures the local network data plane, and
|
||||
@@ -4490,7 +4612,9 @@ func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
|
||||
func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.logf("LocalBackend.ResetForClientDisconnect")
|
||||
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
prevCC := b.resetControlClientLocked()
|
||||
if prevCC != nil {
|
||||
// Needs to happen without b.mu held.
|
||||
@@ -4509,7 +4633,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.authURLTime = time.Time{}
|
||||
b.activeLogin = ""
|
||||
b.setAtomicValuesFromPrefsLocked(ipn.PrefsView{})
|
||||
b.enterStateLockedOnEntry(ipn.Stopped)
|
||||
b.enterStateLockedOnEntry(ipn.Stopped, unlock)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
|
||||
@@ -4565,10 +4689,11 @@ func (b *LocalBackend) ShouldHandleViaIP(ip netip.Addr) bool {
|
||||
// Logout logs out the current profile, if any, and waits for the logout to
|
||||
// complete.
|
||||
func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if !b.hasNodeKeyLocked() {
|
||||
// Already logged out.
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
cc := b.cc
|
||||
@@ -4576,16 +4701,16 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
// Grab the current profile before we unlock the mutex, so that we can
|
||||
// delete it later.
|
||||
profile := b.pm.CurrentProfile()
|
||||
b.mu.Unlock()
|
||||
|
||||
_, err := b.EditPrefs(&ipn.MaskedPrefs{
|
||||
_, err := b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
LoggedOutSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||
})
|
||||
}, unlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// b.mu is now unlocked, after editPrefsLockedOnEntry.
|
||||
|
||||
// Clear any previous dial plan(s), if set.
|
||||
b.dialPlan.Store(nil)
|
||||
@@ -4604,13 +4729,15 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
if err := cc.Logout(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
b.mu.Lock()
|
||||
|
||||
unlock = b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if err := b.pm.DeleteProfile(profile.ID); err != nil {
|
||||
b.mu.Unlock()
|
||||
b.logf("error deleting profile: %v", err)
|
||||
return err
|
||||
}
|
||||
return b.resetForProfileChangeLockedOnEntry()
|
||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
// assertClientLocked crashes if there is no controlclient in this backend.
|
||||
@@ -4718,8 +4845,8 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
b.updateTailFSPeersLocked(nm)
|
||||
b.tailFSNotifyCurrentSharesLocked()
|
||||
b.updateDrivePeersLocked(nm)
|
||||
b.driveNotifyCurrentSharesLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
@@ -4746,10 +4873,10 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// tailFSTransport is an http.RoundTripper that uses the latest value of
|
||||
// driveTransport is an http.RoundTripper that uses the latest value of
|
||||
// b.Dialer().PeerAPITransport() for each round trip and imposes a short
|
||||
// dial timeout to avoid hanging on connecting to offline/unreachable hosts.
|
||||
type tailFSTransport struct {
|
||||
type driveTransport struct {
|
||||
b *LocalBackend
|
||||
}
|
||||
|
||||
@@ -4769,7 +4896,7 @@ type responseBodyWrapper struct {
|
||||
contentLength int64
|
||||
}
|
||||
|
||||
// logAccess logs the tailfs: access: log line. If the logger is nil,
|
||||
// logAccess logs the taildrive: access: log line. If the logger is nil,
|
||||
// the log will not be written.
|
||||
func (rbw *responseBodyWrapper) logAccess(err string) {
|
||||
if rbw.log == nil {
|
||||
@@ -4779,7 +4906,7 @@ func (rbw *responseBodyWrapper) logAccess(err string) {
|
||||
// Some operating systems create and copy lots of 0 length hidden files for
|
||||
// tracking various states. Omit these to keep logs from being too verbose.
|
||||
if rbw.contentLength > 0 {
|
||||
rbw.log("tailfs: access: %s from %s to %s: status-code=%d ext=%q content-type=%q content-length=%.f tx=%.f rx=%.f err=%q", rbw.method, rbw.selfNodeKey, rbw.shareNodeKey, rbw.statusCode, rbw.fileExtension, rbw.contentType, roundTraffic(rbw.contentLength), roundTraffic(rbw.bytesTx), roundTraffic(rbw.bytesRx), err)
|
||||
rbw.log("taildrive: access: %s from %s to %s: status-code=%d ext=%q content-type=%q content-length=%.f tx=%.f rx=%.f err=%q", rbw.method, rbw.selfNodeKey, rbw.shareNodeKey, rbw.statusCode, rbw.fileExtension, rbw.contentType, roundTraffic(rbw.contentLength), roundTraffic(rbw.bytesTx), roundTraffic(rbw.bytesRx), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4806,7 +4933,7 @@ func (rbw *responseBodyWrapper) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *tailFSTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
bw := &requestBodyWrapper{}
|
||||
if req.Body != nil {
|
||||
bw.ReadCloser = req.Body
|
||||
@@ -4828,24 +4955,24 @@ func (t *tailFSTransport) RoundTrip(req *http.Request) (resp *http.Response, err
|
||||
return
|
||||
}
|
||||
|
||||
t.b.mu.Lock()
|
||||
selfNodeKey := t.b.netMap.SelfNode.Key().ShortString()
|
||||
t.b.mu.Unlock()
|
||||
n, _, ok := t.b.WhoIs(netip.MustParseAddrPort(req.URL.Host))
|
||||
dt.b.mu.Lock()
|
||||
selfNodeKey := dt.b.netMap.SelfNode.Key().ShortString()
|
||||
dt.b.mu.Unlock()
|
||||
n, _, ok := dt.b.WhoIs(netip.MustParseAddrPort(req.URL.Host))
|
||||
shareNodeKey := "unknown"
|
||||
if ok {
|
||||
shareNodeKey = string(n.Key().ShortString())
|
||||
}
|
||||
|
||||
rbw := responseBodyWrapper{
|
||||
log: t.b.logf,
|
||||
log: dt.b.logf,
|
||||
method: req.Method,
|
||||
bytesTx: int64(bw.bytesRead),
|
||||
selfNodeKey: selfNodeKey,
|
||||
shareNodeKey: shareNodeKey,
|
||||
contentType: contentType,
|
||||
contentLength: resp.ContentLength,
|
||||
fileExtension: parseTailFSFileExtensionForLog(req.URL.Path),
|
||||
fileExtension: parseDriveFileExtensionForLog(req.URL.Path),
|
||||
statusCode: resp.StatusCode,
|
||||
ReadCloser: resp.Body,
|
||||
}
|
||||
@@ -4862,7 +4989,7 @@ func (t *tailFSTransport) RoundTrip(req *http.Request) (resp *http.Response, err
|
||||
// unreachable hosts.
|
||||
dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this
|
||||
|
||||
tr := t.b.Dialer().PeerAPITransport().Clone()
|
||||
tr := dt.b.Dialer().PeerAPITransport().Clone()
|
||||
dialContext := tr.DialContext
|
||||
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
@@ -5789,12 +5916,13 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
|
||||
if b.CurrentProfile().ID == profile {
|
||||
return nil
|
||||
}
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if err := b.pm.SwitchProfile(profile); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
return b.resetForProfileChangeLockedOnEntry()
|
||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) initTKALocked() error {
|
||||
@@ -5847,24 +5975,24 @@ func (b *LocalBackend) initTKALocked() error {
|
||||
// resetForProfileChangeLockedOnEntry resets the backend for a profile change.
|
||||
//
|
||||
// b.mu must held on entry. It is released on exit.
|
||||
func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
|
||||
func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) error {
|
||||
defer unlock()
|
||||
|
||||
if b.shutdownCalled {
|
||||
// Prevent a call back to Start during Shutdown, which calls Logout for
|
||||
// ephemeral nodes, which can then call back here. But we're shutting
|
||||
// down, so no need to do any work.
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.setNetMapLocked(nil) // Reset netmap.
|
||||
// Reset the NetworkMap in the engine
|
||||
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||
if err := b.initTKALocked(); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
b.enterStateLockedOnEntry(ipn.NoState) // Reset state; releases b.mu
|
||||
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
||||
health.SetLocalLogConfigHealth(nil)
|
||||
return b.Start(ipn.Options{})
|
||||
}
|
||||
@@ -5872,8 +6000,9 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
|
||||
// DeleteProfile deletes a profile with the given ID.
|
||||
// If the profile is not known, it is a no-op.
|
||||
func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
needToRestart := b.pm.CurrentProfile().ID == p
|
||||
if err := b.pm.DeleteProfile(p); err != nil {
|
||||
if err == errProfileNotFound {
|
||||
@@ -5884,7 +6013,7 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
|
||||
if !needToRestart {
|
||||
return nil
|
||||
}
|
||||
return b.resetForProfileChangeLockedOnEntry()
|
||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
// CurrentProfile returns the current LoginProfile.
|
||||
@@ -5897,9 +6026,11 @@ func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
|
||||
|
||||
// NewProfile creates and switches to the new profile.
|
||||
func (b *LocalBackend) NewProfile() error {
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
b.pm.NewProfile()
|
||||
return b.resetForProfileChangeLockedOnEntry()
|
||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
// ListProfiles returns a list of all LoginProfiles.
|
||||
@@ -5914,20 +6045,20 @@ func (b *LocalBackend) ListProfiles() []ipn.LoginProfile {
|
||||
// backend is left with a new profile, ready for StartLoginInterative to be
|
||||
// called to register it as new node.
|
||||
func (b *LocalBackend) ResetAuth() error {
|
||||
b.mu.Lock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
prevCC := b.resetControlClientLocked()
|
||||
if prevCC != nil {
|
||||
defer prevCC.Shutdown() // call must happen after release b.mu
|
||||
}
|
||||
if err := b.clearMachineKeyLocked(); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
if err := b.pm.DeleteAllProfiles(); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
return b.resetForProfileChangeLockedOnEntry()
|
||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
// StreamDebugCapture writes a pcap stream of packets traversing
|
||||
|
||||
@@ -24,13 +24,13 @@ import (
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -455,6 +455,61 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestSetUseExitNodeEnabled(t *testing.T) {
|
||||
lb := newTestLocalBackend(t)
|
||||
|
||||
// Can't turn it on if it never had an old value.
|
||||
if _, err := lb.SetUseExitNodeEnabled(true); err == nil {
|
||||
t.Fatal("expected success")
|
||||
}
|
||||
|
||||
// But we can turn it off when it's already off.
|
||||
if _, err := lb.SetUseExitNodeEnabled(false); err != nil {
|
||||
t.Fatal("expected failure")
|
||||
}
|
||||
|
||||
// Give it an initial exit node in use.
|
||||
if _, err := lb.EditPrefs(&ipn.MaskedPrefs{
|
||||
ExitNodeIDSet: true,
|
||||
Prefs: ipn.Prefs{
|
||||
ExitNodeID: "foo",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("enabling first exit node: %v", err)
|
||||
}
|
||||
|
||||
// Now turn off that exit node.
|
||||
if prefs, err := lb.SetUseExitNodeEnabled(false); err != nil {
|
||||
t.Fatal("expected failure")
|
||||
} else {
|
||||
if g, w := prefs.ExitNodeID(), tailcfg.StableNodeID(""); g != w {
|
||||
t.Fatalf("unexpected exit node ID %q; want %q", g, w)
|
||||
}
|
||||
if g, w := prefs.InternalExitNodePrior(), "foo"; g != w {
|
||||
t.Fatalf("unexpected exit node prior %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
// And turn it back on.
|
||||
if prefs, err := lb.SetUseExitNodeEnabled(true); err != nil {
|
||||
t.Fatal("expected failure")
|
||||
} else {
|
||||
if g, w := prefs.ExitNodeID(), tailcfg.StableNodeID("foo"); g != w {
|
||||
t.Fatalf("unexpected exit node ID %q; want %q", g, w)
|
||||
}
|
||||
if g, w := prefs.InternalExitNodePrior(), "foo"; g != w {
|
||||
t.Fatalf("unexpected exit node prior %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we block setting an Internal field.
|
||||
if _, err := lb.EditPrefs(&ipn.MaskedPrefs{
|
||||
InternalExitNodePriorSet: true,
|
||||
}); err == nil {
|
||||
t.Fatalf("unexpected success; want an error trying to set an internal field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileTargets(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
_, err := b.FileTargets()
|
||||
@@ -736,6 +791,100 @@ func TestStatusWithoutPeers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusPeerCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []tailcfg.NodeView
|
||||
expectedPeerCapabilities map[tailcfg.StableNodeID][]tailcfg.NodeCapability
|
||||
expectedPeerCapMap map[tailcfg.StableNodeID]tailcfg.NodeCapMap
|
||||
}{
|
||||
{
|
||||
name: "peers-with-capabilities",
|
||||
peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 1,
|
||||
StableID: "foo",
|
||||
IsWireGuardOnly: true,
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilitySSH},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilitySSH: nil,
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "bar",
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityAdmin},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilityAdmin: {`{"test": "true}`},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
expectedPeerCapabilities: map[tailcfg.StableNodeID][]tailcfg.NodeCapability{
|
||||
tailcfg.StableNodeID("foo"): {tailcfg.CapabilitySSH},
|
||||
tailcfg.StableNodeID("bar"): {tailcfg.CapabilityAdmin},
|
||||
},
|
||||
expectedPeerCapMap: map[tailcfg.StableNodeID]tailcfg.NodeCapMap{
|
||||
tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilitySSH: nil,
|
||||
}),
|
||||
tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilityAdmin: {`{"test": "true}`},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "peers-without-capabilities",
|
||||
peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 1,
|
||||
StableID: "foo",
|
||||
IsWireGuardOnly: true,
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "bar",
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
}
|
||||
b := newTestLocalBackend(t)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
|
||||
t.Logf("ccGen: new mockControl.")
|
||||
cc.called("New")
|
||||
return cc, nil
|
||||
})
|
||||
b.Start(ipn.Options{})
|
||||
b.Login(nil)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b.setNetMapLocked(&netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
MachineAuthorized: true,
|
||||
Addresses: ipps("100.101.101.101"),
|
||||
}).View(),
|
||||
Peers: tt.peers,
|
||||
})
|
||||
got := b.Status()
|
||||
for _, peer := range got.Peer {
|
||||
if !reflect.DeepEqual(peer.Capabilities, tt.expectedPeerCapabilities[peer.ID]) {
|
||||
t.Errorf("peer capabilities: expected %v got %v", tt.expectedPeerCapabilities, peer.Capabilities)
|
||||
}
|
||||
if !reflect.DeepEqual(peer.CapMap, tt.expectedPeerCapMap[peer.ID]) {
|
||||
t.Errorf("peer capmap: expected %v got %v", tt.expectedPeerCapMap, peer.CapMap)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// legacyBackend was the interface between Tailscale frontends
|
||||
// (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale
|
||||
// backend (e.g. cmd/tailscaled) running on the same machine.
|
||||
@@ -2201,12 +2350,12 @@ func TestTCPHandlerForDst(t *testing.T) {
|
||||
intercept: false,
|
||||
},
|
||||
{
|
||||
desc: "intercept port 8080 (TailFS) on quad100 IPv4",
|
||||
desc: "intercept port 8080 (Taildrive) on quad100 IPv4",
|
||||
dst: "100.100.100.100:8080",
|
||||
intercept: true,
|
||||
},
|
||||
{
|
||||
desc: "intercept port 8080 (TailFS) on quad100 IPv6",
|
||||
desc: "intercept port 8080 (Taildrive) on quad100 IPv6",
|
||||
dst: "[fd7a:115c:a1e0::53]:8080",
|
||||
intercept: true,
|
||||
},
|
||||
@@ -2246,24 +2395,24 @@ func TestTCPHandlerForDst(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailFSManageShares(t *testing.T) {
|
||||
func TestDriveManageShares(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disabled bool
|
||||
existing []*tailfs.Share
|
||||
add *tailfs.Share
|
||||
existing []*drive.Share
|
||||
add *drive.Share
|
||||
remove string
|
||||
rename [2]string
|
||||
expect any
|
||||
}{
|
||||
{
|
||||
name: "append",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " E "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " E "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
{Name: "e"},
|
||||
@@ -2271,12 +2420,12 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "prepend",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " A "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " A "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
@@ -2284,12 +2433,12 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "insert",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " C "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " C "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
{Name: "d"},
|
||||
@@ -2297,43 +2446,43 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "replace",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b", Path: "i"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " B ", Path: "ii"},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " B ", Path: "ii"},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b", Path: "ii"},
|
||||
{Name: "d"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add_bad_name",
|
||||
add: &tailfs.Share{Name: "$"},
|
||||
expect: ErrInvalidShareName,
|
||||
add: &drive.Share{Name: "$"},
|
||||
expect: drive.ErrInvalidShareName,
|
||||
},
|
||||
{
|
||||
name: "add_disabled",
|
||||
disabled: true,
|
||||
add: &tailfs.Share{Name: "a"},
|
||||
expect: ErrTailFSNotEnabled,
|
||||
add: &drive.Share{Name: "a"},
|
||||
expect: drive.ErrDriveNotEnabled,
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
},
|
||||
remove: "b",
|
||||
expect: []*tailfs.Share{
|
||||
expect: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove_non_existing",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
@@ -2345,23 +2494,23 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
name: "remove_disabled",
|
||||
disabled: true,
|
||||
remove: "b",
|
||||
expect: ErrTailFSNotEnabled,
|
||||
expect: drive.ErrDriveNotEnabled,
|
||||
},
|
||||
{
|
||||
name: "rename",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
rename: [2]string{"a", " C "},
|
||||
expect: []*tailfs.Share{
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rename_not_exist",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
@@ -2370,7 +2519,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "rename_exists",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
@@ -2380,19 +2529,19 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
{
|
||||
name: "rename_bad_name",
|
||||
rename: [2]string{"a", "$"},
|
||||
expect: ErrInvalidShareName,
|
||||
expect: drive.ErrInvalidShareName,
|
||||
},
|
||||
{
|
||||
name: "rename_disabled",
|
||||
disabled: true,
|
||||
rename: [2]string{"a", "c"},
|
||||
expect: ErrTailFSNotEnabled,
|
||||
expect: drive.ErrDriveNotEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
tailfs.DisallowShareAs = true
|
||||
drive.DisallowShareAs = true
|
||||
t.Cleanup(func() {
|
||||
tailfs.DisallowShareAs = false
|
||||
drive.DisallowShareAs = false
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -2400,20 +2549,20 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
b.mu.Lock()
|
||||
if tt.existing != nil {
|
||||
b.tailFSSetSharesLocked(tt.existing)
|
||||
b.driveSetSharesLocked(tt.existing)
|
||||
}
|
||||
if !tt.disabled {
|
||||
self := b.netMap.SelfNode.AsStruct()
|
||||
self.CapMap = tailcfg.NodeCapMap{tailcfg.NodeAttrsTailFSShare: nil}
|
||||
self.CapMap = tailcfg.NodeCapMap{tailcfg.NodeAttrsTaildriveShare: nil}
|
||||
b.netMap.SelfNode = self.View()
|
||||
b.sys.Set(tailfsimpl.NewFileSystemForRemote(b.logf))
|
||||
b.sys.Set(driveimpl.NewFileSystemForRemote(b.logf))
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result := make(chan views.SliceView[*tailfs.Share, tailfs.ShareView], 1)
|
||||
result := make(chan views.SliceView[*drive.Share, drive.ShareView], 1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
@@ -2423,7 +2572,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
func() { wg.Done() },
|
||||
func(n *ipn.Notify) bool {
|
||||
select {
|
||||
case result <- n.TailFSShares:
|
||||
case result <- n.DriveShares:
|
||||
default:
|
||||
//
|
||||
}
|
||||
@@ -2435,11 +2584,11 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
var err error
|
||||
switch {
|
||||
case tt.add != nil:
|
||||
err = b.TailFSSetShare(tt.add)
|
||||
err = b.DriveSetShare(tt.add)
|
||||
case tt.remove != "":
|
||||
err = b.TailFSRemoveShare(tt.remove)
|
||||
err = b.DriveRemoveShare(tt.remove)
|
||||
default:
|
||||
err = b.TailFSRenameShare(tt.rename[0], tt.rename[1])
|
||||
err = b.DriveRenameShare(tt.rename[0], tt.rename[1])
|
||||
}
|
||||
|
||||
switch e := tt.expect.(type) {
|
||||
@@ -2447,7 +2596,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
if !errors.Is(err, e) {
|
||||
t.Errorf("expected error, want: %v got: %v", e, err)
|
||||
}
|
||||
case []*tailfs.Share:
|
||||
case []*drive.Share:
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -39,7 +40,6 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
@@ -48,7 +48,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tailFSPrefix = "/v0/tailfs"
|
||||
taildrivePrefix = "/v0/drive"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, string) error
|
||||
@@ -324,8 +324,8 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleDNSQuery(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, tailFSPrefix) {
|
||||
h.handleServeTailFS(w, r)
|
||||
if strings.HasPrefix(r.URL.Path, taildrivePrefix) {
|
||||
h.handleServeDrive(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
@@ -1141,37 +1141,37 @@ func (rbw *requestBodyWrapper) Read(b []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeTailFS(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.TailFSSharingEnabled() {
|
||||
h.logf("tailfs: not enabled")
|
||||
http.Error(w, "tailfs not enabled", http.StatusNotFound)
|
||||
func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.DriveSharingEnabled() {
|
||||
h.logf("taildrive: not enabled")
|
||||
http.Error(w, "taildrive not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
capsMap := h.peerCaps()
|
||||
tailfsCaps, ok := capsMap[tailcfg.PeerCapabilityTailFS]
|
||||
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
|
||||
if !ok {
|
||||
h.logf("tailfs: not permitted")
|
||||
http.Error(w, "tailfs not permitted", http.StatusForbidden)
|
||||
h.logf("taildrive: not permitted")
|
||||
http.Error(w, "taildrive not permitted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
rawPerms := make([][]byte, 0, len(tailfsCaps))
|
||||
for _, cap := range tailfsCaps {
|
||||
rawPerms := make([][]byte, 0, len(driveCaps))
|
||||
for _, cap := range driveCaps {
|
||||
rawPerms = append(rawPerms, []byte(cap))
|
||||
}
|
||||
|
||||
p, err := tailfs.ParsePermissions(rawPerms)
|
||||
p, err := drive.ParsePermissions(rawPerms)
|
||||
if err != nil {
|
||||
h.logf("tailfs: error parsing permissions: %w", err.Error())
|
||||
h.logf("taildrive: error parsing permissions: %w", err.Error())
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fs, ok := h.ps.b.sys.TailFSForRemote.GetOK()
|
||||
fs, ok := h.ps.b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
h.logf("tailfs: not supported on platform")
|
||||
http.Error(w, "tailfs not supported on platform", http.StatusNotFound)
|
||||
h.logf("taildrive: not supported on platform")
|
||||
http.Error(w, "taildrive not supported on platform", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
wr := &httpResponseWrapper{
|
||||
@@ -1193,22 +1193,22 @@ func (h *peerAPIHandler) handleServeTailFS(w http.ResponseWriter, r *http.Reques
|
||||
contentType = ct
|
||||
}
|
||||
|
||||
h.logf("tailfs: share: %s from %s to %s: status-code=%d ext=%q content-type=%q tx=%.f rx=%.f", r.Method, h.peerNode.Key().ShortString(), h.selfNode.Key().ShortString(), wr.statusCode, parseTailFSFileExtensionForLog(r.URL.Path), contentType, roundTraffic(wr.contentLength), roundTraffic(bw.bytesRead))
|
||||
h.logf("taildrive: share: %s from %s to %s: status-code=%d ext=%q content-type=%q tx=%.f rx=%.f", r.Method, h.peerNode.Key().ShortString(), h.selfNode.Key().ShortString(), wr.statusCode, parseDriveFileExtensionForLog(r.URL.Path), contentType, roundTraffic(wr.contentLength), roundTraffic(bw.bytesRead))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, tailFSPrefix)
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, taildrivePrefix)
|
||||
fs.ServeHTTPWithPerms(p, wr, r)
|
||||
}
|
||||
|
||||
// parseTailFSFileExtensionForLog parses the file extension, if available.
|
||||
// parseDriveFileExtensionForLog parses the file extension, if available.
|
||||
// If a file extension is not present or parsable, the file extension is
|
||||
// set to "unknown". If the file extension contains a double quote, it is
|
||||
// replaced with "removed".
|
||||
// All whitespace is removed from a parsed file extension.
|
||||
// File extensions including the leading ., e.g. ".gif".
|
||||
func parseTailFSFileExtensionForLog(path string) string {
|
||||
func parseDriveFileExtensionForLog(path string) string {
|
||||
fileExt := "unknown"
|
||||
if fe := filepath.Ext(path); fe != "" {
|
||||
if strings.Contains(fe, "\"") {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user