diff options
Diffstat (limited to 'cmd')
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 - -[](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
+
+[](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
|
