summaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
committerNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
commit0267fe83b200f1702a2fa0a395442c02a053fadb (patch)
tree63654c55225eeb834de59a5a0bc8d19033c6145b /cmd
parent87546a5edf6b6503a87eeb2d666baba57398a066 (diff)
downloadtailscale-1.78.0.tar.xz
tailscale-1.78.0.zip
VERSION.txt: this is v1.78.0v1.78.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/addlicense/main.go146
-rw-r--r--cmd/cloner/cloner_test.go120
-rw-r--r--cmd/containerboot/test_tailscale.sh16
-rw-r--r--cmd/containerboot/test_tailscaled.sh76
-rw-r--r--cmd/get-authkey/.gitignore2
-rw-r--r--cmd/gitops-pusher/.gitignore2
-rw-r--r--cmd/gitops-pusher/README.md96
-rw-r--r--cmd/gitops-pusher/cache.go132
-rw-r--r--cmd/gitops-pusher/gitops-pusher_test.go110
-rw-r--r--cmd/k8s-operator/deploy/chart/.helmignore46
-rw-r--r--cmd/k8s-operator/deploy/chart/Chart.yaml58
-rw-r--r--cmd/k8s-operator/deploy/chart/templates/apiserverproxy-rbac.yaml52
-rw-r--r--cmd/k8s-operator/deploy/chart/templates/oauth-secret.yaml26
-rw-r--r--cmd/k8s-operator/deploy/manifests/authproxy-rbac.yaml46
-rw-r--r--cmd/mkmanifest/main.go102
-rw-r--r--cmd/mkpkg/main.go268
-rw-r--r--cmd/mkversion/mkversion.go88
-rw-r--r--cmd/nardump/README.md14
-rw-r--r--cmd/nardump/nardump.go368
-rw-r--r--cmd/nginx-auth/.gitignore8
-rw-r--r--cmd/nginx-auth/README.md322
-rwxr-xr-xcmd/nginx-auth/deb/postinst.sh28
-rwxr-xr-xcmd/nginx-auth/deb/postrm.sh38
-rwxr-xr-xcmd/nginx-auth/deb/prerm.sh16
-rwxr-xr-xcmd/nginx-auth/mkdeb.sh64
-rw-r--r--cmd/nginx-auth/nginx-auth.go256
-rwxr-xr-xcmd/nginx-auth/rpm/postrm.sh18
-rwxr-xr-xcmd/nginx-auth/rpm/prerm.sh18
-rw-r--r--cmd/nginx-auth/tailscale.nginx-auth.service22
-rw-r--r--cmd/nginx-auth/tailscale.nginx-auth.socket16
-rw-r--r--cmd/pgproxy/README.md84
-rw-r--r--cmd/printdep/printdep.go82
-rw-r--r--cmd/sniproxy/.gitignore2
-rw-r--r--cmd/sniproxy/handlers_test.go318
-rw-r--r--cmd/sniproxy/server.go654
-rw-r--r--cmd/sniproxy/server_test.go190
-rw-r--r--cmd/sniproxy/sniproxy.go582
-rw-r--r--cmd/speedtest/speedtest.go242
-rw-r--r--cmd/ssh-auth-none-demo/ssh-auth-none-demo.go374
-rw-r--r--cmd/sync-containers/main.go428
-rw-r--r--cmd/tailscale/cli/diag.go148
-rw-r--r--cmd/tailscale/cli/diag_other.go30
-rw-r--r--cmd/tailscale/cli/set_test.go262
-rw-r--r--cmd/tailscale/cli/ssh_exec.go48
-rw-r--r--cmd/tailscale/cli/ssh_exec_js.go32
-rw-r--r--cmd/tailscale/cli/ssh_exec_windows.go74
-rw-r--r--cmd/tailscale/cli/ssh_unix.go98
-rw-r--r--cmd/tailscale/cli/web_test.go90
-rw-r--r--cmd/tailscale/generate.go16
-rw-r--r--cmd/tailscale/tailscale.go52
-rw-r--r--cmd/tailscale/windows-manifest.xml26
-rw-r--r--cmd/tailscaled/childproc/childproc.go38
-rw-r--r--cmd/tailscaled/generate.go16
-rw-r--r--cmd/tailscaled/install_darwin.go398
-rw-r--r--cmd/tailscaled/install_windows.go248
-rw-r--r--cmd/tailscaled/proxy.go160
-rw-r--r--cmd/tailscaled/sigpipe.go24
-rw-r--r--cmd/tailscaled/tailscaled.defaults16
-rwxr-xr-xcmd/tailscaled/tailscaled.openrc50
-rw-r--r--cmd/tailscaled/tailscaled_bird.go34
-rw-r--r--cmd/tailscaled/tailscaled_notwindows.go28
-rw-r--r--cmd/tailscaled/windows-manifest.xml26
-rw-r--r--cmd/tailscaled/with_cli.go46
-rw-r--r--cmd/testwrapper/args_test.go194
-rw-r--r--cmd/testwrapper/flakytest/flakytest.go88
-rw-r--r--cmd/testwrapper/flakytest/flakytest_test.go86
-rw-r--r--cmd/tsconnect/.gitignore6
-rw-r--r--cmd/tsconnect/README.md98
-rw-r--r--cmd/tsconnect/README.pkg.md6
-rw-r--r--cmd/tsconnect/build-pkg.go198
-rw-r--r--cmd/tsconnect/dev-pkg.go36
-rw-r--r--cmd/tsconnect/dev.go36
-rw-r--r--cmd/tsconnect/dist/placeholder4
-rw-r--r--cmd/tsconnect/index.html40
-rw-r--r--cmd/tsconnect/package.json50
-rw-r--r--cmd/tsconnect/package.json.tmpl32
-rw-r--r--cmd/tsconnect/serve.go288
-rw-r--r--cmd/tsconnect/src/app/app.tsx294
-rw-r--r--cmd/tsconnect/src/app/go-panic-display.tsx40
-rw-r--r--cmd/tsconnect/src/app/header.tsx74
-rw-r--r--cmd/tsconnect/src/app/index.css148
-rw-r--r--cmd/tsconnect/src/app/index.ts72
-rw-r--r--cmd/tsconnect/src/app/ssh.tsx314
-rw-r--r--cmd/tsconnect/src/app/url-display.tsx62
-rw-r--r--cmd/tsconnect/src/lib/js-state-store.ts26
-rw-r--r--cmd/tsconnect/src/pkg/pkg.css16
-rw-r--r--cmd/tsconnect/src/pkg/pkg.ts80
-rw-r--r--cmd/tsconnect/src/types/esbuild.d.ts28
-rw-r--r--cmd/tsconnect/src/types/wasm_js.d.ts206
-rw-r--r--cmd/tsconnect/tailwind.config.js16
-rw-r--r--cmd/tsconnect/tsconfig.json30
-rw-r--r--cmd/tsconnect/tsconnect.go142
-rw-r--r--cmd/tsconnect/yarn.lock1426
-rw-r--r--cmd/tsshd/tsshd.go24
94 files changed, 5825 insertions, 5825 deletions
diff --git a/cmd/addlicense/main.go b/cmd/addlicense/main.go
index a8fd9dd4a..58ef7a471 100644
--- a/cmd/addlicense/main.go
+++ b/cmd/addlicense/main.go
@@ -1,73 +1,73 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Program addlicense adds a license header to a file.
-// It is intended for use with 'go generate',
-// so it has a slightly weird usage.
-package main
-
-import (
- "flag"
- "fmt"
- "os"
- "os/exec"
-)
-
-var (
- file = flag.String("file", "", "file to modify")
-)
-
-func usage() {
- fmt.Fprintf(os.Stderr, `
-usage: addlicense -file FILE <subcommand args...>
-`[1:])
-
- flag.PrintDefaults()
- fmt.Fprintf(os.Stderr, `
-addlicense adds a Tailscale license to the beginning of file.
-
-It is intended for use with 'go generate', so it also runs a subcommand,
-which presumably creates the file.
-
-Sample usage:
-
-addlicense -file pull_strings.go stringer -type=pull
-`[1:])
- os.Exit(2)
-}
-
-func main() {
- flag.Usage = usage
- flag.Parse()
- if len(flag.Args()) == 0 {
- flag.Usage()
- }
- cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
- check(err)
- b, err := os.ReadFile(*file)
- check(err)
- f, err := os.OpenFile(*file, os.O_TRUNC|os.O_WRONLY, 0644)
- check(err)
- _, err = fmt.Fprint(f, license)
- check(err)
- _, err = f.Write(b)
- check(err)
- err = f.Close()
- check(err)
-}
-
-func check(err error) {
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
-}
-
-var license = `
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-`[1:]
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Program addlicense adds a license header to a file.
+// It is intended for use with 'go generate',
+// so it has a slightly weird usage.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+)
+
+var (
+ file = flag.String("file", "", "file to modify")
+)
+
+func usage() {
+ fmt.Fprintf(os.Stderr, `
+usage: addlicense -file FILE <subcommand args...>
+`[1:])
+
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr, `
+addlicense adds a Tailscale license to the beginning of file.
+
+It is intended for use with 'go generate', so it also runs a subcommand,
+which presumably creates the file.
+
+Sample usage:
+
+addlicense -file pull_strings.go stringer -type=pull
+`[1:])
+ os.Exit(2)
+}
+
+func main() {
+ flag.Usage = usage
+ flag.Parse()
+ if len(flag.Args()) == 0 {
+ flag.Usage()
+ }
+ cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ check(err)
+ b, err := os.ReadFile(*file)
+ check(err)
+ f, err := os.OpenFile(*file, os.O_TRUNC|os.O_WRONLY, 0644)
+ check(err)
+ _, err = fmt.Fprint(f, license)
+ check(err)
+ _, err = f.Write(b)
+ check(err)
+ err = f.Close()
+ check(err)
+}
+
+func check(err error) {
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+var license = `
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+`[1:]
diff --git a/cmd/cloner/cloner_test.go b/cmd/cloner/cloner_test.go
index d8d5df3cb..83d33ab0e 100644
--- a/cmd/cloner/cloner_test.go
+++ b/cmd/cloner/cloner_test.go
@@ -1,60 +1,60 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-package main
-
-import (
- "reflect"
- "testing"
-
- "tailscale.com/cmd/cloner/clonerex"
-)
-
-func TestSliceContainer(t *testing.T) {
- num := 5
- examples := []struct {
- name string
- in *clonerex.SliceContainer
- }{
- {
- name: "nil",
- in: nil,
- },
- {
- name: "zero",
- in: &clonerex.SliceContainer{},
- },
- {
- name: "empty",
- in: &clonerex.SliceContainer{
- Slice: []*int{},
- },
- },
- {
- name: "nils",
- in: &clonerex.SliceContainer{
- Slice: []*int{nil, nil, nil, nil, nil},
- },
- },
- {
- name: "one",
- in: &clonerex.SliceContainer{
- Slice: []*int{&num},
- },
- },
- {
- name: "several",
- in: &clonerex.SliceContainer{
- Slice: []*int{&num, &num, &num, &num, &num},
- },
- },
- }
-
- for _, ex := range examples {
- t.Run(ex.name, func(t *testing.T) {
- out := ex.in.Clone()
- if !reflect.DeepEqual(ex.in, out) {
- t.Errorf("Clone() = %v, want %v", out, ex.in)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+package main
+
+import (
+ "reflect"
+ "testing"
+
+ "tailscale.com/cmd/cloner/clonerex"
+)
+
+func TestSliceContainer(t *testing.T) {
+ num := 5
+ examples := []struct {
+ name string
+ in *clonerex.SliceContainer
+ }{
+ {
+ name: "nil",
+ in: nil,
+ },
+ {
+ name: "zero",
+ in: &clonerex.SliceContainer{},
+ },
+ {
+ name: "empty",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{},
+ },
+ },
+ {
+ name: "nils",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{nil, nil, nil, nil, nil},
+ },
+ },
+ {
+ name: "one",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{&num},
+ },
+ },
+ {
+ name: "several",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{&num, &num, &num, &num, &num},
+ },
+ },
+ }
+
+ for _, ex := range examples {
+ t.Run(ex.name, func(t *testing.T) {
+ out := ex.in.Clone()
+ if !reflect.DeepEqual(ex.in, out) {
+ t.Errorf("Clone() = %v, want %v", out, ex.in)
+ }
+ })
+ }
+}
diff --git a/cmd/containerboot/test_tailscale.sh b/cmd/containerboot/test_tailscale.sh
index 1fa10abb1..dd56adf04 100644
--- a/cmd/containerboot/test_tailscale.sh
+++ b/cmd/containerboot/test_tailscale.sh
@@ -1,8 +1,8 @@
-#!/usr/bin/env bash
-#
-# This is a fake tailscale CLI (and also iptables and ip6tables) that
-# records its arguments and exits successfully.
-#
-# It is used by main_test.go to test the behavior of containerboot.
-
-echo $0 $@ >>$TS_TEST_RECORD_ARGS
+#!/usr/bin/env bash
+#
+# This is a fake tailscale CLI (and also iptables and ip6tables) that
+# records its arguments and exits successfully.
+#
+# It is used by main_test.go to test the behavior of containerboot.
+
+echo $0 $@ >>$TS_TEST_RECORD_ARGS
diff --git a/cmd/containerboot/test_tailscaled.sh b/cmd/containerboot/test_tailscaled.sh
index 335e2cb0d..b7404a0a9 100644
--- a/cmd/containerboot/test_tailscaled.sh
+++ b/cmd/containerboot/test_tailscaled.sh
@@ -1,38 +1,38 @@
-#!/usr/bin/env bash
-#
-# This is a fake tailscale daemon that records its arguments, symlinks a
-# fake LocalAPI socket into place, and does nothing until terminated.
-#
-# It is used by main_test.go to test the behavior of containerboot.
-
-set -eu
-
-echo $0 $@ >>$TS_TEST_RECORD_ARGS
-
-socket=""
-while [[ $# -gt 0 ]]; do
- case $1 in
- --socket=*)
- socket="${1#--socket=}"
- shift
- ;;
- --socket)
- shift
- socket="$1"
- shift
- ;;
- *)
- shift
- ;;
- esac
-done
-
-if [[ -z "$socket" ]]; then
- echo "didn't find socket path in args"
- exit 1
-fi
-
-ln -s "$TS_TEST_SOCKET" "$socket"
-trap 'rm -f "$socket"' EXIT
-
-while sleep 10; do :; done
+#!/usr/bin/env bash
+#
+# This is a fake tailscale daemon that records its arguments, symlinks a
+# fake LocalAPI socket into place, and does nothing until terminated.
+#
+# It is used by main_test.go to test the behavior of containerboot.
+
+set -eu
+
+echo $0 $@ >>$TS_TEST_RECORD_ARGS
+
+socket=""
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --socket=*)
+ socket="${1#--socket=}"
+ shift
+ ;;
+ --socket)
+ shift
+ socket="$1"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [[ -z "$socket" ]]; then
+ echo "didn't find socket path in args"
+ exit 1
+fi
+
+ln -s "$TS_TEST_SOCKET" "$socket"
+trap 'rm -f "$socket"' EXIT
+
+while sleep 10; do :; done
diff --git a/cmd/get-authkey/.gitignore b/cmd/get-authkey/.gitignore
index 3f9c9fb90..e00856fa1 100644
--- a/cmd/get-authkey/.gitignore
+++ b/cmd/get-authkey/.gitignore
@@ -1 +1 @@
-get-authkey
+get-authkey
diff --git a/cmd/gitops-pusher/.gitignore b/cmd/gitops-pusher/.gitignore
index 504452249..eeed6e4bf 100644
--- a/cmd/gitops-pusher/.gitignore
+++ b/cmd/gitops-pusher/.gitignore
@@ -1 +1 @@
-version-cache.json
+version-cache.json
diff --git a/cmd/gitops-pusher/README.md b/cmd/gitops-pusher/README.md
index 9f77ea970..b08125397 100644
--- a/cmd/gitops-pusher/README.md
+++ b/cmd/gitops-pusher/README.md
@@ -1,48 +1,48 @@
-# gitops-pusher
-
-This is a small tool to help people achieve a
-[GitOps](https://about.gitlab.com/topics/gitops/) workflow with Tailscale ACL
-changes. This tool is intended to be used in a CI flow that looks like this:
-
-```yaml
-name: Tailscale ACL syncing
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- acls:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup Go environment
- uses: actions/setup-go@v3.2.0
-
- - name: Install gitops-pusher
- run: go install tailscale.com/cmd/gitops-pusher@latest
-
- - name: Deploy ACL
- if: github.event_name == 'push'
- env:
- TS_API_KEY: ${{ secrets.TS_API_KEY }}
- TS_TAILNET: ${{ secrets.TS_TAILNET }}
- run: |
- ~/go/bin/gitops-pusher --policy-file ./policy.hujson apply
-
- - name: ACL tests
- if: github.event_name == 'pull_request'
- env:
- TS_API_KEY: ${{ secrets.TS_API_KEY }}
- TS_TAILNET: ${{ secrets.TS_TAILNET }}
- run: |
- ~/go/bin/gitops-pusher --policy-file ./policy.hujson test
-```
-
-Change the value of the `--policy-file` flag to point to the policy file on
-disk. Policy files should be in [HuJSON](https://github.com/tailscale/hujson)
-format.
+# gitops-pusher
+
+This is a small tool to help people achieve a
+[GitOps](https://about.gitlab.com/topics/gitops/) workflow with Tailscale ACL
+changes. This tool is intended to be used in a CI flow that looks like this:
+
+```yaml
+name: Tailscale ACL syncing
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ acls:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Go environment
+ uses: actions/setup-go@v3.2.0
+
+ - name: Install gitops-pusher
+ run: go install tailscale.com/cmd/gitops-pusher@latest
+
+ - name: Deploy ACL
+ if: github.event_name == 'push'
+ env:
+ TS_API_KEY: ${{ secrets.TS_API_KEY }}
+ TS_TAILNET: ${{ secrets.TS_TAILNET }}
+ run: |
+ ~/go/bin/gitops-pusher --policy-file ./policy.hujson apply
+
+ - name: ACL tests
+ if: github.event_name == 'pull_request'
+ env:
+ TS_API_KEY: ${{ secrets.TS_API_KEY }}
+ TS_TAILNET: ${{ secrets.TS_TAILNET }}
+ run: |
+ ~/go/bin/gitops-pusher --policy-file ./policy.hujson test
+```
+
+Change the value of the `--policy-file` flag to point to the policy file on
+disk. Policy files should be in [HuJSON](https://github.com/tailscale/hujson)
+format.
diff --git a/cmd/gitops-pusher/cache.go b/cmd/gitops-pusher/cache.go
index 6792e5e63..89225e6f8 100644
--- a/cmd/gitops-pusher/cache.go
+++ b/cmd/gitops-pusher/cache.go
@@ -1,66 +1,66 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "encoding/json"
- "os"
-)
-
-// Cache contains cached information about the last time this tool was run.
-//
-// This is serialized to a JSON file that should NOT be checked into git.
-// It should be managed with either CI cache tools or stored locally somehow. The
-// exact mechanism is irrelevant as long as it is consistent.
-//
-// This allows gitops-pusher to detect external ACL changes. I'm not sure what to
-// call this problem, so I've been calling it the "three version problem" in my
-// notes. The basic problem is that at any given time we only have two versions
-// of the ACL file at any given point. In order to check if there has been
-// tampering of the ACL files in the admin panel, we need to have a _third_ version
-// to compare against.
-//
-// In this case I am not storing the old ACL entirely (though that could be a
-// reasonable thing to add in the future), but only its sha256sum. This allows
-// us to detect if the shasum in control matches the shasum we expect, and if that
-// expectation fails, then we can react accordingly.
-type Cache struct {
- PrevETag string // Stores the previous ETag of the ACL to allow
-}
-
-// Save persists the cache to a given file.
-func (c *Cache) Save(fname string) error {
- os.Remove(fname)
- fout, err := os.Create(fname)
- if err != nil {
- return err
- }
- defer fout.Close()
-
- return json.NewEncoder(fout).Encode(c)
-}
-
-// LoadCache loads the cache from a given file.
-func LoadCache(fname string) (*Cache, error) {
- var result Cache
-
- fin, err := os.Open(fname)
- if err != nil {
- return nil, err
- }
- defer fin.Close()
-
- err = json.NewDecoder(fin).Decode(&result)
- if err != nil {
- return nil, err
- }
-
- return &result, nil
-}
-
-// Shuck removes the first and last character of a string, analogous to
-// shucking off the husk of an ear of corn.
-func Shuck(s string) string {
- return s[1 : len(s)-1]
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+import (
+ "encoding/json"
+ "os"
+)
+
+// Cache contains cached information about the last time this tool was run.
+//
+// This is serialized to a JSON file that should NOT be checked into git.
+// It should be managed with either CI cache tools or stored locally somehow. The
+// exact mechanism is irrelevant as long as it is consistent.
+//
+// This allows gitops-pusher to detect external ACL changes. I'm not sure what to
+// call this problem, so I've been calling it the "three version problem" in my
+// notes. The basic problem is that at any given time we only have two versions
+// of the ACL file at any given point. In order to check if there has been
+// tampering of the ACL files in the admin panel, we need to have a _third_ version
+// to compare against.
+//
+// In this case I am not storing the old ACL entirely (though that could be a
+// reasonable thing to add in the future), but only its sha256sum. This allows
+// us to detect if the shasum in control matches the shasum we expect, and if that
+// expectation fails, then we can react accordingly.
+type Cache struct {
+ PrevETag string // Stores the previous ETag of the ACL to allow
+}
+
+// Save persists the cache to a given file.
+func (c *Cache) Save(fname string) error {
+ os.Remove(fname)
+ fout, err := os.Create(fname)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ return json.NewEncoder(fout).Encode(c)
+}
+
+// LoadCache loads the cache from a given file.
+func LoadCache(fname string) (*Cache, error) {
+ var result Cache
+
+ fin, err := os.Open(fname)
+ if err != nil {
+ return nil, err
+ }
+ defer fin.Close()
+
+ err = json.NewDecoder(fin).Decode(&result)
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+// Shuck removes the first and last character of a string, analogous to
+// shucking off the husk of an ear of corn.
+func Shuck(s string) string {
+ return s[1 : len(s)-1]
+}
diff --git a/cmd/gitops-pusher/gitops-pusher_test.go b/cmd/gitops-pusher/gitops-pusher_test.go
index b050761d9..1beb049c6 100644
--- a/cmd/gitops-pusher/gitops-pusher_test.go
+++ b/cmd/gitops-pusher/gitops-pusher_test.go
@@ -1,55 +1,55 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-package main
-
-import (
- "encoding/json"
- "strings"
- "testing"
-
- "tailscale.com/client/tailscale"
-)
-
-func TestEmbeddedTypeUnmarshal(t *testing.T) {
- var gitopsErr ACLGitopsTestError
- gitopsErr.Message = "gitops response error"
- gitopsErr.Data = []tailscale.ACLTestFailureSummary{
- {
- User: "GitopsError",
- Errors: []string{"this was initially created as a gitops error"},
- },
- }
-
- var aclTestErr tailscale.ACLTestError
- aclTestErr.Message = "native ACL response error"
- aclTestErr.Data = []tailscale.ACLTestFailureSummary{
- {
- User: "ACLError",
- Errors: []string{"this was initially created as an ACL error"},
- },
- }
-
- t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
- b, _ := json.Marshal(aclTestErr)
- var e ACLGitopsTestError
- err := json.Unmarshal(b, &e)
- if err != nil {
- t.Fatal(err)
- }
- if !strings.Contains(e.Error(), "For user ACLError") { // the gitops error prints out the user, the acl error doesn't
- t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
- }
- })
- t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
- b, _ := json.Marshal(gitopsErr)
- var e tailscale.ACLTestError
- err := json.Unmarshal(b, &e)
- if err != nil {
- t.Fatal(err)
- }
- expectedErr := `Status: 0, Message: "gitops response error", Data: [{User:GitopsError Errors:[this was initially created as a gitops error] Warnings:[]}]`
- if e.Error() != expectedErr {
- t.Fatalf("got %v\n, expected %v", e.Error(), expectedErr)
- }
- })
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+package main
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "tailscale.com/client/tailscale"
+)
+
+func TestEmbeddedTypeUnmarshal(t *testing.T) {
+ var gitopsErr ACLGitopsTestError
+ gitopsErr.Message = "gitops response error"
+ gitopsErr.Data = []tailscale.ACLTestFailureSummary{
+ {
+ User: "GitopsError",
+ Errors: []string{"this was initially created as a gitops error"},
+ },
+ }
+
+ var aclTestErr tailscale.ACLTestError
+ aclTestErr.Message = "native ACL response error"
+ aclTestErr.Data = []tailscale.ACLTestFailureSummary{
+ {
+ User: "ACLError",
+ Errors: []string{"this was initially created as an ACL error"},
+ },
+ }
+
+ t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
+ b, _ := json.Marshal(aclTestErr)
+ var e ACLGitopsTestError
+ err := json.Unmarshal(b, &e)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(e.Error(), "For user ACLError") { // the gitops error prints out the user, the acl error doesn't
+ t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
+ }
+ })
+ t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
+ b, _ := json.Marshal(gitopsErr)
+ var e tailscale.ACLTestError
+ err := json.Unmarshal(b, &e)
+ if err != nil {
+ t.Fatal(err)
+ }
+ expectedErr := `Status: 0, Message: "gitops response error", Data: [{User:GitopsError Errors:[this was initially created as a gitops error] Warnings:[]}]`
+ if e.Error() != expectedErr {
+ t.Fatalf("got %v\n, expected %v", e.Error(), expectedErr)
+ }
+ })
+}
diff --git a/cmd/k8s-operator/deploy/chart/.helmignore b/cmd/k8s-operator/deploy/chart/.helmignore
index 0e8a0eb36..f82e96d46 100644
--- a/cmd/k8s-operator/deploy/chart/.helmignore
+++ b/cmd/k8s-operator/deploy/chart/.helmignore
@@ -1,23 +1,23 @@
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*.orig
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
-.vscode/
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/cmd/k8s-operator/deploy/chart/Chart.yaml b/cmd/k8s-operator/deploy/chart/Chart.yaml
index 363d87d15..472850c41 100644
--- a/cmd/k8s-operator/deploy/chart/Chart.yaml
+++ b/cmd/k8s-operator/deploy/chart/Chart.yaml
@@ -1,29 +1,29 @@
-# Copyright (c) Tailscale Inc & AUTHORS
-# SPDX-License-Identifier: BSD-3-Clause
-
-apiVersion: v2
-name: tailscale-operator
-description: A Helm chart for Tailscale Kubernetes operator
-home: https://github.com/tailscale/tailscale
-
-keywords:
- - "tailscale"
- - "vpn"
- - "ingress"
- - "egress"
- - "wireguard"
-
-sources:
-- https://github.com/tailscale/tailscale
-
-type: application
-
-maintainers:
- - name: tailscale-maintainers
- url: https://tailscale.com/
-
-# version will be set to Tailscale repo tag (without 'v') at release time.
-version: 0.1.0
-
-# appVersion will be set to Tailscale repo tag at release time.
-appVersion: "unstable"
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+apiVersion: v2
+name: tailscale-operator
+description: A Helm chart for Tailscale Kubernetes operator
+home: https://github.com/tailscale/tailscale
+
+keywords:
+ - "tailscale"
+ - "vpn"
+ - "ingress"
+ - "egress"
+ - "wireguard"
+
+sources:
+- https://github.com/tailscale/tailscale
+
+type: application
+
+maintainers:
+ - name: tailscale-maintainers
+ url: https://tailscale.com/
+
+# version will be set to Tailscale repo tag (without 'v') at release time.
+version: 0.1.0
+
+# appVersion will be set to Tailscale repo tag at release time.
+appVersion: "unstable"
diff --git a/cmd/k8s-operator/deploy/chart/templates/apiserverproxy-rbac.yaml b/cmd/k8s-operator/deploy/chart/templates/apiserverproxy-rbac.yaml
index 072ecf6d2..488c87d8a 100644
--- a/cmd/k8s-operator/deploy/chart/templates/apiserverproxy-rbac.yaml
+++ b/cmd/k8s-operator/deploy/chart/templates/apiserverproxy-rbac.yaml
@@ -1,26 +1,26 @@
-# Copyright (c) Tailscale Inc & AUTHORS
-# SPDX-License-Identifier: BSD-3-Clause
-
-{{ if eq .Values.apiServerProxyConfig.mode "true" }}
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: tailscale-auth-proxy
-rules:
-- apiGroups: [""]
- resources: ["users", "groups"]
- verbs: ["impersonate"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
- name: tailscale-auth-proxy
-subjects:
-- kind: ServiceAccount
- name: operator
- namespace: {{ .Release.Namespace }}
-roleRef:
- kind: ClusterRole
- name: tailscale-auth-proxy
- apiGroup: rbac.authorization.k8s.io
-{{ end }}
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+{{ if eq .Values.apiServerProxyConfig.mode "true" }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: tailscale-auth-proxy
+rules:
+- apiGroups: [""]
+ resources: ["users", "groups"]
+ verbs: ["impersonate"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: tailscale-auth-proxy
+subjects:
+- kind: ServiceAccount
+ name: operator
+ namespace: {{ .Release.Namespace }}
+roleRef:
+ kind: ClusterRole
+ name: tailscale-auth-proxy
+ apiGroup: rbac.authorization.k8s.io
+{{ end }}
diff --git a/cmd/k8s-operator/deploy/chart/templates/oauth-secret.yaml b/cmd/k8s-operator/deploy/chart/templates/oauth-secret.yaml
index b44fde0a1..bde64b7f6 100644
--- a/cmd/k8s-operator/deploy/chart/templates/oauth-secret.yaml
+++ b/cmd/k8s-operator/deploy/chart/templates/oauth-secret.yaml
@@ -1,13 +1,13 @@
-# Copyright (c) Tailscale Inc & AUTHORS
-# SPDX-License-Identifier: BSD-3-Clause
-
-{{ if and .Values.oauth .Values.oauth.clientId -}}
-apiVersion: v1
-kind: Secret
-metadata:
- name: operator-oauth
- namespace: {{ .Release.Namespace }}
-stringData:
- client_id: {{ .Values.oauth.clientId }}
- client_secret: {{ .Values.oauth.clientSecret }}
-{{- end -}}
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+{{ if and .Values.oauth .Values.oauth.clientId -}}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: operator-oauth
+ namespace: {{ .Release.Namespace }}
+stringData:
+ client_id: {{ .Values.oauth.clientId }}
+ client_secret: {{ .Values.oauth.clientSecret }}
+{{- end -}}
diff --git a/cmd/k8s-operator/deploy/manifests/authproxy-rbac.yaml b/cmd/k8s-operator/deploy/manifests/authproxy-rbac.yaml
index ddbdda32e..d957260eb 100644
--- a/cmd/k8s-operator/deploy/manifests/authproxy-rbac.yaml
+++ b/cmd/k8s-operator/deploy/manifests/authproxy-rbac.yaml
@@ -1,24 +1,24 @@
-# Copyright (c) Tailscale Inc & AUTHORS
-# SPDX-License-Identifier: BSD-3-Clause
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: tailscale-auth-proxy
-rules:
-- apiGroups: [""]
- resources: ["users", "groups"]
- verbs: ["impersonate"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
- name: tailscale-auth-proxy
-subjects:
-- kind: ServiceAccount
- name: operator
- namespace: tailscale
-roleRef:
- kind: ClusterRole
- name: tailscale-auth-proxy
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: tailscale-auth-proxy
+rules:
+- apiGroups: [""]
+ resources: ["users", "groups"]
+ verbs: ["impersonate"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: tailscale-auth-proxy
+subjects:
+- kind: ServiceAccount
+ name: operator
+ namespace: tailscale
+roleRef:
+ kind: ClusterRole
+ name: tailscale-auth-proxy
apiGroup: rbac.authorization.k8s.io \ No newline at end of file
diff --git a/cmd/mkmanifest/main.go b/cmd/mkmanifest/main.go
index fb3c729f1..22cd15026 100644
--- a/cmd/mkmanifest/main.go
+++ b/cmd/mkmanifest/main.go
@@ -1,51 +1,51 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The mkmanifest command is a simple helper utility to create a '.syso' file
-// that contains a Windows manifest file.
-package main
-
-import (
- "log"
- "os"
-
- "github.com/tc-hib/winres"
-)
-
-func main() {
- if len(os.Args) != 4 {
- log.Fatalf("usage: %s arch manifest.xml output.syso", os.Args[0])
- }
-
- arch := winres.Arch(os.Args[1])
- switch arch {
- case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
- default:
- log.Fatalf("unsupported arch: %s", arch)
- }
-
- manifest, err := os.ReadFile(os.Args[2])
- if err != nil {
- log.Fatalf("error reading manifest file %q: %v", os.Args[2], err)
- }
-
- out := os.Args[3]
-
- // Start by creating an empty resource set
- rs := winres.ResourceSet{}
-
- // Add resources
- rs.Set(winres.RT_MANIFEST, winres.ID(1), 0, manifest)
-
- // Compile to a COFF object file
- f, err := os.Create(out)
- if err != nil {
- log.Fatalf("error creating output file %q: %v", out, err)
- }
- if err := rs.WriteObject(f, arch); err != nil {
- log.Fatalf("error writing object: %v", err)
- }
- if err := f.Close(); err != nil {
- log.Fatalf("error writing output file %q: %v", out, err)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The mkmanifest command is a simple helper utility to create a '.syso' file
+// that contains a Windows manifest file.
+package main
+
+import (
+ "log"
+ "os"
+
+ "github.com/tc-hib/winres"
+)
+
+func main() {
+ if len(os.Args) != 4 {
+ log.Fatalf("usage: %s arch manifest.xml output.syso", os.Args[0])
+ }
+
+ arch := winres.Arch(os.Args[1])
+ switch arch {
+ case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
+ default:
+ log.Fatalf("unsupported arch: %s", arch)
+ }
+
+ manifest, err := os.ReadFile(os.Args[2])
+ if err != nil {
+ log.Fatalf("error reading manifest file %q: %v", os.Args[2], err)
+ }
+
+ out := os.Args[3]
+
+ // Start by creating an empty resource set
+ rs := winres.ResourceSet{}
+
+ // Add resources
+ rs.Set(winres.RT_MANIFEST, winres.ID(1), 0, manifest)
+
+ // Compile to a COFF object file
+ f, err := os.Create(out)
+ if err != nil {
+ log.Fatalf("error creating output file %q: %v", out, err)
+ }
+ if err := rs.WriteObject(f, arch); err != nil {
+ log.Fatalf("error writing object: %v", err)
+ }
+ if err := f.Close(); err != nil {
+ log.Fatalf("error writing output file %q: %v", out, err)
+ }
+}
diff --git a/cmd/mkpkg/main.go b/cmd/mkpkg/main.go
index 5e26b07f8..e942c0162 100644
--- a/cmd/mkpkg/main.go
+++ b/cmd/mkpkg/main.go
@@ -1,134 +1,134 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// mkpkg builds the Tailscale rpm and deb packages.
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
- "strings"
-
- "github.com/goreleaser/nfpm/v2"
- _ "github.com/goreleaser/nfpm/v2/deb"
- "github.com/goreleaser/nfpm/v2/files"
- _ "github.com/goreleaser/nfpm/v2/rpm"
-)
-
-// parseFiles parses a comma-separated list of colon-separated pairs
-// into files.Contents format.
-func parseFiles(s string, typ string) (files.Contents, error) {
- if len(s) == 0 {
- return nil, nil
- }
- var contents files.Contents
- for _, f := range strings.Split(s, ",") {
- fs := strings.Split(f, ":")
- if len(fs) != 2 {
- return nil, fmt.Errorf("unparseable file field %q", f)
- }
- contents = append(contents, &files.Content{Type: files.TypeFile, Source: fs[0], Destination: fs[1]})
- }
- return contents, nil
-}
-
-func parseEmptyDirs(s string) files.Contents {
- // strings.Split("", ",") would return []string{""}, which is not suitable:
- // this would create an empty dir record with path "", breaking the package
- if s == "" {
- return nil
- }
- var contents files.Contents
- for _, d := range strings.Split(s, ",") {
- contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
- }
- return contents
-}
-
-func main() {
- out := flag.String("out", "", "output file to write")
- name := flag.String("name", "tailscale", "package name")
- description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
- goarch := flag.String("arch", "amd64", "GOARCH this package is for")
- pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
- regularFiles := flag.String("files", "", "comma-separated list of files in src:dst form")
- configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
- emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
- version := flag.String("version", "0.0.0", "version of the package")
- postinst := flag.String("postinst", "", "debian postinst script path")
- prerm := flag.String("prerm", "", "debian prerm script path")
- postrm := flag.String("postrm", "", "debian postrm script path")
- replaces := flag.String("replaces", "", "package which this package replaces, if any")
- depends := flag.String("depends", "", "comma-separated list of packages this package depends on")
- recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
- flag.Parse()
-
- filesList, err := parseFiles(*regularFiles, files.TypeFile)
- if err != nil {
- log.Fatalf("Parsing --files: %v", err)
- }
- configsList, err := parseFiles(*configFiles, files.TypeConfig)
- if err != nil {
- log.Fatalf("Parsing --configs: %v", err)
- }
- emptyDirList := parseEmptyDirs(*emptyDirs)
- contents := append(filesList, append(configsList, emptyDirList...)...)
- contents, err = files.PrepareForPackager(contents, 0, *pkgType, false)
- if err != nil {
- log.Fatalf("Building package contents: %v", err)
- }
- info := nfpm.WithDefaults(&nfpm.Info{
- Name: *name,
- Arch: *goarch,
- Platform: "linux",
- Version: *version,
- Maintainer: "Tailscale Inc <info@tailscale.com>",
- Description: *description,
- Homepage: "https://www.tailscale.com",
- License: "MIT",
- Overridables: nfpm.Overridables{
- Contents: contents,
- Scripts: nfpm.Scripts{
- PostInstall: *postinst,
- PreRemove: *prerm,
- PostRemove: *postrm,
- },
- },
- })
-
- if len(*depends) != 0 {
- info.Overridables.Depends = strings.Split(*depends, ",")
- }
- if len(*recommends) != 0 {
- info.Overridables.Recommends = strings.Split(*recommends, ",")
- }
- if *replaces != "" {
- info.Overridables.Replaces = []string{*replaces}
- info.Overridables.Conflicts = []string{*replaces}
- }
-
- switch *pkgType {
- case "deb":
- info.Section = "net"
- info.Priority = "extra"
- case "rpm":
- info.Overridables.RPM.Group = "Network"
- }
-
- pkg, err := nfpm.Get(*pkgType)
- if err != nil {
- log.Fatalf("Getting packager for %q: %v", *pkgType, err)
- }
-
- f, err := os.Create(*out)
- if err != nil {
- log.Fatalf("Creating output file %q: %v", *out, err)
- }
- defer f.Close()
-
- if err := pkg.Package(info, f); err != nil {
- log.Fatalf("Creating package %q: %v", *out, err)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// mkpkg builds the Tailscale rpm and deb packages.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+
+ "github.com/goreleaser/nfpm/v2"
+ _ "github.com/goreleaser/nfpm/v2/deb"
+ "github.com/goreleaser/nfpm/v2/files"
+ _ "github.com/goreleaser/nfpm/v2/rpm"
+)
+
+// parseFiles parses a comma-separated list of colon-separated pairs
+// into files.Contents format.
+func parseFiles(s string, typ string) (files.Contents, error) {
+ if len(s) == 0 {
+ return nil, nil
+ }
+ var contents files.Contents
+ for _, f := range strings.Split(s, ",") {
+ fs := strings.Split(f, ":")
+ if len(fs) != 2 {
+ return nil, fmt.Errorf("unparseable file field %q", f)
+ }
+ contents = append(contents, &files.Content{Type: files.TypeFile, Source: fs[0], Destination: fs[1]})
+ }
+ return contents, nil
+}
+
+func parseEmptyDirs(s string) files.Contents {
+ // strings.Split("", ",") would return []string{""}, which is not suitable:
+ // this would create an empty dir record with path "", breaking the package
+ if s == "" {
+ return nil
+ }
+ var contents files.Contents
+ for _, d := range strings.Split(s, ",") {
+ contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
+ }
+ return contents
+}
+
+func main() {
+ out := flag.String("out", "", "output file to write")
+ name := flag.String("name", "tailscale", "package name")
+ description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
+ goarch := flag.String("arch", "amd64", "GOARCH this package is for")
+ pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
+ regularFiles := flag.String("files", "", "comma-separated list of files in src:dst form")
+ configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
+ emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
+ version := flag.String("version", "0.0.0", "version of the package")
+ postinst := flag.String("postinst", "", "debian postinst script path")
+ prerm := flag.String("prerm", "", "debian prerm script path")
+ postrm := flag.String("postrm", "", "debian postrm script path")
+ replaces := flag.String("replaces", "", "package which this package replaces, if any")
+ depends := flag.String("depends", "", "comma-separated list of packages this package depends on")
+ recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
+ flag.Parse()
+
+ filesList, err := parseFiles(*regularFiles, files.TypeFile)
+ if err != nil {
+ log.Fatalf("Parsing --files: %v", err)
+ }
+ configsList, err := parseFiles(*configFiles, files.TypeConfig)
+ if err != nil {
+ log.Fatalf("Parsing --configs: %v", err)
+ }
+ emptyDirList := parseEmptyDirs(*emptyDirs)
+ contents := append(filesList, append(configsList, emptyDirList...)...)
+ contents, err = files.PrepareForPackager(contents, 0, *pkgType, false)
+ if err != nil {
+ log.Fatalf("Building package contents: %v", err)
+ }
+ info := nfpm.WithDefaults(&nfpm.Info{
+ Name: *name,
+ Arch: *goarch,
+ Platform: "linux",
+ Version: *version,
+ Maintainer: "Tailscale Inc <info@tailscale.com>",
+ Description: *description,
+ Homepage: "https://www.tailscale.com",
+ License: "MIT",
+ Overridables: nfpm.Overridables{
+ Contents: contents,
+ Scripts: nfpm.Scripts{
+ PostInstall: *postinst,
+ PreRemove: *prerm,
+ PostRemove: *postrm,
+ },
+ },
+ })
+
+ if len(*depends) != 0 {
+ info.Overridables.Depends = strings.Split(*depends, ",")
+ }
+ if len(*recommends) != 0 {
+ info.Overridables.Recommends = strings.Split(*recommends, ",")
+ }
+ if *replaces != "" {
+ info.Overridables.Replaces = []string{*replaces}
+ info.Overridables.Conflicts = []string{*replaces}
+ }
+
+ switch *pkgType {
+ case "deb":
+ info.Section = "net"
+ info.Priority = "extra"
+ case "rpm":
+ info.Overridables.RPM.Group = "Network"
+ }
+
+ pkg, err := nfpm.Get(*pkgType)
+ if err != nil {
+ log.Fatalf("Getting packager for %q: %v", *pkgType, err)
+ }
+
+ f, err := os.Create(*out)
+ if err != nil {
+ log.Fatalf("Creating output file %q: %v", *out, err)
+ }
+ defer f.Close()
+
+ if err := pkg.Package(info, f); err != nil {
+ log.Fatalf("Creating package %q: %v", *out, err)
+ }
+}
diff --git a/cmd/mkversion/mkversion.go b/cmd/mkversion/mkversion.go
index c8c8bf179..6a6a18a50 100644
--- a/cmd/mkversion/mkversion.go
+++ b/cmd/mkversion/mkversion.go
@@ -1,44 +1,44 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// mkversion gets version info from git and outputs a bunch of shell variables
-// that get used elsewhere in the build system to embed version numbers into
-// binaries.
-package main
-
-import (
- "bufio"
- "bytes"
- "fmt"
- "io"
- "os"
- "time"
-
- "tailscale.com/tailcfg"
- "tailscale.com/version/mkversion"
-)
-
-func main() {
- prefix := ""
- if len(os.Args) > 1 {
- if os.Args[1] == "--export" {
- prefix = "export "
- } else {
- fmt.Println("usage: mkversion [--export|-h|--help]")
- os.Exit(1)
- }
- }
-
- var b bytes.Buffer
- io.WriteString(&b, mkversion.Info().String())
- // Copyright and the client capability are not part of the version
- // information, but similarly used in Xcode builds to embed in the metadata,
- // thus generate them now.
- copyright := fmt.Sprintf("Copyright © %d Tailscale Inc. All Rights Reserved.", time.Now().Year())
- fmt.Fprintf(&b, "VERSION_COPYRIGHT=%q\n", copyright)
- fmt.Fprintf(&b, "VERSION_CAPABILITY=%d\n", tailcfg.CurrentCapabilityVersion)
- s := bufio.NewScanner(&b)
- for s.Scan() {
- fmt.Println(prefix + s.Text())
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// mkversion gets version info from git and outputs a bunch of shell variables
+// that get used elsewhere in the build system to embed version numbers into
+// binaries.
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "tailscale.com/tailcfg"
+ "tailscale.com/version/mkversion"
+)
+
+func main() {
+ prefix := ""
+ if len(os.Args) > 1 {
+ if os.Args[1] == "--export" {
+ prefix = "export "
+ } else {
+ fmt.Println("usage: mkversion [--export|-h|--help]")
+ os.Exit(1)
+ }
+ }
+
+ var b bytes.Buffer
+ io.WriteString(&b, mkversion.Info().String())
+ // Copyright and the client capability are not part of the version
+ // information, but similarly used in Xcode builds to embed in the metadata,
+ // thus generate them now.
+ copyright := fmt.Sprintf("Copyright © %d Tailscale Inc. All Rights Reserved.", time.Now().Year())
+ fmt.Fprintf(&b, "VERSION_COPYRIGHT=%q\n", copyright)
+ fmt.Fprintf(&b, "VERSION_CAPABILITY=%d\n", tailcfg.CurrentCapabilityVersion)
+ s := bufio.NewScanner(&b)
+ for s.Scan() {
+ fmt.Println(prefix + s.Text())
+ }
+}
diff --git a/cmd/nardump/README.md b/cmd/nardump/README.md
index 6fa7fc2f1..6c73ff9b0 100644
--- a/cmd/nardump/README.md
+++ b/cmd/nardump/README.md
@@ -1,7 +1,7 @@
-# nardump
-
-nardump is like nix-store --dump, but in Go, writing a NAR file (tar-like,
-but focused on being reproducible) to stdout or to a hash with the --sri flag.
-
-It lets us calculate the Nix sha256 in shell.nix without the person running
-git-pull-oss.sh having Nix available.
+# nardump
+
+nardump is like nix-store --dump, but in Go, writing a NAR file (tar-like,
+but focused on being reproducible) to stdout or to a hash with the --sri flag.
+
+It lets us calculate the Nix sha256 in shell.nix without the person running
+git-pull-oss.sh having Nix available.
diff --git a/cmd/nardump/nardump.go b/cmd/nardump/nardump.go
index 05be7b65a..241475537 100644
--- a/cmd/nardump/nardump.go
+++ b/cmd/nardump/nardump.go
@@ -1,184 +1,184 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// nardump is like nix-store --dump, but in Go, writing a NAR
-// file (tar-like, but focused on being reproducible) to stdout
-// or to a hash with the --sri flag.
-//
-// It lets us calculate a Nix sha256 without the person running
-// git-pull-oss.sh having Nix available.
-package main
-
-// For the format, see:
-// See https://gist.github.com/jbeda/5c79d2b1434f0018d693
-
-import (
- "bufio"
- "crypto/sha256"
- "encoding/base64"
- "encoding/binary"
- "flag"
- "fmt"
- "io"
- "io/fs"
- "log"
- "os"
- "path"
- "sort"
-)
-
-var sri = flag.Bool("sri", false, "print SRI")
-
-func main() {
- flag.Parse()
- if flag.NArg() != 1 {
- log.Fatal("usage: nardump <dir>")
- }
- arg := flag.Arg(0)
- if err := os.Chdir(arg); err != nil {
- log.Fatal(err)
- }
- if *sri {
- hash := sha256.New()
- if err := writeNAR(hash, os.DirFS(".")); err != nil {
- log.Fatal(err)
- }
- fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
- return
- }
- bw := bufio.NewWriter(os.Stdout)
- if err := writeNAR(bw, os.DirFS(".")); err != nil {
- log.Fatal(err)
- }
- bw.Flush()
-}
-
-// writeNARError is a sentinel panic type that's recovered by writeNAR
-// and converted into the wrapped error.
-type writeNARError struct{ err error }
-
-// narWriter writes NAR files.
-type narWriter struct {
- w io.Writer
- fs fs.FS
-}
-
-// writeNAR writes a NAR file to w from the root of fs.
-func writeNAR(w io.Writer, fs fs.FS) (err error) {
- defer func() {
- if e := recover(); e != nil {
- if we, ok := e.(writeNARError); ok {
- err = we.err
- return
- }
- panic(e)
- }
- }()
- nw := &narWriter{w: w, fs: fs}
- nw.str("nix-archive-1")
- return nw.writeDir(".")
-}
-
-func (nw *narWriter) writeDir(dirPath string) error {
- ents, err := fs.ReadDir(nw.fs, dirPath)
- if err != nil {
- return err
- }
- sort.Slice(ents, func(i, j int) bool {
- return ents[i].Name() < ents[j].Name()
- })
- nw.str("(")
- nw.str("type")
- nw.str("directory")
- for _, ent := range ents {
- nw.str("entry")
- nw.str("(")
- nw.str("name")
- nw.str(ent.Name())
- nw.str("node")
- mode := ent.Type()
- sub := path.Join(dirPath, ent.Name())
- var err error
- switch {
- case mode.IsRegular():
- err = nw.writeRegular(sub)
- case mode.IsDir():
- err = nw.writeDir(sub)
- default:
- // TODO(bradfitz): symlink, but requires fighting io/fs a bit
- // to get at Readlink or the osFS via fs. But for now
- // we don't need symlinks because they're not in Go's archive.
- return fmt.Errorf("unsupported file type %v at %q", sub, mode)
- }
- if err != nil {
- return err
- }
- nw.str(")")
- }
- nw.str(")")
- return nil
-}
-
-func (nw *narWriter) writeRegular(path string) error {
- nw.str("(")
- nw.str("type")
- nw.str("regular")
- fi, err := fs.Stat(nw.fs, path)
- if err != nil {
- return err
- }
- if fi.Mode()&0111 != 0 {
- nw.str("executable")
- nw.str("")
- }
- contents, err := fs.ReadFile(nw.fs, path)
- if err != nil {
- return err
- }
- nw.str("contents")
- if err := writeBytes(nw.w, contents); err != nil {
- return err
- }
- nw.str(")")
- return nil
-}
-
-func (nw *narWriter) str(s string) {
- if err := writeString(nw.w, s); err != nil {
- panic(writeNARError{err})
- }
-}
-
-func writeString(w io.Writer, s string) error {
- var buf [8]byte
- binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
- if _, err := w.Write(buf[:]); err != nil {
- return err
- }
- if _, err := io.WriteString(w, s); err != nil {
- return err
- }
- return writePad(w, len(s))
-}
-
-func writeBytes(w io.Writer, b []byte) error {
- var buf [8]byte
- binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
- if _, err := w.Write(buf[:]); err != nil {
- return err
- }
- if _, err := w.Write(b); err != nil {
- return err
- }
- return writePad(w, len(b))
-}
-
-func writePad(w io.Writer, n int) error {
- pad := n % 8
- if pad == 0 {
- return nil
- }
- var zeroes [8]byte
- _, err := w.Write(zeroes[:8-pad])
- return err
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// nardump is like nix-store --dump, but in Go, writing a NAR
+// file (tar-like, but focused on being reproducible) to stdout
+// or to a hash with the --sri flag.
+//
+// It lets us calculate a Nix sha256 without the person running
+// git-pull-oss.sh having Nix available.
+package main
+
+// For the format, see:
+// See https://gist.github.com/jbeda/5c79d2b1434f0018d693
+
+import (
+ "bufio"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/binary"
+ "flag"
+ "fmt"
+ "io"
+ "io/fs"
+ "log"
+ "os"
+ "path"
+ "sort"
+)
+
+var sri = flag.Bool("sri", false, "print SRI")
+
+func main() {
+ flag.Parse()
+ if flag.NArg() != 1 {
+ log.Fatal("usage: nardump <dir>")
+ }
+ arg := flag.Arg(0)
+ if err := os.Chdir(arg); err != nil {
+ log.Fatal(err)
+ }
+ if *sri {
+ hash := sha256.New()
+ if err := writeNAR(hash, os.DirFS(".")); err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
+ return
+ }
+ bw := bufio.NewWriter(os.Stdout)
+ if err := writeNAR(bw, os.DirFS(".")); err != nil {
+ log.Fatal(err)
+ }
+ bw.Flush()
+}
+
+// writeNARError is a sentinel panic type that's recovered by writeNAR
+// and converted into the wrapped error.
+type writeNARError struct{ err error }
+
+// narWriter writes NAR files.
+type narWriter struct {
+ w io.Writer
+ fs fs.FS
+}
+
+// writeNAR writes a NAR file to w from the root of fs.
+func writeNAR(w io.Writer, fs fs.FS) (err error) {
+ defer func() {
+ if e := recover(); e != nil {
+ if we, ok := e.(writeNARError); ok {
+ err = we.err
+ return
+ }
+ panic(e)
+ }
+ }()
+ nw := &narWriter{w: w, fs: fs}
+ nw.str("nix-archive-1")
+ return nw.writeDir(".")
+}
+
+func (nw *narWriter) writeDir(dirPath string) error {
+ ents, err := fs.ReadDir(nw.fs, dirPath)
+ if err != nil {
+ return err
+ }
+ sort.Slice(ents, func(i, j int) bool {
+ return ents[i].Name() < ents[j].Name()
+ })
+ nw.str("(")
+ nw.str("type")
+ nw.str("directory")
+ for _, ent := range ents {
+ nw.str("entry")
+ nw.str("(")
+ nw.str("name")
+ nw.str(ent.Name())
+ nw.str("node")
+ mode := ent.Type()
+ sub := path.Join(dirPath, ent.Name())
+ var err error
+ switch {
+ case mode.IsRegular():
+ err = nw.writeRegular(sub)
+ case mode.IsDir():
+ err = nw.writeDir(sub)
+ default:
+ // TODO(bradfitz): symlink, but requires fighting io/fs a bit
+ // to get at Readlink or the osFS via fs. But for now
+ // we don't need symlinks because they're not in Go's archive.
+ return fmt.Errorf("unsupported file type %v at %q", sub, mode)
+ }
+ if err != nil {
+ return err
+ }
+ nw.str(")")
+ }
+ nw.str(")")
+ return nil
+}
+
+func (nw *narWriter) writeRegular(path string) error {
+ nw.str("(")
+ nw.str("type")
+ nw.str("regular")
+ fi, err := fs.Stat(nw.fs, path)
+ if err != nil {
+ return err
+ }
+ if fi.Mode()&0111 != 0 {
+ nw.str("executable")
+ nw.str("")
+ }
+ contents, err := fs.ReadFile(nw.fs, path)
+ if err != nil {
+ return err
+ }
+ nw.str("contents")
+ if err := writeBytes(nw.w, contents); err != nil {
+ return err
+ }
+ nw.str(")")
+ return nil
+}
+
+func (nw *narWriter) str(s string) {
+ if err := writeString(nw.w, s); err != nil {
+ panic(writeNARError{err})
+ }
+}
+
+func writeString(w io.Writer, s string) error {
+ var buf [8]byte
+ binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ if _, err := io.WriteString(w, s); err != nil {
+ return err
+ }
+ return writePad(w, len(s))
+}
+
+func writeBytes(w io.Writer, b []byte) error {
+ var buf [8]byte
+ binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ if _, err := w.Write(b); err != nil {
+ return err
+ }
+ return writePad(w, len(b))
+}
+
+func writePad(w io.Writer, n int) error {
+ pad := n % 8
+ if pad == 0 {
+ return nil
+ }
+ var zeroes [8]byte
+ _, err := w.Write(zeroes[:8-pad])
+ return err
+}
diff --git a/cmd/nginx-auth/.gitignore b/cmd/nginx-auth/.gitignore
index 3c608aeb1..255276578 100644
--- a/cmd/nginx-auth/.gitignore
+++ b/cmd/nginx-auth/.gitignore
@@ -1,4 +1,4 @@
-nga.sock
-*.deb
-*.rpm
-tailscale.nginx-auth
+nga.sock
+*.deb
+*.rpm
+tailscale.nginx-auth
diff --git a/cmd/nginx-auth/README.md b/cmd/nginx-auth/README.md
index 858f9ab81..869b1487b 100644
--- a/cmd/nginx-auth/README.md
+++ b/cmd/nginx-auth/README.md
@@ -1,161 +1,161 @@
-# nginx-auth
-
-[![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental)
-
-This is a tool that allows users to use Tailscale Whois authentication with
-NGINX as a reverse proxy. This allows users that already have a bunch of
-services hosted on an internal NGINX server to point those domains to the
-Tailscale IP of the NGINX server and then seamlessly use Tailscale for
-authentication.
-
-Many thanks to [@zrail](https://twitter.com/zrail/status/1511788463586222087) on
-Twitter for introducing the basic idea and offering some sample code. This
-program is based on that sample code with security enhancements. Namely:
-
-* This listens over a UNIX socket instead of a TCP socket, to prevent
- leakage to the network
-* This uses systemd socket activation so that systemd owns the socket
- and can then lock down the service to the bare minimum required to do
- its job without having to worry about dropping permissions
-* This provides additional information in HTTP response headers that can
- be useful for integrating with various services
-
-## Configuration
-
-In order to protect a service with this tool, do the following in the respective
-`server` block:
-
-Create an authentication location with the `internal` flag set:
-
-```nginx
-location /auth {
- internal;
-
- proxy_pass http://unix:/run/tailscale.nginx-auth.sock;
- proxy_pass_request_body off;
-
- proxy_set_header Host $http_host;
- proxy_set_header Remote-Addr $remote_addr;
- proxy_set_header Remote-Port $remote_port;
- proxy_set_header Original-URI $request_uri;
-}
-```
-
-Then add the following to the `location /` block:
-
-```
-auth_request /auth;
-auth_request_set $auth_user $upstream_http_tailscale_user;
-auth_request_set $auth_name $upstream_http_tailscale_name;
-auth_request_set $auth_login $upstream_http_tailscale_login;
-auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
-auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
-
-proxy_set_header X-Webauth-User "$auth_user";
-proxy_set_header X-Webauth-Name "$auth_name";
-proxy_set_header X-Webauth-Login "$auth_login";
-proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
-proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
-```
-
-When this configuration is used with a Go HTTP handler such as this:
-
-```go
-http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
- e := json.NewEncoder(w)
- e.SetIndent("", " ")
- e.Encode(r.Header)
-})
-```
-
-You will get output like this:
-
-```json
-{
- "Accept": [
- "*/*"
- ],
- "Connection": [
- "upgrade"
- ],
- "User-Agent": [
- "curl/7.82.0"
- ],
- "X-Webauth-Login": [
- "Xe"
- ],
- "X-Webauth-Name": [
- "Xe Iaso"
- ],
- "X-Webauth-Profile-Picture": [
- "https://avatars.githubusercontent.com/u/529003?v=4"
- ],
- "X-Webauth-Tailnet": [
- "cetacean.org.github"
- ]
- "X-Webauth-User": [
- "Xe@github"
- ]
-}
-```
-
-## Headers
-
-The authentication service provides the following headers to decorate your
-proxied requests:
-
-| Header | Example Value | Description |
-| :------ | :-------------- | :---------- |
-| `Tailscale-User` | `azurediamond@hunter2.net` | The Tailscale username the remote machine is logged in as in user@host form |
-| `Tailscale-Login` | `azurediamond` | The user portion of the Tailscale username the remote machine is logged in as |
-| `Tailscale-Name` | `Azure Diamond` | The "real name" of the Tailscale user the machine is logged in as |
-| `Tailscale-Profile-Picture` | `https://i.kym-cdn.com/photos/images/newsfeed/001/065/963/ae0.png` | The profile picture provided by the Identity Provider your tailnet uses |
-| `Tailscale-Tailnet` | `hunter2.net` | The tailnet name |
-
-Most of the time you can set `X-Webauth-User` to the contents of the
-`Tailscale-User` header, but some services may not accept a username with an `@`
-symbol in it. If this is the case, set `X-Webauth-User` to the `Tailscale-Login`
-header.
-
-The `Tailscale-Tailnet` header can help you identify which tailnet the session
-is coming from. If you are using node sharing, this can help you make sure that
-you aren't giving administrative access to people outside your tailnet.
-
-### Allow Requests From Only One Tailnet
-
-If you want to prevent node sharing from allowing users to access a service, add
-the `Expected-Tailnet` header to your auth request:
-
-```nginx
-location /auth {
- # ...
- proxy_set_header Expected-Tailnet "tailnet012345.ts.net";
-}
-```
-
-If a user from a different tailnet tries to use that service, this will return a
-generic "forbidden" error page:
-
-```html
-<html>
-<head><title>403 Forbidden</title></head>
-<body>
-<center><h1>403 Forbidden</h1></center>
-<hr><center>nginx/1.18.0 (Ubuntu)</center>
-</body>
-</html>
-```
-
-You can get the tailnet name from [the admin panel](https://login.tailscale.com/admin/dns).
-
-## Building
-
-Install `cmd/mkpkg`:
-
-```
-cd .. && go install ./mkpkg
-```
-
-Then run `./mkdeb.sh`. It will emit a `.deb` and `.rpm` package for amd64
-machines (Linux uname flag: `x86_64`). You can add these to your deployment
-methods as you see fit.
+# nginx-auth
+
+[![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental)
+
+This is a tool that allows users to use Tailscale Whois authentication with
+NGINX as a reverse proxy. This allows users that already have a bunch of
+services hosted on an internal NGINX server to point those domains to the
+Tailscale IP of the NGINX server and then seamlessly use Tailscale for
+authentication.
+
+Many thanks to [@zrail](https://twitter.com/zrail/status/1511788463586222087) on
+Twitter for introducing the basic idea and offering some sample code. This
+program is based on that sample code with security enhancements. Namely:
+
+* This listens over a UNIX socket instead of a TCP socket, to prevent
+ leakage to the network
+* This uses systemd socket activation so that systemd owns the socket
+ and can then lock down the service to the bare minimum required to do
+ its job without having to worry about dropping permissions
+* This provides additional information in HTTP response headers that can
+ be useful for integrating with various services
+
+## Configuration
+
+In order to protect a service with this tool, do the following in the respective
+`server` block:
+
+Create an authentication location with the `internal` flag set:
+
+```nginx
+location /auth {
+ internal;
+
+ proxy_pass http://unix:/run/tailscale.nginx-auth.sock;
+ proxy_pass_request_body off;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header Remote-Addr $remote_addr;
+ proxy_set_header Remote-Port $remote_port;
+ proxy_set_header Original-URI $request_uri;
+}
+```
+
+Then add the following to the `location /` block:
+
+```
+auth_request /auth;
+auth_request_set $auth_user $upstream_http_tailscale_user;
+auth_request_set $auth_name $upstream_http_tailscale_name;
+auth_request_set $auth_login $upstream_http_tailscale_login;
+auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
+auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
+
+proxy_set_header X-Webauth-User "$auth_user";
+proxy_set_header X-Webauth-Name "$auth_name";
+proxy_set_header X-Webauth-Login "$auth_login";
+proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
+proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
+```
+
+When this configuration is used with a Go HTTP handler such as this:
+
+```go
+http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
+ e := json.NewEncoder(w)
+ e.SetIndent("", " ")
+ e.Encode(r.Header)
+})
+```
+
+You will get output like this:
+
+```json
+{
+ "Accept": [
+ "*/*"
+ ],
+ "Connection": [
+ "upgrade"
+ ],
+ "User-Agent": [
+ "curl/7.82.0"
+ ],
+ "X-Webauth-Login": [
+ "Xe"
+ ],
+ "X-Webauth-Name": [
+ "Xe Iaso"
+ ],
+ "X-Webauth-Profile-Picture": [
+ "https://avatars.githubusercontent.com/u/529003?v=4"
+ ],
+ "X-Webauth-Tailnet": [
+ "cetacean.org.github"
+ ]
+ "X-Webauth-User": [
+ "Xe@github"
+ ]
+}
+```
+
+## Headers
+
+The authentication service provides the following headers to decorate your
+proxied requests:
+
+| Header | Example Value | Description |
+| :------ | :-------------- | :---------- |
+| `Tailscale-User` | `azurediamond@hunter2.net` | The Tailscale username the remote machine is logged in as in user@host form |
+| `Tailscale-Login` | `azurediamond` | The user portion of the Tailscale username the remote machine is logged in as |
+| `Tailscale-Name` | `Azure Diamond` | The "real name" of the Tailscale user the machine is logged in as |
+| `Tailscale-Profile-Picture` | `https://i.kym-cdn.com/photos/images/newsfeed/001/065/963/ae0.png` | The profile picture provided by the Identity Provider your tailnet uses |
+| `Tailscale-Tailnet` | `hunter2.net` | The tailnet name |
+
+Most of the time you can set `X-Webauth-User` to the contents of the
+`Tailscale-User` header, but some services may not accept a username with an `@`
+symbol in it. If this is the case, set `X-Webauth-User` to the `Tailscale-Login`
+header.
+
+The `Tailscale-Tailnet` header can help you identify which tailnet the session
+is coming from. If you are using node sharing, this can help you make sure that
+you aren't giving administrative access to people outside your tailnet.
+
+### Allow Requests From Only One Tailnet
+
+If you want to prevent node sharing from allowing users to access a service, add
+the `Expected-Tailnet` header to your auth request:
+
+```nginx
+location /auth {
+ # ...
+ proxy_set_header Expected-Tailnet "tailnet012345.ts.net";
+}
+```
+
+If a user from a different tailnet tries to use that service, this will return a
+generic "forbidden" error page:
+
+```html
+<html>
+<head><title>403 Forbidden</title></head>
+<body>
+<center><h1>403 Forbidden</h1></center>
+<hr><center>nginx/1.18.0 (Ubuntu)</center>
+</body>
+</html>
+```
+
+You can get the tailnet name from [the admin panel](https://login.tailscale.com/admin/dns).
+
+## Building
+
+Install `cmd/mkpkg`:
+
+```
+cd .. && go install ./mkpkg
+```
+
+Then run `./mkdeb.sh`. It will emit a `.deb` and `.rpm` package for amd64
+machines (Linux uname flag: `x86_64`). You can add these to your deployment
+methods as you see fit.
diff --git a/cmd/nginx-auth/deb/postinst.sh b/cmd/nginx-auth/deb/postinst.sh
index d352a8488..e692ced07 100755
--- a/cmd/nginx-auth/deb/postinst.sh
+++ b/cmd/nginx-auth/deb/postinst.sh
@@ -1,14 +1,14 @@
-if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
- deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
- if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
- deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
- else
- deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
- fi
-
- if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
- systemctl --system daemon-reload >/dev/null || true
- deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
- deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
- fi
-fi
+if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
+ deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
+ if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
+ deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
+ else
+ deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
+ fi
+
+ if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
+ systemctl --system daemon-reload >/dev/null || true
+ deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
+ deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
+ fi
+fi
diff --git a/cmd/nginx-auth/deb/postrm.sh b/cmd/nginx-auth/deb/postrm.sh
index 4bce86139..7870efd18 100755
--- a/cmd/nginx-auth/deb/postrm.sh
+++ b/cmd/nginx-auth/deb/postrm.sh
@@ -1,19 +1,19 @@
-#!/bin/sh
-set -e
-if [ -d /run/systemd/system ] ; then
- systemctl --system daemon-reload >/dev/null || true
-fi
-
-if [ -x "/usr/bin/deb-systemd-helper" ]; then
- if [ "$1" = "remove" ]; then
- deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
- deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
- fi
-
- if [ "$1" = "purge" ]; then
- deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
- deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
- deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
- deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
- fi
-fi
+#!/bin/sh
+set -e
+if [ -d /run/systemd/system ] ; then
+ systemctl --system daemon-reload >/dev/null || true
+fi
+
+if [ -x "/usr/bin/deb-systemd-helper" ]; then
+ if [ "$1" = "remove" ]; then
+ deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
+ deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
+ fi
+
+ if [ "$1" = "purge" ]; then
+ deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
+ deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
+ deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
+ deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
+ fi
+fi
diff --git a/cmd/nginx-auth/deb/prerm.sh b/cmd/nginx-auth/deb/prerm.sh
index e4becd170..22be23387 100755
--- a/cmd/nginx-auth/deb/prerm.sh
+++ b/cmd/nginx-auth/deb/prerm.sh
@@ -1,8 +1,8 @@
-#!/bin/sh
-set -e
-if [ "$1" = "remove" ]; then
- if [ -d /run/systemd/system ]; then
- deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
- deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
- fi
-fi
+#!/bin/sh
+set -e
+if [ "$1" = "remove" ]; then
+ if [ -d /run/systemd/system ]; then
+ deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
+ deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
+ fi
+fi
diff --git a/cmd/nginx-auth/mkdeb.sh b/cmd/nginx-auth/mkdeb.sh
index 59f43230d..6a5721093 100755
--- a/cmd/nginx-auth/mkdeb.sh
+++ b/cmd/nginx-auth/mkdeb.sh
@@ -1,32 +1,32 @@
-#!/usr/bin/env bash
-
-set -e
-
-VERSION=0.1.3
-for ARCH in amd64 arm64; do
- CGO_ENABLED=0 GOARCH=${ARCH} GOOS=linux go build -o tailscale.nginx-auth .
-
- mkpkg \
- --out=tailscale-nginx-auth-${VERSION}-${ARCH}.deb \
- --name=tailscale-nginx-auth \
- --version=${VERSION} \
- --type=deb \
- --arch=${ARCH} \
- --postinst=deb/postinst.sh \
- --postrm=deb/postrm.sh \
- --prerm=deb/prerm.sh \
- --description="Tailscale NGINX authentication protocol handler" \
- --files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
-
- mkpkg \
- --out=tailscale-nginx-auth-${VERSION}-${ARCH}.rpm \
- --name=tailscale-nginx-auth \
- --version=${VERSION} \
- --type=rpm \
- --arch=${ARCH} \
- --postinst=rpm/postinst.sh \
- --postrm=rpm/postrm.sh \
- --prerm=rpm/prerm.sh \
- --description="Tailscale NGINX authentication protocol handler" \
- --files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
-done
+#!/usr/bin/env bash
+
+set -e
+
+VERSION=0.1.3
+for ARCH in amd64 arm64; do
+ CGO_ENABLED=0 GOARCH=${ARCH} GOOS=linux go build -o tailscale.nginx-auth .
+
+ mkpkg \
+ --out=tailscale-nginx-auth-${VERSION}-${ARCH}.deb \
+ --name=tailscale-nginx-auth \
+ --version=${VERSION} \
+ --type=deb \
+ --arch=${ARCH} \
+ --postinst=deb/postinst.sh \
+ --postrm=deb/postrm.sh \
+ --prerm=deb/prerm.sh \
+ --description="Tailscale NGINX authentication protocol handler" \
+ --files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
+
+ mkpkg \
+ --out=tailscale-nginx-auth-${VERSION}-${ARCH}.rpm \
+ --name=tailscale-nginx-auth \
+ --version=${VERSION} \
+ --type=rpm \
+ --arch=${ARCH} \
+ --postinst=rpm/postinst.sh \
+ --postrm=rpm/postrm.sh \
+ --prerm=rpm/prerm.sh \
+ --description="Tailscale NGINX authentication protocol handler" \
+ --files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
+done
diff --git a/cmd/nginx-auth/nginx-auth.go b/cmd/nginx-auth/nginx-auth.go
index 09da74da1..befcb6d6c 100644
--- a/cmd/nginx-auth/nginx-auth.go
+++ b/cmd/nginx-auth/nginx-auth.go
@@ -1,128 +1,128 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux
-
-// Command nginx-auth is a tool that allows users to use Tailscale Whois
-// authentication with NGINX as a reverse proxy. This allows users that
-// already have a bunch of services hosted on an internal NGINX server
-// to point those domains to the Tailscale IP of the NGINX server and
-// then seamlessly use Tailscale for authentication.
-package main
-
-import (
- "flag"
- "log"
- "net"
- "net/http"
- "net/netip"
- "net/url"
- "os"
- "strings"
-
- "github.com/coreos/go-systemd/activation"
- "tailscale.com/client/tailscale"
-)
-
-var (
- sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
-)
-
-func main() {
- flag.Parse()
-
- mux := http.NewServeMux()
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- remoteHost := r.Header.Get("Remote-Addr")
- remotePort := r.Header.Get("Remote-Port")
- if remoteHost == "" || remotePort == "" {
- w.WriteHeader(http.StatusBadRequest)
- log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
- return
- }
-
- remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
- remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
- if err != nil {
- w.WriteHeader(http.StatusUnauthorized)
- log.Printf("remote address and port are not valid: %v", err)
- return
- }
-
- info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
- if err != nil {
- w.WriteHeader(http.StatusUnauthorized)
- log.Printf("can't look up %s: %v", remoteAddr, err)
- return
- }
-
- if info.Node.IsTagged() {
- w.WriteHeader(http.StatusForbidden)
- log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
- return
- }
-
- // tailnet of connected node. When accessing shared nodes, this
- // will be empty because the tailnet of the sharee is not exposed.
- var tailnet string
-
- if !info.Node.Hostinfo.ShareeNode() {
- var ok bool
- _, tailnet, ok = strings.Cut(info.Node.Name, info.Node.ComputedName+".")
- if !ok {
- w.WriteHeader(http.StatusUnauthorized)
- log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
- return
- }
- tailnet = strings.TrimSuffix(tailnet, ".beta.tailscale.net")
- }
-
- if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
- w.WriteHeader(http.StatusForbidden)
- log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
- return
- }
-
- h := w.Header()
- h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
- h.Set("Tailscale-User", info.UserProfile.LoginName)
- h.Set("Tailscale-Name", info.UserProfile.DisplayName)
- h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
- h.Set("Tailscale-Tailnet", tailnet)
- w.WriteHeader(http.StatusNoContent)
- })
-
- if *sockPath != "" {
- _ = os.Remove(*sockPath) // ignore error, this file may not already exist
- ln, err := net.Listen("unix", *sockPath)
- if err != nil {
- log.Fatalf("can't listen on %s: %v", *sockPath, err)
- }
- defer ln.Close()
-
- log.Printf("listening on %s", *sockPath)
- log.Fatal(http.Serve(ln, mux))
- }
-
- listeners, err := activation.Listeners()
- if err != nil {
- log.Fatalf("no sockets passed to this service with systemd: %v", err)
- }
-
- // NOTE(Xe): normally you'd want to make a waitgroup here and then register
- // each listener with it. In this case I want this to blow up horribly if
- // any of the listeners stop working. systemd will restart it due to the
- // socket activation at play.
- //
- // TL;DR: Let it crash, it will come back
- for _, ln := range listeners {
- go func(ln net.Listener) {
- log.Printf("listening on %s", ln.Addr())
- log.Fatal(http.Serve(ln, mux))
- }(ln)
- }
-
- for {
- select {}
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+// Command nginx-auth is a tool that allows users to use Tailscale Whois
+// authentication with NGINX as a reverse proxy. This allows users that
+// already have a bunch of services hosted on an internal NGINX server
+// to point those domains to the Tailscale IP of the NGINX server and
+// then seamlessly use Tailscale for authentication.
+package main
+
+import (
+ "flag"
+ "log"
+ "net"
+ "net/http"
+ "net/netip"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/coreos/go-systemd/activation"
+ "tailscale.com/client/tailscale"
+)
+
+var (
+ sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
+)
+
+func main() {
+ flag.Parse()
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ remoteHost := r.Header.Get("Remote-Addr")
+ remotePort := r.Header.Get("Remote-Port")
+ if remoteHost == "" || remotePort == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
+ return
+ }
+
+ remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
+ remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("remote address and port are not valid: %v", err)
+ return
+ }
+
+ info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("can't look up %s: %v", remoteAddr, err)
+ return
+ }
+
+ if info.Node.IsTagged() {
+ w.WriteHeader(http.StatusForbidden)
+ log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
+ return
+ }
+
+ // tailnet of connected node. When accessing shared nodes, this
+ // will be empty because the tailnet of the sharee is not exposed.
+ var tailnet string
+
+ if !info.Node.Hostinfo.ShareeNode() {
+ var ok bool
+ _, tailnet, ok = strings.Cut(info.Node.Name, info.Node.ComputedName+".")
+ if !ok {
+ w.WriteHeader(http.StatusUnauthorized)
+ log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
+ return
+ }
+ tailnet = strings.TrimSuffix(tailnet, ".beta.tailscale.net")
+ }
+
+ if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
+ w.WriteHeader(http.StatusForbidden)
+ log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
+ return
+ }
+
+ h := w.Header()
+ h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
+ h.Set("Tailscale-User", info.UserProfile.LoginName)
+ h.Set("Tailscale-Name", info.UserProfile.DisplayName)
+ h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
+ h.Set("Tailscale-Tailnet", tailnet)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ if *sockPath != "" {
+ _ = os.Remove(*sockPath) // ignore error, this file may not already exist
+ ln, err := net.Listen("unix", *sockPath)
+ if err != nil {
+ log.Fatalf("can't listen on %s: %v", *sockPath, err)
+ }
+ defer ln.Close()
+
+ log.Printf("listening on %s", *sockPath)
+ log.Fatal(http.Serve(ln, mux))
+ }
+
+ listeners, err := activation.Listeners()
+ if err != nil {
+ log.Fatalf("no sockets passed to this service with systemd: %v", err)
+ }
+
+ // NOTE(Xe): normally you'd want to make a waitgroup here and then register
+ // each listener with it. In this case I want this to blow up horribly if
+ // any of the listeners stop working. systemd will restart it due to the
+ // socket activation at play.
+ //
+ // TL;DR: Let it crash, it will come back
+ for _, ln := range listeners {
+ go func(ln net.Listener) {
+ log.Printf("listening on %s", ln.Addr())
+ log.Fatal(http.Serve(ln, mux))
+ }(ln)
+ }
+
+ for {
+ select {}
+ }
+}
diff --git a/cmd/nginx-auth/rpm/postrm.sh b/cmd/nginx-auth/rpm/postrm.sh
index 3d0abfb19..d8d36893f 100755
--- a/cmd/nginx-auth/rpm/postrm.sh
+++ b/cmd/nginx-auth/rpm/postrm.sh
@@ -1,9 +1,9 @@
-# $1 == 0 for uninstallation.
-# $1 == 1 for removing old package during upgrade.
-
-systemctl daemon-reload >/dev/null 2>&1 || :
-if [ $1 -ge 1 ] ; then
- # Package upgrade, not uninstall
- systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
- systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
-fi
+# $1 == 0 for uninstallation.
+# $1 == 1 for removing old package during upgrade.
+
+systemctl daemon-reload >/dev/null 2>&1 || :
+if [ $1 -ge 1 ] ; then
+ # Package upgrade, not uninstall
+ systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
+ systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
+fi
diff --git a/cmd/nginx-auth/rpm/prerm.sh b/cmd/nginx-auth/rpm/prerm.sh
index 1f198d829..2e47a53ed 100755
--- a/cmd/nginx-auth/rpm/prerm.sh
+++ b/cmd/nginx-auth/rpm/prerm.sh
@@ -1,9 +1,9 @@
-# $1 == 0 for uninstallation.
-# $1 == 1 for removing old package during upgrade.
-
-if [ $1 -eq 0 ] ; then
- # Package removal, not upgrade
- systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
- systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
- systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
-fi
+# $1 == 0 for uninstallation.
+# $1 == 1 for removing old package during upgrade.
+
+if [ $1 -eq 0 ] ; then
+ # Package removal, not upgrade
+ systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
+ systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
+ systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
+fi
diff --git a/cmd/nginx-auth/tailscale.nginx-auth.service b/cmd/nginx-auth/tailscale.nginx-auth.service
index 086f6c774..8534e25c1 100644
--- a/cmd/nginx-auth/tailscale.nginx-auth.service
+++ b/cmd/nginx-auth/tailscale.nginx-auth.service
@@ -1,11 +1,11 @@
-[Unit]
-Description=Tailscale NGINX Authentication service
-After=nginx.service
-Wants=nginx.service
-
-[Service]
-ExecStart=/usr/sbin/tailscale.nginx-auth
-DynamicUser=yes
-
-[Install]
-WantedBy=default.target
+[Unit]
+Description=Tailscale NGINX Authentication service
+After=nginx.service
+Wants=nginx.service
+
+[Service]
+ExecStart=/usr/sbin/tailscale.nginx-auth
+DynamicUser=yes
+
+[Install]
+WantedBy=default.target
diff --git a/cmd/nginx-auth/tailscale.nginx-auth.socket b/cmd/nginx-auth/tailscale.nginx-auth.socket
index 7e5641ff3..53e3e8d83 100644
--- a/cmd/nginx-auth/tailscale.nginx-auth.socket
+++ b/cmd/nginx-auth/tailscale.nginx-auth.socket
@@ -1,9 +1,9 @@
-[Unit]
-Description=Tailscale NGINX Authentication socket
-PartOf=tailscale.nginx-auth.service
-
-[Socket]
-ListenStream=/var/run/tailscale.nginx-auth.sock
-
-[Install]
+[Unit]
+Description=Tailscale NGINX Authentication socket
+PartOf=tailscale.nginx-auth.service
+
+[Socket]
+ListenStream=/var/run/tailscale.nginx-auth.sock
+
+[Install]
WantedBy=sockets.target \ No newline at end of file
diff --git a/cmd/pgproxy/README.md b/cmd/pgproxy/README.md
index 2e013072a..a867ad8ca 100644
--- a/cmd/pgproxy/README.md
+++ b/cmd/pgproxy/README.md
@@ -1,42 +1,42 @@
-# pgproxy
-
-The pgproxy server is a proxy for the Postgres wire protocol. [Read
-more in our blog
-post](https://tailscale.com/blog/introducing-pgproxy/) about it!
-
-The proxy runs an in-process Tailscale instance, accepts postgres
-client connections over Tailscale only, and proxies them to the
-configured upstream postgres server.
-
-This proxy exists because postgres clients default to very insecure
-connection settings: either they "prefer" but do not require TLS; or
-they set sslmode=require, which merely requires that a TLS handshake
-took place, but don't verify the server's TLS certificate or the
-presented TLS hostname. In other words, sslmode=require enforces that
-a TLS session is created, but that session can trivially be
-machine-in-the-middled to steal credentials, data, inject malicious
-queries, and so forth.
-
-Because this flaw is in the client's validation of the TLS session,
-you have no way of reliably detecting the misconfiguration
-server-side. You could fix the configuration of all the clients you
-know of, but the default makes it very easy to accidentally regress.
-
-Instead of trying to verify client configuration over time, this proxy
-removes the need for postgres clients to be configured correctly: the
-upstream database is configured to only accept connections from the
-proxy, and the proxy is only available to clients over Tailscale.
-
-Therefore, clients must use the proxy to connect to the database. The
-client<>proxy connection is secured end-to-end by Tailscale, which the
-proxy enforces by verifying that the connecting client is a known
-current Tailscale peer. The proxy<>server connection is established by
-the proxy itself, using strict TLS verification settings, and the
-client is only allowed to communicate with the server once we've
-established that the upstream connection is safe to use.
-
-A couple side benefits: because clients can only connect via
-Tailscale, you can use Tailscale ACLs as an extra layer of defense on
-top of the postgres user/password authentication. And, the proxy can
-maintain an audit log of who connected to the database, complete with
-the strongly authenticated Tailscale identity of the client.
+# pgproxy
+
+The pgproxy server is a proxy for the Postgres wire protocol. [Read
+more in our blog
+post](https://tailscale.com/blog/introducing-pgproxy/) about it!
+
+The proxy runs an in-process Tailscale instance, accepts postgres
+client connections over Tailscale only, and proxies them to the
+configured upstream postgres server.
+
+This proxy exists because postgres clients default to very insecure
+connection settings: either they "prefer" but do not require TLS; or
+they set sslmode=require, which merely requires that a TLS handshake
+took place, but don't verify the server's TLS certificate or the
+presented TLS hostname. In other words, sslmode=require enforces that
+a TLS session is created, but that session can trivially be
+machine-in-the-middled to steal credentials, data, inject malicious
+queries, and so forth.
+
+Because this flaw is in the client's validation of the TLS session,
+you have no way of reliably detecting the misconfiguration
+server-side. You could fix the configuration of all the clients you
+know of, but the default makes it very easy to accidentally regress.
+
+Instead of trying to verify client configuration over time, this proxy
+removes the need for postgres clients to be configured correctly: the
+upstream database is configured to only accept connections from the
+proxy, and the proxy is only available to clients over Tailscale.
+
+Therefore, clients must use the proxy to connect to the database. The
+client<>proxy connection is secured end-to-end by Tailscale, which the
+proxy enforces by verifying that the connecting client is a known
+current Tailscale peer. The proxy<>server connection is established by
+the proxy itself, using strict TLS verification settings, and the
+client is only allowed to communicate with the server once we've
+established that the upstream connection is safe to use.
+
+A couple side benefits: because clients can only connect via
+Tailscale, you can use Tailscale ACLs as an extra layer of defense on
+top of the postgres user/password authentication. And, the proxy can
+maintain an audit log of who connected to the database, complete with
+the strongly authenticated Tailscale identity of the client.
diff --git a/cmd/printdep/printdep.go b/cmd/printdep/printdep.go
index 044283209..0790a8b81 100644
--- a/cmd/printdep/printdep.go
+++ b/cmd/printdep/printdep.go
@@ -1,41 +1,41 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The printdep command is a build system tool for printing out information
-// about dependencies.
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "runtime"
- "strings"
-
- ts "tailscale.com"
-)
-
-var (
- goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
- goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
- alpine = flag.Bool("alpine", false, "print the tag of alpine docker image")
-)
-
-func main() {
- flag.Parse()
- if *alpine {
- fmt.Println(strings.TrimSpace(ts.AlpineDockerTag))
- return
- }
- if *goToolchain {
- fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
- }
- if *goToolchainURL {
- switch runtime.GOOS {
- case "linux", "darwin":
- default:
- log.Fatalf("unsupported GOOS %q", runtime.GOOS)
- }
- fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s-%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, runtime.GOARCH)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The printdep command is a build system tool for printing out information
+// about dependencies.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "runtime"
+ "strings"
+
+ ts "tailscale.com"
+)
+
+var (
+ goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
+ goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
+ alpine = flag.Bool("alpine", false, "print the tag of alpine docker image")
+)
+
+func main() {
+ flag.Parse()
+ if *alpine {
+ fmt.Println(strings.TrimSpace(ts.AlpineDockerTag))
+ return
+ }
+ if *goToolchain {
+ fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
+ }
+ if *goToolchainURL {
+ switch runtime.GOOS {
+ case "linux", "darwin":
+ default:
+ log.Fatalf("unsupported GOOS %q", runtime.GOOS)
+ }
+ fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s-%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, runtime.GOARCH)
+ }
+}
diff --git a/cmd/sniproxy/.gitignore b/cmd/sniproxy/.gitignore
index b1399c881..0bca33912 100644
--- a/cmd/sniproxy/.gitignore
+++ b/cmd/sniproxy/.gitignore
@@ -1 +1 @@
-sniproxy
+sniproxy
diff --git a/cmd/sniproxy/handlers_test.go b/cmd/sniproxy/handlers_test.go
index 4f9fc6a34..8ec5b097c 100644
--- a/cmd/sniproxy/handlers_test.go
+++ b/cmd/sniproxy/handlers_test.go
@@ -1,159 +1,159 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "bytes"
- "context"
- "encoding/hex"
- "io"
- "net"
- "net/netip"
- "strings"
- "testing"
-
- "tailscale.com/net/memnet"
-)
-
-func echoConnOnce(conn net.Conn) {
- defer conn.Close()
-
- b := make([]byte, 256)
- n, err := conn.Read(b)
- if err != nil {
- return
- }
-
- if _, err := conn.Write(b[:n]); err != nil {
- return
- }
-}
-
-func TestTCPRoundRobinHandler(t *testing.T) {
- h := tcpRoundRobinHandler{
- To: []string{"yeet.com"},
- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
- if network != "tcp" {
- t.Errorf("network = %s, want %s", network, "tcp")
- }
- if addr != "yeet.com:22" {
- t.Errorf("addr = %s, want %s", addr, "yeet.com:22")
- }
-
- c, s := memnet.NewConn("outbound", 1024)
- go echoConnOnce(s)
- return c, nil
- },
- }
-
- cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:22"), 1024)
- h.Handle(sSock)
-
- // Test data write and read, the other end will echo back
- // a single stanza
- want := "hello"
- if _, err := io.WriteString(cSock, want); err != nil {
- t.Fatal(err)
- }
- got := make([]byte, len(want))
- if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
- t.Fatal(err)
- }
- if string(got) != want {
- t.Errorf("got %q, want %q", got, want)
- }
-
- // The other end closed the socket after the first echo, so
- // any following read should error.
- io.WriteString(cSock, "deadass heres some data on god fr")
- if _, err := io.ReadAtLeast(cSock, got, len(got)); err == nil {
- t.Error("read succeeded on closed socket")
- }
-}
-
-// Capture of first TCP data segment for a connection to https://pkgs.tailscale.com
-const tlsStart = `45000239ff1840004006f9f5c0a801f2
-c726b5efcf9e01bbe803b21394e3b752
-801801f641dc00000101080ade3474f2
-2fb93ee71603010200010001fc030303
-c3acbd19d2624765bb19af4bce03365e
-1d197f5bb939cdadeff26b0f8e7a0620
-295b04127b82bae46aac4ff58cffef25
-eba75a4b7a6de729532c411bd9dd0d2c
-00203a3a130113021303c02bc02fc02c
-c030cca9cca8c013c014009c009d002f
-003501000193caca0000000a000a0008
-1a1a001d001700180010000e000c0268
-3208687474702f312e31002b0007062a
-2a03040303ff01000100000d00120010
-04030804040105030805050108060601
-000b00020100002300000033002b0029
-1a1a000100001d0020d3c76bef062979
-a812ce935cfb4dbe6b3a84dc5ba9226f
-23b0f34af9d1d03b4a001b0003020002
-00120000446900050003026832000000
-170015000012706b67732e7461696c73
-63616c652e636f6d002d000201010005
-00050100000000001700003a3a000100
-0015002d000000000000000000000000
-00000000000000000000000000000000
-00000000000000000000000000000000
-0000290094006f0069e76f2016f963ad
-38c8632d1f240cd75e00e25fdef295d4
-7042b26f3a9a543b1c7dc74939d77803
-20527d423ff996997bda2c6383a14f49
-219eeef8a053e90a32228df37ddbe126
-eccf6b085c93890d08341d819aea6111
-0d909f4cd6b071d9ea40618e74588a33
-90d494bbb5c3002120d5a164a16c9724
-c9ef5e540d8d6f007789a7acf9f5f16f
-bf6a1907a6782ed02b`
-
-func fakeSNIHeader() []byte {
- b, err := hex.DecodeString(strings.Replace(tlsStart, "\n", "", -1))
- if err != nil {
- panic(err)
- }
- return b[0x34:] // trim IP + TCP header
-}
-
-func TestTCPSNIHandler(t *testing.T) {
- h := tcpSNIHandler{
- Allowlist: []string{"pkgs.tailscale.com"},
- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
- if network != "tcp" {
- t.Errorf("network = %s, want %s", network, "tcp")
- }
- if addr != "pkgs.tailscale.com:443" {
- t.Errorf("addr = %s, want %s", addr, "pkgs.tailscale.com:443")
- }
-
- c, s := memnet.NewConn("outbound", 1024)
- go echoConnOnce(s)
- return c, nil
- },
- }
-
- cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:443"), 1024)
- h.Handle(sSock)
-
- // Fake a TLS handshake record with an SNI in it.
- if _, err := cSock.Write(fakeSNIHeader()); err != nil {
- t.Fatal(err)
- }
-
- // Test read, the other end will echo back
- // a single stanza, which is at least the beginning of the SNI header.
- want := fakeSNIHeader()[:5]
- if _, err := cSock.Write(want); err != nil {
- t.Fatal(err)
- }
- got := make([]byte, len(want))
- if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(got, want) {
- t.Errorf("got %q, want %q", got, want)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "io"
+ "net"
+ "net/netip"
+ "strings"
+ "testing"
+
+ "tailscale.com/net/memnet"
+)
+
+func echoConnOnce(conn net.Conn) {
+ defer conn.Close()
+
+ b := make([]byte, 256)
+ n, err := conn.Read(b)
+ if err != nil {
+ return
+ }
+
+ if _, err := conn.Write(b[:n]); err != nil {
+ return
+ }
+}
+
+func TestTCPRoundRobinHandler(t *testing.T) {
+ h := tcpRoundRobinHandler{
+ To: []string{"yeet.com"},
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ if network != "tcp" {
+ t.Errorf("network = %s, want %s", network, "tcp")
+ }
+ if addr != "yeet.com:22" {
+ t.Errorf("addr = %s, want %s", addr, "yeet.com:22")
+ }
+
+ c, s := memnet.NewConn("outbound", 1024)
+ go echoConnOnce(s)
+ return c, nil
+ },
+ }
+
+ cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:22"), 1024)
+ h.Handle(sSock)
+
+ // Test data write and read, the other end will echo back
+ // a single stanza
+ want := "hello"
+ if _, err := io.WriteString(cSock, want); err != nil {
+ t.Fatal(err)
+ }
+ got := make([]byte, len(want))
+ if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
+
+ // The other end closed the socket after the first echo, so
+ // any following read should error.
+ io.WriteString(cSock, "deadass heres some data on god fr")
+ if _, err := io.ReadAtLeast(cSock, got, len(got)); err == nil {
+ t.Error("read succeeded on closed socket")
+ }
+}
+
+// Capture of first TCP data segment for a connection to https://pkgs.tailscale.com
+const tlsStart = `45000239ff1840004006f9f5c0a801f2
+c726b5efcf9e01bbe803b21394e3b752
+801801f641dc00000101080ade3474f2
+2fb93ee71603010200010001fc030303
+c3acbd19d2624765bb19af4bce03365e
+1d197f5bb939cdadeff26b0f8e7a0620
+295b04127b82bae46aac4ff58cffef25
+eba75a4b7a6de729532c411bd9dd0d2c
+00203a3a130113021303c02bc02fc02c
+c030cca9cca8c013c014009c009d002f
+003501000193caca0000000a000a0008
+1a1a001d001700180010000e000c0268
+3208687474702f312e31002b0007062a
+2a03040303ff01000100000d00120010
+04030804040105030805050108060601
+000b00020100002300000033002b0029
+1a1a000100001d0020d3c76bef062979
+a812ce935cfb4dbe6b3a84dc5ba9226f
+23b0f34af9d1d03b4a001b0003020002
+00120000446900050003026832000000
+170015000012706b67732e7461696c73
+63616c652e636f6d002d000201010005
+00050100000000001700003a3a000100
+0015002d000000000000000000000000
+00000000000000000000000000000000
+00000000000000000000000000000000
+0000290094006f0069e76f2016f963ad
+38c8632d1f240cd75e00e25fdef295d4
+7042b26f3a9a543b1c7dc74939d77803
+20527d423ff996997bda2c6383a14f49
+219eeef8a053e90a32228df37ddbe126
+eccf6b085c93890d08341d819aea6111
+0d909f4cd6b071d9ea40618e74588a33
+90d494bbb5c3002120d5a164a16c9724
+c9ef5e540d8d6f007789a7acf9f5f16f
+bf6a1907a6782ed02b`
+
+func fakeSNIHeader() []byte {
+ b, err := hex.DecodeString(strings.Replace(tlsStart, "\n", "", -1))
+ if err != nil {
+ panic(err)
+ }
+ return b[0x34:] // trim IP + TCP header
+}
+
+func TestTCPSNIHandler(t *testing.T) {
+ h := tcpSNIHandler{
+ Allowlist: []string{"pkgs.tailscale.com"},
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ if network != "tcp" {
+ t.Errorf("network = %s, want %s", network, "tcp")
+ }
+ if addr != "pkgs.tailscale.com:443" {
+ t.Errorf("addr = %s, want %s", addr, "pkgs.tailscale.com:443")
+ }
+
+ c, s := memnet.NewConn("outbound", 1024)
+ go echoConnOnce(s)
+ return c, nil
+ },
+ }
+
+ cSock, sSock := memnet.NewTCPConn(netip.MustParseAddrPort("10.64.1.2:22"), netip.MustParseAddrPort("10.64.1.2:443"), 1024)
+ h.Handle(sSock)
+
+ // Fake a TLS handshake record with an SNI in it.
+ if _, err := cSock.Write(fakeSNIHeader()); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test read, the other end will echo back
+ // a single stanza, which is at least the beginning of the SNI header.
+ want := fakeSNIHeader()[:5]
+ if _, err := cSock.Write(want); err != nil {
+ t.Fatal(err)
+ }
+ got := make([]byte, len(want))
+ if _, err := io.ReadAtLeast(cSock, got, len(got)); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Errorf("got %q, want %q", got, want)
+ }
+}
diff --git a/cmd/sniproxy/server.go b/cmd/sniproxy/server.go
index b322b6f4b..c89420661 100644
--- a/cmd/sniproxy/server.go
+++ b/cmd/sniproxy/server.go
@@ -1,327 +1,327 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "expvar"
- "log"
- "net"
- "net/netip"
- "sync"
- "time"
-
- "golang.org/x/net/dns/dnsmessage"
- "tailscale.com/metrics"
- "tailscale.com/tailcfg"
- "tailscale.com/types/appctype"
- "tailscale.com/types/ipproto"
- "tailscale.com/types/nettype"
- "tailscale.com/util/clientmetric"
- "tailscale.com/util/mak"
-)
-
-var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
-
-// target describes the predicates which route some inbound
-// traffic to the app connector to a specific handler.
-type target struct {
- Dest netip.Prefix
- Matching tailcfg.ProtoPortRange
-}
-
-// Server implements an App Connector as expressed in sniproxy.
-type Server struct {
- mu sync.RWMutex // mu guards following fields
- connectors map[appctype.ConfigID]connector
-}
-
-type appcMetrics struct {
- dnsResponses expvar.Int
- dnsFailures expvar.Int
- tcpConns expvar.Int
- sniConns expvar.Int
- unhandledConns expvar.Int
-}
-
-var getMetrics = sync.OnceValue[*appcMetrics](func() *appcMetrics {
- m := appcMetrics{}
-
- stats := new(metrics.Set)
- stats.Set("tls_sessions", &m.sniConns)
- clientmetric.NewCounterFunc("sniproxy_tls_sessions", m.sniConns.Value)
- stats.Set("tcp_sessions", &m.tcpConns)
- clientmetric.NewCounterFunc("sniproxy_tcp_sessions", m.tcpConns.Value)
- stats.Set("dns_responses", &m.dnsResponses)
- clientmetric.NewCounterFunc("sniproxy_dns_responses", m.dnsResponses.Value)
- stats.Set("dns_failed", &m.dnsFailures)
- clientmetric.NewCounterFunc("sniproxy_dns_failed", m.dnsFailures.Value)
- expvar.Publish("sniproxy", stats)
-
- return &m
-})
-
-// Configure applies the provided configuration to the app connector.
-func (s *Server) Configure(cfg *appctype.AppConnectorConfig) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.connectors = makeConnectorsFromConfig(cfg)
- log.Printf("installed app connector config: %+v", s.connectors)
-}
-
-// HandleTCPFlow implements tsnet.FallbackTCPHandler.
-func (s *Server) HandleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
- m := getMetrics()
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- for _, c := range s.connectors {
- if handler, intercept := c.handleTCPFlow(src, dst, m); intercept {
- return handler, intercept
- }
- }
-
- return nil, false
-}
-
-// HandleDNS handles a DNS request to the app connector.
-func (s *Server) HandleDNS(c nettype.ConnPacketConn) {
- defer c.Close()
- c.SetReadDeadline(time.Now().Add(5 * time.Second))
- m := getMetrics()
-
- buf := make([]byte, 1500)
- n, err := c.Read(buf)
- if err != nil {
- log.Printf("HandleDNS: read failed: %v\n ", err)
- m.dnsFailures.Add(1)
- return
- }
-
- addrPortStr := c.LocalAddr().String()
- host, _, err := net.SplitHostPort(addrPortStr)
- if err != nil {
- log.Printf("HandleDNS: bogus addrPort %q", addrPortStr)
- m.dnsFailures.Add(1)
- return
- }
- localAddr, err := netip.ParseAddr(host)
- if err != nil {
- log.Printf("HandleDNS: bogus local address %q", host)
- m.dnsFailures.Add(1)
- return
- }
-
- var msg dnsmessage.Message
- err = msg.Unpack(buf[:n])
- if err != nil {
- log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
- m.dnsFailures.Add(1)
- return
- }
-
- s.mu.RLock()
- defer s.mu.RUnlock()
- for _, connector := range s.connectors {
- resp, err := connector.handleDNS(&msg, localAddr)
- if err != nil {
- log.Printf("HandleDNS: connector handling failed: %v\n", err)
- m.dnsFailures.Add(1)
- return
- }
- if len(resp) > 0 {
- // This connector handled the DNS request
- _, err = c.Write(resp)
- if err != nil {
- log.Printf("HandleDNS: write failed: %v\n", err)
- m.dnsFailures.Add(1)
- return
- }
-
- m.dnsResponses.Add(1)
- return
- }
- }
-}
-
-// connector describes a logical collection of
-// services which need to be proxied.
-type connector struct {
- Handlers map[target]handler
-}
-
-// handleTCPFlow implements tsnet.FallbackTCPHandler.
-func (c *connector) handleTCPFlow(src, dst netip.AddrPort, m *appcMetrics) (handler func(net.Conn), intercept bool) {
- for t, h := range c.Handlers {
- if t.Matching.Proto != 0 && t.Matching.Proto != int(ipproto.TCP) {
- continue
- }
- if !t.Dest.Contains(dst.Addr()) {
- continue
- }
- if !t.Matching.Ports.Contains(dst.Port()) {
- continue
- }
-
- switch h.(type) {
- case *tcpSNIHandler:
- m.sniConns.Add(1)
- case *tcpRoundRobinHandler:
- m.tcpConns.Add(1)
- default:
- log.Printf("handleTCPFlow: unhandled handler type %T", h)
- }
-
- return h.Handle, true
- }
-
- m.unhandledConns.Add(1)
- return nil, false
-}
-
-// handleDNS returns the DNS response to the given query. If this
-// connector is unable to handle the request, nil is returned.
-func (c *connector) handleDNS(req *dnsmessage.Message, localAddr netip.Addr) (response []byte, err error) {
- for t, h := range c.Handlers {
- if t.Dest.Contains(localAddr) {
- return makeDNSResponse(req, h.ReachableOn())
- }
- }
-
- // Did not match, signal 'not handled' to caller
- return nil, nil
-}
-
-func makeDNSResponse(req *dnsmessage.Message, reachableIPs []netip.Addr) (response []byte, err error) {
- resp := dnsmessage.NewBuilder(response,
- dnsmessage.Header{
- ID: req.Header.ID,
- Response: true,
- Authoritative: true,
- })
- resp.EnableCompression()
-
- if len(req.Questions) == 0 {
- response, _ = resp.Finish()
- return response, nil
- }
- q := req.Questions[0]
- err = resp.StartQuestions()
- if err != nil {
- return
- }
- resp.Question(q)
-
- err = resp.StartAnswers()
- if err != nil {
- return
- }
-
- switch q.Type {
- case dnsmessage.TypeAAAA:
- for _, ip := range reachableIPs {
- if ip.Is6() {
- err = resp.AAAAResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.AAAAResource{AAAA: ip.As16()},
- )
- }
- }
-
- case dnsmessage.TypeA:
- for _, ip := range reachableIPs {
- if ip.Is4() {
- err = resp.AResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.AResource{A: ip.As4()},
- )
- }
- }
-
- case dnsmessage.TypeSOA:
- err = resp.SOAResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
- Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
- )
- case dnsmessage.TypeNS:
- err = resp.NSResource(
- dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
- dnsmessage.NSResource{NS: tsMBox},
- )
- }
-
- if err != nil {
- return nil, err
- }
- return resp.Finish()
-}
-
-type handler interface {
- // Handle handles the given socket.
- Handle(c net.Conn)
-
- // ReachableOn returns the IP addresses this handler is reachable on.
- ReachableOn() []netip.Addr
-}
-
-func installDNATHandler(d *appctype.DNATConfig, out *connector) {
- // These handlers don't actually do DNAT, they just
- // proxy the data over the connection.
- var dialer net.Dialer
- dialer.Timeout = 5 * time.Second
- h := tcpRoundRobinHandler{
- To: d.To,
- DialContext: dialer.DialContext,
- ReachableIPs: d.Addrs,
- }
-
- for _, addr := range d.Addrs {
- for _, protoPort := range d.IP {
- t := target{
- Dest: netip.PrefixFrom(addr, addr.BitLen()),
- Matching: protoPort,
- }
-
- mak.Set(&out.Handlers, t, handler(&h))
- }
- }
-}
-
-func installSNIHandler(c *appctype.SNIProxyConfig, out *connector) {
- var dialer net.Dialer
- dialer.Timeout = 5 * time.Second
- h := tcpSNIHandler{
- Allowlist: c.AllowedDomains,
- DialContext: dialer.DialContext,
- ReachableIPs: c.Addrs,
- }
-
- for _, addr := range c.Addrs {
- for _, protoPort := range c.IP {
- t := target{
- Dest: netip.PrefixFrom(addr, addr.BitLen()),
- Matching: protoPort,
- }
-
- mak.Set(&out.Handlers, t, handler(&h))
- }
- }
-}
-
-func makeConnectorsFromConfig(cfg *appctype.AppConnectorConfig) map[appctype.ConfigID]connector {
- var connectors map[appctype.ConfigID]connector
-
- for cID, d := range cfg.DNAT {
- c := connectors[cID]
- installDNATHandler(&d, &c)
- mak.Set(&connectors, cID, c)
- }
- for cID, d := range cfg.SNIProxy {
- c := connectors[cID]
- installSNIHandler(&d, &c)
- mak.Set(&connectors, cID, c)
- }
-
- return connectors
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+import (
+ "expvar"
+ "log"
+ "net"
+ "net/netip"
+ "sync"
+ "time"
+
+ "golang.org/x/net/dns/dnsmessage"
+ "tailscale.com/metrics"
+ "tailscale.com/tailcfg"
+ "tailscale.com/types/appctype"
+ "tailscale.com/types/ipproto"
+ "tailscale.com/types/nettype"
+ "tailscale.com/util/clientmetric"
+ "tailscale.com/util/mak"
+)
+
+var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
+
+// target describes the predicates which route some inbound
+// traffic to the app connector to a specific handler.
+type target struct {
+ Dest netip.Prefix
+ Matching tailcfg.ProtoPortRange
+}
+
+// Server implements an App Connector as expressed in sniproxy.
+type Server struct {
+ mu sync.RWMutex // mu guards following fields
+ connectors map[appctype.ConfigID]connector
+}
+
+type appcMetrics struct {
+ dnsResponses expvar.Int
+ dnsFailures expvar.Int
+ tcpConns expvar.Int
+ sniConns expvar.Int
+ unhandledConns expvar.Int
+}
+
+var getMetrics = sync.OnceValue[*appcMetrics](func() *appcMetrics {
+ m := appcMetrics{}
+
+ stats := new(metrics.Set)
+ stats.Set("tls_sessions", &m.sniConns)
+ clientmetric.NewCounterFunc("sniproxy_tls_sessions", m.sniConns.Value)
+ stats.Set("tcp_sessions", &m.tcpConns)
+ clientmetric.NewCounterFunc("sniproxy_tcp_sessions", m.tcpConns.Value)
+ stats.Set("dns_responses", &m.dnsResponses)
+ clientmetric.NewCounterFunc("sniproxy_dns_responses", m.dnsResponses.Value)
+ stats.Set("dns_failed", &m.dnsFailures)
+ clientmetric.NewCounterFunc("sniproxy_dns_failed", m.dnsFailures.Value)
+ expvar.Publish("sniproxy", stats)
+
+ return &m
+})
+
+// Configure applies the provided configuration to the app connector.
+func (s *Server) Configure(cfg *appctype.AppConnectorConfig) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.connectors = makeConnectorsFromConfig(cfg)
+ log.Printf("installed app connector config: %+v", s.connectors)
+}
+
+// HandleTCPFlow implements tsnet.FallbackTCPHandler.
+func (s *Server) HandleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
+ m := getMetrics()
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ for _, c := range s.connectors {
+ if handler, intercept := c.handleTCPFlow(src, dst, m); intercept {
+ return handler, intercept
+ }
+ }
+
+ return nil, false
+}
+
+// HandleDNS handles a DNS request to the app connector.
+func (s *Server) HandleDNS(c nettype.ConnPacketConn) {
+ defer c.Close()
+ c.SetReadDeadline(time.Now().Add(5 * time.Second))
+ m := getMetrics()
+
+ buf := make([]byte, 1500)
+ n, err := c.Read(buf)
+ if err != nil {
+ log.Printf("HandleDNS: read failed: %v\n ", err)
+ m.dnsFailures.Add(1)
+ return
+ }
+
+ addrPortStr := c.LocalAddr().String()
+ host, _, err := net.SplitHostPort(addrPortStr)
+ if err != nil {
+ log.Printf("HandleDNS: bogus addrPort %q", addrPortStr)
+ m.dnsFailures.Add(1)
+ return
+ }
+ localAddr, err := netip.ParseAddr(host)
+ if err != nil {
+ log.Printf("HandleDNS: bogus local address %q", host)
+ m.dnsFailures.Add(1)
+ return
+ }
+
+ var msg dnsmessage.Message
+ err = msg.Unpack(buf[:n])
+ if err != nil {
+ log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
+ m.dnsFailures.Add(1)
+ return
+ }
+
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ for _, connector := range s.connectors {
+ resp, err := connector.handleDNS(&msg, localAddr)
+ if err != nil {
+ log.Printf("HandleDNS: connector handling failed: %v\n", err)
+ m.dnsFailures.Add(1)
+ return
+ }
+ if len(resp) > 0 {
+ // This connector handled the DNS request
+ _, err = c.Write(resp)
+ if err != nil {
+ log.Printf("HandleDNS: write failed: %v\n", err)
+ m.dnsFailures.Add(1)
+ return
+ }
+
+ m.dnsResponses.Add(1)
+ return
+ }
+ }
+}
+
+// connector describes a logical collection of
+// services which need to be proxied.
+type connector struct {
+ Handlers map[target]handler
+}
+
+// handleTCPFlow implements tsnet.FallbackTCPHandler.
+func (c *connector) handleTCPFlow(src, dst netip.AddrPort, m *appcMetrics) (handler func(net.Conn), intercept bool) {
+ for t, h := range c.Handlers {
+ if t.Matching.Proto != 0 && t.Matching.Proto != int(ipproto.TCP) {
+ continue
+ }
+ if !t.Dest.Contains(dst.Addr()) {
+ continue
+ }
+ if !t.Matching.Ports.Contains(dst.Port()) {
+ continue
+ }
+
+ switch h.(type) {
+ case *tcpSNIHandler:
+ m.sniConns.Add(1)
+ case *tcpRoundRobinHandler:
+ m.tcpConns.Add(1)
+ default:
+ log.Printf("handleTCPFlow: unhandled handler type %T", h)
+ }
+
+ return h.Handle, true
+ }
+
+ m.unhandledConns.Add(1)
+ return nil, false
+}
+
+// handleDNS returns the DNS response to the given query. If this
+// connector is unable to handle the request, nil is returned.
+func (c *connector) handleDNS(req *dnsmessage.Message, localAddr netip.Addr) (response []byte, err error) {
+ for t, h := range c.Handlers {
+ if t.Dest.Contains(localAddr) {
+ return makeDNSResponse(req, h.ReachableOn())
+ }
+ }
+
+ // Did not match, signal 'not handled' to caller
+ return nil, nil
+}
+
+func makeDNSResponse(req *dnsmessage.Message, reachableIPs []netip.Addr) (response []byte, err error) {
+ resp := dnsmessage.NewBuilder(response,
+ dnsmessage.Header{
+ ID: req.Header.ID,
+ Response: true,
+ Authoritative: true,
+ })
+ resp.EnableCompression()
+
+ if len(req.Questions) == 0 {
+ response, _ = resp.Finish()
+ return response, nil
+ }
+ q := req.Questions[0]
+ err = resp.StartQuestions()
+ if err != nil {
+ return
+ }
+ resp.Question(q)
+
+ err = resp.StartAnswers()
+ if err != nil {
+ return
+ }
+
+ switch q.Type {
+ case dnsmessage.TypeAAAA:
+ for _, ip := range reachableIPs {
+ if ip.Is6() {
+ err = resp.AAAAResource(
+ dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
+ dnsmessage.AAAAResource{AAAA: ip.As16()},
+ )
+ }
+ }
+
+ case dnsmessage.TypeA:
+ for _, ip := range reachableIPs {
+ if ip.Is4() {
+ err = resp.AResource(
+ dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
+ dnsmessage.AResource{A: ip.As4()},
+ )
+ }
+ }
+
+ case dnsmessage.TypeSOA:
+ err = resp.SOAResource(
+ dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
+ dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
+ Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
+ )
+ case dnsmessage.TypeNS:
+ err = resp.NSResource(
+ dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
+ dnsmessage.NSResource{NS: tsMBox},
+ )
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ return resp.Finish()
+}
+
+type handler interface {
+ // Handle handles the given socket.
+ Handle(c net.Conn)
+
+ // ReachableOn returns the IP addresses this handler is reachable on.
+ ReachableOn() []netip.Addr
+}
+
+func installDNATHandler(d *appctype.DNATConfig, out *connector) {
+ // These handlers don't actually do DNAT, they just
+ // proxy the data over the connection.
+ var dialer net.Dialer
+ dialer.Timeout = 5 * time.Second
+ h := tcpRoundRobinHandler{
+ To: d.To,
+ DialContext: dialer.DialContext,
+ ReachableIPs: d.Addrs,
+ }
+
+ for _, addr := range d.Addrs {
+ for _, protoPort := range d.IP {
+ t := target{
+ Dest: netip.PrefixFrom(addr, addr.BitLen()),
+ Matching: protoPort,
+ }
+
+ mak.Set(&out.Handlers, t, handler(&h))
+ }
+ }
+}
+
+func installSNIHandler(c *appctype.SNIProxyConfig, out *connector) {
+ var dialer net.Dialer
+ dialer.Timeout = 5 * time.Second
+ h := tcpSNIHandler{
+ Allowlist: c.AllowedDomains,
+ DialContext: dialer.DialContext,
+ ReachableIPs: c.Addrs,
+ }
+
+ for _, addr := range c.Addrs {
+ for _, protoPort := range c.IP {
+ t := target{
+ Dest: netip.PrefixFrom(addr, addr.BitLen()),
+ Matching: protoPort,
+ }
+
+ mak.Set(&out.Handlers, t, handler(&h))
+ }
+ }
+}
+
+func makeConnectorsFromConfig(cfg *appctype.AppConnectorConfig) map[appctype.ConfigID]connector {
+ var connectors map[appctype.ConfigID]connector
+
+ for cID, d := range cfg.DNAT {
+ c := connectors[cID]
+ installDNATHandler(&d, &c)
+ mak.Set(&connectors, cID, c)
+ }
+ for cID, d := range cfg.SNIProxy {
+ c := connectors[cID]
+ installSNIHandler(&d, &c)
+ mak.Set(&connectors, cID, c)
+ }
+
+ return connectors
+}
diff --git a/cmd/sniproxy/server_test.go b/cmd/sniproxy/server_test.go
index d56f2aa75..2a51c874c 100644
--- a/cmd/sniproxy/server_test.go
+++ b/cmd/sniproxy/server_test.go
@@ -1,95 +1,95 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "net/netip"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "github.com/google/go-cmp/cmp/cmpopts"
- "tailscale.com/tailcfg"
- "tailscale.com/types/appctype"
-)
-
-func TestMakeConnectorsFromConfig(t *testing.T) {
- tcs := []struct {
- name string
- input *appctype.AppConnectorConfig
- want map[appctype.ConfigID]connector
- }{
- {
- "empty",
- &appctype.AppConnectorConfig{},
- nil,
- },
- {
- "DNAT",
- &appctype.AppConnectorConfig{
- DNAT: map[appctype.ConfigID]appctype.DNATConfig{
- "swiggity_swooty": {
- Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
- To: []string{"example.org"},
- IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
- },
- },
- },
- map[appctype.ConfigID]connector{
- "swiggity_swooty": {
- Handlers: map[target]handler{
- {
- Dest: netip.MustParsePrefix("100.64.0.1/32"),
- Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
- }: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
- {
- Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
- Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
- }: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
- },
- },
- },
- },
- {
- "SNIProxy",
- &appctype.AppConnectorConfig{
- SNIProxy: map[appctype.ConfigID]appctype.SNIProxyConfig{
- "swiggity_swooty": {
- Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
- AllowedDomains: []string{"example.org"},
- IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
- },
- },
- },
- map[appctype.ConfigID]connector{
- "swiggity_swooty": {
- Handlers: map[target]handler{
- {
- Dest: netip.MustParsePrefix("100.64.0.1/32"),
- Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
- }: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
- {
- Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
- Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
- }: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
- },
- },
- },
- },
- }
-
- for _, tc := range tcs {
- t.Run(tc.name, func(t *testing.T) {
- connectors := makeConnectorsFromConfig(tc.input)
-
- if diff := cmp.Diff(connectors, tc.want,
- cmpopts.IgnoreFields(tcpRoundRobinHandler{}, "DialContext"),
- cmpopts.IgnoreFields(tcpSNIHandler{}, "DialContext"),
- cmp.Comparer(func(x, y netip.Addr) bool {
- return x == y
- })); diff != "" {
- t.Fatalf("mismatch (-want +got):\n%s", diff)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+import (
+ "net/netip"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "tailscale.com/tailcfg"
+ "tailscale.com/types/appctype"
+)
+
+func TestMakeConnectorsFromConfig(t *testing.T) {
+ tcs := []struct {
+ name string
+ input *appctype.AppConnectorConfig
+ want map[appctype.ConfigID]connector
+ }{
+ {
+ "empty",
+ &appctype.AppConnectorConfig{},
+ nil,
+ },
+ {
+ "DNAT",
+ &appctype.AppConnectorConfig{
+ DNAT: map[appctype.ConfigID]appctype.DNATConfig{
+ "swiggity_swooty": {
+ Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
+ To: []string{"example.org"},
+ IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
+ },
+ },
+ },
+ map[appctype.ConfigID]connector{
+ "swiggity_swooty": {
+ Handlers: map[target]handler{
+ {
+ Dest: netip.MustParsePrefix("100.64.0.1/32"),
+ Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
+ }: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
+ {
+ Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
+ Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
+ }: &tcpRoundRobinHandler{To: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
+ },
+ },
+ },
+ },
+ {
+ "SNIProxy",
+ &appctype.AppConnectorConfig{
+ SNIProxy: map[appctype.ConfigID]appctype.SNIProxyConfig{
+ "swiggity_swooty": {
+ Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
+ AllowedDomains: []string{"example.org"},
+ IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
+ },
+ },
+ },
+ map[appctype.ConfigID]connector{
+ "swiggity_swooty": {
+ Handlers: map[target]handler{
+ {
+ Dest: netip.MustParsePrefix("100.64.0.1/32"),
+ Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
+ }: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
+ {
+ Dest: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
+ Matching: tailcfg.ProtoPortRange{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}},
+ }: &tcpSNIHandler{Allowlist: []string{"example.org"}, ReachableIPs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")}},
+ },
+ },
+ },
+ },
+ }
+
+ for _, tc := range tcs {
+ t.Run(tc.name, func(t *testing.T) {
+ connectors := makeConnectorsFromConfig(tc.input)
+
+ if diff := cmp.Diff(connectors, tc.want,
+ cmpopts.IgnoreFields(tcpRoundRobinHandler{}, "DialContext"),
+ cmpopts.IgnoreFields(tcpSNIHandler{}, "DialContext"),
+ cmp.Comparer(func(x, y netip.Addr) bool {
+ return x == y
+ })); diff != "" {
+ t.Fatalf("mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/cmd/sniproxy/sniproxy.go b/cmd/sniproxy/sniproxy.go
index fa83aaf4a..c048c8e7e 100644
--- a/cmd/sniproxy/sniproxy.go
+++ b/cmd/sniproxy/sniproxy.go
@@ -1,291 +1,291 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The sniproxy is an outbound SNI proxy. It receives TLS connections over
-// Tailscale on one or more TCP ports and sends them out to the same SNI
-// hostname & port on the internet. It can optionally forward one or more
-// TCP ports to a specific destination. It only does TCP.
-package main
-
-import (
- "context"
- "errors"
- "flag"
- "fmt"
- "log"
- "net"
- "net/http"
- "net/netip"
- "os"
- "sort"
- "strconv"
- "strings"
-
- "github.com/peterbourgon/ff/v3"
- "tailscale.com/client/tailscale"
- "tailscale.com/hostinfo"
- "tailscale.com/ipn"
- "tailscale.com/tailcfg"
- "tailscale.com/tsnet"
- "tailscale.com/tsweb"
- "tailscale.com/types/appctype"
- "tailscale.com/types/ipproto"
- "tailscale.com/types/nettype"
- "tailscale.com/util/mak"
-)
-
-const configCapKey = "tailscale.com/sniproxy"
-
-// portForward is the state for a single port forwarding entry, as passed to the --forward flag.
-type portForward struct {
- Port int
- Proto string
- Destination string
-}
-
-// parseForward takes a proto/port/destination tuple as an input, as would be passed
-// to the --forward command line flag, and returns a *portForward struct of those parameters.
-func parseForward(value string) (*portForward, error) {
- parts := strings.Split(value, "/")
- if len(parts) != 3 {
- return nil, errors.New("cannot parse: " + value)
- }
-
- proto := parts[0]
- if proto != "tcp" {
- return nil, errors.New("unsupported forwarding protocol: " + proto)
- }
- port, err := strconv.ParseUint(parts[1], 10, 16)
- if err != nil {
- return nil, errors.New("bad forwarding port: " + parts[1])
- }
- host := parts[2]
- if host == "" {
- return nil, errors.New("bad destination: " + value)
- }
-
- return &portForward{Port: int(port), Proto: proto, Destination: host}, nil
-}
-
-func main() {
- // Parse flags
- fs := flag.NewFlagSet("sniproxy", flag.ContinueOnError)
- var (
- ports = fs.String("ports", "443", "comma-separated list of ports to proxy")
- forwards = fs.String("forwards", "", "comma-separated list of ports to transparently forward, protocol/number/destination. For example, --forwards=tcp/22/github.com,tcp/5432/sql.example.com")
- wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
- promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
- debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
- hostname = fs.String("hostname", "", "Hostname to register the service under")
- )
- err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
- if err != nil {
- log.Fatal("ff.Parse")
- }
-
- var ts tsnet.Server
- defer ts.Close()
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- run(ctx, &ts, *wgPort, *hostname, *promoteHTTPS, *debugPort, *ports, *forwards)
-}
-
-// run actually runs the sniproxy. Its separate from main() to assist in testing.
-func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, promoteHTTPS bool, debugPort int, ports, forwards string) {
- // Wire up Tailscale node + app connector server
- hostinfo.SetApp("sniproxy")
- var s sniproxy
- s.ts = ts
-
- s.ts.Port = uint16(wgPort)
- s.ts.Hostname = hostname
-
- lc, err := s.ts.LocalClient()
- if err != nil {
- log.Fatalf("LocalClient() failed: %v", err)
- }
- s.lc = lc
- s.ts.RegisterFallbackTCPHandler(s.srv.HandleTCPFlow)
-
- // Start special-purpose listeners: dns, http promotion, debug server
- ln, err := s.ts.Listen("udp", ":53")
- if err != nil {
- log.Fatalf("failed listening on port 53: %v", err)
- }
- defer ln.Close()
- go s.serveDNS(ln)
- if promoteHTTPS {
- ln, err := s.ts.Listen("tcp", ":80")
- if err != nil {
- log.Fatalf("failed listening on port 80: %v", err)
- }
- defer ln.Close()
- log.Printf("Promoting HTTP to HTTPS ...")
- go s.promoteHTTPS(ln)
- }
- if debugPort != 0 {
- mux := http.NewServeMux()
- tsweb.Debugger(mux)
- dln, err := s.ts.Listen("tcp", fmt.Sprintf(":%d", debugPort))
- if err != nil {
- log.Fatalf("failed listening on debug port: %v", err)
- }
- defer dln.Close()
- go func() {
- log.Fatalf("debug serve: %v", http.Serve(dln, mux))
- }()
- }
-
- // Finally, start mainloop to configure app connector based on information
- // in the netmap.
- // We set the NotifyInitialNetMap flag so we will always get woken with the
- // current netmap, before only being woken on changes.
- bus, err := lc.WatchIPNBus(ctx, ipn.NotifyWatchEngineUpdates|ipn.NotifyInitialNetMap|ipn.NotifyNoPrivateKeys)
- if err != nil {
- log.Fatalf("watching IPN bus: %v", err)
- }
- defer bus.Close()
- for {
- msg, err := bus.Next()
- if err != nil {
- if errors.Is(err, context.Canceled) {
- return
- }
- log.Fatalf("reading IPN bus: %v", err)
- }
-
- // NetMap contains app-connector configuration
- if nm := msg.NetMap; nm != nil && nm.SelfNode.Valid() {
- sn := nm.SelfNode.AsStruct()
-
- var c appctype.AppConnectorConfig
- nmConf, err := tailcfg.UnmarshalNodeCapJSON[appctype.AppConnectorConfig](sn.CapMap, configCapKey)
- if err != nil {
- log.Printf("failed to read app connector configuration from coordination server: %v", err)
- } else if len(nmConf) > 0 {
- c = nmConf[0]
- }
-
- if c.AdvertiseRoutes {
- if err := s.advertiseRoutesFromConfig(ctx, &c); err != nil {
- log.Printf("failed to advertise routes: %v", err)
- }
- }
-
- // Backwards compatibility: combine any configuration from control with flags specified
- // on the command line. This is intentionally done after we advertise any routes
- // because its never correct to advertise the nodes native IP addresses.
- s.mergeConfigFromFlags(&c, ports, forwards)
- s.srv.Configure(&c)
- }
- }
-}
-
-type sniproxy struct {
- srv Server
- ts *tsnet.Server
- lc *tailscale.LocalClient
-}
-
-func (s *sniproxy) advertiseRoutesFromConfig(ctx context.Context, c *appctype.AppConnectorConfig) error {
- // Collect the set of addresses to advertise, using a map
- // to avoid duplicate entries.
- addrs := map[netip.Addr]struct{}{}
- for _, c := range c.SNIProxy {
- for _, ip := range c.Addrs {
- addrs[ip] = struct{}{}
- }
- }
- for _, c := range c.DNAT {
- for _, ip := range c.Addrs {
- addrs[ip] = struct{}{}
- }
- }
-
- var routes []netip.Prefix
- for a := range addrs {
- routes = append(routes, netip.PrefixFrom(a, a.BitLen()))
- }
- sort.SliceStable(routes, func(i, j int) bool {
- return routes[i].Addr().Less(routes[j].Addr()) // determinism r us
- })
-
- _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
- Prefs: ipn.Prefs{
- AdvertiseRoutes: routes,
- },
- AdvertiseRoutesSet: true,
- })
- return err
-}
-
-func (s *sniproxy) mergeConfigFromFlags(out *appctype.AppConnectorConfig, ports, forwards string) {
- ip4, ip6 := s.ts.TailscaleIPs()
-
- sniConfigFromFlags := appctype.SNIProxyConfig{
- Addrs: []netip.Addr{ip4, ip6},
- }
- if ports != "" {
- for _, portStr := range strings.Split(ports, ",") {
- port, err := strconv.ParseUint(portStr, 10, 16)
- if err != nil {
- log.Fatalf("invalid port: %s", portStr)
- }
- sniConfigFromFlags.IP = append(sniConfigFromFlags.IP, tailcfg.ProtoPortRange{
- Proto: int(ipproto.TCP),
- Ports: tailcfg.PortRange{First: uint16(port), Last: uint16(port)},
- })
- }
- }
-
- var forwardConfigFromFlags []appctype.DNATConfig
- for _, forwStr := range strings.Split(forwards, ",") {
- if forwStr == "" {
- continue
- }
- forw, err := parseForward(forwStr)
- if err != nil {
- log.Printf("invalid forwarding spec: %v", err)
- continue
- }
-
- forwardConfigFromFlags = append(forwardConfigFromFlags, appctype.DNATConfig{
- Addrs: []netip.Addr{ip4, ip6},
- To: []string{forw.Destination},
- IP: []tailcfg.ProtoPortRange{
- {
- Proto: int(ipproto.TCP),
- Ports: tailcfg.PortRange{First: uint16(forw.Port), Last: uint16(forw.Port)},
- },
- },
- })
- }
-
- if len(forwardConfigFromFlags) == 0 && len(sniConfigFromFlags.IP) == 0 {
- return // no config specified on the command line
- }
-
- mak.Set(&out.SNIProxy, "flags", sniConfigFromFlags)
- for i, forward := range forwardConfigFromFlags {
- mak.Set(&out.DNAT, appctype.ConfigID(fmt.Sprintf("flags_%d", i)), forward)
- }
-}
-
-func (s *sniproxy) serveDNS(ln net.Listener) {
- for {
- c, err := ln.Accept()
- if err != nil {
- log.Printf("serveDNS accept: %v", err)
- return
- }
- go s.srv.HandleDNS(c.(nettype.ConnPacketConn))
- }
-}
-
-func (s *sniproxy) promoteHTTPS(ln net.Listener) {
- err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
- }))
- log.Fatalf("promoteHTTPS http.Serve: %v", err)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The sniproxy is an outbound SNI proxy. It receives TLS connections over
+// Tailscale on one or more TCP ports and sends them out to the same SNI
+// hostname & port on the internet. It can optionally forward one or more
+// TCP ports to a specific destination. It only does TCP.
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "net/netip"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/peterbourgon/ff/v3"
+ "tailscale.com/client/tailscale"
+ "tailscale.com/hostinfo"
+ "tailscale.com/ipn"
+ "tailscale.com/tailcfg"
+ "tailscale.com/tsnet"
+ "tailscale.com/tsweb"
+ "tailscale.com/types/appctype"
+ "tailscale.com/types/ipproto"
+ "tailscale.com/types/nettype"
+ "tailscale.com/util/mak"
+)
+
+const configCapKey = "tailscale.com/sniproxy"
+
+// portForward is the state for a single port forwarding entry, as passed to the --forward flag.
+type portForward struct {
+ Port int
+ Proto string
+ Destination string
+}
+
+// parseForward takes a proto/port/destination tuple as an input, as would be passed
+// to the --forward command line flag, and returns a *portForward struct of those parameters.
+func parseForward(value string) (*portForward, error) {
+ parts := strings.Split(value, "/")
+ if len(parts) != 3 {
+ return nil, errors.New("cannot parse: " + value)
+ }
+
+ proto := parts[0]
+ if proto != "tcp" {
+ return nil, errors.New("unsupported forwarding protocol: " + proto)
+ }
+ port, err := strconv.ParseUint(parts[1], 10, 16)
+ if err != nil {
+ return nil, errors.New("bad forwarding port: " + parts[1])
+ }
+ host := parts[2]
+ if host == "" {
+ return nil, errors.New("bad destination: " + value)
+ }
+
+ return &portForward{Port: int(port), Proto: proto, Destination: host}, nil
+}
+
+func main() {
+ // Parse flags
+ fs := flag.NewFlagSet("sniproxy", flag.ContinueOnError)
+ var (
+ ports = fs.String("ports", "443", "comma-separated list of ports to proxy")
+ forwards = fs.String("forwards", "", "comma-separated list of ports to transparently forward, protocol/number/destination. For example, --forwards=tcp/22/github.com,tcp/5432/sql.example.com")
+ wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
+ promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
+ debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
+ hostname = fs.String("hostname", "", "Hostname to register the service under")
+ )
+ err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
+ if err != nil {
+ log.Fatal("ff.Parse")
+ }
+
+ var ts tsnet.Server
+ defer ts.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ run(ctx, &ts, *wgPort, *hostname, *promoteHTTPS, *debugPort, *ports, *forwards)
+}
+
+// run actually runs the sniproxy. Its separate from main() to assist in testing.
+func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, promoteHTTPS bool, debugPort int, ports, forwards string) {
+ // Wire up Tailscale node + app connector server
+ hostinfo.SetApp("sniproxy")
+ var s sniproxy
+ s.ts = ts
+
+ s.ts.Port = uint16(wgPort)
+ s.ts.Hostname = hostname
+
+ lc, err := s.ts.LocalClient()
+ if err != nil {
+ log.Fatalf("LocalClient() failed: %v", err)
+ }
+ s.lc = lc
+ s.ts.RegisterFallbackTCPHandler(s.srv.HandleTCPFlow)
+
+ // Start special-purpose listeners: dns, http promotion, debug server
+ ln, err := s.ts.Listen("udp", ":53")
+ if err != nil {
+ log.Fatalf("failed listening on port 53: %v", err)
+ }
+ defer ln.Close()
+ go s.serveDNS(ln)
+ if promoteHTTPS {
+ ln, err := s.ts.Listen("tcp", ":80")
+ if err != nil {
+ log.Fatalf("failed listening on port 80: %v", err)
+ }
+ defer ln.Close()
+ log.Printf("Promoting HTTP to HTTPS ...")
+ go s.promoteHTTPS(ln)
+ }
+ if debugPort != 0 {
+ mux := http.NewServeMux()
+ tsweb.Debugger(mux)
+ dln, err := s.ts.Listen("tcp", fmt.Sprintf(":%d", debugPort))
+ if err != nil {
+ log.Fatalf("failed listening on debug port: %v", err)
+ }
+ defer dln.Close()
+ go func() {
+ log.Fatalf("debug serve: %v", http.Serve(dln, mux))
+ }()
+ }
+
+ // Finally, start mainloop to configure app connector based on information
+ // in the netmap.
+ // We set the NotifyInitialNetMap flag so we will always get woken with the
+ // current netmap, before only being woken on changes.
+ bus, err := lc.WatchIPNBus(ctx, ipn.NotifyWatchEngineUpdates|ipn.NotifyInitialNetMap|ipn.NotifyNoPrivateKeys)
+ if err != nil {
+ log.Fatalf("watching IPN bus: %v", err)
+ }
+ defer bus.Close()
+ for {
+ msg, err := bus.Next()
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return
+ }
+ log.Fatalf("reading IPN bus: %v", err)
+ }
+
+ // NetMap contains app-connector configuration
+ if nm := msg.NetMap; nm != nil && nm.SelfNode.Valid() {
+ sn := nm.SelfNode.AsStruct()
+
+ var c appctype.AppConnectorConfig
+ nmConf, err := tailcfg.UnmarshalNodeCapJSON[appctype.AppConnectorConfig](sn.CapMap, configCapKey)
+ if err != nil {
+ log.Printf("failed to read app connector configuration from coordination server: %v", err)
+ } else if len(nmConf) > 0 {
+ c = nmConf[0]
+ }
+
+ if c.AdvertiseRoutes {
+ if err := s.advertiseRoutesFromConfig(ctx, &c); err != nil {
+ log.Printf("failed to advertise routes: %v", err)
+ }
+ }
+
+ // Backwards compatibility: combine any configuration from control with flags specified
+ // on the command line. This is intentionally done after we advertise any routes
+ // because its never correct to advertise the nodes native IP addresses.
+ s.mergeConfigFromFlags(&c, ports, forwards)
+ s.srv.Configure(&c)
+ }
+ }
+}
+
+type sniproxy struct {
+ srv Server
+ ts *tsnet.Server
+ lc *tailscale.LocalClient
+}
+
+func (s *sniproxy) advertiseRoutesFromConfig(ctx context.Context, c *appctype.AppConnectorConfig) error {
+ // Collect the set of addresses to advertise, using a map
+ // to avoid duplicate entries.
+ addrs := map[netip.Addr]struct{}{}
+ for _, c := range c.SNIProxy {
+ for _, ip := range c.Addrs {
+ addrs[ip] = struct{}{}
+ }
+ }
+ for _, c := range c.DNAT {
+ for _, ip := range c.Addrs {
+ addrs[ip] = struct{}{}
+ }
+ }
+
+ var routes []netip.Prefix
+ for a := range addrs {
+ routes = append(routes, netip.PrefixFrom(a, a.BitLen()))
+ }
+ sort.SliceStable(routes, func(i, j int) bool {
+ return routes[i].Addr().Less(routes[j].Addr()) // determinism r us
+ })
+
+ _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
+ Prefs: ipn.Prefs{
+ AdvertiseRoutes: routes,
+ },
+ AdvertiseRoutesSet: true,
+ })
+ return err
+}
+
+func (s *sniproxy) mergeConfigFromFlags(out *appctype.AppConnectorConfig, ports, forwards string) {
+ ip4, ip6 := s.ts.TailscaleIPs()
+
+ sniConfigFromFlags := appctype.SNIProxyConfig{
+ Addrs: []netip.Addr{ip4, ip6},
+ }
+ if ports != "" {
+ for _, portStr := range strings.Split(ports, ",") {
+ port, err := strconv.ParseUint(portStr, 10, 16)
+ if err != nil {
+ log.Fatalf("invalid port: %s", portStr)
+ }
+ sniConfigFromFlags.IP = append(sniConfigFromFlags.IP, tailcfg.ProtoPortRange{
+ Proto: int(ipproto.TCP),
+ Ports: tailcfg.PortRange{First: uint16(port), Last: uint16(port)},
+ })
+ }
+ }
+
+ var forwardConfigFromFlags []appctype.DNATConfig
+ for _, forwStr := range strings.Split(forwards, ",") {
+ if forwStr == "" {
+ continue
+ }
+ forw, err := parseForward(forwStr)
+ if err != nil {
+ log.Printf("invalid forwarding spec: %v", err)
+ continue
+ }
+
+ forwardConfigFromFlags = append(forwardConfigFromFlags, appctype.DNATConfig{
+ Addrs: []netip.Addr{ip4, ip6},
+ To: []string{forw.Destination},
+ IP: []tailcfg.ProtoPortRange{
+ {
+ Proto: int(ipproto.TCP),
+ Ports: tailcfg.PortRange{First: uint16(forw.Port), Last: uint16(forw.Port)},
+ },
+ },
+ })
+ }
+
+ if len(forwardConfigFromFlags) == 0 && len(sniConfigFromFlags.IP) == 0 {
+ return // no config specified on the command line
+ }
+
+ mak.Set(&out.SNIProxy, "flags", sniConfigFromFlags)
+ for i, forward := range forwardConfigFromFlags {
+ mak.Set(&out.DNAT, appctype.ConfigID(fmt.Sprintf("flags_%d", i)), forward)
+ }
+}
+
+func (s *sniproxy) serveDNS(ln net.Listener) {
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ log.Printf("serveDNS accept: %v", err)
+ return
+ }
+ go s.srv.HandleDNS(c.(nettype.ConnPacketConn))
+ }
+}
+
+func (s *sniproxy) promoteHTTPS(ln net.Listener) {
+ err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
+ }))
+ log.Fatalf("promoteHTTPS http.Serve: %v", err)
+}
diff --git a/cmd/speedtest/speedtest.go b/cmd/speedtest/speedtest.go
index 9a457ed6c..1555c0dcc 100644
--- a/cmd/speedtest/speedtest.go
+++ b/cmd/speedtest/speedtest.go
@@ -1,121 +1,121 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Program speedtest provides the speedtest command. The reason to keep it separate from
-// the normal tailscale cli is because it is not yet ready to go in the tailscale binary.
-// It will be included in the tailscale cli after it has been added to tailscaled.
-
-// Example usage for client command: go run cmd/speedtest -host 127.0.0.1:20333 -t 5s
-// This will connect to the server on 127.0.0.1:20333 and start a 5 second download speedtest.
-// Example usage for server command: go run cmd/speedtest -s -host :20333
-// This will start a speedtest server on port 20333.
-package main
-
-import (
- "context"
- "errors"
- "flag"
- "fmt"
- "net"
- "os"
- "strconv"
- "text/tabwriter"
- "time"
-
- "github.com/peterbourgon/ff/v3/ffcli"
- "tailscale.com/net/speedtest"
-)
-
-// Runs the speedtest command as a commandline program
-func main() {
- args := os.Args[1:]
- if err := speedtestCmd.Parse(args); err != nil {
- fmt.Fprintln(os.Stderr, err.Error())
- os.Exit(1)
- }
-
- err := speedtestCmd.Run(context.Background())
- if errors.Is(err, flag.ErrHelp) {
- fmt.Fprintln(os.Stderr, speedtestCmd.ShortUsage)
- os.Exit(2)
- }
- if err != nil {
- fmt.Fprintln(os.Stderr, err.Error())
- os.Exit(1)
- }
-}
-
-// speedtestCmd is the root command. It runs either the server or client depending on the
-// flags passed to it.
-var speedtestCmd = &ffcli.Command{
- Name: "speedtest",
- ShortUsage: "speedtest [-host <host:port>] [-s] [-r] [-t <test duration>]",
- ShortHelp: "Run a speed test",
- FlagSet: (func() *flag.FlagSet {
- fs := flag.NewFlagSet("speedtest", flag.ExitOnError)
- fs.StringVar(&speedtestArgs.host, "host", ":20333", "host:port pair to connect to or listen on")
- fs.DurationVar(&speedtestArgs.testDuration, "t", speedtest.DefaultDuration, "duration of the speed test")
- fs.BoolVar(&speedtestArgs.runServer, "s", false, "run a speedtest server")
- fs.BoolVar(&speedtestArgs.reverse, "r", false, "run in reverse mode (server sends, client receives)")
- return fs
- })(),
- Exec: runSpeedtest,
-}
-
-var speedtestArgs struct {
- host string
- testDuration time.Duration
- runServer bool
- reverse bool
-}
-
-func runSpeedtest(ctx context.Context, args []string) error {
-
- if _, _, err := net.SplitHostPort(speedtestArgs.host); err != nil {
- var addrErr *net.AddrError
- if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
- // if no port is provided, append the default port
- speedtestArgs.host = net.JoinHostPort(speedtestArgs.host, strconv.Itoa(speedtest.DefaultPort))
- }
- }
-
- if speedtestArgs.runServer {
- listener, err := net.Listen("tcp", speedtestArgs.host)
- if err != nil {
- return err
- }
-
- fmt.Printf("listening on %v\n", listener.Addr())
-
- return speedtest.Serve(listener)
- }
-
- // Ensure the duration is within the allowed range
- if speedtestArgs.testDuration < speedtest.MinDuration || speedtestArgs.testDuration > speedtest.MaxDuration {
- return fmt.Errorf("test duration must be within %v and %v", speedtest.MinDuration, speedtest.MaxDuration)
- }
-
- dir := speedtest.Download
- if speedtestArgs.reverse {
- dir = speedtest.Upload
- }
-
- fmt.Printf("Starting a %s test with %s\n", dir, speedtestArgs.host)
- results, err := speedtest.RunClient(dir, speedtestArgs.testDuration, speedtestArgs.host)
- if err != nil {
- return err
- }
-
- w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
- fmt.Println("Results:")
- fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
- startTime := results[0].IntervalStart
- for _, r := range results {
- if r.Total {
- fmt.Fprintln(w, "-------------------------------------------------------------------------")
- }
- fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
- }
- w.Flush()
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Program speedtest provides the speedtest command. The reason to keep it separate from
+// the normal tailscale cli is because it is not yet ready to go in the tailscale binary.
+// It will be included in the tailscale cli after it has been added to tailscaled.
+
+// Example usage for client command: go run cmd/speedtest -host 127.0.0.1:20333 -t 5s
+// This will connect to the server on 127.0.0.1:20333 and start a 5 second download speedtest.
+// Example usage for server command: go run cmd/speedtest -s -host :20333
+// This will start a speedtest server on port 20333.
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "net"
+ "os"
+ "strconv"
+ "text/tabwriter"
+ "time"
+
+ "github.com/peterbourgon/ff/v3/ffcli"
+ "tailscale.com/net/speedtest"
+)
+
+// Runs the speedtest command as a commandline program
+func main() {
+ args := os.Args[1:]
+ if err := speedtestCmd.Parse(args); err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+
+ err := speedtestCmd.Run(context.Background())
+ if errors.Is(err, flag.ErrHelp) {
+ fmt.Fprintln(os.Stderr, speedtestCmd.ShortUsage)
+ os.Exit(2)
+ }
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+}
+
+// speedtestCmd is the root command. It runs either the server or client depending on the
+// flags passed to it.
+var speedtestCmd = &ffcli.Command{
+ Name: "speedtest",
+ ShortUsage: "speedtest [-host <host:port>] [-s] [-r] [-t <test duration>]",
+ ShortHelp: "Run a speed test",
+ FlagSet: (func() *flag.FlagSet {
+ fs := flag.NewFlagSet("speedtest", flag.ExitOnError)
+ fs.StringVar(&speedtestArgs.host, "host", ":20333", "host:port pair to connect to or listen on")
+ fs.DurationVar(&speedtestArgs.testDuration, "t", speedtest.DefaultDuration, "duration of the speed test")
+ fs.BoolVar(&speedtestArgs.runServer, "s", false, "run a speedtest server")
+ fs.BoolVar(&speedtestArgs.reverse, "r", false, "run in reverse mode (server sends, client receives)")
+ return fs
+ })(),
+ Exec: runSpeedtest,
+}
+
+var speedtestArgs struct {
+ host string
+ testDuration time.Duration
+ runServer bool
+ reverse bool
+}
+
+func runSpeedtest(ctx context.Context, args []string) error {
+
+ if _, _, err := net.SplitHostPort(speedtestArgs.host); err != nil {
+ var addrErr *net.AddrError
+ if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
+ // if no port is provided, append the default port
+ speedtestArgs.host = net.JoinHostPort(speedtestArgs.host, strconv.Itoa(speedtest.DefaultPort))
+ }
+ }
+
+ if speedtestArgs.runServer {
+ listener, err := net.Listen("tcp", speedtestArgs.host)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("listening on %v\n", listener.Addr())
+
+ return speedtest.Serve(listener)
+ }
+
+ // Ensure the duration is within the allowed range
+ if speedtestArgs.testDuration < speedtest.MinDuration || speedtestArgs.testDuration > speedtest.MaxDuration {
+ return fmt.Errorf("test duration must be within %v and %v", speedtest.MinDuration, speedtest.MaxDuration)
+ }
+
+ dir := speedtest.Download
+ if speedtestArgs.reverse {
+ dir = speedtest.Upload
+ }
+
+ fmt.Printf("Starting a %s test with %s\n", dir, speedtestArgs.host)
+ results, err := speedtest.RunClient(dir, speedtestArgs.testDuration, speedtestArgs.host)
+ if err != nil {
+ return err
+ }
+
+ w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
+ fmt.Println("Results:")
+ fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
+ startTime := results[0].IntervalStart
+ for _, r := range results {
+ if r.Total {
+ fmt.Fprintln(w, "-------------------------------------------------------------------------")
+ }
+ fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
+ }
+ w.Flush()
+ return nil
+}
diff --git a/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go b/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
index ee929299a..ade272c4b 100644
--- a/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
+++ b/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
@@ -1,187 +1,187 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// ssh-auth-none-demo is a demo SSH server that's meant to run on the
-// public internet (at 188.166.70.128 port 2222) and
-// highlight the unique parts of the Tailscale SSH server so SSH
-// client authors can hit it easily and fix their SSH clients without
-// needing to set up Tailscale and Tailscale SSH.
-package main
-
-import (
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "flag"
- "fmt"
- "io"
- "log"
- "os"
- "path/filepath"
- "time"
-
- gossh "github.com/tailscale/golang-x-crypto/ssh"
- "tailscale.com/tempfork/gliderlabs/ssh"
-)
-
-// keyTypes are the SSH key types that we either try to read from the
-// system's OpenSSH keys.
-var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
-
-var (
- addr = flag.String("addr", ":2222", "address to listen on")
-)
-
-func main() {
- flag.Parse()
-
- cacheDir, err := os.UserCacheDir()
- if err != nil {
- log.Fatal(err)
- }
- dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
- if err := os.MkdirAll(dir, 0700); err != nil {
- log.Fatal(err)
- }
-
- keys, err := getHostKeys(dir)
- if err != nil {
- log.Fatal(err)
- }
- if len(keys) == 0 {
- log.Fatal("no host keys")
- }
-
- srv := &ssh.Server{
- Addr: *addr,
- Version: "Tailscale",
- Handler: handleSessionPostSSHAuth,
- ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
- start := time.Now()
- return &gossh.ServerConfig{
- NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
- return []string{"tailscale"}
- },
- NoClientAuth: true, // required for the NoClientAuthCallback to run
- NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
- cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
-
- totalBanners := 2
- if cm.User() == "banners" {
- totalBanners = 5
- }
- for banner := 2; banner <= totalBanners; banner++ {
- time.Sleep(time.Second)
- if banner == totalBanners {
- cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
- } else {
- cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
- }
- }
- return nil, nil
- },
- BannerCallback: func(cm gossh.ConnMetadata) string {
- log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
- return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
- },
- }
- },
- }
-
- for _, signer := range keys {
- srv.AddHostKey(signer)
- }
-
- log.Printf("Running on %s ...", srv.Addr)
- if err := srv.ListenAndServe(); err != nil {
- log.Fatal(err)
- }
- log.Printf("done")
-}
-
-func handleSessionPostSSHAuth(s ssh.Session) {
- log.Printf("Started session from user %q", s.User())
- fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
-
- // Abort the session on Control-C or Control-D.
- go func() {
- buf := make([]byte, 1024)
- for {
- n, err := s.Read(buf)
- for _, b := range buf[:n] {
- if b <= 4 { // abort on Control-C (3) or Control-D (4)
- io.WriteString(s, "bye\n")
- s.Exit(1)
- }
- }
- if err != nil {
- return
- }
- }
- }()
-
- for i := 10; i > 0; i-- {
- fmt.Fprintf(s, "%v ...\n", i)
- time.Sleep(time.Second)
- }
- s.Exit(0)
-}
-
-func getHostKeys(dir string) (ret []ssh.Signer, err error) {
- for _, typ := range keyTypes {
- hostKey, err := hostKeyFileOrCreate(dir, typ)
- if err != nil {
- return nil, err
- }
- signer, err := gossh.ParsePrivateKey(hostKey)
- if err != nil {
- return nil, err
- }
- ret = append(ret, signer)
- }
- return ret, nil
-}
-
-func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
- path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
- v, err := os.ReadFile(path)
- if err == nil {
- return v, nil
- }
- if !os.IsNotExist(err) {
- return nil, err
- }
- var priv any
- switch typ {
- default:
- return nil, fmt.Errorf("unsupported key type %q", typ)
- case "ed25519":
- _, priv, err = ed25519.GenerateKey(rand.Reader)
- case "ecdsa":
- // curve is arbitrary. We pick whatever will at
- // least pacify clients as the actual encryption
- // doesn't matter: it's all over WireGuard anyway.
- curve := elliptic.P256()
- priv, err = ecdsa.GenerateKey(curve, rand.Reader)
- case "rsa":
- // keySize is arbitrary. We pick whatever will at
- // least pacify clients as the actual encryption
- // doesn't matter: it's all over WireGuard anyway.
- const keySize = 2048
- priv, err = rsa.GenerateKey(rand.Reader, keySize)
- }
- if err != nil {
- return nil, err
- }
- mk, err := x509.MarshalPKCS8PrivateKey(priv)
- if err != nil {
- return nil, err
- }
- pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
- err = os.WriteFile(path, pemGen, 0700)
- return pemGen, err
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// ssh-auth-none-demo is a demo SSH server that's meant to run on the
+// public internet (at 188.166.70.128 port 2222) and
+// highlight the unique parts of the Tailscale SSH server so SSH
+// client authors can hit it easily and fix their SSH clients without
+// needing to set up Tailscale and Tailscale SSH.
+package main
+
+import (
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+ "tailscale.com/tempfork/gliderlabs/ssh"
+)
+
+// keyTypes are the SSH key types that we either try to read from the
+// system's OpenSSH keys.
+var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
+
+var (
+ addr = flag.String("addr", ":2222", "address to listen on")
+)
+
+func main() {
+ flag.Parse()
+
+ cacheDir, err := os.UserCacheDir()
+ if err != nil {
+ log.Fatal(err)
+ }
+ dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ log.Fatal(err)
+ }
+
+ keys, err := getHostKeys(dir)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(keys) == 0 {
+ log.Fatal("no host keys")
+ }
+
+ srv := &ssh.Server{
+ Addr: *addr,
+ Version: "Tailscale",
+ Handler: handleSessionPostSSHAuth,
+ ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
+ start := time.Now()
+ return &gossh.ServerConfig{
+ NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
+ return []string{"tailscale"}
+ },
+ NoClientAuth: true, // required for the NoClientAuthCallback to run
+ NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
+ cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
+
+ totalBanners := 2
+ if cm.User() == "banners" {
+ totalBanners = 5
+ }
+ for banner := 2; banner <= totalBanners; banner++ {
+ time.Sleep(time.Second)
+ if banner == totalBanners {
+ cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
+ } else {
+ cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
+ }
+ }
+ return nil, nil
+ },
+ BannerCallback: func(cm gossh.ConnMetadata) string {
+ log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
+ return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
+ },
+ }
+ },
+ }
+
+ for _, signer := range keys {
+ srv.AddHostKey(signer)
+ }
+
+ log.Printf("Running on %s ...", srv.Addr)
+ if err := srv.ListenAndServe(); err != nil {
+ log.Fatal(err)
+ }
+ log.Printf("done")
+}
+
+func handleSessionPostSSHAuth(s ssh.Session) {
+ log.Printf("Started session from user %q", s.User())
+ fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
+
+ // Abort the session on Control-C or Control-D.
+ go func() {
+ buf := make([]byte, 1024)
+ for {
+ n, err := s.Read(buf)
+ for _, b := range buf[:n] {
+ if b <= 4 { // abort on Control-C (3) or Control-D (4)
+ io.WriteString(s, "bye\n")
+ s.Exit(1)
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+ }()
+
+ for i := 10; i > 0; i-- {
+ fmt.Fprintf(s, "%v ...\n", i)
+ time.Sleep(time.Second)
+ }
+ s.Exit(0)
+}
+
+func getHostKeys(dir string) (ret []ssh.Signer, err error) {
+ for _, typ := range keyTypes {
+ hostKey, err := hostKeyFileOrCreate(dir, typ)
+ if err != nil {
+ return nil, err
+ }
+ signer, err := gossh.ParsePrivateKey(hostKey)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, signer)
+ }
+ return ret, nil
+}
+
+func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
+ path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
+ v, err := os.ReadFile(path)
+ if err == nil {
+ return v, nil
+ }
+ if !os.IsNotExist(err) {
+ return nil, err
+ }
+ var priv any
+ switch typ {
+ default:
+ return nil, fmt.Errorf("unsupported key type %q", typ)
+ case "ed25519":
+ _, priv, err = ed25519.GenerateKey(rand.Reader)
+ case "ecdsa":
+ // curve is arbitrary. We pick whatever will at
+ // least pacify clients as the actual encryption
+ // doesn't matter: it's all over WireGuard anyway.
+ curve := elliptic.P256()
+ priv, err = ecdsa.GenerateKey(curve, rand.Reader)
+ case "rsa":
+ // keySize is arbitrary. We pick whatever will at
+ // least pacify clients as the actual encryption
+ // doesn't matter: it's all over WireGuard anyway.
+ const keySize = 2048
+ priv, err = rsa.GenerateKey(rand.Reader, keySize)
+ }
+ if err != nil {
+ return nil, err
+ }
+ mk, err := x509.MarshalPKCS8PrivateKey(priv)
+ if err != nil {
+ return nil, err
+ }
+ pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
+ err = os.WriteFile(path, pemGen, 0700)
+ return pemGen, err
+}
diff --git a/cmd/sync-containers/main.go b/cmd/sync-containers/main.go
index 6317b4943..68308cfeb 100644
--- a/cmd/sync-containers/main.go
+++ b/cmd/sync-containers/main.go
@@ -1,214 +1,214 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-// The sync-containers command synchronizes container image tags from one
-// registry to another.
-//
-// It is intended as a workaround for ghcr.io's lack of good push credentials:
-// you can either authorize "classic" Personal Access Tokens in your org (which
-// are a common vector of very bad compromise), or you can get a short-lived
-// credential in a Github action.
-//
-// Since we publish to both Docker Hub and ghcr.io, we use this program in a
-// Github action to effectively rsync from docker hub into ghcr.io, so that we
-// can continue to forbid dangerous Personal Access Tokens in the tailscale org.
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "log"
- "sort"
- "strings"
-
- "github.com/google/go-containerregistry/pkg/authn"
- "github.com/google/go-containerregistry/pkg/authn/github"
- "github.com/google/go-containerregistry/pkg/name"
- v1 "github.com/google/go-containerregistry/pkg/v1"
- "github.com/google/go-containerregistry/pkg/v1/remote"
- "github.com/google/go-containerregistry/pkg/v1/types"
-)
-
-var (
- src = flag.String("src", "", "Source image")
- dst = flag.String("dst", "", "Destination image")
- max = flag.Int("max", 0, "Maximum number of tags to sync (0 for all tags)")
- dryRun = flag.Bool("dry-run", true, "Don't actually sync anything")
-)
-
-func main() {
- flag.Parse()
-
- if *src == "" {
- log.Fatalf("--src is required")
- }
- if *dst == "" {
- log.Fatalf("--dst is required")
- }
-
- keychain := authn.NewMultiKeychain(authn.DefaultKeychain, github.Keychain)
- opts := []remote.Option{
- remote.WithAuthFromKeychain(keychain),
- remote.WithContext(context.Background()),
- }
-
- stags, err := listTags(*src, opts...)
- if err != nil {
- log.Fatalf("listing source tags: %v", err)
- }
- dtags, err := listTags(*dst, opts...)
- if err != nil {
- log.Fatalf("listing destination tags: %v", err)
- }
-
- add, remove := diffTags(stags, dtags)
- if l := len(add); l > 0 {
- log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", "))
- if *max > 0 && l > *max {
- log.Printf("Limiting sync to %d tags", *max)
- add = add[:*max]
- }
- }
- for _, tag := range add {
- if !*dryRun {
- log.Printf("Syncing tag %q", tag)
- if err := copyTag(*src, *dst, tag, opts...); err != nil {
- log.Printf("Syncing tag %q: progress error: %v", tag, err)
- }
- } else {
- log.Printf("Dry run: would sync tag %q", tag)
- }
- }
-
- if len(remove) > 0 {
- log.Printf("%d tags to remove: %s\n", len(remove), strings.Join(remove, ", "))
- log.Printf("Not removing any tags for safety.\n")
- }
-
- var wellKnown = [...]string{"latest", "stable"}
- for _, tag := range wellKnown {
- if needsUpdate(*src, *dst, tag) {
- if err := copyTag(*src, *dst, tag, opts...); err != nil {
- log.Printf("Updating tag %q: progress error: %v", tag, err)
- }
- }
- }
-}
-
-func copyTag(srcStr, dstStr, tag string, opts ...remote.Option) error {
- src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
- if err != nil {
- return err
- }
- dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
- if err != nil {
- return err
- }
-
- desc, err := remote.Get(src)
- if err != nil {
- return err
- }
-
- ch := make(chan v1.Update, 10)
- opts = append(opts, remote.WithProgress(ch))
- progressDone := make(chan struct{})
-
- go func() {
- defer close(progressDone)
- for p := range ch {
- fmt.Printf("Syncing tag %q: %d%% (%d/%d)\n", tag, int(float64(p.Complete)/float64(p.Total)*100), p.Complete, p.Total)
- if p.Error != nil {
- fmt.Printf("error: %v\n", p.Error)
- }
- }
- }()
-
- switch desc.MediaType {
- case types.OCIManifestSchema1, types.DockerManifestSchema2:
- img, err := desc.Image()
- if err != nil {
- return err
- }
- if err := remote.Write(dst, img, opts...); err != nil {
- return err
- }
- case types.OCIImageIndex, types.DockerManifestList:
- idx, err := desc.ImageIndex()
- if err != nil {
- return err
- }
- if err := remote.WriteIndex(dst, idx, opts...); err != nil {
- return err
- }
- }
-
- <-progressDone
- return nil
-}
-
-func listTags(repoStr string, opts ...remote.Option) ([]string, error) {
- repo, err := name.NewRepository(repoStr)
- if err != nil {
- return nil, err
- }
-
- tags, err := remote.List(repo, opts...)
- if err != nil {
- return nil, err
- }
-
- sort.Strings(tags)
- return tags, nil
-}
-
-func diffTags(src, dst []string) (add, remove []string) {
- srcd := make(map[string]bool)
- for _, tag := range src {
- srcd[tag] = true
- }
- dstd := make(map[string]bool)
- for _, tag := range dst {
- dstd[tag] = true
- }
-
- for _, tag := range src {
- if !dstd[tag] {
- add = append(add, tag)
- }
- }
- for _, tag := range dst {
- if !srcd[tag] {
- remove = append(remove, tag)
- }
- }
- sort.Strings(add)
- sort.Strings(remove)
- return add, remove
-}
-
-func needsUpdate(srcStr, dstStr, tag string) bool {
- src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
- if err != nil {
- return false
- }
- dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
- if err != nil {
- return false
- }
-
- srcDesc, err := remote.Get(src)
- if err != nil {
- return false
- }
-
- dstDesc, err := remote.Get(dst)
- if err != nil {
- return true
- }
-
- return srcDesc.Digest != dstDesc.Digest
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+// The sync-containers command synchronizes container image tags from one
+// registry to another.
+//
+// It is intended as a workaround for ghcr.io's lack of good push credentials:
+// you can either authorize "classic" Personal Access Tokens in your org (which
+// are a common vector of very bad compromise), or you can get a short-lived
+// credential in a Github action.
+//
+// Since we publish to both Docker Hub and ghcr.io, we use this program in a
+// Github action to effectively rsync from docker hub into ghcr.io, so that we
+// can continue to forbid dangerous Personal Access Tokens in the tailscale org.
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "sort"
+ "strings"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/authn/github"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+)
+
+var (
+ src = flag.String("src", "", "Source image")
+ dst = flag.String("dst", "", "Destination image")
+ max = flag.Int("max", 0, "Maximum number of tags to sync (0 for all tags)")
+ dryRun = flag.Bool("dry-run", true, "Don't actually sync anything")
+)
+
+func main() {
+ flag.Parse()
+
+ if *src == "" {
+ log.Fatalf("--src is required")
+ }
+ if *dst == "" {
+ log.Fatalf("--dst is required")
+ }
+
+ keychain := authn.NewMultiKeychain(authn.DefaultKeychain, github.Keychain)
+ opts := []remote.Option{
+ remote.WithAuthFromKeychain(keychain),
+ remote.WithContext(context.Background()),
+ }
+
+ stags, err := listTags(*src, opts...)
+ if err != nil {
+ log.Fatalf("listing source tags: %v", err)
+ }
+ dtags, err := listTags(*dst, opts...)
+ if err != nil {
+ log.Fatalf("listing destination tags: %v", err)
+ }
+
+ add, remove := diffTags(stags, dtags)
+ if l := len(add); l > 0 {
+ log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", "))
+ if *max > 0 && l > *max {
+ log.Printf("Limiting sync to %d tags", *max)
+ add = add[:*max]
+ }
+ }
+ for _, tag := range add {
+ if !*dryRun {
+ log.Printf("Syncing tag %q", tag)
+ if err := copyTag(*src, *dst, tag, opts...); err != nil {
+ log.Printf("Syncing tag %q: progress error: %v", tag, err)
+ }
+ } else {
+ log.Printf("Dry run: would sync tag %q", tag)
+ }
+ }
+
+ if len(remove) > 0 {
+ log.Printf("%d tags to remove: %s\n", len(remove), strings.Join(remove, ", "))
+ log.Printf("Not removing any tags for safety.\n")
+ }
+
+ var wellKnown = [...]string{"latest", "stable"}
+ for _, tag := range wellKnown {
+ if needsUpdate(*src, *dst, tag) {
+ if err := copyTag(*src, *dst, tag, opts...); err != nil {
+ log.Printf("Updating tag %q: progress error: %v", tag, err)
+ }
+ }
+ }
+}
+
+func copyTag(srcStr, dstStr, tag string, opts ...remote.Option) error {
+ src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
+ if err != nil {
+ return err
+ }
+ dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
+ if err != nil {
+ return err
+ }
+
+ desc, err := remote.Get(src)
+ if err != nil {
+ return err
+ }
+
+ ch := make(chan v1.Update, 10)
+ opts = append(opts, remote.WithProgress(ch))
+ progressDone := make(chan struct{})
+
+ go func() {
+ defer close(progressDone)
+ for p := range ch {
+ fmt.Printf("Syncing tag %q: %d%% (%d/%d)\n", tag, int(float64(p.Complete)/float64(p.Total)*100), p.Complete, p.Total)
+ if p.Error != nil {
+ fmt.Printf("error: %v\n", p.Error)
+ }
+ }
+ }()
+
+ switch desc.MediaType {
+ case types.OCIManifestSchema1, types.DockerManifestSchema2:
+ img, err := desc.Image()
+ if err != nil {
+ return err
+ }
+ if err := remote.Write(dst, img, opts...); err != nil {
+ return err
+ }
+ case types.OCIImageIndex, types.DockerManifestList:
+ idx, err := desc.ImageIndex()
+ if err != nil {
+ return err
+ }
+ if err := remote.WriteIndex(dst, idx, opts...); err != nil {
+ return err
+ }
+ }
+
+ <-progressDone
+ return nil
+}
+
+func listTags(repoStr string, opts ...remote.Option) ([]string, error) {
+ repo, err := name.NewRepository(repoStr)
+ if err != nil {
+ return nil, err
+ }
+
+ tags, err := remote.List(repo, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Strings(tags)
+ return tags, nil
+}
+
+func diffTags(src, dst []string) (add, remove []string) {
+ srcd := make(map[string]bool)
+ for _, tag := range src {
+ srcd[tag] = true
+ }
+ dstd := make(map[string]bool)
+ for _, tag := range dst {
+ dstd[tag] = true
+ }
+
+ for _, tag := range src {
+ if !dstd[tag] {
+ add = append(add, tag)
+ }
+ }
+ for _, tag := range dst {
+ if !srcd[tag] {
+ remove = append(remove, tag)
+ }
+ }
+ sort.Strings(add)
+ sort.Strings(remove)
+ return add, remove
+}
+
+func needsUpdate(srcStr, dstStr, tag string) bool {
+ src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
+ if err != nil {
+ return false
+ }
+ dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
+ if err != nil {
+ return false
+ }
+
+ srcDesc, err := remote.Get(src)
+ if err != nil {
+ return false
+ }
+
+ dstDesc, err := remote.Get(dst)
+ if err != nil {
+ return true
+ }
+
+ return srcDesc.Digest != dstDesc.Digest
+}
diff --git a/cmd/tailscale/cli/diag.go b/cmd/tailscale/cli/diag.go
index ebf26985f..a1616f851 100644
--- a/cmd/tailscale/cli/diag.go
+++ b/cmd/tailscale/cli/diag.go
@@ -1,74 +1,74 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux || windows || darwin
-
-package cli
-
-import (
- "fmt"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
-
- ps "github.com/mitchellh/go-ps"
- "tailscale.com/version/distro"
-)
-
-// fixTailscaledConnectError is called when the local tailscaled has
-// been determined unreachable due to the provided origErr value. It
-// returns either the same error or a better one to help the user
-// understand why tailscaled isn't running for their platform.
-func fixTailscaledConnectError(origErr error) error {
- procs, err := ps.Processes()
- if err != nil {
- return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
- }
- var foundProc ps.Process
- for _, proc := range procs {
- base := filepath.Base(proc.Executable())
- if base == "tailscaled" {
- foundProc = proc
- break
- }
- if runtime.GOOS == "darwin" && base == "IPNExtension" {
- foundProc = proc
- break
- }
- if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
- foundProc = proc
- break
- }
- }
- if foundProc == nil {
- switch runtime.GOOS {
- case "windows":
- return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
- case "darwin":
- return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
- case "linux":
- var hint string
- if isSystemdSystem() {
- hint = " (sudo systemctl start tailscaled ?)"
- }
- return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running%s", hint)
- }
- return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
- }
- return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
-}
-
-// isSystemdSystem reports whether the current machine uses systemd
-// and in particular whether the systemctl command is available.
-func isSystemdSystem() bool {
- if runtime.GOOS != "linux" {
- return false
- }
- switch distro.Get() {
- case distro.QNAP, distro.Gokrazy, distro.Synology, distro.Unraid:
- return false
- }
- _, err := exec.LookPath("systemctl")
- return err == nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux || windows || darwin
+
+package cli
+
+import (
+ "fmt"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ ps "github.com/mitchellh/go-ps"
+ "tailscale.com/version/distro"
+)
+
+// fixTailscaledConnectError is called when the local tailscaled has
+// been determined unreachable due to the provided origErr value. It
+// returns either the same error or a better one to help the user
+// understand why tailscaled isn't running for their platform.
+func fixTailscaledConnectError(origErr error) error {
+ procs, err := ps.Processes()
+ if err != nil {
+ return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
+ }
+ var foundProc ps.Process
+ for _, proc := range procs {
+ base := filepath.Base(proc.Executable())
+ if base == "tailscaled" {
+ foundProc = proc
+ break
+ }
+ if runtime.GOOS == "darwin" && base == "IPNExtension" {
+ foundProc = proc
+ break
+ }
+ if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
+ foundProc = proc
+ break
+ }
+ }
+ if foundProc == nil {
+ switch runtime.GOOS {
+ case "windows":
+ return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
+ case "darwin":
+ return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
+ case "linux":
+ var hint string
+ if isSystemdSystem() {
+ hint = " (sudo systemctl start tailscaled ?)"
+ }
+ return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running%s", hint)
+ }
+ return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
+ }
+ return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
+}
+
+// isSystemdSystem reports whether the current machine uses systemd
+// and in particular whether the systemctl command is available.
+func isSystemdSystem() bool {
+ if runtime.GOOS != "linux" {
+ return false
+ }
+ switch distro.Get() {
+ case distro.QNAP, distro.Gokrazy, distro.Synology, distro.Unraid:
+ return false
+ }
+ _, err := exec.LookPath("systemctl")
+ return err == nil
+}
diff --git a/cmd/tailscale/cli/diag_other.go b/cmd/tailscale/cli/diag_other.go
index ece10cc79..82058ef7a 100644
--- a/cmd/tailscale/cli/diag_other.go
+++ b/cmd/tailscale/cli/diag_other.go
@@ -1,15 +1,15 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !linux && !windows && !darwin
-
-package cli
-
-import "fmt"
-
-// The github.com/mitchellh/go-ps package doesn't work on all platforms,
-// so just don't diagnose connect failures.
-
-func fixTailscaledConnectError(origErr error) error {
- return fmt.Errorf("failed to connect to local tailscaled process (is it running?); got: %w", origErr)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !linux && !windows && !darwin
+
+package cli
+
+import "fmt"
+
+// The github.com/mitchellh/go-ps package doesn't work on all platforms,
+// so just don't diagnose connect failures.
+
+func fixTailscaledConnectError(origErr error) error {
+ return fmt.Errorf("failed to connect to local tailscaled process (is it running?); got: %w", origErr)
+}
diff --git a/cmd/tailscale/cli/set_test.go b/cmd/tailscale/cli/set_test.go
index 15305c3ce..06ef8503f 100644
--- a/cmd/tailscale/cli/set_test.go
+++ b/cmd/tailscale/cli/set_test.go
@@ -1,131 +1,131 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package cli
-
-import (
- "net/netip"
- "reflect"
- "testing"
-
- "tailscale.com/ipn"
- "tailscale.com/net/tsaddr"
- "tailscale.com/types/ptr"
-)
-
-func TestCalcAdvertiseRoutesForSet(t *testing.T) {
- pfx := netip.MustParsePrefix
- tests := []struct {
- name string
- setExit *bool
- setRoutes *string
- was []netip.Prefix
- want []netip.Prefix
- }{
- {
- name: "empty",
- },
- {
- name: "advertise-exit",
- setExit: ptr.To(true),
- want: tsaddr.ExitRoutes(),
- },
- {
- name: "advertise-exit/already-routes",
- was: []netip.Prefix{pfx("34.0.0.0/16")},
- setExit: ptr.To(true),
- want: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- },
- {
- name: "advertise-exit/already-exit",
- was: tsaddr.ExitRoutes(),
- setExit: ptr.To(true),
- want: tsaddr.ExitRoutes(),
- },
- {
- name: "stop-advertise-exit",
- was: tsaddr.ExitRoutes(),
- setExit: ptr.To(false),
- want: nil,
- },
- {
- name: "stop-advertise-exit/with-routes",
- was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- setExit: ptr.To(false),
- want: []netip.Prefix{pfx("34.0.0.0/16")},
- },
- {
- name: "advertise-routes",
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
- },
- {
- name: "advertise-routes/already-exit",
- was: tsaddr.ExitRoutes(),
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- },
- {
- name: "advertise-routes/already-diff-routes",
- was: []netip.Prefix{pfx("34.0.0.0/16")},
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
- },
- {
- name: "stop-advertise-routes",
- was: []netip.Prefix{pfx("34.0.0.0/16")},
- setRoutes: ptr.To(""),
- want: nil,
- },
- {
- name: "stop-advertise-routes/already-exit",
- was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- setRoutes: ptr.To(""),
- want: tsaddr.ExitRoutes(),
- },
- {
- name: "advertise-routes-and-exit",
- setExit: ptr.To(true),
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- },
- {
- name: "advertise-routes-and-exit/already-exit",
- was: tsaddr.ExitRoutes(),
- setExit: ptr.To(true),
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- },
- {
- name: "advertise-routes-and-exit/already-routes",
- was: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
- setExit: ptr.To(true),
- setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
- want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- curPrefs := &ipn.Prefs{
- AdvertiseRoutes: tc.was,
- }
- sa := setArgsT{}
- if tc.setExit != nil {
- sa.advertiseDefaultRoute = *tc.setExit
- }
- if tc.setRoutes != nil {
- sa.advertiseRoutes = *tc.setRoutes
- }
- got, err := calcAdvertiseRoutesForSet(tc.setExit != nil, tc.setRoutes != nil, curPrefs, sa)
- if err != nil {
- t.Fatal(err)
- }
- tsaddr.SortPrefixes(got)
- tsaddr.SortPrefixes(tc.want)
- if !reflect.DeepEqual(got, tc.want) {
- t.Errorf("got %v, want %v", got, tc.want)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package cli
+
+import (
+ "net/netip"
+ "reflect"
+ "testing"
+
+ "tailscale.com/ipn"
+ "tailscale.com/net/tsaddr"
+ "tailscale.com/types/ptr"
+)
+
+func TestCalcAdvertiseRoutesForSet(t *testing.T) {
+ pfx := netip.MustParsePrefix
+ tests := []struct {
+ name string
+ setExit *bool
+ setRoutes *string
+ was []netip.Prefix
+ want []netip.Prefix
+ }{
+ {
+ name: "empty",
+ },
+ {
+ name: "advertise-exit",
+ setExit: ptr.To(true),
+ want: tsaddr.ExitRoutes(),
+ },
+ {
+ name: "advertise-exit/already-routes",
+ was: []netip.Prefix{pfx("34.0.0.0/16")},
+ setExit: ptr.To(true),
+ want: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ },
+ {
+ name: "advertise-exit/already-exit",
+ was: tsaddr.ExitRoutes(),
+ setExit: ptr.To(true),
+ want: tsaddr.ExitRoutes(),
+ },
+ {
+ name: "stop-advertise-exit",
+ was: tsaddr.ExitRoutes(),
+ setExit: ptr.To(false),
+ want: nil,
+ },
+ {
+ name: "stop-advertise-exit/with-routes",
+ was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ setExit: ptr.To(false),
+ want: []netip.Prefix{pfx("34.0.0.0/16")},
+ },
+ {
+ name: "advertise-routes",
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
+ },
+ {
+ name: "advertise-routes/already-exit",
+ was: tsaddr.ExitRoutes(),
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ },
+ {
+ name: "advertise-routes/already-diff-routes",
+ was: []netip.Prefix{pfx("34.0.0.0/16")},
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
+ },
+ {
+ name: "stop-advertise-routes",
+ was: []netip.Prefix{pfx("34.0.0.0/16")},
+ setRoutes: ptr.To(""),
+ want: nil,
+ },
+ {
+ name: "stop-advertise-routes/already-exit",
+ was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ setRoutes: ptr.To(""),
+ want: tsaddr.ExitRoutes(),
+ },
+ {
+ name: "advertise-routes-and-exit",
+ setExit: ptr.To(true),
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ },
+ {
+ name: "advertise-routes-and-exit/already-exit",
+ was: tsaddr.ExitRoutes(),
+ setExit: ptr.To(true),
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ },
+ {
+ name: "advertise-routes-and-exit/already-routes",
+ was: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
+ setExit: ptr.To(true),
+ setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
+ want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ curPrefs := &ipn.Prefs{
+ AdvertiseRoutes: tc.was,
+ }
+ sa := setArgsT{}
+ if tc.setExit != nil {
+ sa.advertiseDefaultRoute = *tc.setExit
+ }
+ if tc.setRoutes != nil {
+ sa.advertiseRoutes = *tc.setRoutes
+ }
+ got, err := calcAdvertiseRoutesForSet(tc.setExit != nil, tc.setRoutes != nil, curPrefs, sa)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tsaddr.SortPrefixes(got)
+ tsaddr.SortPrefixes(tc.want)
+ if !reflect.DeepEqual(got, tc.want) {
+ t.Errorf("got %v, want %v", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/cmd/tailscale/cli/ssh_exec.go b/cmd/tailscale/cli/ssh_exec.go
index 10e52903d..7f7d2a4d5 100644
--- a/cmd/tailscale/cli/ssh_exec.go
+++ b/cmd/tailscale/cli/ssh_exec.go
@@ -1,24 +1,24 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !js && !windows
-
-package cli
-
-import (
- "errors"
- "os"
- "os/exec"
- "syscall"
-)
-
-func findSSH() (string, error) {
- return exec.LookPath("ssh")
-}
-
-func execSSH(ssh string, argv []string) error {
- if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
- return err
- }
- return errors.New("unreachable")
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !js && !windows
+
+package cli
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "syscall"
+)
+
+func findSSH() (string, error) {
+ return exec.LookPath("ssh")
+}
+
+func execSSH(ssh string, argv []string) error {
+ if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
+ return err
+ }
+ return errors.New("unreachable")
+}
diff --git a/cmd/tailscale/cli/ssh_exec_js.go b/cmd/tailscale/cli/ssh_exec_js.go
index 40effc7ca..aa0c09e89 100644
--- a/cmd/tailscale/cli/ssh_exec_js.go
+++ b/cmd/tailscale/cli/ssh_exec_js.go
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package cli
-
-import (
- "errors"
-)
-
-func findSSH() (string, error) {
- return "", errors.New("Not implemented")
-}
-
-func execSSH(ssh string, argv []string) error {
- return errors.New("Not implemented")
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package cli
+
+import (
+ "errors"
+)
+
+func findSSH() (string, error) {
+ return "", errors.New("Not implemented")
+}
+
+func execSSH(ssh string, argv []string) error {
+ return errors.New("Not implemented")
+}
diff --git a/cmd/tailscale/cli/ssh_exec_windows.go b/cmd/tailscale/cli/ssh_exec_windows.go
index e249afe66..30ab70d04 100644
--- a/cmd/tailscale/cli/ssh_exec_windows.go
+++ b/cmd/tailscale/cli/ssh_exec_windows.go
@@ -1,37 +1,37 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package cli
-
-import (
- "errors"
- "os"
- "os/exec"
- "path/filepath"
-)
-
-func findSSH() (string, error) {
- // use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
- // occurred with ssh.exe provided by msys2/cygwin and other environments.
- if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
- exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
- if st, err := os.Stat(exe); err == nil && !st.IsDir() {
- return exe, nil
- }
- }
- return exec.LookPath("ssh")
-}
-
-func execSSH(ssh string, argv []string) error {
- // Don't use syscall.Exec on Windows, it's not fully implemented.
- cmd := exec.Command(ssh, argv[1:]...)
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- var ee *exec.ExitError
- err := cmd.Run()
- if errors.As(err, &ee) {
- os.Exit(ee.ExitCode())
- }
- return err
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package cli
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func findSSH() (string, error) {
+ // use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
+ // occurred with ssh.exe provided by msys2/cygwin and other environments.
+ if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
+ exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
+ if st, err := os.Stat(exe); err == nil && !st.IsDir() {
+ return exe, nil
+ }
+ }
+ return exec.LookPath("ssh")
+}
+
+func execSSH(ssh string, argv []string) error {
+ // Don't use syscall.Exec on Windows, it's not fully implemented.
+ cmd := exec.Command(ssh, argv[1:]...)
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ var ee *exec.ExitError
+ err := cmd.Run()
+ if errors.As(err, &ee) {
+ os.Exit(ee.ExitCode())
+ }
+ return err
+}
diff --git a/cmd/tailscale/cli/ssh_unix.go b/cmd/tailscale/cli/ssh_unix.go
index 71c0caaa6..07423b69f 100644
--- a/cmd/tailscale/cli/ssh_unix.go
+++ b/cmd/tailscale/cli/ssh_unix.go
@@ -1,49 +1,49 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !wasm && !windows && !plan9
-
-package cli
-
-import (
- "bytes"
- "os"
- "path/filepath"
- "runtime"
- "strconv"
-
- "golang.org/x/sys/unix"
-)
-
-func init() {
- getSSHClientEnvVar = func() string {
- if os.Getenv("SUDO_USER") == "" {
- // No sudo, just check the env.
- return os.Getenv("SSH_CLIENT")
- }
- if runtime.GOOS != "linux" {
- // TODO(maisem): implement this for other platforms. It's not clear
- // if there is a way to get the environment for a given process on
- // darwin and bsd.
- return ""
- }
- // SID is the session ID of the user's login session.
- // It is also the process ID of the original shell that the user logged in with.
- // We only need to check the environment of that process.
- sid, err := unix.Getsid(os.Getpid())
- if err != nil {
- return ""
- }
- b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(sid), "environ"))
- if err != nil {
- return ""
- }
- prefix := []byte("SSH_CLIENT=")
- for _, env := range bytes.Split(b, []byte{0}) {
- if bytes.HasPrefix(env, prefix) {
- return string(env[len(prefix):])
- }
- }
- return ""
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !wasm && !windows && !plan9
+
+package cli
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ getSSHClientEnvVar = func() string {
+ if os.Getenv("SUDO_USER") == "" {
+ // No sudo, just check the env.
+ return os.Getenv("SSH_CLIENT")
+ }
+ if runtime.GOOS != "linux" {
+ // TODO(maisem): implement this for other platforms. It's not clear
+ // if there is a way to get the environment for a given process on
+ // darwin and bsd.
+ return ""
+ }
+ // SID is the session ID of the user's login session.
+ // It is also the process ID of the original shell that the user logged in with.
+ // We only need to check the environment of that process.
+ sid, err := unix.Getsid(os.Getpid())
+ if err != nil {
+ return ""
+ }
+ b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(sid), "environ"))
+ if err != nil {
+ return ""
+ }
+ prefix := []byte("SSH_CLIENT=")
+ for _, env := range bytes.Split(b, []byte{0}) {
+ if bytes.HasPrefix(env, prefix) {
+ return string(env[len(prefix):])
+ }
+ }
+ return ""
+ }
+}
diff --git a/cmd/tailscale/cli/web_test.go b/cmd/tailscale/cli/web_test.go
index f2470b364..f1880597e 100644
--- a/cmd/tailscale/cli/web_test.go
+++ b/cmd/tailscale/cli/web_test.go
@@ -1,45 +1,45 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package cli
-
-import (
- "testing"
-)
-
-func TestUrlOfListenAddr(t *testing.T) {
- tests := []struct {
- name string
- in, want string
- }{
- {
- name: "TestLocalhost",
- in: "localhost:8088",
- want: "http://localhost:8088",
- },
- {
- name: "TestNoHost",
- in: ":8088",
- want: "http://127.0.0.1:8088",
- },
- {
- name: "TestExplicitHost",
- in: "127.0.0.2:8088",
- want: "http://127.0.0.2:8088",
- },
- {
- name: "TestIPv6",
- in: "[::1]:8088",
- want: "http://[::1]:8088",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- u := urlOfListenAddr(tt.in)
- if u != tt.want {
- t.Errorf("expected url: %q, got: %q", tt.want, u)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package cli
+
+import (
+ "testing"
+)
+
+func TestUrlOfListenAddr(t *testing.T) {
+ tests := []struct {
+ name string
+ in, want string
+ }{
+ {
+ name: "TestLocalhost",
+ in: "localhost:8088",
+ want: "http://localhost:8088",
+ },
+ {
+ name: "TestNoHost",
+ in: ":8088",
+ want: "http://127.0.0.1:8088",
+ },
+ {
+ name: "TestExplicitHost",
+ in: "127.0.0.2:8088",
+ want: "http://127.0.0.2:8088",
+ },
+ {
+ name: "TestIPv6",
+ in: "[::1]:8088",
+ want: "http://[::1]:8088",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ u := urlOfListenAddr(tt.in)
+ if u != tt.want {
+ t.Errorf("expected url: %q, got: %q", tt.want, u)
+ }
+ })
+ }
+}
diff --git a/cmd/tailscale/generate.go b/cmd/tailscale/generate.go
index 5c2e9be91..fa38b3704 100644
--- a/cmd/tailscale/generate.go
+++ b/cmd/tailscale/generate.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
-//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
-//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
+//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
+//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go
index f6adb6c19..1848d6508 100644
--- a/cmd/tailscale/tailscale.go
+++ b/cmd/tailscale/tailscale.go
@@ -1,26 +1,26 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// The tailscale command is the Tailscale command-line client. It interacts
-// with the tailscaled node agent.
-package main // import "tailscale.com/cmd/tailscale"
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "tailscale.com/cmd/tailscale/cli"
-)
-
-func main() {
- args := os.Args[1:]
- if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
- args = []string{"web", "-cgi"}
- }
- if err := cli.Run(args); err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// The tailscale command is the Tailscale command-line client. It interacts
+// with the tailscaled node agent.
+package main // import "tailscale.com/cmd/tailscale"
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "tailscale.com/cmd/tailscale/cli"
+)
+
+func main() {
+ args := os.Args[1:]
+ if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
+ args = []string{"web", "-cgi"}
+ }
+ if err := cli.Run(args); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/tailscale/windows-manifest.xml b/cmd/tailscale/windows-manifest.xml
index 6c5f46058..5eaa54fa5 100644
--- a/cmd/tailscale/windows-manifest.xml
+++ b/cmd/tailscale/windows-manifest.xml
@@ -1,13 +1,13 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-
- <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
- <application>
- <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
- <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
- <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
- <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
- </application>
- </compatibility>
-
-</assembly>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
+ </application>
+ </compatibility>
+
+</assembly>
diff --git a/cmd/tailscaled/childproc/childproc.go b/cmd/tailscaled/childproc/childproc.go
index cc83a06c6..068015c59 100644
--- a/cmd/tailscaled/childproc/childproc.go
+++ b/cmd/tailscaled/childproc/childproc.go
@@ -1,19 +1,19 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package childproc allows other packages to register "tailscaled be-child"
-// child process hook code. This avoids duplicating build tags in the
-// tailscaled package. Instead, the code that needs to fork/exec the self
-// executable (when it's tailscaled) can instead register the code
-// they want to run.
-package childproc
-
-var Code = map[string]func([]string) error{}
-
-// Add registers code f to run as 'tailscaled be-child <typ> [args]'.
-func Add(typ string, f func(args []string) error) {
- if _, dup := Code[typ]; dup {
- panic("dup hook " + typ)
- }
- Code[typ] = f
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package childproc allows other packages to register "tailscaled be-child"
+// child process hook code. This avoids duplicating build tags in the
+// tailscaled package. Instead, the code that needs to fork/exec the self
+// executable (when it's tailscaled) can instead register the code
+// they want to run.
+package childproc
+
+var Code = map[string]func([]string) error{}
+
+// Add registers code f to run as 'tailscaled be-child <typ> [args]'.
+func Add(typ string, f func(args []string) error) {
+ if _, dup := Code[typ]; dup {
+ panic("dup hook " + typ)
+ }
+ Code[typ] = f
+}
diff --git a/cmd/tailscaled/generate.go b/cmd/tailscaled/generate.go
index 5c2e9be91..fa38b3704 100644
--- a/cmd/tailscaled/generate.go
+++ b/cmd/tailscaled/generate.go
@@ -1,8 +1,8 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
-//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
-//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
+//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
+//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
diff --git a/cmd/tailscaled/install_darwin.go b/cmd/tailscaled/install_darwin.go
index 05e5eaed8..9013b39ba 100644
--- a/cmd/tailscaled/install_darwin.go
+++ b/cmd/tailscaled/install_darwin.go
@@ -1,199 +1,199 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build go1.19
-
-package main
-
-import (
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "os/exec"
- "path/filepath"
-)
-
-func init() {
- installSystemDaemon = installSystemDaemonDarwin
- uninstallSystemDaemon = uninstallSystemDaemonDarwin
-}
-
-// darwinLaunchdPlist is the launchd.plist that's written to
-// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
-// future) a user-specific location.
-//
-// See man launchd.plist.
-const darwinLaunchdPlist = `
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-
- <key>Label</key>
- <string>com.tailscale.tailscaled</string>
-
- <key>ProgramArguments</key>
- <array>
- <string>/usr/local/bin/tailscaled</string>
- </array>
-
- <key>RunAtLoad</key>
- <true/>
-
-</dict>
-</plist>
-`
-
-const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
-const targetBin = "/usr/local/bin/tailscaled"
-const service = "com.tailscale.tailscaled"
-
-func uninstallSystemDaemonDarwin(args []string) (ret error) {
- if len(args) > 0 {
- return errors.New("uninstall subcommand takes no arguments")
- }
-
- plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
- _ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
- running := err == nil
-
- if running {
- out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
- if err != nil {
- fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
- ret = err
- }
- out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
- if err != nil {
- fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
- if ret == nil {
- ret = err
- }
- }
- }
-
- if err := os.Remove(sysPlist); err != nil {
- if os.IsNotExist(err) {
- err = nil
- }
- if ret == nil {
- ret = err
- }
- }
-
- // Do not delete targetBin if it's a symlink, which happens if it was installed via
- // Homebrew.
- if isSymlink(targetBin) {
- return ret
- }
-
- if err := os.Remove(targetBin); err != nil {
- if os.IsNotExist(err) {
- err = nil
- }
- if ret == nil {
- ret = err
- }
- }
- return ret
-}
-
-func installSystemDaemonDarwin(args []string) (err error) {
- if len(args) > 0 {
- return errors.New("install subcommand takes no arguments")
- }
- defer func() {
- if err != nil && os.Getuid() != 0 {
- err = fmt.Errorf("%w; try running tailscaled with sudo", err)
- }
- }()
-
- // Best effort:
- uninstallSystemDaemonDarwin(nil)
-
- exe, err := os.Executable()
- if err != nil {
- return fmt.Errorf("failed to find our own executable path: %w", err)
- }
-
- same, err := sameFile(exe, targetBin)
- if err != nil {
- return err
- }
-
- // Do not overwrite targetBin with the binary file if it it's already
- // pointing to it. This is primarily to handle Homebrew that writes
- // /usr/local/bin/tailscaled is a symlink to the actual binary.
- if !same {
- if err := copyBinary(exe, targetBin); err != nil {
- return err
- }
- }
- if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
- return err
- }
-
- if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
- return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
- }
-
- if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
- return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
- }
-
- return nil
-}
-
-// copyBinary copies binary file `src` into `dst`.
-func copyBinary(src, dst string) error {
- if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
- return err
- }
- tmpBin := dst + ".tmp"
- f, err := os.Create(tmpBin)
- if err != nil {
- return err
- }
- srcf, err := os.Open(src)
- if err != nil {
- f.Close()
- return err
- }
- _, err = io.Copy(f, srcf)
- srcf.Close()
- if err != nil {
- f.Close()
- return err
- }
- if err := f.Close(); err != nil {
- return err
- }
- if err := os.Chmod(tmpBin, 0755); err != nil {
- return err
- }
- if err := os.Rename(tmpBin, dst); err != nil {
- return err
- }
-
- return nil
-}
-
-func isSymlink(path string) bool {
- fi, err := os.Lstat(path)
- return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink)
-}
-
-// sameFile returns true if both file paths exist and resolve to the same file.
-func sameFile(path1, path2 string) (bool, error) {
- dst1, err := filepath.EvalSymlinks(path1)
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err)
- }
- dst2, err := filepath.EvalSymlinks(path2)
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err)
- }
- return dst1 == dst2, nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build go1.19
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func init() {
+ installSystemDaemon = installSystemDaemonDarwin
+ uninstallSystemDaemon = uninstallSystemDaemonDarwin
+}
+
+// darwinLaunchdPlist is the launchd.plist that's written to
+// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
+// future) a user-specific location.
+//
+// See man launchd.plist.
+const darwinLaunchdPlist = `
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+
+ <key>Label</key>
+ <string>com.tailscale.tailscaled</string>
+
+ <key>ProgramArguments</key>
+ <array>
+ <string>/usr/local/bin/tailscaled</string>
+ </array>
+
+ <key>RunAtLoad</key>
+ <true/>
+
+</dict>
+</plist>
+`
+
+const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
+const targetBin = "/usr/local/bin/tailscaled"
+const service = "com.tailscale.tailscaled"
+
+func uninstallSystemDaemonDarwin(args []string) (ret error) {
+ if len(args) > 0 {
+ return errors.New("uninstall subcommand takes no arguments")
+ }
+
+ plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
+ _ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
+ running := err == nil
+
+ if running {
+ out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
+ if err != nil {
+ fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
+ ret = err
+ }
+ out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
+ if err != nil {
+ fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
+ if ret == nil {
+ ret = err
+ }
+ }
+ }
+
+ if err := os.Remove(sysPlist); err != nil {
+ if os.IsNotExist(err) {
+ err = nil
+ }
+ if ret == nil {
+ ret = err
+ }
+ }
+
+ // Do not delete targetBin if it's a symlink, which happens if it was installed via
+ // Homebrew.
+ if isSymlink(targetBin) {
+ return ret
+ }
+
+ if err := os.Remove(targetBin); err != nil {
+ if os.IsNotExist(err) {
+ err = nil
+ }
+ if ret == nil {
+ ret = err
+ }
+ }
+ return ret
+}
+
+func installSystemDaemonDarwin(args []string) (err error) {
+ if len(args) > 0 {
+ return errors.New("install subcommand takes no arguments")
+ }
+ defer func() {
+ if err != nil && os.Getuid() != 0 {
+ err = fmt.Errorf("%w; try running tailscaled with sudo", err)
+ }
+ }()
+
+ // Best effort:
+ uninstallSystemDaemonDarwin(nil)
+
+ exe, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("failed to find our own executable path: %w", err)
+ }
+
+ same, err := sameFile(exe, targetBin)
+ if err != nil {
+ return err
+ }
+
+ // Do not overwrite targetBin with the binary file if it it's already
+ // pointing to it. This is primarily to handle Homebrew that writes
+ // /usr/local/bin/tailscaled is a symlink to the actual binary.
+ if !same {
+ if err := copyBinary(exe, targetBin); err != nil {
+ return err
+ }
+ }
+ if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
+ return err
+ }
+
+ if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
+ return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
+ }
+
+ if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
+ return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
+ }
+
+ return nil
+}
+
+// copyBinary copies binary file `src` into `dst`.
+func copyBinary(src, dst string) error {
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ return err
+ }
+ tmpBin := dst + ".tmp"
+ f, err := os.Create(tmpBin)
+ if err != nil {
+ return err
+ }
+ srcf, err := os.Open(src)
+ if err != nil {
+ f.Close()
+ return err
+ }
+ _, err = io.Copy(f, srcf)
+ srcf.Close()
+ if err != nil {
+ f.Close()
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ if err := os.Chmod(tmpBin, 0755); err != nil {
+ return err
+ }
+ if err := os.Rename(tmpBin, dst); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func isSymlink(path string) bool {
+ fi, err := os.Lstat(path)
+ return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink)
+}
+
+// sameFile returns true if both file paths exist and resolve to the same file.
+func sameFile(path1, path2 string) (bool, error) {
+ dst1, err := filepath.EvalSymlinks(path1)
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
+ return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err)
+ }
+ dst2, err := filepath.EvalSymlinks(path2)
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
+ return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err)
+ }
+ return dst1 == dst2, nil
+}
diff --git a/cmd/tailscaled/install_windows.go b/cmd/tailscaled/install_windows.go
index c36418642..9e39c8ab3 100644
--- a/cmd/tailscaled/install_windows.go
+++ b/cmd/tailscaled/install_windows.go
@@ -1,124 +1,124 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build go1.19
-
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
- "time"
-
- "golang.org/x/sys/windows"
- "golang.org/x/sys/windows/svc"
- "golang.org/x/sys/windows/svc/mgr"
- "tailscale.com/logtail/backoff"
- "tailscale.com/types/logger"
- "tailscale.com/util/osshare"
-)
-
-func init() {
- installSystemDaemon = installSystemDaemonWindows
- uninstallSystemDaemon = uninstallSystemDaemonWindows
-}
-
-func installSystemDaemonWindows(args []string) (err error) {
- m, err := mgr.Connect()
- if err != nil {
- return fmt.Errorf("failed to connect to Windows service manager: %v", err)
- }
-
- service, err := m.OpenService(serviceName)
- if err == nil {
- service.Close()
- return fmt.Errorf("service %q is already installed", serviceName)
- }
-
- // no such service; proceed to install the service.
-
- exe, err := os.Executable()
- if err != nil {
- return err
- }
-
- c := mgr.Config{
- ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
- StartType: mgr.StartAutomatic,
- ErrorControl: mgr.ErrorNormal,
- DisplayName: serviceName,
- Description: "Connects this computer to others on the Tailscale network.",
- }
-
- service, err = m.CreateService(serviceName, exe, c)
- if err != nil {
- return fmt.Errorf("failed to create %q service: %v", serviceName, err)
- }
- defer service.Close()
-
- // Exponential backoff is often too aggressive, so use (mostly)
- // squares instead.
- ra := []mgr.RecoveryAction{
- {mgr.ServiceRestart, 1 * time.Second},
- {mgr.ServiceRestart, 2 * time.Second},
- {mgr.ServiceRestart, 4 * time.Second},
- {mgr.ServiceRestart, 9 * time.Second},
- {mgr.ServiceRestart, 16 * time.Second},
- {mgr.ServiceRestart, 25 * time.Second},
- {mgr.ServiceRestart, 36 * time.Second},
- {mgr.ServiceRestart, 49 * time.Second},
- {mgr.ServiceRestart, 64 * time.Second},
- }
- const resetPeriodSecs = 60
- err = service.SetRecoveryActions(ra, resetPeriodSecs)
- if err != nil {
- return fmt.Errorf("failed to set service recovery actions: %v", err)
- }
-
- return nil
-}
-
-func uninstallSystemDaemonWindows(args []string) (ret error) {
- // Remove file sharing from Windows shell (noop in non-windows)
- osshare.SetFileSharingEnabled(false, logger.Discard)
-
- m, err := mgr.Connect()
- if err != nil {
- return fmt.Errorf("failed to connect to Windows service manager: %v", err)
- }
- defer m.Disconnect()
-
- service, err := m.OpenService(serviceName)
- if err != nil {
- return fmt.Errorf("failed to open %q service: %v", serviceName, err)
- }
-
- st, err := service.Query()
- if err != nil {
- service.Close()
- return fmt.Errorf("failed to query service state: %v", err)
- }
- if st.State != svc.Stopped {
- service.Control(svc.Stop)
- }
- err = service.Delete()
- service.Close()
- if err != nil {
- return fmt.Errorf("failed to delete service: %v", err)
- }
-
- bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
- end := time.Now().Add(15 * time.Second)
- for time.Until(end) > 0 {
- service, err = m.OpenService(serviceName)
- if err != nil {
- // service is no longer openable; success!
- break
- }
- service.Close()
- bo.BackOff(context.Background(), errors.New("service not deleted"))
- }
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build go1.19
+
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/svc"
+ "golang.org/x/sys/windows/svc/mgr"
+ "tailscale.com/logtail/backoff"
+ "tailscale.com/types/logger"
+ "tailscale.com/util/osshare"
+)
+
+func init() {
+ installSystemDaemon = installSystemDaemonWindows
+ uninstallSystemDaemon = uninstallSystemDaemonWindows
+}
+
+func installSystemDaemonWindows(args []string) (err error) {
+ m, err := mgr.Connect()
+ if err != nil {
+ return fmt.Errorf("failed to connect to Windows service manager: %v", err)
+ }
+
+ service, err := m.OpenService(serviceName)
+ if err == nil {
+ service.Close()
+ return fmt.Errorf("service %q is already installed", serviceName)
+ }
+
+ // no such service; proceed to install the service.
+
+ exe, err := os.Executable()
+ if err != nil {
+ return err
+ }
+
+ c := mgr.Config{
+ ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
+ StartType: mgr.StartAutomatic,
+ ErrorControl: mgr.ErrorNormal,
+ DisplayName: serviceName,
+ Description: "Connects this computer to others on the Tailscale network.",
+ }
+
+ service, err = m.CreateService(serviceName, exe, c)
+ if err != nil {
+ return fmt.Errorf("failed to create %q service: %v", serviceName, err)
+ }
+ defer service.Close()
+
+ // Exponential backoff is often too aggressive, so use (mostly)
+ // squares instead.
+ ra := []mgr.RecoveryAction{
+ {mgr.ServiceRestart, 1 * time.Second},
+ {mgr.ServiceRestart, 2 * time.Second},
+ {mgr.ServiceRestart, 4 * time.Second},
+ {mgr.ServiceRestart, 9 * time.Second},
+ {mgr.ServiceRestart, 16 * time.Second},
+ {mgr.ServiceRestart, 25 * time.Second},
+ {mgr.ServiceRestart, 36 * time.Second},
+ {mgr.ServiceRestart, 49 * time.Second},
+ {mgr.ServiceRestart, 64 * time.Second},
+ }
+ const resetPeriodSecs = 60
+ err = service.SetRecoveryActions(ra, resetPeriodSecs)
+ if err != nil {
+ return fmt.Errorf("failed to set service recovery actions: %v", err)
+ }
+
+ return nil
+}
+
+func uninstallSystemDaemonWindows(args []string) (ret error) {
+ // Remove file sharing from Windows shell (noop in non-windows)
+ osshare.SetFileSharingEnabled(false, logger.Discard)
+
+ m, err := mgr.Connect()
+ if err != nil {
+ return fmt.Errorf("failed to connect to Windows service manager: %v", err)
+ }
+ defer m.Disconnect()
+
+ service, err := m.OpenService(serviceName)
+ if err != nil {
+ return fmt.Errorf("failed to open %q service: %v", serviceName, err)
+ }
+
+ st, err := service.Query()
+ if err != nil {
+ service.Close()
+ return fmt.Errorf("failed to query service state: %v", err)
+ }
+ if st.State != svc.Stopped {
+ service.Control(svc.Stop)
+ }
+ err = service.Delete()
+ service.Close()
+ if err != nil {
+ return fmt.Errorf("failed to delete service: %v", err)
+ }
+
+ bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
+ end := time.Now().Add(15 * time.Second)
+ for time.Until(end) > 0 {
+ service, err = m.OpenService(serviceName)
+ if err != nil {
+ // service is no longer openable; success!
+ break
+ }
+ service.Close()
+ bo.BackOff(context.Background(), errors.New("service not deleted"))
+ }
+ return nil
+}
diff --git a/cmd/tailscaled/proxy.go b/cmd/tailscaled/proxy.go
index a91c62bfa..109ad029d 100644
--- a/cmd/tailscaled/proxy.go
+++ b/cmd/tailscaled/proxy.go
@@ -1,80 +1,80 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build go1.19
-
-// HTTP proxy code
-
-package main
-
-import (
- "context"
- "io"
- "net"
- "net/http"
- "net/http/httputil"
- "strings"
-)
-
-// httpProxyHandler returns an HTTP proxy http.Handler using the
-// provided backend dialer.
-func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler {
- rp := &httputil.ReverseProxy{
- Director: func(r *http.Request) {}, // no change
- Transport: &http.Transport{
- DialContext: dialer,
- },
- }
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.Method != "CONNECT" {
- backURL := r.RequestURI
- if strings.HasPrefix(backURL, "/") || backURL == "*" {
- http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400)
- return
- }
- rp.ServeHTTP(w, r)
- return
- }
-
- // CONNECT support:
-
- dst := r.RequestURI
- c, err := dialer(r.Context(), "tcp", dst)
- if err != nil {
- w.Header().Set("Tailscale-Connect-Error", err.Error())
- http.Error(w, err.Error(), 500)
- return
- }
- defer c.Close()
-
- cc, ccbuf, err := w.(http.Hijacker).Hijack()
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
- defer cc.Close()
-
- io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n")
-
- var clientSrc io.Reader = ccbuf
- if ccbuf.Reader.Buffered() == 0 {
- // In the common case (with no
- // buffered data), read directly from
- // the underlying client connection to
- // save some memory, letting the
- // bufio.Reader/Writer get GC'ed.
- clientSrc = cc
- }
-
- errc := make(chan error, 1)
- go func() {
- _, err := io.Copy(cc, c)
- errc <- err
- }()
- go func() {
- _, err := io.Copy(c, clientSrc)
- errc <- err
- }()
- <-errc
- })
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build go1.19
+
+// HTTP proxy code
+
+package main
+
+import (
+ "context"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "strings"
+)
+
+// httpProxyHandler returns an HTTP proxy http.Handler using the
+// provided backend dialer.
+func httpProxyHandler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler {
+ rp := &httputil.ReverseProxy{
+ Director: func(r *http.Request) {}, // no change
+ Transport: &http.Transport{
+ DialContext: dialer,
+ },
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "CONNECT" {
+ backURL := r.RequestURI
+ if strings.HasPrefix(backURL, "/") || backURL == "*" {
+ http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400)
+ return
+ }
+ rp.ServeHTTP(w, r)
+ return
+ }
+
+ // CONNECT support:
+
+ dst := r.RequestURI
+ c, err := dialer(r.Context(), "tcp", dst)
+ if err != nil {
+ w.Header().Set("Tailscale-Connect-Error", err.Error())
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ defer c.Close()
+
+ cc, ccbuf, err := w.(http.Hijacker).Hijack()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ defer cc.Close()
+
+ io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n")
+
+ var clientSrc io.Reader = ccbuf
+ if ccbuf.Reader.Buffered() == 0 {
+ // In the common case (with no
+ // buffered data), read directly from
+ // the underlying client connection to
+ // save some memory, letting the
+ // bufio.Reader/Writer get GC'ed.
+ clientSrc = cc
+ }
+
+ errc := make(chan error, 1)
+ go func() {
+ _, err := io.Copy(cc, c)
+ errc <- err
+ }()
+ go func() {
+ _, err := io.Copy(c, clientSrc)
+ errc <- err
+ }()
+ <-errc
+ })
+}
diff --git a/cmd/tailscaled/sigpipe.go b/cmd/tailscaled/sigpipe.go
index 2fcdab2a4..695a88024 100644
--- a/cmd/tailscaled/sigpipe.go
+++ b/cmd/tailscaled/sigpipe.go
@@ -1,12 +1,12 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build go1.21 && !plan9
-
-package main
-
-import "syscall"
-
-func init() {
- sigPipe = syscall.SIGPIPE
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build go1.21 && !plan9
+
+package main
+
+import "syscall"
+
+func init() {
+ sigPipe = syscall.SIGPIPE
+}
diff --git a/cmd/tailscaled/tailscaled.defaults b/cmd/tailscaled/tailscaled.defaults
index e8384a4f8..693a6190b 100644
--- a/cmd/tailscaled/tailscaled.defaults
+++ b/cmd/tailscaled/tailscaled.defaults
@@ -1,8 +1,8 @@
-# Set the port to listen on for incoming VPN packets.
-# Remote nodes will automatically be informed about the new port number,
-# but you might want to configure this in order to set external firewall
-# settings.
-PORT="41641"
-
-# Extra flags you might want to pass to tailscaled.
-FLAGS=""
+# Set the port to listen on for incoming VPN packets.
+# Remote nodes will automatically be informed about the new port number,
+# but you might want to configure this in order to set external firewall
+# settings.
+PORT="41641"
+
+# Extra flags you might want to pass to tailscaled.
+FLAGS=""
diff --git a/cmd/tailscaled/tailscaled.openrc b/cmd/tailscaled/tailscaled.openrc
index 309d70f23..6193247ce 100755
--- a/cmd/tailscaled/tailscaled.openrc
+++ b/cmd/tailscaled/tailscaled.openrc
@@ -1,25 +1,25 @@
-#!/sbin/openrc-run
-
-set -a
-source /etc/default/tailscaled
-set +a
-
-command="/usr/sbin/tailscaled"
-command_args="--state=/var/lib/tailscale/tailscaled.state --port=$PORT --socket=/var/run/tailscale/tailscaled.sock $FLAGS"
-command_background=true
-pidfile="/run/tailscaled.pid"
-start_stop_daemon_args="-1 /var/log/tailscaled.log -2 /var/log/tailscaled.log"
-
-depend() {
- need net
-}
-
-start_pre() {
- mkdir -p /var/run/tailscale
- mkdir -p /var/lib/tailscale
- $command --cleanup
-}
-
-stop_post() {
- $command --cleanup
-}
+#!/sbin/openrc-run
+
+set -a
+source /etc/default/tailscaled
+set +a
+
+command="/usr/sbin/tailscaled"
+command_args="--state=/var/lib/tailscale/tailscaled.state --port=$PORT --socket=/var/run/tailscale/tailscaled.sock $FLAGS"
+command_background=true
+pidfile="/run/tailscaled.pid"
+start_stop_daemon_args="-1 /var/log/tailscaled.log -2 /var/log/tailscaled.log"
+
+depend() {
+ need net
+}
+
+start_pre() {
+ mkdir -p /var/run/tailscale
+ mkdir -p /var/lib/tailscale
+ $command --cleanup
+}
+
+stop_post() {
+ $command --cleanup
+}
diff --git a/cmd/tailscaled/tailscaled_bird.go b/cmd/tailscaled/tailscaled_bird.go
index c76f77bec..885f552cb 100644
--- a/cmd/tailscaled/tailscaled_bird.go
+++ b/cmd/tailscaled/tailscaled_bird.go
@@ -1,17 +1,17 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build go1.19 && (linux || darwin || freebsd || openbsd) && !ts_omit_bird
-
-package main
-
-import (
- "tailscale.com/chirp"
- "tailscale.com/wgengine"
-)
-
-func init() {
- createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) {
- return chirp.New(ctlSocket)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build go1.19 && (linux || darwin || freebsd || openbsd) && !ts_omit_bird
+
+package main
+
+import (
+ "tailscale.com/chirp"
+ "tailscale.com/wgengine"
+)
+
+func init() {
+ createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) {
+ return chirp.New(ctlSocket)
+ }
+}
diff --git a/cmd/tailscaled/tailscaled_notwindows.go b/cmd/tailscaled/tailscaled_notwindows.go
index d5361cf28..b0a7c1598 100644
--- a/cmd/tailscaled/tailscaled_notwindows.go
+++ b/cmd/tailscaled/tailscaled_notwindows.go
@@ -1,14 +1,14 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !windows && go1.19
-
-package main // import "tailscale.com/cmd/tailscaled"
-
-import "tailscale.com/logpolicy"
-
-func isWindowsService() bool { return false }
-
-func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
-
-func beWindowsSubprocess() bool { return false }
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !windows && go1.19
+
+package main // import "tailscale.com/cmd/tailscaled"
+
+import "tailscale.com/logpolicy"
+
+func isWindowsService() bool { return false }
+
+func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
+
+func beWindowsSubprocess() bool { return false }
diff --git a/cmd/tailscaled/windows-manifest.xml b/cmd/tailscaled/windows-manifest.xml
index 6c5f46058..5eaa54fa5 100644
--- a/cmd/tailscaled/windows-manifest.xml
+++ b/cmd/tailscaled/windows-manifest.xml
@@ -1,13 +1,13 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-
- <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
- <application>
- <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
- <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
- <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
- <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
- </application>
- </compatibility>
-
-</assembly>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
+ </application>
+ </compatibility>
+
+</assembly>
diff --git a/cmd/tailscaled/with_cli.go b/cmd/tailscaled/with_cli.go
index a8554eb8c..f191fdb45 100644
--- a/cmd/tailscaled/with_cli.go
+++ b/cmd/tailscaled/with_cli.go
@@ -1,23 +1,23 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build ts_include_cli
-
-package main
-
-import (
- "fmt"
- "os"
-
- "tailscale.com/cmd/tailscale/cli"
-)
-
-func init() {
- beCLI = func() {
- args := os.Args[1:]
- if err := cli.Run(args); err != nil {
- fmt.Fprintln(os.Stderr, err)
- os.Exit(1)
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ts_include_cli
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "tailscale.com/cmd/tailscale/cli"
+)
+
+func init() {
+ beCLI = func() {
+ args := os.Args[1:]
+ if err := cli.Run(args); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ }
+}
diff --git a/cmd/testwrapper/args_test.go b/cmd/testwrapper/args_test.go
index 10063d7bc..f7f30a7eb 100644
--- a/cmd/testwrapper/args_test.go
+++ b/cmd/testwrapper/args_test.go
@@ -1,97 +1,97 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package main
-
-import (
- "slices"
- "testing"
-)
-
-func TestSplitArgs(t *testing.T) {
- tests := []struct {
- name string
- in []string
- pre, pkgs, post []string
- }{
- {
- name: "empty",
- },
- {
- name: "all",
- in: []string{"-v", "pkg1", "pkg2", "-run", "TestFoo", "-timeout=20s"},
- pre: []string{"-v"},
- pkgs: []string{"pkg1", "pkg2"},
- post: []string{"-run", "TestFoo", "-timeout=20s"},
- },
- {
- name: "only_pkgs",
- in: []string{"./..."},
- pkgs: []string{"./..."},
- },
- {
- name: "pkgs_and_post",
- in: []string{"pkg1", "-run", "TestFoo"},
- pkgs: []string{"pkg1"},
- post: []string{"-run", "TestFoo"},
- },
- {
- name: "pkgs_and_post",
- in: []string{"-v", "pkg2"},
- pre: []string{"-v"},
- pkgs: []string{"pkg2"},
- },
- {
- name: "only_args",
- in: []string{"-v", "-run=TestFoo"},
- pre: []string{"-run", "TestFoo", "-v"}, // sorted
- },
- {
- name: "space_in_pre_arg",
- in: []string{"-run", "TestFoo", "./cmd/testwrapper"},
- pre: []string{"-run", "TestFoo"},
- pkgs: []string{"./cmd/testwrapper"},
- },
- {
- name: "space_in_arg",
- in: []string{"-exec", "sudo -E", "./cmd/testwrapper"},
- pre: []string{"-exec", "sudo -E"},
- pkgs: []string{"./cmd/testwrapper"},
- },
- {
- name: "test-arg",
- in: []string{"-exec", "sudo -E", "./cmd/testwrapper", "--", "--some-flag"},
- pre: []string{"-exec", "sudo -E"},
- pkgs: []string{"./cmd/testwrapper"},
- post: []string{"--", "--some-flag"},
- },
- {
- name: "dupe-args",
- in: []string{"-v", "-v", "-race", "-race", "./cmd/testwrapper", "--", "--some-flag"},
- pre: []string{"-race", "-v"},
- pkgs: []string{"./cmd/testwrapper"},
- post: []string{"--", "--some-flag"},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- pre, pkgs, post, err := splitArgs(tt.in)
- if err != nil {
- t.Fatal(err)
- }
- if !slices.Equal(pre, tt.pre) {
- t.Errorf("pre = %q; want %q", pre, tt.pre)
- }
- if !slices.Equal(pkgs, tt.pkgs) {
- t.Errorf("pattern = %q; want %q", pkgs, tt.pkgs)
- }
- if !slices.Equal(post, tt.post) {
- t.Errorf("post = %q; want %q", post, tt.post)
- }
- if t.Failed() {
- t.Logf("SplitArgs(%q) = %q %q %q", tt.in, pre, pkgs, post)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package main
+
+import (
+ "slices"
+ "testing"
+)
+
+func TestSplitArgs(t *testing.T) {
+ tests := []struct {
+ name string
+ in []string
+ pre, pkgs, post []string
+ }{
+ {
+ name: "empty",
+ },
+ {
+ name: "all",
+ in: []string{"-v", "pkg1", "pkg2", "-run", "TestFoo", "-timeout=20s"},
+ pre: []string{"-v"},
+ pkgs: []string{"pkg1", "pkg2"},
+ post: []string{"-run", "TestFoo", "-timeout=20s"},
+ },
+ {
+ name: "only_pkgs",
+ in: []string{"./..."},
+ pkgs: []string{"./..."},
+ },
+ {
+ name: "pkgs_and_post",
+ in: []string{"pkg1", "-run", "TestFoo"},
+ pkgs: []string{"pkg1"},
+ post: []string{"-run", "TestFoo"},
+ },
+ {
+ name: "pkgs_and_post",
+ in: []string{"-v", "pkg2"},
+ pre: []string{"-v"},
+ pkgs: []string{"pkg2"},
+ },
+ {
+ name: "only_args",
+ in: []string{"-v", "-run=TestFoo"},
+ pre: []string{"-run", "TestFoo", "-v"}, // sorted
+ },
+ {
+ name: "space_in_pre_arg",
+ in: []string{"-run", "TestFoo", "./cmd/testwrapper"},
+ pre: []string{"-run", "TestFoo"},
+ pkgs: []string{"./cmd/testwrapper"},
+ },
+ {
+ name: "space_in_arg",
+ in: []string{"-exec", "sudo -E", "./cmd/testwrapper"},
+ pre: []string{"-exec", "sudo -E"},
+ pkgs: []string{"./cmd/testwrapper"},
+ },
+ {
+ name: "test-arg",
+ in: []string{"-exec", "sudo -E", "./cmd/testwrapper", "--", "--some-flag"},
+ pre: []string{"-exec", "sudo -E"},
+ pkgs: []string{"./cmd/testwrapper"},
+ post: []string{"--", "--some-flag"},
+ },
+ {
+ name: "dupe-args",
+ in: []string{"-v", "-v", "-race", "-race", "./cmd/testwrapper", "--", "--some-flag"},
+ pre: []string{"-race", "-v"},
+ pkgs: []string{"./cmd/testwrapper"},
+ post: []string{"--", "--some-flag"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ pre, pkgs, post, err := splitArgs(tt.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Equal(pre, tt.pre) {
+ t.Errorf("pre = %q; want %q", pre, tt.pre)
+ }
+ if !slices.Equal(pkgs, tt.pkgs) {
+ t.Errorf("pattern = %q; want %q", pkgs, tt.pkgs)
+ }
+ if !slices.Equal(post, tt.post) {
+ t.Errorf("post = %q; want %q", post, tt.post)
+ }
+ if t.Failed() {
+ t.Logf("SplitArgs(%q) = %q %q %q", tt.in, pre, pkgs, post)
+ }
+ })
+ }
+}
diff --git a/cmd/testwrapper/flakytest/flakytest.go b/cmd/testwrapper/flakytest/flakytest.go
index 494ed080b..e5e21dd21 100644
--- a/cmd/testwrapper/flakytest/flakytest.go
+++ b/cmd/testwrapper/flakytest/flakytest.go
@@ -1,44 +1,44 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package flakytest contains test helpers for marking a test as flaky. For
-// tests run using cmd/testwrapper, a failed flaky test will cause tests to be
-// re-run a few time until they succeed or exceed our iteration limit.
-package flakytest
-
-import (
- "fmt"
- "os"
- "regexp"
- "testing"
-)
-
-// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
-// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
-// and retry them.
-const FlakyTestLogMessage = "flakytest: this is a known flaky test"
-
-// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
-// when a flaky test is being (re)tried. It contains the attempt number,
-// starting at 1.
-const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
-
-var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
-
-// Mark sets the current test as a flaky test, such that if it fails, it will
-// be retried a few times on failure. issue must be a GitHub issue that tracks
-// the status of the flaky test being marked, of the format:
-//
-// https://github.com/tailscale/myRepo-H3re/issues/12345
-func Mark(t testing.TB, issue string) {
- if !issueRegexp.MatchString(issue) {
- t.Fatalf("bad issue format: %q", issue)
- }
- if _, ok := os.LookupEnv(FlakeAttemptEnv); ok {
- // We're being run under cmd/testwrapper so send our sentinel message
- // to stderr. (We avoid doing this when the env is absent to avoid
- // spamming people running tests without the wrapper)
- fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
- }
- t.Logf("flakytest: issue tracking this flaky test: %s", issue)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package flakytest contains test helpers for marking a test as flaky. For
+// tests run using cmd/testwrapper, a failed flaky test will cause tests to be
+// re-run a few time until they succeed or exceed our iteration limit.
+package flakytest
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+ "testing"
+)
+
+// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
+// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
+// and retry them.
+const FlakyTestLogMessage = "flakytest: this is a known flaky test"
+
+// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
+// when a flaky test is being (re)tried. It contains the attempt number,
+// starting at 1.
+const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
+
+var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
+
+// Mark sets the current test as a flaky test, such that if it fails, it will
+// be retried a few times on failure. issue must be a GitHub issue that tracks
+// the status of the flaky test being marked, of the format:
+//
+// https://github.com/tailscale/myRepo-H3re/issues/12345
+func Mark(t testing.TB, issue string) {
+ if !issueRegexp.MatchString(issue) {
+ t.Fatalf("bad issue format: %q", issue)
+ }
+ if _, ok := os.LookupEnv(FlakeAttemptEnv); ok {
+ // We're being run under cmd/testwrapper so send our sentinel message
+ // to stderr. (We avoid doing this when the env is absent to avoid
+ // spamming people running tests without the wrapper)
+ fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
+ }
+ t.Logf("flakytest: issue tracking this flaky test: %s", issue)
+}
diff --git a/cmd/testwrapper/flakytest/flakytest_test.go b/cmd/testwrapper/flakytest/flakytest_test.go
index 85e77a939..551352f6a 100644
--- a/cmd/testwrapper/flakytest/flakytest_test.go
+++ b/cmd/testwrapper/flakytest/flakytest_test.go
@@ -1,43 +1,43 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package flakytest
-
-import (
- "os"
- "testing"
-)
-
-func TestIssueFormat(t *testing.T) {
- testCases := []struct {
- issue string
- want bool
- }{
- {"https://github.com/tailscale/cOrp/issues/1234", true},
- {"https://github.com/otherproject/corp/issues/1234", false},
- {"https://github.com/tailscale/corp/issues/", false},
- }
- for _, testCase := range testCases {
- if issueRegexp.MatchString(testCase.issue) != testCase.want {
- ss := ""
- if !testCase.want {
- ss = " not"
- }
- t.Errorf("expected issueRegexp to%s match %q", ss, testCase.issue)
- }
- }
-}
-
-// TestFlakeRun is a test that fails when run in the testwrapper
-// for the first time, but succeeds on the second run.
-// It's used to test whether the testwrapper retries flaky tests.
-func TestFlakeRun(t *testing.T) {
- Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
- e := os.Getenv(FlakeAttemptEnv)
- if e == "" {
- t.Skip("not running in testwrapper")
- }
- if e == "1" {
- t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package flakytest
+
+import (
+ "os"
+ "testing"
+)
+
+func TestIssueFormat(t *testing.T) {
+ testCases := []struct {
+ issue string
+ want bool
+ }{
+ {"https://github.com/tailscale/cOrp/issues/1234", true},
+ {"https://github.com/otherproject/corp/issues/1234", false},
+ {"https://github.com/tailscale/corp/issues/", false},
+ }
+ for _, testCase := range testCases {
+ if issueRegexp.MatchString(testCase.issue) != testCase.want {
+ ss := ""
+ if !testCase.want {
+ ss = " not"
+ }
+ t.Errorf("expected issueRegexp to%s match %q", ss, testCase.issue)
+ }
+ }
+}
+
+// TestFlakeRun is a test that fails when run in the testwrapper
+// for the first time, but succeeds on the second run.
+// It's used to test whether the testwrapper retries flaky tests.
+func TestFlakeRun(t *testing.T) {
+ Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
+ e := os.Getenv(FlakeAttemptEnv)
+ if e == "" {
+ t.Skip("not running in testwrapper")
+ }
+ if e == "1" {
+ t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
+ }
+}
diff --git a/cmd/tsconnect/.gitignore b/cmd/tsconnect/.gitignore
index 13615d121..b791f8e64 100644
--- a/cmd/tsconnect/.gitignore
+++ b/cmd/tsconnect/.gitignore
@@ -1,3 +1,3 @@
-node_modules/
-/dist
-/pkg
+node_modules/
+/dist
+/pkg
diff --git a/cmd/tsconnect/README.md b/cmd/tsconnect/README.md
index 536cd7bbf..f518f932e 100644
--- a/cmd/tsconnect/README.md
+++ b/cmd/tsconnect/README.md
@@ -1,49 +1,49 @@
-# tsconnect
-
-The tsconnect command builds and serves the static site that is generated for
-the Tailscale Connect JS/WASM client.
-
-## Development
-
-To start the development server:
-
-```
-./tool/go run ./cmd/tsconnect dev
-```
-
-The site is served at http://localhost:9090/. JavaScript, CSS and Go `wasm` package changes can be picked up with a browser reload. Server-side Go changes require the server to be stopped and restarted. In development mode the state the Tailscale client state is stored in `sessionStorage` and will thus survive page reloads (but not the tab being closed).
-
-## Deployment
-
-To build the static assets necessary for serving, run:
-
-```
-./tool/go run ./cmd/tsconnect build
-```
-
-To serve them, run:
-
-```
-./tool/go run ./cmd/tsconnect serve
-```
-
-By default the build output is placed in the `dist/` directory and embedded in the binary, but this can be controlled by the `-distdir` flag. The `-addr` flag controls the interface and port that the serve listens on.
-
-# Library / NPM Package
-
-The client is also available as [an NPM package](https://www.npmjs.com/package/@tailscale/connect). To build it, run:
-
-```
-./tool/go run ./cmd/tsconnect build-pkg
-```
-
-That places the output in the `pkg/` directory, which may then be uploaded to a package registry (or installed from the file path directly).
-
-To do two-sided development (on both the NPM package and code that uses it), run:
-
-```
-./tool/go run ./cmd/tsconnect dev-pkg
-
-```
-
-This serves the module at http://localhost:9090/pkg/pkg.js and the generated wasm file at http://localhost:9090/pkg/main.wasm. The two files can be used as drop-in replacements for normal imports of the NPM module.
+# tsconnect
+
+The tsconnect command builds and serves the static site that is generated for
+the Tailscale Connect JS/WASM client.
+
+## Development
+
+To start the development server:
+
+```
+./tool/go run ./cmd/tsconnect dev
+```
+
+The site is served at http://localhost:9090/. JavaScript, CSS and Go `wasm` package changes can be picked up with a browser reload. Server-side Go changes require the server to be stopped and restarted. In development mode the state the Tailscale client state is stored in `sessionStorage` and will thus survive page reloads (but not the tab being closed).
+
+## Deployment
+
+To build the static assets necessary for serving, run:
+
+```
+./tool/go run ./cmd/tsconnect build
+```
+
+To serve them, run:
+
+```
+./tool/go run ./cmd/tsconnect serve
+```
+
+By default the build output is placed in the `dist/` directory and embedded in the binary, but this can be controlled by the `-distdir` flag. The `-addr` flag controls the interface and port that the serve listens on.
+
+# Library / NPM Package
+
+The client is also available as [an NPM package](https://www.npmjs.com/package/@tailscale/connect). To build it, run:
+
+```
+./tool/go run ./cmd/tsconnect build-pkg
+```
+
+That places the output in the `pkg/` directory, which may then be uploaded to a package registry (or installed from the file path directly).
+
+To do two-sided development (on both the NPM package and code that uses it), run:
+
+```
+./tool/go run ./cmd/tsconnect dev-pkg
+
+```
+
+This serves the module at http://localhost:9090/pkg/pkg.js and the generated wasm file at http://localhost:9090/pkg/main.wasm. The two files can be used as drop-in replacements for normal imports of the NPM module.
diff --git a/cmd/tsconnect/README.pkg.md b/cmd/tsconnect/README.pkg.md
index df8d66789..df5799578 100644
--- a/cmd/tsconnect/README.pkg.md
+++ b/cmd/tsconnect/README.pkg.md
@@ -1,3 +1,3 @@
-# @tailscale/connect
-
-NPM package that contains a WebAssembly-based Tailscale client, see [the `cmd/tsconnect` directory in the tailscale repo](https://github.com/tailscale/tailscale/tree/main/cmd/tsconnect#library--npm-package) for more details.
+# @tailscale/connect
+
+NPM package that contains a WebAssembly-based Tailscale client, see [the `cmd/tsconnect` directory in the tailscale repo](https://github.com/tailscale/tailscale/tree/main/cmd/tsconnect#library--npm-package) for more details.
diff --git a/cmd/tsconnect/build-pkg.go b/cmd/tsconnect/build-pkg.go
index 047504858..2b6cc9b1f 100644
--- a/cmd/tsconnect/build-pkg.go
+++ b/cmd/tsconnect/build-pkg.go
@@ -1,99 +1,99 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-package main
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "os"
- "path"
-
- "github.com/tailscale/hujson"
- "tailscale.com/util/precompress"
- "tailscale.com/version"
-)
-
-func runBuildPkg() {
- buildOptions, err := commonPkgSetup(prodMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
-
- log.Printf("Linting...\n")
- if err := runYarn("lint"); err != nil {
- log.Fatalf("Linting failed: %v", err)
- }
-
- if err := cleanDir(*pkgDir); err != nil {
- log.Fatalf("Cannot clean %s: %v", *pkgDir, err)
- }
-
- buildOptions.Write = true
- buildOptions.MinifyWhitespace = true
- buildOptions.MinifyIdentifiers = true
- buildOptions.MinifySyntax = true
-
- runEsbuild(*buildOptions)
-
- if err := precompressWasm(); err != nil {
- log.Fatalf("Could not pre-recompress wasm: %v", err)
- }
-
- log.Printf("Generating types...\n")
- if err := runYarn("pkg-types"); err != nil {
- log.Fatalf("Type generation failed: %v", err)
- }
-
- if err := updateVersion(); err != nil {
- log.Fatalf("Cannot update version: %v", err)
- }
-
- if err := copyReadme(); err != nil {
- log.Fatalf("Cannot copy readme: %v", err)
- }
-
- log.Printf("Built package version %s", version.Long())
-}
-
-func precompressWasm() error {
- log.Printf("Pre-compressing main.wasm...\n")
- return precompress.Precompress(path.Join(*pkgDir, "main.wasm"), precompress.Options{
- FastCompression: *fastCompression,
- })
-}
-
-func updateVersion() error {
- packageJSONBytes, err := os.ReadFile("package.json.tmpl")
- if err != nil {
- return fmt.Errorf("Could not read package.json: %w", err)
- }
-
- var packageJSON map[string]any
- packageJSONBytes, err = hujson.Standardize(packageJSONBytes)
- if err != nil {
- return fmt.Errorf("Could not standardize template package.json: %w", err)
- }
- if err := json.Unmarshal(packageJSONBytes, &packageJSON); err != nil {
- return fmt.Errorf("Could not unmarshal package.json: %w", err)
- }
- packageJSON["version"] = version.Long()
-
- packageJSONBytes, err = json.MarshalIndent(packageJSON, "", " ")
- if err != nil {
- return fmt.Errorf("Could not marshal package.json: %w", err)
- }
-
- return os.WriteFile(path.Join(*pkgDir, "package.json"), packageJSONBytes, 0644)
-}
-
-func copyReadme() error {
- readmeBytes, err := os.ReadFile("README.pkg.md")
- if err != nil {
- return fmt.Errorf("Could not read README.pkg.md: %w", err)
- }
- return os.WriteFile(path.Join(*pkgDir, "README.md"), readmeBytes, 0644)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "path"
+
+ "github.com/tailscale/hujson"
+ "tailscale.com/util/precompress"
+ "tailscale.com/version"
+)
+
+func runBuildPkg() {
+ buildOptions, err := commonPkgSetup(prodMode)
+ if err != nil {
+ log.Fatalf("Cannot setup: %v", err)
+ }
+
+ log.Printf("Linting...\n")
+ if err := runYarn("lint"); err != nil {
+ log.Fatalf("Linting failed: %v", err)
+ }
+
+ if err := cleanDir(*pkgDir); err != nil {
+ log.Fatalf("Cannot clean %s: %v", *pkgDir, err)
+ }
+
+ buildOptions.Write = true
+ buildOptions.MinifyWhitespace = true
+ buildOptions.MinifyIdentifiers = true
+ buildOptions.MinifySyntax = true
+
+ runEsbuild(*buildOptions)
+
+ if err := precompressWasm(); err != nil {
+ log.Fatalf("Could not pre-recompress wasm: %v", err)
+ }
+
+ log.Printf("Generating types...\n")
+ if err := runYarn("pkg-types"); err != nil {
+ log.Fatalf("Type generation failed: %v", err)
+ }
+
+ if err := updateVersion(); err != nil {
+ log.Fatalf("Cannot update version: %v", err)
+ }
+
+ if err := copyReadme(); err != nil {
+ log.Fatalf("Cannot copy readme: %v", err)
+ }
+
+ log.Printf("Built package version %s", version.Long())
+}
+
+func precompressWasm() error {
+ log.Printf("Pre-compressing main.wasm...\n")
+ return precompress.Precompress(path.Join(*pkgDir, "main.wasm"), precompress.Options{
+ FastCompression: *fastCompression,
+ })
+}
+
+func updateVersion() error {
+ packageJSONBytes, err := os.ReadFile("package.json.tmpl")
+ if err != nil {
+ return fmt.Errorf("Could not read package.json: %w", err)
+ }
+
+ var packageJSON map[string]any
+ packageJSONBytes, err = hujson.Standardize(packageJSONBytes)
+ if err != nil {
+ return fmt.Errorf("Could not standardize template package.json: %w", err)
+ }
+ if err := json.Unmarshal(packageJSONBytes, &packageJSON); err != nil {
+ return fmt.Errorf("Could not unmarshal package.json: %w", err)
+ }
+ packageJSON["version"] = version.Long()
+
+ packageJSONBytes, err = json.MarshalIndent(packageJSON, "", " ")
+ if err != nil {
+ return fmt.Errorf("Could not marshal package.json: %w", err)
+ }
+
+ return os.WriteFile(path.Join(*pkgDir, "package.json"), packageJSONBytes, 0644)
+}
+
+func copyReadme() error {
+ readmeBytes, err := os.ReadFile("README.pkg.md")
+ if err != nil {
+ return fmt.Errorf("Could not read README.pkg.md: %w", err)
+ }
+ return os.WriteFile(path.Join(*pkgDir, "README.md"), readmeBytes, 0644)
+}
diff --git a/cmd/tsconnect/dev-pkg.go b/cmd/tsconnect/dev-pkg.go
index de534c3b2..cb5ebf39e 100644
--- a/cmd/tsconnect/dev-pkg.go
+++ b/cmd/tsconnect/dev-pkg.go
@@ -1,18 +1,18 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-package main
-
-import (
- "log"
-)
-
-func runDevPkg() {
- buildOptions, err := commonPkgSetup(devMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
- runEsbuildServe(*buildOptions)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+package main
+
+import (
+ "log"
+)
+
+func runDevPkg() {
+ buildOptions, err := commonPkgSetup(devMode)
+ if err != nil {
+ log.Fatalf("Cannot setup: %v", err)
+ }
+ runEsbuildServe(*buildOptions)
+}
diff --git a/cmd/tsconnect/dev.go b/cmd/tsconnect/dev.go
index 87b10adaf..161eb3b86 100644
--- a/cmd/tsconnect/dev.go
+++ b/cmd/tsconnect/dev.go
@@ -1,18 +1,18 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-package main
-
-import (
- "log"
-)
-
-func runDev() {
- buildOptions, err := commonSetup(devMode)
- if err != nil {
- log.Fatalf("Cannot setup: %v", err)
- }
- runEsbuildServe(*buildOptions)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+package main
+
+import (
+ "log"
+)
+
+func runDev() {
+ buildOptions, err := commonSetup(devMode)
+ if err != nil {
+ log.Fatalf("Cannot setup: %v", err)
+ }
+ runEsbuildServe(*buildOptions)
+}
diff --git a/cmd/tsconnect/dist/placeholder b/cmd/tsconnect/dist/placeholder
index 4af99d997..dddaba4d7 100644
--- a/cmd/tsconnect/dist/placeholder
+++ b/cmd/tsconnect/dist/placeholder
@@ -1,2 +1,2 @@
-This is here to make sure the dist/ directory exists for the go:embed command
-in serve.go.
+This is here to make sure the dist/ directory exists for the go:embed command
+in serve.go.
diff --git a/cmd/tsconnect/index.html b/cmd/tsconnect/index.html
index 3db45fdef..39aa7571a 100644
--- a/cmd/tsconnect/index.html
+++ b/cmd/tsconnect/index.html
@@ -1,20 +1,20 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Tailscale Connect</title>
- <link rel="stylesheet" type="text/css" href="dist/index.css" />
- <script src="dist/index.js" defer></script>
- </head>
- <body class="flex flex-col h-screen overflow-hidden">
- <!-- Placeholder so that we don't have an empty page while the JS loads.
- It should match the markup generated by Header component. -->
- <div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
- <header class="container mx-auto px-4 flex flex-row items-center">
- <h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
- <div class="text-gray-600">Loading…</div>
- </header>
- </div>
- </body>
-</html>
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Tailscale Connect</title>
+ <link rel="stylesheet" type="text/css" href="dist/index.css" />
+ <script src="dist/index.js" defer></script>
+ </head>
+ <body class="flex flex-col h-screen overflow-hidden">
+ <!-- Placeholder so that we don't have an empty page while the JS loads.
+ It should match the markup generated by Header component. -->
+ <div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
+ <header class="container mx-auto px-4 flex flex-row items-center">
+ <h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
+ <div class="text-gray-600">Loading…</div>
+ </header>
+ </div>
+ </body>
+</html>
diff --git a/cmd/tsconnect/package.json b/cmd/tsconnect/package.json
index bf4eb7c09..8ea726cc6 100644
--- a/cmd/tsconnect/package.json
+++ b/cmd/tsconnect/package.json
@@ -1,25 +1,25 @@
-{
- "name": "tsconnect",
- "version": "0.0.1",
- "license": "BSD-3-Clause",
- "devDependencies": {
- "@types/golang-wasm-exec": "^1.15.0",
- "@types/qrcode": "^1.4.2",
- "dts-bundle-generator": "^6.12.0",
- "preact": "^10.10.0",
- "qrcode": "^1.5.0",
- "tailwindcss": "^3.1.6",
- "typescript": "^4.7.4",
- "xterm": "^5.1.0",
- "xterm-addon-fit": "^0.7.0",
- "xterm-addon-web-links": "^0.8.0"
- },
- "scripts": {
- "lint": "tsc --noEmit",
- "pkg-types": "dts-bundle-generator --inline-declare-global=true --no-banner -o pkg/pkg.d.ts src/pkg/pkg.ts"
- },
- "prettier": {
- "semi": false,
- "printWidth": 80
- }
-}
+{
+ "name": "tsconnect",
+ "version": "0.0.1",
+ "license": "BSD-3-Clause",
+ "devDependencies": {
+ "@types/golang-wasm-exec": "^1.15.0",
+ "@types/qrcode": "^1.4.2",
+ "dts-bundle-generator": "^6.12.0",
+ "preact": "^10.10.0",
+ "qrcode": "^1.5.0",
+ "tailwindcss": "^3.1.6",
+ "typescript": "^4.7.4",
+ "xterm": "^5.1.0",
+ "xterm-addon-fit": "^0.7.0",
+ "xterm-addon-web-links": "^0.8.0"
+ },
+ "scripts": {
+ "lint": "tsc --noEmit",
+ "pkg-types": "dts-bundle-generator --inline-declare-global=true --no-banner -o pkg/pkg.d.ts src/pkg/pkg.ts"
+ },
+ "prettier": {
+ "semi": false,
+ "printWidth": 80
+ }
+}
diff --git a/cmd/tsconnect/package.json.tmpl b/cmd/tsconnect/package.json.tmpl
index 404b896ea..0263bf481 100644
--- a/cmd/tsconnect/package.json.tmpl
+++ b/cmd/tsconnect/package.json.tmpl
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Template for the package.json that is generated by the build-pkg command.
-// The version number will be replaced by the current Tailscale client version
-// number.
-{
- "author": "Tailscale Inc.",
- "description": "Tailscale Connect SDK",
- "license": "BSD-3-Clause",
- "name": "@tailscale/connect",
- "type": "module",
- "main": "./pkg.js",
- "types": "./pkg.d.ts",
- "version": "AUTO_GENERATED"
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Template for the package.json that is generated by the build-pkg command.
+// The version number will be replaced by the current Tailscale client version
+// number.
+{
+ "author": "Tailscale Inc.",
+ "description": "Tailscale Connect SDK",
+ "license": "BSD-3-Clause",
+ "name": "@tailscale/connect",
+ "type": "module",
+ "main": "./pkg.js",
+ "types": "./pkg.d.ts",
+ "version": "AUTO_GENERATED"
+}
diff --git a/cmd/tsconnect/serve.go b/cmd/tsconnect/serve.go
index d780bdd57..80844bea7 100644
--- a/cmd/tsconnect/serve.go
+++ b/cmd/tsconnect/serve.go
@@ -1,144 +1,144 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-package main
-
-import (
- "bytes"
- "embed"
- "encoding/json"
- "fmt"
- "io"
- "io/fs"
- "log"
- "net/http"
- "os"
- "path"
- "time"
-
- "tailscale.com/tsweb"
- "tailscale.com/util/precompress"
-)
-
-//go:embed index.html
-var embeddedFS embed.FS
-
-//go:embed dist/*
-var embeddedDistFS embed.FS
-
-var serveStartTime = time.Now()
-
-func runServe() {
- mux := http.NewServeMux()
-
- var distFS fs.FS
- if *distDir == "./dist" {
- var err error
- distFS, err = fs.Sub(embeddedDistFS, "dist")
- if err != nil {
- log.Fatalf("Could not drop dist/ prefix from embedded FS: %v", err)
- }
- } else {
- distFS = os.DirFS(*distDir)
- }
-
- indexBytes, err := generateServeIndex(distFS)
- if err != nil {
- log.Fatalf("Could not generate index.html: %v", err)
- }
- mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.ServeContent(w, r, "index.html", serveStartTime, bytes.NewReader(indexBytes))
- }))
- mux.Handle("/dist/", http.StripPrefix("/dist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handleServeDist(w, r, distFS)
- })))
- tsweb.Debugger(mux)
-
- log.Printf("Listening on %s", *addr)
- err = http.ListenAndServe(*addr, mux)
- if err != nil {
- log.Fatal(err)
- }
-}
-
-func generateServeIndex(distFS fs.FS) ([]byte, error) {
- log.Printf("Generating index.html...\n")
- rawIndexBytes, err := embeddedFS.ReadFile("index.html")
- if err != nil {
- return nil, fmt.Errorf("Could not read index.html: %w", err)
- }
-
- esbuildMetadataFile, err := distFS.Open("esbuild-metadata.json")
- if err != nil {
- return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
- }
- defer esbuildMetadataFile.Close()
- esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
- if err != nil {
- return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
- }
- var esbuildMetadata EsbuildMetadata
- if err := json.Unmarshal(esbuildMetadataBytes, &esbuildMetadata); err != nil {
- return nil, fmt.Errorf("Could not parse esbuild-metadata.json: %w", err)
- }
- entryPointsToHashedDistPaths := make(map[string]string)
- mainWasmPath := ""
- for outputPath, output := range esbuildMetadata.Outputs {
- if output.EntryPoint != "" {
- entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
- }
- if path.Ext(outputPath) == ".wasm" {
- for input := range output.Inputs {
- if input == "src/main.wasm" {
- mainWasmPath = path.Join("dist", outputPath)
- break
- }
- }
- }
- }
-
- indexBytes := rawIndexBytes
- for entryPointPath, defaultDistPath := range entryPointsToDefaultDistPaths {
- hashedDistPath := entryPointsToHashedDistPaths[entryPointPath]
- if hashedDistPath != "" {
- indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
- }
- }
- if mainWasmPath != "" {
- mainWasmPrefetch := fmt.Sprintf("</title>\n<link rel='preload' as='fetch' crossorigin='anonymous' href='%s'>", mainWasmPath)
- indexBytes = bytes.ReplaceAll(indexBytes, []byte("</title>"), []byte(mainWasmPrefetch))
- }
-
- return indexBytes, nil
-}
-
-var entryPointsToDefaultDistPaths = map[string]string{
- "src/app/index.css": "dist/index.css",
- "src/app/index.ts": "dist/index.js",
-}
-
-func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
- path := r.URL.Path
- f, err := precompress.OpenPrecompressedFile(w, r, path, distFS)
- if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
- return
- }
- defer f.Close()
-
- // fs.File does not claim to implement Seeker, but in practice it does.
- fSeeker, ok := f.(io.ReadSeeker)
- if !ok {
- http.Error(w, "Not seekable", http.StatusInternalServerError)
- return
- }
-
- // Aggressively cache static assets, since we cache-bust our assets with
- // hashed filenames.
- w.Header().Set("Cache-Control", "public, max-age=31535996")
- w.Header().Set("Vary", "Accept-Encoding")
-
- http.ServeContent(w, r, path, serveStartTime, fSeeker)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+package main
+
+import (
+ "bytes"
+ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/fs"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "time"
+
+ "tailscale.com/tsweb"
+ "tailscale.com/util/precompress"
+)
+
+//go:embed index.html
+var embeddedFS embed.FS
+
+//go:embed dist/*
+var embeddedDistFS embed.FS
+
+var serveStartTime = time.Now()
+
+func runServe() {
+ mux := http.NewServeMux()
+
+ var distFS fs.FS
+ if *distDir == "./dist" {
+ var err error
+ distFS, err = fs.Sub(embeddedDistFS, "dist")
+ if err != nil {
+ log.Fatalf("Could not drop dist/ prefix from embedded FS: %v", err)
+ }
+ } else {
+ distFS = os.DirFS(*distDir)
+ }
+
+ indexBytes, err := generateServeIndex(distFS)
+ if err != nil {
+ log.Fatalf("Could not generate index.html: %v", err)
+ }
+ mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.ServeContent(w, r, "index.html", serveStartTime, bytes.NewReader(indexBytes))
+ }))
+ mux.Handle("/dist/", http.StripPrefix("/dist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ handleServeDist(w, r, distFS)
+ })))
+ tsweb.Debugger(mux)
+
+ log.Printf("Listening on %s", *addr)
+ err = http.ListenAndServe(*addr, mux)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func generateServeIndex(distFS fs.FS) ([]byte, error) {
+ log.Printf("Generating index.html...\n")
+ rawIndexBytes, err := embeddedFS.ReadFile("index.html")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read index.html: %w", err)
+ }
+
+ esbuildMetadataFile, err := distFS.Open("esbuild-metadata.json")
+ if err != nil {
+ return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
+ }
+ defer esbuildMetadataFile.Close()
+ esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
+ if err != nil {
+ return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
+ }
+ var esbuildMetadata EsbuildMetadata
+ if err := json.Unmarshal(esbuildMetadataBytes, &esbuildMetadata); err != nil {
+ return nil, fmt.Errorf("Could not parse esbuild-metadata.json: %w", err)
+ }
+ entryPointsToHashedDistPaths := make(map[string]string)
+ mainWasmPath := ""
+ for outputPath, output := range esbuildMetadata.Outputs {
+ if output.EntryPoint != "" {
+ entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
+ }
+ if path.Ext(outputPath) == ".wasm" {
+ for input := range output.Inputs {
+ if input == "src/main.wasm" {
+ mainWasmPath = path.Join("dist", outputPath)
+ break
+ }
+ }
+ }
+ }
+
+ indexBytes := rawIndexBytes
+ for entryPointPath, defaultDistPath := range entryPointsToDefaultDistPaths {
+ hashedDistPath := entryPointsToHashedDistPaths[entryPointPath]
+ if hashedDistPath != "" {
+ indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
+ }
+ }
+ if mainWasmPath != "" {
+ mainWasmPrefetch := fmt.Sprintf("</title>\n<link rel='preload' as='fetch' crossorigin='anonymous' href='%s'>", mainWasmPath)
+ indexBytes = bytes.ReplaceAll(indexBytes, []byte("</title>"), []byte(mainWasmPrefetch))
+ }
+
+ return indexBytes, nil
+}
+
+var entryPointsToDefaultDistPaths = map[string]string{
+ "src/app/index.css": "dist/index.css",
+ "src/app/index.ts": "dist/index.js",
+}
+
+func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
+ path := r.URL.Path
+ f, err := precompress.OpenPrecompressedFile(w, r, path, distFS)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+ defer f.Close()
+
+ // fs.File does not claim to implement Seeker, but in practice it does.
+ fSeeker, ok := f.(io.ReadSeeker)
+ if !ok {
+ http.Error(w, "Not seekable", http.StatusInternalServerError)
+ return
+ }
+
+ // Aggressively cache static assets, since we cache-bust our assets with
+ // hashed filenames.
+ w.Header().Set("Cache-Control", "public, max-age=31535996")
+ w.Header().Set("Vary", "Accept-Encoding")
+
+ http.ServeContent(w, r, path, serveStartTime, fSeeker)
+}
diff --git a/cmd/tsconnect/src/app/app.tsx b/cmd/tsconnect/src/app/app.tsx
index ee538eaea..c0aa7a5e8 100644
--- a/cmd/tsconnect/src/app/app.tsx
+++ b/cmd/tsconnect/src/app/app.tsx
@@ -1,147 +1,147 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { render, Component } from "preact"
-import { URLDisplay } from "./url-display"
-import { Header } from "./header"
-import { GoPanicDisplay } from "./go-panic-display"
-import { SSH } from "./ssh"
-
-type AppState = {
- ipn?: IPN
- ipnState: IPNState
- netMap?: IPNNetMap
- browseToURL?: string
- goPanicError?: string
-}
-
-class App extends Component<{}, AppState> {
- state: AppState = { ipnState: "NoState" }
- #goPanicTimeout?: number
-
- render() {
- const { ipn, ipnState, goPanicError, netMap, browseToURL } = this.state
-
- let goPanicDisplay
- if (goPanicError) {
- goPanicDisplay = (
- <GoPanicDisplay error={goPanicError} dismiss={this.clearGoPanic} />
- )
- }
-
- let urlDisplay
- if (browseToURL) {
- urlDisplay = <URLDisplay url={browseToURL} />
- }
-
- let machineAuthInstructions
- if (ipnState === "NeedsMachineAuth") {
- machineAuthInstructions = (
- <div class="container mx-auto px-4 text-center">
- An administrator needs to approve this device.
- </div>
- )
- }
-
- const lockedOut = netMap?.lockedOut
- let lockedOutInstructions
- if (lockedOut) {
- lockedOutInstructions = (
- <div class="container mx-auto px-4 text-center space-y-4">
- <p>This instance of Tailscale Connect needs to be signed, due to
- {" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
- being enabled on this domain.
- </p>
-
- <p>
- Run the following command on a device with a trusted tailnet lock key:
- <pre>tailscale lock sign {netMap.self.nodeKey}</pre>
- </p>
- </div>
- )
- }
-
- let ssh
- if (ipn && ipnState === "Running" && netMap && !lockedOut) {
- ssh = <SSH netMap={netMap} ipn={ipn} />
- }
-
- return (
- <>
- <Header state={ipnState} ipn={ipn} />
- {goPanicDisplay}
- <div class="flex-grow flex flex-col justify-center overflow-hidden">
- {urlDisplay}
- {machineAuthInstructions}
- {lockedOutInstructions}
- {ssh}
- </div>
- </>
- )
- }
-
- runWithIPN(ipn: IPN) {
- this.setState({ ipn }, () => {
- ipn.run({
- notifyState: this.handleIPNState,
- notifyNetMap: this.handleNetMap,
- notifyBrowseToURL: this.handleBrowseToURL,
- notifyPanicRecover: this.handleGoPanic,
- })
- })
- }
-
- handleIPNState = (state: IPNState) => {
- const { ipn } = this.state
- this.setState({ ipnState: state })
- if (state === "NeedsLogin") {
- ipn?.login()
- } else if (["Running", "NeedsMachineAuth"].includes(state)) {
- this.setState({ browseToURL: undefined })
- }
- }
-
- handleNetMap = (netMapStr: string) => {
- const netMap = JSON.parse(netMapStr) as IPNNetMap
- if (DEBUG) {
- console.log("Received net map: " + JSON.stringify(netMap, null, 2))
- }
- this.setState({ netMap })
- }
-
- handleBrowseToURL = (url: string) => {
- if (this.state.ipnState === "Running") {
- // Ignore URL requests if we're already running -- it's most likely an
- // SSH check mode trigger and we already linkify the displayed URL
- // in the terminal.
- return
- }
- this.setState({ browseToURL: url })
- }
-
- handleGoPanic = (error: string) => {
- if (DEBUG) {
- console.error("Go panic", error)
- }
- this.setState({ goPanicError: error })
- if (this.#goPanicTimeout) {
- window.clearTimeout(this.#goPanicTimeout)
- }
- this.#goPanicTimeout = window.setTimeout(this.clearGoPanic, 10000)
- }
-
- clearGoPanic = () => {
- window.clearTimeout(this.#goPanicTimeout)
- this.#goPanicTimeout = undefined
- this.setState({ goPanicError: undefined })
- }
-}
-
-export function renderApp(): Promise<App> {
- return new Promise((resolve) => {
- render(
- <App ref={(app) => (app ? resolve(app) : undefined)} />,
- document.body
- )
- })
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+import { render, Component } from "preact"
+import { URLDisplay } from "./url-display"
+import { Header } from "./header"
+import { GoPanicDisplay } from "./go-panic-display"
+import { SSH } from "./ssh"
+
+type AppState = {
+ ipn?: IPN
+ ipnState: IPNState
+ netMap?: IPNNetMap
+ browseToURL?: string
+ goPanicError?: string
+}
+
+class App extends Component<{}, AppState> {
+ state: AppState = { ipnState: "NoState" }
+ #goPanicTimeout?: number
+
+ render() {
+ const { ipn, ipnState, goPanicError, netMap, browseToURL } = this.state
+
+ let goPanicDisplay
+ if (goPanicError) {
+ goPanicDisplay = (
+ <GoPanicDisplay error={goPanicError} dismiss={this.clearGoPanic} />
+ )
+ }
+
+ let urlDisplay
+ if (browseToURL) {
+ urlDisplay = <URLDisplay url={browseToURL} />
+ }
+
+ let machineAuthInstructions
+ if (ipnState === "NeedsMachineAuth") {
+ machineAuthInstructions = (
+ <div class="container mx-auto px-4 text-center">
+ An administrator needs to approve this device.
+ </div>
+ )
+ }
+
+ const lockedOut = netMap?.lockedOut
+ let lockedOutInstructions
+ if (lockedOut) {
+ lockedOutInstructions = (
+ <div class="container mx-auto px-4 text-center space-y-4">
+ <p>This instance of Tailscale Connect needs to be signed, due to
+ {" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
+ being enabled on this domain.
+ </p>
+
+ <p>
+ Run the following command on a device with a trusted tailnet lock key:
+ <pre>tailscale lock sign {netMap.self.nodeKey}</pre>
+ </p>
+ </div>
+ )
+ }
+
+ let ssh
+ if (ipn && ipnState === "Running" && netMap && !lockedOut) {
+ ssh = <SSH netMap={netMap} ipn={ipn} />
+ }
+
+ return (
+ <>
+ <Header state={ipnState} ipn={ipn} />
+ {goPanicDisplay}
+ <div class="flex-grow flex flex-col justify-center overflow-hidden">
+ {urlDisplay}
+ {machineAuthInstructions}
+ {lockedOutInstructions}
+ {ssh}
+ </div>
+ </>
+ )
+ }
+
+ runWithIPN(ipn: IPN) {
+ this.setState({ ipn }, () => {
+ ipn.run({
+ notifyState: this.handleIPNState,
+ notifyNetMap: this.handleNetMap,
+ notifyBrowseToURL: this.handleBrowseToURL,
+ notifyPanicRecover: this.handleGoPanic,
+ })
+ })
+ }
+
+ handleIPNState = (state: IPNState) => {
+ const { ipn } = this.state
+ this.setState({ ipnState: state })
+ if (state === "NeedsLogin") {
+ ipn?.login()
+ } else if (["Running", "NeedsMachineAuth"].includes(state)) {
+ this.setState({ browseToURL: undefined })
+ }
+ }
+
+ handleNetMap = (netMapStr: string) => {
+ const netMap = JSON.parse(netMapStr) as IPNNetMap
+ if (DEBUG) {
+ console.log("Received net map: " + JSON.stringify(netMap, null, 2))
+ }
+ this.setState({ netMap })
+ }
+
+ handleBrowseToURL = (url: string) => {
+ if (this.state.ipnState === "Running") {
+ // Ignore URL requests if we're already running -- it's most likely an
+ // SSH check mode trigger and we already linkify the displayed URL
+ // in the terminal.
+ return
+ }
+ this.setState({ browseToURL: url })
+ }
+
+ handleGoPanic = (error: string) => {
+ if (DEBUG) {
+ console.error("Go panic", error)
+ }
+ this.setState({ goPanicError: error })
+ if (this.#goPanicTimeout) {
+ window.clearTimeout(this.#goPanicTimeout)
+ }
+ this.#goPanicTimeout = window.setTimeout(this.clearGoPanic, 10000)
+ }
+
+ clearGoPanic = () => {
+ window.clearTimeout(this.#goPanicTimeout)
+ this.#goPanicTimeout = undefined
+ this.setState({ goPanicError: undefined })
+ }
+}
+
+export function renderApp(): Promise<App> {
+ return new Promise((resolve) => {
+ render(
+ <App ref={(app) => (app ? resolve(app) : undefined)} />,
+ document.body
+ )
+ })
+}
diff --git a/cmd/tsconnect/src/app/go-panic-display.tsx b/cmd/tsconnect/src/app/go-panic-display.tsx
index 5dd7095a2..aab35c4d5 100644
--- a/cmd/tsconnect/src/app/go-panic-display.tsx
+++ b/cmd/tsconnect/src/app/go-panic-display.tsx
@@ -1,20 +1,20 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-export function GoPanicDisplay({
- error,
- dismiss,
-}: {
- error: string
- dismiss: () => void
-}) {
- return (
- <div
- class="rounded bg-red-500 p-2 absolute top-2 right-2 text-white font-bold text-right cursor-pointer"
- onClick={dismiss}
- >
- Tailscale has encountered an error.
- <div class="text-sm font-normal">Click to reload</div>
- </div>
- )
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+export function GoPanicDisplay({
+ error,
+ dismiss,
+}: {
+ error: string
+ dismiss: () => void
+}) {
+ return (
+ <div
+ class="rounded bg-red-500 p-2 absolute top-2 right-2 text-white font-bold text-right cursor-pointer"
+ onClick={dismiss}
+ >
+ Tailscale has encountered an error.
+ <div class="text-sm font-normal">Click to reload</div>
+ </div>
+ )
+}
diff --git a/cmd/tsconnect/src/app/header.tsx b/cmd/tsconnect/src/app/header.tsx
index 099ff2f8c..8449f4563 100644
--- a/cmd/tsconnect/src/app/header.tsx
+++ b/cmd/tsconnect/src/app/header.tsx
@@ -1,37 +1,37 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
- const stateText = STATE_LABELS[state]
-
- let logoutButton
- if (state === "Running") {
- logoutButton = (
- <button
- class="button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
- onClick={() => ipn?.logout()}
- >
- Logout
- </button>
- )
- }
- return (
- <div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
- <header class="container mx-auto px-4 flex flex-row items-center">
- <h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
- <div class="text-gray-600">{stateText}</div>
- {logoutButton}
- </header>
- </div>
- )
-}
-
-const STATE_LABELS = {
- NoState: "Initializing…",
- InUseOtherUser: "In-use by another user",
- NeedsLogin: "Needs login",
- NeedsMachineAuth: "Needs approval",
- Stopped: "Stopped",
- Starting: "Starting…",
- Running: "Running",
-} as const
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+export function Header({ state, ipn }: { state: IPNState; ipn?: IPN }) {
+ const stateText = STATE_LABELS[state]
+
+ let logoutButton
+ if (state === "Running") {
+ logoutButton = (
+ <button
+ class="button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
+ onClick={() => ipn?.logout()}
+ >
+ Logout
+ </button>
+ )
+ }
+ return (
+ <div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2">
+ <header class="container mx-auto px-4 flex flex-row items-center">
+ <h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
+ <div class="text-gray-600">{stateText}</div>
+ {logoutButton}
+ </header>
+ </div>
+ )
+}
+
+const STATE_LABELS = {
+ NoState: "Initializing…",
+ InUseOtherUser: "In-use by another user",
+ NeedsLogin: "Needs login",
+ NeedsMachineAuth: "Needs approval",
+ Stopped: "Stopped",
+ Starting: "Starting…",
+ Running: "Running",
+} as const
diff --git a/cmd/tsconnect/src/app/index.css b/cmd/tsconnect/src/app/index.css
index 751b313d9..848b83d12 100644
--- a/cmd/tsconnect/src/app/index.css
+++ b/cmd/tsconnect/src/app/index.css
@@ -1,74 +1,74 @@
-/* Copyright (c) Tailscale Inc & AUTHORS */
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-@import "xterm/css/xterm.css";
-
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-.link {
- @apply text-blue-600;
-}
-
-.link:hover {
- @apply underline;
-}
-
-.button {
- @apply font-medium py-1 px-2 rounded-md border border-transparent text-center cursor-pointer;
- transition-property: background-color, border-color, color, box-shadow;
- transition-duration: 120ms;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
- min-width: 80px;
-}
-.button:focus {
- @apply outline-none ring;
-}
-.button:disabled {
- @apply pointer-events-none select-none;
-}
-
-.input {
- @apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors px-3;
- height: 2.375rem;
-}
-
-.input::placeholder {
- @apply text-gray-400;
-}
-
-.input:disabled {
- @apply border-gray-200;
- @apply bg-gray-50;
- @apply cursor-not-allowed;
-}
-
-.input:focus {
- @apply outline-none ring border-transparent;
-}
-
-.select {
- @apply appearance-none py-2 px-3 leading-tight rounded-md bg-white border border-gray-300;
-}
-
-.select-with-arrow {
- @apply relative;
-}
-
-.select-with-arrow .select {
- width: 100%;
-}
-
-.select-with-arrow::after {
- @apply absolute;
- content: "";
- top: 50%;
- right: 0.5rem;
- transform: translate(-0.3em, -0.15em);
- width: 0.6em;
- height: 0.4em;
- opacity: 0.6;
- background-color: currentColor;
- clip-path: polygon(100% 0%, 0 0%, 50% 100%);
-}
+/* Copyright (c) Tailscale Inc & AUTHORS */
+/* SPDX-License-Identifier: BSD-3-Clause */
+
+@import "xterm/css/xterm.css";
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+.link {
+ @apply text-blue-600;
+}
+
+.link:hover {
+ @apply underline;
+}
+
+.button {
+ @apply font-medium py-1 px-2 rounded-md border border-transparent text-center cursor-pointer;
+ transition-property: background-color, border-color, color, box-shadow;
+ transition-duration: 120ms;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+ min-width: 80px;
+}
+.button:focus {
+ @apply outline-none ring;
+}
+.button:disabled {
+ @apply pointer-events-none select-none;
+}
+
+.input {
+ @apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors px-3;
+ height: 2.375rem;
+}
+
+.input::placeholder {
+ @apply text-gray-400;
+}
+
+.input:disabled {
+ @apply border-gray-200;
+ @apply bg-gray-50;
+ @apply cursor-not-allowed;
+}
+
+.input:focus {
+ @apply outline-none ring border-transparent;
+}
+
+.select {
+ @apply appearance-none py-2 px-3 leading-tight rounded-md bg-white border border-gray-300;
+}
+
+.select-with-arrow {
+ @apply relative;
+}
+
+.select-with-arrow .select {
+ width: 100%;
+}
+
+.select-with-arrow::after {
+ @apply absolute;
+ content: "";
+ top: 50%;
+ right: 0.5rem;
+ transform: translate(-0.3em, -0.15em);
+ width: 0.6em;
+ height: 0.4em;
+ opacity: 0.6;
+ background-color: currentColor;
+ clip-path: polygon(100% 0%, 0 0%, 50% 100%);
+}
diff --git a/cmd/tsconnect/src/app/index.ts b/cmd/tsconnect/src/app/index.ts
index 24ca45439..1432188ae 100644
--- a/cmd/tsconnect/src/app/index.ts
+++ b/cmd/tsconnect/src/app/index.ts
@@ -1,36 +1,36 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import "../wasm_exec"
-import wasmUrl from "./main.wasm"
-import { sessionStateStorage } from "../lib/js-state-store"
-import { renderApp } from "./app"
-
-async function main() {
- const app = await renderApp()
- const go = new Go()
- const wasmInstance = await WebAssembly.instantiateStreaming(
- fetch(`./dist/${wasmUrl}`),
- go.importObject
- )
- // The Go process should never exit, if it does then it's an unhandled panic.
- go.run(wasmInstance.instance).then(() =>
- app.handleGoPanic("Unexpected shutdown")
- )
-
- const params = new URLSearchParams(window.location.search)
- const authKey = params.get("authkey") ?? undefined
-
- const ipn = newIPN({
- // Persist IPN state in sessionStorage in development, so that we don't need
- // to re-authorize every time we reload the page.
- stateStorage: DEBUG ? sessionStateStorage : undefined,
- // authKey allows for an auth key to be
- // specified as a url param which automatically
- // authorizes the client for use.
- authKey: DEBUG ? authKey : undefined,
- })
- app.runWithIPN(ipn)
-}
-
-main()
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+import "../wasm_exec"
+import wasmUrl from "./main.wasm"
+import { sessionStateStorage } from "../lib/js-state-store"
+import { renderApp } from "./app"
+
+async function main() {
+ const app = await renderApp()
+ const go = new Go()
+ const wasmInstance = await WebAssembly.instantiateStreaming(
+ fetch(`./dist/${wasmUrl}`),
+ go.importObject
+ )
+ // The Go process should never exit, if it does then it's an unhandled panic.
+ go.run(wasmInstance.instance).then(() =>
+ app.handleGoPanic("Unexpected shutdown")
+ )
+
+ const params = new URLSearchParams(window.location.search)
+ const authKey = params.get("authkey") ?? undefined
+
+ const ipn = newIPN({
+ // Persist IPN state in sessionStorage in development, so that we don't need
+ // to re-authorize every time we reload the page.
+ stateStorage: DEBUG ? sessionStateStorage : undefined,
+ // authKey allows for an auth key to be
+ // specified as a url param which automatically
+ // authorizes the client for use.
+ authKey: DEBUG ? authKey : undefined,
+ })
+ app.runWithIPN(ipn)
+}
+
+main()
diff --git a/cmd/tsconnect/src/app/ssh.tsx b/cmd/tsconnect/src/app/ssh.tsx
index df81745bd..1534fd5db 100644
--- a/cmd/tsconnect/src/app/ssh.tsx
+++ b/cmd/tsconnect/src/app/ssh.tsx
@@ -1,157 +1,157 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks"
-import { createPortal } from "preact/compat"
-import type { VNode } from "preact"
-import { runSSHSession, SSHSessionDef } from "../lib/ssh"
-
-export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
- const [sshSessionDef, setSSHSessionDef] = useState<SSHFormSessionDef | null>(
- null
- )
- const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
- if (sshSessionDef) {
- const sshSession = (
- <SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
- )
- if (sshSessionDef.newWindow) {
- return <NewWindow close={clearSSHSessionDef}>{sshSession}</NewWindow>
- }
- return sshSession
- }
- const sshPeers = netMap.peers.filter(
- (p) => p.tailscaleSSHEnabled && p.online !== false
- )
-
- if (sshPeers.length == 0) {
- return <NoSSHPeers />
- }
-
- return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} />
-}
-
-type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
-
-function SSHSession({
- def,
- ipn,
- onDone,
-}: {
- def: SSHSessionDef
- ipn: IPN
- onDone: () => void
-}) {
- const ref = useRef<HTMLDivElement>(null)
- useEffect(() => {
- if (ref.current) {
- runSSHSession(ref.current, def, ipn, {
- onConnectionProgress: (p) => console.log("Connection progress", p),
- onConnected() {},
- onError: (err) => console.error(err),
- onDone,
- })
- }
- }, [ref])
-
- return <div class="flex-grow bg-black p-2 overflow-hidden" ref={ref} />
-}
-
-function NoSSHPeers() {
- return (
- <div class="container mx-auto px-4 text-center">
- None of your machines have{" "}
- <a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
- Tailscale SSH
- </a>
- {" "}enabled. Give it a try!
- </div>
- )
-}
-
-function SSHForm({
- sshPeers,
- onSubmit,
-}: {
- sshPeers: IPNNetMapPeerNode[]
- onSubmit: (def: SSHFormSessionDef) => void
-}) {
- sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
- const [username, setUsername] = useState("")
- const [hostname, setHostname] = useState(sshPeers[0].name)
- return (
- <form
- class="container mx-auto px-4 flex justify-center"
- onSubmit={(e) => {
- e.preventDefault()
- onSubmit({ username, hostname })
- }}
- >
- <input
- type="text"
- class="input username"
- placeholder="Username"
- onChange={(e) => setUsername(e.currentTarget.value)}
- />
- <div class="select-with-arrow mx-2">
- <select
- class="select"
- onChange={(e) => setHostname(e.currentTarget.value)}
- >
- {sshPeers.map((p) => (
- <option key={p.nodeKey}>{p.name.split(".")[0]}</option>
- ))}
- </select>
- </div>
- <input
- type="submit"
- class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
- value="SSH"
- onClick={(e) => {
- if (e.altKey) {
- e.preventDefault()
- e.stopPropagation()
- onSubmit({ username, hostname, newWindow: true })
- }
- }}
- />
- </form>
- )
-}
-
-const NewWindow = ({
- children,
- close,
-}: {
- children: VNode
- close: () => void
-}) => {
- const newWindow = useMemo(() => {
- const newWindow = window.open(undefined, undefined, "width=600,height=400")
- if (newWindow) {
- const containerNode = newWindow.document.createElement("div")
- containerNode.className = "h-screen flex flex-col overflow-hidden"
- newWindow.document.body.appendChild(containerNode)
-
- for (const linkNode of document.querySelectorAll(
- "head link[rel=stylesheet]"
- )) {
- const newLink = document.createElement("link")
- newLink.rel = "stylesheet"
- newLink.href = (linkNode as HTMLLinkElement).href
- newWindow.document.head.appendChild(newLink)
- }
- }
- return newWindow
- }, [])
- if (!newWindow) {
- console.error("Could not open window")
- return null
- }
- newWindow.onbeforeunload = () => {
- close()
- }
-
- useEffect(() => () => newWindow.close(), [])
- return createPortal(children, newWindow.document.body.lastChild as Element)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+import { useState, useCallback, useMemo, useEffect, useRef } from "preact/hooks"
+import { createPortal } from "preact/compat"
+import type { VNode } from "preact"
+import { runSSHSession, SSHSessionDef } from "../lib/ssh"
+
+export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) {
+ const [sshSessionDef, setSSHSessionDef] = useState<SSHFormSessionDef | null>(
+ null
+ )
+ const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), [])
+ if (sshSessionDef) {
+ const sshSession = (
+ <SSHSession def={sshSessionDef} ipn={ipn} onDone={clearSSHSessionDef} />
+ )
+ if (sshSessionDef.newWindow) {
+ return <NewWindow close={clearSSHSessionDef}>{sshSession}</NewWindow>
+ }
+ return sshSession
+ }
+ const sshPeers = netMap.peers.filter(
+ (p) => p.tailscaleSSHEnabled && p.online !== false
+ )
+
+ if (sshPeers.length == 0) {
+ return <NoSSHPeers />
+ }
+
+ return <SSHForm sshPeers={sshPeers} onSubmit={setSSHSessionDef} />
+}
+
+type SSHFormSessionDef = SSHSessionDef & { newWindow?: boolean }
+
+function SSHSession({
+ def,
+ ipn,
+ onDone,
+}: {
+ def: SSHSessionDef
+ ipn: IPN
+ onDone: () => void
+}) {
+ const ref = useRef<HTMLDivElement>(null)
+ useEffect(() => {
+ if (ref.current) {
+ runSSHSession(ref.current, def, ipn, {
+ onConnectionProgress: (p) => console.log("Connection progress", p),
+ onConnected() {},
+ onError: (err) => console.error(err),
+ onDone,
+ })
+ }
+ }, [ref])
+
+ return <div class="flex-grow bg-black p-2 overflow-hidden" ref={ref} />
+}
+
+function NoSSHPeers() {
+ return (
+ <div class="container mx-auto px-4 text-center">
+ None of your machines have{" "}
+ <a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
+ Tailscale SSH
+ </a>
+ {" "}enabled. Give it a try!
+ </div>
+ )
+}
+
+function SSHForm({
+ sshPeers,
+ onSubmit,
+}: {
+ sshPeers: IPNNetMapPeerNode[]
+ onSubmit: (def: SSHFormSessionDef) => void
+}) {
+ sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name))
+ const [username, setUsername] = useState("")
+ const [hostname, setHostname] = useState(sshPeers[0].name)
+ return (
+ <form
+ class="container mx-auto px-4 flex justify-center"
+ onSubmit={(e) => {
+ e.preventDefault()
+ onSubmit({ username, hostname })
+ }}
+ >
+ <input
+ type="text"
+ class="input username"
+ placeholder="Username"
+ onChange={(e) => setUsername(e.currentTarget.value)}
+ />
+ <div class="select-with-arrow mx-2">
+ <select
+ class="select"
+ onChange={(e) => setHostname(e.currentTarget.value)}
+ >
+ {sshPeers.map((p) => (
+ <option key={p.nodeKey}>{p.name.split(".")[0]}</option>
+ ))}
+ </select>
+ </div>
+ <input
+ type="submit"
+ class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
+ value="SSH"
+ onClick={(e) => {
+ if (e.altKey) {
+ e.preventDefault()
+ e.stopPropagation()
+ onSubmit({ username, hostname, newWindow: true })
+ }
+ }}
+ />
+ </form>
+ )
+}
+
+const NewWindow = ({
+ children,
+ close,
+}: {
+ children: VNode
+ close: () => void
+}) => {
+ const newWindow = useMemo(() => {
+ const newWindow = window.open(undefined, undefined, "width=600,height=400")
+ if (newWindow) {
+ const containerNode = newWindow.document.createElement("div")
+ containerNode.className = "h-screen flex flex-col overflow-hidden"
+ newWindow.document.body.appendChild(containerNode)
+
+ for (const linkNode of document.querySelectorAll(
+ "head link[rel=stylesheet]"
+ )) {
+ const newLink = document.createElement("link")
+ newLink.rel = "stylesheet"
+ newLink.href = (linkNode as HTMLLinkElement).href
+ newWindow.document.head.appendChild(newLink)
+ }
+ }
+ return newWindow
+ }, [])
+ if (!newWindow) {
+ console.error("Could not open window")
+ return null
+ }
+ newWindow.onbeforeunload = () => {
+ close()
+ }
+
+ useEffect(() => () => newWindow.close(), [])
+ return createPortal(children, newWindow.document.body.lastChild as Element)
+}
diff --git a/cmd/tsconnect/src/app/url-display.tsx b/cmd/tsconnect/src/app/url-display.tsx
index fc82c7fb9..c9b590181 100644
--- a/cmd/tsconnect/src/app/url-display.tsx
+++ b/cmd/tsconnect/src/app/url-display.tsx
@@ -1,31 +1,31 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-import { useState } from "preact/hooks"
-import * as qrcode from "qrcode"
-
-export function URLDisplay({ url }: { url: string }) {
- const [dataURL, setDataURL] = useState("")
- qrcode.toDataURL(url, { width: 512 }, (err, dataURL) => {
- if (err) {
- console.error("Error generating QR code", err)
- } else {
- setDataURL(dataURL)
- }
- })
-
- return (
- <div class="flex flex-col items-center justify-items-center">
- <a href={url} class="link" target="_blank">
- <img
- src={dataURL}
- class="mx-auto"
- width="256"
- height="256"
- alt="QR Code of URL"
- />
- {url}
- </a>
- </div>
- )
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+import { useState } from "preact/hooks"
+import * as qrcode from "qrcode"
+
+export function URLDisplay({ url }: { url: string }) {
+ const [dataURL, setDataURL] = useState("")
+ qrcode.toDataURL(url, { width: 512 }, (err, dataURL) => {
+ if (err) {
+ console.error("Error generating QR code", err)
+ } else {
+ setDataURL(dataURL)
+ }
+ })
+
+ return (
+ <div class="flex flex-col items-center justify-items-center">
+ <a href={url} class="link" target="_blank">
+ <img
+ src={dataURL}
+ class="mx-auto"
+ width="256"
+ height="256"
+ alt="QR Code of URL"
+ />
+ {url}
+ </a>
+ </div>
+ )
+}
diff --git a/cmd/tsconnect/src/lib/js-state-store.ts b/cmd/tsconnect/src/lib/js-state-store.ts
index e57dfd98e..7685e28a9 100644
--- a/cmd/tsconnect/src/lib/js-state-store.ts
+++ b/cmd/tsconnect/src/lib/js-state-store.ts
@@ -1,13 +1,13 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/** @fileoverview Callbacks used by jsStateStore to persist IPN state. */
-
-export const sessionStateStorage: IPNStateStorage = {
- setState(id, value) {
- window.sessionStorage[`ipn-state-${id}`] = value
- },
- getState(id) {
- return window.sessionStorage[`ipn-state-${id}`] || ""
- },
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+/** @fileoverview Callbacks used by jsStateStore to persist IPN state. */
+
+export const sessionStateStorage: IPNStateStorage = {
+ setState(id, value) {
+ window.sessionStorage[`ipn-state-${id}`] = value
+ },
+ getState(id) {
+ return window.sessionStorage[`ipn-state-${id}`] || ""
+ },
+}
diff --git a/cmd/tsconnect/src/pkg/pkg.css b/cmd/tsconnect/src/pkg/pkg.css
index 76ea21f5b..60146d5b7 100644
--- a/cmd/tsconnect/src/pkg/pkg.css
+++ b/cmd/tsconnect/src/pkg/pkg.css
@@ -1,8 +1,8 @@
-/* Copyright (c) Tailscale Inc & AUTHORS */
-/* SPDX-License-Identifier: BSD-3-Clause */
-
-@import "xterm/css/xterm.css";
-
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+/* Copyright (c) Tailscale Inc & AUTHORS */
+/* SPDX-License-Identifier: BSD-3-Clause */
+
+@import "xterm/css/xterm.css";
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/cmd/tsconnect/src/pkg/pkg.ts b/cmd/tsconnect/src/pkg/pkg.ts
index 4d535cb40..c0dcb5652 100644
--- a/cmd/tsconnect/src/pkg/pkg.ts
+++ b/cmd/tsconnect/src/pkg/pkg.ts
@@ -1,40 +1,40 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Type definitions need to be manually imported for dts-bundle-generator to
-// discover them.
-/// <reference path="../types/esbuild.d.ts" />
-/// <reference path="../types/wasm_js.d.ts" />
-
-import "../wasm_exec"
-import wasmURL from "./main.wasm"
-
-/**
- * Superset of the IPNConfig type, with additional configuration that is
- * needed for the package to function.
- */
-type IPNPackageConfig = IPNConfig & {
- // Auth key used to initialize the Tailscale client (required)
- authKey: string
- // URL of the main.wasm file that is included in the page, if it is not
- // accessible via a relative URL.
- wasmURL?: string
- // Function invoked if the Go process panics or unexpectedly exits.
- panicHandler: (err: string) => void
-}
-
-export async function createIPN(config: IPNPackageConfig): Promise<IPN> {
- const go = new Go()
- const wasmInstance = await WebAssembly.instantiateStreaming(
- fetch(config.wasmURL ?? wasmURL),
- go.importObject
- )
- // The Go process should never exit, if it does then it's an unhandled panic.
- go.run(wasmInstance.instance).then(() =>
- config.panicHandler("Unexpected shutdown")
- )
-
- return newIPN(config)
-}
-
-export { runSSHSession } from "../lib/ssh"
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Type definitions need to be manually imported for dts-bundle-generator to
+// discover them.
+/// <reference path="../types/esbuild.d.ts" />
+/// <reference path="../types/wasm_js.d.ts" />
+
+import "../wasm_exec"
+import wasmURL from "./main.wasm"
+
+/**
+ * Superset of the IPNConfig type, with additional configuration that is
+ * needed for the package to function.
+ */
+type IPNPackageConfig = IPNConfig & {
+ // Auth key used to initialize the Tailscale client (required)
+ authKey: string
+ // URL of the main.wasm file that is included in the page, if it is not
+ // accessible via a relative URL.
+ wasmURL?: string
+ // Function invoked if the Go process panics or unexpectedly exits.
+ panicHandler: (err: string) => void
+}
+
+export async function createIPN(config: IPNPackageConfig): Promise<IPN> {
+ const go = new Go()
+ const wasmInstance = await WebAssembly.instantiateStreaming(
+ fetch(config.wasmURL ?? wasmURL),
+ go.importObject
+ )
+ // The Go process should never exit, if it does then it's an unhandled panic.
+ go.run(wasmInstance.instance).then(() =>
+ config.panicHandler("Unexpected shutdown")
+ )
+
+ return newIPN(config)
+}
+
+export { runSSHSession } from "../lib/ssh"
diff --git a/cmd/tsconnect/src/types/esbuild.d.ts b/cmd/tsconnect/src/types/esbuild.d.ts
index ef28f7b1c..7153b4244 100644
--- a/cmd/tsconnect/src/types/esbuild.d.ts
+++ b/cmd/tsconnect/src/types/esbuild.d.ts
@@ -1,14 +1,14 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/**
- * @fileoverview Type definitions for types generated by the esbuild build
- * process.
- */
-
-declare module "*.wasm" {
- const path: string
- export default path
-}
-
-declare const DEBUG: boolean
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+/**
+ * @fileoverview Type definitions for types generated by the esbuild build
+ * process.
+ */
+
+declare module "*.wasm" {
+ const path: string
+ export default path
+}
+
+declare const DEBUG: boolean
diff --git a/cmd/tsconnect/src/types/wasm_js.d.ts b/cmd/tsconnect/src/types/wasm_js.d.ts
index 492197ccb..82822c508 100644
--- a/cmd/tsconnect/src/types/wasm_js.d.ts
+++ b/cmd/tsconnect/src/types/wasm_js.d.ts
@@ -1,103 +1,103 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-/**
- * @fileoverview Type definitions for types exported by the wasm_js.go Go
- * module.
- */
-
-declare global {
- function newIPN(config: IPNConfig): IPN
-
- interface IPN {
- run(callbacks: IPNCallbacks): void
- login(): void
- logout(): void
- ssh(
- host: string,
- username: string,
- termConfig: {
- writeFn: (data: string) => void
- writeErrorFn: (err: string) => void
- setReadFn: (readFn: (data: string) => void) => void
- rows: number
- cols: number
- /** Defaults to 5 seconds */
- timeoutSeconds?: number
- onConnectionProgress: (message: string) => void
- onConnected: () => void
- onDone: () => void
- }
- ): IPNSSHSession
- fetch(url: string): Promise<{
- status: number
- statusText: string
- text: () => Promise<string>
- }>
- }
-
- interface IPNSSHSession {
- resize(rows: number, cols: number): boolean
- close(): boolean
- }
-
- interface IPNStateStorage {
- setState(id: string, value: string): void
- getState(id: string): string
- }
-
- type IPNConfig = {
- stateStorage?: IPNStateStorage
- authKey?: string
- controlURL?: string
- hostname?: string
- }
-
- type IPNCallbacks = {
- notifyState: (state: IPNState) => void
- notifyNetMap: (netMapStr: string) => void
- notifyBrowseToURL: (url: string) => void
- notifyPanicRecover: (err: string) => void
- }
-
- type IPNNetMap = {
- self: IPNNetMapSelfNode
- peers: IPNNetMapPeerNode[]
- lockedOut: boolean
- }
-
- type IPNNetMapNode = {
- name: string
- addresses: string[]
- machineKey: string
- nodeKey: string
- }
-
- type IPNNetMapSelfNode = IPNNetMapNode & {
- machineStatus: IPNMachineStatus
- }
-
- type IPNNetMapPeerNode = IPNNetMapNode & {
- online?: boolean
- tailscaleSSHEnabled: boolean
- }
-
- /** Mirrors values from ipn/backend.go */
- type IPNState =
- | "NoState"
- | "InUseOtherUser"
- | "NeedsLogin"
- | "NeedsMachineAuth"
- | "Stopped"
- | "Starting"
- | "Running"
-
- /** Mirrors values from MachineStatus in tailcfg.go */
- type IPNMachineStatus =
- | "MachineUnknown"
- | "MachineUnauthorized"
- | "MachineAuthorized"
- | "MachineInvalid"
-}
-
-export {}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+/**
+ * @fileoverview Type definitions for types exported by the wasm_js.go Go
+ * module.
+ */
+
+declare global {
+ function newIPN(config: IPNConfig): IPN
+
+ interface IPN {
+ run(callbacks: IPNCallbacks): void
+ login(): void
+ logout(): void
+ ssh(
+ host: string,
+ username: string,
+ termConfig: {
+ writeFn: (data: string) => void
+ writeErrorFn: (err: string) => void
+ setReadFn: (readFn: (data: string) => void) => void
+ rows: number
+ cols: number
+ /** Defaults to 5 seconds */
+ timeoutSeconds?: number
+ onConnectionProgress: (message: string) => void
+ onConnected: () => void
+ onDone: () => void
+ }
+ ): IPNSSHSession
+ fetch(url: string): Promise<{
+ status: number
+ statusText: string
+ text: () => Promise<string>
+ }>
+ }
+
+ interface IPNSSHSession {
+ resize(rows: number, cols: number): boolean
+ close(): boolean
+ }
+
+ interface IPNStateStorage {
+ setState(id: string, value: string): void
+ getState(id: string): string
+ }
+
+ type IPNConfig = {
+ stateStorage?: IPNStateStorage
+ authKey?: string
+ controlURL?: string
+ hostname?: string
+ }
+
+ type IPNCallbacks = {
+ notifyState: (state: IPNState) => void
+ notifyNetMap: (netMapStr: string) => void
+ notifyBrowseToURL: (url: string) => void
+ notifyPanicRecover: (err: string) => void
+ }
+
+ type IPNNetMap = {
+ self: IPNNetMapSelfNode
+ peers: IPNNetMapPeerNode[]
+ lockedOut: boolean
+ }
+
+ type IPNNetMapNode = {
+ name: string
+ addresses: string[]
+ machineKey: string
+ nodeKey: string
+ }
+
+ type IPNNetMapSelfNode = IPNNetMapNode & {
+ machineStatus: IPNMachineStatus
+ }
+
+ type IPNNetMapPeerNode = IPNNetMapNode & {
+ online?: boolean
+ tailscaleSSHEnabled: boolean
+ }
+
+ /** Mirrors values from ipn/backend.go */
+ type IPNState =
+ | "NoState"
+ | "InUseOtherUser"
+ | "NeedsLogin"
+ | "NeedsMachineAuth"
+ | "Stopped"
+ | "Starting"
+ | "Running"
+
+ /** Mirrors values from MachineStatus in tailcfg.go */
+ type IPNMachineStatus =
+ | "MachineUnknown"
+ | "MachineUnauthorized"
+ | "MachineAuthorized"
+ | "MachineInvalid"
+}
+
+export {}
diff --git a/cmd/tsconnect/tailwind.config.js b/cmd/tsconnect/tailwind.config.js
index 31823000b..38bc5b97b 100644
--- a/cmd/tsconnect/tailwind.config.js
+++ b/cmd/tsconnect/tailwind.config.js
@@ -1,8 +1,8 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./index.html", "./src/**/*.ts", "./src/**/*.tsx"],
- theme: {
- extend: {},
- },
- plugins: [],
-}
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["./index.html", "./src/**/*.ts", "./src/**/*.tsx"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/cmd/tsconnect/tsconfig.json b/cmd/tsconnect/tsconfig.json
index 52c25c727..1148e2ef0 100644
--- a/cmd/tsconnect/tsconfig.json
+++ b/cmd/tsconnect/tsconfig.json
@@ -1,15 +1,15 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "module": "ES2020",
- "moduleResolution": "node",
- "isolatedModules": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "sourceMap": true,
- "jsx": "react-jsx",
- "jsxImportSource": "preact"
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules"]
-}
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "module": "ES2020",
+ "moduleResolution": "node",
+ "isolatedModules": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "sourceMap": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+}
diff --git a/cmd/tsconnect/tsconnect.go b/cmd/tsconnect/tsconnect.go
index 4c8a0a52e..60ea6ef82 100644
--- a/cmd/tsconnect/tsconnect.go
+++ b/cmd/tsconnect/tsconnect.go
@@ -1,71 +1,71 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !plan9
-
-// The tsconnect command builds and serves the static site that is generated for
-// the Tailscale Connect JS/WASM client. Can be run in 3 modes:
-// - dev: builds the site and serves it. JS and CSS changes can be picked up
-// with a reload.
-// - build: builds the site and writes it to dist/
-// - serve: serves the site from dist/ (embedded in the binary)
-package main // import "tailscale.com/cmd/tsconnect"
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
-)
-
-var (
- addr = flag.String("addr", ":9090", "address to listen on")
- distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
- pkgDir = flag.String("pkgdir", "./pkg", "path of directory to place NPM package build output in")
- yarnPath = flag.String("yarnpath", "", "path yarn executable used to install JavaScript dependencies")
- fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
- devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
- rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
-)
-
-func main() {
- flag.Usage = usage
- flag.Parse()
- if len(flag.Args()) != 1 {
- flag.Usage()
- }
-
- switch flag.Arg(0) {
- case "dev":
- runDev()
- case "dev-pkg":
- runDevPkg()
- case "build":
- runBuild()
- case "build-pkg":
- runBuildPkg()
- case "serve":
- runServe()
- default:
- log.Printf("Unknown command: %s", flag.Arg(0))
- flag.Usage()
- }
-}
-
-func usage() {
- fmt.Fprintf(os.Stderr, `
-usage: tsconnect {dev|build|serve}
-`[1:])
-
- flag.PrintDefaults()
- fmt.Fprintf(os.Stderr, `
-
-tsconnect implements development/build/serving workflows for Tailscale Connect.
-It can be invoked with one of three subcommands:
-
-- dev: Run in development mode, allowing JS and CSS changes to be picked up without a rebuilt or restart.
-- build: Run in production build mode (generating static assets)
-- serve: Run in production serve mode (serving static assets)
-`[1:])
- os.Exit(2)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !plan9
+
+// The tsconnect command builds and serves the static site that is generated for
+// the Tailscale Connect JS/WASM client. Can be run in 3 modes:
+// - dev: builds the site and serves it. JS and CSS changes can be picked up
+// with a reload.
+// - build: builds the site and writes it to dist/
+// - serve: serves the site from dist/ (embedded in the binary)
+package main // import "tailscale.com/cmd/tsconnect"
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+)
+
+var (
+ addr = flag.String("addr", ":9090", "address to listen on")
+ distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
+ pkgDir = flag.String("pkgdir", "./pkg", "path of directory to place NPM package build output in")
+ yarnPath = flag.String("yarnpath", "", "path yarn executable used to install JavaScript dependencies")
+ fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
+ devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
+ rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
+)
+
+func main() {
+ flag.Usage = usage
+ flag.Parse()
+ if len(flag.Args()) != 1 {
+ flag.Usage()
+ }
+
+ switch flag.Arg(0) {
+ case "dev":
+ runDev()
+ case "dev-pkg":
+ runDevPkg()
+ case "build":
+ runBuild()
+ case "build-pkg":
+ runBuildPkg()
+ case "serve":
+ runServe()
+ default:
+ log.Printf("Unknown command: %s", flag.Arg(0))
+ flag.Usage()
+ }
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, `
+usage: tsconnect {dev|build|serve}
+`[1:])
+
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr, `
+
+tsconnect implements development/build/serving workflows for Tailscale Connect.
+It can be invoked with one of three subcommands:
+
+- dev: Run in development mode, allowing JS and CSS changes to be picked up without a rebuilt or restart.
+- build: Run in production build mode (generating static assets)
+- serve: Run in production serve mode (serving static assets)
+`[1:])
+ os.Exit(2)
+}
diff --git a/cmd/tsconnect/yarn.lock b/cmd/tsconnect/yarn.lock
index 663a1244e..914b4e6d0 100644
--- a/cmd/tsconnect/yarn.lock
+++ b/cmd/tsconnect/yarn.lock
@@ -1,713 +1,713 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@nodelib/fs.scandir@2.1.5":
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
- integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
- dependencies:
- "@nodelib/fs.stat" "2.0.5"
- run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
- integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-
-"@nodelib/fs.walk@^1.2.3":
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
- integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
- dependencies:
- "@nodelib/fs.scandir" "2.1.5"
- fastq "^1.6.0"
-
-"@types/golang-wasm-exec@^1.15.0":
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.0.tgz#d0aafbb2b0dc07eaf45dfb83bfb6cdd5b2b3c55c"
- integrity sha512-FrL97mp7WW8LqNinVkzTVKOIQKuYjQqgucnh41+1vRQ+bf1LT8uh++KRf9otZPXsa6H1p8ruIGz1BmCGttOL6Q==
-
-"@types/node@*":
- version "18.6.1"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
- integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
-
-"@types/qrcode@^1.4.2":
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74"
- integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==
- dependencies:
- "@types/node" "*"
-
-acorn-node@^1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
- integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
- dependencies:
- acorn "^7.0.0"
- acorn-walk "^7.0.0"
- xtend "^4.0.2"
-
-acorn-walk@^7.0.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
- integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
-
-acorn@^7.0.0:
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
- integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-anymatch@~3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
- integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-arg@^5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
- integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
-
-binary-extensions@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
- integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-braces@^3.0.2, braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
-
-camelcase-css@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
- integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
-
-camelcase@^5.0.0:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
- integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-chokidar@^3.5.3:
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
- integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
- dependencies:
- anymatch "~3.1.2"
- braces "~3.0.2"
- glob-parent "~5.1.2"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.6.0"
- optionalDependencies:
- fsevents "~2.3.2"
-
-cliui@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
- integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^6.2.0"
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@^1.1.4, color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-cssesc@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
- integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-
-decamelize@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
- integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-
-defined@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
- integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
-
-detective@^5.2.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
- integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
- dependencies:
- acorn-node "^1.8.2"
- defined "^1.0.0"
- minimist "^1.2.6"
-
-didyoumean@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
- integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
-
-dijkstrajs@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
- integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
-
-dlv@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
- integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
-
-dts-bundle-generator@^6.12.0:
- version "6.12.0"
- resolved "https://registry.yarnpkg.com/dts-bundle-generator/-/dts-bundle-generator-6.12.0.tgz#0a221bdce5fdd309a56c8556e645f16ed87ab07d"
- integrity sha512-k/QAvuVaLIdyWRUHduDrWBe4j8PcE6TDt06+f32KHbW7/SmUPbX1O23fFtQgKwUyTBkbIjJFOFtNrF97tJcKug==
- dependencies:
- typescript ">=3.0.1"
- yargs "^17.2.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encode-utf8@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
- integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-fast-glob@^3.2.11:
- version "3.2.11"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
- integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
- dependencies:
- "@nodelib/fs.stat" "^2.0.2"
- "@nodelib/fs.walk" "^1.2.3"
- glob-parent "^5.1.2"
- merge2 "^1.3.0"
- micromatch "^4.0.4"
-
-fastq@^1.6.0:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
- integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
- dependencies:
- reusify "^1.0.4"
-
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
- dependencies:
- to-regex-range "^5.0.1"
-
-find-up@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-caller-file@^2.0.1, get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob-parent@^5.1.2, glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
-glob-parent@^6.0.2:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
- integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
- dependencies:
- is-glob "^4.0.3"
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-is-binary-path@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-core-module@^2.9.0:
- version "2.9.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
- integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
- dependencies:
- has "^1.0.3"
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
- integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-lilconfig@^2.0.5:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
- integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
-
-locate-path@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
- integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
- dependencies:
- p-locate "^4.1.0"
-
-merge2@^1.3.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
- integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-
-micromatch@^4.0.4:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
- integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
- dependencies:
- braces "^3.0.2"
- picomatch "^2.3.1"
-
-minimist@^1.2.6:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
- integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
-nanoid@^3.3.4:
- version "3.3.4"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
- integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-object-hash@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
- integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
-
-p-limit@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
- integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
- dependencies:
- p-try "^2.0.0"
-
-p-locate@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
- integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
- dependencies:
- p-limit "^2.2.0"
-
-p-try@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
- integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-parse@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
- integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-
-picocolors@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
- integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
-
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
- integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-pify@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
- integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
-
-pngjs@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
- integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
-
-postcss-import@^14.1.0:
- version "14.1.0"
- resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
- integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
- dependencies:
- postcss-value-parser "^4.0.0"
- read-cache "^1.0.0"
- resolve "^1.1.7"
-
-postcss-js@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
- integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
- dependencies:
- camelcase-css "^2.0.1"
-
-postcss-load-config@^3.1.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
- integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
- dependencies:
- lilconfig "^2.0.5"
- yaml "^1.10.2"
-
-postcss-nested@5.0.6:
- version "5.0.6"
- resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
- integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
- dependencies:
- postcss-selector-parser "^6.0.6"
-
-postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
- version "6.0.10"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
- integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
- dependencies:
- cssesc "^3.0.0"
- util-deprecate "^1.0.2"
-
-postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
- integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-
-postcss@^8.4.14:
- version "8.4.14"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
- integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
- dependencies:
- nanoid "^3.3.4"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
-preact@^10.10.0:
- version "10.10.0"
- resolved "https://registry.yarnpkg.com/preact/-/preact-10.10.0.tgz#7434750a24b59dae1957d95dc0aa47a4a8e9a180"
- integrity sha512-fszkg1iJJjq68I4lI8ZsmBiaoQiQHbxf1lNq+72EmC/mZOsFF5zn3k1yv9QGoFgIXzgsdSKtYymLJsrJPoamjQ==
-
-qrcode@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
- integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
- dependencies:
- dijkstrajs "^1.0.1"
- encode-utf8 "^1.0.3"
- pngjs "^5.0.0"
- yargs "^15.3.1"
-
-queue-microtask@^1.2.2:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
- integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-
-quick-lru@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
- integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
-
-read-cache@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
- integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
- dependencies:
- pify "^2.3.0"
-
-readdirp@~3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
- integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
- dependencies:
- picomatch "^2.2.1"
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-require-main-filename@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
- integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
-resolve@^1.1.7, resolve@^1.22.1:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
- dependencies:
- is-core-module "^2.9.0"
- path-parse "^1.0.7"
- supports-preserve-symlinks-flag "^1.0.0"
-
-reusify@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
- integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-run-parallel@^1.1.9:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
- integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
- dependencies:
- queue-microtask "^1.2.2"
-
-set-blocking@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
- integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
-
-source-map-js@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
- integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-supports-preserve-symlinks-flag@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
- integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
-
-tailwindcss@^3.1.6:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.6.tgz#bcb719357776c39e6376a8d84e9834b2b19a49f1"
- integrity sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==
- dependencies:
- arg "^5.0.2"
- chokidar "^3.5.3"
- color-name "^1.1.4"
- detective "^5.2.1"
- didyoumean "^1.2.2"
- dlv "^1.1.3"
- fast-glob "^3.2.11"
- glob-parent "^6.0.2"
- is-glob "^4.0.3"
- lilconfig "^2.0.5"
- normalize-path "^3.0.0"
- object-hash "^3.0.0"
- picocolors "^1.0.0"
- postcss "^8.4.14"
- postcss-import "^14.1.0"
- postcss-js "^4.0.0"
- postcss-load-config "^3.1.4"
- postcss-nested "5.0.6"
- postcss-selector-parser "^6.0.10"
- postcss-value-parser "^4.2.0"
- quick-lru "^5.1.1"
- resolve "^1.22.1"
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-typescript@>=3.0.1, typescript@^4.7.4:
- version "4.7.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
- integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
-
-util-deprecate@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-which-module@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
- integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
-
-wrap-ansi@^6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
- integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-xtend@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
- integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
-xterm-addon-fit@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
- integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
-
-xterm-addon-web-links@^0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.8.0.tgz#2cb1d57129271022569208578b0bf4774e7e6ea9"
- integrity sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==
-
-xterm@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0.tgz#3e160d60e6801c864b55adf19171c49d2ff2b4fc"
- integrity sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==
-
-y18n@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
- integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yaml@^1.10.2:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
- integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-
-yargs-parser@^18.1.2:
- version "18.1.3"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
- integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
- dependencies:
- camelcase "^5.0.0"
- decamelize "^1.2.0"
-
-yargs-parser@^21.0.0:
- version "21.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
- integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-
-yargs@^15.3.1:
- version "15.4.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
- integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
- dependencies:
- cliui "^6.0.0"
- decamelize "^1.2.0"
- find-up "^4.1.0"
- get-caller-file "^2.0.1"
- require-directory "^2.1.1"
- require-main-filename "^2.0.0"
- set-blocking "^2.0.0"
- string-width "^4.2.0"
- which-module "^2.0.0"
- y18n "^4.0.0"
- yargs-parser "^18.1.2"
-
-yargs@^17.2.1:
- version "17.5.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
- integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.3"
- y18n "^5.0.5"
- yargs-parser "^21.0.0"
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@types/golang-wasm-exec@^1.15.0":
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.0.tgz#d0aafbb2b0dc07eaf45dfb83bfb6cdd5b2b3c55c"
+ integrity sha512-FrL97mp7WW8LqNinVkzTVKOIQKuYjQqgucnh41+1vRQ+bf1LT8uh++KRf9otZPXsa6H1p8ruIGz1BmCGttOL6Q==
+
+"@types/node@*":
+ version "18.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
+ integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
+
+"@types/qrcode@^1.4.2":
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74"
+ integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==
+ dependencies:
+ "@types/node" "*"
+
+acorn-node@^1.8.2:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+ integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+ dependencies:
+ acorn "^7.0.0"
+ acorn-walk "^7.0.0"
+ xtend "^4.0.2"
+
+acorn-walk@^7.0.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+ integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+
+acorn@^7.0.0:
+ version "7.4.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+ integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+arg@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+braces@^3.0.2, braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+camelcase-css@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+camelcase@^5.0.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+chokidar@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
+
+cliui@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+ integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@^1.1.4, color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+ integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
+
+detective@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
+ integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
+ dependencies:
+ acorn-node "^1.8.2"
+ defined "^1.0.0"
+ minimist "^1.2.6"
+
+didyoumean@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
+ integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
+dijkstrajs@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
+ integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
+
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+dts-bundle-generator@^6.12.0:
+ version "6.12.0"
+ resolved "https://registry.yarnpkg.com/dts-bundle-generator/-/dts-bundle-generator-6.12.0.tgz#0a221bdce5fdd309a56c8556e645f16ed87ab07d"
+ integrity sha512-k/QAvuVaLIdyWRUHduDrWBe4j8PcE6TDt06+f32KHbW7/SmUPbX1O23fFtQgKwUyTBkbIjJFOFtNrF97tJcKug==
+ dependencies:
+ typescript ">=3.0.1"
+ yargs "^17.2.1"
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+encode-utf8@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
+ integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+fast-glob@^3.2.11:
+ version "3.2.11"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
+ integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+ integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+ dependencies:
+ reusify "^1.0.4"
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+ integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
+ dependencies:
+ has "^1.0.3"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+lilconfig@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
+ integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+minimist@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+nanoid@^3.3.4:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+ integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-hash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
+ integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pngjs@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
+ integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
+
+postcss-import@^14.1.0:
+ version "14.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
+ integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
+ dependencies:
+ postcss-value-parser "^4.0.0"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-js@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
+ integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
+ dependencies:
+ camelcase-css "^2.0.1"
+
+postcss-load-config@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
+ integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
+ dependencies:
+ lilconfig "^2.0.5"
+ yaml "^1.10.2"
+
+postcss-nested@5.0.6:
+ version "5.0.6"
+ resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
+ integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
+ dependencies:
+ postcss-selector-parser "^6.0.6"
+
+postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
+ version "6.0.10"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+ integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.4.14:
+ version "8.4.14"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
+ integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
+ dependencies:
+ nanoid "^3.3.4"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
+preact@^10.10.0:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-10.10.0.tgz#7434750a24b59dae1957d95dc0aa47a4a8e9a180"
+ integrity sha512-fszkg1iJJjq68I4lI8ZsmBiaoQiQHbxf1lNq+72EmC/mZOsFF5zn3k1yv9QGoFgIXzgsdSKtYymLJsrJPoamjQ==
+
+qrcode@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
+ integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
+ dependencies:
+ dijkstrajs "^1.0.1"
+ encode-utf8 "^1.0.3"
+ pngjs "^5.0.0"
+ yargs "^15.3.1"
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-lru@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+ integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
+ integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+ dependencies:
+ pify "^2.3.0"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+resolve@^1.1.7, resolve@^1.22.1:
+ version "1.22.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+ integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+ dependencies:
+ is-core-module "^2.9.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+source-map-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tailwindcss@^3.1.6:
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.6.tgz#bcb719357776c39e6376a8d84e9834b2b19a49f1"
+ integrity sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==
+ dependencies:
+ arg "^5.0.2"
+ chokidar "^3.5.3"
+ color-name "^1.1.4"
+ detective "^5.2.1"
+ didyoumean "^1.2.2"
+ dlv "^1.1.3"
+ fast-glob "^3.2.11"
+ glob-parent "^6.0.2"
+ is-glob "^4.0.3"
+ lilconfig "^2.0.5"
+ normalize-path "^3.0.0"
+ object-hash "^3.0.0"
+ picocolors "^1.0.0"
+ postcss "^8.4.14"
+ postcss-import "^14.1.0"
+ postcss-js "^4.0.0"
+ postcss-load-config "^3.1.4"
+ postcss-nested "5.0.6"
+ postcss-selector-parser "^6.0.10"
+ postcss-value-parser "^4.2.0"
+ quick-lru "^5.1.1"
+ resolve "^1.22.1"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+typescript@>=3.0.1, typescript@^4.7.4:
+ version "4.7.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+ integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
+
+util-deprecate@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+xtend@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+xterm-addon-fit@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
+ integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
+
+xterm-addon-web-links@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.8.0.tgz#2cb1d57129271022569208578b0bf4774e7e6ea9"
+ integrity sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==
+
+xterm@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0.tgz#3e160d60e6801c864b55adf19171c49d2ff2b4fc"
+ integrity sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==
+
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yaml@^1.10.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+ integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yargs-parser@^18.1.2:
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs-parser@^21.0.0:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs@^15.3.1:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+ dependencies:
+ cliui "^6.0.0"
+ decamelize "^1.2.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^4.2.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.2"
+
+yargs@^17.2.1:
+ version "17.5.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
+ integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.3"
+ y18n "^5.0.5"
+ yargs-parser "^21.0.0"
diff --git a/cmd/tsshd/tsshd.go b/cmd/tsshd/tsshd.go
index 950eb661c..1ec09a0d4 100644
--- a/cmd/tsshd/tsshd.go
+++ b/cmd/tsshd/tsshd.go
@@ -1,12 +1,12 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build ignore
-
-// The tsshd binary was an experimental SSH server that accepts connections
-// from anybody on the same Tailscale network.
-//
-// Its functionality moved into tailscaled.
-//
-// See https://github.com/tailscale/tailscale/issues/3802
-package main
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ignore
+
+// The tsshd binary was an experimental SSH server that accepts connections
+// from anybody on the same Tailscale network.
+//
+// Its functionality moved into tailscaled.
+//
+// See https://github.com/tailscale/tailscale/issues/3802
+package main