Compare commits
122 Commits
v1.26.2
...
Xe/gitops-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2716667c9d | ||
|
|
affa25097c | ||
|
|
227c6b2a53 | ||
|
|
90ccba6730 | ||
|
|
f7a36dfeb1 | ||
|
|
9514ed33d2 | ||
|
|
1d33157ab9 | ||
|
|
3e06b9ea7a | ||
|
|
480fd6c797 | ||
|
|
41e60dae80 | ||
|
|
43f3a969ca | ||
|
|
d8cb5aae17 | ||
|
|
b763a12331 | ||
|
|
de2dcda2e0 | ||
|
|
b7f1fe7b0d | ||
|
|
9bd3b5b89c | ||
|
|
cfdb862673 | ||
|
|
6f5096fa61 | ||
|
|
2a22ea3e83 | ||
|
|
4d0461f721 | ||
|
|
393a229de9 | ||
|
|
165c8f898e | ||
|
|
2491fe1afe | ||
|
|
c1cb3efbba | ||
|
|
4c0feba38e | ||
|
|
3c892d106c | ||
|
|
bd4b27753e | ||
|
|
469c30c33b | ||
|
|
c6648db333 | ||
|
|
9fcda1f0a0 | ||
|
|
0d52674a84 | ||
|
|
931f18b575 | ||
|
|
4f1374ec9e | ||
|
|
af412e8874 | ||
|
|
004f0ca3e0 | ||
|
|
16c85d0dc5 | ||
|
|
7fb6781bda | ||
|
|
ec4f849079 | ||
|
|
505ca2750d | ||
|
|
96afd1db46 | ||
|
|
755396d6fe | ||
|
|
cca25f6107 | ||
|
|
e37167b3ef | ||
|
|
d6b8a18b09 | ||
|
|
5bb44a4a5c | ||
|
|
c7993d2b88 | ||
|
|
3709074e55 | ||
|
|
1cfd96cdc2 | ||
|
|
e6572a0f08 | ||
|
|
fe3426b4c7 | ||
|
|
d6817d0f22 | ||
|
|
c93fd0d22b | ||
|
|
9584d8aa7d | ||
|
|
ea6e9099b9 | ||
|
|
72b7edbba9 | ||
|
|
6b71568eb7 | ||
|
|
ec649e707f | ||
|
|
3f4fd64311 | ||
|
|
aa37aece9c | ||
|
|
88c2afd1e3 | ||
|
|
6f58497647 | ||
|
|
88133c361e | ||
|
|
cfa484e1a2 | ||
|
|
ef351ac0dd | ||
|
|
06aa141632 | ||
|
|
3b1f99ded1 | ||
|
|
9280d39678 | ||
|
|
d7f452c0a1 | ||
|
|
09eaba91ad | ||
|
|
b5553c6ad2 | ||
|
|
de4c635e54 | ||
|
|
3ac8ab1791 | ||
|
|
bef6e2831a | ||
|
|
40503ef07a | ||
|
|
412c4c55e2 | ||
|
|
c434e47f2d | ||
|
|
a7d2024e35 | ||
|
|
1d04e01d1e | ||
|
|
9294a14a37 | ||
|
|
7f807fef6c | ||
|
|
35782f891d | ||
|
|
7b9a901489 | ||
|
|
c88bd53b1b | ||
|
|
2d65c1a950 | ||
|
|
4baf34cf25 | ||
|
|
8cdfd12977 | ||
|
|
76256d22d8 | ||
|
|
8c5c87be26 | ||
|
|
4f6fa3d63a | ||
|
|
dee95d0894 | ||
|
|
1007983159 | ||
|
|
51cc0e503b | ||
|
|
87a4c75fd4 | ||
|
|
ef0d740270 | ||
|
|
70a2797064 | ||
|
|
a1e429f7c3 | ||
|
|
526b0b6890 | ||
|
|
467eb2eca0 | ||
|
|
99ed54926b | ||
|
|
13d0b8e6a4 | ||
|
|
59ed846277 | ||
|
|
757ecf7e80 | ||
|
|
22c544bca7 | ||
|
|
f31588786f | ||
|
|
36ea837736 | ||
|
|
4005134263 | ||
|
|
0f05b2c13f | ||
|
|
1392a93445 | ||
|
|
7fffddce8e | ||
|
|
2990c2b1cf | ||
|
|
32c6823cf5 | ||
|
|
b788a5ba7e | ||
|
|
d8c05fc1b2 | ||
|
|
d3643fa151 | ||
|
|
96f73b3894 | ||
|
|
3a182d5dd6 | ||
|
|
6246fa32f0 | ||
|
|
c41837842b | ||
|
|
bb67999808 | ||
|
|
09363064b5 | ||
|
|
edc90ebc61 | ||
|
|
cfb5bd0559 |
4
.github/workflows/cross-wasm.yml
vendored
4
.github/workflows/cross-wasm.yml
vendored
@@ -25,11 +25,11 @@ jobs:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Wasm build CLI and client modules
|
||||
- name: Wasm client build
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
run: go build ./cmd/tailscale/cli ./ipn/... ./net/... ./safesocket ./types/... ./wgengine/...
|
||||
run: go build ./cmd/tsconnect/wasm
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
11
.github/workflows/windows-race.yml
vendored
11
.github/workflows/windows-race.yml
vendored
@@ -43,6 +43,17 @@ jobs:
|
||||
# TODO(raggi): add a go version here.
|
||||
key: ${{ runner.os }}-go-2-race-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Print toolchain details
|
||||
run: gcc -v
|
||||
|
||||
# There is currently an issue in the race detector in Go on Windows when
|
||||
# used with a newer version of GCC.
|
||||
# See https://github.com/tailscale/tailscale/issues/4926.
|
||||
- name: Downgrade MinGW
|
||||
shell: bash
|
||||
run: |
|
||||
choco install mingw --version 10.2.0 --allow-downgrade
|
||||
|
||||
- name: Test with -race flag
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
|
||||
1
ALPINE.txt
Normal file
1
ALPINE.txt
Normal file
@@ -0,0 +1 @@
|
||||
3.16
|
||||
17
Dockerfile
17
Dockerfile
@@ -39,6 +39,18 @@ WORKDIR /go/src/tailscale
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Pre-build some stuff before the following COPY line invalidates the Docker cache.
|
||||
RUN go install \
|
||||
github.com/aws/aws-sdk-go-v2/aws \
|
||||
github.com/aws/aws-sdk-go-v2/config \
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet \
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack \
|
||||
golang.org/x/crypto/ssh \
|
||||
golang.org/x/crypto/acme \
|
||||
nhooyr.io/websocket \
|
||||
github.com/mdlayher/netlink \
|
||||
golang.zx2c4.com/wireguard/device
|
||||
|
||||
COPY . .
|
||||
|
||||
# see build_docker.sh
|
||||
@@ -56,5 +68,8 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled
|
||||
|
||||
FROM ghcr.io/tailscale/alpine-base:3.14
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
COPY --from=build-env /go/src/tailscale/docs/k8s/run.sh /usr/local/bin/
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
FROM alpine:3.15
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.25.0
|
||||
1.29.0
|
||||
|
||||
2
api.md
2
api.md
@@ -1120,7 +1120,7 @@ Replaces the list of searchpaths with the list supplied by the user and returns
|
||||
`searchPaths` - A list of searchpaths in JSON.
|
||||
```
|
||||
{
|
||||
"searchPaths: ["user1.example.com", "user2.example.com"]
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export PATH=$PWD/tool:$PATH
|
||||
eval $(./build_dist.sh shellvars)
|
||||
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
|
||||
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
|
||||
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
|
||||
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.16"
|
||||
|
||||
PUSH="${PUSH:-false}"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
|
||||
@@ -17,18 +17,27 @@ import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ACLRow defines a rule that grants access by a set of users or groups to a set of servers and ports.
|
||||
// ACLRow defines a rule that grants access by a set of users or groups to a set
|
||||
// of servers and ports.
|
||||
// Only one of Src/Dst or Users/Ports may be specified.
|
||||
type ACLRow struct {
|
||||
Action string `json:"action,omitempty"` // valid values: "accept"
|
||||
Users []string `json:"users,omitempty"`
|
||||
Ports []string `json:"ports,omitempty"`
|
||||
Users []string `json:"users,omitempty"` // old name for src
|
||||
Ports []string `json:"ports,omitempty"` // old name for dst
|
||||
Src []string `json:"src,omitempty"`
|
||||
Dst []string `json:"dst,omitempty"`
|
||||
}
|
||||
|
||||
// ACLTest defines a test for your ACLs to prevent accidental exposure or revoking of access to key servers and ports.
|
||||
// ACLTest defines a test for your ACLs to prevent accidental exposure or
|
||||
// revoking of access to key servers and ports. Only one of Src or User may be
|
||||
// specified, and only one of Allow/Accept may be specified.
|
||||
type ACLTest struct {
|
||||
User string `json:"user,omitempty"` // source
|
||||
Allow []string `json:"allow,omitempty"` // expected destination ip:port that user can access
|
||||
Deny []string `json:"deny,omitempty"` // expected destination ip:port that user cannot access
|
||||
Src string `json:"src,omitempty"` // source
|
||||
User string `json:"user,omitempty"` // old name for source
|
||||
Accept []string `json:"accept,omitempty"` // expected destination ip:port that user can access
|
||||
Deny []string `json:"deny,omitempty"` // expected destination ip:port that user cannot access
|
||||
|
||||
Allow []string `json:"allow,omitempty"` // old name for accept
|
||||
}
|
||||
|
||||
// ACLDetails contains all the details for an ACL.
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
@@ -45,8 +44,8 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
counterWebSocketAccepts.Add(1)
|
||||
wc := wsconn.New(c)
|
||||
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(wc, brw, r.RemoteAddr)
|
||||
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
|
||||
})
|
||||
}
|
||||
|
||||
1
cmd/gitops-pusher/.gitignore
vendored
Normal file
1
cmd/gitops-pusher/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
version-cache.json
|
||||
48
cmd/gitops-pusher/README.md
Normal file
48
cmd/gitops-pusher/README.md
Normal file
@@ -0,0 +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@v3
|
||||
|
||||
- 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.
|
||||
67
cmd/gitops-pusher/cache.go
Normal file
67
cmd/gitops-pusher/cache.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
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]
|
||||
}
|
||||
373
cmd/gitops-pusher/gitops-pusher.go
Normal file
373
cmd/gitops-pusher/gitops-pusher.go
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Command gitops-pusher allows users to use a GitOps flow for managing Tailscale ACLs.
|
||||
//
|
||||
// See README.md for more details.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
var (
|
||||
rootFlagSet = flag.NewFlagSet("gitops-pusher", flag.ExitOnError)
|
||||
policyFname = rootFlagSet.String("policy-file", "./policy.hujson", "filename for policy file")
|
||||
cacheFname = rootFlagSet.String("cache-file", "./version-cache.json", "filename for the previous known version hash")
|
||||
timeout = rootFlagSet.Duration("timeout", 5*time.Minute, "timeout for the entire CI run")
|
||||
githubSyntax = rootFlagSet.Bool("github-syntax", true, "use GitHub Action error syntax (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message)")
|
||||
|
||||
modifiedExternallyFailure = make(chan struct{}, 1)
|
||||
)
|
||||
|
||||
func modifiedExternallyError() {
|
||||
if *githubSyntax {
|
||||
fmt.Printf("::error file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
|
||||
} else {
|
||||
fmt.Printf("The policy file was modified externally in the admin console.\n")
|
||||
}
|
||||
modifiedExternallyFailure <- struct{}{}
|
||||
}
|
||||
|
||||
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localEtag, err := sumFile(*policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cache.PrevETag == "" {
|
||||
log.Println("no previous etag found, assuming local file is correct and recording that")
|
||||
cache.PrevETag = localEtag
|
||||
}
|
||||
|
||||
log.Printf("control: %s", controlEtag)
|
||||
log.Printf("local: %s", localEtag)
|
||||
log.Printf("cache: %s", cache.PrevETag)
|
||||
|
||||
if cache.PrevETag != controlEtag {
|
||||
modifiedExternallyError()
|
||||
}
|
||||
|
||||
if controlEtag == localEtag {
|
||||
cache.PrevETag = localEtag
|
||||
log.Println("no update needed, doing nothing")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.PrevETag = localEtag
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localEtag, err := sumFile(*policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cache.PrevETag == "" {
|
||||
log.Println("no previous etag found, assuming local file is correct and recording that")
|
||||
cache.PrevETag = localEtag
|
||||
}
|
||||
|
||||
log.Printf("control: %s", controlEtag)
|
||||
log.Printf("local: %s", localEtag)
|
||||
log.Printf("cache: %s", cache.PrevETag)
|
||||
|
||||
if cache.PrevETag != controlEtag {
|
||||
modifiedExternallyError()
|
||||
}
|
||||
|
||||
if controlEtag == localEtag {
|
||||
log.Println("no updates found, doing nothing")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localEtag, err := sumFile(*policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cache.PrevETag == "" {
|
||||
log.Println("no previous etag found, assuming local file is correct and recording that")
|
||||
cache.PrevETag = Shuck(localEtag)
|
||||
}
|
||||
|
||||
log.Printf("control: %s", controlEtag)
|
||||
log.Printf("local: %s", localEtag)
|
||||
log.Printf("cache: %s", cache.PrevETag)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
tailnet, ok := os.LookupEnv("TS_TAILNET")
|
||||
if !ok {
|
||||
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
|
||||
}
|
||||
apiKey, ok := os.LookupEnv("TS_API_KEY")
|
||||
if !ok {
|
||||
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
|
||||
}
|
||||
cache, err := LoadCache(*cacheFname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
cache = &Cache{}
|
||||
} else {
|
||||
log.Fatalf("error loading cache: %v", err)
|
||||
}
|
||||
}
|
||||
defer cache.Save(*cacheFname)
|
||||
|
||||
applyCmd := &ffcli.Command{
|
||||
Name: "apply",
|
||||
ShortUsage: "gitops-pusher [options] apply",
|
||||
ShortHelp: "Pushes changes to CONTROL",
|
||||
LongHelp: `Pushes changes to CONTROL`,
|
||||
Exec: apply(cache, tailnet, apiKey),
|
||||
}
|
||||
|
||||
testCmd := &ffcli.Command{
|
||||
Name: "test",
|
||||
ShortUsage: "gitops-pusher [options] test",
|
||||
ShortHelp: "Tests ACL changes",
|
||||
LongHelp: "Tests ACL changes",
|
||||
Exec: test(cache, tailnet, apiKey),
|
||||
}
|
||||
|
||||
cksumCmd := &ffcli.Command{
|
||||
Name: "checksum",
|
||||
ShortUsage: "Shows checksums of ACL files",
|
||||
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
Exec: getChecksums(cache, tailnet, apiKey),
|
||||
}
|
||||
|
||||
root := &ffcli.Command{
|
||||
ShortUsage: "gitops-pusher [options] <command>",
|
||||
ShortHelp: "Push Tailscale ACLs to CONTROL using a GitOps workflow",
|
||||
Subcommands: []*ffcli.Command{applyCmd, cksumCmd, testCmd},
|
||||
FlagSet: rootFlagSet,
|
||||
}
|
||||
|
||||
if err := root.Parse(os.Args[1:]); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
|
||||
if err := root.Run(ctx); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(modifiedExternallyFailure) != 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func sumFile(fname string) (string, error) {
|
||||
data, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
formatted, err := hujson.Format(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
_, err = h.Write(formatted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), fin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
req.Header.Set("If-Match", `"`+oldEtag+`"`)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
got := resp.StatusCode
|
||||
want := http.StatusOK
|
||||
if got != want {
|
||||
var ate ACLTestError
|
||||
err := json.NewDecoder(resp.Body).Decode(&ate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), fin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
got := resp.StatusCode
|
||||
want := http.StatusOK
|
||||
if got != want {
|
||||
return fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
|
||||
}
|
||||
|
||||
var ate ACLTestError
|
||||
err = json.NewDecoder(resp.Body).Decode(&ate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ate.Message) != 0 || len(ate.Data) != 0 {
|
||||
return ate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)
|
||||
|
||||
type ACLTestError struct {
|
||||
Message string `json:"message"`
|
||||
Data []ACLTestErrorDetail `json:"data"`
|
||||
}
|
||||
|
||||
func (ate ACLTestError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
if *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {
|
||||
sp := lineColMessageSplit.FindStringSubmatch(ate.Message)
|
||||
|
||||
line := sp[1]
|
||||
col := sp[2]
|
||||
msg := sp[3]
|
||||
|
||||
fmt.Fprintf(&sb, "::error file=%s,line=%s,col=%s::%s", *policyFname, line, col, msg)
|
||||
} else {
|
||||
fmt.Fprintln(&sb, ate.Message)
|
||||
}
|
||||
fmt.Fprintln(&sb)
|
||||
|
||||
for _, data := range ate.Data {
|
||||
fmt.Fprintf(&sb, "For user %s:\n", data.User)
|
||||
for _, err := range data.Errors {
|
||||
fmt.Fprintf(&sb, "- %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type ACLTestErrorDetail struct {
|
||||
User string `json:"user"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Accept", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
got := resp.StatusCode
|
||||
want := http.StatusOK
|
||||
if got != want {
|
||||
return "", fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
|
||||
}
|
||||
|
||||
return Shuck(resp.Header.Get("ETag")), nil
|
||||
}
|
||||
@@ -19,10 +19,15 @@ import (
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func main() {
|
||||
if *useHTTPS {
|
||||
ln, err = ts.Listen("tcp", ":443")
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
GetCertificate: localClient.GetCertificate,
|
||||
})
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -46,7 +45,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
if certArgs.serve {
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
GetCertificate: localClient.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
|
||||
@@ -90,7 +89,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
certArgs.certFile = domain + ".crt"
|
||||
certArgs.keyFile = domain + ".key"
|
||||
}
|
||||
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
|
||||
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ var localClient tailscale.LocalClient
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) (err error) {
|
||||
args = CleanUpArgs(args)
|
||||
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
@@ -493,14 +493,16 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
|
||||
upEnv := upCheckEnv{
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
user: tt.curUser,
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, upEnv)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upEnv); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
@@ -784,6 +786,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
|
||||
// updatePrefs might've mutated them (from applyImplicitPrefs).
|
||||
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
|
||||
|
||||
wantSimpleUp bool
|
||||
wantJustEditMP *ipn.MaskedPrefs
|
||||
wantErrSubtr string
|
||||
@@ -885,6 +891,28 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
// Issue 3808: explicitly empty --operator= should clear value.
|
||||
name: "explicit_empty_operator",
|
||||
flags: []string{"--operator="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "somebody",
|
||||
},
|
||||
env: upCheckEnv{user: "somebody", backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
OperatorUserSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, prefs *ipn.Prefs) {
|
||||
if prefs.OperatorUser != "" {
|
||||
t.Errorf("operator sent to backend should be empty; got %q", prefs.OperatorUser)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -909,6 +937,9 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.checkUpdatePrefsMutations != nil {
|
||||
tt.checkUpdatePrefsMutations(t, newPrefs)
|
||||
}
|
||||
if simpleUp != tt.wantSimpleUp {
|
||||
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -23,11 +25,14 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -118,6 +123,17 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
{
|
||||
Name: "ts2021",
|
||||
Exec: runTS2021,
|
||||
ShortHelp: "debug ts2021 protocol connectivity",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ts2021")
|
||||
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
|
||||
fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -425,3 +441,67 @@ func runVia(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ts2021Args struct {
|
||||
host string // "controlplane.tailscale.com"
|
||||
version int // 27 or whatever
|
||||
}
|
||||
|
||||
func runTS2021(ctx context.Context, args []string) error {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
machinePrivate := key.NewMachine()
|
||||
var dialer net.Dialer
|
||||
|
||||
var keys struct {
|
||||
PublicKey key.MachinePublic
|
||||
}
|
||||
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
|
||||
log.Printf("Fetching keys from %s ...", keysURL)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Do: %v", err)
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
log.Printf("Status: %v", res.Status)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&keys); err != nil {
|
||||
log.Printf("JSON: %v", err)
|
||||
return fmt.Errorf("decoding /keys JSON: %w", err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
log.Printf("Dial(%q, %q) ...", network, address)
|
||||
c, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
log.Printf("Dial(%q, %q) = %v", network, address, err)
|
||||
} else {
|
||||
log.Printf("Dial(%q, %q) = %v / %v", network, address, c.LocalAddr(), c.RemoteAddr())
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("did noise handshake")
|
||||
|
||||
gotPeer := conn.Peer()
|
||||
if gotPeer != keys.PublicKey {
|
||||
log.Printf("peer = %v, want %v", gotPeer, keys.PublicKey)
|
||||
return errors.New("key mismatch")
|
||||
}
|
||||
|
||||
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,8 +126,10 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
printf("\t* IPv6: (no addr found)\n")
|
||||
} else if report.OSHasIPv6 {
|
||||
printf("\t* IPv6: no, but OS has support\n")
|
||||
} else {
|
||||
printf("\t* IPv6: no\n")
|
||||
printf("\t* IPv6: no, unavailable in OS\n")
|
||||
}
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
|
||||
@@ -121,6 +121,12 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
printf("ping %q timed out\n", ip)
|
||||
if n == pingArgs.num {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -63,7 +62,7 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
hostForSSH = v
|
||||
}
|
||||
|
||||
ssh, err := exec.LookPath("ssh")
|
||||
ssh, err := findSSH()
|
||||
if err != nil {
|
||||
// TODO(bradfitz): use Go's crypto/ssh client instead
|
||||
// of failing. But for now:
|
||||
|
||||
@@ -10,9 +10,14 @@ 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
|
||||
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func findSSH() (string, error) {
|
||||
return "", errors.New("Not implemented")
|
||||
}
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
return errors.New("Not implemented")
|
||||
}
|
||||
|
||||
@@ -8,8 +8,21 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func findSSH() (string, error) {
|
||||
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
|
||||
// occured 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:]...)
|
||||
|
||||
@@ -362,7 +362,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
// without changing any settings.
|
||||
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
|
||||
if !env.upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, env.user)
|
||||
applyImplicitPrefs(prefs, curPrefs, env)
|
||||
|
||||
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
|
||||
return false, nil, err
|
||||
@@ -404,7 +404,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return simpleUp, justEditMP, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
@@ -481,6 +481,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
checkSSHUpWarnings(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
@@ -676,6 +682,28 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func checkSSHUpWarnings(ctx context.Context) {
|
||||
if !upArgs.runSSH {
|
||||
return
|
||||
}
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
// Ignore. Don't spam more.
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 0 {
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") {
|
||||
printf("%s\n", st.Health[0])
|
||||
return
|
||||
}
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
printf(" - %s\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
||||
data, err := json.MarshalIndent(js, "", " ")
|
||||
@@ -853,13 +881,20 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
|
||||
// this is just the operator user, which only needs to be set if it doesn't
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences for the user operator.
|
||||
// If the operator flag is passed no action is taken, otherwise this only needs to be set if it doesn't
|
||||
// match the current user.
|
||||
//
|
||||
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
|
||||
explicitOperator := false
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "operator" {
|
||||
explicitOperator = true
|
||||
}
|
||||
})
|
||||
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == env.user && !explicitOperator {
|
||||
prefs.OperatorUser = oldPrefs.OperatorUser
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +208,20 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||
return "", nil, err
|
||||
}
|
||||
token, err := r.Cookie("qtoken")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
if err == nil {
|
||||
return qnapAuthnQtoken(r, user.Value, token.Value)
|
||||
}
|
||||
sid, err := r.Cookie("NAS_SID")
|
||||
if err == nil {
|
||||
return qnapAuthnSid(r, user.Value, sid.Value)
|
||||
}
|
||||
return "", nil, fmt.Errorf("not authenticated by any mechanism")
|
||||
}
|
||||
|
||||
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"qtoken": []string{token.Value},
|
||||
"user": []string{user.Value},
|
||||
"qtoken": []string{token},
|
||||
"user": []string{user},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
@@ -221,7 +229,26 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
resp, err := http.Get(u.String())
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
}
|
||||
|
||||
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"sid": []string{sid},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.URL.Host,
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
}
|
||||
|
||||
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -237,7 +264,7 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||
if authResp.AuthPassed == 0 {
|
||||
return "", nil, fmt.Errorf("not authenticated")
|
||||
}
|
||||
return user.Value, authResp, nil
|
||||
return user, authResp, nil
|
||||
}
|
||||
|
||||
func synoAuthn() (string, error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
@@ -31,14 +31,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
@@ -48,21 +50,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
L tailscale.com/net/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
@@ -83,22 +85,25 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
@@ -111,7 +116,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
@@ -198,7 +202,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight+
|
||||
runtime/debug from tailscale.com/util/singleflight+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
args = cli.CleanUpArgs(args)
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2
|
||||
💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
@@ -143,7 +144,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
@@ -152,6 +152,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/multicast from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
@@ -231,7 +232,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/net/dns+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
@@ -260,6 +260,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
@@ -272,6 +273,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
|
||||
@@ -317,7 +319,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sync/singleflight from tailscale.com/control/controlclient+
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
@@ -406,7 +407,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight+
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
runtime/trace from net/http/pprof
|
||||
sort from compress/flate+
|
||||
|
||||
@@ -161,7 +161,7 @@ func main() {
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
// Windows subprocess is spawned with /subprocess, so we need to avoid this check there.
|
||||
if runtime.GOOS != "windows" || flag.Arg(0) != "/subproc" {
|
||||
if runtime.GOOS != "windows" || (flag.Arg(0) != "/subproc" && flag.Arg(0) != "/firewall") {
|
||||
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
||||
}
|
||||
}
|
||||
@@ -404,7 +404,6 @@ func run() error {
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
defer dialer.Close()
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
@@ -437,6 +436,7 @@ func run() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
defer dialer.Close()
|
||||
|
||||
err = srv.Run(ctx, ln)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
@@ -515,7 +515,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
} else {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
tstun.Diagnose(logf, name, err)
|
||||
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
}
|
||||
conf.Tun = dev
|
||||
|
||||
4
cmd/tsconnect/.gitignore
vendored
Normal file
4
cmd/tsconnect/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
src/wasm_exec.js
|
||||
src/main.wasm
|
||||
node_modules/
|
||||
dist/
|
||||
190
cmd/tsconnect/build.go
Normal file
190
cmd/tsconnect/build.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func runBuild() {
|
||||
buildOptions, err := commonSetup(prodMode)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot setup: %v", err)
|
||||
}
|
||||
|
||||
if err := cleanDist(); err != nil {
|
||||
log.Fatalf("Cannot clean %s: %v", *distDir, err)
|
||||
}
|
||||
|
||||
buildOptions.Write = true
|
||||
buildOptions.MinifyWhitespace = true
|
||||
buildOptions.MinifyIdentifiers = true
|
||||
buildOptions.MinifySyntax = true
|
||||
|
||||
buildOptions.EntryNames = "[dir]/[name]-[hash]"
|
||||
buildOptions.AssetNames = "[name]-[hash]"
|
||||
buildOptions.Metafile = true
|
||||
|
||||
log.Printf("Running esbuild...\n")
|
||||
result := esbuild.Build(*buildOptions)
|
||||
if len(result.Errors) > 0 {
|
||||
log.Printf("ESBuild Error:\n")
|
||||
for _, e := range result.Errors {
|
||||
log.Printf("%v", e)
|
||||
}
|
||||
log.Fatal("Build failed")
|
||||
}
|
||||
if len(result.Warnings) > 0 {
|
||||
log.Printf("ESBuild Warnings:\n")
|
||||
for _, w := range result.Warnings {
|
||||
log.Printf("%v", w)
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve build metadata so we can extract hashed file names for serving.
|
||||
metadataBytes, err := fixEsbuildMetadataPaths(result.Metafile)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
|
||||
log.Fatalf("Cannot write metadata: %v", err)
|
||||
}
|
||||
|
||||
if er := precompressDist(); err != nil {
|
||||
log.Fatalf("Cannot precompress resources: %v", er)
|
||||
}
|
||||
}
|
||||
|
||||
// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
|
||||
// relative to the dist directory (it normally uses paths relative to the cwd,
|
||||
// which are akward if we're running with a different cwd at serving time).
|
||||
func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
|
||||
var metadata EsbuildMetadata
|
||||
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse metadata: %w", err)
|
||||
}
|
||||
distAbsPath, err := filepath.Abs(*distDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot get absolute path from %s: %w", *distDir, err)
|
||||
}
|
||||
for outputPath, output := range metadata.Outputs {
|
||||
outputAbsPath, err := filepath.Abs(outputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot get absolute path from %s: %w", outputPath, err)
|
||||
}
|
||||
outputRelPath, err := filepath.Rel(distAbsPath, outputAbsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot get relative path from %s: %w", outputRelPath, err)
|
||||
}
|
||||
delete(metadata.Outputs, outputPath)
|
||||
metadata.Outputs[outputRelPath] = output
|
||||
}
|
||||
return json.Marshal(metadata)
|
||||
}
|
||||
|
||||
// cleanDist removes files from the dist build directory, except the placeholder
|
||||
// one that we keep to make sure Git still creates the directory.
|
||||
func cleanDist() error {
|
||||
log.Printf("Cleaning %s...\n", *distDir)
|
||||
files, err := os.ReadDir(*distDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(*distDir, 0755)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Name() != "placeholder" {
|
||||
if err := os.Remove(filepath.Join(*distDir, file.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func precompressDist() error {
|
||||
log.Printf("Pre-compressing files in %s/...\n", *distDir)
|
||||
var eg errgroup.Group
|
||||
err := fs.WalkDir(os.DirFS(*distDir), ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !compressibleExtensions[filepath.Ext(p)] {
|
||||
return nil
|
||||
}
|
||||
p = path.Join(*distDir, p)
|
||||
log.Printf("Pre-compressing %v\n", p)
|
||||
|
||||
eg.Go(func() error {
|
||||
return precompress(p)
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
var compressibleExtensions = map[string]bool{
|
||||
".js": true,
|
||||
".css": true,
|
||||
".wasm": true,
|
||||
}
|
||||
|
||||
func precompress(path string) error {
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
|
||||
return gzip.NewWriterLevel(w, gzip.BestCompression)
|
||||
}, path+".gz", fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
|
||||
return brotli.NewWriterLevel(w, brotli.BestCompression), nil
|
||||
}, path+".br", fi.Mode())
|
||||
}
|
||||
|
||||
func writeCompressed(contents []byte, compressedWriterCreator func(io.Writer) (io.WriteCloser, error), outputPath string, outputMode fs.FileMode) error {
|
||||
var buf bytes.Buffer
|
||||
compressedWriter, err := compressedWriterCreator(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := compressedWriter.Write(contents); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compressedWriter.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(outputPath, buf.Bytes(), outputMode)
|
||||
}
|
||||
114
cmd/tsconnect/common.go
Normal file
114
cmd/tsconnect/common.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
devMode = true
|
||||
prodMode = false
|
||||
)
|
||||
|
||||
// commonSetup performs setup that is common to both dev and build modes.
|
||||
func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
|
||||
// Change cwd to to where this file lives -- that's where all inputs for
|
||||
// esbuild and other build steps live.
|
||||
if _, filename, _, ok := runtime.Caller(0); ok {
|
||||
if err := os.Chdir(path.Dir(filename)); err != nil {
|
||||
return nil, fmt.Errorf("Cannot change cwd: %w", err)
|
||||
}
|
||||
}
|
||||
if err := buildDeps(dev); err != nil {
|
||||
return nil, fmt.Errorf("Cannot build deps: %w", err)
|
||||
}
|
||||
|
||||
return &esbuild.BuildOptions{
|
||||
EntryPoints: []string{"src/index.js", "src/index.css"},
|
||||
Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
|
||||
Outdir: *distDir,
|
||||
Bundle: true,
|
||||
Sourcemap: esbuild.SourceMapLinked,
|
||||
LogLevel: esbuild.LogLevelInfo,
|
||||
Define: map[string]string{"DEBUG": strconv.FormatBool(dev)},
|
||||
Target: esbuild.ES2017,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildDeps builds the static assets that are needed for the server (except for
|
||||
// JS/CSS bundling, which is handled by esbuild).
|
||||
func buildDeps(dev bool) error {
|
||||
if err := copyWasmExec(); err != nil {
|
||||
return fmt.Errorf("Cannot copy wasm_exec.js: %w", err)
|
||||
}
|
||||
if err := buildWasm(dev); err != nil {
|
||||
return fmt.Errorf("Cannot build main.wasm: %w", err)
|
||||
}
|
||||
if err := installJSDeps(); err != nil {
|
||||
return fmt.Errorf("Cannot install JS deps: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyWasmExec grabs the current wasm_exec.js runtime helper library from the
|
||||
// Go toolchain.
|
||||
func copyWasmExec() error {
|
||||
log.Printf("Copying wasm_exec.js...\n")
|
||||
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
|
||||
wasmExecDstPath := filepath.Join("src", "wasm_exec.js")
|
||||
contents, err := os.ReadFile(wasmExecSrcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(wasmExecDstPath, contents, 0600)
|
||||
}
|
||||
|
||||
// buildWasm builds the Tailscale wasm binary and places it where the JS can
|
||||
// load it.
|
||||
func buildWasm(dev bool) error {
|
||||
log.Printf("Building wasm...\n")
|
||||
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
|
||||
if !dev {
|
||||
// Omit long paths and debug symbols in release builds, to reduce the
|
||||
// generated WASM binary size.
|
||||
args = append(args, "-trimpath", "-ldflags", "-s -w")
|
||||
}
|
||||
args = append(args, "-o", "src/main.wasm", "./wasm")
|
||||
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
|
||||
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// installJSDeps installs the JavaScript dependencies specified by package.json
|
||||
func installJSDeps() error {
|
||||
log.Printf("Installing JS deps...\n")
|
||||
stdoutStderr, err := exec.Command("yarn").CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("yarn failed: %s", stdoutStderr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// EsbuildMetadata is the subset of metadata struct (described by
|
||||
// https://esbuild.github.io/api/#metafile) that we care about for mapping
|
||||
// from entry points to hashed file names.
|
||||
type EsbuildMetadata struct {
|
||||
Outputs map[string]struct {
|
||||
EntryPoint string `json:"entryPoint,omitempty"`
|
||||
} `json:"outputs,omitempty"`
|
||||
}
|
||||
38
cmd/tsconnect/dev.go
Normal file
38
cmd/tsconnect/dev.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
)
|
||||
|
||||
func runDev() {
|
||||
buildOptions, err := commonSetup(devMode)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot setup: %v", err)
|
||||
}
|
||||
host, portStr, err := net.SplitHostPort(*addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse addr: %v", err)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot parse port: %v", err)
|
||||
}
|
||||
result, err := esbuild.Serve(esbuild.ServeOptions{
|
||||
Port: uint16(port),
|
||||
Host: host,
|
||||
Servedir: "./",
|
||||
}, *buildOptions)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot start esbuild server: %v", err)
|
||||
}
|
||||
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
|
||||
result.Wait()
|
||||
}
|
||||
2
cmd/tsconnect/dist/placeholder
vendored
Normal file
2
cmd/tsconnect/dist/placeholder
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
This is here to make sure the dist/ directory exists for the go:embed command
|
||||
in serve.go.
|
||||
16
cmd/tsconnect/index.html
Normal file
16
cmd/tsconnect/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="dist/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1>Tailscale Connect</h1>
|
||||
<div id="state">Loading…</div>
|
||||
</div>
|
||||
<div id="peers"></div>
|
||||
<script src="dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
cmd/tsconnect/package.json
Normal file
12
cmd/tsconnect/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@tailscale/ssh",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"qrcode": "^1.5.0",
|
||||
"xterm": "^4.18.0"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 80
|
||||
}
|
||||
}
|
||||
148
cmd/tsconnect/serve.go
Normal file
148
cmd/tsconnect/serve.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
//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 := ioutil.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)
|
||||
for outputPath, output := range esbuildMetadata.Outputs {
|
||||
if output.EntryPoint != "" {
|
||||
entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
indexBytes := rawIndexBytes
|
||||
for entryPointPath, defaultDistPath := range entryPointsToDefaultDistPaths {
|
||||
hashedDistPath := entryPointsToHashedDistPaths[entryPointPath]
|
||||
if hashedDistPath != "" {
|
||||
indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
|
||||
}
|
||||
}
|
||||
|
||||
return indexBytes, nil
|
||||
}
|
||||
|
||||
var entryPointsToDefaultDistPaths = map[string]string{
|
||||
"src/index.css": "dist/index.css",
|
||||
"src/index.js": "dist/index.js",
|
||||
}
|
||||
|
||||
func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
|
||||
path := r.URL.Path
|
||||
var f fs.File
|
||||
// Prefer pre-compressed versions generated during the build step.
|
||||
if tsweb.AcceptsEncoding(r, "br") {
|
||||
if brotliFile, err := distFS.Open(path + ".br"); err == nil {
|
||||
f = brotliFile
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
}
|
||||
}
|
||||
if f == nil && tsweb.AcceptsEncoding(r, "gzip") {
|
||||
if gzipFile, err := distFS.Open(path + ".gz"); err == nil {
|
||||
f = gzipFile
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
if rawFile, err := distFS.Open(path); err == nil {
|
||||
f = rawFile
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
91
cmd/tsconnect/src/index.css
Normal file
91
cmd/tsconnect/src/index.css
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. */
|
||||
/* Use of this source code is governed by a BSD-style */
|
||||
/* license that can be found in the LICENSE file. */
|
||||
|
||||
@import "xterm/css/xterm.css";
|
||||
|
||||
html {
|
||||
background: #fff;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
border: solid 1px #ccc;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#header {
|
||||
background: #f7f5f4;
|
||||
border-bottom: 1px solid #eeebea;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#header #state {
|
||||
padding: 0 8px;
|
||||
color: #444342;
|
||||
}
|
||||
|
||||
#peers {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.login {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logout {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.peer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.peer:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.peer .name {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.peer .ssh {
|
||||
background-color: #cbf4c9;
|
||||
}
|
||||
|
||||
.term-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.xterm-viewport.xterm-viewport {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.xterm-viewport::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
.xterm-viewport::-webkit-scrollbar-track {
|
||||
opacity: 0;
|
||||
}
|
||||
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||
min-height: 20px;
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
26
cmd/tsconnect/src/index.js
Normal file
26
cmd/tsconnect/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import "./wasm_exec"
|
||||
import wasmUrl from "./main.wasm"
|
||||
import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier"
|
||||
import { sessionStateStorage } from "./js-state-store"
|
||||
|
||||
const go = new window.Go()
|
||||
WebAssembly.instantiateStreaming(
|
||||
fetch(`./dist/${wasmUrl}`),
|
||||
go.importObject
|
||||
).then((result) => {
|
||||
go.run(result.instance)
|
||||
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,
|
||||
})
|
||||
ipn.run({
|
||||
notifyState: notifyState.bind(null, ipn),
|
||||
notifyNetMap: notifyNetMap.bind(null, ipn),
|
||||
notifyBrowseToURL: notifyBrowseToURL.bind(null, ipn),
|
||||
})
|
||||
})
|
||||
16
cmd/tsconnect/src/js-state-store.js
Normal file
16
cmd/tsconnect/src/js-state-store.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/**
|
||||
* @fileoverview Callbacks used by jsStateStore to persist IPN state.
|
||||
*/
|
||||
|
||||
export const sessionStateStorage = {
|
||||
setState(id, value) {
|
||||
window.sessionStorage[`ipn-state-${id}`] = value
|
||||
},
|
||||
getState(id) {
|
||||
return window.sessionStorage[`ipn-state-${id}`] || ""
|
||||
},
|
||||
}
|
||||
71
cmd/tsconnect/src/login.js
Normal file
71
cmd/tsconnect/src/login.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import QRCode from "qrcode"
|
||||
|
||||
export async function showLoginURL(url) {
|
||||
if (loginNode) {
|
||||
loginNode.remove()
|
||||
}
|
||||
loginNode = document.createElement("div")
|
||||
loginNode.className = "login"
|
||||
const linkNode = document.createElement("a")
|
||||
linkNode.href = url
|
||||
linkNode.target = "_blank"
|
||||
loginNode.appendChild(linkNode)
|
||||
|
||||
try {
|
||||
const dataURL = await QRCode.toDataURL(url, { width: 512 })
|
||||
const imageNode = document.createElement("img")
|
||||
imageNode.src = dataURL
|
||||
imageNode.width = 256
|
||||
imageNode.height = 256
|
||||
imageNode.border = "0"
|
||||
linkNode.appendChild(imageNode)
|
||||
} catch (err) {
|
||||
console.error("Could not generate QR code:", err)
|
||||
}
|
||||
|
||||
linkNode.appendChild(document.createElement("br"))
|
||||
linkNode.appendChild(document.createTextNode(url))
|
||||
|
||||
document.body.appendChild(loginNode)
|
||||
}
|
||||
|
||||
export function hideLoginURL() {
|
||||
if (!loginNode) {
|
||||
return
|
||||
}
|
||||
loginNode.remove()
|
||||
loginNode = undefined
|
||||
}
|
||||
|
||||
let loginNode
|
||||
|
||||
export function showLogoutButton(ipn) {
|
||||
if (logoutButtonNode) {
|
||||
logoutButtonNode.remove()
|
||||
}
|
||||
logoutButtonNode = document.createElement("button")
|
||||
logoutButtonNode.className = "logout"
|
||||
logoutButtonNode.textContent = "Logout"
|
||||
logoutButtonNode.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
ipn.logout()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
document.getElementById("header").appendChild(logoutButtonNode)
|
||||
}
|
||||
|
||||
export function hideLogoutButton() {
|
||||
if (!logoutButtonNode) {
|
||||
return
|
||||
}
|
||||
logoutButtonNode.remove()
|
||||
logoutButtonNode = undefined
|
||||
}
|
||||
|
||||
let logoutButtonNode
|
||||
75
cmd/tsconnect/src/notifier.js
Normal file
75
cmd/tsconnect/src/notifier.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import {
|
||||
showLoginURL,
|
||||
hideLoginURL,
|
||||
showLogoutButton,
|
||||
hideLogoutButton,
|
||||
} from "./login"
|
||||
import { showSSHPeers, hideSSHPeers } from "./ssh"
|
||||
|
||||
/**
|
||||
* @fileoverview Notification callback functions (bridged from ipn.Notify)
|
||||
*/
|
||||
|
||||
/** Mirrors values from ipn/backend.go */
|
||||
const State = {
|
||||
NoState: 0,
|
||||
InUseOtherUser: 1,
|
||||
NeedsLogin: 2,
|
||||
NeedsMachineAuth: 3,
|
||||
Stopped: 4,
|
||||
Starting: 5,
|
||||
Running: 6,
|
||||
}
|
||||
|
||||
export function notifyState(ipn, state) {
|
||||
let stateLabel
|
||||
switch (state) {
|
||||
case State.NoState:
|
||||
stateLabel = "Initializing…"
|
||||
break
|
||||
case State.InUseOtherUser:
|
||||
stateLabel = "In-use by another user"
|
||||
break
|
||||
case State.NeedsLogin:
|
||||
stateLabel = "Needs Login"
|
||||
hideLogoutButton()
|
||||
hideSSHPeers()
|
||||
ipn.login()
|
||||
break
|
||||
case State.NeedsMachineAuth:
|
||||
stateLabel = "Needs authorization"
|
||||
break
|
||||
case State.Stopped:
|
||||
stateLabel = "Stopped"
|
||||
hideLogoutButton()
|
||||
hideSSHPeers()
|
||||
break
|
||||
case State.Starting:
|
||||
stateLabel = "Starting…"
|
||||
break
|
||||
case State.Running:
|
||||
stateLabel = "Running"
|
||||
hideLoginURL()
|
||||
showLogoutButton(ipn)
|
||||
break
|
||||
}
|
||||
const stateNode = document.getElementById("state")
|
||||
stateNode.textContent = stateLabel ?? ""
|
||||
}
|
||||
|
||||
export function notifyNetMap(ipn, netMapStr) {
|
||||
const netMap = JSON.parse(netMapStr)
|
||||
if (DEBUG) {
|
||||
console.log("Received net map: " + JSON.stringify(netMap, null, 2))
|
||||
}
|
||||
|
||||
showSSHPeers(netMap.peers, ipn)
|
||||
}
|
||||
|
||||
export function notifyBrowseToURL(ipn, url) {
|
||||
showLoginURL(url)
|
||||
}
|
||||
77
cmd/tsconnect/src/ssh.js
Normal file
77
cmd/tsconnect/src/ssh.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
import { Terminal } from "xterm"
|
||||
|
||||
export function showSSHPeers(peers, ipn) {
|
||||
const peersNode = document.getElementById("peers")
|
||||
peersNode.innerHTML = ""
|
||||
|
||||
const sshPeers = peers.filter((p) => p.tailscaleSSHEnabled)
|
||||
if (!sshPeers.length) {
|
||||
peersNode.textContent = "No machines have Tailscale SSH installed."
|
||||
return
|
||||
}
|
||||
|
||||
for (const peer of sshPeers) {
|
||||
const peerNode = document.createElement("div")
|
||||
peerNode.className = "peer"
|
||||
const nameNode = document.createElement("div")
|
||||
nameNode.className = "name"
|
||||
nameNode.textContent = peer.name
|
||||
peerNode.appendChild(nameNode)
|
||||
|
||||
const sshButtonNode = document.createElement("button")
|
||||
sshButtonNode.className = "ssh"
|
||||
sshButtonNode.addEventListener("click", function () {
|
||||
ssh(peer.name, ipn)
|
||||
})
|
||||
sshButtonNode.textContent = "SSH"
|
||||
peerNode.appendChild(sshButtonNode)
|
||||
|
||||
peersNode.appendChild(peerNode)
|
||||
}
|
||||
}
|
||||
|
||||
export function hideSSHPeers() {
|
||||
const peersNode = document.getElementById("peers")
|
||||
peersNode.innerHTML = ""
|
||||
}
|
||||
|
||||
function ssh(hostname, ipn) {
|
||||
const termContainerNode = document.createElement("div")
|
||||
termContainerNode.className = "term-container"
|
||||
document.body.appendChild(termContainerNode)
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
})
|
||||
term.open(termContainerNode)
|
||||
|
||||
// Cancel wheel events from scrolling the page if the terminal has scrollback
|
||||
termContainerNode.addEventListener("wheel", (e) => {
|
||||
if (term.buffer.active.baseY > 0) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
let onDataHook
|
||||
term.onData((e) => {
|
||||
onDataHook?.(e)
|
||||
})
|
||||
|
||||
term.focus()
|
||||
|
||||
ipn.ssh(
|
||||
hostname,
|
||||
(input) => term.write(input),
|
||||
(hook) => (onDataHook = hook),
|
||||
term.rows,
|
||||
term.cols,
|
||||
() => {
|
||||
term.dispose()
|
||||
termContainerNode.remove()
|
||||
}
|
||||
)
|
||||
}
|
||||
61
cmd/tsconnect/tsconnect.go
Normal file
61
cmd/tsconnect/tsconnect.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// 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")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if len(flag.Args()) != 1 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
switch flag.Arg(0) {
|
||||
case "dev":
|
||||
runDev()
|
||||
case "build":
|
||||
runBuild()
|
||||
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)
|
||||
}
|
||||
411
cmd/tsconnect/wasm/wasm_js.go
Normal file
411
cmd/tsconnect/wasm/wasm_js.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The wasm package builds a WebAssembly module that provides a subset of
|
||||
// Tailscale APIs to JavaScript.
|
||||
//
|
||||
// When run in the browser, a newIPN(config) function is added to the global JS
|
||||
// namespace. When called it returns an ipn object with the methods
|
||||
// run(callbacks), login(), logout(), and ssh(...).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/words"
|
||||
)
|
||||
|
||||
func main() {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("Usage: newIPN(config)")
|
||||
return nil
|
||||
}
|
||||
return newIPN(args[0])
|
||||
}))
|
||||
// Keep Go runtime alive, otherwise it will be shut down before newIPN gets
|
||||
// called.
|
||||
<-make(chan bool)
|
||||
}
|
||||
|
||||
func newIPN(jsConfig js.Value) map[string]any {
|
||||
netns.SetEnabled(false)
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
dialer := new(tsdial.Dialer)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = true
|
||||
ns.ProcessSubnets = true
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
return true
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
jsStateStorage := jsConfig.Get("stateStorage")
|
||||
var store ipn.StateStore
|
||||
if jsStateStorage.IsUndefined() {
|
||||
store = new(mem.Store)
|
||||
} else {
|
||||
store = &jsStateStore{jsStateStorage}
|
||||
}
|
||||
srv, err := ipnserver.New(log.Printf, "some-logid", store, eng, dialer, nil, ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
LoginFlags: controlclient.LoginEphemeral,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver.New: %v", err)
|
||||
}
|
||||
lb := srv.LocalBackend()
|
||||
|
||||
jsIPN := &jsIPN{
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 1 {
|
||||
log.Fatal(`Usage: run({
|
||||
notifyState(state: int): void,
|
||||
notifyNetMap(netMap: object): void,
|
||||
notifyBrowseToURL(url: string): void,
|
||||
})`)
|
||||
return nil
|
||||
}
|
||||
jsIPN.run(args[0])
|
||||
return nil
|
||||
}),
|
||||
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: login()")
|
||||
return nil
|
||||
}
|
||||
jsIPN.login()
|
||||
return nil
|
||||
}),
|
||||
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: logout()")
|
||||
return nil
|
||||
}
|
||||
jsIPN.logout()
|
||||
return nil
|
||||
}),
|
||||
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 6 {
|
||||
log.Printf("Usage: ssh(hostname, writeFn, readFn, rows, cols, onDone)")
|
||||
return nil
|
||||
}
|
||||
go jsIPN.ssh(
|
||||
args[0].String(),
|
||||
args[1],
|
||||
args[2],
|
||||
args[3].Int(),
|
||||
args[4].Int(),
|
||||
args[5])
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type jsIPN struct {
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
notifyState := func(state ipn.State) {
|
||||
jsCallbacks.Call("notifyState", int(state))
|
||||
}
|
||||
notifyState(ipn.NoState)
|
||||
|
||||
i.lb.SetNotifyCallback(func(n ipn.Notify) {
|
||||
log.Printf("NOTIFY: %+v", n)
|
||||
if n.State != nil {
|
||||
notifyState(*n.State)
|
||||
}
|
||||
if nm := n.NetMap; nm != nil {
|
||||
jsNetMap := jsNetMap{
|
||||
Self: jsNetMapSelfNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
Name: nm.Name,
|
||||
Addresses: mapSlice(nm.Addresses, func(a netaddr.IPPrefix) string { return a.IP().String() }),
|
||||
NodeKey: nm.NodeKey.String(),
|
||||
MachineKey: nm.MachineKey.String(),
|
||||
},
|
||||
MachineStatus: int(nm.MachineStatus),
|
||||
},
|
||||
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
|
||||
return jsNetMapPeerNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
Name: p.Name,
|
||||
Addresses: mapSlice(p.Addresses, func(a netaddr.IPPrefix) string { return a.IP().String() }),
|
||||
MachineKey: p.Machine.String(),
|
||||
NodeKey: p.Key.String(),
|
||||
},
|
||||
Online: *p.Online,
|
||||
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
|
||||
}
|
||||
}),
|
||||
}
|
||||
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
|
||||
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
|
||||
} else {
|
||||
log.Printf("Could not generate JSON netmap: %v", err)
|
||||
}
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := i.lb.Start(ipn.Options{
|
||||
StateKey: "wasm",
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: generateHostname(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Start error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ln, _, err := safesocket.Listen("", 0)
|
||||
if err != nil {
|
||||
log.Fatalf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = i.srv.Run(context.Background(), ln)
|
||||
log.Fatalf("ipnserver.Run exited: %v", err)
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *jsIPN) login() {
|
||||
go i.lb.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (i *jsIPN) logout() {
|
||||
if i.lb.State() == ipn.NoState {
|
||||
log.Printf("Backend not running")
|
||||
}
|
||||
go i.lb.Logout()
|
||||
}
|
||||
|
||||
func (i *jsIPN) ssh(host string, writeFn js.Value, setReadFn js.Value, rows, cols int, onDone js.Value) {
|
||||
defer onDone.Invoke()
|
||||
|
||||
write := func(s string) {
|
||||
writeFn.Invoke(s)
|
||||
}
|
||||
writeError := func(label string, err error) {
|
||||
write(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c, err := i.dialer.UserDial(ctx, "tcp", net.JoinHostPort(host, "22"))
|
||||
if err != nil {
|
||||
writeError("Dial", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
sshConn, _, _, err := ssh.NewClientConn(c, host, config)
|
||||
if err != nil {
|
||||
writeError("SSH Connection", err)
|
||||
return
|
||||
}
|
||||
defer sshConn.Close()
|
||||
write("SSH Connected\r\n")
|
||||
|
||||
sshClient := ssh.NewClient(sshConn, nil, nil)
|
||||
defer sshClient.Close()
|
||||
|
||||
session, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
writeError("SSH Session", err)
|
||||
return
|
||||
}
|
||||
write("Session Established\r\n")
|
||||
defer session.Close()
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
writeError("SSH Stdin", err)
|
||||
return
|
||||
}
|
||||
|
||||
session.Stdout = termWriter{writeFn}
|
||||
session.Stderr = termWriter{writeFn}
|
||||
|
||||
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
input := args[0].String()
|
||||
_, err := stdin.Write([]byte(input))
|
||||
if err != nil {
|
||||
writeError("Write Input", err)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
|
||||
|
||||
if err != nil {
|
||||
writeError("Pseudo Terminal", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = session.Shell()
|
||||
if err != nil {
|
||||
writeError("Shell", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
writeError("Exit", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type termWriter struct {
|
||||
f js.Value
|
||||
}
|
||||
|
||||
func (w termWriter) Write(p []byte) (n int, err error) {
|
||||
r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1)
|
||||
w.f.Invoke(string(r))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type jsNetMap struct {
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
}
|
||||
|
||||
type jsNetMapNode struct {
|
||||
Name string `json:"name"`
|
||||
Addresses []string `json:"addresses"`
|
||||
MachineStatus int `json:"machineStatus"`
|
||||
MachineKey string `json:"machineKey"`
|
||||
NodeKey string `json:"nodeKey"`
|
||||
}
|
||||
|
||||
type jsNetMapSelfNode struct {
|
||||
jsNetMapNode
|
||||
MachineStatus int `json:"machineStatus"`
|
||||
}
|
||||
|
||||
type jsNetMapPeerNode struct {
|
||||
jsNetMapNode
|
||||
Online bool `json:"online"`
|
||||
TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
|
||||
}
|
||||
|
||||
type jsStateStore struct {
|
||||
jsStateStorage js.Value
|
||||
}
|
||||
|
||||
func (s *jsStateStore) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
jsValue := s.jsStateStorage.Call("getState", string(id))
|
||||
if jsValue.String() == "" {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return hex.DecodeString(jsValue.String())
|
||||
}
|
||||
|
||||
func (s *jsStateStore) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
s.jsStateStorage.Call("setState", string(id), hex.EncodeToString(bs))
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapSlice[T any, M any](a []T, f func(T) M) []M {
|
||||
n := make([]M, len(a))
|
||||
for i, e := range a {
|
||||
n[i] = f(e)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func filterSlice[T any](a []T, f func(T) bool) []T {
|
||||
n := make([]T, 0, len(a))
|
||||
for _, e := range a {
|
||||
if f(e) {
|
||||
n = append(n, e)
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func generateHostname() string {
|
||||
tails := words.Tails()
|
||||
scales := words.Scales()
|
||||
if rand.Int()%2 == 0 {
|
||||
// JavaScript
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "j") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "s") })
|
||||
} else {
|
||||
// WebAssembly
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "w") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "a") })
|
||||
}
|
||||
|
||||
tail := tails[rand.Intn(len(tails))]
|
||||
scale := scales[rand.Intn(len(scales))]
|
||||
return fmt.Sprintf("%s-%s", tail, scale)
|
||||
}
|
||||
205
cmd/tsconnect/yarn.lock
Normal file
205
cmd/tsconnect/yarn.lock
Normal file
@@ -0,0 +1,205 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
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"
|
||||
|
||||
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==
|
||||
|
||||
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"
|
||||
|
||||
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:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||
|
||||
dijkstrajs@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
|
||||
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
|
||||
|
||||
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==
|
||||
|
||||
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"
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
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==
|
||||
|
||||
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==
|
||||
|
||||
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"
|
||||
|
||||
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==
|
||||
|
||||
pngjs@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
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"
|
||||
|
||||
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==
|
||||
|
||||
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=
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
xterm@^4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
|
||||
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
|
||||
|
||||
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==
|
||||
|
||||
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@^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"
|
||||
@@ -6,6 +6,7 @@ package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
@@ -41,20 +42,22 @@ func (g *LoginGoal) sendLogoutError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
var _ Client = (*Auto)(nil)
|
||||
|
||||
// Auto connects to a tailcontrol server for a node.
|
||||
// It's a concrete implementation of the Client interface.
|
||||
type Auto struct {
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
logf logger.Logf
|
||||
expiry *time.Time
|
||||
closed bool
|
||||
newMapCh chan struct{} // readable when we must restart a map request
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
logf logger.Logf
|
||||
expiry *time.Time
|
||||
closed bool
|
||||
newMapCh chan struct{} // readable when we must restart a map request
|
||||
statusFunc func(Status) // called to update Client status; always non-nil
|
||||
|
||||
unregisterHealthWatch func()
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
@@ -90,6 +93,9 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.Status == nil {
|
||||
return nil, errors.New("missing required Options.Status")
|
||||
}
|
||||
if opts.Logf == nil {
|
||||
opts.Logf = func(fmt string, args ...any) {}
|
||||
}
|
||||
@@ -97,13 +103,14 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
opts.TimeNow = time.Now
|
||||
}
|
||||
c := &Auto{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newMapCh: make(chan struct{}, 1),
|
||||
quit: make(chan struct{}),
|
||||
authDone: make(chan struct{}),
|
||||
mapDone: make(chan struct{}),
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newMapCh: make(chan struct{}, 1),
|
||||
quit: make(chan struct{}),
|
||||
authDone: make(chan struct{}),
|
||||
mapDone: make(chan struct{}),
|
||||
statusFunc: opts.Status,
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
@@ -462,7 +469,7 @@ func (c *Auto) mapRoutine() {
|
||||
c.mu.Unlock()
|
||||
health.SetInPollNetMap(false)
|
||||
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
|
||||
err := c.direct.PollNetMap(ctx, func(nm *netmap.NetworkMap) {
|
||||
health.SetInPollNetMap(true)
|
||||
c.mu.Lock()
|
||||
|
||||
@@ -531,13 +538,6 @@ func (c *Auto) AuthCantContinue() bool {
|
||||
return !c.loggedIn && (c.loginGoal == nil || c.loginGoal.url != "")
|
||||
}
|
||||
|
||||
// SetStatusFunc sets fn as the callback to run on any status change.
|
||||
func (c *Auto) SetStatusFunc(fn func(Status)) {
|
||||
c.mu.Lock()
|
||||
c.statusFunc = fn
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
@@ -565,10 +565,13 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
|
||||
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
synced := c.synced
|
||||
statusFunc := c.statusFunc
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -599,9 +602,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
State: state,
|
||||
Err: err,
|
||||
}
|
||||
if statusFunc != nil {
|
||||
statusFunc(new)
|
||||
}
|
||||
c.statusFunc(new)
|
||||
|
||||
c.mu.Lock()
|
||||
c.inSendStatus--
|
||||
@@ -666,11 +667,8 @@ func (c *Auto) SetExpirySooner(ctx context.Context, expiry time.Time) error {
|
||||
// them to the control server if they've changed.
|
||||
//
|
||||
// It does not retain the provided slice.
|
||||
//
|
||||
// The localPort field is unused except for integration tests in
|
||||
// another repo.
|
||||
func (c *Auto) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
changed := c.direct.SetEndpoints(localPort, endpoints)
|
||||
func (c *Auto) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
||||
changed := c.direct.SetEndpoints(endpoints)
|
||||
if changed {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
@@ -685,7 +683,6 @@ func (c *Auto) Shutdown() {
|
||||
direct := c.direct
|
||||
if !closed {
|
||||
c.closed = true
|
||||
c.statusFunc = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
@@ -29,9 +27,6 @@ const (
|
||||
// Currently this is done through a pair of polling https requests in
|
||||
// the Auto client, but that might change eventually.
|
||||
type Client interface {
|
||||
// SetStatusFunc provides a callback to call when control sends us
|
||||
// a message.
|
||||
SetStatusFunc(func(Status))
|
||||
// Shutdown closes this session, which should not be used any further
|
||||
// afterwards.
|
||||
Shutdown()
|
||||
@@ -47,9 +42,6 @@ type Client interface {
|
||||
// Logout starts a synchronous logout process. It doesn't return
|
||||
// until the logout operation has been completed.
|
||||
Logout(context.Context) error
|
||||
// SetExpirySooner sets the node's expiry time via the controlclient,
|
||||
// as long as it's shorter than the current expiry time.
|
||||
SetExpirySooner(context.Context, time.Time) error
|
||||
// SetPaused pauses or unpauses the controlclient activity as much
|
||||
// as possible, without losing its internal state, to minimize
|
||||
// unnecessary network activity.
|
||||
@@ -75,17 +67,10 @@ type Client interface {
|
||||
SetNetInfo(*tailcfg.NetInfo)
|
||||
// UpdateEndpoints changes the Endpoint structure that will be sent
|
||||
// in subsequent node registration requests.
|
||||
// The localPort field is unused except for integration tests in another repo.
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
|
||||
// DoNoiseRequest sends an HTTP request to the control plane
|
||||
// over the Noise transport.
|
||||
DoNoiseRequest(*http.Request) (*http.Response, error)
|
||||
UpdateEndpoints(endpoints []tailcfg.Endpoint)
|
||||
}
|
||||
|
||||
// UserVisibleError is an error that should be shown to users.
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
@@ -50,6 +49,7 @@ import (
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
@@ -77,7 +77,7 @@ type Direct struct {
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
|
||||
sfGroup singleflight.Group // protects noiseClient creation.
|
||||
sfGroup singleflight.Group[struct{}, *noiseClient] // protects noiseClient creation.
|
||||
noiseClient *noiseClient
|
||||
|
||||
persist persist.Persist
|
||||
@@ -88,7 +88,6 @@ type Direct struct {
|
||||
netinfo *tailcfg.NetInfo
|
||||
endpoints []tailcfg.Endpoint
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
localPort uint16 // or zero to mean auto
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
}
|
||||
|
||||
@@ -109,6 +108,9 @@ type Options struct {
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
|
||||
// Status is called when there's a change in status.
|
||||
Status func(Status)
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
KeepSharerAndUserSplit bool
|
||||
@@ -586,20 +588,19 @@ func sameEndpoints(a, b []tailcfg.Endpoint) bool {
|
||||
// whether they've changed.
|
||||
//
|
||||
// It does not retain the provided slice.
|
||||
func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
func (c *Direct) newEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Nothing new?
|
||||
if c.localPort == localPort && sameEndpoints(c.endpoints, endpoints) {
|
||||
if sameEndpoints(c.endpoints, endpoints) {
|
||||
return false // unchanged
|
||||
}
|
||||
var epStrs []string
|
||||
for _, ep := range endpoints {
|
||||
epStrs = append(epStrs, ep.Addr.String())
|
||||
}
|
||||
c.logf("[v2] client.newEndpoints(%v, %v)", localPort, epStrs)
|
||||
c.localPort = localPort
|
||||
c.logf("[v2] client.newEndpoints(%v)", epStrs)
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
c.everEndpoints = true
|
||||
@@ -610,28 +611,37 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (c
|
||||
// SetEndpoints updates the list of locally advertised endpoints.
|
||||
// It won't be replicated to the server until a *fresh* call to PollNetMap().
|
||||
// You don't need to restart PollNetMap if we return changed==false.
|
||||
func (c *Direct) SetEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
// (no log message on function entry, because it clutters the logs
|
||||
// if endpoints haven't changed. newEndpoints() will log it.)
|
||||
return c.newEndpoints(localPort, endpoints)
|
||||
return c.newEndpoints(endpoints)
|
||||
}
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
// PollNetMap makes a /map request to download the network map, calling cb with
|
||||
// each new netmap.
|
||||
//
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
return c.sendMapRequest(ctx, maxPolls, cb)
|
||||
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
|
||||
return c.sendMapRequest(ctx, -1, false, cb)
|
||||
}
|
||||
|
||||
// FetchNetMap fetches the netmap once.
|
||||
func (c *Direct) FetchNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
|
||||
var ret *netmap.NetworkMap
|
||||
err := c.sendMapRequest(ctx, 1, false, func(nm *netmap.NetworkMap) {
|
||||
ret = nm
|
||||
})
|
||||
if err == nil && ret == nil {
|
||||
return nil, errors.New("[unexpected] sendMapRequest success without callback")
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// SendLiteMapUpdate makes a /map request to update the server of our latest state,
|
||||
// but does not fetch anything. It returns an error if the server did not return a
|
||||
// successful 200 OK response.
|
||||
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
return c.sendMapRequest(ctx, 1, nil)
|
||||
return c.sendMapRequest(ctx, 1, false, nil)
|
||||
}
|
||||
|
||||
// If we go more than pollTimeout without hearing from the server,
|
||||
@@ -640,7 +650,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
const pollTimeout = 120 * time.Second
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool, cb func(*netmap.NetworkMap)) error {
|
||||
metricMapRequests.Add(1)
|
||||
metricMapRequestsActive.Add(1)
|
||||
defer metricMapRequestsActive.Add(-1)
|
||||
@@ -657,7 +667,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
localPort := c.localPort
|
||||
var epStrs []string
|
||||
var epTypes []tailcfg.EndpointType
|
||||
for _, ep := range c.endpoints {
|
||||
@@ -683,7 +692,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, epStrs)
|
||||
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if Debug.NetMap {
|
||||
@@ -703,6 +712,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
Hostinfo: hi,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: cb == nil,
|
||||
|
||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
||||
// to tell the control server not to distribute out our (empty) endpoints to peers.
|
||||
// Presumably we'll learn our endpoints in a half second and do another post
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyperspecific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
|
||||
}
|
||||
var extraDebugFlags []string
|
||||
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
|
||||
@@ -725,17 +744,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
}
|
||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
||||
// to tell the control server not to distribute out our (empty) endpoints to peers.
|
||||
// Presumably we'll learn our endpoints in a half second and do another post
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyperspecific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
if len(epStrs) == 0 && !everEndpoints && !inTest() {
|
||||
request.ReadOnly = true
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
if err != nil {
|
||||
@@ -940,14 +948,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
nm.SelfNode.Capabilities = nil
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occurred meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
nm.LocalPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
// Occasionally print the netmap header.
|
||||
// This is handy for debugging, and our logs processing
|
||||
// pipeline depends on it. (TODO: Remove this dependency.)
|
||||
@@ -1280,13 +1280,12 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
if nc != nil {
|
||||
return nc, nil
|
||||
}
|
||||
np, err, _ := c.sfGroup.Do("noise", func() (any, error) {
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
|
||||
k, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1298,7 +1297,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return np.(*noiseClient), nil
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// setDNSNoise sends the SetDNSRequest request to the control plane server over Noise,
|
||||
|
||||
@@ -68,22 +68,18 @@ func TestNewDirect(t *testing.T) {
|
||||
}
|
||||
|
||||
endpoints := fakeEndpoints(1, 2, 3)
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
changed = c.newEndpoints(endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(12) want true got %v", changed)
|
||||
t.Errorf("c.newEndpoints want true got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
changed = c.newEndpoints(endpoints)
|
||||
if changed {
|
||||
t.Errorf("c.newEndpoints(12) want false got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
t.Errorf("c.newEndpoints want false got %v", changed)
|
||||
}
|
||||
endpoints = fakeEndpoints(4, 5, 6)
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
changed = c.newEndpoints(endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
t.Errorf("c.newEndpoints want true got %v", changed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
@@ -165,12 +166,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
@@ -244,7 +239,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
sortNodes(newFull)
|
||||
}
|
||||
|
||||
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
|
||||
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 || len(mapRes.PeersChangedPatch) != 0 {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
@@ -265,6 +260,16 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
n.Online = &online
|
||||
}
|
||||
}
|
||||
for _, ec := range mapRes.PeersChangedPatch {
|
||||
if n, ok := peerByID[ec.NodeID]; ok {
|
||||
if ec.DERPRegion != 0 {
|
||||
n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion)
|
||||
}
|
||||
if ec.Endpoints != nil {
|
||||
n.Endpoints = ec.Endpoints
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
|
||||
@@ -34,6 +34,16 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
n.LastSeen = &t
|
||||
}
|
||||
}
|
||||
withDERP := func(d string) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.DERP = d
|
||||
}
|
||||
}
|
||||
withEP := func(ep string) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Endpoints = []string{ep}
|
||||
}
|
||||
}
|
||||
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name}
|
||||
for _, f := range mod {
|
||||
@@ -137,7 +147,53 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
n(2, "bar", seenAt(time.Unix(123, 0))),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ep_change_derp",
|
||||
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"))),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
DERPRegion: 4,
|
||||
}},
|
||||
},
|
||||
want: peers(n(1, "foo", withDERP("127.3.3.40:4"))),
|
||||
},
|
||||
{
|
||||
name: "ep_change_udp",
|
||||
prev: peers(n(1, "foo", withEP("1.2.3.4:111"))),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
Endpoints: []string{"1.2.3.4:56"},
|
||||
}},
|
||||
},
|
||||
want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
|
||||
},
|
||||
{
|
||||
name: "ep_change_udp",
|
||||
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
Endpoints: []string{"1.2.3.4:56"},
|
||||
}},
|
||||
},
|
||||
want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))),
|
||||
},
|
||||
{
|
||||
name: "ep_change_both",
|
||||
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
DERPRegion: 2,
|
||||
Endpoints: []string{"1.2.3.4:56"},
|
||||
}},
|
||||
},
|
||||
want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.curTime.IsZero() {
|
||||
|
||||
@@ -13,11 +13,10 @@ import (
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebScokets, since we cannot do
|
||||
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
|
||||
// bi-directional communication over an HTTP connection when in JS.
|
||||
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
|
||||
@@ -25,13 +24,19 @@ func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, contr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, addr, err := net.SplitHostPort(addr)
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsScheme := "wss"
|
||||
wsHost := host
|
||||
if host == "localhost" {
|
||||
wsScheme = "ws"
|
||||
wsHost = addr
|
||||
}
|
||||
wsURL := &url.URL{
|
||||
Scheme: "ws",
|
||||
Host: net.JoinHostPort(host, addr),
|
||||
Scheme: wsScheme,
|
||||
Host: wsHost,
|
||||
Path: serverUpgradePath,
|
||||
// Can't set HTTP headers on the websocket request, so we have to to send
|
||||
// the handshake via an HTTP header.
|
||||
@@ -45,7 +50,7 @@ func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, contr
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn := wsconn.New(wsConn)
|
||||
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -106,7 +105,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := wsconn.New(c)
|
||||
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
|
||||
@@ -156,6 +156,8 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
// Version is the DERP protocol version that the client was built with.
|
||||
// See the ProtocolVersion const.
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
// MeshKey optionally specifies a pre-shared key used by
|
||||
|
||||
@@ -410,7 +410,7 @@ func (s *Server) IsClientConnectedForTest(k key.NodePublic) bool {
|
||||
// on its own.
|
||||
//
|
||||
// Accept closes nc.
|
||||
func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
|
||||
func (s *Server) Accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
|
||||
closed := make(chan struct{})
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -428,7 +428,7 @@ func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.accept(nc, brw, remoteAddr, connNum); err != nil && !s.isClosed() {
|
||||
if err := s.accept(ctx, nc, brw, remoteAddr, connNum); err != nil && !s.isClosed() {
|
||||
s.logf("derp: %s: %v", remoteAddr, err)
|
||||
}
|
||||
}
|
||||
@@ -641,7 +641,7 @@ func (s *Server) addWatcher(c *sclient) {
|
||||
go c.requestMeshUpdate()
|
||||
}
|
||||
|
||||
func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
|
||||
func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
|
||||
br := brw.Reader
|
||||
nc.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
bw := &lazyBufioWriter{w: nc, lbw: brw.Writer}
|
||||
@@ -660,7 +660,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
// At this point we trust the client so we don't time out.
|
||||
nc.SetDeadline(time.Time{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
remoteIPPort, _ := netaddr.ParseIPPort(remoteAddr)
|
||||
|
||||
@@ -85,8 +85,12 @@ func TestSendRecv(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cin.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
|
||||
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
go s.Accept(ctx, cin, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
|
||||
key := clientPrivateKeys[i]
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
|
||||
@@ -231,10 +235,10 @@ func TestSendFreeze(t *testing.T) {
|
||||
// Then cathy stops processing messsages.
|
||||
// That should not interfere with alice talking to bob.
|
||||
|
||||
newClient := func(name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
|
||||
newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
|
||||
t.Helper()
|
||||
c1, c2 := nettest.NewConn(name, 1024)
|
||||
go s.Accept(c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
|
||||
go s.Accept(ctx, c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(c2), bufio.NewWriter(c2))
|
||||
c, err := NewClient(k, c2, brw, t.Logf)
|
||||
@@ -245,14 +249,17 @@ func TestSendFreeze(t *testing.T) {
|
||||
return c, c2
|
||||
}
|
||||
|
||||
ctx, clientCtxCancel := context.WithCancel(context.Background())
|
||||
defer clientCtxCancel()
|
||||
|
||||
aliceKey := key.NewNode()
|
||||
aliceClient, aliceConn := newClient("alice", aliceKey)
|
||||
aliceClient, aliceConn := newClient(ctx, "alice", aliceKey)
|
||||
|
||||
bobKey := key.NewNode()
|
||||
bobClient, bobConn := newClient("bob", bobKey)
|
||||
bobClient, bobConn := newClient(ctx, "bob", bobKey)
|
||||
|
||||
cathyKey := key.NewNode()
|
||||
cathyClient, cathyConn := newClient("cathy", cathyKey)
|
||||
cathyClient, cathyConn := newClient(ctx, "cathy", cathyKey)
|
||||
|
||||
var (
|
||||
aliceCh = make(chan struct{}, 32)
|
||||
@@ -455,7 +462,7 @@ func (ts *testServer) close(t *testing.T) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) *testServer {
|
||||
func newTestServer(t *testing.T, ctx context.Context) *testServer {
|
||||
t.Helper()
|
||||
logf := logger.WithPrefix(t.Logf, "derp-server: ")
|
||||
s := NewServer(key.NewNode(), logf)
|
||||
@@ -475,7 +482,7 @@ func newTestServer(t *testing.T) *testServer {
|
||||
// TODO: register c in ts so Close also closes it?
|
||||
go func(i int) {
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
|
||||
go s.Accept(c, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
go s.Accept(ctx, c, brwServer, fmt.Sprintf("test-client-%d", i))
|
||||
}(i)
|
||||
}
|
||||
}()
|
||||
@@ -610,7 +617,10 @@ func (c *testClient) close(t *testing.T) {
|
||||
// TestWatch tests the connection watcher mechanism used by regional
|
||||
// DERP nodes to mesh up with each other.
|
||||
func TestWatch(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
ts := newTestServer(t, ctx)
|
||||
defer ts.close(t)
|
||||
|
||||
w1 := newTestWatcher(t, ts, "w1")
|
||||
@@ -1198,7 +1208,10 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
defer connIn.Close()
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn))
|
||||
go s.Accept(connIn, brwServer, "test-client")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go s.Accept(ctx, connIn, brwServer, "test-client")
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
|
||||
client, err := NewClient(k, connOut, brw, logger.Discard)
|
||||
@@ -1354,7 +1367,10 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerRepliesToPing(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
ts := newTestServer(t, ctx)
|
||||
defer ts.close(t)
|
||||
|
||||
tc := newRegularClient(t, ts, "alice")
|
||||
|
||||
@@ -56,6 +56,6 @@ func Handler(s *derp.Server) http.Handler {
|
||||
pubKey.UntypedHexString())
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
s.Accept(r.Context(), netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -29,5 +28,6 @@ func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
return wsconn.New(c), nil
|
||||
netConn := websocket.NetConn(context.Background(), c, websocket.MessageBinary)
|
||||
return netConn, nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
ROUTES ?= ""
|
||||
TS_ROUTES ?= ""
|
||||
SA_NAME ?= tailscale
|
||||
KUBE_SECRET ?= tailscale
|
||||
TS_KUBE_SECRET ?= tailscale
|
||||
|
||||
rbac:
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#! /bin/sh
|
||||
|
||||
set -m # enable job control
|
||||
|
||||
export PATH=$PATH:/tailscale/bin
|
||||
|
||||
TS_AUTH_KEY="${TS_AUTH_KEY:-}"
|
||||
@@ -14,17 +16,20 @@ TS_USERSPACE="${TS_USERSPACE:-true}"
|
||||
TS_STATE_DIR="${TS_STATE_DIR:-}"
|
||||
TS_ACCEPT_DNS="${TS_ACCEPT_DNS:-false}"
|
||||
TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
|
||||
TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}"
|
||||
TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}"
|
||||
TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}"
|
||||
|
||||
set -e
|
||||
|
||||
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
|
||||
|
||||
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET}"
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}"
|
||||
elif [[ ! -z "${TS_STATE_DIR}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --statedir=${TS_STATE_DIR}"
|
||||
else
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=mem:"
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=mem: --statedir=/tmp"
|
||||
fi
|
||||
|
||||
if [[ "${TS_USERSPACE}" == "true" ]]; then
|
||||
@@ -43,9 +48,20 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -z "${TS_SOCKS5_SERVER}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --socks5-server ${TS_SOCKS5_SERVER}"
|
||||
fi
|
||||
|
||||
if [[ ! -z "${TS_OUTBOUND_HTTP_PROXY_LISTEN}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --outbound-http-proxy-listen ${TS_OUTBOUND_HTTP_PROXY_LISTEN}"
|
||||
fi
|
||||
|
||||
if [[ ! -z "${TS_TAILSCALED_EXTRA_ARGS}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} ${TS_TAILSCALED_EXTRA_ARGS}"
|
||||
fi
|
||||
|
||||
echo "Starting tailscaled"
|
||||
tailscaled ${TAILSCALED_ARGS} &
|
||||
PID=$!
|
||||
|
||||
UP_ARGS="--accept-dns=${TS_ACCEPT_DNS}"
|
||||
if [[ ! -z "${TS_ROUTES}" ]]; then
|
||||
@@ -66,4 +82,4 @@ if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
fi
|
||||
|
||||
wait ${PID}
|
||||
fg
|
||||
|
||||
27
go.mod
27
go.mod
@@ -6,6 +6,7 @@ require (
|
||||
filippo.io/mkcert v1.4.3
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/andybalholm/brotli v1.0.3
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.11.0
|
||||
@@ -16,13 +17,16 @@ require (
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/evanw/esbuild v0.14.39
|
||||
github.com/frankban/quicktest v1.14.0
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e
|
||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b
|
||||
@@ -42,7 +46,7 @@ require (
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17
|
||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
@@ -51,17 +55,17 @@ require (
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961
|
||||
golang.org/x/tools v0.1.11
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445
|
||||
gvisor.dev/gvisor v0.0.0-20220721202624-0b2c11c2773c
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
@@ -69,6 +73,7 @@ require (
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
|
||||
github.com/Antonboom/errname v0.1.5 // indirect
|
||||
github.com/Antonboom/nilnil v0.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
@@ -255,14 +260,14 @@ require (
|
||||
github.com/uudashr/gocognit v1.0.5 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
|
||||
61
go.sum
61
go.sum
@@ -52,6 +52,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o=
|
||||
filippo.io/mkcert v1.4.3/go.mod h1:64ke566uBwAQcdK3vRDABgsgVHqrfORPTw6YytZCTxk=
|
||||
github.com/Antonboom/errname v0.1.5 h1:IM+A/gz0pDhKmlt5KSNTVAvfLMb+65RxavBXpRtCUEg=
|
||||
@@ -110,6 +112,7 @@ github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pO
|
||||
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
|
||||
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
@@ -278,6 +281,8 @@ github.com/esimonov/ifshort v1.0.3 h1:JD6x035opqGec5fZ0TLjXeROD2p5H7oLGn8MKfy9HT
|
||||
github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE=
|
||||
github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
|
||||
github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
|
||||
github.com/evanw/esbuild v0.14.39 h1:1TMZtCXOY4ctAbGY4QT9sjT203I/cQ16vXt2F9rLT58=
|
||||
github.com/evanw/esbuild v0.14.39/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY=
|
||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
@@ -294,12 +299,15 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
|
||||
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
@@ -339,9 +347,11 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
@@ -376,9 +386,11 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@@ -603,6 +615,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU=
|
||||
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
@@ -833,6 +847,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
@@ -1094,8 +1110,8 @@ github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17 h1:QaQrUggZ7U2lE3HhoPx6bDK7fO385FR7pHRYSPEv70Q=
|
||||
github.com/tailscale/hujson v0.0.0-20220506202205-92b4b88a9e17/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f h1:n4r/sJ92cBSBHK8n9lR1XLFr0OiTVeGfN5TR+9LaN7E=
|
||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/dIMvD0t77g0FxFfZIQjghDQxyG2U=
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
||||
@@ -1141,6 +1157,7 @@ github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
@@ -1172,6 +1189,8 @@ github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:tw
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
|
||||
@@ -1231,8 +1250,9 @@ go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofe
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1304,8 +1324,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1369,8 +1389,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1402,8 +1422,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1500,6 +1520,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1510,8 +1531,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
@@ -1644,19 +1665,17 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.8-0.20211102182255-bb4add04ddef/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1 h1:Z3vE1sGlC7qiyFJkkDcZms8Y3+yV8+W7HmDSmuf71tM=
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961 h1:oIXcKhP1Ge6cRqdpQuldl0hf4mjIsNaXojabghlHuTs=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10/go.mod h1:v7w/8FC48tTBm1IzScDVPEEb0/GjLta+T0ybpP9UWRg=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -1838,8 +1857,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445 h1:pLNQCtMzh4O6rdhoUeWHuutt4yMft+B9Cgw/bezWchE=
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445/go.mod h1:tWwEcFvJavs154OdjFCw78axNrsDlz4Zh8jvPqwcpGI=
|
||||
gvisor.dev/gvisor v0.0.0-20220721202624-0b2c11c2773c h1:frrINYSQqhraHqy23/dWqdNt7mRlsGJJBwGHvI3Q+/c=
|
||||
gvisor.dev/gvisor v0.0.0-20220721202624-0b2c11c2773c/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1855,8 +1874,8 @@ howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCU
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310 h1:0jKHTf+W75kYRyg5bto1UT+r18QmAz2u/5pAs/fx4zo=
|
||||
|
||||
@@ -1 +1 @@
|
||||
04d67b90d8cfd6f822664220f79e0e69cacb6b5c
|
||||
149f7d88f11384083d42ccbcdb31693510385ec6
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
@@ -38,6 +39,7 @@ func New() *tailcfg.Hostinfo {
|
||||
Package: packageTypeCached(),
|
||||
GoArch: runtime.GOARCH,
|
||||
DeviceModel: deviceModel(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
os string // version.OS value; empty means linux
|
||||
cloud cloudenv.Cloud
|
||||
prefs *ipn.Prefs
|
||||
want *dns.Config
|
||||
wantLog string
|
||||
|
||||
@@ -138,8 +138,9 @@ type LocalBackend struct {
|
||||
sshServer SSHServer // or nil, initialized lazily.
|
||||
notify func(ipn.Notify)
|
||||
cc controlclient.Client
|
||||
stateKey ipn.StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
||||
stateKey ipn.StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *ipn.Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey key.MachinePrivate
|
||||
@@ -341,7 +342,19 @@ func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
|
||||
// can no longer be used after Shutdown returns.
|
||||
func (b *LocalBackend) Shutdown() {
|
||||
b.mu.Lock()
|
||||
if b.shutdownCalled {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
b.shutdownCalled = true
|
||||
|
||||
if b.loginFlags&controlclient.LoginEphemeral != 0 {
|
||||
b.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
b.LogoutSync(ctx) // best effort
|
||||
b.mu.Lock()
|
||||
}
|
||||
cc := b.cc
|
||||
if b.sshServer != nil {
|
||||
b.sshServer.Shutdown()
|
||||
@@ -416,6 +429,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
s.Health = append(s.Health, err.Error())
|
||||
}
|
||||
}
|
||||
if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" {
|
||||
s.Health = append(s.Health, m)
|
||||
}
|
||||
if b.netMap != nil {
|
||||
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
@@ -425,6 +441,20 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
|
||||
s.CurrentTailnet.Name = b.netMap.Domain
|
||||
if b.prefs != nil && !b.prefs.ExitNodeID.IsZero() {
|
||||
if exitPeer, ok := b.netMap.PeerWithStableID(b.prefs.ExitNodeID); ok {
|
||||
var online = false
|
||||
if exitPeer.Online != nil {
|
||||
online = *exitPeer.Online
|
||||
}
|
||||
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
|
||||
ID: b.prefs.ExitNodeID,
|
||||
Online: online,
|
||||
TailscaleIPs: exitPeer.Addresses,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
@@ -779,7 +809,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
|
||||
if cc != nil {
|
||||
if needUpdateEndpoints {
|
||||
cc.UpdateEndpoints(0, s.LocalAddrs)
|
||||
cc.UpdateEndpoints(s.LocalAddrs)
|
||||
}
|
||||
b.stateMachine()
|
||||
}
|
||||
@@ -1038,6 +1068,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
Pinger: b,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
@@ -1049,14 +1080,14 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
b.mu.Lock()
|
||||
b.cc = cc
|
||||
b.ccAuto, _ = cc.(*controlclient.Auto)
|
||||
endpoints := b.endpoints
|
||||
b.mu.Unlock()
|
||||
|
||||
if endpoints != nil {
|
||||
cc.UpdateEndpoints(0, endpoints)
|
||||
cc.UpdateEndpoints(endpoints)
|
||||
}
|
||||
|
||||
cc.SetStatusFunc(b.setClientStatus)
|
||||
b.e.SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
b.mu.Lock()
|
||||
@@ -1826,39 +1857,88 @@ func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
var errs []error
|
||||
if p.Hostname == "badhostname.tailscale." {
|
||||
// Keep this one just for testing.
|
||||
return errors.New("bad hostname [test]")
|
||||
errs = append(errs, errors.New("bad hostname [test]"))
|
||||
}
|
||||
if p.RunSSH {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||
}
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
if err := b.checkSSHPrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
if !p.RunSSH {
|
||||
return nil
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
|
||||
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return nil
|
||||
}
|
||||
if b.netMap != nil {
|
||||
if !hasCapability(b.netMap, tailcfg.CapabilitySSH) {
|
||||
if b.isDefaultServerLocked() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh")
|
||||
}
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
|
||||
if b.prefs == nil || !b.prefs.RunSSH {
|
||||
return ""
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return "development SSH policy in use"
|
||||
}
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return ""
|
||||
}
|
||||
if nm.SSHPolicy != nil && len(nm.SSHPolicy.Rules) > 0 {
|
||||
return ""
|
||||
}
|
||||
isDefault := b.isDefaultServerLocked()
|
||||
isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin)
|
||||
|
||||
if !isAdmin {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access."
|
||||
}
|
||||
if !isDefault {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access."
|
||||
}
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy"
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isDefaultServerLocked() bool {
|
||||
if b.prefs == nil {
|
||||
return true // assume true until set otherwise
|
||||
}
|
||||
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.mu.Lock()
|
||||
p0 := b.prefs.Clone()
|
||||
@@ -2107,7 +2187,6 @@ func (b *LocalBackend) authReconfig() {
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
||||
oneCGNATRoute := nm != nil && nm.Debug != nil && nm.Debug.OneCGNATRoute.EqualBool(true)
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -2152,6 +2231,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
return
|
||||
}
|
||||
|
||||
oneCGNATRoute := shouldUseOneCGNATRoute(nm, b.logf, version.OS())
|
||||
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
|
||||
dcfg := dnsConfigForNetmap(nm, prefs, b.logf, version.OS())
|
||||
|
||||
@@ -2164,8 +2244,40 @@ func (b *LocalBackend) authReconfig() {
|
||||
b.initPeerAPIListener()
|
||||
}
|
||||
|
||||
// shouldUseOneCGNATRoute reports whether we should prefer to make one big
|
||||
// CGNAT /10 route rather than a /32 per peer.
|
||||
//
|
||||
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
|
||||
// a runtime.GOOS.
|
||||
func shouldUseOneCGNATRoute(nm *netmap.NetworkMap, logf logger.Logf, versionOS string) bool {
|
||||
// Explicit enabling or disabling always take precedence.
|
||||
if nm.Debug != nil {
|
||||
if v, ok := nm.Debug.OneCGNATRoute.Get(); ok {
|
||||
logf("[v1] shouldUseOneCGNATRoute: explicit=%v", v)
|
||||
return v
|
||||
}
|
||||
}
|
||||
// Also prefer to do this on the Mac, so that we don't need to constantly
|
||||
// update the network extension configuration (which is disruptive to
|
||||
// Chrome, see https://github.com/tailscale/tailscale/issues/3102). Only
|
||||
// use fine-grained routes if another interfaces is also using the CGNAT
|
||||
// IP range.
|
||||
if versionOS == "macOS" {
|
||||
hasCGNATInterface, err := interfaces.HasCGNATInterface()
|
||||
if err != nil {
|
||||
logf("shouldUseOneCGNATRoute: Could not determine if any interfaces use CGNAT: %v", err)
|
||||
return false
|
||||
}
|
||||
logf("[v1] shouldUseOneCGNATRoute: macOS automatic=%v", !hasCGNATInterface)
|
||||
if !hasCGNATInterface {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dnsConfigForNetmap returns a *dns.Config for the given netmap,
|
||||
// prefs, and client OS version.
|
||||
// prefs, client OS version, and cloud hosting environment.
|
||||
//
|
||||
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
|
||||
// a runtime.GOOS.
|
||||
@@ -2260,7 +2372,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
|
||||
addDefault := func(resolvers []*dnstype.Resolver) {
|
||||
for _, r := range resolvers {
|
||||
r := r
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
|
||||
}
|
||||
}
|
||||
@@ -3094,7 +3205,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
defer b.mu.Unlock()
|
||||
nm := b.netMap
|
||||
if b.state != ipn.Running || nm == nil {
|
||||
return nil, errors.New("not connected")
|
||||
return nil, errors.New("not connected to the tailnet")
|
||||
}
|
||||
if !b.capFileSharing {
|
||||
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||
@@ -3133,7 +3244,7 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
cc := b.ccAuto
|
||||
if prefs := b.prefs; prefs != nil {
|
||||
req.NodeKey = prefs.Persist.PrivateNodeKey.Public()
|
||||
}
|
||||
@@ -3301,7 +3412,13 @@ func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
|
||||
// If t is in the past, the key is expired immediately.
|
||||
// If t is after the current expiry, an error is returned.
|
||||
func (b *LocalBackend) SetExpirySooner(ctx context.Context, expiry time.Time) error {
|
||||
return b.cc.SetExpirySooner(ctx, expiry)
|
||||
b.mu.Lock()
|
||||
cc := b.ccAuto
|
||||
b.mu.Unlock()
|
||||
if cc == nil {
|
||||
return errors.New("not running")
|
||||
}
|
||||
return cc.SetExpirySooner(ctx, expiry)
|
||||
}
|
||||
|
||||
// exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
|
||||
@@ -3361,7 +3478,7 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||
// Noise connection.
|
||||
func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
cc := b.ccAuto
|
||||
b.mu.Unlock()
|
||||
if cc == nil {
|
||||
return nil, errors.New("no client")
|
||||
|
||||
@@ -511,13 +511,13 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
func TestFileTargets(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
_, err := b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
if got, want := fmt.Sprint(err), "not connected to the tailnet"; got != want {
|
||||
t.Errorf("before connect: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.netMap = new(netmap.NetworkMap)
|
||||
_, err = b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
if got, want := fmt.Sprint(err), "not connected to the tailnet"; got != want {
|
||||
t.Errorf("non-running netmap: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,8 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
func (b *LocalBackend) getSystemSSH_HostKeys() (ret []ssh.Signer, err error) {
|
||||
// TODO(bradfitz): cache this?
|
||||
for _, typ := range keyTypes {
|
||||
hostKey, err := ioutil.ReadFile("/etc/ssh/ssh_host_" + typ + "_key")
|
||||
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
||||
hostKey, err := ioutil.ReadFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
@@ -126,7 +127,7 @@ func (b *LocalBackend) getSystemSSH_HostKeys() (ret []ssh.Signer, err error) {
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading private key %s: %w", filename, err)
|
||||
}
|
||||
ret = append(ret, signer)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -115,10 +114,6 @@ func (cc *mockControl) logf(format string, args ...any) {
|
||||
cc.logfActual(format, args...)
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
|
||||
cc.statusFunc = fn
|
||||
}
|
||||
|
||||
func (cc *mockControl) populateKeys() (newKeys bool) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
@@ -225,12 +220,6 @@ func (cc *mockControl) Logout(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetExpirySooner(context.Context, time.Time) error {
|
||||
cc.logf("SetExpirySooner")
|
||||
cc.called("SetExpirySooner")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetPaused(paused bool) {
|
||||
cc.logf("SetPaused=%v", paused)
|
||||
if paused {
|
||||
@@ -258,20 +247,12 @@ func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
cc.called("SetNetInfo")
|
||||
}
|
||||
|
||||
func (cc *mockControl) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
||||
// validate endpoint information here?
|
||||
cc.logf("UpdateEndpoints: lp=%v ep=%v", localPort, endpoints)
|
||||
cc.logf("UpdateEndpoints: ep=%v", endpoints)
|
||||
cc.called("UpdateEndpoints")
|
||||
}
|
||||
|
||||
func (*mockControl) SetDNS(context.Context, *tailcfg.SetDNSRequest) error {
|
||||
panic("unexpected SetDNS call")
|
||||
}
|
||||
|
||||
func (*mockControl) DoNoiseRequest(*http.Request) (*http.Response, error) {
|
||||
panic("unexpected DoNoiseRequest call")
|
||||
}
|
||||
|
||||
// A very precise test of the sequence of function calls generated by
|
||||
// ipnlocal.Local into its controlclient instance, and the events it
|
||||
// produces upstream into the UI.
|
||||
@@ -304,12 +285,15 @@ func TestStateMachine(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
cc := newMockControl(t)
|
||||
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
cc := newMockControl(t)
|
||||
cc.statusFunc = b.setClientStatus
|
||||
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
|
||||
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
cc.opts = opts
|
||||
|
||||
@@ -38,6 +38,10 @@ type Status struct {
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
// ExitNodeStatus describes the current exit node.
|
||||
// If nil, an exit node is not in use.
|
||||
ExitNodeStatus *ExitNodeStatus `json:"ExitNodeStatus,omitempty"`
|
||||
|
||||
// Health contains health check problems.
|
||||
// Empty means everything is good. (or at least that no known
|
||||
// problems are detected)
|
||||
@@ -81,6 +85,18 @@ type TailnetStatus struct {
|
||||
MagicDNSEnabled bool
|
||||
}
|
||||
|
||||
// ExitNodeStatus describes the current exit node.
|
||||
type ExitNodeStatus struct {
|
||||
// ID is the exit node's ID.
|
||||
ID tailcfg.StableNodeID
|
||||
|
||||
// Online is whether the exit node is alive.
|
||||
Online bool
|
||||
|
||||
// TailscaleIPs are the exit node's IP addresses assigned to the node.
|
||||
TailscaleIPs []netaddr.IPPrefix
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.NodePublic {
|
||||
kk := make([]key.NodePublic, 0, len(s.Peer))
|
||||
for k := range s.Peer {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// Process-wide cache. (A new *Handler is created per connection,
|
||||
@@ -53,6 +55,13 @@ var (
|
||||
|
||||
func (h *Handler) certDir() (string, error) {
|
||||
d := h.b.TailscaleVarRoot()
|
||||
|
||||
// As a workaround for Synology DSM6 not having a "var" directory, use the
|
||||
// app's "etc" directory (on a small partition) to hold certs at least.
|
||||
// See https://github.com/tailscale/tailscale/issues/4060#issuecomment-1186592251
|
||||
if d == "" && runtime.GOOS == "linux" && distro.Get() == distro.Synology && distro.DSMVersion() == 6 {
|
||||
d = "/var/packages/Tailscale/etc" // base; we append "certs" below
|
||||
}
|
||||
if d == "" {
|
||||
return "", errors.New("no TailscaleVarRoot")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -41,6 +42,16 @@ func randHex(n int) string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
var (
|
||||
// The clientmetrics package is stateful, but we want to expose a simple
|
||||
// imperative API to local clients, so we need to keep track of
|
||||
// clientmetric.Metric instances that we've created for them. These need to
|
||||
// be globals because we end up creating many Handler instances for the
|
||||
// lifetime of a client.
|
||||
metricsMu sync.Mutex
|
||||
metrics = map[string]*clientmetric.Metric{}
|
||||
)
|
||||
|
||||
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID string) *Handler {
|
||||
return &Handler{b: b, logf: logf, backendLogID: logID}
|
||||
}
|
||||
@@ -137,6 +148,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveDial(w, r)
|
||||
case "/localapi/v0/id-token":
|
||||
h.serveIDToken(w, r)
|
||||
case "/localapi/v0/upload-client-metrics":
|
||||
h.serveUploadClientMetrics(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
@@ -730,6 +743,54 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
|
||||
<-errc
|
||||
}
|
||||
|
||||
func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type clientMetricJSON struct {
|
||||
Name string `json:"name"`
|
||||
// One of "counter" or "gauge"
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
var clientMetrics []clientMetricJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
metricsMu.Lock()
|
||||
defer metricsMu.Unlock()
|
||||
|
||||
for _, m := range clientMetrics {
|
||||
if metric, ok := metrics[m.Name]; ok {
|
||||
metric.Add(int64(m.Value))
|
||||
} else {
|
||||
if clientmetric.HasPublished(m.Name) {
|
||||
http.Error(w, "Already have a metric named "+m.Name, 400)
|
||||
return
|
||||
}
|
||||
var metric *clientmetric.Metric
|
||||
switch m.Type {
|
||||
case "counter":
|
||||
metric = clientmetric.NewCounter(m.Name)
|
||||
case "gauge":
|
||||
metric = clientmetric.NewGauge(m.Name)
|
||||
default:
|
||||
http.Error(w, "Unknown metric type "+m.Type, 400)
|
||||
return
|
||||
}
|
||||
metrics[m.Name] = metric
|
||||
metric.Add(int64(m.Value))
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(struct{}{})
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
|
||||
58
jsondb/db.go
Normal file
58
jsondb/db.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package jsondb provides a trivial "database": a Go object saved to
|
||||
// disk as JSON.
|
||||
package jsondb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
)
|
||||
|
||||
// DB is a database backed by a JSON file.
|
||||
type DB[T any] struct {
|
||||
// Data is the contents of the database.
|
||||
Data *T
|
||||
|
||||
path string
|
||||
}
|
||||
|
||||
// Open opens the database at path, creating it with a zero value if
|
||||
// necessary.
|
||||
func Open[T any](path string) (*DB[T], error) {
|
||||
bs, err := os.ReadFile(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return &DB[T]{
|
||||
Data: new(T),
|
||||
path: path,
|
||||
}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var val T
|
||||
if err := json.Unmarshal(bs, &val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DB[T]{
|
||||
Data: &val,
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Save writes db.Data back to disk.
|
||||
func (db *DB[T]) Save() error {
|
||||
bs, err := json.Marshal(db.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return atomicfile.WriteFile(db.path, bs, 0600)
|
||||
}
|
||||
56
jsondb/db_test.go
Normal file
56
jsondb/db_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsondb
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDB(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "db-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
path := filepath.Join(dir, "db.json")
|
||||
db, err := Open[testDB](path)
|
||||
if err != nil {
|
||||
t.Fatalf("creating empty DB: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(db.Data, &testDB{}, cmp.AllowUnexported(testDB{})); diff != "" {
|
||||
t.Fatalf("unexpected empty DB content (-got+want):\n%s", diff)
|
||||
}
|
||||
db.Data.MyString = "test"
|
||||
db.Data.unexported = "don't keep"
|
||||
db.Data.AnInt = 42
|
||||
if err := db.Save(); err != nil {
|
||||
t.Fatalf("saving database: %v", err)
|
||||
}
|
||||
|
||||
db2, err := Open[testDB](path)
|
||||
if err != nil {
|
||||
log.Fatalf("opening DB again: %v", err)
|
||||
}
|
||||
want := &testDB{
|
||||
MyString: "test",
|
||||
AnInt: 42,
|
||||
}
|
||||
if diff := cmp.Diff(db2.Data, want, cmp.AllowUnexported(testDB{})); diff != "" {
|
||||
t.Fatalf("unexpected saved DB content (-got+want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
type testDB struct {
|
||||
MyString string
|
||||
unexported string
|
||||
AnInt int64
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// Package logheap logs a heap pprof profile.
|
||||
package logheap
|
||||
|
||||
|
||||
8
log/logheap/logheap_js.go
Normal file
8
log/logheap/logheap_js.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package logheap
|
||||
|
||||
func LogHeap(postURL string) {
|
||||
}
|
||||
@@ -95,6 +95,35 @@ type Policy struct {
|
||||
PublicID logtail.PublicID
|
||||
}
|
||||
|
||||
// NewConfig creates a Config with collection and a newly generated PrivateID.
|
||||
func NewConfig(collection string) *Config {
|
||||
id, err := logtail.NewPrivateID()
|
||||
if err != nil {
|
||||
panic("logtail.NewPrivateID should never fail")
|
||||
}
|
||||
return &Config{
|
||||
Collection: collection,
|
||||
PrivateID: id,
|
||||
PublicID: id.Public(),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate verifies that the Config matches the collection,
|
||||
// and that the PrivateID and PublicID pair are sensible.
|
||||
func (c *Config) Validate(collection string) error {
|
||||
switch {
|
||||
case c == nil:
|
||||
return errors.New("config is nil")
|
||||
case c.Collection != collection:
|
||||
return fmt.Errorf("config collection %q does not match %q", c.Collection, collection)
|
||||
case c.PrivateID.IsZero():
|
||||
return errors.New("config has zero PrivateID")
|
||||
case c.PrivateID.Public() != c.PublicID:
|
||||
return errors.New("config PrivateID does not match PublicID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToBytes returns the JSON representation of c.
|
||||
func (c *Config) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(c, "", "\t")
|
||||
@@ -105,7 +134,7 @@ func (c *Config) ToBytes() []byte {
|
||||
}
|
||||
|
||||
// Save writes the JSON representation of c to stateFile.
|
||||
func (c *Config) save(stateFile string) error {
|
||||
func (c *Config) Save(stateFile string) error {
|
||||
c.PublicID = c.PrivateID.Public()
|
||||
if err := os.MkdirAll(filepath.Dir(stateFile), 0750); err != nil {
|
||||
return err
|
||||
@@ -117,7 +146,16 @@ func (c *Config) save(stateFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigFromBytes parses a a Config from its JSON encoding.
|
||||
// ConfigFromFile reads a Config from a JSON file.
|
||||
func ConfigFromFile(statefile string) (*Config, error) {
|
||||
b, err := os.ReadFile(statefile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ConfigFromBytes(b)
|
||||
}
|
||||
|
||||
// ConfigFromBytes parses a Config from its JSON encoding.
|
||||
func ConfigFromBytes(jsonEnc []byte) (*Config, error) {
|
||||
c := &Config{}
|
||||
if err := json.Unmarshal(jsonEnc, c); err != nil {
|
||||
@@ -470,38 +508,15 @@ func New(collection string) *Policy {
|
||||
}
|
||||
}
|
||||
|
||||
var oldc *Config
|
||||
data, err := ioutil.ReadFile(cfgPath)
|
||||
newc, err := ConfigFromFile(cfgPath)
|
||||
if err != nil {
|
||||
earlyLogf("logpolicy.Read %v: %v", cfgPath, err)
|
||||
oldc = &Config{}
|
||||
oldc.Collection = collection
|
||||
} else {
|
||||
oldc, err = ConfigFromBytes(data)
|
||||
if err != nil {
|
||||
earlyLogf("logpolicy.Config unmarshal: %v", err)
|
||||
oldc = &Config{}
|
||||
}
|
||||
earlyLogf("logpolicy.ConfigFromFile %v: %v", cfgPath, err)
|
||||
}
|
||||
|
||||
newc := *oldc
|
||||
if newc.Collection != collection {
|
||||
log.Printf("logpolicy.Config: config collection %q does not match %q", newc.Collection, collection)
|
||||
// We picked up an incompatible config file.
|
||||
// Regenerate the private ID.
|
||||
newc.PrivateID = logtail.PrivateID{}
|
||||
newc.Collection = collection
|
||||
}
|
||||
if newc.PrivateID.IsZero() {
|
||||
newc.PrivateID, err = logtail.NewPrivateID()
|
||||
if err != nil {
|
||||
log.Fatalf("logpolicy: NewPrivateID() should never fail")
|
||||
}
|
||||
}
|
||||
newc.PublicID = newc.PrivateID.Public()
|
||||
if newc != *oldc {
|
||||
if err := newc.save(cfgPath); err != nil {
|
||||
earlyLogf("logpolicy.Config.Save: %v", err)
|
||||
if err := newc.Validate(collection); err != nil {
|
||||
earlyLogf("logpolicy.Config.Validate for %v: %v", cfgPath, err)
|
||||
newc = NewConfig(collection)
|
||||
if err := newc.Save(cfgPath); err != nil {
|
||||
earlyLogf("logpolicy.Config.Save for %v: %v", cfgPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,10 +80,6 @@ func (id PrivateID) String() string {
|
||||
}
|
||||
|
||||
func (id PrivateID) Public() (pub PublicID) {
|
||||
var emptyID PrivateID
|
||||
if id == emptyID {
|
||||
panic("invalid logtail.Public() on an empty private ID")
|
||||
}
|
||||
h := sha256.New()
|
||||
h.Write(id[:])
|
||||
if n := copy(pub[:], h.Sum(pub[:0])); n != len(pub) {
|
||||
|
||||
@@ -20,27 +20,12 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
||||
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
|
||||
|
||||
nrptBase = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\`
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
|
||||
// This is the legacy rule ID that previous versions used when we supported
|
||||
// only a single rule. Now that we support multiple rules are required, we
|
||||
// generate their GUIDs and store them under the Tailscale registry key.
|
||||
nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
// Apparently NRPT rules cannot handle > 50 domains.
|
||||
nrptMaxDomainsPerRule = 50
|
||||
|
||||
// This is the name of the registry value we use to save Rule IDs under
|
||||
// the Tailscale registry key.
|
||||
nrptRuleIDValueName = `NRPTRuleIDs`
|
||||
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
@@ -49,35 +34,19 @@ var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
nrptWorks bool
|
||||
nrptDB *nrptRuleDatabase
|
||||
wslManager *wslManager
|
||||
}
|
||||
|
||||
func loadRuleSubkeyNames() []string {
|
||||
result := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if result == nil {
|
||||
// Use the legacy rule ID if none are specified in our registry key
|
||||
result = []string{nrptSingleRuleID}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
ret := windowsManager{
|
||||
logf: logf,
|
||||
guid: interfaceName,
|
||||
nrptWorks: isWindows10OrBetter(),
|
||||
wslManager: newWSLManager(logf),
|
||||
}
|
||||
|
||||
// Best-effort: if our NRPT rule exists, try to delete it. Unlike
|
||||
// per-interface configuration, NRPT rules survive the unclean
|
||||
// termination of the Tailscale process, and depending on the
|
||||
// rule, it may prevent us from reaching login.tailscale.com to
|
||||
// boot up. The bootstrap resolver logic will save us, but it
|
||||
// slows down start-up a bunch.
|
||||
if ret.nrptWorks {
|
||||
ret.delAllRuleKeys()
|
||||
if isWindows10OrBetter() {
|
||||
ret.nrptDB = newNRPTRuleDatabase(logf)
|
||||
}
|
||||
|
||||
// Log WSL status once at startup.
|
||||
@@ -108,29 +77,6 @@ func (m windowsManager) ifPath(basePath string) string {
|
||||
return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
}
|
||||
|
||||
func (m windowsManager) delAllRuleKeys() error {
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
if err := m.delRuleKeys(nrptRuleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
m.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) delRuleKeys(nrptRuleIDs []string) error {
|
||||
for _, rid := range nrptRuleIDs {
|
||||
keyName := nrptBase + rid
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyName); err != nil && err != registry.ErrNotExist {
|
||||
m.logf("Error deleting NRPT rule key %q: %v", keyName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func delValue(key registry.Key, name string) error {
|
||||
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
||||
return err
|
||||
@@ -144,8 +90,17 @@ func delValue(key registry.Key, name string) error {
|
||||
//
|
||||
// If no resolvers are provided, the Tailscale NRPT rules are deleted.
|
||||
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
|
||||
if m.nrptDB == nil {
|
||||
if resolvers == nil {
|
||||
// Just a no-op in this case.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Split DNS unsupported on this Windows version")
|
||||
}
|
||||
|
||||
defer m.nrptDB.Refresh()
|
||||
if len(resolvers) == 0 {
|
||||
return m.delAllRuleKeys()
|
||||
return m.nrptDB.DelAllRuleKeys()
|
||||
}
|
||||
|
||||
servers := make([]string, 0, len(resolvers))
|
||||
@@ -153,92 +108,7 @@ func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQ
|
||||
servers = append(servers, resolver.String())
|
||||
}
|
||||
|
||||
// NRPT has an undocumented restriction that each rule may only be associated
|
||||
// with a maximum of 50 domains. If we are setting rules for more domains
|
||||
// than that, we need to split domains into chunks and write out a rule per chunk.
|
||||
dq := len(domains) / nrptMaxDomainsPerRule
|
||||
dr := len(domains) % nrptMaxDomainsPerRule
|
||||
|
||||
domainRulesLen := dq
|
||||
if dr > 0 {
|
||||
domainRulesLen++
|
||||
}
|
||||
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
for len(nrptRuleIDs) < domainRulesLen {
|
||||
guid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nrptRuleIDs = append(nrptRuleIDs, guid.String())
|
||||
}
|
||||
|
||||
// Remove any surplus rules that are no longer needed.
|
||||
ruleIDsToRemove := nrptRuleIDs[domainRulesLen:]
|
||||
m.delRuleKeys(ruleIDsToRemove)
|
||||
|
||||
// We need to save the list of rule IDs to our Tailscale registry key so that
|
||||
// we know which rules are ours during subsequent modifications to NRPT rules.
|
||||
ruleIDsToWrite := nrptRuleIDs[:domainRulesLen]
|
||||
if len(ruleIDsToWrite) > 0 {
|
||||
if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
doms := make([]string, 0, nrptMaxDomainsPerRule)
|
||||
for i := 0; i < domainRulesLen; i++ {
|
||||
// Each iteration consumes nrptMaxDomainsPerRule domains...
|
||||
curLen := nrptMaxDomainsPerRule
|
||||
// Except for the final iteration: when we have a remainder, use that instead.
|
||||
if i == domainRulesLen-1 && dr > 0 {
|
||||
curLen = dr
|
||||
}
|
||||
|
||||
// Obtain the slice of domains to consume within the current iteration.
|
||||
start := i * nrptMaxDomainsPerRule
|
||||
end := start + curLen
|
||||
for _, domain := range domains[start:end] {
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if err := writeNRPTRule(nrptRuleIDs[i], doms, servers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doms = doms[:0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeNRPTRule(ruleID string, doms, servers []string) error {
|
||||
// CreateKey is actually open-or-create, which suits us fine.
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase+ruleID, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", nrptBase+ruleID, err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetDWordValue("Version", 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringsValue("Name", doms); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringValue("GenericDNSServers", strings.Join(servers, "; ")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.nrptDB.WriteSplitDNSConfig(servers, domains)
|
||||
}
|
||||
|
||||
// setPrimaryDNS sets the given resolvers and domains as the Tailscale
|
||||
@@ -345,6 +215,11 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
// configuration only, routing one set of things to the "split"
|
||||
// resolver and the rest to the primary.
|
||||
|
||||
// Unconditionally disable dynamic DNS updates on our interfaces.
|
||||
if err := m.disableDynamicUpdates(); err != nil {
|
||||
m.logf("disableDynamicUpdates error: %v\n", err)
|
||||
}
|
||||
|
||||
if len(cfg.MatchDomains) == 0 {
|
||||
if err := m.setSplitDNS(nil, nil); err != nil {
|
||||
return err
|
||||
@@ -352,7 +227,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !m.nrptWorks {
|
||||
} else if m.nrptDB == nil {
|
||||
return errors.New("cannot set per-domain resolvers on Windows 7")
|
||||
} else {
|
||||
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
|
||||
@@ -418,13 +293,36 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
}
|
||||
|
||||
func (m windowsManager) SupportsSplitDNS() bool {
|
||||
return m.nrptWorks
|
||||
return m.nrptDB != nil
|
||||
}
|
||||
|
||||
func (m windowsManager) Close() error {
|
||||
return m.SetDNS(OSConfig{})
|
||||
}
|
||||
|
||||
// disableDynamicUpdates sets the appropriate registry values to prevent the
|
||||
// Windows DHCP client from sending dynamic DNS updates for our interface to
|
||||
// AD domain controllers.
|
||||
func (m windowsManager) disableDynamicUpdates() error {
|
||||
setRegValue := func(regBase string) error {
|
||||
key, err := m.openKey(m.ifPath(regBase))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
return key.SetDWordValue("DisableDynamicUpdate", 1)
|
||||
}
|
||||
|
||||
for _, regBase := range []string{ipv4RegBase, ipv6RegBase} {
|
||||
if err := setRegValue(regBase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) GetBaseConfig() (OSConfig, error) {
|
||||
resolvers, err := m.getBasePrimaryResolver()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -17,11 +18,62 @@ import (
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
func TestManagerWindows(t *testing.T) {
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
t.Skipf("test requires running as elevated user")
|
||||
const testGPRuleID = "{7B1B6151-84E6-41A3-8967-62F7F7B45687}"
|
||||
|
||||
var (
|
||||
procRegisterGPNotification = libUserenv.NewProc("RegisterGPNotification")
|
||||
procUnregisterGPNotification = libUserenv.NewProc("UnregisterGPNotification")
|
||||
)
|
||||
|
||||
func TestManagerWindowsLocal(t *testing.T) {
|
||||
if !isWindows10OrBetter() || !winutil.IsCurrentProcessElevated() {
|
||||
t.Skipf("test requires running as elevated user on Windows 10+")
|
||||
}
|
||||
|
||||
runTest(t, true)
|
||||
}
|
||||
|
||||
func TestManagerWindowsGP(t *testing.T) {
|
||||
if !isWindows10OrBetter() || !winutil.IsCurrentProcessElevated() {
|
||||
t.Skipf("test requires running as elevated user on Windows 10+")
|
||||
}
|
||||
|
||||
checkGPNotificationsWork(t)
|
||||
|
||||
// Make sure group policy is refreshed before this test exits but after we've
|
||||
// cleaned everything else up.
|
||||
defer procRefreshPolicyEx.Call(uintptr(1), uintptr(_RP_FORCE))
|
||||
|
||||
err := createFakeGPKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Creating fake GP key: %v\n", err)
|
||||
}
|
||||
defer deleteFakeGPKey(t)
|
||||
|
||||
runTest(t, false)
|
||||
}
|
||||
|
||||
func checkGPNotificationsWork(t *testing.T) {
|
||||
// Test to ensure that RegisterGPNotification work on this machine,
|
||||
// otherwise this test will fail.
|
||||
trk, err := newGPNotificationTracker()
|
||||
if err != nil {
|
||||
t.Skipf("newGPNotificationTracker error: %v\n", err)
|
||||
}
|
||||
defer trk.Close()
|
||||
|
||||
r, _, err := procRefreshPolicyEx.Call(uintptr(1), uintptr(_RP_FORCE))
|
||||
if r == 0 {
|
||||
t.Fatalf("RefreshPolicyEx error: %v\n", err)
|
||||
}
|
||||
|
||||
timeout := uint32(10000) // Milliseconds
|
||||
if !trk.DidRefreshTimeout(timeout) {
|
||||
t.Skipf("GP notifications are not working on this machine\n")
|
||||
}
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, isLocal bool) {
|
||||
logf := func(format string, args ...any) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
@@ -37,6 +89,11 @@ func TestManagerWindows(t *testing.T) {
|
||||
}
|
||||
mgr := cfg.(windowsManager)
|
||||
|
||||
usingGP := mgr.nrptDB.writeAsGP
|
||||
if isLocal == usingGP {
|
||||
t.Fatalf("usingGP %v, want %v\n", usingGP, !usingGP)
|
||||
}
|
||||
|
||||
// Upon initialization of cfg, we should not have any NRPT rules
|
||||
ensureNoRules(t)
|
||||
|
||||
@@ -74,14 +131,61 @@ func TestManagerWindows(t *testing.T) {
|
||||
51,
|
||||
}
|
||||
|
||||
for _, n := range cases {
|
||||
var regBaseValidate string
|
||||
var regBaseEnsure string
|
||||
if isLocal {
|
||||
regBaseValidate = nrptBaseLocal
|
||||
regBaseEnsure = nrptBaseGP
|
||||
} else {
|
||||
regBaseValidate = nrptBaseGP
|
||||
regBaseEnsure = nrptBaseLocal
|
||||
}
|
||||
|
||||
var trk *gpNotificationTracker
|
||||
if isLocal {
|
||||
// (dblohm7) When isLocal == true, we keep trk active through the entire
|
||||
// sequence of test cases, and then we verify that no policy notifications
|
||||
// occurred. Because policy notifications are scoped to the entire computer,
|
||||
// this check could potentially fail if another process concurrently modifies
|
||||
// group policies while this test is running. I don't expect this to be an
|
||||
// issue on any computer on which we run this test, but something to keep in
|
||||
// mind if we start seeing flakiness around these GP notifications.
|
||||
trk, err = newGPNotificationTracker()
|
||||
if err != nil {
|
||||
t.Fatalf("newGPNotificationTracker: %v\n", err)
|
||||
}
|
||||
defer trk.Close()
|
||||
}
|
||||
|
||||
runCase := func(n int) {
|
||||
t.Logf("Test case: %d domains\n", n)
|
||||
if !isLocal {
|
||||
// When !isLocal, we want to check that a GP notification occured for
|
||||
// every single test case.
|
||||
trk, err = newGPNotificationTracker()
|
||||
if err != nil {
|
||||
t.Fatalf("newGPNotificationTracker: %v\n", err)
|
||||
}
|
||||
defer trk.Close()
|
||||
}
|
||||
caseDomains := domains[:n]
|
||||
err := mgr.setSplitDNS(resolvers, caseDomains)
|
||||
err = mgr.setSplitDNS(resolvers, caseDomains)
|
||||
if err != nil {
|
||||
t.Fatalf("setSplitDNS: %v\n", err)
|
||||
}
|
||||
validateRegistry(t, caseDomains)
|
||||
validateRegistry(t, regBaseValidate, caseDomains)
|
||||
ensureNoRulesInSubkey(t, regBaseEnsure)
|
||||
if !isLocal && !trk.DidRefresh(true) {
|
||||
t.Fatalf("DidRefresh false, want true\n")
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range cases {
|
||||
runCase(n)
|
||||
}
|
||||
|
||||
if isLocal && trk.DidRefresh(false) {
|
||||
t.Errorf("DidRefresh true, want false\n")
|
||||
}
|
||||
|
||||
t.Logf("Test case: nil resolver\n")
|
||||
@@ -92,23 +196,92 @@ func TestManagerWindows(t *testing.T) {
|
||||
ensureNoRules(t)
|
||||
}
|
||||
|
||||
func createFakeGPKey() error {
|
||||
keyStr := nrptBaseGP + `\` + testGPRuleID
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, keyStr, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", keyStr, err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetDWordValue("Version", 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringsValue("Name", []string{"._setbygp_.example.com"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringValue("GenericDNSServers", "1.1.1.1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteFakeGPKey(t *testing.T) {
|
||||
keyName := nrptBaseGP + `\` + testGPRuleID
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyName); err != nil && err != registry.ErrNotExist {
|
||||
t.Fatalf("Error deleting NRPT rule key %q: %v\n", keyName, err)
|
||||
}
|
||||
|
||||
isEmpty, err := isPolicyConfigSubkeyEmpty()
|
||||
if err != nil {
|
||||
t.Fatalf("isPolicyConfigSubkeyEmpty: %v", err)
|
||||
}
|
||||
|
||||
if !isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseGP); err != nil {
|
||||
t.Fatalf("Deleting DnsPolicyKey Subkey: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureNoRules(t *testing.T) {
|
||||
ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if ruleIDs != nil {
|
||||
t.Errorf("%s: %v, want nil\n", nrptRuleIDValueName, ruleIDs)
|
||||
}
|
||||
|
||||
legacyKeyPath := nrptBase + nrptSingleRuleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, legacyKeyPath, registry.READ)
|
||||
for _, base := range []string{nrptBaseLocal, nrptBaseGP} {
|
||||
ensureNoSingleRule(t, base)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureNoRulesInSubkey(t *testing.T, base string) {
|
||||
ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if ruleIDs == nil {
|
||||
for _, base := range []string{nrptBaseLocal, nrptBaseGP} {
|
||||
ensureNoSingleRule(t, base)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, ruleID := range ruleIDs {
|
||||
keyName := base + `\` + ruleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyName, registry.READ)
|
||||
if err == nil {
|
||||
key.Close()
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Fatalf("%s: %q, want %q\n", keyName, err, registry.ErrNotExist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureNoSingleRule(t *testing.T, base string) {
|
||||
singleKeyPath := base + `\` + nrptSingleRuleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, singleKeyPath, registry.READ)
|
||||
if err == nil {
|
||||
key.Close()
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Errorf("%s: %q, want %q\n", legacyKeyPath, err, registry.ErrNotExist)
|
||||
t.Fatalf("%s: %q, want %q\n", singleKeyPath, err, registry.ErrNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
func validateRegistry(t *testing.T, domains []dnsname.FQDN) {
|
||||
func validateRegistry(t *testing.T, nrptBase string, domains []dnsname.FQDN) {
|
||||
q := len(domains) / nrptMaxDomainsPerRule
|
||||
r := len(domains) % nrptMaxDomainsPerRule
|
||||
numRules := q
|
||||
@@ -124,9 +297,9 @@ func validateRegistry(t *testing.T, domains []dnsname.FQDN) {
|
||||
}
|
||||
|
||||
for i, ruleID := range ruleIDs {
|
||||
savedDomains, err := getSavedDomainsForRule(ruleID)
|
||||
savedDomains, err := getSavedDomainsForRule(nrptBase, ruleID)
|
||||
if err != nil {
|
||||
t.Fatalf("getSavedDomainsForRule(%q): %v\n", ruleID, err)
|
||||
t.Fatalf("getSavedDomainsForRule(%q, %q): %v\n", nrptBase, ruleID, err)
|
||||
}
|
||||
|
||||
start := i * nrptMaxDomainsPerRule
|
||||
@@ -148,8 +321,8 @@ func validateRegistry(t *testing.T, domains []dnsname.FQDN) {
|
||||
}
|
||||
}
|
||||
|
||||
func getSavedDomainsForRule(ruleID string) ([]string, error) {
|
||||
keyPath := nrptBase + ruleID
|
||||
func getSavedDomainsForRule(base, ruleID string) ([]string, error) {
|
||||
keyPath := base + `\` + ruleID
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.READ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -158,3 +331,56 @@ func getSavedDomainsForRule(ruleID string) ([]string, error) {
|
||||
result, _, err := key.GetStringsValue("Name")
|
||||
return result, err
|
||||
}
|
||||
|
||||
// gpNotificationTracker registers with the Windows policy engine and receives
|
||||
// notifications when policy refreshes occur.
|
||||
type gpNotificationTracker struct {
|
||||
event windows.Handle
|
||||
}
|
||||
|
||||
func newGPNotificationTracker() (*gpNotificationTracker, error) {
|
||||
var err error
|
||||
evt, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(evt)
|
||||
}
|
||||
}()
|
||||
|
||||
ok, _, e := procRegisterGPNotification.Call(
|
||||
uintptr(evt),
|
||||
uintptr(1), // We want computer policy changes, not user policy changes.
|
||||
)
|
||||
if ok == 0 {
|
||||
err = e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gpNotificationTracker{evt}, nil
|
||||
}
|
||||
|
||||
func (trk *gpNotificationTracker) DidRefresh(isExpected bool) bool {
|
||||
// If we're not expecting a refresh event, then we need to use a timeout.
|
||||
timeout := uint32(1000) // 1 second (in milliseconds)
|
||||
if isExpected {
|
||||
// Otherwise, since it is imperative that we see an event, we wait infinitely.
|
||||
timeout = windows.INFINITE
|
||||
}
|
||||
|
||||
return trk.DidRefreshTimeout(timeout)
|
||||
}
|
||||
|
||||
func (trk *gpNotificationTracker) DidRefreshTimeout(timeout uint32) bool {
|
||||
waitCode, _ := windows.WaitForSingleObject(trk.event, timeout)
|
||||
return waitCode == windows.WAIT_OBJECT_0
|
||||
}
|
||||
|
||||
func (trk *gpNotificationTracker) Close() error {
|
||||
procUnregisterGPNotification.Call(uintptr(trk.event))
|
||||
windows.CloseHandle(trk.event)
|
||||
trk.event = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
331
net/dns/nrpt_windows.go
Normal file
331
net/dns/nrpt_windows.go
Normal file
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient`
|
||||
nrptBaseLocal = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig`
|
||||
nrptBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig`
|
||||
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
|
||||
// Apparently NRPT rules cannot handle > 50 domains.
|
||||
nrptMaxDomainsPerRule = 50
|
||||
|
||||
// This is the legacy rule ID that previous versions used when we supported
|
||||
// only a single rule. Now that we support multiple rules are required, we
|
||||
// generate their GUIDs and store them under the Tailscale registry key.
|
||||
nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
|
||||
// This is the name of the registry value we use to save Rule IDs under
|
||||
// the Tailscale registry key.
|
||||
nrptRuleIDValueName = `NRPTRuleIDs`
|
||||
)
|
||||
|
||||
var (
|
||||
libUserenv = windows.NewLazySystemDLL("userenv.dll")
|
||||
procRefreshPolicyEx = libUserenv.NewProc("RefreshPolicyEx")
|
||||
)
|
||||
|
||||
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
|
||||
|
||||
// nrptRuleDatabase ensapsulates access to the Windows Name Resolution Policy
|
||||
// Table (NRPT).
|
||||
type nrptRuleDatabase struct {
|
||||
logf logger.Logf
|
||||
ruleIDs []string
|
||||
writeAsGP bool
|
||||
isGPDirty bool
|
||||
}
|
||||
|
||||
func newNRPTRuleDatabase(logf logger.Logf) *nrptRuleDatabase {
|
||||
ret := &nrptRuleDatabase{logf: logf}
|
||||
ret.loadRuleSubkeyNames()
|
||||
ret.initWriteAsGP()
|
||||
logf("nrptRuleDatabase using group policy: %v\n", ret.writeAsGP)
|
||||
// Best-effort: if our NRPT rule exists, try to delete it. Unlike
|
||||
// per-interface configuration, NRPT rules survive the unclean
|
||||
// termination of the Tailscale process, and depending on the
|
||||
// rule, it may prevent us from reaching login.tailscale.com to
|
||||
// boot up. The bootstrap resolver logic will save us, but it
|
||||
// slows down start-up a bunch.
|
||||
ret.DelAllRuleKeys()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) loadRuleSubkeyNames() {
|
||||
result := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if result == nil {
|
||||
// Use the legacy rule ID if none are specified in our registry key
|
||||
result = []string{nrptSingleRuleID}
|
||||
}
|
||||
db.ruleIDs = result
|
||||
}
|
||||
|
||||
// initWriteAsGP determines which registry path should be used for writing
|
||||
// NRPT rules. If there are rules in the GP path that don't belong to us, then
|
||||
// we should use the GP path.
|
||||
func (db *nrptRuleDatabase) initWriteAsGP() {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
db.writeAsGP = false
|
||||
}
|
||||
}()
|
||||
|
||||
dnsKey, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
db.logf("Failed to open key %q with error: %v\n", dnsBaseGP, err)
|
||||
return
|
||||
}
|
||||
defer dnsKey.Close()
|
||||
|
||||
ki, err := dnsKey.Stat()
|
||||
if err != nil {
|
||||
db.logf("Failed to stat key %q with error: %v\n", dnsBaseGP, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If the dnsKey contains any values, then we need to use the GP key.
|
||||
if ki.ValueCount > 0 {
|
||||
db.writeAsGP = true
|
||||
return
|
||||
}
|
||||
|
||||
if ki.SubKeyCount == 0 {
|
||||
// If dnsKey contains no values and no subkeys, then we definitely don't
|
||||
// need to use the GP key.
|
||||
db.writeAsGP = false
|
||||
return
|
||||
}
|
||||
|
||||
// Get a list of all the NRPT rules under the GP subkey.
|
||||
nrptKey, err := registry.OpenKey(registry.LOCAL_MACHINE, nrptBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
db.logf("Failed to open key %q with error: %v\n", nrptBaseGP, err)
|
||||
return
|
||||
}
|
||||
defer nrptKey.Close()
|
||||
|
||||
gpSubkeyNames, err := nrptKey.ReadSubKeyNames(0)
|
||||
if err != nil {
|
||||
db.logf("Failed to list subkeys under %q with error: %v\n", nrptBaseGP, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add *all* rules from the GP subkey into a set.
|
||||
gpSubkeyMap := make(map[string]struct{}, len(gpSubkeyNames))
|
||||
for _, gpSubkey := range gpSubkeyNames {
|
||||
gpSubkeyMap[strings.ToUpper(gpSubkey)] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove *our* rules from the set.
|
||||
for _, ourRuleID := range db.ruleIDs {
|
||||
delete(gpSubkeyMap, strings.ToUpper(ourRuleID))
|
||||
}
|
||||
|
||||
// Any leftover rules do not belong to us. When group policy is being used
|
||||
// by something else, we must also use the GP path.
|
||||
db.writeAsGP = len(gpSubkeyMap) > 0
|
||||
}
|
||||
|
||||
// DelAllRuleKeys removes any and all NRPT rules that are owned by Tailscale.
|
||||
func (db *nrptRuleDatabase) DelAllRuleKeys() error {
|
||||
if err := db.delRuleKeys(db.ruleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
db.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err)
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// delRuleKeys removes the NRPT rules specified by nrptRuleIDs from the
|
||||
// Windows registry. It attempts to remove the rules from both possible registry
|
||||
// keys: the local key and the group policy key.
|
||||
func (db *nrptRuleDatabase) delRuleKeys(nrptRuleIDs []string) error {
|
||||
for _, rid := range nrptRuleIDs {
|
||||
keyNameLocal := nrptBaseLocal + `\` + rid
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyNameLocal); err != nil && err != registry.ErrNotExist {
|
||||
db.logf("Error deleting NRPT rule key %q: %v", keyNameLocal, err)
|
||||
return err
|
||||
}
|
||||
|
||||
keyNameGP := nrptBaseGP + `\` + rid
|
||||
err := registry.DeleteKey(registry.LOCAL_MACHINE, keyNameGP)
|
||||
if err == nil {
|
||||
// If this deleted subkey existed under the GP key, we will need to refresh.
|
||||
db.isGPDirty = true
|
||||
} else if err != registry.ErrNotExist {
|
||||
db.logf("Error deleting NRPT rule key %q: %v", keyNameGP, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !db.isGPDirty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we've removed keys from the Group Policy subkey, and the DNSPolicyConfig
|
||||
// subkey is now empty, we need to remove that subkey.
|
||||
isEmpty, err := isPolicyConfigSubkeyEmpty()
|
||||
if err != nil || !isEmpty {
|
||||
return err
|
||||
}
|
||||
|
||||
return registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseGP)
|
||||
}
|
||||
|
||||
// isPolicyConfigSubkeyEmpty returns true if and only if the nrptBaseGP exists
|
||||
// and does not contain any values or subkeys.
|
||||
func isPolicyConfigSubkeyEmpty() (bool, error) {
|
||||
subKey, err := registry.OpenKey(registry.LOCAL_MACHINE, nrptBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
defer subKey.Close()
|
||||
|
||||
ki, err := subKey.Stat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (ki.ValueCount == 0 && ki.SubKeyCount == 0), nil
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsname.FQDN) error {
|
||||
// NRPT has an undocumented restriction that each rule may only be associated
|
||||
// with a maximum of 50 domains. If we are setting rules for more domains
|
||||
// than that, we need to split domains into chunks and write out a rule per chunk.
|
||||
dq := len(domains) / nrptMaxDomainsPerRule
|
||||
dr := len(domains) % nrptMaxDomainsPerRule
|
||||
|
||||
domainRulesLen := dq
|
||||
if dr > 0 {
|
||||
domainRulesLen++
|
||||
}
|
||||
|
||||
db.loadRuleSubkeyNames()
|
||||
for len(db.ruleIDs) < domainRulesLen {
|
||||
guid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = append(db.ruleIDs, guid.String())
|
||||
}
|
||||
|
||||
// Remove any surplus rules that are no longer needed.
|
||||
ruleIDsToRemove := db.ruleIDs[domainRulesLen:]
|
||||
db.delRuleKeys(ruleIDsToRemove)
|
||||
|
||||
// We need to save the list of rule IDs to our Tailscale registry key so that
|
||||
// we know which rules are ours during subsequent modifications to NRPT rules.
|
||||
ruleIDsToWrite := db.ruleIDs[:domainRulesLen]
|
||||
if len(ruleIDsToWrite) == 0 {
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = ruleIDsToWrite
|
||||
|
||||
curRuleID := 0
|
||||
doms := make([]string, 0, nrptMaxDomainsPerRule)
|
||||
|
||||
for _, domain := range domains {
|
||||
if len(doms) == nrptMaxDomainsPerRule {
|
||||
if err := db.writeNRPTRule(db.ruleIDs[curRuleID], servers, doms); err != nil {
|
||||
return err
|
||||
}
|
||||
curRuleID++
|
||||
doms = doms[:0]
|
||||
}
|
||||
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if len(doms) > 0 {
|
||||
if err := db.writeNRPTRule(db.ruleIDs[curRuleID], servers, doms); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh notifies the Windows group policy engine when policies have changed.
|
||||
func (db *nrptRuleDatabase) Refresh() {
|
||||
if !db.isGPDirty {
|
||||
return
|
||||
}
|
||||
ok, _, err := procRefreshPolicyEx.Call(
|
||||
uintptr(1), // Win32 TRUE: Refresh computer policy, not user policy.
|
||||
uintptr(_RP_FORCE),
|
||||
)
|
||||
if ok == 0 {
|
||||
db.logf("RefreshPolicyEx failed: %v", err)
|
||||
return
|
||||
}
|
||||
db.isGPDirty = false
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) writeNRPTRule(ruleID string, servers, doms []string) error {
|
||||
var nrptBase string
|
||||
if db.writeAsGP {
|
||||
nrptBase = nrptBaseGP
|
||||
} else {
|
||||
nrptBase = nrptBaseLocal
|
||||
}
|
||||
|
||||
keyStr := nrptBase + `\` + ruleID
|
||||
|
||||
// CreateKey is actually open-or-create, which suits us fine.
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, keyStr, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", keyStr, err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetDWordValue("Version", 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringsValue("Name", doms); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringValue("GenericDNSServers", strings.Join(servers, "; ")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if db.writeAsGP {
|
||||
db.isGPDirty = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
@@ -197,6 +198,16 @@ type forwarder struct {
|
||||
// routes are per-suffix resolvers to use, with
|
||||
// the most specific routes first.
|
||||
routes []route
|
||||
// cloudHostFallback are last resort resolvers to use if no per-suffix
|
||||
// resolver matches. These are only populated on cloud hosts where the
|
||||
// platform provides a well-known recursive resolver.
|
||||
//
|
||||
// That is, if we're running on GCP or AWS where there's always a well-known
|
||||
// IP of a recursive resolver, return that rather than having callers return
|
||||
// errNoUpstreams. This fixes both normal 100.100.100.100 resolution when
|
||||
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
||||
// resolver lookup.
|
||||
cloudHostFallback []resolverAndDelay
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -296,18 +307,52 @@ func resolversWithDelays(resolvers []*dnstype.Resolver) []resolverAndDelay {
|
||||
return rr
|
||||
}
|
||||
|
||||
var (
|
||||
cloudResolversOnce sync.Once
|
||||
cloudResolversLazy []resolverAndDelay
|
||||
)
|
||||
|
||||
func cloudResolvers() []resolverAndDelay {
|
||||
cloudResolversOnce.Do(func() {
|
||||
if ip := cloudenv.Get().ResolverIP(); ip != "" {
|
||||
cloudResolver := []*dnstype.Resolver{{Addr: ip}}
|
||||
cloudResolversLazy = resolversWithDelays(cloudResolver)
|
||||
}
|
||||
})
|
||||
return cloudResolversLazy
|
||||
}
|
||||
|
||||
// setRoutes sets the routes to use for DNS forwarding. It's called by
|
||||
// Resolver.SetConfig on reconfig.
|
||||
//
|
||||
// The memory referenced by routesBySuffix should not be modified.
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
routes := make([]route, 0, len(routesBySuffix))
|
||||
|
||||
cloudHostFallback := cloudResolvers()
|
||||
for suffix, rs := range routesBySuffix {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: resolversWithDelays(rs),
|
||||
})
|
||||
if suffix == "." && len(rs) == 0 && len(cloudHostFallback) > 0 {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: cloudHostFallback,
|
||||
})
|
||||
} else {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: resolversWithDelays(rs),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if cloudenv.Get().HasInternalTLD() && len(cloudHostFallback) > 0 {
|
||||
if _, ok := routesBySuffix["internal."]; !ok {
|
||||
routes = append(routes, route{
|
||||
Suffix: "internal.",
|
||||
Resolvers: cloudHostFallback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort from longest prefix to shortest.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
||||
@@ -316,6 +361,7 @@ func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolve
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.routes = routes
|
||||
f.cloudHostFallback = cloudHostFallback
|
||||
}
|
||||
|
||||
var stdNetPacketListener packetListener = new(net.ListenConfig)
|
||||
@@ -472,6 +518,8 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
return f.sendUDP(ctx, fq, rr)
|
||||
}
|
||||
|
||||
var errServerFailure = errors.New("response code indicates server issue")
|
||||
|
||||
func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
|
||||
ipp, ok := rr.name.IPPort()
|
||||
if !ok {
|
||||
@@ -535,7 +583,7 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
if rcode == dns.RCodeServerFailure {
|
||||
f.logf("recv: response code indicating server failure: %d", rcode)
|
||||
metricDNSFwdUDPErrorServer.Add(1)
|
||||
return nil, errors.New("response code indicates server issue")
|
||||
return nil, errServerFailure
|
||||
}
|
||||
|
||||
if truncated {
|
||||
@@ -564,13 +612,14 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
cloudHostFallback := f.cloudHostFallback
|
||||
f.mu.Unlock()
|
||||
for _, route := range routes {
|
||||
if route.Suffix == "." || route.Suffix.Contains(domain) {
|
||||
return route.Resolvers
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return cloudHostFallback // or nil if no fallback
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
@@ -704,6 +753,20 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
}
|
||||
numErr++
|
||||
if numErr == len(resolvers) {
|
||||
if firstErr == errServerFailure {
|
||||
res, err := servfailResponse(query)
|
||||
if err != nil {
|
||||
f.logf("building servfail response: %v", err)
|
||||
return firstErr
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
metricDNSFwdErrorContextGotError.Add(1)
|
||||
case responseChan <- res:
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
case <-ctx.Done():
|
||||
@@ -762,6 +825,27 @@ func nxDomainResponse(req packet) (res packet, err error) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// servfailResponse returns a SERVFAIL error reply for the provided request.
|
||||
func servfailResponse(req packet) (res packet, err error) {
|
||||
p := dnsParserPool.Get().(*dnsParser)
|
||||
defer dnsParserPool.Put(p)
|
||||
|
||||
if err := p.parseQuery(req.bs); err != nil {
|
||||
return packet{}, err
|
||||
}
|
||||
|
||||
h := p.Header
|
||||
h.Response = true
|
||||
h.Authoritative = true
|
||||
h.RCode = dns.RCodeServerFailure
|
||||
b := dns.NewBuilder(nil, h)
|
||||
b.StartQuestions()
|
||||
b.Question(p.Question)
|
||||
res.bs, err = b.Finish()
|
||||
res.addr = req.addr
|
||||
return res, err
|
||||
}
|
||||
|
||||
// closePool is a dynamic set of io.Closers to close as a group.
|
||||
// It's intended to be Closed at most once.
|
||||
//
|
||||
|
||||
@@ -201,3 +201,61 @@ func BenchmarkNameFromQuery(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduces https://github.com/tailscale/tailscale/issues/2533
|
||||
// Fixed by https://github.com/tailscale/tailscale/commit/f414a9cc01f3264912513d07c0244ff4f3e4ba54
|
||||
//
|
||||
// NOTE: fuzz tests act like unit tests when run without `-fuzz`
|
||||
func FuzzClampEDNSSize(f *testing.F) {
|
||||
// Empty DNS packet
|
||||
f.Add([]byte{
|
||||
// query id
|
||||
0x12, 0x34,
|
||||
// flags: standard query, recurse
|
||||
0x01, 0x20,
|
||||
// num questions
|
||||
0x00, 0x00,
|
||||
// num answers
|
||||
0x00, 0x00,
|
||||
// num authority RRs
|
||||
0x00, 0x00,
|
||||
// num additional RRs
|
||||
0x00, 0x00,
|
||||
})
|
||||
|
||||
// Empty OPT
|
||||
f.Add([]byte{
|
||||
// header
|
||||
0xaf, 0x66, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01,
|
||||
// query
|
||||
0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f,
|
||||
0x6d, 0x00, 0x00, 0x01, 0x00, 0x01,
|
||||
// OPT
|
||||
0x00, // name: <root>
|
||||
0x00, 0x29, // type: OPT
|
||||
0x10, 0x00, // UDP payload size
|
||||
0x00, // higher bits in extended RCODE
|
||||
0x00, // EDNS0 version
|
||||
0x80, 0x00, // "Z" field
|
||||
0x00, 0x00, // data length
|
||||
})
|
||||
|
||||
// Query for "google.com"
|
||||
f.Add([]byte{
|
||||
// header
|
||||
0xaf, 0x66, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01,
|
||||
// query
|
||||
0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f,
|
||||
0x6d, 0x00, 0x00, 0x01, 0x00, 0x01,
|
||||
// OPT
|
||||
0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
|
||||
0x0c, 0x00, 0x0a, 0x00, 0x08, 0x62, 0x18, 0x1a, 0xcb, 0x19,
|
||||
0xd7, 0xee, 0x23,
|
||||
})
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
clampEDNSSize(data, maxResponseBytes)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
@@ -97,6 +98,9 @@ func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
|
||||
if arpa > 0 {
|
||||
fmt.Fprintf(w, "+%darpa", arpa)
|
||||
}
|
||||
if c := cloudenv.Get(); c != "" {
|
||||
fmt.Fprintf(w, ", cloud=%q", string(c))
|
||||
}
|
||||
w.WriteString("}")
|
||||
}
|
||||
|
||||
@@ -278,7 +282,15 @@ func (r *Resolver) Query(ctx context.Context, bs []byte, from netaddr.IPPort) ([
|
||||
defer cancel()
|
||||
err = r.forwarder.forwardWithDestChan(ctx, packet{bs, from}, responses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
select {
|
||||
// Best effort: use any error response sent by forwardWithDestChan.
|
||||
// This is present in some errors paths, such as when all upstream
|
||||
// DNS servers replied with an error.
|
||||
case resp := <-responses:
|
||||
return resp.bs, err
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return (<-responses).bs, nil
|
||||
}
|
||||
|
||||
@@ -1417,3 +1417,45 @@ func TestUnARPA(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestServfail validates that a SERVFAIL error response is returned if
|
||||
// all upstream resolvers respond with SERVFAIL.
|
||||
//
|
||||
// See: https://github.com/tailscale/tailscale/issues/4722
|
||||
func TestServfail(t *testing.T) {
|
||||
server := serveDNS(t, "127.0.0.1:0", "test.site.", miekdns.HandlerFunc(func(w miekdns.ResponseWriter, req *miekdns.Msg) {
|
||||
m := new(miekdns.Msg)
|
||||
m.Rcode = miekdns.RcodeServerFailure
|
||||
w.WriteMsg(m)
|
||||
}))
|
||||
defer server.Shutdown()
|
||||
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
cfg := dnsCfg
|
||||
cfg.Routes = map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
".": {{Addr: server.PacketConn.LocalAddr().String()}},
|
||||
}
|
||||
r.SetConfig(cfg)
|
||||
|
||||
pkt, err := syncRespond(r, dnspacket("test.site.", dns.TypeA, noEdns))
|
||||
if err != errServerFailure {
|
||||
t.Errorf("err = %v, want %v", err, errServerFailure)
|
||||
}
|
||||
|
||||
wantPkt := []byte{
|
||||
0x00, 0x00, // transaction id: 0
|
||||
0x84, 0x02, // flags: response, authoritative, error: servfail
|
||||
0x00, 0x01, // one question
|
||||
0x00, 0x00, // no answers
|
||||
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
|
||||
// Question:
|
||||
0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x73, 0x69, 0x74, 0x65, 0x00, // name
|
||||
0x00, 0x01, 0x00, 0x01, // type A, class IN
|
||||
}
|
||||
|
||||
if !bytes.Equal(pkt, wantPkt) {
|
||||
t.Errorf("response was %X, want %X", pkt, wantPkt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/singleflight"
|
||||
)
|
||||
|
||||
var single = &Resolver{
|
||||
@@ -81,12 +82,18 @@ type Resolver struct {
|
||||
// It is required when SingleHostStaticResult is present.
|
||||
SingleHost string
|
||||
|
||||
sf singleflight.Group
|
||||
sf singleflight.Group[string, ipRes]
|
||||
|
||||
mu sync.Mutex
|
||||
ipCache map[string]ipCacheEntry
|
||||
}
|
||||
|
||||
// ipRes is the type used by the Resolver.sf singleflight group.
|
||||
type ipRes struct {
|
||||
ip, ip6 net.IP
|
||||
allIPs []net.IPAddr
|
||||
}
|
||||
|
||||
type ipCacheEntry struct {
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
@@ -101,6 +108,30 @@ func (r *Resolver) fwd() *net.Resolver {
|
||||
return net.DefaultResolver
|
||||
}
|
||||
|
||||
// cloudHostResolver returns a Resolver for the current cloud hosting environment.
|
||||
// It currently only supports Google Cloud.
|
||||
func (r *Resolver) cloudHostResolver() (v *net.Resolver, ok bool) {
|
||||
switch runtime.GOOS {
|
||||
case "android", "ios", "darwin":
|
||||
return nil, false
|
||||
case "windows":
|
||||
// TODO(bradfitz): remove this restriction once we're using Go 1.19
|
||||
// which supports net.Resolver.PreferGo on Windows.
|
||||
return nil, false
|
||||
}
|
||||
ip := cloudenv.Get().ResolverIP()
|
||||
if ip == "" {
|
||||
return nil, false
|
||||
}
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, net.JoinHostPort(ip, "53"))
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
func (r *Resolver) ttl() time.Duration {
|
||||
if r.TTL > 0 {
|
||||
return r.TTL
|
||||
@@ -150,14 +181,10 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, al
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
type ipRes struct {
|
||||
ip, ip6 net.IP
|
||||
allIPs []net.IPAddr
|
||||
}
|
||||
ch := r.sf.DoChan(host, func() (any, error) {
|
||||
ch := r.sf.DoChan(host, func() (ret ipRes, _ error) {
|
||||
ip, ip6, allIPs, err := r.lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ret, err
|
||||
}
|
||||
return ipRes{ip, ip6, allIPs}, nil
|
||||
})
|
||||
@@ -177,7 +204,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, al
|
||||
}
|
||||
return nil, nil, nil, res.Err
|
||||
}
|
||||
r := res.Val.(ipRes)
|
||||
r := res.Val
|
||||
return r.ip, r.ip6, r.allIPs, nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
@@ -233,6 +260,11 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, e
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
if resolver, ok := r.cloudHostResolver(); ok {
|
||||
ips, err = resolver.LookupIPAddr(ctx, host)
|
||||
}
|
||||
}
|
||||
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -720,3 +720,25 @@ func DefaultRouteInterface() (string, error) {
|
||||
func DefaultRoute() (DefaultRouteDetails, error) {
|
||||
return defaultRoute()
|
||||
}
|
||||
|
||||
// HasCGNATInterface reports whether there are any non-Tailscale interfaces that
|
||||
// use a CGNAT IP range.
|
||||
func HasCGNATInterface() (bool, error) {
|
||||
hasCGNATInterface := false
|
||||
cgnatRange := tsaddr.CGNATRange()
|
||||
err := ForeachInterface(func(i Interface, pfxs []netaddr.IPPrefix) {
|
||||
if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if cgnatRange.Overlaps(pfx) {
|
||||
hasCGNATInterface = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return hasCGNATInterface, nil
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ type Report struct {
|
||||
IPv4 bool // an IPv4 STUN round trip completed
|
||||
IPv6CanSend bool // an IPv6 packet was able to be sent
|
||||
IPv4CanSend bool // an IPv4 packet was able to be sent
|
||||
OSHasIPv6 bool // could bind a socket to ::1
|
||||
|
||||
// MappingVariesByDestIP is whether STUN results depend which
|
||||
// STUN server you're talking to (on IPv4).
|
||||
@@ -806,6 +807,14 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See if IPv6 works at all, or if it's been hard disabled at the
|
||||
// OS level.
|
||||
v6udp, err := netns.Listener(c.logf).ListenPacket(ctx, "udp6", "[::1]:0")
|
||||
if err == nil {
|
||||
rs.report.OSHasIPv6 = true
|
||||
v6udp.Close()
|
||||
}
|
||||
|
||||
// Create a UDP4 socket used for sending to our discovered IPv4 address.
|
||||
rs.pc4Hair, err = netns.Listener(c.logf).ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
|
||||
@@ -111,6 +111,9 @@ func TestWorksWhenUDPBlocked(t *testing.T) {
|
||||
// That's not relevant to this test, so just accept what we're
|
||||
// given.
|
||||
want.IPv4CanSend = r.IPv4CanSend
|
||||
// OS IPv6 test is irrelevant here, accept whatever the current
|
||||
// machine has.
|
||||
want.OSHasIPv6 = r.OSHasIPv6
|
||||
|
||||
if !reflect.DeepEqual(r, want) {
|
||||
t.Errorf("mismatch\n got: %+v\nwant: %+v\n", r, want)
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ios || js
|
||||
// +build ios js
|
||||
|
||||
// (https://github.com/tailscale/tailscale/issues/2495)
|
||||
//go:build js
|
||||
// +build js
|
||||
|
||||
package portmapper
|
||||
|
||||
|
||||
@@ -115,6 +115,9 @@ func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid PCP common header")
|
||||
}
|
||||
if res.ResultCode == pcpCodeNotAuthorized {
|
||||
return nil, fmt.Errorf("PCP is implemented but not enabled in the router")
|
||||
}
|
||||
if res.ResultCode != pcpCodeOK {
|
||||
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
|
||||
}
|
||||
|
||||
@@ -749,9 +749,16 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
// See https://github.com/tailscale/tailscale/issues/3197 for
|
||||
// an example of a device that strictly implements UPnP, and
|
||||
// only responds to multicast queries.
|
||||
//
|
||||
// Then we send a discovery packet looking for
|
||||
// urn:schemas-upnp-org:device:InternetGatewayDevice:1 specifically, not
|
||||
// just ssdp:all, because there appear to be devices which only send
|
||||
// their first descriptor (like urn:schemas-wifialliance-org:device:WFADevice:1)
|
||||
// in response to ssdp:all. https://github.com/tailscale/tailscale/issues/3557
|
||||
metricUPnPSent.Add(1)
|
||||
uc.WriteTo(uPnPPacket, upnpAddr)
|
||||
uc.WriteTo(uPnPPacket, upnpMulticastAddr)
|
||||
uc.WriteTo(uPnPIGDPacket, upnpMulticastAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
@@ -877,6 +884,15 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
// Send a discovery frame for InternetGatewayDevice, since some devices respond
|
||||
// to ssdp:all with only their first descriptor (which is often not IGD).
|
||||
// https://github.com/tailscale/tailscale/issues/3557
|
||||
var uPnPIGDPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
// PCP/PMP metrics
|
||||
var (
|
||||
// metricPXPResponse counts the number of times we received a PMP/PCP response.
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !js
|
||||
// +build !ios,!js
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// (https://github.com/tailscale/tailscale/issues/2495)
|
||||
// (no raw sockets in JS/WASM)
|
||||
|
||||
package portmapper
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ const (
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="1337"><specVersion><major>1</major><minor>1</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType><friendlyName>FreeBSD router</friendlyName><manufacturer>FreeBSD</manufacturer><manufacturerURL>http://www.freebsd.org/</manufacturerURL><modelDescription>FreeBSD router</modelDescription><modelName>FreeBSD router</modelName><modelNumber>2.5.0-RELEASE</modelNumber><modelURL>http://www.freebsd.org/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac11</UDN><serviceList><service><serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType><serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId><SCPDURL>/L3F.xml</SCPDURL><controlURL>/ctl/L3F</controlURL><eventSubURL>/evt/L3F</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType><friendlyName>WANDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>WAN Device</modelDescription><modelName>WAN Device</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac12</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType><serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId><SCPDURL>/WANCfg.xml</SCPDURL><controlURL>/ctl/CmnIfCfg</controlURL><eventSubURL>/evt/CmnIfCfg</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType><friendlyName>WANConnectionDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>MiniUPnP daemon</modelDescription><modelName>MiniUPnPd</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac13</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType><serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId><SCPDURL>/WANIPCn.xml</SCPDURL><controlURL>/ctl/IPConn</controlURL><eventSubURL>/evt/IPConn</eventSubURL></service></serviceList></device></deviceList></device></deviceList><presentationURL>https://192.168.1.1/</presentationURL></device></root>`
|
||||
)
|
||||
|
||||
// Sagemcom FAST3890V3, https://github.com/tailscale/tailscale/issues/3557
|
||||
const (
|
||||
sagemcomUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=1800\r\nDATE: Tue, 14 Dec 2021 07:51:29 GMT\r\nEXT:\r\nLOCATION: http://192.168.0.1:49153/69692b70/gatedesc0b.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: cabd6488-1dd1-11b2-9e52-a7461e1f098e\r\nSERVER: \r\nUser-Agent: redsonic\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:75802409-bccb-40e7-8e6c-fa095ecce13e::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n"
|
||||
)
|
||||
|
||||
func TestParseUPnPDiscoResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -51,6 +56,11 @@ func TestParseUPnPDiscoResponse(t *testing.T) {
|
||||
Server: "FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1",
|
||||
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
}},
|
||||
{"sagemcom", sagemcomUPnPDisco, uPnPDiscoResponse{
|
||||
Location: "http://192.168.0.1:49153/69692b70/gatedesc0b.xml",
|
||||
Server: "",
|
||||
USN: "uuid:75802409-bccb-40e7-8e6c-fa095ecce13e::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
77
net/socks5/socks5_test.go
Normal file
77
net/socks5/socks5_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func socks5Server(listener net.Listener) {
|
||||
var server Server
|
||||
err := server.Serve(listener)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
func backendServer(listener net.Listener) {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conn.Write([]byte("Test"))
|
||||
conn.Close()
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
// backend server which we'll use SOCKS5 to connect to
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backendServerPort := listener.Addr().(*net.TCPAddr).Port
|
||||
go backendServer(listener)
|
||||
|
||||
// SOCKS5 server
|
||||
socks5, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
socks5Port := socks5.Addr().(*net.TCPAddr).Port
|
||||
go socks5Server(socks5)
|
||||
|
||||
addr := fmt.Sprintf("localhost:%d", socks5Port)
|
||||
socksDialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr = fmt.Sprintf("localhost:%d", backendServerPort)
|
||||
conn, err := socksDialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4)
|
||||
_, err = io.ReadFull(conn, buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(buf) != "Test" {
|
||||
t.Fatalf("got: %q want: Test", buf)
|
||||
}
|
||||
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
@@ -290,7 +289,7 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
|
||||
}
|
||||
|
||||
func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netaddr.IPPort) []byte {
|
||||
buf := buffer.NewView(header.EthernetMinimumSize + header.UDPMinimumSize + header.IPv4MinimumSize + len(payload))
|
||||
buf := make([]byte, header.EthernetMinimumSize+header.UDPMinimumSize+header.IPv4MinimumSize+len(payload))
|
||||
payloadStart := len(buf) - len(payload)
|
||||
copy(buf[payloadStart:], payload)
|
||||
srcB := src.IP().As4()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user