Compare commits
241 Commits
shayne/ser
...
coral-gito
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c684ca7a0c | ||
|
|
1116602d4c | ||
|
|
be67b8e75b | ||
|
|
8047dfa2dc | ||
|
|
ebbf5c57b3 | ||
|
|
39efba528f | ||
|
|
69c0b7e712 | ||
|
|
673b3d8dbd | ||
|
|
10eec37cd9 | ||
|
|
8f2bc0708b | ||
|
|
0088c5ddc0 | ||
|
|
907f85cd67 | ||
|
|
8724aa254f | ||
|
|
c4e262a0fc | ||
|
|
eafbf8886d | ||
|
|
b2b8e62476 | ||
|
|
91e64ca74f | ||
|
|
d72575eaaa | ||
|
|
b2c55e62c8 | ||
|
|
467ace7d0c | ||
|
|
aad6830df0 | ||
|
|
ea70aa3d98 | ||
|
|
692eac23ad | ||
|
|
c86d9f2ab1 | ||
|
|
7bfb9999ee | ||
|
|
d2beaea523 | ||
|
|
3599364312 | ||
|
|
a7f05c6bb0 | ||
|
|
eb682d2a0b | ||
|
|
2a1f1c79ca | ||
|
|
6107c65f1e | ||
|
|
a45c9f982a | ||
|
|
84eaef0bbb | ||
|
|
f3c83a06ff | ||
|
|
011f661d5b | ||
|
|
caa2fe394f | ||
|
|
82b9689e25 | ||
|
|
1011e64ad7 | ||
|
|
be10b529ec | ||
|
|
e36cdacf70 | ||
|
|
14e8afe444 | ||
|
|
8aac77aa19 | ||
|
|
f837d179b9 | ||
|
|
2eff9c8277 | ||
|
|
0372e14d79 | ||
|
|
98daf99775 | ||
|
|
7c77c48bd4 | ||
|
|
243490f932 | ||
|
|
a1ded4c166 | ||
|
|
e5fe205c31 | ||
|
|
237f030cd9 | ||
|
|
296f53524c | ||
|
|
a06217a8bd | ||
|
|
5caf609d7b | ||
|
|
0f604923d3 | ||
|
|
3c452b9880 | ||
|
|
14d07b7b20 | ||
|
|
914d115f65 | ||
|
|
af3127711a | ||
|
|
6d5527e4b3 | ||
|
|
d9df023e6f | ||
|
|
651e0d8aad | ||
|
|
fc0fe99edf | ||
|
|
c02ccf6424 | ||
|
|
97c615889d | ||
|
|
a8f07153a6 | ||
|
|
53c4892841 | ||
|
|
8171eb600c | ||
|
|
56f7da0cfd | ||
|
|
350aab05e5 | ||
|
|
55b24009f7 | ||
|
|
3a5fc233aa | ||
|
|
a7ab3429b6 | ||
|
|
da53b1347b | ||
|
|
835a73cc1f | ||
|
|
d857fd00b3 | ||
|
|
8ccd707218 | ||
|
|
c0fcab01ac | ||
|
|
3f4d51c588 | ||
|
|
44be59c15a | ||
|
|
0d47cd2284 | ||
|
|
d81a2b2ce2 | ||
|
|
9c77205ba1 | ||
|
|
c902190e67 | ||
|
|
8dbb3b8bbe | ||
|
|
53a9cc76c7 | ||
|
|
bc8f5a7734 | ||
|
|
3b7ae39a06 | ||
|
|
ca08e316af | ||
|
|
bd2995c14b | ||
|
|
c47578b528 | ||
|
|
041a0e3c27 | ||
|
|
b2d4abf25a | ||
|
|
47002d93a3 | ||
|
|
53e2010b8a | ||
|
|
9c67395334 | ||
|
|
7b65b7f85c | ||
|
|
5a523fdc7f | ||
|
|
9d335aabb2 | ||
|
|
ab4992e10d | ||
|
|
ea5ee6f87c | ||
|
|
b63094431b | ||
|
|
eb1adf629f | ||
|
|
383e203fd2 | ||
|
|
76389d8baf | ||
|
|
389238fe4a | ||
|
|
bdc45b9066 | ||
|
|
e27f4f022e | ||
|
|
d1a5757639 | ||
|
|
2d271f3bd1 | ||
|
|
5f68763cb2 | ||
|
|
98114bf608 | ||
|
|
1b65630e83 | ||
|
|
55e0512a05 | ||
|
|
98f21354c6 | ||
|
|
367228ef82 | ||
|
|
a887ca7efe | ||
|
|
e36c27bcd1 | ||
|
|
e79a1eb24a | ||
|
|
e04aaa7575 | ||
|
|
a469ec8ff6 | ||
|
|
c084c7d7ed | ||
|
|
5ff946a9e6 | ||
|
|
7048024e04 | ||
|
|
1598cd0361 | ||
|
|
4b34c88426 | ||
|
|
79f3a5d753 | ||
|
|
cb525a1aad | ||
|
|
3f16dec1bb | ||
|
|
9c773af04c | ||
|
|
c933b8882c | ||
|
|
964d723aba | ||
|
|
86b6ff61e6 | ||
|
|
cdb924f87b | ||
|
|
7c4d017636 | ||
|
|
57124e2126 | ||
|
|
96cad35870 | ||
|
|
a87e0b4ea8 | ||
|
|
b9dd3fa534 | ||
|
|
5b8323509f | ||
|
|
d6dbefaa91 | ||
|
|
71c0e8d428 | ||
|
|
e1d7d072a3 | ||
|
|
d5b4d2e276 | ||
|
|
74b47eaad6 | ||
|
|
99aa335923 | ||
|
|
a5a3188b7e | ||
|
|
a1084047ce | ||
|
|
74c1f632f6 | ||
|
|
8dd1418774 | ||
|
|
197a4f1ae8 | ||
|
|
a277eb4dcf | ||
|
|
978d6af91a | ||
|
|
5bdca747b7 | ||
|
|
f1ab11e961 | ||
|
|
9a80b8fb10 | ||
|
|
731be07777 | ||
|
|
a6dff4fb74 | ||
|
|
74744b0a4c | ||
|
|
f710d1cb20 | ||
|
|
d2a51f03ce | ||
|
|
a200a23f97 | ||
|
|
82ad585b5b | ||
|
|
adc302f428 | ||
|
|
45042a76cd | ||
|
|
c4980f33f7 | ||
|
|
6d012547b6 | ||
|
|
390d1bb871 | ||
|
|
f1130421f0 | ||
|
|
659e7837c6 | ||
|
|
ad41cbd9d5 | ||
|
|
0a10a5632b | ||
|
|
256ba62e00 | ||
|
|
0cb2ccce7f | ||
|
|
06c4c47d46 | ||
|
|
2e5d08ec4f | ||
|
|
35c10373b5 | ||
|
|
6cc6c70d70 | ||
|
|
6e33d2da2b | ||
|
|
3b0de97e07 | ||
|
|
ea25ef8236 | ||
|
|
5c8d2fa695 | ||
|
|
e8cc78b1af | ||
|
|
8049053f86 | ||
|
|
3b73727e39 | ||
|
|
5676d201d6 | ||
|
|
f45106d47c | ||
|
|
109aa3b2fb | ||
|
|
b0545873e5 | ||
|
|
e567902aa9 | ||
|
|
f3ba268a96 | ||
|
|
699b39dec1 | ||
|
|
7e016c1d90 | ||
|
|
624d9c759b | ||
|
|
b8fe89d15f | ||
|
|
1fdfb0dd08 | ||
|
|
c258015165 | ||
|
|
0a842f353c | ||
|
|
5ea7c7d603 | ||
|
|
732c3d2ed0 | ||
|
|
a3cd171773 | ||
|
|
d321b0ea4f | ||
|
|
992749c44c | ||
|
|
0c4c66948b | ||
|
|
344abaf3d3 | ||
|
|
250edeb3da | ||
|
|
033bd94d4c | ||
|
|
d6021ae71c | ||
|
|
b68d008fee | ||
|
|
20b27df4d0 | ||
|
|
4d3713f631 | ||
|
|
6e6f27dd21 | ||
|
|
dc75b7cfd1 | ||
|
|
b1441d0044 | ||
|
|
7bff7345cc | ||
|
|
5f6fec0eba | ||
|
|
ec790e58df | ||
|
|
3a5d02cb31 | ||
|
|
300aba61a6 | ||
|
|
d4f6efa1df | ||
|
|
b45b948776 | ||
|
|
1ef4be2f86 | ||
|
|
aeb80bf8cb | ||
|
|
6708f9a93f | ||
|
|
ed1fae6c73 | ||
|
|
0f7da5c7dc | ||
|
|
f053f16460 | ||
|
|
8d84178884 | ||
|
|
aeac4bc8e2 | ||
|
|
18c7c3981a | ||
|
|
41dd49391f | ||
|
|
0480a925c1 | ||
|
|
b190c1667b | ||
|
|
5c9203669a | ||
|
|
a0ef51f570 | ||
|
|
b94b91c168 | ||
|
|
575fd5f22b | ||
|
|
33520920c3 | ||
|
|
41e1d336cc | ||
|
|
bdd8ce6692 | ||
|
|
d1e1c025b0 |
57
.github/workflows/cross-loong64.yml
vendored
Normal file
57
.github/workflows/cross-loong64.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Loongnix-Cross
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- 'release-branch/*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
|
||||
- name: Loongnix build cmd
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: loong64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Loongnix build tests
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: loong64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -15,7 +15,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
@@ -39,10 +39,6 @@ jobs:
|
||||
|
||||
- name: Get QEMU
|
||||
run: |
|
||||
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
|
||||
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
|
||||
# use this PPA which brings in a modern qemu.
|
||||
sudo add-apt-repository -y ppa:jacob/virtualisation
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -23,5 +23,8 @@ cmd/tailscaled/tailscaled
|
||||
# to make this nonspecific.
|
||||
.envrc
|
||||
|
||||
# vscode project specific settings
|
||||
# Ignore personal VS Code settings
|
||||
.vscode/
|
||||
|
||||
# Ignore direnv nix-shell environment cache
|
||||
.direnv/
|
||||
|
||||
15
Makefile
15
Makefile
@@ -35,6 +35,9 @@ buildlinuxarm:
|
||||
buildwasm:
|
||||
GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
|
||||
|
||||
buildlinuxloong64:
|
||||
GOOS=linux GOARCH=loong64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
buildmultiarchimage:
|
||||
./build_docker.sh
|
||||
|
||||
@@ -59,4 +62,14 @@ publishdevimage:
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
TAGS=latest REPOS=${REPO} PUSH=true ./build_docker.sh
|
||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
TAGS=latest REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
|
||||
|
||||
publishdevoperator:
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
TAGS=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
|
||||
|
||||
35
README.md
35
README.md
@@ -6,27 +6,41 @@ Private WireGuard® networks made easy
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
|
||||
This repository contains the majority of Tailscale's open source code.
|
||||
Notably, it includes the `tailscaled` daemon and
|
||||
the `tailscale` CLI tool. The `tailscaled` daemon runs on Linux, Windows,
|
||||
[macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees
|
||||
on FreeBSD and OpenBSD. The Tailscale iOS and Android apps use this repo's
|
||||
code, but this repo doesn't contain the mobile GUI code.
|
||||
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
Other [Tailscale repos](https://github.com/orgs/tailscale/repositories) of note:
|
||||
|
||||
The Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
* the Android app is at https://github.com/tailscale/tailscale-android
|
||||
* the Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
* the QNAP package is at https://github.com/tailscale/tailscale-qpkg
|
||||
* the Chocolatey packaging is at https://github.com/tailscale/tailscale-chocolatey
|
||||
|
||||
For background on which parts of Tailscale are open source and why,
|
||||
see [https://tailscale.com/opensource/](https://tailscale.com/opensource/).
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
https://pkgs.tailscale.com .
|
||||
We serve packages for a variety of distros and platforms at
|
||||
[https://pkgs.tailscale.com](https://pkgs.tailscale.com/).
|
||||
|
||||
## Other clients
|
||||
|
||||
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
|
||||
use the code in this repository but additionally include small GUI
|
||||
wrappers that are not open source.
|
||||
wrappers. The GUI wrappers on non-open source platforms are themselves
|
||||
not open source.
|
||||
|
||||
## Building
|
||||
|
||||
We always require the latest Go release, currently Go 1.19. (While we build
|
||||
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
|
||||
required.)
|
||||
|
||||
```
|
||||
go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
@@ -43,8 +57,6 @@ If your distro has conventions that preclude the use of
|
||||
`build_dist.sh`, please do the equivalent of what it does in your
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We require the latest Go release, currently Go 1.19.
|
||||
|
||||
## Bugs
|
||||
|
||||
Please file any issues about this code or the hosted service on
|
||||
@@ -59,6 +71,9 @@ We require [Developer Certificate of
|
||||
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
`Signed-off-by` lines in commits.
|
||||
|
||||
See `git log` for our commit message style. It's basically the same as
|
||||
[Go's style](https://github.com/golang/go/wiki/CommitMessage).
|
||||
|
||||
## About Us
|
||||
|
||||
[Tailscale](https://tailscale.com/) is primarily developed by the
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.33.0
|
||||
1.35.0
|
||||
|
||||
69
api.md
69
api.md
@@ -3,7 +3,7 @@
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
|
||||
Currently based on {some authentication method}. Visit the [admin console](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
|
||||
|
||||
# APIs
|
||||
|
||||
@@ -402,20 +402,20 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
"Tests": [],
|
||||
"tests": [],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"groups": {
|
||||
"group:example": [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
"acls": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{
|
||||
@@ -485,6 +485,8 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
###### Headers
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
A special value `ts-default` will ensure that ACL will be set only if current ACL is the default one (created automatically for each tailnet).
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
###### POST Body
|
||||
@@ -492,11 +494,14 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
|
||||
An ACL policy may contain the following top-level properties:
|
||||
|
||||
* `Groups` - Static groups of users which can be used for ACL rules.
|
||||
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
* `ACLs` - Access control lists.
|
||||
* `TagOwners` - Defines who is allowed to use which tags.
|
||||
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
* `groups` - Static groups of users which can be used for ACL rules.
|
||||
* `hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
* `acls` - Access control lists.
|
||||
* `tagOwners` - Defines who is allowed to use which tags.
|
||||
* `tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
* `autoApprovers` - Defines which users can advertise routes or exit nodes without further approval.
|
||||
* `ssh` - Configures access policy for Tailscale SSH.
|
||||
* `nodeAttrs` - Defines which devices can use certain features.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on those properties.
|
||||
|
||||
@@ -509,22 +514,22 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
"tests": [
|
||||
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
"acls": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
@@ -534,22 +539,22 @@ Response:
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
"tests": [
|
||||
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
"acls": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -592,22 +597,22 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFo
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
"tests": [
|
||||
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
"acls": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
@@ -643,7 +648,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
[
|
||||
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
|
||||
{"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]}
|
||||
]'
|
||||
```
|
||||
|
||||
@@ -654,10 +659,10 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
{
|
||||
"ACLs": [
|
||||
{ "Action": "accept", "src": ["100.105.106.107"], "dst": ["1.2.3.4:*"] },
|
||||
"acls": [
|
||||
{ "action": "accept", "src": ["100.105.106.107"], "dst": ["1.2.3.4:*"] },
|
||||
],
|
||||
"Tests", [
|
||||
"tests", [
|
||||
{"src": "100.105.106.107", "allow": ["1.2.3.4:80"]}
|
||||
],
|
||||
}'
|
||||
|
||||
@@ -26,23 +26,46 @@ 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.16"
|
||||
DEFAULT_TARGET="client"
|
||||
|
||||
PUSH="${PUSH:-false}"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
TAGS="${TAGS:-${DEFAULT_TAGS}}"
|
||||
BASE="${BASE:-${DEFAULT_BASE}}"
|
||||
TARGET="${TARGET:-${DEFAULT_TARGET}}"
|
||||
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="\
|
||||
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
|
||||
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled, \
|
||||
tailscale.com/cmd/containerboot:/usr/local/bin/containerboot" \
|
||||
--ldflags="\
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
/usr/local/bin/containerboot
|
||||
case "$TARGET" in
|
||||
client)
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="\
|
||||
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
|
||||
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled, \
|
||||
tailscale.com/cmd/containerboot:/usr/local/bin/containerboot" \
|
||||
--ldflags="\
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
/usr/local/bin/containerboot
|
||||
;;
|
||||
operator)
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="tailscale.com/cmd/k8s-operator:/usr/local/bin/operator" \
|
||||
--ldflags="\
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
/usr/local/bin/operator
|
||||
;;
|
||||
*)
|
||||
echo "unknown target: $TARGET"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
147
client/tailscale/keys.go
Normal file
147
client/tailscale/keys.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// 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 tailscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Key represents a Tailscale API or auth key.
|
||||
type Key struct {
|
||||
ID string `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Capabilities KeyCapabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
// KeyCapabilities are the capabilities of a Key.
|
||||
type KeyCapabilities struct {
|
||||
Devices KeyDeviceCapabilities `json:"devices,omitempty"`
|
||||
}
|
||||
|
||||
// KeyDeviceCapabilities are the device-related capabilities of a Key.
|
||||
type KeyDeviceCapabilities struct {
|
||||
Create KeyDeviceCreateCapabilities `json:"create"`
|
||||
}
|
||||
|
||||
// KeyDeviceCreateCapabilities are the device creation capabilities of a Key.
|
||||
type KeyDeviceCreateCapabilities struct {
|
||||
Reusable bool `json:"reusable"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
Preauthorized bool `json:"preauthorized"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// Keys returns the list of keys for the current user.
|
||||
func (c *Client) Keys(ctx context.Context) ([]string, error) {
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, resp, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, handleErrorResponse(b, resp)
|
||||
}
|
||||
|
||||
var keys []struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
ret = append(ret, k.ID)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// CreateKey creates a new key for the current user. Currently, only auth keys
|
||||
// can be created. Returns the key itself, which cannot be retrieved again
|
||||
// later, and the key metadata.
|
||||
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (string, *Key, error) {
|
||||
keyRequest := struct {
|
||||
Capabilities KeyCapabilities `json:"capabilities"`
|
||||
}{caps}
|
||||
bs, err := json.Marshal(keyRequest)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b, resp, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", nil, handleErrorResponse(b, resp)
|
||||
}
|
||||
|
||||
var key struct {
|
||||
Key
|
||||
Secret string `json:"key"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &key); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return key.Secret, &key.Key, nil
|
||||
}
|
||||
|
||||
// Key returns the metadata for the given key ID. Currently, capabilities are
|
||||
// only returned for auth keys, API keys only return general metadata.
|
||||
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, resp, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, handleErrorResponse(b, resp)
|
||||
}
|
||||
|
||||
var key Key
|
||||
if err := json.Unmarshal(b, &key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// DeleteKey deletes the key with the given ID.
|
||||
func (c *Client) DeleteKey(ctx context.Context, id string) error {
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
|
||||
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, resp, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return handleErrorResponse(b, resp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -100,9 +101,6 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
|
||||
}
|
||||
}
|
||||
s := safesocket.DefaultConnectionStrategy(lc.socket())
|
||||
// The user provided a non-default tailscaled socket address.
|
||||
// Connect only to exactly what they provided.
|
||||
s.UseFallback(false)
|
||||
return safesocket.Connect(s)
|
||||
}
|
||||
|
||||
@@ -132,8 +130,8 @@ func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error)
|
||||
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
|
||||
res, err := lc.DoLocalRequest(req)
|
||||
if err == nil {
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != ipn.IPCVersion() && onVersionMismatch != nil {
|
||||
onVersionMismatch(ipn.IPCVersion(), server)
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != envknob.IPCVersion() && onVersionMismatch != nil {
|
||||
onVersionMismatch(envknob.IPCVersion(), server)
|
||||
}
|
||||
if res.StatusCode == 403 {
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
@@ -426,8 +424,20 @@ func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenR
|
||||
return decodeJSON[*tailcfg.TokenResponse](body)
|
||||
}
|
||||
|
||||
// WaitingFiles returns the list of received Taildrop files that have been
|
||||
// received by the Tailscale daemon in its staging/cache directory but not yet
|
||||
// transferred by the user's CLI or GUI client and written to a user's home
|
||||
// directory somewhere.
|
||||
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/files/")
|
||||
return lc.AwaitWaitingFiles(ctx, 0)
|
||||
}
|
||||
|
||||
// AwaitWaitingFiles is like WaitingFiles but takes a duration to await for an answer.
|
||||
// If the duration is 0, it will return immediately. The duration is respected at second
|
||||
// granularity only. If no files are available, it returns (nil, nil).
|
||||
func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
|
||||
path := "/localapi/v0/files/?waitsec=" + fmt.Sprint(int(d.Seconds()))
|
||||
body, err := lc.get200(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -543,6 +553,20 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
|
||||
return decodeJSON[*ipn.Prefs](body)
|
||||
}
|
||||
|
||||
// StartLoginInteractive starts an interactive login.
|
||||
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start applies the configuration specified in opts, and starts the
|
||||
// state machine.
|
||||
func (lc *LocalClient) Start(ctx context.Context, opts ipn.Options) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/start", http.StatusNoContent, jsonBody(opts))
|
||||
return err
|
||||
}
|
||||
|
||||
// Logout logs out the current node.
|
||||
func (lc *LocalClient) Logout(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
|
||||
return err
|
||||
@@ -761,14 +785,15 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
//
|
||||
// TODO(tom): Plumb through disablement secrets.
|
||||
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte) (*ipnstate.NetworkLockStatus, error) {
|
||||
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
SupportDisablement []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues}); err != nil {
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -780,7 +805,7 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) (*ipnstate.NetworkLockStatus, error) {
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
@@ -788,14 +813,13 @@ func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKey
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
||||
@@ -817,6 +841,31 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
v.Set("limit", fmt.Sprint(maxEntries))
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
||||
}
|
||||
|
||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||
func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||
// This endpoint expects an empty JSON stanza as the payload.
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
@@ -943,3 +992,82 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
|
||||
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
|
||||
v := url.Values{"region": {regionIDOrCode}}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*ipnstate.DebugDERPRegionReport](body)
|
||||
}
|
||||
|
||||
// WatchIPNBus subscribes to the IPN notification bus. It returns a watcher
|
||||
// once the bus is connected successfully.
|
||||
//
|
||||
// The context is used for the life of the watch, not just the call to
|
||||
// WatchIPNBus.
|
||||
//
|
||||
// The returned IPNBusWatcher's Close method must be called when done to release
|
||||
// resources.
|
||||
//
|
||||
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
|
||||
func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
|
||||
nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
res.Body.Close()
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
dec := json.NewDecoder(res.Body)
|
||||
return &IPNBusWatcher{
|
||||
ctx: ctx,
|
||||
httpRes: res,
|
||||
dec: dec,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
|
||||
// It's returned by LocalClient.WatchIPNBus.
|
||||
//
|
||||
// It must be closed when done.
|
||||
type IPNBusWatcher struct {
|
||||
ctx context.Context // from original WatchIPNBus call
|
||||
httpRes *http.Response
|
||||
dec *json.Decoder
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// Close stops the watcher and releases its resources.
|
||||
func (w *IPNBusWatcher) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
w.closed = true
|
||||
return w.httpRes.Body.Close()
|
||||
}
|
||||
|
||||
// Next returns the next ipn.Notify from the stream.
|
||||
// If the context from LocalClient.WatchIPNBus is done, that error is returned.
|
||||
func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
|
||||
var n ipn.Notify
|
||||
if err := w.dec.Decode(&n); err != nil {
|
||||
if cerr := w.ctx.Err(); cerr != nil {
|
||||
err = cerr
|
||||
}
|
||||
return ipn.Notify{}, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func main() {
|
||||
w("}")
|
||||
}
|
||||
cloneOutput := pkg.Name + "_clone.go"
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, codegen.CopyrightYear("."), it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,85 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// checkSecretPermissions checks the secret access permissions of the current
|
||||
// pod. It returns an error if the basic permissions tailscale needs are
|
||||
// missing, and reports whether the patch permission is additionally present.
|
||||
//
|
||||
// Errors encountered during the access checking process are logged, but ignored
|
||||
// so that the pod tries to fail alive if the permissions exist and there's just
|
||||
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
|
||||
// should always be able to use SSARs to assess their own permissions, but since
|
||||
// we didn't use to check permissions this way we'll be cautious in case some
|
||||
// old version of k8s deviates from the current behavior.
|
||||
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
|
||||
var errs []error
|
||||
for _, verb := range []string{"get", "update"} {
|
||||
ok, err := checkPermission(ctx, verb, secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
|
||||
} else if !ok {
|
||||
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
ok, err := checkPermission(ctx, "patch", secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
|
||||
return false, nil
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// checkPermission reports whether the current pod has permission to use the
|
||||
// given verb (e.g. get, update, patch) on secretName.
|
||||
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
|
||||
sar := map[string]any{
|
||||
"apiVersion": "authorization.k8s.io/v1",
|
||||
"kind": "SelfSubjectAccessReview",
|
||||
"spec": map[string]any{
|
||||
"resourceAttributes": map[string]any{
|
||||
"namespace": kubeNamespace,
|
||||
"verb": verb,
|
||||
"resource": "secrets",
|
||||
"name": secretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
bs, err := json.Marshal(sar)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var res struct {
|
||||
Status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
} `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(bs, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Status.Allowed, nil
|
||||
}
|
||||
|
||||
// findKeyInKubeSecret inspects the kube secret secretName for a data
|
||||
// field called "authkey", and returns its value if present.
|
||||
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
|
||||
@@ -64,9 +141,9 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// storeDeviceID writes deviceID into the "device_id" data field of
|
||||
// the kube secret secretName.
|
||||
func storeDeviceID(ctx context.Context, secretName, deviceID string) error {
|
||||
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
|
||||
// secret secretName.
|
||||
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
|
||||
// First check if the secret exists at all. Even if running on
|
||||
// kubernetes, we do not necessarily store state in a k8s secret.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
||||
@@ -84,8 +161,9 @@ func storeDeviceID(ctx context.Context, secretName, deviceID string) error {
|
||||
}
|
||||
|
||||
m := map[string]map[string]string{
|
||||
"stringData": map[string]string{
|
||||
"device_id": deviceID,
|
||||
"stringData": {
|
||||
"device_id": string(deviceID),
|
||||
"device_fqdn": fqdn,
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
@@ -193,8 +271,8 @@ func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return resp, fmt.Errorf("got non-200 status code %d", resp.StatusCode)
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
//go:build linux
|
||||
|
||||
// The containerboot binary is a wrapper for starting tailscaled in a
|
||||
// container. It handles reading the desired mode of operation out of
|
||||
// environment variables, bringing up and authenticating Tailscale,
|
||||
// and any other kubernetes-specific side jobs.
|
||||
// The containerboot binary is a wrapper for starting tailscaled in a container.
|
||||
// It handles reading the desired mode of operation out of environment
|
||||
// variables, bringing up and authenticating Tailscale, and any other
|
||||
// kubernetes-specific side jobs.
|
||||
//
|
||||
// As with most container things, configuration is passed through
|
||||
// environment variables. All configuration is optional.
|
||||
// As with most container things, configuration is passed through environment
|
||||
// variables. All configuration is optional.
|
||||
//
|
||||
// - TS_AUTH_KEY: the authkey to use for login.
|
||||
// - TS_AUTHKEY: the authkey to use for login.
|
||||
// - TS_ROUTES: subnet routes to advertise.
|
||||
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
|
||||
// destination.
|
||||
@@ -37,9 +37,13 @@
|
||||
// compatibility), forcibly log in every time the
|
||||
// container starts.
|
||||
//
|
||||
// When running on Kubernetes, TS_KUBE_SECRET takes precedence over
|
||||
// TS_STATE_DIR. Additionally, if TS_AUTH_KEY is not provided and the
|
||||
// TS_KUBE_SECRET contains an "authkey" field, that key is used.
|
||||
// When running on Kubernetes, containerboot defaults to storing state in the
|
||||
// "tailscale" kube secret. To store state on local disk instead, set
|
||||
// TS_KUBE_SECRET="" and TS_STATE_DIR=/path/to/storage/dir. The state dir should
|
||||
// be persistent storage.
|
||||
//
|
||||
// Additionally, if TS_AUTHKEY is not set and the TS_KUBE_SECRET contains an
|
||||
// "authkey" field, that key is used as the tailscale authkey.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -60,7 +64,8 @@ import (
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/deephash"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -68,7 +73,7 @@ func main() {
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
cfg := &settings{
|
||||
AuthKey: defaultEnv("TS_AUTH_KEY", ""),
|
||||
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
|
||||
Routes: defaultEnv("TS_ROUTES", ""),
|
||||
ProxyTo: defaultEnv("TS_DEST_IP", ""),
|
||||
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
|
||||
@@ -116,67 +121,190 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.AuthKey == "" {
|
||||
key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Getting authkey from kube secret: %v", err)
|
||||
}
|
||||
if key != "" {
|
||||
log.Print("Using authkey found in kube secret")
|
||||
cfg.AuthKey = key
|
||||
} else {
|
||||
log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
|
||||
}
|
||||
}
|
||||
|
||||
st, daemonPid, err := startAndAuthTailscaled(ctx, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to bring up tailscale: %v", err)
|
||||
}
|
||||
|
||||
if cfg.ProxyTo != "" {
|
||||
if err := installIPTablesRule(ctx, cfg.ProxyTo, st.TailscaleIPs); err != nil {
|
||||
log.Fatalf("installing proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
if cfg.InKubernetes && cfg.KubeSecret != "" {
|
||||
if err := storeDeviceID(ctx, cfg.KubeSecret, string(st.Self.ID)); err != nil {
|
||||
log.Fatalf("storing device ID in kube secret: %v", err)
|
||||
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
|
||||
}
|
||||
if cfg.AuthOnce {
|
||||
// We were told to only auth once, so any secret-bound
|
||||
// authkey is no longer needed. We don't strictly need to
|
||||
// wipe it, but it's good hygiene.
|
||||
log.Printf("Deleting authkey from kube secret")
|
||||
if err := deleteAuthKey(ctx, cfg.KubeSecret); err != nil {
|
||||
log.Fatalf("deleting authkey from kube secret: %v", err)
|
||||
cfg.KubernetesCanPatch = canPatch
|
||||
|
||||
if cfg.AuthKey == "" {
|
||||
key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Getting authkey from kube secret: %v", err)
|
||||
}
|
||||
if key != "" {
|
||||
// This behavior of pulling authkeys from kube secrets was added
|
||||
// at the same time as the patch permission, so we can enforce
|
||||
// that we must be able to patch out the authkey after
|
||||
// authenticating if you want to use this feature. This avoids
|
||||
// us having to deal with the case where we might leave behind
|
||||
// an unnecessary reusable authkey in a secret, like a rake in
|
||||
// the grass.
|
||||
if !cfg.KubernetesCanPatch {
|
||||
log.Fatalf("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the secret to manage the authkey.")
|
||||
}
|
||||
log.Print("Using authkey found in kube secret")
|
||||
cfg.AuthKey = key
|
||||
} else {
|
||||
log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Startup complete, waiting for shutdown signal")
|
||||
// Reap all processes, since we are PID1 and need to collect
|
||||
// zombies.
|
||||
for {
|
||||
var status unix.WaitStatus
|
||||
pid, err := unix.Wait4(-1, &status, 0, nil)
|
||||
if errors.Is(err, unix.EINTR) {
|
||||
continue
|
||||
client, daemonPid, err := startTailscaled(ctx, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to bring up tailscale: %v", err)
|
||||
}
|
||||
|
||||
w, err := client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to watch tailscaled for updates: %v", err)
|
||||
}
|
||||
|
||||
// Because we're still shelling out to `tailscale up` to get access to its
|
||||
// flag parser, we have to stop watching the IPN bus so that we can block on
|
||||
// the subcommand without stalling anything. Then once it's done, we resume
|
||||
// watching the bus.
|
||||
//
|
||||
// Depending on the requested mode of operation, this auth step happens at
|
||||
// different points in containerboot's lifecycle, hence the helper function.
|
||||
didLogin := false
|
||||
authTailscale := func() error {
|
||||
if didLogin {
|
||||
return nil
|
||||
}
|
||||
didLogin = true
|
||||
w.Close()
|
||||
if err := tailscaleUp(ctx, cfg); err != nil {
|
||||
return fmt.Errorf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
w, err = client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
|
||||
if err != nil {
|
||||
log.Fatalf("Waiting for exited processes: %v", err)
|
||||
return fmt.Errorf("rewatching tailscaled for updates after auth: %v", err)
|
||||
}
|
||||
if pid == daemonPid {
|
||||
log.Printf("Tailscaled exited")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !cfg.AuthOnce {
|
||||
if err := authTailscale(); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
authLoop:
|
||||
for {
|
||||
n, err := w.Next()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read from tailscaled: %v", err)
|
||||
}
|
||||
|
||||
if n.State != nil {
|
||||
switch *n.State {
|
||||
case ipn.NeedsLogin:
|
||||
if err := authTailscale(); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
case ipn.NeedsMachineAuth:
|
||||
log.Printf("machine authorization required, please visit the admin panel")
|
||||
case ipn.Running:
|
||||
// Technically, all we want is to keep monitoring the bus for
|
||||
// netmap updates. However, in order to make the container crash
|
||||
// if tailscale doesn't initially come up, the watch has a
|
||||
// startup deadline on it. So, we have to break out of this
|
||||
// watch loop, cancel the watch, and watch again with no
|
||||
// deadline to continue monitoring for changes.
|
||||
break authLoop
|
||||
default:
|
||||
log.Printf("tailscaled in state %q, waiting", *n.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
|
||||
// We were told to only auth once, so any secret-bound
|
||||
// authkey is no longer needed. We don't strictly need to
|
||||
// wipe it, but it's good hygiene.
|
||||
log.Printf("Deleting authkey from kube secret")
|
||||
if err := deleteAuthKey(ctx, cfg.KubeSecret); err != nil {
|
||||
log.Fatalf("deleting authkey from kube secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w, err = client.WatchIPNBus(context.Background(), ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
|
||||
if err != nil {
|
||||
log.Fatalf("rewatching tailscaled for updates after auth: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
wantProxy = cfg.ProxyTo != ""
|
||||
wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
|
||||
startupTasksDone = false
|
||||
currentIPs deephash.Sum // tailscale IPs assigned to device
|
||||
currentDeviceInfo deephash.Sum // device ID and fqdn
|
||||
)
|
||||
for {
|
||||
n, err := w.Next()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read from tailscaled: %v", err)
|
||||
}
|
||||
|
||||
if n.State != nil && *n.State != ipn.Running {
|
||||
// Something's gone wrong and we've left the authenticated state.
|
||||
// Our container image never recovered gracefully from this, and the
|
||||
// control flow required to make it work now is hard. So, just crash
|
||||
// the container and rely on the container runtime to restart us,
|
||||
// whereupon we'll go through initial auth again.
|
||||
log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State)
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(¤tIPs, &n.NetMap.Addresses) {
|
||||
if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil {
|
||||
log.Fatalf("installing proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name}
|
||||
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(¤tDeviceInfo, &deviceInfo) {
|
||||
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil {
|
||||
log.Fatalf("storing device ID in kube secret: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !startupTasksDone {
|
||||
if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) {
|
||||
// This log message is used in tests to detect when all
|
||||
// post-auth configuration is done.
|
||||
log.Println("Startup complete, waiting for shutdown signal")
|
||||
startupTasksDone = true
|
||||
|
||||
// Reap all processes, since we are PID1 and need to collect zombies. We can
|
||||
// only start doing this once we've stopped shelling out to things
|
||||
// `tailscale up`, otherwise this goroutine can reap the CLI subprocesses
|
||||
// and wedge bringup.
|
||||
go func() {
|
||||
for {
|
||||
var status unix.WaitStatus
|
||||
pid, err := unix.Wait4(-1, &status, 0, nil)
|
||||
if errors.Is(err, unix.EINTR) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Waiting for exited processes: %v", err)
|
||||
}
|
||||
if pid == daemonPid {
|
||||
log.Printf("Tailscaled exited")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startAndAuthTailscaled starts the tailscale daemon and attempts to
|
||||
// auth it, according to the settings in cfg. If successful, returns
|
||||
// tailscaled's Status and pid.
|
||||
func startAndAuthTailscaled(ctx context.Context, cfg *settings) (*ipnstate.Status, int, error) {
|
||||
func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, int, error) {
|
||||
args := tailscaledArgs(cfg)
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT)
|
||||
@@ -198,8 +326,7 @@ func startAndAuthTailscaled(ctx context.Context, cfg *settings) (*ipnstate.Statu
|
||||
cmd.Process.Signal(unix.SIGTERM)
|
||||
}()
|
||||
|
||||
// Wait for the socket file to appear, otherwise 'tailscale up'
|
||||
// can fail.
|
||||
// Wait for the socket file to appear, otherwise API ops will racily fail.
|
||||
log.Printf("Waiting for tailscaled socket")
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
@@ -215,57 +342,12 @@ func startAndAuthTailscaled(ctx context.Context, cfg *settings) (*ipnstate.Statu
|
||||
break
|
||||
}
|
||||
|
||||
didLogin := false
|
||||
if !cfg.AuthOnce {
|
||||
if err := tailscaleUp(ctx, cfg); err != nil {
|
||||
return nil, 0, fmt.Errorf("couldn't log in: %v", err)
|
||||
}
|
||||
didLogin = true
|
||||
}
|
||||
|
||||
tsClient := tailscale.LocalClient{
|
||||
tsClient := &tailscale.LocalClient{
|
||||
Socket: cfg.Socket,
|
||||
UseSocketOnly: true,
|
||||
}
|
||||
|
||||
// Poll for daemon state until it goes to either Running or
|
||||
// NeedsLogin. The latter only happens if cfg.AuthOnce is true,
|
||||
// because in that case we only try to auth when it's necessary to
|
||||
// reach the running state.
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil, 0, ctx.Err()
|
||||
}
|
||||
|
||||
loopCtx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
st, err := tsClient.Status(loopCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("Getting tailscaled state: %w", err)
|
||||
}
|
||||
|
||||
switch st.BackendState {
|
||||
case "Running":
|
||||
if len(st.TailscaleIPs) > 0 {
|
||||
return st, cmd.Process.Pid, nil
|
||||
}
|
||||
log.Printf("No Tailscale IPs assigned yet")
|
||||
case "NeedsLogin":
|
||||
if !didLogin {
|
||||
// Alas, we cannot currently trigger an authkey login from
|
||||
// LocalAPI, so we still have to shell out to the
|
||||
// tailscale CLI for this bit.
|
||||
if err := tailscaleUp(ctx, cfg); err != nil {
|
||||
return nil, 0, fmt.Errorf("couldn't log in: %v", err)
|
||||
}
|
||||
didLogin = true
|
||||
}
|
||||
default:
|
||||
log.Printf("tailscaled in state %q, waiting", st.BackendState)
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return tsClient, cmd.Process.Pid, nil
|
||||
}
|
||||
|
||||
// tailscaledArgs uses cfg to construct the argv for tailscaled.
|
||||
@@ -275,7 +357,7 @@ func tailscaledArgs(cfg *settings) []string {
|
||||
case cfg.InKubernetes && cfg.KubeSecret != "":
|
||||
args = append(args, "--state=kube:"+cfg.KubeSecret, "--statedir=/tmp")
|
||||
case cfg.StateDir != "":
|
||||
args = append(args, "--state="+cfg.StateDir)
|
||||
args = append(args, "--statedir="+cfg.StateDir)
|
||||
default:
|
||||
args = append(args, "--state=mem:", "--statedir=/tmp")
|
||||
}
|
||||
@@ -402,7 +484,7 @@ func ensureIPForwarding(root, proxyTo, routes string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Addr) error {
|
||||
func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -412,16 +494,22 @@ func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Addr)
|
||||
argv0 = "ip6tables"
|
||||
}
|
||||
var local string
|
||||
for _, ip := range tsIPs {
|
||||
if ip.Is4() != dst.Is4() {
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
local = ip.String()
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr().String()
|
||||
break
|
||||
}
|
||||
if local == "" {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
// Technically, if the control server ever changes the IPs assigned to this
|
||||
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
|
||||
// for now we'll live with it.
|
||||
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -433,32 +521,42 @@ func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Addr)
|
||||
|
||||
// settings is all the configuration for containerboot.
|
||||
type settings struct {
|
||||
AuthKey string
|
||||
Routes string
|
||||
ProxyTo string
|
||||
DaemonExtraArgs string
|
||||
ExtraArgs string
|
||||
InKubernetes bool
|
||||
UserspaceMode bool
|
||||
StateDir string
|
||||
AcceptDNS bool
|
||||
KubeSecret string
|
||||
SOCKSProxyAddr string
|
||||
HTTPProxyAddr string
|
||||
Socket string
|
||||
AuthOnce bool
|
||||
Root string
|
||||
AuthKey string
|
||||
Routes string
|
||||
ProxyTo string
|
||||
DaemonExtraArgs string
|
||||
ExtraArgs string
|
||||
InKubernetes bool
|
||||
UserspaceMode bool
|
||||
StateDir string
|
||||
AcceptDNS bool
|
||||
KubeSecret string
|
||||
SOCKSProxyAddr string
|
||||
HTTPProxyAddr string
|
||||
Socket string
|
||||
AuthOnce bool
|
||||
Root string
|
||||
KubernetesCanPatch bool
|
||||
}
|
||||
|
||||
// defaultEnv returns the value of the given envvar name, or defVal if
|
||||
// unset.
|
||||
func defaultEnv(name, defVal string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
if v, ok := os.LookupEnv(name); ok {
|
||||
return v
|
||||
}
|
||||
return defVal
|
||||
}
|
||||
|
||||
func defaultEnvs(names []string, defVal string) string {
|
||||
for _, name := range names {
|
||||
if v, ok := os.LookupEnv(name); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defVal
|
||||
}
|
||||
|
||||
// defaultBool returns the boolean value of the given envvar name, or
|
||||
// defVal if unset or not a bool.
|
||||
func defaultBool(name string, defVal bool) bool {
|
||||
|
||||
@@ -31,8 +31,11 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestContainerBoot(t *testing.T) {
|
||||
@@ -90,190 +93,246 @@ func TestContainerBoot(t *testing.T) {
|
||||
}
|
||||
|
||||
argFile := filepath.Join(d, "args")
|
||||
tsIPs := []netip.Addr{netip.MustParseAddr("100.64.0.1")}
|
||||
runningSockPath := filepath.Join(d, "tmp/tailscaled.sock")
|
||||
|
||||
// TODO: refactor this 1-2 stuff if we ever need a third
|
||||
// step. Right now all of containerboot's modes either converge
|
||||
// with no further interaction needed, or with one extra step
|
||||
// only.
|
||||
tests := []struct {
|
||||
Name string
|
||||
Env map[string]string
|
||||
KubeSecret map[string]string
|
||||
WantArgs1 []string // Wait for containerboot to run these commands...
|
||||
Status1 ipnstate.Status // ... then report this status in LocalAPI.
|
||||
WantArgs2 []string // If non-nil, wait for containerboot to run these additional commands...
|
||||
Status2 ipnstate.Status // ... then report this status in LocalAPI.
|
||||
type phase struct {
|
||||
// If non-nil, send this IPN bus notification (and remember it as the
|
||||
// initial update for any future new watchers, then wait for all the
|
||||
// Waits below to be true before proceeding to the next phase.
|
||||
Notify *ipn.Notify
|
||||
|
||||
// WantCmds is the commands that containerboot should run in this phase.
|
||||
WantCmds []string
|
||||
// WantKubeSecret is the secret keys/values that should exist in the
|
||||
// kube secret.
|
||||
WantKubeSecret map[string]string
|
||||
WantFiles map[string]string
|
||||
// WantFiles files that should exist in the container and their
|
||||
// contents.
|
||||
WantFiles map[string]string
|
||||
}
|
||||
runningNotify := &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
NetMap: &netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("myID"),
|
||||
Name: "test-node.test.ts.net",
|
||||
},
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
Name string
|
||||
Env map[string]string
|
||||
KubeSecret map[string]string
|
||||
KubeDenyPatch bool
|
||||
Phases []phase
|
||||
}{
|
||||
{
|
||||
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
|
||||
Name: "no_args",
|
||||
Env: nil,
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
// The tailscale up call blocks until auth is complete, so
|
||||
// by the time it returns the next converged state is
|
||||
// Running.
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey-old-flag",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authkey_disk_state",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes_kernel_ipv4",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes_kernel_ipv6",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1::/64",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes_kernel_all_families",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1.2.3.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "proxy",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_DEST_IP": "1.2.3.4",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
},
|
||||
WantArgs2: []string{
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
|
||||
},
|
||||
Status2: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authkey_once",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "true",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "NeedsLogin",
|
||||
},
|
||||
WantArgs2: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status2: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: &ipn.Notify{
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -285,20 +344,72 @@ func TestContainerBoot(t *testing.T) {
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Self: &ipnstate.PeerStatus{
|
||||
ID: tailcfg.StableNodeID("myID"),
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
"device_id": "myID",
|
||||
},
|
||||
},
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_id": "myID",
|
||||
},
|
||||
{
|
||||
Name: "kube_disk_storage",
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
// Explicitly set to an empty value, to override the default of "tailscale".
|
||||
"TS_KUBE_SECRET": "",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kube_storage_no_patch",
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
KubeDenyPatch: true,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -312,24 +423,79 @@ func TestContainerBoot(t *testing.T) {
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "NeedsLogin",
|
||||
},
|
||||
WantArgs2: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
Status2: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Self: &ipnstate.PeerStatus{
|
||||
ID: tailcfg.StableNodeID("myID"),
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: &ipn.Notify{
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
"device_id": "myID",
|
||||
},
|
||||
},
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"device_id": "myID",
|
||||
},
|
||||
{
|
||||
Name: "kube_storage_updates",
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
},
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
"device_id": "myID",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
NetMap: &netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("newID"),
|
||||
Name: "new-name.test.ts.net",
|
||||
},
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
},
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "new-name.test.ts.net",
|
||||
"device_id": "newID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -338,16 +504,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_SOCKS5_SERVER": "localhost:1080",
|
||||
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
// The tailscale up call blocks until auth is complete, so
|
||||
// by the time it returns the next converged state is
|
||||
// Running.
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -355,13 +521,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_ACCEPT_DNS": "true",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -370,13 +539,15 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_EXTRA_ARGS": "--widget=rotated",
|
||||
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
|
||||
},
|
||||
WantArgs1: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
|
||||
},
|
||||
Status1: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -392,6 +563,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
for k, v := range test.KubeSecret {
|
||||
kube.SetSecret(k, v)
|
||||
}
|
||||
kube.SetPatching(!test.KubeDenyPatch)
|
||||
|
||||
cmd := exec.Command(boot)
|
||||
cmd.Env = []string{
|
||||
@@ -419,35 +591,45 @@ func TestContainerBoot(t *testing.T) {
|
||||
cmd.Process.Wait()
|
||||
}()
|
||||
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(test.WantArgs1, "\n"))
|
||||
lapi.SetStatus(test.Status1)
|
||||
if test.WantArgs2 != nil {
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(append(test.WantArgs1, test.WantArgs2...), "\n"))
|
||||
lapi.SetStatus(test.Status2)
|
||||
var wantCmds []string
|
||||
for _, p := range test.Phases {
|
||||
lapi.Notify(p.Notify)
|
||||
wantCmds = append(wantCmds, p.WantCmds...)
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
if p.WantKubeSecret != nil {
|
||||
got := kube.Secret()
|
||||
if diff := cmp.Diff(got, p.WantKubeSecret); diff != "" {
|
||||
return fmt.Errorf("unexpected kube secret data (-got+want):\n%s", diff)
|
||||
}
|
||||
} else {
|
||||
got := kube.Secret()
|
||||
if len(got) > 0 {
|
||||
return fmt.Errorf("kube secret unexpectedly not empty, got %#v", got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tstest.WaitFor(2*time.Second, func() error {
|
||||
for path, want := range p.WantFiles {
|
||||
gotBs, err := os.ReadFile(filepath.Join(d, path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading wanted file %q: %v", path, err)
|
||||
}
|
||||
if got := strings.TrimSpace(string(gotBs)); got != want {
|
||||
return fmt.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
|
||||
|
||||
if test.WantKubeSecret != nil {
|
||||
got := kube.Secret()
|
||||
if diff := cmp.Diff(got, test.WantKubeSecret); diff != "" {
|
||||
t.Fatalf("unexpected kube secret data (-got+want):\n%s", diff)
|
||||
}
|
||||
} else {
|
||||
got := kube.Secret()
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("kube secret unexpectedly not empty, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
for path, want := range test.WantFiles {
|
||||
gotBs, err := os.ReadFile(filepath.Join(d, path))
|
||||
if err != nil {
|
||||
t.Fatalf("reading wanted file %q: %v", path, err)
|
||||
}
|
||||
if got := strings.TrimSpace(string(gotBs)); got != want {
|
||||
t.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -547,7 +729,8 @@ type localAPI struct {
|
||||
srv *http.Server
|
||||
|
||||
sync.Mutex
|
||||
status ipnstate.Status
|
||||
cond *sync.Cond
|
||||
notify *ipn.Notify
|
||||
}
|
||||
|
||||
func (l *localAPI) Start() error {
|
||||
@@ -565,6 +748,7 @@ func (l *localAPI) Start() error {
|
||||
Handler: l,
|
||||
}
|
||||
l.Path = path
|
||||
l.cond = sync.NewCond(&l.Mutex)
|
||||
go l.srv.Serve(ln)
|
||||
return nil
|
||||
}
|
||||
@@ -574,29 +758,49 @@ func (l *localAPI) Close() {
|
||||
}
|
||||
|
||||
func (l *localAPI) Reset() {
|
||||
l.SetStatus(ipnstate.Status{
|
||||
BackendState: "NoState",
|
||||
})
|
||||
}
|
||||
|
||||
func (l *localAPI) SetStatus(st ipnstate.Status) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.status = st
|
||||
l.notify = nil
|
||||
l.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (l *localAPI) Notify(n *ipn.Notify) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.notify = n
|
||||
l.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
panic(fmt.Sprintf("unsupported method %q", r.Method))
|
||||
}
|
||||
if r.URL.Path != "/localapi/v0/status" {
|
||||
panic(fmt.Sprintf("unsupported localAPI path %q", r.URL.Path))
|
||||
if r.URL.Path != "/localapi/v0/watch-ipn-bus" {
|
||||
panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
if err := json.NewEncoder(w).Encode(l.status); err != nil {
|
||||
panic("json encode failed")
|
||||
for {
|
||||
if l.notify != nil {
|
||||
if err := enc.Encode(l.notify); err != nil {
|
||||
// Usually broken pipe as the test client disconnects.
|
||||
return
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
l.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,7 +816,8 @@ type kubeServer struct {
|
||||
srv *httptest.Server
|
||||
|
||||
sync.Mutex
|
||||
secret map[string]string
|
||||
secret map[string]string
|
||||
canPatch bool
|
||||
}
|
||||
|
||||
func (k *kubeServer) Secret() map[string]string {
|
||||
@@ -631,6 +836,12 @@ func (k *kubeServer) SetSecret(key, val string) {
|
||||
k.secret[key] = val
|
||||
}
|
||||
|
||||
func (k *kubeServer) SetPatching(canPatch bool) {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
k.canPatch = canPatch
|
||||
}
|
||||
|
||||
func (k *kubeServer) Reset() {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
@@ -674,10 +885,39 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer bearer_token" {
|
||||
panic("client didn't provide bearer token in request")
|
||||
}
|
||||
if r.URL.Path != "/api/v1/namespaces/default/secrets/tailscale" {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/namespaces/default/secrets/tailscale":
|
||||
k.serveSecret(w, r)
|
||||
case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
|
||||
k.serveSSAR(w, r)
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Spec struct {
|
||||
ResourceAttributes struct {
|
||||
Verb string `json:"verb"`
|
||||
} `json:"resourceAttributes"`
|
||||
} `json:"spec"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
panic(fmt.Sprintf("decoding SSAR request: %v", err))
|
||||
}
|
||||
ok := true
|
||||
if req.Spec.ResourceAttributes.Verb == "patch" {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
ok = k.canPatch
|
||||
}
|
||||
// Just say yes to all SARs, we don't enforce RBAC.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
|
||||
@@ -688,7 +928,7 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "GET":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
ret := map[string]map[string]string{
|
||||
"data": map[string]string{},
|
||||
"data": {},
|
||||
}
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
@@ -703,6 +943,11 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
panic("encode failed")
|
||||
}
|
||||
case "PATCH":
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
if !k.canPatch {
|
||||
panic("containerboot tried to patch despite not being allowed")
|
||||
}
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case "application/json-patch+json":
|
||||
req := []struct {
|
||||
@@ -712,8 +957,6 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
for _, op := range req {
|
||||
if op.Op != "remove" {
|
||||
panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
|
||||
@@ -730,8 +973,6 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
for key, val := range req.Data {
|
||||
k.secret[key] = val
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
@@ -66,6 +69,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnstate+
|
||||
@@ -74,11 +78,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/syncs
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
@@ -98,7 +101,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
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+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr
|
||||
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
@@ -183,6 +186,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W os/user from tailscale.com/util/winutil
|
||||
path from golang.org/x/crypto/acme/autocert+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/strs"
|
||||
)
|
||||
|
||||
func startMesh(s *derp.Server) error {
|
||||
@@ -50,8 +51,7 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
}
|
||||
var d net.Dialer
|
||||
var r net.Resolver
|
||||
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
|
||||
base := strings.TrimSuffix(host, ".tailscale.com")
|
||||
if base, ok := strs.CutSuffix(host, ".tailscale.com"); ok && port == "443" {
|
||||
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
vpcHost := base + "-vpc.tailscale.com"
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
||||
)
|
||||
|
||||
// certReissueAfter is the time after which we expect all certs to be
|
||||
@@ -63,6 +64,20 @@ func main() {
|
||||
defer cancel()
|
||||
_, _ = getDERPMap(ctx)
|
||||
|
||||
if *probeOnce {
|
||||
log.Printf("Starting probe (may take up to 1m)")
|
||||
probe()
|
||||
log.Printf("Probe results:")
|
||||
st := getOverallStatus()
|
||||
for _, s := range st.good {
|
||||
log.Printf("good: %s", s)
|
||||
}
|
||||
for _, s := range st.bad {
|
||||
log.Printf("bad: %s", s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
go probeLoop()
|
||||
go slackLoop()
|
||||
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
|
||||
|
||||
@@ -31,6 +31,7 @@ var (
|
||||
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)")
|
||||
apiServer = rootFlagSet.String("api-server", "api.tailscale.com", "API server to contact")
|
||||
)
|
||||
|
||||
func modifiedExternallyError() {
|
||||
@@ -234,7 +235,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), fin)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), fin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -274,7 +275,7 @@ func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), bytes.NewBuffer(data))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl/validate", *apiServer, tailnet), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -346,7 +347,7 @@ type ACLTestErrorDetail struct {
|
||||
}
|
||||
|
||||
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)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
155
cmd/k8s-operator/manifests/operator.yaml
Normal file
155
cmd/k8s-operator/manifests/operator.yaml
Normal file
@@ -0,0 +1,155 @@
|
||||
# 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.
|
||||
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: tailscale
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: proxies
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: tailscale-operator
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services", "services/status"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: tailscale-operator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: tailscale-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["statefulsets"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: operator-oauth
|
||||
namespace: tailscale
|
||||
stringData:
|
||||
client_id: # SET CLIENT ID HERE
|
||||
client_secret: # SET CLIENT SECRET HERE
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: operator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: operator
|
||||
spec:
|
||||
serviceAccountName: operator
|
||||
volumes:
|
||||
- name: oauth
|
||||
secret:
|
||||
secretName: operator-oauth
|
||||
containers:
|
||||
- name: operator
|
||||
image: tailscale/k8s-operator:latest
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 100Mi
|
||||
env:
|
||||
- name: OPERATOR_HOSTNAME
|
||||
value: tailscale-operator
|
||||
- name: OPERATOR_SECRET
|
||||
value: operator
|
||||
- name: OPERATOR_LOGGING
|
||||
value: info
|
||||
- name: OPERATOR_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: CLIENT_ID_FILE
|
||||
value: /oauth/client_id
|
||||
- name: CLIENT_SECRET_FILE
|
||||
value: /oauth/client_secret
|
||||
- name: PROXY_IMAGE
|
||||
value: tailscale/tailscale:latest
|
||||
- name: PROXY_TAGS
|
||||
value: tag:k8s
|
||||
volumeMounts:
|
||||
- name: oauth
|
||||
mountPath: /oauth
|
||||
readOnly: true
|
||||
37
cmd/k8s-operator/manifests/proxy.yaml
Normal file
37
cmd/k8s-operator/manifests/proxy.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# This file is not a complete manifest, it's a skeleton that the operator embeds
|
||||
# at build time and then uses to construct Tailscale proxy pods.
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
deletionGracePeriodSeconds: 10
|
||||
spec:
|
||||
serviceAccountName: proxies
|
||||
initContainers:
|
||||
- name: sysctler
|
||||
image: busybox
|
||||
securityContext:
|
||||
privileged: true
|
||||
command: ["/bin/sh"]
|
||||
args:
|
||||
- -c
|
||||
- sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1m
|
||||
memory: 1Mi
|
||||
containers:
|
||||
- name: tailscale
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: TS_USERSPACE
|
||||
value: "false"
|
||||
- name: TS_AUTH_ONCE
|
||||
value: "true"
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
685
cmd/k8s-operator/operator.go
Normal file
685
cmd/k8s-operator/operator.go
Normal file
@@ -0,0 +1,685 @@
|
||||
// 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.
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
kzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
"sigs.k8s.io/yaml"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/kubestore"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
// client lives in the same repo as this code.
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
var (
|
||||
hostname = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
|
||||
kubeSecret = defaultEnv("OPERATOR_SECRET", "")
|
||||
operatorTags = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
|
||||
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
|
||||
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
|
||||
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
|
||||
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
|
||||
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
||||
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
||||
)
|
||||
|
||||
var opts []kzap.Opts
|
||||
switch tslogging {
|
||||
case "info":
|
||||
opts = append(opts, kzap.Level(zapcore.InfoLevel))
|
||||
case "debug":
|
||||
opts = append(opts, kzap.Level(zapcore.DebugLevel))
|
||||
case "dev":
|
||||
opts = append(opts, kzap.UseDevMode(true), kzap.Level(zapcore.DebugLevel))
|
||||
}
|
||||
zlog := kzap.NewRaw(opts...).Sugar()
|
||||
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
|
||||
startlog := zlog.Named("startup")
|
||||
|
||||
if clientIDPath == "" || clientSecretPath == "" {
|
||||
startlog.Fatalf("CLIENT_ID_FILE and CLIENT_SECRET_FILE must be set")
|
||||
}
|
||||
clientID, err := os.ReadFile(clientIDPath)
|
||||
if err != nil {
|
||||
startlog.Fatalf("reading client ID %q: %v", clientIDPath, err)
|
||||
}
|
||||
clientSecret, err := os.ReadFile(clientSecretPath)
|
||||
if err != nil {
|
||||
startlog.Fatalf("reading client secret %q: %v", clientSecretPath, err)
|
||||
}
|
||||
credentials := clientcredentials.Config{
|
||||
ClientID: string(clientID),
|
||||
ClientSecret: string(clientSecret),
|
||||
TokenURL: "https://login.tailscale.com/api/v2/oauth/token",
|
||||
}
|
||||
tsClient := tailscale.NewClient("-", nil)
|
||||
tsClient.HTTPClient = credentials.Client(context.Background())
|
||||
s := &tsnet.Server{
|
||||
Hostname: hostname,
|
||||
Logf: zlog.Named("tailscaled").Debugf,
|
||||
}
|
||||
if kubeSecret != "" {
|
||||
st, err := kubestore.New(logger.Discard, kubeSecret)
|
||||
if err != nil {
|
||||
startlog.Fatalf("creating kube store: %v", err)
|
||||
}
|
||||
s.Store = st
|
||||
}
|
||||
if err := s.Start(); err != nil {
|
||||
startlog.Fatalf("starting tailscale server: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
startlog.Fatalf("getting local client: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
loginDone := false
|
||||
machineAuthShown := false
|
||||
waitOnline:
|
||||
for {
|
||||
startlog.Debugf("querying tailscaled status")
|
||||
st, err := lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
startlog.Fatalf("getting status: %v", err)
|
||||
}
|
||||
switch st.BackendState {
|
||||
case "Running":
|
||||
break waitOnline
|
||||
case "NeedsLogin":
|
||||
if loginDone {
|
||||
break
|
||||
}
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
Reusable: false,
|
||||
Preauthorized: true,
|
||||
Tags: strings.Split(operatorTags, ","),
|
||||
},
|
||||
},
|
||||
}
|
||||
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
startlog.Fatalf("creating operator authkey: %v", err)
|
||||
}
|
||||
if err := lc.Start(ctx, ipn.Options{
|
||||
AuthKey: authkey,
|
||||
}); err != nil {
|
||||
startlog.Fatalf("starting tailscale: %v", err)
|
||||
}
|
||||
if err := lc.StartLoginInteractive(ctx); err != nil {
|
||||
startlog.Fatalf("starting login: %v", err)
|
||||
}
|
||||
startlog.Debugf("requested login by authkey")
|
||||
loginDone = true
|
||||
case "NeedsMachineAuth":
|
||||
if !machineAuthShown {
|
||||
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
|
||||
machineAuthShown = true
|
||||
}
|
||||
default:
|
||||
startlog.Debugf("waiting for tailscale to start: %v", st.BackendState)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
sr := &ServiceReconciler{
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
}
|
||||
|
||||
// For secrets and statefulsets, we only get permission to touch the objects
|
||||
// in the controller's own namespace. This cannot be expressed by
|
||||
// .Watches(...) below, instead you have to add a per-type field selector to
|
||||
// the cache that sits a few layers below the builder stuff, which will
|
||||
// implicitly filter what parts of the world the builder code gets to see at
|
||||
// all.
|
||||
nsFilter := cache.ObjectSelector{
|
||||
Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
|
||||
}
|
||||
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{
|
||||
NewCache: cache.BuilderWithOptions(cache.Options{
|
||||
SelectorsByObject: map[client.Object]cache.ObjectSelector{
|
||||
&corev1.Secret{}: nsFilter,
|
||||
&appsv1.StatefulSet{}: nsFilter,
|
||||
},
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create manager: %v", err)
|
||||
}
|
||||
|
||||
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
|
||||
ls := o.GetLabels()
|
||||
if ls[LabelManaged] != "true" {
|
||||
return nil
|
||||
}
|
||||
if ls[LabelParentType] != "svc" {
|
||||
return nil
|
||||
}
|
||||
return []reconcile.Request{
|
||||
{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: ls[LabelParentNamespace],
|
||||
Name: ls[LabelParentName],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
err = builder.
|
||||
ControllerManagedBy(mgr).
|
||||
For(&corev1.Service{}).
|
||||
Watches(&source.Kind{Type: &appsv1.StatefulSet{}}, reconcileFilter).
|
||||
Watches(&source.Kind{Type: &corev1.Secret{}}, reconcileFilter).
|
||||
Complete(sr)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create controller: %v", err)
|
||||
}
|
||||
|
||||
startlog.Infof("Startup complete, operator running")
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
startlog.Fatalf("could not start manager: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
LabelManaged = "tailscale.com/managed"
|
||||
LabelParentType = "tailscale.com/parent-resource-type"
|
||||
LabelParentName = "tailscale.com/parent-resource"
|
||||
LabelParentNamespace = "tailscale.com/parent-resource-ns"
|
||||
|
||||
FinalizerName = "tailscale.com/finalizer"
|
||||
|
||||
AnnotationExpose = "tailscale.com/expose"
|
||||
AnnotationTags = "tailscale.com/tags"
|
||||
)
|
||||
|
||||
// ServiceReconciler is a simple ControllerManagedBy example implementation.
|
||||
type ServiceReconciler struct {
|
||||
client.Client
|
||||
tsClient tsClient
|
||||
defaultTags []string
|
||||
operatorNamespace string
|
||||
proxyImage string
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type tsClient interface {
|
||||
CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error)
|
||||
DeleteDevice(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
func childResourceLabels(parent *corev1.Service) map[string]string {
|
||||
// You might wonder why we're using owner references, since they seem to be
|
||||
// built for exactly this. Unfortunately, Kubernetes does not support
|
||||
// cross-namespace ownership, by design. This means we cannot make the
|
||||
// service being exposed the owner of the implementation details of the
|
||||
// proxying. Instead, we have to do our own filtering and tracking with
|
||||
// labels.
|
||||
return map[string]string{
|
||||
LabelManaged: "true",
|
||||
LabelParentName: parent.GetName(),
|
||||
LabelParentNamespace: parent.GetNamespace(),
|
||||
LabelParentType: "svc",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
|
||||
logger := a.logger.With("service-ns", req.Namespace, "service-name", req.Name)
|
||||
logger.Debugf("starting reconcile")
|
||||
defer logger.Debugf("reconcile finished")
|
||||
|
||||
svc := new(corev1.Service)
|
||||
err = a.Get(ctx, req.NamespacedName, svc)
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Request object not found, could have been deleted after reconcile request.
|
||||
logger.Debugf("service not found, assuming it was deleted")
|
||||
return reconcile.Result{}, nil
|
||||
} else if err != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("failed to get svc: %w", err)
|
||||
}
|
||||
if !svc.DeletionTimestamp.IsZero() || !a.shouldExpose(svc) {
|
||||
logger.Debugf("service is being deleted or should not be exposed, cleaning up")
|
||||
return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc)
|
||||
}
|
||||
|
||||
return reconcile.Result{}, a.maybeProvision(ctx, logger, svc)
|
||||
}
|
||||
|
||||
// maybeCleanup removes any existing resources related to serving svc over tailscale.
|
||||
//
|
||||
// This function is responsible for removing the finalizer from the service,
|
||||
// once all associated resources are gone.
|
||||
func (a *ServiceReconciler) maybeCleanup(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) error {
|
||||
ix := slices.Index(svc.Finalizers, FinalizerName)
|
||||
if ix < 0 {
|
||||
logger.Debugf("no finalizer, nothing to do")
|
||||
return nil
|
||||
}
|
||||
|
||||
ml := childResourceLabels(svc)
|
||||
|
||||
// Need to delete the StatefulSet first, and delete it with foreground
|
||||
// cascading deletion. That way, the pod that's writing to the Secret will
|
||||
// stop running before we start looking at the Secret's contents, and
|
||||
// assuming k8s ordering semantics don't mess with us, that should avoid
|
||||
// tailscale device deletion races where we fail to notice a device that
|
||||
// should be removed.
|
||||
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, ml)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting statefulset: %w", err)
|
||||
}
|
||||
if sts != nil {
|
||||
if !sts.GetDeletionTimestamp().IsZero() {
|
||||
// Deletion in progress, check again later. We'll get another
|
||||
// notification when the deletion is complete.
|
||||
logger.Debugf("waiting for statefulset %s/%s deletion", sts.GetNamespace(), sts.GetName())
|
||||
return nil
|
||||
}
|
||||
err := a.DeleteAllOf(ctx, &appsv1.StatefulSet{}, client.InNamespace(a.operatorNamespace), client.MatchingLabels(ml), client.PropagationPolicy(metav1.DeletePropagationForeground))
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting statefulset: %w", err)
|
||||
}
|
||||
logger.Debugf("started deletion of statefulset %s/%s", sts.GetNamespace(), sts.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
id, _, err := a.getDeviceInfo(ctx, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting device info: %w", err)
|
||||
}
|
||||
if id != "" {
|
||||
// TODO: handle case where the device is already deleted, but the secret
|
||||
// is still around.
|
||||
if err := a.tsClient.DeleteDevice(ctx, id); err != nil {
|
||||
return fmt.Errorf("deleting device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
types := []client.Object{
|
||||
&corev1.Service{},
|
||||
&corev1.Secret{},
|
||||
}
|
||||
for _, typ := range types {
|
||||
if err := a.DeleteAllOf(ctx, typ, client.InNamespace(a.operatorNamespace), client.MatchingLabels(ml)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
svc.Finalizers = append(svc.Finalizers[:ix], svc.Finalizers[ix+1:]...)
|
||||
if err := a.Update(ctx, svc); err != nil {
|
||||
return fmt.Errorf("failed to remove finalizer: %w", err)
|
||||
}
|
||||
|
||||
// Unlike most log entries in the reconcile loop, this will get printed
|
||||
// exactly once at the very end of cleanup, because the final step of
|
||||
// cleanup removes the tailscale finalizer, which will make all future
|
||||
// reconciles exit early.
|
||||
logger.Infof("unexposed service from tailnet")
|
||||
return nil
|
||||
}
|
||||
|
||||
// maybeProvision ensures that svc is exposed over tailscale, taking any actions
|
||||
// necessary to reach that state.
|
||||
//
|
||||
// This function adds a finalizer to svc, ensuring that we can handle orderly
|
||||
// deprovisioning later.
|
||||
func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) error {
|
||||
if !slices.Contains(svc.Finalizers, FinalizerName) {
|
||||
// This log line is printed exactly once during initial provisioning,
|
||||
// because once the finalizer is in place this block gets skipped. So,
|
||||
// this is a nice place to tell the operator that the high level,
|
||||
// multi-reconcile operation is underway.
|
||||
logger.Infof("exposing service over tailscale")
|
||||
svc.Finalizers = append(svc.Finalizers, FinalizerName)
|
||||
if err := a.Update(ctx, svc); err != nil {
|
||||
return fmt.Errorf("failed to add finalizer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Do full reconcile.
|
||||
hsvc, err := a.reconcileHeadlessService(ctx, logger, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconcile headless service: %w", err)
|
||||
}
|
||||
|
||||
tags := a.defaultTags
|
||||
if tstr, ok := svc.Annotations[AnnotationTags]; ok {
|
||||
tags = strings.Split(tstr, ",")
|
||||
}
|
||||
secretName, err := a.createOrGetSecret(ctx, logger, svc, hsvc, tags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create or get API key secret: %w", err)
|
||||
}
|
||||
_, err = a.reconcileSTS(ctx, logger, svc, hsvc, secretName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconcile statefulset: %w", err)
|
||||
}
|
||||
|
||||
if !a.hasLoadBalancerClass(svc) {
|
||||
logger.Debugf("service is not a LoadBalancer, so not updating ingress")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, tsHost, err := a.getDeviceInfo(ctx, svc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device ID: %w", err)
|
||||
}
|
||||
if tsHost == "" {
|
||||
logger.Debugf("no Tailscale hostname known yet, waiting for proxy pod to finish auth")
|
||||
// No hostname yet. Wait for the proxy pod to auth.
|
||||
svc.Status.LoadBalancer.Ingress = nil
|
||||
if err := a.Status().Update(ctx, svc); err != nil {
|
||||
return fmt.Errorf("failed to update service status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debugf("setting ingress hostname to %q", tsHost)
|
||||
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: tsHost,
|
||||
},
|
||||
}
|
||||
if err := a.Status().Update(ctx, svc); err != nil {
|
||||
return fmt.Errorf("failed to update service status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) shouldExpose(svc *corev1.Service) bool {
|
||||
// Headless services can't be exposed, since there is no ClusterIP to
|
||||
// forward to.
|
||||
if svc.Spec.ClusterIP == "" || svc.Spec.ClusterIP == "None" {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.hasLoadBalancerClass(svc) || a.hasAnnotation(svc)
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
|
||||
return svc != nil &&
|
||||
svc.Spec.Type == corev1.ServiceTypeLoadBalancer &&
|
||||
svc.Spec.LoadBalancerClass != nil &&
|
||||
*svc.Spec.LoadBalancerClass == "tailscale"
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) hasAnnotation(svc *corev1.Service) bool {
|
||||
return svc != nil &&
|
||||
svc.Annotations[AnnotationExpose] == "true"
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) (*corev1.Service, error) {
|
||||
hsvc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "ts-" + svc.Name + "-",
|
||||
Namespace: a.operatorNamespace,
|
||||
Labels: childResourceLabels(svc),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "None",
|
||||
Selector: map[string]string{
|
||||
"app": string(svc.UID),
|
||||
},
|
||||
},
|
||||
}
|
||||
logger.Debugf("reconciling headless service for StatefulSet")
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, hsvc, func(svc *corev1.Service) { svc.Spec = hsvc.Spec })
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, svc, hsvc *corev1.Service, tags []string) (string, error) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// Hardcode a -0 suffix so that in future, if we support
|
||||
// multiple StatefulSet replicas, we can provision -N for
|
||||
// those.
|
||||
Name: hsvc.Name + "-0",
|
||||
Namespace: a.operatorNamespace,
|
||||
Labels: childResourceLabels(svc),
|
||||
},
|
||||
}
|
||||
if err := a.Get(ctx, client.ObjectKeyFromObject(secret), secret); err == nil {
|
||||
logger.Debugf("secret %s/%s already exists", secret.GetNamespace(), secret.GetName())
|
||||
return secret.Name, nil
|
||||
} else if !apierrors.IsNotFound(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Secret doesn't exist yet, create one. Initially it contains
|
||||
// only the Tailscale authkey, but once Tailscale starts it'll
|
||||
// also store the daemon state.
|
||||
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, childResourceLabels(svc))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sts != nil {
|
||||
// StatefulSet exists, so we have already created the secret.
|
||||
// If the secret is missing, they should delete the StatefulSet.
|
||||
logger.Errorf("Tailscale proxy secret doesn't exist, but the corresponding StatefulSet %s/%s already does. Something is wrong, please delete the StatefulSet.", sts.GetNamespace(), sts.GetName())
|
||||
return "", nil
|
||||
}
|
||||
// Create API Key secret which is going to be used by the statefulset
|
||||
// to authenticate with Tailscale.
|
||||
logger.Debugf("creating authkey for new tailscale proxy")
|
||||
authKey, err := a.newAuthKey(ctx, tags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
secret.StringData = map[string]string{
|
||||
"authkey": authKey,
|
||||
}
|
||||
if err := a.Create(ctx, secret); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return secret.Name, nil
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) getDeviceInfo(ctx context.Context, svc *corev1.Service) (id, hostname string, err error) {
|
||||
sec, err := getSingleObject[corev1.Secret](ctx, a.Client, a.operatorNamespace, childResourceLabels(svc))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
id = string(sec.Data["device_id"])
|
||||
if id == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
// Kubernetes chokes on well-formed FQDNs with the trailing dot, so we have
|
||||
// to remove it.
|
||||
hostname = strings.TrimSuffix(string(sec.Data["device_fqdn"]), ".")
|
||||
if hostname == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
return id, hostname, nil
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) newAuthKey(ctx context.Context, tags []string) (string, error) {
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
Reusable: false,
|
||||
Preauthorized: true,
|
||||
Tags: tags,
|
||||
},
|
||||
},
|
||||
}
|
||||
key, _, err := a.tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
//go:embed manifests/proxy.yaml
|
||||
var proxyYaml []byte
|
||||
|
||||
func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, parentSvc, headlessSvc *corev1.Service, authKeySecret string) (*appsv1.StatefulSet, error) {
|
||||
var ss appsv1.StatefulSet
|
||||
if err := yaml.Unmarshal(proxyYaml, &ss); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal proxy spec: %w", err)
|
||||
}
|
||||
container := &ss.Spec.Template.Spec.Containers[0]
|
||||
container.Image = a.proxyImage
|
||||
container.Env = append(container.Env,
|
||||
corev1.EnvVar{
|
||||
Name: "TS_DEST_IP",
|
||||
Value: parentSvc.Spec.ClusterIP,
|
||||
},
|
||||
corev1.EnvVar{
|
||||
Name: "TS_KUBE_SECRET",
|
||||
Value: authKeySecret,
|
||||
})
|
||||
ss.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: headlessSvc.Name,
|
||||
Namespace: a.operatorNamespace,
|
||||
Labels: childResourceLabels(parentSvc),
|
||||
}
|
||||
ss.Spec.ServiceName = headlessSvc.Name
|
||||
ss.Spec.Selector = &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": string(parentSvc.UID),
|
||||
},
|
||||
}
|
||||
ss.Spec.Template.ObjectMeta.Labels = map[string]string{
|
||||
"app": string(parentSvc.UID),
|
||||
}
|
||||
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) InjectClient(c client.Client) error {
|
||||
a.Client = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// ptrObject is a type constraint for pointer types that implement
|
||||
// client.Object.
|
||||
type ptrObject[T any] interface {
|
||||
client.Object
|
||||
*T
|
||||
}
|
||||
|
||||
// createOrUpdate adds obj to the k8s cluster, unless the object already exists,
|
||||
// in which case update is called to make changes to it. If update is nil, the
|
||||
// existing object is returned unmodified.
|
||||
//
|
||||
// obj is looked up by its Name and Namespace if Name is set, otherwise it's
|
||||
// looked up by labels.
|
||||
func createOrUpdate[T any, O ptrObject[T]](ctx context.Context, c client.Client, ns string, obj O, update func(O)) (O, error) {
|
||||
var (
|
||||
existing O
|
||||
err error
|
||||
)
|
||||
if obj.GetName() != "" {
|
||||
existing = new(T)
|
||||
existing.SetName(obj.GetName())
|
||||
existing.SetNamespace(obj.GetNamespace())
|
||||
err = c.Get(ctx, client.ObjectKeyFromObject(obj), existing)
|
||||
} else {
|
||||
existing, err = getSingleObject[T, O](ctx, c, ns, obj.GetLabels())
|
||||
}
|
||||
if err == nil && existing != nil {
|
||||
if update != nil {
|
||||
update(existing)
|
||||
if err := c.Update(ctx, existing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("failed to get object: %w", err)
|
||||
}
|
||||
if err := c.Create(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// getSingleObject searches for k8s objects of type T
|
||||
// (e.g. corev1.Service) with the given labels, and returns
|
||||
// it. Returns nil if no objects match the labels, and an error if
|
||||
// more than one object matches.
|
||||
func getSingleObject[T any, O ptrObject[T]](ctx context.Context, c client.Client, ns string, labels map[string]string) (O, error) {
|
||||
ret := O(new(T))
|
||||
kinds, _, err := c.Scheme().ObjectKinds(ret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(kinds) != 1 {
|
||||
// TODO: the runtime package apparently has a "pick the best
|
||||
// GVK" function somewhere that might be good enough?
|
||||
return nil, fmt.Errorf("more than 1 GroupVersionKind for %T", ret)
|
||||
}
|
||||
|
||||
gvk := kinds[0]
|
||||
gvk.Kind += "List"
|
||||
lst := unstructured.UnstructuredList{}
|
||||
lst.SetGroupVersionKind(gvk)
|
||||
if err := c.List(ctx, &lst, client.InNamespace(ns), client.MatchingLabels(labels)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(lst.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(lst.Items) > 1 {
|
||||
return nil, fmt.Errorf("found multiple matching %T objects", ret)
|
||||
}
|
||||
if err := c.Scheme().Convert(&lst.Items[0], ret, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func defaultEnv(envName, defVal string) string {
|
||||
v := os.Getenv(envName)
|
||||
if v == "" {
|
||||
return defVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
739
cmd/k8s-operator/operator_test.go
Normal file
739
cmd/k8s-operator/operator_test.go
Normal file
@@ -0,0 +1,739 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go.uber.org/zap"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestLoadBalancerClass(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
// that we get to the end.
|
||||
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
|
||||
if s.Data == nil {
|
||||
s.Data = map[string][]byte{}
|
||||
}
|
||||
s.Data["device_id"] = []byte("ts-id-1234")
|
||||
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
Status: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "tailscale.device.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
|
||||
// Turn the service back into a ClusterIP service, which should make the
|
||||
// operator clean up.
|
||||
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
s.Spec.Type = corev1.ServiceTypeClusterIP
|
||||
s.Spec.LoadBalancerClass = nil
|
||||
// Fake client doesn't automatically delete the LoadBalancer status when
|
||||
// changing away from the LoadBalancer type, we have to do
|
||||
// controller-manager's work by hand.
|
||||
s.Status = corev1.ServiceStatus{}
|
||||
})
|
||||
// synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
|
||||
// didn't create any child resources since this is all faked, so the
|
||||
// deletion goes through immediately.
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
// The deletion triggers another reconcile, to finish the cleanup.
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
}
|
||||
|
||||
func TestAnnotations(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
|
||||
// Turn the service back into a ClusterIP service, which should make the
|
||||
// operator clean up.
|
||||
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
|
||||
})
|
||||
// synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
|
||||
// didn't create any child resources since this is all faked, so the
|
||||
// deletion goes through immediately.
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
// Second time around, the rest of cleanup happens.
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
}
|
||||
|
||||
func TestAnnotationIntoLB(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, since it would have normally happened at
|
||||
// this point and the LoadBalancer is going to expect this.
|
||||
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
|
||||
if s.Data == nil {
|
||||
s.Data = map[string][]byte{}
|
||||
}
|
||||
s.Data["device_id"] = []byte("ts-id-1234")
|
||||
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
UID: types.UID("1234-UID"),
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
|
||||
// Remove Tailscale's annotation, and at the same time convert the service
|
||||
// into a tailscale LoadBalancer.
|
||||
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
|
||||
s.Spec.Type = corev1.ServiceTypeLoadBalancer
|
||||
s.Spec.LoadBalancerClass = ptr.To("tailscale")
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
// None of the proxy machinery should have changed...
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
// ... but the service should have a LoadBalancer status.
|
||||
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
Status: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "tailscale.device.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
}
|
||||
|
||||
func TestLBIntoAnnotation(t *testing.T) {
|
||||
fc := fake.NewFakeClient()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// Create a service that we should manage, and check that the initial round
|
||||
// of objects looks right.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSecret(fullName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
// that we get to the end.
|
||||
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
|
||||
if s.Data == nil {
|
||||
s.Data = map[string][]byte{}
|
||||
}
|
||||
s.Data["device_id"] = []byte("ts-id-1234")
|
||||
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
Status: corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "tailscale.device.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
|
||||
// Turn the service back into a ClusterIP service, but also add the
|
||||
// tailscale annotation.
|
||||
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
|
||||
s.ObjectMeta.Annotations = map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
}
|
||||
s.Spec.Type = corev1.ServiceTypeClusterIP
|
||||
s.Spec.LoadBalancerClass = nil
|
||||
// Fake client doesn't automatically delete the LoadBalancer status when
|
||||
// changing away from the LoadBalancer type, we have to do
|
||||
// controller-manager's work by hand.
|
||||
s.Status = corev1.ServiceStatus{}
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(shortName, fullName))
|
||||
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
Annotations: map[string]string{
|
||||
"tailscale.com/expose": "true",
|
||||
},
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
}
|
||||
|
||||
func expectedSecret(name string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": "default",
|
||||
"tailscale.com/parent-resource-type": "svc",
|
||||
},
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"authkey": "secret-authkey",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedHeadlessService(name string) *corev1.Service {
|
||||
return &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
GenerateName: "ts-test-",
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": "default",
|
||||
"tailscale.com/parent-resource-type": "svc",
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "1234-UID",
|
||||
},
|
||||
ClusterIP: "None",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedSTS(stsName, secretName string) *appsv1.StatefulSet {
|
||||
return &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: stsName,
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": "default",
|
||||
"tailscale.com/parent-resource-type": "svc",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
ServiceName: stsName,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
DeletionGracePeriodSeconds: ptr.To[int64](10),
|
||||
Labels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: "proxies",
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "sysctler",
|
||||
Image: "busybox",
|
||||
Command: []string{"/bin/sh"},
|
||||
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []v1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
|
||||
{Name: "TS_KUBE_SECRET", Value: secretName},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
ImagePullPolicy: "Always",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func findGenName(t *testing.T, client client.Client, ns, name string) (full, noSuffix string) {
|
||||
t.Helper()
|
||||
labels := map[string]string{
|
||||
LabelManaged: "true",
|
||||
LabelParentName: name,
|
||||
LabelParentNamespace: ns,
|
||||
LabelParentType: "svc",
|
||||
}
|
||||
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
|
||||
if err != nil {
|
||||
t.Fatalf("finding secret for %q: %v", name, err)
|
||||
}
|
||||
return s.GetName(), strings.TrimSuffix(s.GetName(), "-0")
|
||||
}
|
||||
|
||||
func mustCreate(t *testing.T, client client.Client, obj client.Object) {
|
||||
t.Helper()
|
||||
if err := client.Create(context.Background(), obj); err != nil {
|
||||
t.Fatalf("creating %q: %v", obj.GetName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
|
||||
t.Helper()
|
||||
obj := O(new(T))
|
||||
if err := client.Get(context.Background(), types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
}, obj); err != nil {
|
||||
t.Fatalf("getting %q: %v", name, err)
|
||||
}
|
||||
update(obj)
|
||||
if err := client.Update(context.Background(), obj); err != nil {
|
||||
t.Fatalf("updating %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
|
||||
t.Helper()
|
||||
got := O(new(T))
|
||||
if err := client.Get(context.Background(), types.NamespacedName{
|
||||
Name: want.GetName(),
|
||||
Namespace: want.GetNamespace(),
|
||||
}, got); err != nil {
|
||||
t.Fatalf("getting %q: %v", want.GetName(), err)
|
||||
}
|
||||
// The resource version changes eagerly whenever the operator does even a
|
||||
// no-op update. Asserting a specific value leads to overly brittle tests,
|
||||
// so just remove it from both got and want.
|
||||
got.SetResourceVersion("")
|
||||
want.SetResourceVersion("")
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("unexpected object (-got +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func expectMissing[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string) {
|
||||
t.Helper()
|
||||
obj := O(new(T))
|
||||
if err := client.Get(context.Background(), types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
}, obj); !apierrors.IsNotFound(err) {
|
||||
t.Fatalf("object %s/%s unexpectedly present, wanted missing", ns, name)
|
||||
}
|
||||
}
|
||||
|
||||
func expectReconciled(t *testing.T, sr *ServiceReconciler, ns, name string) {
|
||||
t.Helper()
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
},
|
||||
}
|
||||
res, err := sr.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile: unexpected error: %v", err)
|
||||
}
|
||||
if res.Requeue {
|
||||
t.Fatalf("unexpected immediate requeue")
|
||||
}
|
||||
if res.RequeueAfter != 0 {
|
||||
t.Fatalf("unexpected timed requeue (%v)", res.RequeueAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func expectRequeue(t *testing.T, sr *ServiceReconciler, ns, name string) {
|
||||
t.Helper()
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
},
|
||||
}
|
||||
res, err := sr.Reconcile(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile: unexpected error: %v", err)
|
||||
}
|
||||
if res.Requeue {
|
||||
t.Fatalf("unexpected immediate requeue")
|
||||
}
|
||||
if res.RequeueAfter == 0 {
|
||||
t.Fatalf("expected timed requeue, got success")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTSClient struct {
|
||||
sync.Mutex
|
||||
keyRequests []tailscale.KeyCapabilities
|
||||
deleted []string
|
||||
}
|
||||
|
||||
func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.keyRequests = append(c.keyRequests, caps)
|
||||
k := &tailscale.Key{
|
||||
ID: "key",
|
||||
Created: time.Now(),
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
Capabilities: caps,
|
||||
}
|
||||
return "secret-authkey", k, nil
|
||||
}
|
||||
|
||||
func (c *fakeTSClient) DeleteDevice(ctx context.Context, deviceID string) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.deleted = append(c.deleted, deviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeTSClient) KeyRequests() []tailscale.KeyCapabilities {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.keyRequests
|
||||
}
|
||||
|
||||
func (c *fakeTSClient) Deleted() []string {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.deleted
|
||||
}
|
||||
52
cmd/mkmanifest/main.go
Normal file
52
cmd/mkmanifest/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 mkmanifest command is a simple helper utility to create a '.syso' file
|
||||
// that contains a Windows manifest file.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/tc-hib/winres"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 4 {
|
||||
log.Fatalf("usage: %s arch manifest.xml output.syso", os.Args[0])
|
||||
}
|
||||
|
||||
arch := winres.Arch(os.Args[1])
|
||||
switch arch {
|
||||
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
|
||||
default:
|
||||
log.Fatalf("unsupported arch: %s", arch)
|
||||
}
|
||||
|
||||
manifest, err := os.ReadFile(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("error reading manifest file %q: %v", os.Args[2], err)
|
||||
}
|
||||
|
||||
out := os.Args[3]
|
||||
|
||||
// Start by creating an empty resource set
|
||||
rs := winres.ResourceSet{}
|
||||
|
||||
// Add resources
|
||||
rs.Set(winres.RT_MANIFEST, winres.ID(1), 0, manifest)
|
||||
|
||||
// Compile to a COFF object file
|
||||
f, err := os.Create(out)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating output file %q: %v", out, err)
|
||||
}
|
||||
if err := rs.WriteObject(f, arch); err != nil {
|
||||
log.Fatalf("error writing object: %v", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("error writing output file %q: %v", out, err)
|
||||
}
|
||||
}
|
||||
7
cmd/nardump/README.md
Normal file
7
cmd/nardump/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# nardump
|
||||
|
||||
nardump is like nix-store --dump, but in Go, writing a NAR file (tar-like,
|
||||
but focused on being reproducible) to stdout or to a hash with the --sri flag.
|
||||
|
||||
It lets us calculate the Nix sha256 in shell.nix without the person running
|
||||
git-pull-oss.sh having Nix available.
|
||||
185
cmd/nardump/nardump.go
Normal file
185
cmd/nardump/nardump.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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.
|
||||
|
||||
// nardump is like nix-store --dump, but in Go, writing a NAR
|
||||
// file (tar-like, but focused on being reproducible) to stdout
|
||||
// or to a hash with the --sri flag.
|
||||
//
|
||||
// It lets us calculate a Nix sha256 without the person running
|
||||
// git-pull-oss.sh having Nix available.
|
||||
package main
|
||||
|
||||
// For the format, see:
|
||||
// See https://gist.github.com/jbeda/5c79d2b1434f0018d693
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var sri = flag.Bool("sri", false, "print SRI")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("usage: nardump <dir>")
|
||||
}
|
||||
arg := flag.Arg(0)
|
||||
if err := os.Chdir(arg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *sri {
|
||||
hash := sha256.New()
|
||||
if err := writeNAR(hash, os.DirFS(".")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
|
||||
return
|
||||
}
|
||||
bw := bufio.NewWriter(os.Stdout)
|
||||
if err := writeNAR(bw, os.DirFS(".")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
bw.Flush()
|
||||
}
|
||||
|
||||
// writeNARError is a sentinel panic type that's recovered by writeNAR
|
||||
// and converted into the wrapped error.
|
||||
type writeNARError struct{ err error }
|
||||
|
||||
// narWriter writes NAR files.
|
||||
type narWriter struct {
|
||||
w io.Writer
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
// writeNAR writes a NAR file to w from the root of fs.
|
||||
func writeNAR(w io.Writer, fs fs.FS) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if we, ok := e.(writeNARError); ok {
|
||||
err = we.err
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
nw := &narWriter{w: w, fs: fs}
|
||||
nw.str("nix-archive-1")
|
||||
return nw.writeDir(".")
|
||||
}
|
||||
|
||||
func (nw *narWriter) writeDir(dirPath string) error {
|
||||
ents, err := fs.ReadDir(nw.fs, dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(ents, func(i, j int) bool {
|
||||
return ents[i].Name() < ents[j].Name()
|
||||
})
|
||||
nw.str("(")
|
||||
nw.str("type")
|
||||
nw.str("directory")
|
||||
for _, ent := range ents {
|
||||
nw.str("entry")
|
||||
nw.str("(")
|
||||
nw.str("name")
|
||||
nw.str(ent.Name())
|
||||
nw.str("node")
|
||||
mode := ent.Type()
|
||||
sub := path.Join(dirPath, ent.Name())
|
||||
var err error
|
||||
switch {
|
||||
case mode.IsRegular():
|
||||
err = nw.writeRegular(sub)
|
||||
case mode.IsDir():
|
||||
err = nw.writeDir(sub)
|
||||
default:
|
||||
// TODO(bradfitz): symlink, but requires fighting io/fs a bit
|
||||
// to get at Readlink or the osFS via fs. But for now
|
||||
// we don't need symlinks because they're not in Go's archive.
|
||||
return fmt.Errorf("unsupported file type %v at %q", sub, mode)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nw.str(")")
|
||||
}
|
||||
nw.str(")")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nw *narWriter) writeRegular(path string) error {
|
||||
nw.str("(")
|
||||
nw.str("type")
|
||||
nw.str("regular")
|
||||
fi, err := fs.Stat(nw.fs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.Mode()&0111 != 0 {
|
||||
nw.str("executable")
|
||||
nw.str("")
|
||||
}
|
||||
contents, err := fs.ReadFile(nw.fs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nw.str("contents")
|
||||
if err := writeBytes(nw.w, contents); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.str(")")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nw *narWriter) str(s string) {
|
||||
if err := writeString(nw.w, s); err != nil {
|
||||
panic(writeNARError{err})
|
||||
}
|
||||
}
|
||||
|
||||
func writeString(w io.Writer, s string) error {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
|
||||
if _, err := w.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
return writePad(w, len(s))
|
||||
}
|
||||
|
||||
func writeBytes(w io.Writer, b []byte) error {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
|
||||
if _, err := w.Write(buf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
return writePad(w, len(b))
|
||||
}
|
||||
|
||||
func writePad(w io.Writer, n int) error {
|
||||
pad := n % 8
|
||||
if pad == 0 {
|
||||
return nil
|
||||
}
|
||||
var zeroes [8]byte
|
||||
_, err := w.Write(zeroes[:8-pad])
|
||||
return err
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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")
|
||||
goToolchainSRI = flag.Bool("go-sri", false, "print the SRI hash of the Tailscale Go toolchain")
|
||||
alpine = flag.Bool("alpine", false, "print the tag of alpine docker image")
|
||||
)
|
||||
|
||||
@@ -48,4 +49,7 @@ func main() {
|
||||
}
|
||||
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
|
||||
}
|
||||
if *goToolchainSRI {
|
||||
fmt.Println(strings.TrimSpace(ts.GoToolchainSRI))
|
||||
}
|
||||
}
|
||||
|
||||
58
cmd/stunc/stunc.go
Normal file
58
cmd/stunc/stunc.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.
|
||||
|
||||
// Command stunc makes a STUN request to a STUN server and prints the result.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s <hostname>", os.Args[0])
|
||||
}
|
||||
host := os.Args[1]
|
||||
|
||||
uaddr, err := net.ResolveUDPAddr("udp", host+":3478")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
txID := stun.NewTxID()
|
||||
req := stun.Request(txID)
|
||||
|
||||
_, err = c.WriteToUDP(req, uaddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var buf [1024]byte
|
||||
n, raddr, err := c.ReadFromUDPAddrPort(buf[:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tid, saddr, err := stun.ParseResponse(buf[:n])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if tid != txID {
|
||||
log.Fatalf("txid mismatch: got %v, want %v", tid, txID)
|
||||
}
|
||||
|
||||
log.Printf("sent addr: %v", uaddr)
|
||||
log.Printf("from addr: %v", raddr)
|
||||
log.Printf("stun addr: %v", saddr)
|
||||
}
|
||||
179
cmd/sync-containers/main.go
Normal file
179
cmd/sync-containers/main.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// 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 sync-containers command synchronizes container image tags from one
|
||||
// registry to another.
|
||||
//
|
||||
// It is intended as a workaround for ghcr.io's lack of good push credentials:
|
||||
// you can either authorize "classic" Personal Access Tokens in your org (which
|
||||
// are a common vector of very bad compromise), or you can get a short-lived
|
||||
// credential in a Github action.
|
||||
//
|
||||
// Since we publish to both Docker Hub and ghcr.io, we use this program in a
|
||||
// Github action to effectively rsync from docker hub into ghcr.io, so that we
|
||||
// can continue to forbid dangerous Personal Access Tokens in the tailscale org.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var (
|
||||
src = flag.String("src", "", "Source image")
|
||||
dst = flag.String("dst", "", "Destination image")
|
||||
max = flag.Int("max", 0, "Maximum number of tags to sync (0 for all tags)")
|
||||
dryRun = flag.Bool("dry-run", true, "Don't actually sync anything")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *src == "" {
|
||||
log.Fatalf("--src is required")
|
||||
}
|
||||
if *dst == "" {
|
||||
log.Fatalf("--dst is required")
|
||||
}
|
||||
|
||||
opts := []remote.Option{
|
||||
remote.WithAuthFromKeychain(authn.DefaultKeychain),
|
||||
remote.WithContext(context.Background()),
|
||||
}
|
||||
|
||||
stags, err := listTags(*src, opts...)
|
||||
if err != nil {
|
||||
log.Fatalf("listing source tags: %v", err)
|
||||
}
|
||||
dtags, err := listTags(*dst, opts...)
|
||||
if err != nil {
|
||||
log.Fatalf("listing destination tags: %v", err)
|
||||
}
|
||||
|
||||
add, remove := diffTags(stags, dtags)
|
||||
if l := len(add); l > 0 {
|
||||
log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", "))
|
||||
if *max > 0 && l > *max {
|
||||
log.Printf("Limiting sync to %d tags", *max)
|
||||
add = add[:*max]
|
||||
}
|
||||
}
|
||||
for _, tag := range add {
|
||||
if !*dryRun {
|
||||
log.Printf("Syncing tag %q", tag)
|
||||
if err := copyTag(*src, *dst, tag, opts...); err != nil {
|
||||
log.Printf("Syncing tag %q: progress error: %v", tag, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Dry run: would sync tag %q", tag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remove) > 0 {
|
||||
log.Printf("%d tags to remove: %s\n", len(remove), strings.Join(remove, ", "))
|
||||
log.Printf("Not removing any tags for safety.\n")
|
||||
}
|
||||
}
|
||||
|
||||
func copyTag(srcStr, dstStr, tag string, opts ...remote.Option) error {
|
||||
src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := remote.Get(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch := make(chan v1.Update, 10)
|
||||
opts = append(opts, remote.WithProgress(ch))
|
||||
progressDone := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(progressDone)
|
||||
for p := range ch {
|
||||
fmt.Printf("Syncing tag %q: %d%% (%d/%d)\n", tag, int(float64(p.Complete)/float64(p.Total)*100), p.Complete, p.Total)
|
||||
if p.Error != nil {
|
||||
fmt.Printf("error: %v\n", p.Error)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
switch desc.MediaType {
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
img, err := desc.Image()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := remote.Write(dst, img, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
idx, err := desc.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := remote.WriteIndex(dst, idx, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
<-progressDone
|
||||
return nil
|
||||
}
|
||||
|
||||
func listTags(repoStr string, opts ...remote.Option) ([]string, error) {
|
||||
repo, err := name.NewRepository(repoStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := remote.List(repo, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(tags)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func diffTags(src, dst []string) (add, remove []string) {
|
||||
srcd := make(map[string]bool)
|
||||
for _, tag := range src {
|
||||
srcd[tag] = true
|
||||
}
|
||||
dstd := make(map[string]bool)
|
||||
for _, tag := range dst {
|
||||
dstd[tag] = true
|
||||
}
|
||||
|
||||
for _, tag := range src {
|
||||
if !dstd[tag] {
|
||||
add = append(add, tag)
|
||||
}
|
||||
}
|
||||
for _, tag := range dst {
|
||||
if !srcd[tag] {
|
||||
remove = append(remove, tag)
|
||||
}
|
||||
}
|
||||
sort.Strings(add)
|
||||
sort.Strings(remove)
|
||||
return add, remove
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
var certCmd = &ffcli.Command{
|
||||
Name: "cert",
|
||||
Exec: runCert,
|
||||
ShortHelp: "get TLS certs",
|
||||
ShortHelp: "Get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("cert")
|
||||
|
||||
@@ -13,23 +13,18 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -147,7 +142,7 @@ func Run(args []string) (err error) {
|
||||
})
|
||||
|
||||
rootfs := newFlagSet("tailscale")
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
@@ -163,7 +158,9 @@ change in the future.
|
||||
upCmd,
|
||||
downCmd,
|
||||
setCmd,
|
||||
loginCmd,
|
||||
logoutCmd,
|
||||
switchCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
@@ -197,10 +194,6 @@ change in the future.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "login"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, loginCmd)
|
||||
case slices.Contains(args, "switch"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, switchCmd)
|
||||
case slices.Contains(args, "serve"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||
}
|
||||
@@ -248,58 +241,6 @@ var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
}
|
||||
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-interrupt:
|
||||
case <-ctx.Done():
|
||||
// Context canceled elsewhere.
|
||||
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
return c, bc, ctx, cancel
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return fmt.Errorf("%w (tailscaled stopped running?)", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
|
||||
func usageFuncNoDefaultValues(c *ffcli.Command) string {
|
||||
return usageFuncOpt(c, false)
|
||||
@@ -356,7 +297,14 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
s += "\n \t"
|
||||
s += strings.ReplaceAll(usage, "\n", "\n \t")
|
||||
|
||||
if f.DefValue != "" && withDefaults {
|
||||
showDefault := f.DefValue != "" && withDefaults
|
||||
// Issue 6766: don't show the default Windows socket path. It's long
|
||||
// and distracting. And people on on Windows aren't likely to ever
|
||||
// change it anyway.
|
||||
if runtime.GOOS == "windows" && f.Name == "socket" && strings.HasPrefix(f.DefValue, `\\.\pipe\ProtectedPrefix\`) {
|
||||
showDefault = false
|
||||
}
|
||||
if showDefault {
|
||||
s += fmt.Sprintf(" (default %s)", f.DefValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -480,6 +480,19 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
distro: "", // not Synology
|
||||
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
||||
},
|
||||
{
|
||||
name: "profile_name_ignored_in_up",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ProfileName: "foo",
|
||||
},
|
||||
goos: "linux",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
@@ -37,6 +39,8 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/strs"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -129,6 +133,8 @@ var debugCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("watch-ipn")
|
||||
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
|
||||
fs.BoolVar(&watchIPNArgs.initial, "initial", false, "include initial status")
|
||||
fs.BoolVar(&watchIPNArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
@@ -159,6 +165,11 @@ var debugCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "derp",
|
||||
Exec: runDebugDERP,
|
||||
ShortHelp: "test a DERP configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -228,9 +239,8 @@ func runDebug(ctx context.Context, args []string) error {
|
||||
e.Encode(wfs)
|
||||
return nil
|
||||
}
|
||||
delete := strings.HasPrefix(debugArgs.file, "delete:")
|
||||
if delete {
|
||||
return localClient.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
|
||||
if name, ok := strs.CutPrefix(debugArgs.file, "delete:"); ok {
|
||||
return localClient.DeleteWaitingFile(ctx, name)
|
||||
}
|
||||
rc, size, err := localClient.GetWaitingFile(ctx, debugArgs.file)
|
||||
if err != nil {
|
||||
@@ -255,13 +265,42 @@ func runLocalCreds(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
|
||||
runLocalAPIProxy()
|
||||
return nil
|
||||
}
|
||||
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
printf("curl --unix-socket %s http://local-tailscaled.sock/localapi/v0/status\n", paths.DefaultTailscaledSocket())
|
||||
return nil
|
||||
}
|
||||
|
||||
type localClientRoundTripper struct{}
|
||||
|
||||
func (localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return localClient.DoLocalRequest(req)
|
||||
}
|
||||
|
||||
func runLocalAPIProxy() {
|
||||
rp := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: apitype.LocalAPIHost,
|
||||
Path: "/",
|
||||
})
|
||||
dir := rp.Director
|
||||
rp.Director = func(req *http.Request) {
|
||||
dir(req)
|
||||
req.Host = ""
|
||||
req.RequestURI = ""
|
||||
}
|
||||
rp.Transport = localClientRoundTripper{}
|
||||
lc, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Serving LocalAPI proxy on http://%s\n", lc.Addr())
|
||||
fmt.Printf("curl.exe http://%v/localapi/v0/status\n", lc.Addr())
|
||||
fmt.Printf("Ctrl+C to stop")
|
||||
http.Serve(lc, rp)
|
||||
}
|
||||
|
||||
var prefsArgs struct {
|
||||
pretty bool
|
||||
}
|
||||
@@ -281,23 +320,36 @@ func runPrefs(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var watchIPNArgs struct {
|
||||
netmap bool
|
||||
netmap bool
|
||||
initial bool
|
||||
showPrivateKey bool
|
||||
}
|
||||
|
||||
func runWatchIPN(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
var mask ipn.NotifyWatchOpt
|
||||
if watchIPNArgs.initial {
|
||||
mask = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
|
||||
}
|
||||
if !watchIPNArgs.showPrivateKey {
|
||||
mask |= ipn.NotifyNoPrivateKeys
|
||||
}
|
||||
watcher, err := localClient.WatchIPNBus(ctx, mask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
printf("Connected.\n")
|
||||
for {
|
||||
n, err := watcher.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !watchIPNArgs.netmap {
|
||||
n.NetMap = nil
|
||||
}
|
||||
j, _ := json.MarshalIndent(n, "", "\t")
|
||||
printf("%s\n", j)
|
||||
})
|
||||
bc.RequestEngineStatus()
|
||||
pump(ctx, bc, c)
|
||||
return errors.New("exit")
|
||||
}
|
||||
}
|
||||
|
||||
func runDERPMap(ctx context.Context, args []string) error {
|
||||
@@ -606,3 +658,15 @@ func runDevStoreSet(ctx context.Context, args []string) error {
|
||||
}
|
||||
return localClient.SetDevStoreKeyValue(ctx, key, val)
|
||||
}
|
||||
|
||||
func runDebugDERP(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: debug derp <region>")
|
||||
}
|
||||
st, err := localClient.DebugDERPRegion(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", must.Get(json.MarshalIndent(st, "", " ")))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,17 +19,20 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/quarantine"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -49,6 +52,17 @@ var fileCmd = &ffcli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
type countingReader struct {
|
||||
io.Reader
|
||||
n atomic.Uint64
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(buf []byte) (int, error) {
|
||||
n, err := c.Reader.Read(buf)
|
||||
c.n.Add(uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
var fileCpCmd = &ffcli.Command{
|
||||
Name: "cp",
|
||||
ShortUsage: "file cp <files...> <target>:",
|
||||
@@ -77,10 +91,10 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return errors.New("usage: tailscale file cp <files...> <target>:")
|
||||
}
|
||||
files, target := args[:len(args)-1], args[len(args)-1]
|
||||
if !strings.HasSuffix(target, ":") {
|
||||
target, ok := strs.CutSuffix(target, ":")
|
||||
if !ok {
|
||||
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
|
||||
}
|
||||
target = strings.TrimSuffix(target, ":")
|
||||
hadBrackets := false
|
||||
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
|
||||
hadBrackets = true
|
||||
@@ -116,11 +130,11 @@ func runCp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
for _, fileArg := range files {
|
||||
var fileContents io.Reader
|
||||
var fileContents *countingReader
|
||||
var name = cpArgs.name
|
||||
var contentLength int64 = -1
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
fileContents = &countingReader{Reader: os.Stdin}
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
@@ -144,19 +158,29 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return errors.New("directories not supported")
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
fileContents = io.LimitReader(f, contentLength)
|
||||
fileContents = &countingReader{Reader: io.LimitReader(f, contentLength)}
|
||||
if name == "" {
|
||||
name = filepath.Base(fileArg)
|
||||
}
|
||||
|
||||
if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
|
||||
fileContents = &slowReader{r: fileContents}
|
||||
fileContents = &countingReader{Reader: &slowReader{r: fileContents}}
|
||||
}
|
||||
}
|
||||
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
|
||||
}
|
||||
|
||||
var (
|
||||
done = make(chan struct{}, 1)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
if isatty.IsTerminal(os.Stderr.Fd()) {
|
||||
go printProgress(&wg, done, fileContents, name, contentLength)
|
||||
wg.Add(1)
|
||||
}
|
||||
|
||||
err := localClient.PushFile(ctx, stableID, contentLength, name, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,10 +188,61 @@ func runCp(ctx context.Context, args []string) error {
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
}
|
||||
done <- struct{}{}
|
||||
wg.Wait()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const vtRestartLine = "\r\x1b[K"
|
||||
|
||||
func printProgress(wg *sync.WaitGroup, done <-chan struct{}, r *countingReader, name string, contentLength int64) {
|
||||
defer wg.Done()
|
||||
var lastBytesRead uint64
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
fmt.Fprintln(os.Stderr)
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
n := r.n.Load()
|
||||
contentLengthStr := "???"
|
||||
if contentLength > 0 {
|
||||
contentLengthStr = fmt.Sprint(contentLength / 1024)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s%s\t\t%s", vtRestartLine, padTruncateString(name, 36), padTruncateString(fmt.Sprintf("%d/%s kb", n/1024, contentLengthStr), 16))
|
||||
if contentLength > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\t%.02f%%", float64(n)/float64(contentLength)*100)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\t-------%%")
|
||||
}
|
||||
if lastBytesRead > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\t%d kb/s", (n-lastBytesRead)/1024)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\t-------")
|
||||
}
|
||||
lastBytesRead = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func padTruncateString(str string, truncateAt int) string {
|
||||
if len(str) <= truncateAt {
|
||||
return str + strings.Repeat(" ", truncateAt-len(str))
|
||||
}
|
||||
|
||||
// Truncate the string, but respect unicode codepoint boundaries.
|
||||
// As of RFC3629 utf-8 codepoints can be at most 4 bytes wide.
|
||||
for i := 1; i <= 4 && i < len(str)-truncateAt; i++ {
|
||||
if utf8.ValidString(str[:truncateAt-i]) {
|
||||
return str[:truncateAt-i] + "…"
|
||||
}
|
||||
}
|
||||
return "" // Should be unreachable
|
||||
}
|
||||
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
@@ -230,12 +305,12 @@ func ext(b []byte) string {
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
func pickStdinFilename() (name string, r *countingReader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
return "stdin" + ext(sniff), &countingReader{Reader: io.MultiReader(bytes.NewReader(sniff), os.Stdin)}, nil
|
||||
}
|
||||
|
||||
type slowReader struct {
|
||||
@@ -528,30 +603,16 @@ func wipeInbox(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func waitForFile(ctx context.Context) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
fileWaiting := make(chan bool, 1)
|
||||
notifyError := make(chan error, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
notifyError <- fmt.Errorf("Notify.ErrMessage: %v", *n.ErrMessage)
|
||||
for {
|
||||
ff, err := localClient.AwaitWaitingFiles(ctx, time.Hour)
|
||||
if len(ff) > 0 {
|
||||
return nil
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
case fileWaiting <- true:
|
||||
default:
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
})
|
||||
go pump(pumpCtx, bc, c)
|
||||
select {
|
||||
case <-fileWaiting:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
return pumpCtx.Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-notifyError:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ This command is currently in alpha and may change in the future.`,
|
||||
if err := localClient.SwitchToEmptyProfile(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return runUp(ctx, args, loginArgs)
|
||||
return runUp(ctx, "login", args, loginArgs)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,14 +5,22 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -20,7 +28,8 @@ import (
|
||||
var netlockCmd = &ffcli.Command{
|
||||
Name: "lock",
|
||||
ShortUsage: "lock <sub-command> <arguments>",
|
||||
ShortHelp: "Manipulate the tailnet key authority",
|
||||
ShortHelp: "Manage tailnet lock",
|
||||
LongHelp: "Manage tailnet lock",
|
||||
Subcommands: []*ffcli.Command{
|
||||
nlInitCmd,
|
||||
nlStatusCmd,
|
||||
@@ -29,15 +38,51 @@ var netlockCmd = &ffcli.Command{
|
||||
nlSignCmd,
|
||||
nlDisableCmd,
|
||||
nlDisablementKDFCmd,
|
||||
nlLogCmd,
|
||||
nlLocalDisableCmd,
|
||||
},
|
||||
Exec: runNetworkLockStatus,
|
||||
}
|
||||
|
||||
var nlInitArgs struct {
|
||||
numDisablements int
|
||||
disablementForSupport bool
|
||||
confirm bool
|
||||
}
|
||||
|
||||
var nlInitCmd = &ffcli.Command{
|
||||
Name: "init",
|
||||
ShortUsage: "init <public-key>...",
|
||||
ShortHelp: "Initialize the tailnet key authority",
|
||||
Exec: runNetworkLockInit,
|
||||
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
|
||||
ShortHelp: "Initialize tailnet lock",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale lock init' command initializes tailnet lock for the
|
||||
entire tailnet. The tailnet lock keys specified are those initially
|
||||
trusted to sign nodes or to make further changes to tailnet lock.
|
||||
|
||||
You can identify the tailnet lock key for a node you wish to trust by
|
||||
running 'tailscale lock' on that node, and copying the node's tailnet
|
||||
lock key.
|
||||
|
||||
To disable tailnet lock, use the 'tailscale lock disable' command
|
||||
along with one of the disablement secrets.
|
||||
The number of disablement secrets to be generated is specified using the
|
||||
--gen-disablements flag. Initializing tailnet lock requires at least
|
||||
one disablement.
|
||||
|
||||
If --gen-disablement-for-support is specified, an additional disablement secret
|
||||
will be generated and transmitted to Tailscale, which support can use to disable
|
||||
tailnet lock. We recommend setting this flag.
|
||||
|
||||
`),
|
||||
Exec: runNetworkLockInit,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock init")
|
||||
fs.IntVar(&nlInitArgs.numDisablements, "gen-disablements", 1, "number of disablement secrets to generate")
|
||||
fs.BoolVar(&nlInitArgs.disablementForSupport, "gen-disablement-for-support", false, "generates and transmits a disablement secret for Tailscale support")
|
||||
fs.BoolVar(&nlInitArgs.confirm, "confirm", false, "do not prompt for confirmation")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
@@ -46,7 +91,7 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if st.Enabled {
|
||||
return errors.New("network-lock is already enabled")
|
||||
return errors.New("tailnet lock is already enabled")
|
||||
}
|
||||
|
||||
// Parse initially-trusted keys & disablement values.
|
||||
@@ -55,19 +100,79 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := localClient.NetworkLockInit(ctx, keys, disablementValues)
|
||||
if err != nil {
|
||||
// Common mistake: Not specifying the current node's key as one of the trusted keys.
|
||||
foundSelfKey := false
|
||||
for _, k := range keys {
|
||||
keyID, err := k.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(keyID, st.PublicKey.KeyID()) {
|
||||
foundSelfKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundSelfKey {
|
||||
return errors.New("the tailnet lock key of the current node must be one of the trusted keys during initialization")
|
||||
}
|
||||
|
||||
fmt.Println("You are initializing tailnet lock with the following trusted signing keys:")
|
||||
for _, k := range keys {
|
||||
fmt.Printf(" - tlpub:%x (%s key)\n", k.Public, k.Kind.String())
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if !nlInitArgs.confirm {
|
||||
fmt.Printf("%d disablement secrets will be generated.\n", nlInitArgs.numDisablements)
|
||||
if nlInitArgs.disablementForSupport {
|
||||
fmt.Println("A disablement secret will be generated and transmitted to Tailscale support.")
|
||||
}
|
||||
|
||||
genSupportFlag := ""
|
||||
if nlInitArgs.disablementForSupport {
|
||||
genSupportFlag = "--gen-disablement-for-support "
|
||||
}
|
||||
fmt.Println("\nIf this is correct, please re-run this command with the --confirm flag:")
|
||||
fmt.Printf("\t%s lock init --confirm --gen-disablements %d %s%s", os.Args[0], nlInitArgs.numDisablements, genSupportFlag, strings.Join(args, " "))
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%d disablement secrets have been generated and are printed below. Take note of them now, they WILL NOT be shown again.\n", nlInitArgs.numDisablements)
|
||||
for i := 0; i < nlInitArgs.numDisablements; i++ {
|
||||
var secret [32]byte
|
||||
if _, err := rand.Read(secret[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\tdisablement-secret:%X\n", secret[:])
|
||||
disablementValues = append(disablementValues, tka.DisablementKDF(secret[:]))
|
||||
}
|
||||
|
||||
var supportDisablement []byte
|
||||
if nlInitArgs.disablementForSupport {
|
||||
supportDisablement = make([]byte, 32)
|
||||
if _, err := rand.Read(supportDisablement); err != nil {
|
||||
return err
|
||||
}
|
||||
disablementValues = append(disablementValues, tka.DisablementKDF(supportDisablement))
|
||||
fmt.Println("A disablement secret for Tailscale support has been generated and will be transmitted to Tailscale upon initialization.")
|
||||
}
|
||||
|
||||
// The state returned by NetworkLockInit likely doesn't contain the initialized state,
|
||||
// because that has to tick through from netmaps.
|
||||
if _, err := localClient.NetworkLockInit(ctx, keys, disablementValues, supportDisablement); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Status: %+v\n\n", status)
|
||||
fmt.Println("Initialization complete.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlStatusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status",
|
||||
ShortHelp: "Outputs the state of network lock",
|
||||
ShortHelp: "Outputs the state of tailnet lock",
|
||||
LongHelp: "Outputs the state of tailnet lock",
|
||||
Exec: runNetworkLockStatus,
|
||||
}
|
||||
|
||||
@@ -77,59 +182,71 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if st.Enabled {
|
||||
fmt.Println("Network-lock is ENABLED.")
|
||||
fmt.Println("Tailnet lock is ENABLED.")
|
||||
} else {
|
||||
fmt.Println("Network-lock is NOT enabled.")
|
||||
fmt.Println("Tailnet lock is NOT enabled.")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if st.Enabled && st.NodeKey != nil {
|
||||
if st.Enabled && st.NodeKey != nil && !st.PublicKey.IsZero() {
|
||||
if st.NodeKeySigned {
|
||||
fmt.Println("This node is trusted by network-lock.")
|
||||
fmt.Println("This node is accessible under tailnet lock.")
|
||||
} else {
|
||||
fmt.Println("This node IS NOT trusted by network-lock, and action is required to establish connectivity.")
|
||||
fmt.Printf("Run the following command on a node with a network-lock key:\n\ttailscale lock sign %v\n", st.NodeKey)
|
||||
fmt.Println("This node is LOCKED OUT by tailnet-lock, and action is required to establish connectivity.")
|
||||
fmt.Printf("Run the following command on a node with a trusted key:\n\ttailscale lock sign %v %s\n", st.NodeKey, st.PublicKey.CLIString())
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if !st.PublicKey.IsZero() {
|
||||
p, err := st.PublicKey.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("This node's public-key: %s\n", p)
|
||||
fmt.Printf("This node's tailnet-lock key: %s\n", st.PublicKey.CLIString())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if st.Enabled && len(st.TrustedKeys) > 0 {
|
||||
fmt.Println("Keys trusted to make changes to network-lock:")
|
||||
fmt.Println("Trusted signing keys:")
|
||||
for _, k := range st.TrustedKeys {
|
||||
key, err := k.Key.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var line strings.Builder
|
||||
line.WriteString("\t")
|
||||
line.WriteString(string(key))
|
||||
line.WriteString(k.Key.CLIString())
|
||||
line.WriteString("\t")
|
||||
line.WriteString(fmt.Sprint(k.Votes))
|
||||
line.WriteString("\t")
|
||||
if k.Key == st.PublicKey {
|
||||
line.WriteString("(us)")
|
||||
line.WriteString("(self)")
|
||||
}
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
|
||||
if st.Enabled && len(st.FilteredPeers) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("The following nodes are locked out by tailnet lock and cannot connect to other nodes:")
|
||||
for _, p := range st.FilteredPeers {
|
||||
var line strings.Builder
|
||||
line.WriteString("\t")
|
||||
line.WriteString(p.Name)
|
||||
line.WriteString("\t")
|
||||
for i, addr := range p.TailscaleIPs {
|
||||
line.WriteString(addr.String())
|
||||
if i < len(p.TailscaleIPs)-1 {
|
||||
line.WriteString(", ")
|
||||
}
|
||||
}
|
||||
line.WriteString("\t")
|
||||
line.WriteString(string(p.StableID))
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlAddCmd = &ffcli.Command{
|
||||
Name: "add",
|
||||
ShortUsage: "add <public-key>...",
|
||||
ShortHelp: "Adds one or more signing keys to the tailnet key authority",
|
||||
ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
|
||||
LongHelp: "Adds one or more trusted signing keys to tailnet lock",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, args, nil)
|
||||
},
|
||||
@@ -138,7 +255,8 @@ var nlAddCmd = &ffcli.Command{
|
||||
var nlRemoveCmd = &ffcli.Command{
|
||||
Name: "remove",
|
||||
ShortUsage: "remove <public-key>...",
|
||||
ShortHelp: "Removes one or more signing keys to the tailnet key authority",
|
||||
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, nil, args)
|
||||
},
|
||||
@@ -197,7 +315,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if !st.Enabled {
|
||||
return errors.New("network-lock is not enabled")
|
||||
return errors.New("tailnet lock is not enabled")
|
||||
}
|
||||
|
||||
addKeys, _, err := parseNLArgs(addArgs, true, false)
|
||||
@@ -209,19 +327,17 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := localClient.NetworkLockModify(ctx, addKeys, removeKeys)
|
||||
if err != nil {
|
||||
if err := localClient.NetworkLockModify(ctx, addKeys, removeKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Status: %+v\n\n", status)
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlSignCmd = &ffcli.Command{
|
||||
Name: "sign",
|
||||
ShortUsage: "sign <node-key> [<rotation-key>]",
|
||||
ShortHelp: "Signs a node-key and transmits that signature to the control plane",
|
||||
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
LongHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
Exec: runNetworkLockSign,
|
||||
}
|
||||
|
||||
@@ -249,8 +365,19 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
var nlDisableCmd = &ffcli.Command{
|
||||
Name: "disable",
|
||||
ShortUsage: "disable <disablement-secret>",
|
||||
ShortHelp: "Consumes a disablement secret to shut down network-lock across the tailnet",
|
||||
Exec: runNetworkLockDisable,
|
||||
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale lock disable' command uses the specified disablement
|
||||
secret to disable tailnet lock.
|
||||
|
||||
If tailnet lock is re-enabled, new disablement secrets can be generated.
|
||||
|
||||
Once this secret is used, it has been distributed
|
||||
to all nodes in the tailnet and should be considered public.
|
||||
|
||||
`),
|
||||
Exec: runNetworkLockDisable,
|
||||
}
|
||||
|
||||
func runNetworkLockDisable(ctx context.Context, args []string) error {
|
||||
@@ -264,10 +391,33 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
|
||||
return localClient.NetworkLockDisable(ctx, secrets[0])
|
||||
}
|
||||
|
||||
var nlLocalDisableCmd = &ffcli.Command{
|
||||
Name: "local-disable",
|
||||
ShortUsage: "local-disable",
|
||||
ShortHelp: "Disables tailnet lock for this node only",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale lock local-disable' command disables tailnet lock for only
|
||||
the current node.
|
||||
|
||||
If the current node is locked out, this does not mean that it can initiate
|
||||
connections in a tailnet with tailnet lock enabled. Rather, this means
|
||||
that the current node will accept traffic from other nodes in the tailnet
|
||||
that are locked out.
|
||||
|
||||
`),
|
||||
Exec: runNetworkLockLocalDisable,
|
||||
}
|
||||
|
||||
func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
|
||||
return localClient.NetworkLockForceLocalDisable(ctx)
|
||||
}
|
||||
|
||||
var nlDisablementKDFCmd = &ffcli.Command{
|
||||
Name: "disablement-kdf",
|
||||
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
|
||||
ShortHelp: "Computes a disablement value from a disablement secret",
|
||||
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
|
||||
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
|
||||
Exec: runNetworkLockDisablementKDF,
|
||||
}
|
||||
|
||||
@@ -282,3 +432,106 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
||||
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlLogArgs struct {
|
||||
limit int
|
||||
}
|
||||
|
||||
var nlLogCmd = &ffcli.Command{
|
||||
Name: "log",
|
||||
ShortUsage: "log [--limit N]",
|
||||
ShortHelp: "List changes applied to tailnet lock",
|
||||
LongHelp: "List changes applied to tailnet lock",
|
||||
Exec: runNetworkLockLog,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock log")
|
||||
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, error) {
|
||||
terminalYellow := ""
|
||||
terminalClear := ""
|
||||
if color {
|
||||
terminalYellow = "\x1b[33m"
|
||||
terminalClear = "\x1b[0m"
|
||||
}
|
||||
|
||||
var stanza strings.Builder
|
||||
printKey := func(key *tka.Key, prefix string) {
|
||||
fmt.Fprintf(&stanza, "%sType: %s\n", prefix, key.Kind.String())
|
||||
if keyID, err := key.ID(); err == nil {
|
||||
fmt.Fprintf(&stanza, "%sKeyID: %x\n", prefix, keyID)
|
||||
} else {
|
||||
// Older versions of the client shouldn't explode when they encounter an
|
||||
// unknown key type.
|
||||
fmt.Fprintf(&stanza, "%sKeyID: <Error: %v>\n", prefix, err)
|
||||
}
|
||||
if key.Meta != nil {
|
||||
fmt.Fprintf(&stanza, "%sMetadata: %+v\n", prefix, key.Meta)
|
||||
}
|
||||
}
|
||||
|
||||
var aum tka.AUM
|
||||
if err := aum.Unserialize(update.Raw); err != nil {
|
||||
return "", fmt.Errorf("decoding: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&stanza, "%supdate %x (%s)%s\n", terminalYellow, update.Hash, update.Change, terminalClear)
|
||||
|
||||
switch update.Change {
|
||||
case tka.AUMAddKey.String():
|
||||
printKey(aum.Key, "")
|
||||
case tka.AUMRemoveKey.String():
|
||||
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
|
||||
|
||||
case tka.AUMUpdateKey.String():
|
||||
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
|
||||
if aum.Votes != nil {
|
||||
fmt.Fprintf(&stanza, "Votes: %d\n", aum.Votes)
|
||||
}
|
||||
if aum.Meta != nil {
|
||||
fmt.Fprintf(&stanza, "Metadata: %+v\n", aum.Meta)
|
||||
}
|
||||
|
||||
case tka.AUMCheckpoint.String():
|
||||
fmt.Fprintln(&stanza, "Disablement values:")
|
||||
for _, v := range aum.State.DisablementSecrets {
|
||||
fmt.Fprintf(&stanza, " - %x\n", v)
|
||||
}
|
||||
fmt.Fprintln(&stanza, "Keys:")
|
||||
for _, k := range aum.State.Keys {
|
||||
printKey(&k, " ")
|
||||
}
|
||||
|
||||
default:
|
||||
// Print a JSON encoding of the AUM as a fallback.
|
||||
e := json.NewEncoder(&stanza)
|
||||
e.SetIndent("", "\t")
|
||||
if err := e.Encode(aum); err != nil {
|
||||
return "", err
|
||||
}
|
||||
stanza.WriteRune('\n')
|
||||
}
|
||||
|
||||
return stanza.String(), nil
|
||||
}
|
||||
|
||||
func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
updates, err := localClient.NetworkLockLog(ctx, nlLogArgs.limit)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
useColor := isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
stdOut := colorable.NewColorableStdout()
|
||||
for _, update := range updates {
|
||||
stanza, err := nlDescribeUpdate(update, useColor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(stdOut, stanza)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
var (
|
||||
riskTypes []string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
riskAll = registerRiskType("all")
|
||||
)
|
||||
|
||||
func registerRiskType(riskType string) string {
|
||||
@@ -35,7 +36,7 @@ func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
|
||||
// risks in acceptedRisks.
|
||||
func isRiskAccepted(riskType, acceptedRisks string) bool {
|
||||
for _, r := range strings.Split(acceptedRisks, ",") {
|
||||
if r == riskType {
|
||||
if r == riskType || r == riskAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,9 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
}
|
||||
h.Proxy = t
|
||||
case "text":
|
||||
if args[2] == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = args[2]
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
|
||||
@@ -514,7 +517,7 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
fStatus := "tailnet only"
|
||||
if sc.IsFunnelOn(hp) {
|
||||
if sc.AllowFunnel[hp] {
|
||||
fStatus = "Funnel on"
|
||||
}
|
||||
printf("|-- tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)
|
||||
@@ -532,7 +535,7 @@ func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
|
||||
return
|
||||
}
|
||||
fStatus := "tailnet only"
|
||||
if sc.IsFunnelOn(hp) {
|
||||
if sc.AllowFunnel[hp] {
|
||||
fStatus = "Funnel on"
|
||||
}
|
||||
host, portStr, _ := net.SplitHostPort(string(hp))
|
||||
@@ -682,13 +685,12 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if !slices.Contains(st.Self.Capabilities, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available. See https://tailscale.com/s/no-funnel")
|
||||
if err := checkHasAccess(st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
|
||||
isFun := sc.IsFunnelOn(hp)
|
||||
if on && isFun || !on && !isFun {
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
@@ -706,3 +708,22 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHasAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func checkHasAccess(nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
|
||||
}
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,6 +49,30 @@ func TestCleanMountPoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHasAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := checkHasAccess(tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
continue
|
||||
case tt.wantErr:
|
||||
t.Fatalf("got no error, want error")
|
||||
case !tt.wantErr:
|
||||
t.Fatalf("got error %v, want no error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeConfigMutations(t *testing.T) {
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
|
||||
@@ -43,11 +43,14 @@ type setArgsT struct {
|
||||
advertiseDefaultRoute bool
|
||||
opUser string
|
||||
acceptedRisks string
|
||||
profileName string
|
||||
forceDaemon bool
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf := newFlagSet("set")
|
||||
|
||||
setf.StringVar(&setArgs.profileName, "nickname", "", "nickname for the current account")
|
||||
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
|
||||
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
@@ -60,6 +63,11 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
setf.BoolVar(&setArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
|
||||
registerAcceptRiskFlag(setf, &setArgs.acceptedRisks)
|
||||
return setf
|
||||
}
|
||||
@@ -81,6 +89,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
|
||||
maskedPrefs := &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ProfileName: setArgs.profileName,
|
||||
RouteAll: setArgs.acceptRoutes,
|
||||
CorpDNS: setArgs.acceptDNS,
|
||||
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
|
||||
@@ -88,6 +97,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
RunSSH: setArgs.runSSH,
|
||||
Hostname: setArgs.hostname,
|
||||
OperatorUser: setArgs.opUser,
|
||||
ForceDaemon: setArgs.forceDaemon,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,6 +142,11 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
checkPrefs := curPrefs.Clone()
|
||||
checkPrefs.ApplyEdits(maskedPrefs)
|
||||
if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = localClient.EditPrefs(ctx, maskedPrefs)
|
||||
return err
|
||||
|
||||
@@ -11,10 +11,9 @@ import (
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
func TestCalcAdvertiseRoutesForSet(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
tests := []struct {
|
||||
@@ -29,80 +28,80 @@ func TestCalcAdvertiseRoutesForSet(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "advertise-exit",
|
||||
setExit: ptrTo(true),
|
||||
setExit: ptr.To(true),
|
||||
want: tsaddr.ExitRoutes(),
|
||||
},
|
||||
{
|
||||
name: "advertise-exit/already-routes",
|
||||
was: []netip.Prefix{pfx("34.0.0.0/16")},
|
||||
setExit: ptrTo(true),
|
||||
setExit: ptr.To(true),
|
||||
want: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
},
|
||||
{
|
||||
name: "advertise-exit/already-exit",
|
||||
was: tsaddr.ExitRoutes(),
|
||||
setExit: ptrTo(true),
|
||||
setExit: ptr.To(true),
|
||||
want: tsaddr.ExitRoutes(),
|
||||
},
|
||||
{
|
||||
name: "stop-advertise-exit",
|
||||
was: tsaddr.ExitRoutes(),
|
||||
setExit: ptrTo(false),
|
||||
setExit: ptr.To(false),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "stop-advertise-exit/with-routes",
|
||||
was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
setExit: ptrTo(false),
|
||||
setExit: ptr.To(false),
|
||||
want: []netip.Prefix{pfx("34.0.0.0/16")},
|
||||
},
|
||||
{
|
||||
name: "advertise-routes",
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
|
||||
},
|
||||
{
|
||||
name: "advertise-routes/already-exit",
|
||||
was: tsaddr.ExitRoutes(),
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
},
|
||||
{
|
||||
name: "advertise-routes/already-diff-routes",
|
||||
was: []netip.Prefix{pfx("34.0.0.0/16")},
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
|
||||
},
|
||||
{
|
||||
name: "stop-advertise-routes",
|
||||
was: []netip.Prefix{pfx("34.0.0.0/16")},
|
||||
setRoutes: ptrTo(""),
|
||||
setRoutes: ptr.To(""),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "stop-advertise-routes/already-exit",
|
||||
was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
setRoutes: ptrTo(""),
|
||||
setRoutes: ptr.To(""),
|
||||
want: tsaddr.ExitRoutes(),
|
||||
},
|
||||
{
|
||||
name: "advertise-routes-and-exit",
|
||||
setExit: ptrTo(true),
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setExit: ptr.To(true),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
},
|
||||
{
|
||||
name: "advertise-routes-and-exit/already-exit",
|
||||
was: tsaddr.ExitRoutes(),
|
||||
setExit: ptrTo(true),
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setExit: ptr.To(true),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
},
|
||||
{
|
||||
name: "advertise-routes-and-exit/already-routes",
|
||||
was: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
|
||||
setExit: ptrTo(true),
|
||||
setRoutes: ptrTo("10.0.0.0/24,192.168.0.0/16"),
|
||||
setExit: ptr.To(true),
|
||||
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
|
||||
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ most users running the Tailscale SSH server will prefer to just use the normal
|
||||
|
||||
The 'tailscale ssh' wrapper adds a few things:
|
||||
|
||||
* It resolves the destination server name in its arugments using MagicDNS,
|
||||
* It resolves the destination server name in its arguments using MagicDNS,
|
||||
even if --accept-dns=false.
|
||||
* It works in userspace-networking mode, by supplying a ProxyCommand to the
|
||||
system 'ssh' command that connects via a pipe through tailscaled.
|
||||
@@ -110,10 +111,15 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
// So don't use it for now. MagicDNS is usually working on macOS anyway
|
||||
// and they're not in userspace mode, so 'nc' isn't very useful.
|
||||
if runtime.GOOS != "darwin" {
|
||||
socketArg := ""
|
||||
if rootArgs.socket != "" && rootArgs.socket != paths.DefaultTailscaledSocket() {
|
||||
socketArg = fmt.Sprintf("--socket=%q", rootArgs.socket)
|
||||
}
|
||||
|
||||
argv = append(argv,
|
||||
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
|
||||
"-o", fmt.Sprintf("ProxyCommand %q %s nc %%h %%p",
|
||||
tailscaleBin,
|
||||
rootArgs.socket,
|
||||
socketArg,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"golang.org/x/net/idna"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -221,9 +223,44 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
outln()
|
||||
printHealth()
|
||||
}
|
||||
printFunnelStatus(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// printFunnelStatus prints the status of the funnel, if it's running.
|
||||
// It prints nothing if the funnel is not running.
|
||||
func printFunnelStatus(ctx context.Context) {
|
||||
sc, err := localClient.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
outln()
|
||||
printf("# Funnel:\n")
|
||||
printf("# - Unable to get Funnel status: %v\n", err)
|
||||
return
|
||||
}
|
||||
if !sc.IsFunnelOn() {
|
||||
return
|
||||
}
|
||||
outln()
|
||||
printf("# Funnel on:\n")
|
||||
for hp, on := range sc.AllowFunnel {
|
||||
if !on { // if present, should be on
|
||||
continue
|
||||
}
|
||||
sni, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p))
|
||||
url := "https://"
|
||||
if isTCP {
|
||||
url = "tcp://"
|
||||
}
|
||||
url += sni
|
||||
if isTCP || p != 443 {
|
||||
url += ":" + portStr
|
||||
}
|
||||
printf("# - %s\n", url)
|
||||
}
|
||||
}
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
// It also returns a description of the status suitable to display to a user.
|
||||
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
@@ -248,6 +285,11 @@ func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
if strings.HasPrefix(baseName, "xn-") {
|
||||
if u, err := idna.ToUnicode(baseName); err == nil {
|
||||
return fmt.Sprintf("%s (%s)", baseName, u)
|
||||
}
|
||||
}
|
||||
return baseName
|
||||
}
|
||||
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
|
||||
var switchCmd = &ffcli.Command{
|
||||
Name: "switch",
|
||||
ShortHelp: "Switches to a different Tailscale profile",
|
||||
ShortHelp: "Switches to a different Tailscale account",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("switch", flag.ExitOnError)
|
||||
fs.BoolVar(&switchArgs.list, "list", false, "list available profiles")
|
||||
fs.BoolVar(&switchArgs.list, "list", false, "list available accounts")
|
||||
return fs
|
||||
}(),
|
||||
Exec: switchProfile,
|
||||
@@ -29,7 +29,7 @@ var switchCmd = &ffcli.Command{
|
||||
[ALPHA] switch <name>
|
||||
[ALPHA] switch --list
|
||||
|
||||
"tailscale switch" switches between logged in profiles.
|
||||
"tailscale switch" switches between logged in accounts.
|
||||
This command is currently in alpha and may change in the future.`
|
||||
},
|
||||
}
|
||||
@@ -58,12 +58,12 @@ func switchProfile(ctx context.Context, args []string) error {
|
||||
return listProfiles(ctx)
|
||||
}
|
||||
if len(args) != 1 {
|
||||
outln("usage: tailscale profile switch NAME")
|
||||
outln("usage: tailscale switch NAME")
|
||||
os.Exit(1)
|
||||
}
|
||||
cp, all, err := localClient.ProfileStatus(ctx)
|
||||
if err != nil {
|
||||
errf("Failed to switch to profile: %v\n", err)
|
||||
errf("Failed to switch to account: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var profID ipn.ProfileID
|
||||
@@ -78,14 +78,14 @@ func switchProfile(ctx context.Context, args []string) error {
|
||||
os.Exit(1)
|
||||
}
|
||||
if profID == cp.ID {
|
||||
printf("Already on profile %q\n", args[0])
|
||||
printf("Already on account %q\n", args[0])
|
||||
os.Exit(0)
|
||||
}
|
||||
if err := localClient.SwitchProfile(ctx, profID); err != nil {
|
||||
errf("Failed to switch to profile: %v\n", err)
|
||||
errf("Failed to switch to account: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
printf("Switching to profile %q\n", args[0])
|
||||
printf("Switching to account %q\n", args[0])
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -59,7 +62,7 @@ settings.)
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runUp(ctx, args, upArgsGlobal)
|
||||
return runUp(ctx, "up", args, upArgsGlobal)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -121,6 +124,10 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
}
|
||||
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
|
||||
|
||||
if cmd == "login" {
|
||||
upf.StringVar(&upArgs.profileName, "nickname", "", "short name for the account")
|
||||
}
|
||||
|
||||
if cmd == "up" {
|
||||
// Some flags are only for "up", not "login".
|
||||
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
@@ -128,6 +135,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
|
||||
}
|
||||
|
||||
return upf
|
||||
}
|
||||
|
||||
@@ -162,12 +170,12 @@ type upArgsT struct {
|
||||
json bool
|
||||
timeout time.Duration
|
||||
acceptedRisks string
|
||||
profileName string
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
v := a.authKeyOrFile
|
||||
if strings.HasPrefix(v, "file:") {
|
||||
file := strings.TrimPrefix(v, "file:")
|
||||
if file, ok := strs.CutPrefix(v, "file:"); ok {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -322,7 +330,11 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
// ipn.NewPrefs returns a non-zero Netfilter default. But Synology only
|
||||
// supports "off" mode.
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
if err := prefs.SetExitNodeIP(upArgs.exitNodeIP, st); err != nil {
|
||||
var e ipn.ExitNodeLocalIPError
|
||||
@@ -343,6 +355,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = upArgs.forceDaemon
|
||||
prefs.OperatorUser = upArgs.opUser
|
||||
prefs.ProfileName = upArgs.profileName
|
||||
|
||||
if goos == "linux" {
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
@@ -437,7 +450,7 @@ func presentSSHToggleRisk(wantSSH, haveSSH bool, acceptedRisks string) error {
|
||||
return presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, acceptedRisks)
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) {
|
||||
func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retErr error) {
|
||||
var egg bool
|
||||
if len(args) > 0 {
|
||||
egg = fmt.Sprint(args) == "[up down down left right left right b a]"
|
||||
@@ -496,6 +509,11 @@ func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd == "up" {
|
||||
// "tailscale up" should not be able to change the
|
||||
// profile name.
|
||||
prefs.ProfileName = curPrefs.ProfileName
|
||||
}
|
||||
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
@@ -523,109 +541,101 @@ func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// At this point we need to subscribe to the IPN bus to watch
|
||||
// for state transitions and possible need to authenticate.
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
watcher, err := localClient.WatchIPNBus(watchCtx, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
running := make(chan bool, 1) // gets value once in state ipn.Running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-interrupt:
|
||||
cancelWatch()
|
||||
case <-watchCtx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
running := make(chan bool, 1) // gets value once in state ipn.Running
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
var printed bool // whether we've yet printed anything to stdout or stderr
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { localClient.StartLoginInteractive(ctx) }) }
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.Engine != nil {
|
||||
select {
|
||||
case gotEngineUpdate <- true:
|
||||
default:
|
||||
go func() {
|
||||
for {
|
||||
n, err := watcher.Next()
|
||||
if err != nil {
|
||||
pumpErr <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch effectiveGOOS() {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case running <- true:
|
||||
default:
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
outln(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
select {
|
||||
case running <- true:
|
||||
default:
|
||||
}
|
||||
cancelWatch()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
outln(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
// we're subscribed to updates. Otherwise we can miss
|
||||
// an update upon its transition to running. Do so by causing some traffic
|
||||
// back to the bus that we then wait on.
|
||||
bc.RequestEngineStatus()
|
||||
select {
|
||||
case <-gotEngineUpdate:
|
||||
case <-pumpCtx.Done():
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
}
|
||||
}()
|
||||
|
||||
// Special case: bare "tailscale up" means to just start
|
||||
// running, if there's ever been a login.
|
||||
@@ -648,10 +658,12 @@ func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bc.Start(ipn.Options{
|
||||
if err := localClient.Start(ctx, ipn.Options{
|
||||
AuthKey: authKey,
|
||||
UpdatePrefs: prefs,
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if upArgs.forceReauth {
|
||||
startLoginInteractive()
|
||||
}
|
||||
@@ -673,13 +685,13 @@ func runUp(ctx context.Context, args []string, upArgs upArgsT) (retErr error) {
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
case <-watchCtx.Done():
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
return watchCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
select {
|
||||
case <-running:
|
||||
@@ -764,6 +776,7 @@ func init() {
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
addPrefFlagMapping("ssh", "RunSSH")
|
||||
addPrefFlagMapping("nickname", "ProfileName")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@@ -870,6 +883,10 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
||||
continue
|
||||
}
|
||||
if flagName == "netfilter-mode" && valNew == preftype.NetfilterOn && env.goos == "linux" && env.distro == distro.Synology {
|
||||
// Issue 6811. Ignore on Synology.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
|
||||
@@ -23,13 +23,12 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -317,6 +316,7 @@ req.send(null);
|
||||
`
|
||||
|
||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if authRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
@@ -327,7 +327,18 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
|
||||
w.Write([]byte(authenticationRedirectHTML))
|
||||
io.WriteString(w, authenticationRedirectHTML)
|
||||
return
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
prefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -344,23 +355,31 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
prefs, err := localClient.GetPrefs(r.Context())
|
||||
if err != nil && !postData.Reauthenticate {
|
||||
|
||||
routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mp := &ipn.MaskedPrefs{
|
||||
AdvertiseRoutesSet: true,
|
||||
WantRunningSet: true,
|
||||
}
|
||||
mp.Prefs.WantRunning = true
|
||||
mp.Prefs.AdvertiseRoutes = routes
|
||||
log.Printf("Doing edit: %v", mp.Pretty())
|
||||
|
||||
if _, err := localClient.EditPrefs(ctx, mp); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
prefs.AdvertiseRoutes = routes
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate)
|
||||
log.Printf("tailscaleUp(reauth=%v) ...", postData.Reauthenticate)
|
||||
url, err := tailscaleUp(r.Context(), st, postData.Reauthenticate)
|
||||
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
@@ -374,17 +393,6 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
st, err := localClient.Status(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
prefs, err := localClient.GetPrefs(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
profile := st.User[st.Self.UserID]
|
||||
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
||||
data := tmplData{
|
||||
@@ -418,26 +426,18 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authURL string, retErr error) {
|
||||
if prefs == nil {
|
||||
prefs = ipn.NewPrefs()
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't fetch status: %v", err)
|
||||
}
|
||||
func tailscaleUp(ctx context.Context, st *ipnstate.Status, forceReauth bool) (authURL string, retErr error) {
|
||||
origAuthURL := st.AuthURL
|
||||
isRunning := st.BackendState == ipn.Running.String()
|
||||
|
||||
if !forceReauth {
|
||||
if origAuthURL != "" {
|
||||
return origAuthURL, nil
|
||||
}
|
||||
if isRunning {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
@@ -445,71 +445,34 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
||||
return url != origAuthURL
|
||||
}
|
||||
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
watcher, err := localClient.WatchIPNBus(watchCtx, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
go pump(pumpCtx, bc, c)
|
||||
go func() {
|
||||
if !isRunning {
|
||||
localClient.Start(ctx, ipn.Options{})
|
||||
}
|
||||
if forceReauth {
|
||||
localClient.StartLoginInteractive(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.Engine != nil {
|
||||
select {
|
||||
case gotEngineUpdate <- true:
|
||||
default:
|
||||
}
|
||||
for {
|
||||
n, err := watcher.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
retErr = fmt.Errorf("backend error: %v", msg)
|
||||
cancel()
|
||||
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
authURL = *url
|
||||
cancel()
|
||||
return "", fmt.Errorf("backend error: %v", msg)
|
||||
}
|
||||
if !forceReauth && n.Prefs != nil && n.Prefs.Valid() {
|
||||
p1, p2 := n.Prefs.AsStruct(), *prefs
|
||||
p1.Persist = nil
|
||||
p2.Persist = nil
|
||||
if p1.Equals(&p2) {
|
||||
cancel()
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
return *url, nil
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
// we're subscribed to updates. Otherwise we can miss
|
||||
// an update upon its transition to running. Do so by causing some traffic
|
||||
// back to the bus that we then wait on.
|
||||
bc.RequestEngineStatus()
|
||||
select {
|
||||
case <-gotEngineUpdate:
|
||||
case <-pumpCtx.Done():
|
||||
return authURL, pumpCtx.Err()
|
||||
}
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
bc.Start(ipn.Options{})
|
||||
if forceReauth {
|
||||
bc.StartLoginInteractive()
|
||||
}
|
||||
|
||||
<-pumpCtx.Done() // wait for authURL or complete failure
|
||||
if authURL == "" && retErr == nil {
|
||||
if !forceReauth {
|
||||
return "", nil // no auth URL is fine
|
||||
}
|
||||
retErr = pumpCtx.Err()
|
||||
}
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
return authURL, retErr
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -9,11 +12,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
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
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
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
|
||||
@@ -93,6 +98,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
@@ -101,14 +107,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
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/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
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+
|
||||
@@ -217,7 +223,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
net/http from expvar+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/http/httputil from tailscale.com/cmd/tailscale/cli
|
||||
net/http/internal from net/http+
|
||||
net/netip from net+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
|
||||
10
cmd/tailscale/generate.go
Normal file
10
cmd/tailscale/generate.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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
|
||||
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
BIN
cmd/tailscale/manifest_windows_386.syso
Normal file
BIN
cmd/tailscale/manifest_windows_386.syso
Normal file
Binary file not shown.
BIN
cmd/tailscale/manifest_windows_amd64.syso
Normal file
BIN
cmd/tailscale/manifest_windows_amd64.syso
Normal file
Binary file not shown.
BIN
cmd/tailscale/manifest_windows_arm.syso
Normal file
BIN
cmd/tailscale/manifest_windows_arm.syso
Normal file
Binary file not shown.
BIN
cmd/tailscale/manifest_windows_arm64.syso
Normal file
BIN
cmd/tailscale/manifest_windows_arm64.syso
Normal file
Binary file not shown.
13
cmd/tailscale/windows-manifest.xml
Normal file
13
cmd/tailscale/windows-manifest.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -2,6 +2,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
|
||||
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
@@ -64,6 +67,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
@@ -77,7 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
@@ -111,44 +116,43 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/net/tstun+
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/namedpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
|
||||
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
|
||||
💣 golang.zx2c4.com/wireguard/device from tailscale.com/net/tstun+
|
||||
💣 golang.zx2c4.com/wireguard/ipc from golang.zx2c4.com/wireguard/device
|
||||
W 💣 golang.zx2c4.com/wireguard/ipc/namedpipe from golang.zx2c4.com/wireguard/ipc
|
||||
golang.zx2c4.com/wireguard/ratelimiter from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/replay from golang.zx2c4.com/wireguard/device
|
||||
golang.zx2c4.com/wireguard/rwcancel from golang.zx2c4.com/wireguard/device+
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
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/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
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2+
|
||||
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 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/checksum from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
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+
|
||||
@@ -173,7 +177,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
inet.af/peercred from tailscale.com/ipn/ipnauth
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
@@ -198,6 +202,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
@@ -215,6 +220,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
@@ -228,7 +234,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
@@ -241,13 +247,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
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/tunstats from tailscale.com/net/tstun
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
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+
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/smallzstd from tailscale.com/cmd/tailscaled+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
@@ -264,12 +269,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/control/controlbase+
|
||||
tailscale.com/types/logger from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/netlogtype from tailscale.com/net/tstun+
|
||||
tailscale.com/types/logid from tailscale.com/logtail+
|
||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/tkatype from tailscale.com/tka+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
@@ -279,21 +286,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
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+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
@@ -302,7 +309,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/netlog from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
@@ -312,7 +319,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
@@ -323,7 +330,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/poly1305 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
@@ -338,15 +345,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/conn+
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/conn+
|
||||
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/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+
|
||||
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled+
|
||||
@@ -398,6 +405,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
flag from tailscale.com/control/controlclient+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
@@ -421,12 +429,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
net/http/httputil from github.com/aws/smithy-go/transport/http+
|
||||
net/http/internal from net/http+
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled+
|
||||
net/netip from golang.zx2c4.com/wireguard/conn+
|
||||
net/netip from github.com/tailscale/wireguard-go/conn+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/signal from tailscale.com/cmd/tailscaled
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
|
||||
10
cmd/tailscaled/generate.go
Normal file
10
cmd/tailscaled/generate.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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
|
||||
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
BIN
cmd/tailscaled/manifest_windows_386.syso
Normal file
BIN
cmd/tailscaled/manifest_windows_386.syso
Normal file
Binary file not shown.
BIN
cmd/tailscaled/manifest_windows_amd64.syso
Normal file
BIN
cmd/tailscaled/manifest_windows_amd64.syso
Normal file
Binary file not shown.
BIN
cmd/tailscaled/manifest_windows_arm.syso
Normal file
BIN
cmd/tailscaled/manifest_windows_arm.syso
Normal file
Binary file not shown.
BIN
cmd/tailscaled/manifest_windows_arm64.syso
Normal file
BIN
cmd/tailscaled/manifest_windows_arm64.syso
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || darwin || freebsd
|
||||
//go:build linux || darwin || freebsd || openbsd
|
||||
|
||||
package main
|
||||
|
||||
|
||||
106
cmd/tailscaled/taildrop.go
Normal file
106
cmd/tailscaled/taildrop.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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.
|
||||
|
||||
//go:build go1.19
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
|
||||
dg := distro.Get()
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS, distro.QNAP:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
if err != nil {
|
||||
logf("%s Taildrop support: %v", dg, err)
|
||||
} else {
|
||||
logf("%s Taildrop: using %v", dg, path)
|
||||
lb.SetDirectFileRoot(path)
|
||||
lb.SetDirectFileDoFinalRename(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findTaildropDir(dg distro.Distro) (string, error) {
|
||||
const name = "Taildrop"
|
||||
switch dg {
|
||||
case distro.Synology:
|
||||
return findSynologyTaildropDir(name)
|
||||
case distro.TrueNAS:
|
||||
return findTrueNASTaildropDir(name)
|
||||
case distro.QNAP:
|
||||
return findQnapTaildropDir(name)
|
||||
}
|
||||
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
|
||||
}
|
||||
|
||||
// findSynologyTaildropDir looks for the first volume containing a
|
||||
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
|
||||
// but on DSM7 at least, we lack permissions to run that.
|
||||
func findSynologyTaildropDir(name string) (dir string, err error) {
|
||||
for i := 1; i <= 16; i++ {
|
||||
dir = fmt.Sprintf("/volume%v/%s", i, name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findTrueNASTaildropDir returns the first matching directory of
|
||||
// /mnt/{name} or /mnt/*/{name}
|
||||
func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
|
||||
dir = fmt.Sprintf("/mnt/%s", name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// but if running on the host, it may be something like /mnt/Primary/Taildrop
|
||||
fis, err := os.ReadDir("/mnt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading /mnt: %w", err)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findQnapTaildropDir checks if a Shared Folder named "Taildrop" exists.
|
||||
func findQnapTaildropDir(name string) (string, error) {
|
||||
dir := fmt.Sprintf("/share/%s", name)
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// share/Taildrop is usually a symlink to CACHEDEV1_DATA/Taildrop/ or some such.
|
||||
fullpath, err := filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("symlink to shared folder %q not found", name)
|
||||
}
|
||||
if fi, err = os.Stat(fullpath); err == nil && fi.IsDir() {
|
||||
return dir, nil // return the symlink, how QNAP set it up
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
@@ -33,11 +33,13 @@ import (
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
@@ -45,6 +47,8 @@ import (
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -107,6 +111,9 @@ func defaultPort() uint16 {
|
||||
return uint16(p)
|
||||
}
|
||||
}
|
||||
if envknob.GOOS() == "windows" {
|
||||
return 41641
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -273,13 +280,22 @@ func statePathOrDefault() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func ipnServerOpts() (o ipnserver.Options) {
|
||||
// Allow changing the OS-specific IPN behavior for tests
|
||||
// so we can e.g. test Windows-specific behaviors on Linux.
|
||||
goos := envknob.String("TS_DEBUG_TAILSCALED_IPN_GOOS")
|
||||
if goos == "" {
|
||||
goos = runtime.GOOS
|
||||
}
|
||||
// serverOptions is the configuration of the Tailscale node agent.
|
||||
type serverOptions struct {
|
||||
// VarRoot is the Tailscale daemon's private writable
|
||||
// directory (usually "/var/lib/tailscale" on Linux) that
|
||||
// contains the "tailscaled.state" file, the "certs" directory
|
||||
// for TLS certs, and the "files" directory for incoming
|
||||
// Taildrop files before they're moved to a user directory.
|
||||
// If empty, Taildrop and TLS certs don't function.
|
||||
VarRoot string
|
||||
|
||||
// LoginFlags specifies the LoginFlags to pass to the client.
|
||||
LoginFlags controlclient.LoginFlags
|
||||
}
|
||||
|
||||
func ipnServerOpts() (o serverOptions) {
|
||||
goos := envknob.GOOS()
|
||||
|
||||
o.VarRoot = args.statedir
|
||||
|
||||
@@ -302,20 +318,19 @@ func ipnServerOpts() (o ipnserver.Options) {
|
||||
// TODO(bradfitz): if we start using browser LocalStorage
|
||||
// or something, then rethink this.
|
||||
o.LoginFlags = controlclient.LoginEphemeral
|
||||
fallthrough
|
||||
default:
|
||||
o.SurviveDisconnects = true
|
||||
case "windows":
|
||||
// Not those.
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var err error
|
||||
var logPol *logpolicy.Policy
|
||||
var debugMux *http.ServeMux
|
||||
|
||||
func run() error {
|
||||
pol := logpolicy.New(logtail.CollectionNode)
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
logPol = pol
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
@@ -359,23 +374,97 @@ func run() error {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
logid := pol.PublicID.String()
|
||||
return startIPNServer(context.Background(), logf, logid)
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
// Exit gracefully by cancelling the ipnserver context in most common cases:
|
||||
// interrupted from the TTY or killed by a service manager.
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
// SIGPIPE sometimes gets generated when CLIs disconnect from
|
||||
// tailscaled. The default action is to terminate the process, we
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// continue
|
||||
}
|
||||
}()
|
||||
|
||||
srv := ipnserver.New(logf, logid)
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
var lbErr syncs.AtomicValue[error]
|
||||
|
||||
go func() {
|
||||
t0 := time.Now()
|
||||
if s, ok := envknob.LookupInt("TS_DEBUG_BACKEND_DELAY_SEC"); ok {
|
||||
d := time.Duration(s) * time.Second
|
||||
logf("sleeping %v before starting backend...", d)
|
||||
select {
|
||||
case <-time.After(d):
|
||||
logf("slept %v; starting backend...", d)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
lb, err := getLocalBackend(ctx, logf, logid)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
srv.SetLocalBackend(lb)
|
||||
return
|
||||
}
|
||||
lbErr.Store(err) // before the following cancel
|
||||
cancel() // make srv.Run below complete
|
||||
}()
|
||||
|
||||
err = srv.Run(ctx, ln)
|
||||
|
||||
if err != nil && lbErr.Load() != nil {
|
||||
return fmt.Errorf("getLocalBackend error: %v", lbErr.Load())
|
||||
}
|
||||
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
return nil, fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
if logPol != nil {
|
||||
logPol.Logtail.SetLinkMonitor(linkMon)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
return nil, fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
@@ -391,12 +480,12 @@ func run() error {
|
||||
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
|
||||
ns.ProcessLocalIPs = onlyNetstack
|
||||
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
|
||||
|
||||
if useNetstack {
|
||||
if onlyNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
@@ -425,69 +514,49 @@ func run() error {
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// Exit gracefully by cancelling the ipnserver context in most common cases:
|
||||
// interrupted from the TTY or killed by a service manager.
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
// SIGPIPE sometimes gets generated when CLIs disconnect from
|
||||
// tailscaled. The default action is to terminate the process, we
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// continue
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnServerOpts()
|
||||
|
||||
store, err := store.New(logf, statePathOrDefault())
|
||||
if err != nil {
|
||||
return fmt.Errorf("store.New: %w", err)
|
||||
return nil, fmt.Errorf("store.New: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, opts)
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "", dialer, e, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
}
|
||||
ns.SetLocalBackend(srv.LocalBackend())
|
||||
if err := ns.Start(); err != nil {
|
||||
lb.SetVarRoot(opts.VarRoot)
|
||||
if logPol != nil {
|
||||
lb.SetLogFlusher(logPol.Logtail.StartFlush)
|
||||
}
|
||||
if root := lb.TailscaleVarRoot(); root != "" {
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
|
||||
}
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
configureTaildrop(logf, lb)
|
||||
if err := ns.Start(lb); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
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.
|
||||
if err != nil && err != context.Canceled {
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return lb, nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
// createEngine tries to the wgengine.Engine based on the order of tunnels
|
||||
// specified in the command line flags.
|
||||
//
|
||||
// onlyNetstack is true if the user has explicitly requested that we use netstack
|
||||
// for all networking.
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
e, onlyNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
if err == nil {
|
||||
return e, useNetstack, nil
|
||||
return e, onlyNetstack, nil
|
||||
}
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
@@ -495,8 +564,12 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
|
||||
return nil, false, multierr.New(errs...)
|
||||
}
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
|
||||
// handleSubnetsInNetstack reports whether netstack should handle subnet routers
|
||||
// as opposed to the OS. We do this if the OS doesn't support subnet routers
|
||||
// (e.g. Windows) or if the user has explicitly requested it (e.g.
|
||||
// --tun=userspace-networking).
|
||||
func handleSubnetsInNetstack() bool {
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_NETSTACK_SUBNETS"); ok {
|
||||
return v
|
||||
}
|
||||
if distro.Get() == distro.Synology {
|
||||
@@ -511,15 +584,17 @@ func shouldWrapNetstack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
var tstunNew = tstun.New
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
}
|
||||
|
||||
useNetstack = name == "userspace-networking"
|
||||
netns.SetEnabled(!useNetstack)
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
netns.SetEnabled(!onlyNetstack)
|
||||
|
||||
if args.birdSocketPath != "" && createBIRDClient != nil {
|
||||
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||
@@ -528,7 +603,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
|
||||
}
|
||||
}
|
||||
if useNetstack {
|
||||
if onlyNetstack {
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
// On Synology in netstack mode, still init a DNS
|
||||
// manager (directManager) to avoid the health check
|
||||
@@ -542,7 +617,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
dev, devName, err := tstunNew(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name, err)
|
||||
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
@@ -567,15 +642,15 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if shouldWrapNetstack() {
|
||||
if handleSubnetsInNetstack() {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, useNetstack, err
|
||||
return nil, onlyNetstack, err
|
||||
}
|
||||
return e, useNetstack, nil
|
||||
return e, onlyNetstack, nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
|
||||
@@ -20,39 +20,89 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
// to C:\ to run it, like tswin does.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/dblohm7/wingoes/com"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize COM process-wide.
|
||||
comProcessType := com.Service
|
||||
if !isWindowsService() {
|
||||
comProcessType = com.ConsoleApp
|
||||
}
|
||||
if err := com.StartRuntime(comProcessType); err != nil {
|
||||
log.Printf("wingoes.com.StartRuntime(%d) failed: %v", comProcessType, err)
|
||||
}
|
||||
}
|
||||
|
||||
const serviceName = "Tailscale"
|
||||
|
||||
// Application-defined command codes between 128 and 255
|
||||
// See https://web.archive.org/web/20221007222822/https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-controlservice
|
||||
const (
|
||||
cmdUninstallWinTun = svc.Cmd(128 + iota)
|
||||
)
|
||||
|
||||
func init() {
|
||||
tstunNew = tstunNewWithWindowsRetries
|
||||
}
|
||||
|
||||
// tstunNewOrRetry is a wrapper around tstun.New that retries on Windows for certain
|
||||
// errors.
|
||||
//
|
||||
// TODO(bradfitz): move this into tstun and/or just fix the problems so it doesn't
|
||||
// require a few tries to work.
|
||||
func tstunNewWithWindowsRetries(logf logger.Logf, tunName string) (_ tun.Device, devName string, _ error) {
|
||||
bo := backoff.NewBackoff("tstunNew", logf, 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
for {
|
||||
dev, devName, err := tstun.New(logf, tunName)
|
||||
if err == nil {
|
||||
return dev, devName, err
|
||||
}
|
||||
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) || windowsUptime() < 10*time.Minute {
|
||||
// Wintun is not installing correctly. Dump the state of NetSetupSvc
|
||||
// (which is a user-mode service that must be active for network devices
|
||||
// to install) and its dependencies to the log.
|
||||
winutil.LogSvcState(logf, "NetSetupSvc")
|
||||
}
|
||||
bo.BackOff(ctx, err)
|
||||
if ctx.Err() != nil {
|
||||
return nil, "", ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isWindowsService() bool {
|
||||
v, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
@@ -106,6 +156,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
@@ -117,7 +168,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
// writer that logpolicy already installed as the global
|
||||
// output.
|
||||
logger := log.New(log.Default().Writer(), "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
babysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
|
||||
@@ -141,6 +192,26 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
syslogf("Service session change notification")
|
||||
handleSessionChange(cmd)
|
||||
changes <- cmd.CurrentStatus
|
||||
case cmdUninstallWinTun:
|
||||
syslogf("Stopping tailscaled child process and uninstalling WinTun")
|
||||
// At this point, doneCh is the channel which will be closed when the
|
||||
// tailscaled subprocess exits. We save that to childDoneCh.
|
||||
childDoneCh := doneCh
|
||||
// We reset doneCh to a new channel that will keep the event loop
|
||||
// running until the uninstallation is done.
|
||||
doneCh = make(chan struct{})
|
||||
// Trigger subprocess shutdown.
|
||||
cancel()
|
||||
go func() {
|
||||
// When this goroutine completes, tell the service to break out of its
|
||||
// event loop.
|
||||
defer close(doneCh)
|
||||
// Wait for the subprocess to shutdown.
|
||||
<-childDoneCh
|
||||
// Now uninstall WinTun.
|
||||
uninstallWinTun(log.Printf)
|
||||
}()
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +249,8 @@ func cmdName(c svc.Cmd) string {
|
||||
return "SessionChange"
|
||||
case svc.PreShutdown:
|
||||
return "PreShutdown"
|
||||
case cmdUninstallWinTun:
|
||||
return "(Application Defined) Uninstall WinTun"
|
||||
}
|
||||
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
|
||||
}
|
||||
@@ -201,17 +274,24 @@ func beWindowsSubprocess() bool {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err == io.EOF {
|
||||
// Parent wants us to shut down gracefully.
|
||||
log.Printf("subproc received EOF from stdin")
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("stdin err (parent process died): %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(context.Background(), logid)
|
||||
err := startIPNServer(ctx, log.Printf, logid)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
@@ -258,140 +338,6 @@ func beFirewallKillswitch() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("monitor: %w", err)
|
||||
}
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) {
|
||||
// Wintun is not installing correctly. Dump the state of NetSetupSvc
|
||||
// (which is a user-mode service that must be active for network devices
|
||||
// to install) and its dependencies to the log.
|
||||
winutil.LogSvcState(logf, "NetSetupSvc")
|
||||
}
|
||||
return nil, nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev, nil)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, nil, fmt.Errorf("router: %w", err)
|
||||
}
|
||||
if shouldWrapNetstack() {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
ns, err := newNetstack(logf, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = false
|
||||
ns.ProcessSubnets = shouldWrapNetstack()
|
||||
if err := ns.Start(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), ns, nil
|
||||
}
|
||||
|
||||
type engineOrError struct {
|
||||
Engine wgengine.Engine
|
||||
Netstack *netstack.Impl
|
||||
Err error
|
||||
}
|
||||
engErrc := make(chan engineOrError)
|
||||
t0 := time.Now()
|
||||
go func() {
|
||||
const ms = time.Millisecond
|
||||
for try := 1; ; try++ {
|
||||
logf("tailscaled: getting engine... (try %v)", try)
|
||||
t1 := time.Now()
|
||||
eng, ns, err := getEngineRaw()
|
||||
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
|
||||
if err != nil {
|
||||
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
|
||||
try, d, dt, windowsUptime().Round(time.Second), err)
|
||||
} else {
|
||||
if try > 1 {
|
||||
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
|
||||
} else {
|
||||
logf("tailscaled: got engine in %v", d)
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
engErrc <- engineOrError{eng, ns, err}
|
||||
if err == nil {
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
<-timer.C
|
||||
}
|
||||
}()
|
||||
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
getEngine := func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
return nil, nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
}
|
||||
for {
|
||||
res := <-engErrc
|
||||
if res.Engine != nil {
|
||||
return res.Engine, res.Netstack, nil
|
||||
}
|
||||
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
|
||||
// Ignore errors during early boot. Windows 10 auto logs in the GUI
|
||||
// way sooner than the networking stack components start up.
|
||||
// So the network will fail for a bit (and require a few tries) while
|
||||
// the GUI is still fine.
|
||||
continue
|
||||
}
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
return nil, nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
}
|
||||
}
|
||||
store, err := store.New(logf, statePathOrDefault())
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: %w", err)
|
||||
}
|
||||
|
||||
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func handleSessionChange(chgRequest svc.ChangeRequest) {
|
||||
if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
|
||||
return
|
||||
@@ -415,3 +361,143 @@ func windowsUptime() time.Duration {
|
||||
r, _, _ := getTickCount64Proc.Call()
|
||||
return time.Duration(int64(r)) * time.Millisecond
|
||||
}
|
||||
|
||||
// babysitProc runs the current executable as a child process with the
|
||||
// provided args, capturing its output, writing it to files, and
|
||||
// restarting the process on any crashes.
|
||||
func babysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
wStdin *os.File
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
var sig os.Signal
|
||||
select {
|
||||
case sig = <-interrupt:
|
||||
logf("babysitProc: got signal: %v", sig)
|
||||
close(done)
|
||||
proc.mu.Lock()
|
||||
proc.p.Signal(sig)
|
||||
proc.mu.Unlock()
|
||||
case <-ctx.Done():
|
||||
logf("babysitProc: context done")
|
||||
close(done)
|
||||
proc.mu.Lock()
|
||||
// Closing wStdin gives the subprocess a chance to shut down cleanly,
|
||||
// which is important for cleaning up DNS settings etc.
|
||||
proc.wStdin.Close()
|
||||
proc.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
bo := backoff.NewBackoff("babysitProc", logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
// When the writer goes away, the reader gets EOF.
|
||||
// A subproc can watch its stdin and exit when it gets EOF;
|
||||
// this is a very reliable way to have a subproc die when
|
||||
// its parent (us) disappears.
|
||||
// We never need to actually write to wStdin.
|
||||
rStdin, wStdin, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 1: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a pipe object to use as the subproc's stdout/stderr.
|
||||
// We'll read from this pipe and send it to logf, line by line.
|
||||
// We can't use os.exec's io.Writer for this because it
|
||||
// doesn't care about lines, and thus ends up merging multiple
|
||||
// log lines into one or splitting one line into multiple
|
||||
// logf() calls. bufio is more appropriate.
|
||||
rStdout, wStdout, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 2: %v", err)
|
||||
}
|
||||
go func(r *os.File) {
|
||||
defer r.Close()
|
||||
rb := bufio.NewReader(r)
|
||||
for {
|
||||
s, err := rb.ReadString('\n')
|
||||
if s != "" {
|
||||
logf("%s", s)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(rStdout)
|
||||
|
||||
cmd.Stdin = rStdin
|
||||
cmd.Stdout = wStdout
|
||||
cmd.Stderr = wStdout
|
||||
err = cmd.Start()
|
||||
|
||||
// Now that the subproc is started, get rid of our copy of the
|
||||
// pipe reader. Bad things happen on Windows if more than one
|
||||
// process owns the read side of a pipe.
|
||||
rStdin.Close()
|
||||
wStdout.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("starting subprocess failed: %v", err)
|
||||
} else {
|
||||
proc.mu.Lock()
|
||||
proc.p = cmd.Process
|
||||
proc.wStdin = wStdin
|
||||
proc.mu.Unlock()
|
||||
|
||||
err = cmd.Wait()
|
||||
log.Printf("subprocess exited: %v", err)
|
||||
}
|
||||
|
||||
// If the process finishes, clean up the write side of the
|
||||
// pipe. We'll make a new one when we restart the subproc.
|
||||
wStdin.Close()
|
||||
|
||||
if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" {
|
||||
log.Fatalf("Process ended.")
|
||||
}
|
||||
|
||||
if time.Since(startTime) < 60*time.Second {
|
||||
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
|
||||
} else {
|
||||
// Reset the timeout, since the process ran for a while.
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallWinTun(logf logger.Logf) {
|
||||
dll := windows.NewLazyDLL("wintun.dll")
|
||||
if err := dll.Load(); err != nil {
|
||||
logf("Cannot load wintun.dll for uninstall: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logf("Removing wintun driver...")
|
||||
err := wintun.Uninstall()
|
||||
logf("Uninstall: %v", err)
|
||||
}
|
||||
|
||||
13
cmd/tailscaled/windows-manifest.xml
Normal file
13
cmd/tailscaled/windows-manifest.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
@@ -114,9 +115,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
}
|
||||
ns.ProcessLocalIPs = true
|
||||
ns.ProcessSubnets = true
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
return true
|
||||
}
|
||||
@@ -124,16 +123,19 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
srv, err := ipnserver.New(logf, lpc.PublicID.String(), store, eng, dialer, ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
LoginFlags: controlclient.LoginEphemeral,
|
||||
AutostartStateKey: "wasm",
|
||||
})
|
||||
logid := lpc.PublicID.String()
|
||||
srv := ipnserver.New(logf, logid)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, "wasm", dialer, eng, controlclient.LoginEphemeral)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver.New: %v", err)
|
||||
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
|
||||
}
|
||||
lb := srv.LocalBackend()
|
||||
ns.SetLocalBackend(lb)
|
||||
if err := ns.Start(lb); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
srv.SetLocalBackend(lb)
|
||||
|
||||
jsIPN := &jsIPN{
|
||||
dialer: dialer,
|
||||
|
||||
@@ -384,7 +384,7 @@ func main() {
|
||||
genView(buf, it, typ, pkg.Types)
|
||||
}
|
||||
out := pkg.Name + "_view.go"
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, codegen.CopyrightYear("."), it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if runCloner {
|
||||
|
||||
@@ -74,8 +74,9 @@ type Direct struct {
|
||||
keepSharerAndUserSplit bool
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -109,13 +110,14 @@ type Options struct {
|
||||
NewDecompressor func() (Decompressor, error)
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
|
||||
// Status is called when there's a change in status.
|
||||
Status func(Status)
|
||||
@@ -241,6 +243,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dialPlan: opts.DialPlan,
|
||||
@@ -1008,6 +1011,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
c.logf("netmap: control says to open URL %v; no popBrowser func", u)
|
||||
}
|
||||
}
|
||||
if resp.ClientVersion != nil && c.onClientVersion != nil {
|
||||
c.onClientVersion(resp.ClientVersion)
|
||||
}
|
||||
if resp.ControlTime != nil && !resp.ControlTime.IsZero() {
|
||||
c.logf.JSON(1, "controltime", resp.ControlTime.UTC())
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,7 @@ type mapSession struct {
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastPacketFilterRules views.Slice[tailcfg.FilterRule]
|
||||
lastParsedPacketFilter []filter.Match
|
||||
lastSSHPolicy *tailcfg.SSHPolicy
|
||||
collectServices bool
|
||||
@@ -96,6 +98,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
var err error
|
||||
ms.lastPacketFilterRules = views.SliceOf(pf)
|
||||
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
ms.logf("parsePacketFilter: %v", err)
|
||||
@@ -147,21 +150,22 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
PacketFilterRules: ms.lastPacketFilterRules,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
@@ -201,7 +202,7 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
Key: ptrTo(key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
||||
Key: ptr.To(key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
@@ -229,7 +230,7 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
DiscoKey: ptrTo(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
||||
DiscoKey: ptr.To(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
@@ -243,12 +244,12 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
Online: ptrTo(true),
|
||||
Online: ptr.To(true),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "foo",
|
||||
Online: ptrTo(true),
|
||||
Online: ptr.To(true),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -257,12 +258,12 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
LastSeen: ptrTo(time.Unix(123, 0).UTC()),
|
||||
LastSeen: ptr.To(time.Unix(123, 0).UTC()),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "foo",
|
||||
LastSeen: ptrTo(time.Unix(123, 0).UTC()),
|
||||
LastSeen: ptr.To(time.Unix(123, 0).UTC()),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -271,7 +272,7 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
KeyExpiry: ptrTo(time.Unix(123, 0).UTC()),
|
||||
KeyExpiry: ptr.To(time.Unix(123, 0).UTC()),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
@@ -285,7 +286,7 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
Capabilities: ptrTo([]string{"foo"}),
|
||||
Capabilities: ptr.To([]string{"foo"}),
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
@@ -307,10 +308,6 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTo[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
@@ -1560,22 +1561,20 @@ func (s *Server) AddPacketForwarder(dst key.NodePublic, fwd PacketForwarder) {
|
||||
// Duplicate registration of same forwarder. Ignore.
|
||||
return
|
||||
}
|
||||
if m, ok := prev.(multiForwarder); ok {
|
||||
if _, ok := m[fwd]; ok {
|
||||
if m, ok := prev.(*multiForwarder); ok {
|
||||
if _, ok := m.all[fwd]; ok {
|
||||
// Duplicate registration of same forwarder in set; ignore.
|
||||
return
|
||||
}
|
||||
m[fwd] = m.maxVal() + 1
|
||||
m.add(fwd)
|
||||
return
|
||||
}
|
||||
if prev != nil {
|
||||
// Otherwise, the existing value is not a set,
|
||||
// not a dup, and not local-only (nil) so make
|
||||
// it a set.
|
||||
fwd = multiForwarder{
|
||||
prev: 1, // existed 1st, higher priority
|
||||
fwd: 2, // the passed in fwd is in 2nd place
|
||||
}
|
||||
// it a set. `prev` existed first, so will have higher
|
||||
// priority.
|
||||
fwd = newMultiForwarder(prev, fwd)
|
||||
s.multiForwarderCreated.Add(1)
|
||||
}
|
||||
}
|
||||
@@ -1591,19 +1590,14 @@ func (s *Server) RemovePacketForwarder(dst key.NodePublic, fwd PacketForwarder)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if m, ok := v.(multiForwarder); ok {
|
||||
if len(m) < 2 {
|
||||
if m, ok := v.(*multiForwarder); ok {
|
||||
if len(m.all) < 2 {
|
||||
panic("unexpected")
|
||||
}
|
||||
delete(m, fwd)
|
||||
// If fwd was in m and we no longer need to be a
|
||||
// multiForwarder, replace the entry with the
|
||||
// remaining PacketForwarder.
|
||||
if len(m) == 1 {
|
||||
var remain PacketForwarder
|
||||
for k := range m {
|
||||
remain = k
|
||||
}
|
||||
if remain, isLast := m.deleteLocked(fwd); isLast {
|
||||
// If fwd was in m and we no longer need to be a
|
||||
// multiForwarder, replace the entry with the
|
||||
// remaining PacketForwarder.
|
||||
s.clientsMesh[dst] = remain
|
||||
s.multiForwarderDeleted.Add(1)
|
||||
}
|
||||
@@ -1635,27 +1629,65 @@ func (s *Server) RemovePacketForwarder(dst key.NodePublic, fwd PacketForwarder)
|
||||
// client is. The map value is unique connection number; the lowest
|
||||
// one has been seen the longest. It's used to make sure we forward
|
||||
// packets consistently to the same node and don't pick randomly.
|
||||
type multiForwarder map[PacketForwarder]uint8
|
||||
type multiForwarder struct {
|
||||
fwd syncs.AtomicValue[PacketForwarder] // preferred forwarder.
|
||||
all map[PacketForwarder]uint8 // all forwarders, protected by s.mu.
|
||||
}
|
||||
|
||||
func (m multiForwarder) maxVal() (max uint8) {
|
||||
for _, v := range m {
|
||||
// newMultiForwarder creates a new multiForwarder.
|
||||
// The first PacketForwarder passed to this function will be the preferred one.
|
||||
func newMultiForwarder(fwds ...PacketForwarder) *multiForwarder {
|
||||
f := &multiForwarder{all: make(map[PacketForwarder]uint8)}
|
||||
f.fwd.Store(fwds[0])
|
||||
for idx, fwd := range fwds {
|
||||
f.all[fwd] = uint8(idx)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// add adds a new forwarder to the map with a connection number that
|
||||
// is higher than the existing ones.
|
||||
func (f *multiForwarder) add(fwd PacketForwarder) {
|
||||
var max uint8
|
||||
for _, v := range f.all {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return
|
||||
f.all[fwd] = max + 1
|
||||
}
|
||||
|
||||
func (m multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte) error {
|
||||
var fwd PacketForwarder
|
||||
var lowest uint8
|
||||
for k, v := range m {
|
||||
if fwd == nil || v < lowest {
|
||||
fwd = k
|
||||
lowest = v
|
||||
// deleteLocked removes a packet forwarder from the map. It expects Server.mu to be held.
|
||||
// If only one forwarder remains after the removal, it will be returned alongside a `true` boolean value.
|
||||
func (f *multiForwarder) deleteLocked(fwd PacketForwarder) (_ PacketForwarder, isLast bool) {
|
||||
delete(f.all, fwd)
|
||||
|
||||
if fwd == f.fwd.Load() {
|
||||
// The preferred forwarder has been removed, choose a new one
|
||||
// based on the lowest index.
|
||||
var lowestfwd PacketForwarder
|
||||
var lowest uint8
|
||||
for k, v := range f.all {
|
||||
if lowestfwd == nil || v < lowest {
|
||||
lowestfwd = k
|
||||
lowest = v
|
||||
}
|
||||
}
|
||||
if lowestfwd != nil {
|
||||
f.fwd.Store(lowestfwd)
|
||||
}
|
||||
}
|
||||
return fwd.ForwardPacket(src, dst, payload)
|
||||
|
||||
if len(f.all) == 1 {
|
||||
for k := range f.all {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (f *multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte) error {
|
||||
return f.fwd.Load().ForwardPacket(src, dst, payload)
|
||||
}
|
||||
|
||||
func (s *Server) expVarFunc(f func() any) expvar.Func {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -723,20 +724,14 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
},
|
||||
u1: newMultiForwarder(testFwd(1), testFwd(100)),
|
||||
})
|
||||
wantCounter(&s.multiForwarderCreated, 1)
|
||||
|
||||
// Removing a forwarder in a multi set that doesn't exist; does nothing.
|
||||
s.RemovePacketForwarder(u1, testFwd(55))
|
||||
want(map[key.NodePublic]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
testFwd(100): 2,
|
||||
},
|
||||
u1: newMultiForwarder(testFwd(1), testFwd(100)),
|
||||
})
|
||||
|
||||
// Removing a forwarder in a multi set that does exist should collapse it away
|
||||
@@ -785,6 +780,76 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type channelFwd struct {
|
||||
// id is to ensure that different instances that reference the
|
||||
// same channel are not equal, as they are used as keys in the
|
||||
// multiForwarder map.
|
||||
id int
|
||||
c chan []byte
|
||||
}
|
||||
|
||||
func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error {
|
||||
f.c <- packet
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMultiForwarder(t *testing.T) {
|
||||
received := 0
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan []byte)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
s := &Server{
|
||||
clients: make(map[key.NodePublic]clientSet),
|
||||
clientsMesh: map[key.NodePublic]PacketForwarder{},
|
||||
}
|
||||
u := pubAll(1)
|
||||
s.AddPacketForwarder(u, channelFwd{1, ch})
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
received += 1
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
s.AddPacketForwarder(u, channelFwd{2, ch})
|
||||
s.AddPacketForwarder(u, channelFwd{3, ch})
|
||||
s.RemovePacketForwarder(u, channelFwd{2, ch})
|
||||
s.RemovePacketForwarder(u, channelFwd{1, ch})
|
||||
s.AddPacketForwarder(u, channelFwd{1, ch})
|
||||
s.RemovePacketForwarder(u, channelFwd{3, ch})
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Number of messages is chosen arbitrarily, just for this loop to
|
||||
// run long enough concurrently with {Add,Remove}PacketForwarder loop above.
|
||||
numMsgs := 5000
|
||||
var fwd PacketForwarder
|
||||
for i := 0; i < numMsgs; i++ {
|
||||
s.mu.Lock()
|
||||
fwd = s.clientsMesh[u]
|
||||
s.mu.Unlock()
|
||||
fwd.ForwardPacket(u, u, []byte(strconv.Itoa(i)))
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
if received != numMsgs {
|
||||
t.Errorf("expected %d messages to be forwarded; got %d", numMsgs, received)
|
||||
}
|
||||
}
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := key.NewNode()
|
||||
pub := priv.Public()
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -39,6 +38,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/strs"
|
||||
)
|
||||
|
||||
// Client is a DERP-over-HTTP client.
|
||||
@@ -1028,9 +1028,10 @@ var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.NodePublic, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") {
|
||||
// Look for derpkey prefix added by initMetacert() on the server side.
|
||||
if pubHex, ok := strs.CutPrefix(cert.Subject.CommonName, "derpkey"); ok {
|
||||
var err error
|
||||
serverPub, err = key.ParseNodePublicUntyped(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
serverPub, err = key.ParseNodePublicUntyped(mem.S(pubHex))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/strs"
|
||||
)
|
||||
|
||||
func TestMarshalAndParse(t *testing.T) {
|
||||
@@ -72,10 +72,10 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
foo := []byte("foo")
|
||||
got := string(tt.m.AppendMarshal(foo))
|
||||
if !strings.HasPrefix(got, "foo") {
|
||||
got, ok := strs.CutPrefix(got, "foo")
|
||||
if !ok {
|
||||
t.Fatalf("didn't start with foo: got %q", got)
|
||||
}
|
||||
got = strings.TrimPrefix(got, "foo")
|
||||
|
||||
gotHex := fmt.Sprintf("% x", got)
|
||||
if gotHex != tt.want {
|
||||
|
||||
@@ -9,6 +9,7 @@ package webhooks
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -95,7 +96,7 @@ func verifyWebhookSignature(req *http.Request, secret string) (events []event, e
|
||||
// Verify that the signatures match.
|
||||
var match bool
|
||||
for _, signature := range signatures[currentVersion] {
|
||||
if signature == want {
|
||||
if subtle.ConstantTimeCompare([]byte(signature), []byte(want)) == 1 {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -29,17 +29,20 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
regStr = map[string]*string{}
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
regStr = map[string]*string{}
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
regDuration = map[string]*time.Duration{}
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
@@ -96,6 +99,9 @@ func Setenv(envVar, val string) {
|
||||
if p := regOptBool[envVar]; p != nil {
|
||||
setOptBoolLocked(p, envVar, val)
|
||||
}
|
||||
if p := regDuration[envVar]; p != nil {
|
||||
setDurationLocked(p, envVar, val)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the named environment variable, using os.Getenv.
|
||||
@@ -158,6 +164,25 @@ func RegisterOptBool(envVar string) func() opt.Bool {
|
||||
return func() opt.Bool { return *p }
|
||||
}
|
||||
|
||||
// RegisterDuration returns a func that gets the named environment variable as a
|
||||
// duration, without a map lookup per call. It assumes that any mutations happen
|
||||
// via envknob.Setenv.
|
||||
func RegisterDuration(envVar string) func() time.Duration {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p, ok := regDuration[envVar]
|
||||
if !ok {
|
||||
val := os.Getenv(envVar)
|
||||
if val != "" {
|
||||
noteEnvLocked(envVar, val)
|
||||
}
|
||||
p = new(time.Duration)
|
||||
setDurationLocked(p, envVar, val)
|
||||
regDuration[envVar] = p
|
||||
}
|
||||
return func() time.Duration { return *p }
|
||||
}
|
||||
|
||||
func setBoolLocked(p *bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -184,6 +209,19 @@ func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
p.Set(b)
|
||||
}
|
||||
|
||||
func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
*p = 0
|
||||
return
|
||||
}
|
||||
var err error
|
||||
*p, err = time.ParseDuration(val)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid duration environment variable %s value %q", envVar, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Bool returns the boolean value of the named environment variable.
|
||||
// If the variable is not set, it returns false.
|
||||
// An invalid value exits the binary with a failure.
|
||||
@@ -264,24 +302,27 @@ func LookupInt(envVar string) (v int, ok bool) {
|
||||
// of Work-In-Progress code.
|
||||
func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
|
||||
|
||||
// CanSSHD is whether the Tailscale SSH server is allowed to run.
|
||||
// CanSSHD reports whether the Tailscale SSH server is allowed to run.
|
||||
//
|
||||
// If disabled, the SSH server won't start (won't intercept port 22)
|
||||
// if already enabled and any attempt to re-enable it will result in
|
||||
// an error.
|
||||
// If disabled (when this reports false), the SSH server won't start (won't
|
||||
// intercept port 22) if previously configured to do so and any attempt to
|
||||
// re-enable it will result in an error.
|
||||
func CanSSHD() bool { return !Bool("TS_DISABLE_SSH_SERVER") }
|
||||
|
||||
// CanTaildrop reports whether the Taildrop feature is allowed to function.
|
||||
//
|
||||
// If disabled, Taildrop won't receive files regardless of user & server config.
|
||||
func CanTaildrop() bool { return !Bool("TS_DISABLE_TAILDROP") }
|
||||
|
||||
// SSHPolicyFile returns the path, if any, to the SSHPolicy JSON file for development.
|
||||
func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
|
||||
|
||||
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
|
||||
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
|
||||
|
||||
|
||||
// TKASkipSignatureCheck is whether to skip node-key signature checking for development.
|
||||
func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION") }
|
||||
|
||||
|
||||
// NoLogsNoSupport reports whether the client's opted out of log uploads and
|
||||
// technical support.
|
||||
func NoLogsNoSupport() bool {
|
||||
@@ -429,3 +470,14 @@ func applyKeyValueEnv(r io.Reader) error {
|
||||
}
|
||||
return bs.Err()
|
||||
}
|
||||
|
||||
// IPCVersion returns version.Long usually, unless TS_DEBUG_FAKE_IPC_VERSION is
|
||||
// set, in which it contains that value. This is only used for weird development
|
||||
// cases when testing mismatched versions and you want the client to act like it's
|
||||
// compatible with the server.
|
||||
func IPCVersion() string {
|
||||
if v := String("TS_DEBUG_FAKE_IPC_VERSION"); v != "" {
|
||||
return v
|
||||
}
|
||||
return version.Long
|
||||
}
|
||||
|
||||
17
envknob/envknob_nottest.go
Normal file
17
envknob/envknob_nottest.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
//go:build ts_not_in_tests
|
||||
|
||||
package envknob
|
||||
|
||||
import "runtime"
|
||||
|
||||
func GOOS() string {
|
||||
// When the "ts_not_in_tests" build tag is used, we define this func to just
|
||||
// return a simple constant so callers optimize just as if the knob were not
|
||||
// present. We can then build production/optimized builds with the
|
||||
// "ts_not_in_tests" build tag.
|
||||
return runtime.GOOS
|
||||
}
|
||||
24
envknob/envknob_testable.go
Normal file
24
envknob/envknob_testable.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
|
||||
//go:build !ts_not_in_tests
|
||||
|
||||
package envknob
|
||||
|
||||
import "runtime"
|
||||
|
||||
// GOOS reports the effective runtime.GOOS to run as.
|
||||
//
|
||||
// In practice this returns just runtime.GOOS, unless overridden by
|
||||
// test TS_DEBUG_FAKE_GOOS.
|
||||
//
|
||||
// This allows changing OS-specific stuff like the IPN server behavior
|
||||
// for tests so we can e.g. test Windows-specific behaviors on Linux.
|
||||
// This isn't universally used.
|
||||
func GOOS() string {
|
||||
if v := String("TS_DEBUG_FAKE_GOOS"); v != "" {
|
||||
return v
|
||||
}
|
||||
return runtime.GOOS
|
||||
}
|
||||
60
flake.lock
generated
Normal file
60
flake.lock
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1668681692,
|
||||
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1671848398,
|
||||
"narHash": "sha256-cJIIPd1kvCI6ne/S0facbiBNH7sZUzk405GfdSJPwZE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bb0359be0a1a08c8d74412fe8c69aa2ffb3f477e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
157
flake.nix
Normal file
157
flake.nix
Normal file
@@ -0,0 +1,157 @@
|
||||
# flake.nix describes a Nix source repository that provides
|
||||
# development builds of Tailscale and the fork of the Go compiler
|
||||
# toolchain that Tailscale maintains. It also provides a development
|
||||
# environment for working on tailscale, for use with "nix develop".
|
||||
#
|
||||
# For more information about this and why this file is useful, see:
|
||||
# https://nixos.wiki/wiki/Flakes
|
||||
#
|
||||
# Also look into direnv: https://direnv.net/, this can make it so that you can
|
||||
# automatically get your environment set up when you change folders into the
|
||||
# project.
|
||||
#
|
||||
# WARNING: currently, the packages provided by this flake are brittle,
|
||||
# and importing this flake into your own Nix configs is likely to
|
||||
# leave you with broken builds periodically.
|
||||
#
|
||||
# The issue is that building Tailscale binaries uses the buildGoModule
|
||||
# helper from nixpkgs. This helper demands to know the content hash of
|
||||
# all of the Go dependencies of this repo, in the form of a Nix SRI
|
||||
# hash. This hash isn't automatically kept in sync with changes made
|
||||
# to go.mod yet, and so every time we update go.mod while hacking on
|
||||
# Tailscale, this flake ends up with a broken build due to hash
|
||||
# mismatches.
|
||||
#
|
||||
# Right now, this flake is intended for use by Tailscale developers,
|
||||
# who are aware of this mismatch and willing to live with it. At some
|
||||
# point, we'll add automation to keep the hashes more in sync, at
|
||||
# which point this caveat should go away.
|
||||
#
|
||||
# See https://github.com/tailscale/tailscale/issues/6845 for tracking
|
||||
# how to fix this mismatch.
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
# Used by shell.nix as a compat shim.
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, flake-compat }: let
|
||||
# Grab a helper func out of the Nix language libraries. Annoyingly
|
||||
# these are only accessible through legacyPackages right now,
|
||||
# which forces us to indirect through a platform-specific
|
||||
# path. The x86_64-linux in here doesn't really matter, since all
|
||||
# we're grabbing is a pure Nix string manipulation function that
|
||||
# doesn't build any software.
|
||||
fileContents = nixpkgs.legacyPackages.x86_64-linux.lib.fileContents;
|
||||
|
||||
tailscale-go-rev = fileContents ./go.toolchain.rev;
|
||||
tailscale-go-sri = fileContents ./go.toolchain.sri;
|
||||
|
||||
# pkgsWithTailscaleGo takes a nixpkgs package set, and replaces
|
||||
# its Go 1.19 compiler with tailscale's fork.
|
||||
#
|
||||
# We need to do this because the buildGoModule helper function is
|
||||
# constructed with legacy nix imports, so we cannot construct a
|
||||
# buildGoModule variant that uses tailscale's toolchain. Instead,
|
||||
# we have to replace the toolchain in nixpkgs, and let lazy
|
||||
# evaluation propagate it into the nixpkgs instance of
|
||||
# buildGoModule.
|
||||
#
|
||||
# This is a bit roundabout, but there doesn't seem to be a more
|
||||
# elegant way of resolving the impedance mismatch between legacy
|
||||
# nixpkgs style imports and flake semantics, unless upstream
|
||||
# nixpkgs exposes the buildGoModule constructor func explicitly.
|
||||
pkgsWithTailscaleGo = pkgs: pkgs.extend (final: prev: rec {
|
||||
tailscale_go = prev.lib.overrideDerivation prev.go_1_19 (attrs: rec {
|
||||
name = "tailscale-go-${version}";
|
||||
version = tailscale-go-rev;
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "tailscale";
|
||||
repo = "go";
|
||||
rev = tailscale-go-rev;
|
||||
sha256 = tailscale-go-sri;
|
||||
};
|
||||
nativeBuildInputs = attrs.nativeBuildInputs ++ [ pkgs.git ];
|
||||
# Remove dependency on xcbuild as that causes iOS/macOS builds to fail.
|
||||
propagatedBuildInputs = [];
|
||||
checkPhase = "";
|
||||
TAILSCALE_TOOLCHAIN_REV = tailscale-go-rev;
|
||||
});
|
||||
# Override go_1_19 so that buildGo119Module below uses
|
||||
# tailscale's toolchain as well.
|
||||
go_1_19 = tailscale_go;
|
||||
});
|
||||
|
||||
# tailscaleRev is the git commit at which this flake was imported,
|
||||
# or the empty string when building from a local checkout of the
|
||||
# tailscale repo.
|
||||
tailscaleRev = if builtins.hasAttr "rev" self then self.rev else "";
|
||||
# tailscale takes a nixpkgs package set, and builds Tailscale from
|
||||
# the same commit as this flake. IOW, it provides "tailscale built
|
||||
# from HEAD", where HEAD is "whatever commit you imported the
|
||||
# flake at".
|
||||
#
|
||||
# This is currently unfortunately brittle, because we have to
|
||||
# specify vendorSha256, and that sha changes any time we alter
|
||||
# go.mod. We don't want to force a nix dependency on everyone
|
||||
# hacking on Tailscale, so this flake is likely to have broken
|
||||
# builds periodically until somoene comes through and manually
|
||||
# fixes them up. I sure wish there was a way to express "please
|
||||
# just trust the local go.mod, vendorSha256 has no benefit here",
|
||||
# but alas.
|
||||
#
|
||||
# So really, this flake is for tailscale devs to dogfood with, if
|
||||
# you're an end user you should be prepared for this flake to not
|
||||
# build periodically.
|
||||
tailscale = pkgs: pkgs.buildGo119Module rec {
|
||||
name = "tailscale";
|
||||
|
||||
src = ./.;
|
||||
vendorSha256 = fileContents ./go.mod.sri;
|
||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper pkgs.git ];
|
||||
ldflags = ["-X tailscale.com/version.GitCommit=${tailscaleRev}"];
|
||||
CGO_ENABLED = 0;
|
||||
subPackages = [ "cmd/tailscale" "cmd/tailscaled" ];
|
||||
doCheck = false;
|
||||
postInstall = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
||||
wrapProgram $out/bin/tailscaled --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.iproute2 pkgs.iptables pkgs.getent pkgs.shadow ]}
|
||||
wrapProgram $out/bin/tailscale --suffix PATH : ${pkgs.lib.makeBinPath [ pkgs.procps ]}
|
||||
|
||||
sed -i -e "s#/usr/sbin#$out/bin#" -e "/^EnvironmentFile/d" ./cmd/tailscaled/tailscaled.service
|
||||
install -D -m0444 -t $out/lib/systemd/system ./cmd/tailscaled/tailscaled.service
|
||||
'';
|
||||
};
|
||||
|
||||
# This whole blob makes the tailscale package available for all
|
||||
# OS/CPU combos that nix supports, as well as a dev shell so that
|
||||
# "nix develop" and "nix-shell" give you a dev env.
|
||||
flakeForSystem = nixpkgs: system: let
|
||||
upstreamPkgs = nixpkgs.legacyPackages.${system};
|
||||
pkgs = pkgsWithTailscaleGo upstreamPkgs;
|
||||
ts = tailscale pkgs;
|
||||
in {
|
||||
packages = {
|
||||
tailscale-go = pkgs.tailscale-go;
|
||||
tailscale = ts;
|
||||
};
|
||||
devShell = pkgs.mkShell {
|
||||
packages = with upstreamPkgs; [
|
||||
curl
|
||||
git
|
||||
gopls
|
||||
gotools
|
||||
graphviz
|
||||
perl
|
||||
pkgs.tailscale_go
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-imidcDJGVor43PqdTX7Js4/tjQ0JA2E1GdjuyLiPDHI= sha256-+5icFKDHXt3JMbUjLQGes4R+GeUi48xRgGd0yPKVrw0=
|
||||
92
go.mod
92
go.mod
@@ -4,6 +4,7 @@ go 1.19
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.3
|
||||
github.com/Microsoft/go-winio v0.6.0
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/andybalholm/brotli v1.0.3
|
||||
@@ -17,25 +18,31 @@ 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/dblohm7/wingoes v0.0.0-20221124203957-6ac47ab19aa5
|
||||
github.com/dsnet/try v0.0.3
|
||||
github.com/evanw/esbuild v0.14.53
|
||||
github.com/frankban/quicktest v1.14.0
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/go-json-experiment/json v0.0.0-20221017203807-c5ed296b8c92
|
||||
github.com/go-logr/zapr v1.2.3
|
||||
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/go-containerregistry v0.9.0
|
||||
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/illarion/gonotify v1.0.1
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531
|
||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.15.4
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
|
||||
github.com/mattn/go-colorable v0.1.12
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mdlayher/genetlink v1.2.0
|
||||
github.com/mdlayher/netlink v1.6.0
|
||||
github.com/mdlayher/sdnotify v1.0.0
|
||||
@@ -53,27 +60,35 @@ require (
|
||||
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/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667
|
||||
github.com/tc-hib/winres v0.1.6
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go.uber.org/zap v1.21.0
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/term v0.1.0
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.12
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8
|
||||
golang.org/x/term v0.2.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
golang.org/x/tools v0.2.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20220517111757-f4a2f64ce238
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6
|
||||
k8s.io/api v0.25.0
|
||||
k8s.io/apimachinery v0.25.0
|
||||
nhooyr.io/websocket v1.8.7
|
||||
sigs.k8s.io/controller-runtime v0.13.1
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
software.sslmate.com/src/go-pkcs12 v0.2.0
|
||||
)
|
||||
|
||||
@@ -82,15 +97,16 @@ require (
|
||||
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
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.2.0 // indirect
|
||||
@@ -118,6 +134,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/cloudflare/circl v1.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -126,18 +143,25 @@ require (
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.16+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/esimonov/ifshort v1.0.3 // indirect
|
||||
github.com/ettle/strcase v0.1.1 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fzipp/gocyclo v0.3.1 // indirect
|
||||
github.com/gliderlabs/ssh v0.3.3 // indirect
|
||||
github.com/go-critic/go-critic v0.6.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/go-toolsmith/astcast v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.1 // indirect
|
||||
@@ -148,6 +172,7 @@ require (
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
|
||||
@@ -160,7 +185,8 @@ require (
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/go-containerregistry v0.9.0 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
@@ -181,7 +207,7 @@ require (
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
|
||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||
@@ -196,12 +222,11 @@ require (
|
||||
github.com/ldez/gomoddirectives v0.2.2 // indirect
|
||||
github.com/ldez/tagliatelle v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/maratori/testpackage v1.0.1 // indirect
|
||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||
github.com/mdlayher/socket v0.2.3 // indirect
|
||||
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
|
||||
@@ -210,9 +235,13 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/moricho/tparallel v0.2.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nishanths/exhaustive v0.7.11 // indirect
|
||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
@@ -224,7 +253,7 @@ require (
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
@@ -259,7 +288,7 @@ require (
|
||||
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/ultraware/funlen v0.0.3 // indirect
|
||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
||||
@@ -269,18 +298,31 @@ require (
|
||||
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
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.25.0 // indirect
|
||||
k8s.io/client-go v0.25.0 // indirect
|
||||
k8s.io/component-base v0.25.0 // indirect
|
||||
k8s.io/klog/v2 v2.70.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
mvdan.cc/gofumpt v0.2.0 // indirect
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
|
||||
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
||||
1
go.mod.sri
Normal file
1
go.mod.sri
Normal file
@@ -0,0 +1 @@
|
||||
sha256-+5icFKDHXt3JMbUjLQGes4R+GeUi48xRgGd0yPKVrw0=
|
||||
186
go.sum
186
go.sum
@@ -61,8 +61,8 @@ github.com/Antonboom/nilnil v0.1.0 h1:DLDavmg0a6G/F4Lt9t7Enrbgb3Oph6LnDE6YVsmTt7
|
||||
github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
@@ -84,14 +84,18 @@ github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jB
|
||||
github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 h1:XcF0cTDJeiuZ5NU8w7WUDge0HRwwNRmxj/GGk6KSA6g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
|
||||
@@ -167,6 +171,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZ
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA=
|
||||
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -189,6 +195,7 @@ github.com/breml/bidichk v0.2.1 h1:SRNtZuLdfkxtocj+xyHXKC1Uv3jVi6EPYx+NHSTNQvE=
|
||||
github.com/breml/bidichk v0.2.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso=
|
||||
github.com/butuzov/ireturn v0.1.1 h1:QvrO2QF2+/Cx1WA/vETCIYBKtRjc30vesdoPUNo1EbY=
|
||||
github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -206,6 +213,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
|
||||
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -248,6 +257,8 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dblohm7/wingoes v0.0.0-20221124203957-6ac47ab19aa5 h1:84SSlQpWqllOmtng34NorWGJbzX00SI2J4MQjXNYUuU=
|
||||
github.com/dblohm7/wingoes v0.0.0-20221124203957-6ac47ab19aa5/go.mod h1:LPBSRY0diEb4/R1gqa4OaBexvmklv7XdPv7m6cudDR8=
|
||||
github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0=
|
||||
github.com/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218=
|
||||
github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA=
|
||||
@@ -261,10 +272,13 @@ github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405c
|
||||
github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
|
||||
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
|
||||
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
|
||||
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -280,6 +294,11 @@ 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/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/evanw/esbuild v0.14.53 h1:9uU73SZUmP1jRQhaC6hPm9aoqFGYlPwfk7OrhG6AhpQ=
|
||||
github.com/evanw/esbuild v0.14.53/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
|
||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||
@@ -295,8 +314,9 @@ github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzP
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
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=
|
||||
@@ -338,10 +358,25 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
|
||||
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
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=
|
||||
@@ -394,6 +429,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -466,6 +502,8 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs=
|
||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
||||
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -484,6 +522,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/go-containerregistry v0.9.0 h1:5Ths7RjxyFV0huKChQTgY6fLzvHhZMpLTFNja8U0/0w=
|
||||
github.com/google/go-containerregistry v0.9.0/go.mod h1:9eq4BnSufyT1kHNffX+vSXVonaJ7yaIOulrKZejMxnQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@@ -624,8 +664,8 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -649,8 +689,11 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB
|
||||
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531 h1:3HNVAxEgGca1i23Ai/8DeCmibx02jBvTHAT11INaVfU=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
@@ -741,6 +784,10 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
||||
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ=
|
||||
github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU=
|
||||
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
|
||||
@@ -774,8 +821,9 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
|
||||
github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||
@@ -844,6 +892,8 @@ github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8q
|
||||
github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo=
|
||||
@@ -854,6 +904,8 @@ github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4N
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nishanths/exhaustive v0.1.0/go.mod h1:S1j9110vxV1ECdCudXRkeMnFQ/DQk9ajLT0Uf2MYZQQ=
|
||||
github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8BfaIeMzgBNKvc=
|
||||
@@ -878,6 +930,7 @@ github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
@@ -934,8 +987,9 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -1038,6 +1092,7 @@ github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0H
|
||||
github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||
@@ -1068,6 +1123,7 @@ github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhU
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
@@ -1104,6 +1160,10 @@ github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/
|
||||
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=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 h1:etWp6uUwKu8NEj37K2OuMBnZ7EnVMKA7gJg5AqPFy/o=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667/go.mod h1:iiClgxBTruKI+nmzlQxbFw6c3nB/wb4Td/WCyX2berY=
|
||||
github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
|
||||
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -1137,11 +1197,11 @@ github.com/tommy-muehle/go-mnd/v2 v2.4.0 h1:1t0f8Uiaq+fqKteUR4N9Umr6E99R+lDnLnq7
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d h1:sT5Q2xFrqgm/3yrCkVLkVuEFpG07UXz9ALqxxN1SmZc=
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d/go.mod h1:jMbuI3nENTNzHW9mYwQ57b8/DSuJTq+joYY18x/WGxE=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad h1:0lEUXaz1vhlAtoMpu18vhb16s5rGRpNCl2trxc2/Qbg=
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
|
||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww=
|
||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
||||
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/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
@@ -1220,15 +1280,23 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4=
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
@@ -1261,8 +1329,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1274,12 +1342,14 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1305,8 +1375,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.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1367,8 +1437,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.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1385,6 +1455,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1397,8 +1469,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-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1482,7 +1554,6 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1497,20 +1568,24 @@ golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20211002104244-808efd93c36d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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-20220114195835-da31bd327af9/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-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8 h1:/VqMvhQCyzfuc826eNrpWmMb3AwD2Sxz/HMsYIhwcIs=
|
||||
golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8/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/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1527,8 +1602,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -1640,18 +1715,18 @@ 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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
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.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-20220904105730-b51010ba13f0 h1:5ZkdpbduT/g+9OtbSDvbF3KvfQG45CtH/ppO8FUmvCQ=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220904105730-b51010ba13f0/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
@@ -1688,6 +1763,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
|
||||
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -1726,6 +1802,7 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
@@ -1804,6 +1881,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@@ -1831,9 +1910,9 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 h1:cv/zaNV0nr1mJzaeo4S5mHIm5va1W0/9J3/5prlsuRM=
|
||||
gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=
|
||||
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
|
||||
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=
|
||||
@@ -1852,6 +1931,23 @@ inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1D
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6 h1:BfgDtKnWJTeu+xI1aOEweXdPwqOhB3IbQUDj1XuftcY=
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
|
||||
k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0=
|
||||
k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk=
|
||||
k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY=
|
||||
k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E=
|
||||
k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU=
|
||||
k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0=
|
||||
k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E=
|
||||
k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8=
|
||||
k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y=
|
||||
k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk=
|
||||
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
|
||||
k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ=
|
||||
k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA=
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU=
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||
mvdan.cc/gofumpt v0.0.0-20200802201014-ab5a8192947d/go.mod h1:bzrjFmaD6+xqohD3KYP0H2FEuxknnBmyyOxdhLdaIws=
|
||||
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475/go.mod h1:E4LOcu9JQEtnYXtB1Y51drqh2Qr2Ngk9J3YrRCwcbd0=
|
||||
mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
|
||||
@@ -1870,7 +1966,15 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg=
|
||||
sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237/go.mod h1:/xvNRWUqm0+/ZMiF4EX00vrSCMsE4/NHb+Pt3freEeQ=
|
||||
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
|
||||
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
|
||||
|
||||
@@ -1 +1 @@
|
||||
3fd24dee31726924c1b61c8037a889b30b8aa0f6
|
||||
dc0ce6324d19b7539e8efebc64c94631615fd80a
|
||||
|
||||
1
go.toolchain.sri
Normal file
1
go.toolchain.sri
Normal file
@@ -0,0 +1 @@
|
||||
sha256-imidcDJGVor43PqdTX7Js4/tjQ0JA2E1GdjuyLiPDHI=
|
||||
@@ -19,15 +19,16 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
var (
|
||||
// mu guards everything in this var block.
|
||||
mu sync.Mutex
|
||||
|
||||
sysErr = map[Subsystem]error{} // error key => err (or nil for no error)
|
||||
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
|
||||
warnables = map[*Warnable]struct{}{} // set of warnables
|
||||
sysErr = map[Subsystem]error{} // error key => err (or nil for no error)
|
||||
watchers = set.HandleSet[func(Subsystem, error)]{} // opt func to run if error state changes
|
||||
warnables = map[*Warnable]struct{}{} // set of warnables
|
||||
timer *time.Timer
|
||||
|
||||
debugHandler = map[string]http.Handler{}
|
||||
@@ -47,6 +48,7 @@ var (
|
||||
udp4Unbound bool
|
||||
controlHealth []string
|
||||
lastLoginErr error
|
||||
localLogConfigErr error
|
||||
)
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
@@ -68,6 +70,9 @@ const (
|
||||
|
||||
// SysDNSManager is the name of the net/dns manager subsystem.
|
||||
SysDNSManager = Subsystem("dns-manager")
|
||||
|
||||
// SysTKA is the name of the tailnet key authority subsystem.
|
||||
SysTKA = Subsystem("tailnet-lock")
|
||||
)
|
||||
|
||||
// NewWarnable returns a new warnable item that the caller can mark
|
||||
@@ -148,8 +153,6 @@ func AppendWarnableDebugFlags(base []string) []string {
|
||||
return ret
|
||||
}
|
||||
|
||||
type watchHandle byte
|
||||
|
||||
// RegisterWatcher adds a function that will be called if an
|
||||
// error changes state either to unhealthy or from unhealthy. It is
|
||||
// not called on transition from unknown to healthy. It must be non-nil
|
||||
@@ -157,8 +160,7 @@ type watchHandle byte
|
||||
func RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
handle := new(watchHandle)
|
||||
watchers[handle] = cb
|
||||
handle := watchers.Add(cb)
|
||||
if timer == nil {
|
||||
timer = time.AfterFunc(time.Minute, timerSelfCheck)
|
||||
}
|
||||
@@ -174,27 +176,40 @@ func RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) {
|
||||
}
|
||||
|
||||
// SetRouterHealth sets the state of the wgengine/router.Router.
|
||||
func SetRouterHealth(err error) { set(SysRouter, err) }
|
||||
func SetRouterHealth(err error) { setErr(SysRouter, err) }
|
||||
|
||||
// RouterHealth returns the wgengine/router.Router error state.
|
||||
func RouterHealth() error { return get(SysRouter) }
|
||||
|
||||
// SetDNSHealth sets the state of the net/dns.Manager
|
||||
func SetDNSHealth(err error) { set(SysDNS, err) }
|
||||
func SetDNSHealth(err error) { setErr(SysDNS, err) }
|
||||
|
||||
// DNSHealth returns the net/dns.Manager error state.
|
||||
func DNSHealth() error { return get(SysDNS) }
|
||||
|
||||
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator
|
||||
func SetDNSOSHealth(err error) { set(SysDNSOS, err) }
|
||||
func SetDNSOSHealth(err error) { setErr(SysDNSOS, err) }
|
||||
|
||||
// SetDNSManagerHealth sets the state of the Linux net/dns manager's
|
||||
// discovery of the /etc/resolv.conf situation.
|
||||
func SetDNSManagerHealth(err error) { set(SysDNSManager, err) }
|
||||
func SetDNSManagerHealth(err error) { setErr(SysDNSManager, err) }
|
||||
|
||||
// DNSOSHealth returns the net/dns.OSConfigurator error state.
|
||||
func DNSOSHealth() error { return get(SysDNSOS) }
|
||||
|
||||
// SetTKAHealth sets the health of the tailnet key authority.
|
||||
func SetTKAHealth(err error) { setErr(SysTKA, err) }
|
||||
|
||||
// TKAHealth returns the tailnet key authority error state.
|
||||
func TKAHealth() error { return get(SysTKA) }
|
||||
|
||||
// SetLocalLogConfigHealth sets the error state of this client's local log configuration.
|
||||
func SetLocalLogConfigHealth(err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
localLogConfigErr = err
|
||||
}
|
||||
|
||||
func RegisterDebugHandler(typ string, h http.Handler) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -213,7 +228,7 @@ func get(key Subsystem) error {
|
||||
return sysErr[key]
|
||||
}
|
||||
|
||||
func set(key Subsystem, err error) {
|
||||
func setErr(key Subsystem, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
setLocked(key, err)
|
||||
@@ -399,6 +414,9 @@ func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
return errors.New("network down")
|
||||
}
|
||||
if localLogConfigErr != nil {
|
||||
return localLogConfigErr
|
||||
}
|
||||
if !ipnWantRunning {
|
||||
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
@@ -70,11 +71,9 @@ func condCall[T any](fn func() T) T {
|
||||
}
|
||||
|
||||
var (
|
||||
lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptrTo(inContainer)}
|
||||
lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptr.To(inContainer)}
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
type lazyAtomicValue[T any] struct {
|
||||
// f is a pointer to a fill function. If it's nil or points
|
||||
// to nil, then Get returns the zero value for T.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -22,8 +23,8 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptrTo(freebsdVersionMeta)}
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptrTo(osVersionFreeBSD)}
|
||||
lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptr.To(freebsdVersionMeta)}
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptr.To(osVersionFreeBSD)}
|
||||
)
|
||||
|
||||
func distroNameFreeBSD() string {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -29,8 +30,8 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptrTo(linuxVersionMeta)}
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptrTo(osVersionLinux)}
|
||||
lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptr.To(linuxVersionMeta)}
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptr.To(osVersionLinux)}
|
||||
)
|
||||
|
||||
type versionMeta struct {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
@@ -20,8 +21,8 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptrTo(osVersionWindows)}
|
||||
lazyPackageType = &lazyAtomicValue[string]{f: ptrTo(packageTypeWindows)}
|
||||
lazyOSVersion = &lazyAtomicValue[string]{f: ptr.To(osVersionWindows)}
|
||||
lazyPackageType = &lazyAtomicValue[string]{f: ptr.To(packageTypeWindows)}
|
||||
)
|
||||
|
||||
func osVersionWindows() string {
|
||||
|
||||
@@ -52,6 +52,23 @@ type EngineStatus struct {
|
||||
LivePeers map[key.NodePublic]ipnstate.PeerStatusLite
|
||||
}
|
||||
|
||||
// NotifyWatchOpt is a bitmask of options about what type of Notify messages
|
||||
// to subscribe to.
|
||||
type NotifyWatchOpt uint64
|
||||
|
||||
const (
|
||||
// NotifyWatchEngineUpdates, if set, causes Engine updates to be sent to the
|
||||
// client either regularly or when they change, without having to ask for
|
||||
// each one via RequestEngineStatus.
|
||||
NotifyWatchEngineUpdates NotifyWatchOpt = 1 << iota
|
||||
|
||||
NotifyInitialState // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL
|
||||
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
|
||||
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
|
||||
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
)
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
// (cmd/tailscale, iOS, macOS, Win Tasktray).
|
||||
// In any given notification, any or all of these may be nil, meaning
|
||||
@@ -76,6 +93,8 @@ type Notify struct {
|
||||
// FilesWaiting if non-nil means that files are buffered in
|
||||
// the Tailscale daemon and ready for local transfer to the
|
||||
// user's preferred storage location.
|
||||
//
|
||||
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
|
||||
FilesWaiting *empty.Message `json:",omitempty"`
|
||||
|
||||
// IncomingFiles, if non-nil, specifies which files are in the
|
||||
@@ -83,6 +102,8 @@ type Notify struct {
|
||||
// Notify should not update the state of file transfers. A non-nil
|
||||
// but empty IncomingFiles means that no files are in the middle
|
||||
// of being transferred.
|
||||
//
|
||||
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
|
||||
IncomingFiles []PartialFile `json:",omitempty"`
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
@@ -91,6 +112,10 @@ type Notify struct {
|
||||
// macOS Network Extension.
|
||||
LocalTCPPort *uint16 `json:",omitempty"`
|
||||
|
||||
// ClientVersion, if non-nil, describes whether a client version update
|
||||
// is available.
|
||||
ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
|
||||
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
|
||||
@@ -169,57 +194,16 @@ type Options struct {
|
||||
// frontend to the backend.
|
||||
// If non-nil, they are imported as a new profile.
|
||||
LegacyMigrationPrefs *Prefs `json:"Prefs"`
|
||||
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
|
||||
// already stored in the backend state, *except* for the Persist
|
||||
// Persist member. If you just want to provide prefs, this is
|
||||
// UpdatePrefs, if provided, overrides Options.LegacyMigrationPrefs
|
||||
// *and* the Prefs already stored in the backend state, *except* for
|
||||
// the Persist member. If you just want to provide prefs, this is
|
||||
// probably what you want.
|
||||
//
|
||||
// UpdatePrefs.Persist is always ignored. Prefs.Persist will still
|
||||
// be used even if UpdatePrefs is provided. Other than Persist,
|
||||
// UpdatePrefs takes precedence over Prefs.
|
||||
//
|
||||
// This is intended as a purely temporary workaround for the
|
||||
// currently unexpected behaviour of Options.Prefs.
|
||||
//
|
||||
// TODO(apenwarr): Remove this, or rename Prefs to something else
|
||||
// and rename this to Prefs. Or, move Prefs.Persist elsewhere
|
||||
// entirely (as it always should have been), and then we wouldn't
|
||||
// need two separate fields at all. Or, move the fancy state
|
||||
// migration stuff out of Start().
|
||||
// TODO(apenwarr): Rename this to Prefs, and possibly move Prefs.Persist
|
||||
// elsewhere entirely (as it always should have been). Or, move the
|
||||
// fancy state migration stuff out of Start().
|
||||
UpdatePrefs *Prefs
|
||||
// AuthKey is an optional node auth key used to authorize a
|
||||
// new node key without user interaction.
|
||||
AuthKey string
|
||||
}
|
||||
|
||||
// Backend is the interface between Tailscale frontends
|
||||
// (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale
|
||||
// backend (e.g. cmd/tailscaled) running on the same machine.
|
||||
// (It has nothing to do with the interface between the backends
|
||||
// and the cloud control plane.)
|
||||
type Backend interface {
|
||||
// SetNotifyCallback sets the callback to be called on updates
|
||||
// from the backend to the client.
|
||||
SetNotifyCallback(func(Notify))
|
||||
// Start starts or restarts the backend, typically when a
|
||||
// frontend client connects.
|
||||
Start(Options) error
|
||||
// StartLoginInteractive requests to start a new interactive login
|
||||
// flow. This should trigger a new BrowseToURL notification
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *tailcfg.Oauth2Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
// SetPrefs installs a new set of user preferences, including
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*Prefs)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
// polling.
|
||||
RequestEngineStatus()
|
||||
}
|
||||
|
||||
@@ -24,10 +24,7 @@ func (src *Prefs) Clone() *Prefs {
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if dst.Persist != nil {
|
||||
dst.Persist = new(persist.Persist)
|
||||
*dst.Persist = *src.Persist
|
||||
}
|
||||
dst.Persist = src.Persist.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -53,6 +50,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
NoSNAT bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -86,13 +86,8 @@ func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice {
|
||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||
func (v PrefsView) Persist() *persist.Persist {
|
||||
if v.ж.Persist == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Persist
|
||||
return &x
|
||||
}
|
||||
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
@@ -116,6 +111,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
NoSNAT bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
188
ipn/ipnauth/ipnauth.go
Normal file
188
ipn/ipnauth/ipnauth.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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 ipnauth controls access to the LocalAPI.
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"inet.af/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// ConnIdentity represents the owner of a localhost TCP or unix socket connection
|
||||
// connecting to the LocalAPI.
|
||||
type ConnIdentity struct {
|
||||
conn net.Conn
|
||||
notWindows bool // runtime.GOOS != "windows"
|
||||
|
||||
// Fields used when NotWindows:
|
||||
isUnixSock bool // Conn is a *net.UnixConn
|
||||
creds *peercred.Creds // or nil
|
||||
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
// use that for all.
|
||||
pid int
|
||||
userID ipn.WindowsUserID
|
||||
user *user.User
|
||||
}
|
||||
|
||||
// WindowsUserID returns the local machine's userid of the connection
|
||||
// if it's on Windows. Otherwise it returns the empty string.
|
||||
//
|
||||
// It's suitable for passing to LookupUserFromID (os/user.LookupId) on any
|
||||
// operating system.
|
||||
func (ci *ConnIdentity) WindowsUserID() ipn.WindowsUserID {
|
||||
if envknob.GOOS() != "windows" {
|
||||
return ""
|
||||
}
|
||||
if ci.userID != "" {
|
||||
return ci.userID
|
||||
}
|
||||
// For Linux tests running as Windows:
|
||||
const isBroken = true // TODO(bradfitz,maisem): fix tests; this doesn't work yet
|
||||
if ci.creds != nil && !isBroken {
|
||||
if uid, ok := ci.creds.UserID(); ok {
|
||||
return ipn.WindowsUserID(uid)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ci *ConnIdentity) User() *user.User { return ci.user }
|
||||
func (ci *ConnIdentity) Pid() int { return ci.pid }
|
||||
func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock }
|
||||
func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds }
|
||||
|
||||
var metricIssue869Workaround = clientmetric.NewCounter("issue_869_workaround")
|
||||
|
||||
// LookupUserFromID is a wrapper around os/user.LookupId that works around some
|
||||
// issues on Windows. On non-Windows platforms it's identical to user.LookupId.
|
||||
func LookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" {
|
||||
// See if uid resolves as a pseudo-user. Temporary workaround until
|
||||
// https://github.com/golang/go/issues/49509 resolves and ships.
|
||||
if u, err := winutil.LookupPseudoUser(uid); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// TODO(aaron): With LookupPseudoUser in place, I don't expect us to reach
|
||||
// this point anymore. Leaving the below workaround in for now to confirm
|
||||
// that pseudo-user resolution sufficiently handles this problem.
|
||||
|
||||
// The below workaround is only applicable when uid represents a
|
||||
// valid security principal. Omitting this check causes us to succeed
|
||||
// even when uid represents a deleted user.
|
||||
if !winutil.IsSIDValidPrincipal(uid) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricIssue869Workaround.Add(1)
|
||||
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
// So make up a *user.User if their machine is broken in this way.
|
||||
return &user.User{
|
||||
Uid: uid,
|
||||
Username: "unknown-user-" + uid,
|
||||
Name: "unknown user " + uid,
|
||||
}, nil
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
// IsReadonlyConn reports whether the connection should be considered read-only,
|
||||
// meaning it's not allowed to change the state of the node.
|
||||
//
|
||||
// Read-only also means it's not allowed to access sensitive information, which
|
||||
// admittedly doesn't follow from the name. Consider this "IsUnprivileged".
|
||||
// Also, Windows doesn't use this. For Windows it always returns false.
|
||||
//
|
||||
// TODO(bradfitz): rename it? Also make Windows use this.
|
||||
func (ci *ConnIdentity) IsReadonlyConn(operatorUID string, logf logger.Logf) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
return false
|
||||
}
|
||||
const ro = true
|
||||
const rw = false
|
||||
if !safesocket.PlatformUsesPeerCreds() {
|
||||
return rw
|
||||
}
|
||||
creds := ci.creds
|
||||
if creds == nil {
|
||||
logf("connection from unknown peer; read-only")
|
||||
return ro
|
||||
}
|
||||
uid, ok := creds.UserID()
|
||||
if !ok {
|
||||
logf("connection from peer with unknown userid; read-only")
|
||||
return ro
|
||||
}
|
||||
if uid == "0" {
|
||||
logf("connection from userid %v; root has access", uid)
|
||||
return rw
|
||||
}
|
||||
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||
return rw
|
||||
}
|
||||
if operatorUID != "" && uid == operatorUID {
|
||||
logf("connection from userid %v; is configured operator", uid)
|
||||
return rw
|
||||
}
|
||||
if yes, err := isLocalAdmin(uid); err != nil {
|
||||
logf("connection from userid %v; read-only; %v", uid, err)
|
||||
return ro
|
||||
} else if yes {
|
||||
logf("connection from userid %v; is local admin, has access", uid)
|
||||
return rw
|
||||
}
|
||||
logf("connection from userid %v; read-only", uid)
|
||||
return ro
|
||||
}
|
||||
|
||||
func isLocalAdmin(uid string) (bool, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var adminGroup string
|
||||
switch {
|
||||
case runtime.GOOS == "darwin":
|
||||
adminGroup = "admin"
|
||||
case distro.Get() == distro.QNAP:
|
||||
adminGroup = "administrators"
|
||||
default:
|
||||
return false, fmt.Errorf("no system admin group found")
|
||||
}
|
||||
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||
}
|
||||
|
||||
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
|
||||
for _, e := range entries {
|
||||
if e.Local == ra && e.Remote == la {
|
||||
return e.Pid
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
24
ipn/ipnauth/ipnauth_notwindows.go
Normal file
24
ipn/ipnauth/ipnauth_notwindows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"inet.af/peercred"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// GetConnIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// and couldn't. The returned connIdentity has NotWindows set to true.
|
||||
func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci = &ConnIdentity{conn: c, notWindows: true}
|
||||
_, ci.isUnixSock = c.(*net.UnixConn)
|
||||
ci.creds, _ = peercred.Get(c)
|
||||
return ci, nil
|
||||
}
|
||||
59
ipn/ipnauth/ipnauth_windows.go
Normal file
59
ipn/ipnauth/ipnauth_windows.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 ipnauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetNamedPipeClientProcessId = kernel32.NewProc("GetNamedPipeClientProcessId")
|
||||
)
|
||||
|
||||
func getNamedPipeClientProcessId(h windows.Handle) (pid uint32, err error) {
|
||||
r1, _, err := procGetNamedPipeClientProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)))
|
||||
if r1 > 0 {
|
||||
return pid, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// GetConnIdentity extracts the identity information from the connection
|
||||
// based on the user who owns the other end of the connection.
|
||||
// If c is not backed by a named pipe, an error is returned.
|
||||
func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
|
||||
ci = &ConnIdentity{conn: c}
|
||||
h, ok := c.(interface {
|
||||
Fd() uintptr
|
||||
})
|
||||
if !ok {
|
||||
return ci, fmt.Errorf("not a windows handle: %T", c)
|
||||
}
|
||||
pid, err := getNamedPipeClientProcessId(windows.Handle(h.Fd()))
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("getNamedPipeClientProcessId: %v", err)
|
||||
}
|
||||
ci.pid = int(pid)
|
||||
uid, err := pidowner.OwnerOfPID(ci.pid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user (WSL?): %w", err)
|
||||
}
|
||||
ci.userID = ipn.WindowsUserID(uid)
|
||||
u, err := LookupUserFromID(logf, uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
ci.user = u
|
||||
return ci, nil
|
||||
}
|
||||
@@ -26,6 +26,16 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
// Test handler.
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Write(body)
|
||||
case "/logtail/flush":
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if b.TryFlushLogs() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
|
||||
}
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -38,6 +39,7 @@ import (
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/dns"
|
||||
@@ -58,12 +60,14 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/uniq"
|
||||
"tailscale.com/version"
|
||||
@@ -134,9 +138,9 @@ type LocalBackend struct {
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
logFlushFunc func() // or nil if SetLogFlusher wasn't called
|
||||
sshAtomicBool atomic.Bool
|
||||
shutdownCalled bool // if Shutdown has been called
|
||||
|
||||
@@ -157,11 +161,11 @@ type LocalBackend struct {
|
||||
notify func(ipn.Notify)
|
||||
cc controlclient.Client
|
||||
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
||||
inServerMode bool
|
||||
machinePrivKey key.MachinePrivate
|
||||
tka *tkaState
|
||||
state ipn.State
|
||||
capFileSharing bool // whether netMap contains the file sharing capability
|
||||
capTailnetLock bool // whether netMap contains the tailnet lock capability
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
@@ -181,6 +185,8 @@ type LocalBackend struct {
|
||||
peerAPIListeners []*peerAPIListener
|
||||
loginFlags controlclient.LoginFlags
|
||||
incomingFiles map[*incomingFile]bool
|
||||
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
|
||||
notifyWatchers set.HandleSet[chan *ipn.Notify]
|
||||
lastStatusTime time.Time // status.AsOf value of the last processed status update
|
||||
// directFileRoot, if non-empty, means to write received files
|
||||
// directly to this directory, without staging them in an
|
||||
@@ -202,7 +208,8 @@ type LocalBackend struct {
|
||||
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
||||
serveConfig ipn.ServeConfigView // or !Valid if none
|
||||
|
||||
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
|
||||
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
|
||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -515,7 +522,7 @@ func (b *LocalBackend) Shutdown() {
|
||||
}
|
||||
|
||||
func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
|
||||
if !p.Valid() || p.Persist() == nil {
|
||||
if !p.Valid() || !p.Persist().Valid() {
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -531,13 +538,17 @@ func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
|
||||
func (b *LocalBackend) Prefs() ipn.PrefsView {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.sanitizedPrefsLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sanitizedPrefsLocked() ipn.PrefsView {
|
||||
return stripKeysFromPrefs(b.pm.CurrentPrefs())
|
||||
}
|
||||
|
||||
// Status returns the latest status of the backend and its
|
||||
// sub-components.
|
||||
func (b *LocalBackend) Status() *ipnstate.Status {
|
||||
sb := new(ipnstate.StatusBuilder)
|
||||
sb := &ipnstate.StatusBuilder{WantPeers: true}
|
||||
b.UpdateStatus(sb)
|
||||
return sb.Status()
|
||||
}
|
||||
@@ -545,15 +556,19 @@ func (b *LocalBackend) Status() *ipnstate.Status {
|
||||
// StatusWithoutPeers is like Status but omits any details
|
||||
// of peers.
|
||||
func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
|
||||
sb := new(ipnstate.StatusBuilder)
|
||||
b.updateStatus(sb, nil)
|
||||
sb := &ipnstate.StatusBuilder{WantPeers: false}
|
||||
b.UpdateStatus(sb)
|
||||
return sb.Status()
|
||||
}
|
||||
|
||||
// UpdateStatus implements ipnstate.StatusUpdater.
|
||||
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
b.e.UpdateStatus(sb)
|
||||
b.updateStatus(sb, b.populatePeerStatusLocked)
|
||||
var extraLocked func(*ipnstate.StatusBuilder)
|
||||
if sb.WantPeers {
|
||||
extraLocked = b.populatePeerStatusLocked
|
||||
}
|
||||
b.updateStatus(sb, extraLocked)
|
||||
}
|
||||
|
||||
// updateStatus populates sb with status.
|
||||
@@ -814,14 +829,16 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.mu.Lock()
|
||||
|
||||
if st.LogoutFinished != nil {
|
||||
if p := b.pm.CurrentPrefs(); p.Persist() == nil || p.Persist().LoginName == "" {
|
||||
if p := b.pm.CurrentPrefs(); !p.Persist().Valid() || p.Persist().LoginName() == "" {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if err := b.pm.DeleteProfile(b.pm.CurrentProfile().ID); err != nil {
|
||||
b.logf("error deleting profile: %v", err)
|
||||
}
|
||||
b.resetForProfileChangeLockedOnEntry()
|
||||
if err := b.resetForProfileChangeLockedOnEntry(); err != nil {
|
||||
b.logf("resetForProfileChangeLockedOnEntry err: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -843,9 +860,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if !prefs.Persist.View().Equals(*st.Persist) {
|
||||
prefsChanged = true
|
||||
prefs.Persist = st.Persist.AsStruct()
|
||||
if err := b.initTKALocked(); err != nil {
|
||||
b.logf("initTKALocked: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if st.URL != "" {
|
||||
@@ -865,8 +879,29 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if findExitNodeIDLocked(prefs, st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
// Prefs will be written out; this is not safe unless locked or cloned.
|
||||
|
||||
// Perform all mutations of prefs based on the netmap here.
|
||||
if st.NetMap != nil {
|
||||
if b.updatePersistFromNetMapLocked(st.NetMap, prefs) {
|
||||
prefsChanged = true
|
||||
}
|
||||
}
|
||||
// Prefs will be written out if stale; this is not safe unless locked or cloned.
|
||||
if prefsChanged {
|
||||
if err := b.pm.SetPrefs(prefs.View()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
|
||||
// (for new profiles) on the first call to b.pm.SetPrefs.
|
||||
if err := b.initTKALocked(); err != nil {
|
||||
b.logf("initTKALocked: %v", err)
|
||||
}
|
||||
|
||||
// Perform all reconfiguration based on the netmap here.
|
||||
if st.NetMap != nil {
|
||||
b.capTailnetLock = hasCapability(st.NetMap, tailcfg.CapabilityTailnetLockAlpha)
|
||||
|
||||
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
|
||||
if err := b.tkaSyncIfNeeded(st.NetMap, prefs.View()); err != nil {
|
||||
b.logf("[v1] TKA sync error: %v", err)
|
||||
@@ -886,27 +921,31 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if !envknob.TKASkipSignatureCheck() {
|
||||
b.tkaFilterNetmapLocked(st.NetMap)
|
||||
}
|
||||
if b.updatePersistFromNetMapLocked(st.NetMap, prefs) {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.setNetMapLocked(st.NetMap)
|
||||
b.updateFilterLocked(st.NetMap, prefs.View())
|
||||
}
|
||||
|
||||
if prefsChanged {
|
||||
if err := b.pm.SetPrefs(prefs.View()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now complete the lock-free parts of what we started while locked.
|
||||
if prefsChanged {
|
||||
p := prefs.View()
|
||||
b.send(ipn.Notify{Prefs: &p})
|
||||
b.send(ipn.Notify{Prefs: ptr.To(prefs.View())})
|
||||
}
|
||||
|
||||
if st.NetMap != nil {
|
||||
if envknob.NoLogsNoSupport() && hasCapability(st.NetMap, tailcfg.CapabilityDataPlaneAuditLogs) {
|
||||
msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line."
|
||||
health.SetLocalLogConfigHealth(errors.New(msg))
|
||||
// Connecting to this tailnet without logging is forbidden; boot us outta here.
|
||||
b.mu.Lock()
|
||||
prefs.WantRunning = false
|
||||
p := prefs.View()
|
||||
if err := b.pm.SetPrefs(p); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.send(ipn.Notify{ErrMessage: &msg, Prefs: &p})
|
||||
return
|
||||
}
|
||||
if netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
@@ -1122,6 +1161,17 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
if opts.UpdatePrefs != nil {
|
||||
if err := b.checkPrefsLocked(opts.UpdatePrefs); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
} else if opts.LegacyMigrationPrefs != nil {
|
||||
if err := b.checkPrefsLocked(opts.LegacyMigrationPrefs); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
profileID := b.pm.CurrentProfile().ID
|
||||
|
||||
// The iOS client sends a "Start" whenever its UI screen comes
|
||||
@@ -1133,8 +1183,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.logf("Start: already running; sending notify")
|
||||
nm := b.netMap
|
||||
state := b.state
|
||||
b.mu.Unlock()
|
||||
p := b.pm.CurrentPrefs()
|
||||
b.mu.Unlock()
|
||||
b.send(ipn.Notify{
|
||||
State: &state,
|
||||
NetMap: nm,
|
||||
@@ -1176,7 +1226,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
if opts.UpdatePrefs != nil {
|
||||
oldPrefs := b.pm.CurrentPrefs()
|
||||
newPrefs := opts.UpdatePrefs.Clone()
|
||||
newPrefs.Persist = oldPrefs.Persist()
|
||||
newPrefs.Persist = oldPrefs.Persist().AsStruct()
|
||||
pv := newPrefs.View()
|
||||
if err := b.pm.SetPrefs(pv); err != nil {
|
||||
b.logf("failed to save UpdatePrefs state: %v", err)
|
||||
@@ -1194,15 +1244,14 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
loggedOut := prefs.LoggedOut()
|
||||
|
||||
b.inServerMode = prefs.ForceDaemon()
|
||||
b.serverURL = prefs.ControlURLOrDefault()
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
b.logf("Start: serverMode=%v", b.inServerMode)
|
||||
serverURL := prefs.ControlURLOrDefault()
|
||||
if inServerMode := prefs.ForceDaemon(); inServerMode || runtime.GOOS == "windows" {
|
||||
b.logf("Start: serverMode=%v", inServerMode)
|
||||
}
|
||||
b.applyPrefsToHostinfoLocked(hostinfo, prefs)
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
persistv := prefs.Persist()
|
||||
persistv := prefs.Persist().AsStruct()
|
||||
if persistv == nil {
|
||||
persistv = new(persist.Persist)
|
||||
}
|
||||
@@ -1248,7 +1297,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
|
||||
Logf: logger.WithPrefix(b.logf, "control: "),
|
||||
Persist: *persistv,
|
||||
ServerURL: b.serverURL,
|
||||
ServerURL: serverURL,
|
||||
AuthKey: opts.AuthKey,
|
||||
Hostinfo: hostinfo,
|
||||
KeepAlive: true,
|
||||
@@ -1259,6 +1308,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
OnClientVersion: b.onClientVersion,
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||
@@ -1276,6 +1326,10 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.cc = cc
|
||||
b.ccAuto, _ = cc.(*controlclient.Auto)
|
||||
endpoints := b.endpoints
|
||||
|
||||
if err := b.initTKALocked(); err != nil {
|
||||
b.logf("initTKALocked: %v", err)
|
||||
}
|
||||
var tkaHead string
|
||||
if b.tka != nil {
|
||||
head, err := b.tka.authority.Head().MarshalText()
|
||||
@@ -1675,30 +1729,170 @@ func (b *LocalBackend) readPoller() {
|
||||
}
|
||||
}
|
||||
|
||||
// send delivers n to the connected frontend. If no frontend is
|
||||
// connected, the notification is dropped without being delivered.
|
||||
// WatchNotifications subscribes to the ipn.Notify message bus notification
|
||||
// messages.
|
||||
//
|
||||
// WatchNotifications blocks until ctx is done.
|
||||
//
|
||||
// The provided fn will only be called with non-nil pointers. The caller must
|
||||
// not modify roNotify. If fn returns false, the watch also stops.
|
||||
//
|
||||
// Failure to consume many notifications in a row will result in dropped
|
||||
// notifications. There is currently (2022-11-22) no mechanism provided to
|
||||
// detect when a message has been dropped.
|
||||
func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWatchOpt, fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||
ch := make(chan *ipn.Notify, 128)
|
||||
|
||||
origFn := fn
|
||||
if mask&ipn.NotifyNoPrivateKeys != 0 {
|
||||
fn = func(n *ipn.Notify) bool {
|
||||
if n.NetMap == nil || n.NetMap.PrivateKey.IsZero() {
|
||||
return origFn(n)
|
||||
}
|
||||
|
||||
// The netmap in n is shared across all watchers, so to mutate it for a
|
||||
// single watcher we have to clone the notify and the netmap. We can
|
||||
// make shallow clones, at least.
|
||||
nm2 := *n.NetMap
|
||||
n2 := *n
|
||||
n2.NetMap = &nm2
|
||||
n2.NetMap.PrivateKey = key.NodePrivate{}
|
||||
return origFn(&n2)
|
||||
}
|
||||
}
|
||||
|
||||
var ini *ipn.Notify
|
||||
|
||||
b.mu.Lock()
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
|
||||
if mask&initialBits != 0 {
|
||||
ini = &ipn.Notify{Version: version.Long}
|
||||
if mask&ipn.NotifyInitialState != 0 {
|
||||
ini.State = ptr.To(b.state)
|
||||
if b.state == ipn.NeedsLogin {
|
||||
ini.BrowseToURL = ptr.To(b.authURLSticky)
|
||||
}
|
||||
}
|
||||
if mask&ipn.NotifyInitialPrefs != 0 {
|
||||
ini.Prefs = ptr.To(b.sanitizedPrefsLocked())
|
||||
}
|
||||
if mask&ipn.NotifyInitialNetMap != 0 {
|
||||
ini.NetMap = b.netMap
|
||||
}
|
||||
}
|
||||
|
||||
handle := b.notifyWatchers.Add(ch)
|
||||
b.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
b.mu.Lock()
|
||||
delete(b.notifyWatchers, handle)
|
||||
b.mu.Unlock()
|
||||
}()
|
||||
|
||||
if ini != nil {
|
||||
if !fn(ini) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The GUI clients want to know when peers become active or inactive.
|
||||
// They've historically got this information by polling for it, which is
|
||||
// wasteful. As a step towards making it efficient, they now set this
|
||||
// NotifyWatchEngineUpdates bit to ask for us to send it to them only on
|
||||
// change. That's not yet (as of 2022-11-26) plumbed everywhere in
|
||||
// tailscaled yet, so just do the polling here. This ends up causing all IPN
|
||||
// bus watchers to get the notification every 2 seconds instead of just the
|
||||
// GUI client's bus watcher, but in practice there's only 1 total connection
|
||||
// anyway. And if we're polling, at least the client isn't making a new HTTP
|
||||
// request every 2 seconds.
|
||||
// TODO(bradfitz): plumb this further and only send a Notify on change.
|
||||
if mask&ipn.NotifyWatchEngineUpdates != 0 {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go b.pollRequestEngineStatus(ctx)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case n := <-ch:
|
||||
if !fn(n) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollRequestEngineStatus calls b.RequestEngineStatus every 2 seconds until ctx
|
||||
// is done.
|
||||
func (b *LocalBackend) pollRequestEngineStatus(ctx context.Context) {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.RequestEngineStatus()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DebugNotify injects a fake notify message to clients.
|
||||
//
|
||||
// It should only be used via the LocalAPI's debug handler.
|
||||
func (b *LocalBackend) DebugNotify(n ipn.Notify) {
|
||||
b.send(n)
|
||||
}
|
||||
|
||||
// send delivers n to the connected frontend and any API watchers from
|
||||
// LocalBackend.WatchNotifications (via the LocalAPI).
|
||||
//
|
||||
// If no frontend is connected or API watchers are backed up, the notification
|
||||
// is dropped without being delivered.
|
||||
//
|
||||
// If n contains Prefs, those will be sanitized before being delivered.
|
||||
//
|
||||
// b.mu must not be held.
|
||||
func (b *LocalBackend) send(n ipn.Notify) {
|
||||
if n.Prefs != nil {
|
||||
n.Prefs = ptr.To(stripKeysFromPrefs(*n.Prefs))
|
||||
}
|
||||
if n.Version == "" {
|
||||
n.Version = version.Long
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
notifyFunc := b.notify
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
|
||||
if notifyFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if apiSrv.hasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
|
||||
n.Version = version.Long
|
||||
notifyFunc(n)
|
||||
for _, ch := range b.notifyWatchers {
|
||||
select {
|
||||
case ch <- &n:
|
||||
default:
|
||||
// Drop the notification if the channel is full.
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
if notifyFunc != nil {
|
||||
notifyFunc(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sendFileNotify() {
|
||||
var n ipn.Notify
|
||||
|
||||
b.mu.Lock()
|
||||
for _, wakeWaiter := range b.fileWaiters {
|
||||
wakeWaiter()
|
||||
}
|
||||
notifyFunc := b.notify
|
||||
apiSrv := b.peerAPIServer
|
||||
if notifyFunc == nil || apiSrv == nil {
|
||||
@@ -1757,9 +1951,7 @@ func (b *LocalBackend) validPopBrowserURL(urlStr string) bool {
|
||||
case "https":
|
||||
return true
|
||||
case "http":
|
||||
b.mu.Lock()
|
||||
serverURL := b.serverURL
|
||||
b.mu.Unlock()
|
||||
serverURL := b.Prefs().ControlURLOrDefault()
|
||||
// If the control server is using plain HTTP (likely a dev server),
|
||||
// then permit http://.
|
||||
return strings.HasPrefix(serverURL, "http://")
|
||||
@@ -1773,6 +1965,21 @@ func (b *LocalBackend) tellClientToBrowseToURL(url string) {
|
||||
}
|
||||
}
|
||||
|
||||
// onClientVersion is called on MapResponse updates when a MapResponse contains
|
||||
// a non-nil ClientVersion message.
|
||||
func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios":
|
||||
// These auto-update well enough, and we haven't converted the
|
||||
// ClientVersion types to Swift yet, so don't send them in ipn.Notify
|
||||
// messages.
|
||||
default:
|
||||
// But everything else is a Go client and can deal with this field, even
|
||||
// if they ignore it.
|
||||
b.send(ipn.Notify{ClientVersion: v})
|
||||
}
|
||||
}
|
||||
|
||||
// For testing lazy machine key generation.
|
||||
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
|
||||
|
||||
@@ -1810,8 +2017,8 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
}
|
||||
|
||||
var legacyMachineKey key.MachinePrivate
|
||||
if p := b.pm.CurrentPrefs().Persist(); p != nil {
|
||||
legacyMachineKey = p.LegacyFrontendPrivateMachineKey
|
||||
if p := b.pm.CurrentPrefs().Persist(); p.Valid() {
|
||||
legacyMachineKey = p.LegacyFrontendPrivateMachineKey()
|
||||
}
|
||||
|
||||
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
|
||||
@@ -1933,10 +2140,59 @@ func (b *LocalBackend) State() ipn.State {
|
||||
return b.state
|
||||
}
|
||||
|
||||
// InServerMode reports whether the Tailscale backend is explicitly running in
|
||||
// "server mode" where it continues to run despite whatever the platform's
|
||||
// default is. In practice, this is only used on Windows, where the default
|
||||
// tailscaled behavior is to shut down whenever the GUI disconnects.
|
||||
//
|
||||
// On non-Windows platforms, this usually returns false (because people don't
|
||||
// set unattended mode on other platforms) and also isn't checked on other
|
||||
// platforms.
|
||||
//
|
||||
// TODO(bradfitz): rename to InWindowsUnattendedMode or something? Or make this
|
||||
// return true on Linux etc and always be called? It's kinda messy now.
|
||||
func (b *LocalBackend) InServerMode() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.inServerMode
|
||||
return b.pm.CurrentPrefs().ForceDaemon()
|
||||
}
|
||||
|
||||
// CheckIPNConnectionAllowed returns an error if the identity in ci should not
|
||||
// be allowed to connect or make requests to the LocalAPI currently.
|
||||
//
|
||||
// Currently (as of 2022-11-23), this is only used on Windows to check if
|
||||
// we started in server mode and ci is from an identity other than the one
|
||||
// that started the server.
|
||||
func (b *LocalBackend) CheckIPNConnectionAllowed(ci *ipnauth.ConnIdentity) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
serverModeUid := b.pm.CurrentUserID()
|
||||
if serverModeUid == "" {
|
||||
// Either this platform isn't a "multi-user" platform or we're not yet
|
||||
// running as one.
|
||||
return nil
|
||||
}
|
||||
if !b.pm.CurrentPrefs().ForceDaemon() {
|
||||
return nil
|
||||
}
|
||||
uid := ci.WindowsUserID()
|
||||
if uid == "" {
|
||||
return errors.New("empty user uid in connection identity")
|
||||
}
|
||||
if uid != serverModeUid {
|
||||
return fmt.Errorf("Tailscale running in server mode (%q); connection from %q not allowed", b.tryLookupUserName(string(serverModeUid)), b.tryLookupUserName(string(uid)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryLookupUserName tries to look up the username for the uid.
|
||||
// It returns the username on success, or the UID on failure.
|
||||
func (b *LocalBackend) tryLookupUserName(uid string) string {
|
||||
u, err := ipnauth.LookupUserFromID(b.logf, uid)
|
||||
if err != nil {
|
||||
return uid
|
||||
}
|
||||
return u.Username
|
||||
}
|
||||
|
||||
// Login implements Backend.
|
||||
@@ -2084,13 +2340,29 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
return !p.ShieldsUp() && b.netMap.CollectServices
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetCurrentUserID(uid string) {
|
||||
// SetCurrentUserID is used to implement support for multi-user systems (only
|
||||
// Windows 2022-11-25). On such systems, the uid is used to determine which
|
||||
// user's state should be used. The current user is maintained by active
|
||||
// connections open to the backend.
|
||||
//
|
||||
// When the backend initially starts it will typically start with no user. Then,
|
||||
// the first connection to the backend from the GUI frontend will set the
|
||||
// current user. Once set, the current user cannot be changed until all previous
|
||||
// connections are closed. The user is also used to determine which
|
||||
// LoginProfiles are accessible.
|
||||
//
|
||||
// In unattended mode, the backend will start with the user which enabled
|
||||
// unattended mode. The user must disable unattended mode before the user can be
|
||||
// changed.
|
||||
//
|
||||
// On non-multi-user systems, the uid should be set to empty string.
|
||||
func (b *LocalBackend) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
b.mu.Lock()
|
||||
if b.pm.CurrentUser() == uid {
|
||||
if b.pm.CurrentUserID() == uid {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if err := b.pm.SetCurrentUser(uid); err != nil {
|
||||
if err := b.pm.SetCurrentUserID(uid); err != nil {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -2109,9 +2381,15 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
// Keep this one just for testing.
|
||||
errs = append(errs, errors.New("bad hostname [test]"))
|
||||
}
|
||||
if err := b.checkProfileNameLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := b.checkSSHPrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := b.checkExitNodePrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
@@ -2136,7 +2414,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
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")
|
||||
}
|
||||
case "freebsd":
|
||||
case "freebsd", "openbsd":
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
@@ -2191,6 +2469,13 @@ func (b *LocalBackend) isDefaultServerLocked() bool {
|
||||
return prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error {
|
||||
if (p.ExitNodeIP.IsValid() || p.ExitNodeID != "") && p.AdvertisesExitNode() {
|
||||
return errors.New("Cannot advertise an exit node and use an exit node at the same time.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
b.mu.Lock()
|
||||
if mp.EggSet {
|
||||
@@ -2226,6 +2511,23 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
return stripKeysFromPrefs(newPrefs), nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error {
|
||||
if p.ProfileName == "" {
|
||||
// It is always okay to clear the profile name.
|
||||
return nil
|
||||
}
|
||||
id := b.pm.ProfileIDForName(p.ProfileName)
|
||||
if id == "" {
|
||||
// No profile with that name exists. That's fine.
|
||||
return nil
|
||||
}
|
||||
if id != b.pm.CurrentProfile().ID {
|
||||
// Name is already in use by another profile.
|
||||
return fmt.Errorf("profile name %q already in use", p.ProfileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPrefs saves new user preferences and propagates them throughout
|
||||
// the system. Implements Backend.
|
||||
func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
@@ -2259,14 +2561,13 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||
|
||||
oldp := b.pm.CurrentPrefs()
|
||||
if oldp.Valid() {
|
||||
newp.Persist = oldp.Persist().Clone() // caller isn't allowed to override this
|
||||
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
|
||||
}
|
||||
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
findExitNodeIDLocked(newp, netMap)
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
b.inServerMode = newp.ForceDaemon
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
@@ -2561,8 +2862,8 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
}
|
||||
|
||||
// selfV6Only is whether we only have IPv6 addresses ourselves.
|
||||
selfV6Only := tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
|
||||
!tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
|
||||
selfV6Only := slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
|
||||
!slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
|
||||
dcfg.OnlyIPv6 = selfV6Only
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
@@ -2578,7 +2879,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
have4 := tsaddr.PrefixesContainsFunc(addrs, tsaddr.PrefixIs4)
|
||||
have4 := slices.ContainsFunc(addrs, tsaddr.PrefixIs4)
|
||||
var ips []netip.Addr
|
||||
for _, addr := range addrs {
|
||||
if selfV6Only {
|
||||
@@ -2686,10 +2987,12 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
case len(dcfg.DefaultResolvers) != 0:
|
||||
// Default resolvers already set.
|
||||
case !prefs.ExitNodeID().IsZero():
|
||||
// When using exit nodes, it's very likely the LAN
|
||||
// resolvers will become unreachable. So, force use of the
|
||||
// fallback resolvers until we implement DNS forwarding to
|
||||
// exit nodes.
|
||||
// When using an exit node, we send all DNS traffic to the exit node, so
|
||||
// we don't need a fallback resolver.
|
||||
//
|
||||
// However, if the exit node is too old to run a DoH DNS proxy, then we
|
||||
// need to use a fallback resolver as it's very likely the LAN resolvers
|
||||
// will become unreachable.
|
||||
//
|
||||
// This is especially important on Apple OSes, where
|
||||
// adding the default route to the tunnel interface makes
|
||||
@@ -2713,6 +3016,25 @@ func (b *LocalBackend) SetVarRoot(dir string) {
|
||||
b.varRoot = dir
|
||||
}
|
||||
|
||||
// SetLogFlusher sets a func to be called to flush log uploads.
|
||||
//
|
||||
// It should only be called before the LocalBackend is used.
|
||||
func (b *LocalBackend) SetLogFlusher(flushFunc func()) {
|
||||
b.logFlushFunc = flushFunc
|
||||
}
|
||||
|
||||
// TryFlushLogs calls the log flush function. It returns false if a log flush
|
||||
// function was never initialized with SetLogFlusher.
|
||||
//
|
||||
// TryFlushLogs should not block.
|
||||
func (b *LocalBackend) TryFlushLogs() bool {
|
||||
if b.logFlushFunc == nil {
|
||||
return false
|
||||
}
|
||||
b.logFlushFunc()
|
||||
return true
|
||||
}
|
||||
|
||||
// TailscaleVarRoot returns the root directory of Tailscale's writable
|
||||
// storage area. (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
@@ -2990,7 +3312,7 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
|
||||
}
|
||||
}
|
||||
|
||||
if tsaddr.PrefixesContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
|
||||
if slices.ContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
|
||||
rs.Routes = append(rs.Routes, netip.PrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
}
|
||||
|
||||
@@ -3115,7 +3437,7 @@ func (b *LocalBackend) hasNodeKey() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
p := b.pm.CurrentPrefs()
|
||||
return p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero()
|
||||
return p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero()
|
||||
}
|
||||
|
||||
// nextState returns the state the backend seems to be in, based on
|
||||
@@ -3491,6 +3813,9 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
return true
|
||||
})
|
||||
handlePorts = append(handlePorts, servePorts...)
|
||||
|
||||
b.setServeProxyHandlersLocked()
|
||||
|
||||
// don't listen on netmap addresses if we're in userspace mode
|
||||
if !wgengine.IsNetstack(b.e) {
|
||||
b.updateServeTCPPortNetMapAddrListenersLocked(servePorts)
|
||||
@@ -3506,6 +3831,49 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
b.setTCPPortsIntercepted(handlePorts)
|
||||
}
|
||||
|
||||
// setServeProxyHandlersLocked ensures there is an http proxy handler for each
|
||||
// backend specified in serveConfig. It expects serveConfig to be valid and
|
||||
// up-to-date, so should be called after reloadServeConfigLocked.
|
||||
func (b *LocalBackend) setServeProxyHandlersLocked() {
|
||||
if !b.serveConfig.Valid() {
|
||||
return
|
||||
}
|
||||
var backends map[string]bool
|
||||
b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
|
||||
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
|
||||
backend := h.Proxy()
|
||||
mak.Set(&backends, backend, true)
|
||||
if _, ok := b.serveProxyHandlers.Load(backend); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
b.logf("serve: creating a new proxy handler for %s", backend)
|
||||
p, err := b.proxyHandlerForBackend(backend)
|
||||
if err != nil {
|
||||
// The backend endpoint (h.Proxy) should have been validated by expandProxyTarget
|
||||
// in the CLI, so just log the error here.
|
||||
b.logf("[unexpected] could not create proxy for %v: %s", backend, err)
|
||||
return true
|
||||
}
|
||||
b.serveProxyHandlers.Store(backend, p)
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
// Clean up handlers for proxy backends that are no longer present
|
||||
// in configuration.
|
||||
b.serveProxyHandlers.Range(func(key, value any) bool {
|
||||
backend := key.(string)
|
||||
if !backends[backend] {
|
||||
b.logf("serve: closing idle connections to %s", backend)
|
||||
value.(*httputil.ReverseProxy).Transport.(*http.Transport).CloseIdleConnections()
|
||||
b.serveProxyHandlers.Delete(backend)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
@@ -3551,6 +3919,18 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK
|
||||
return mk, nk
|
||||
}
|
||||
|
||||
func (b *LocalBackend) removeFileWaiter(handle set.Handle) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
delete(b.fileWaiters, handle)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) addFileWaiter(wakeWaiter context.CancelFunc) set.Handle {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.fileWaiters.Add(wakeWaiter)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
@@ -3558,6 +3938,41 @@ func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
return apiSrv.WaitingFiles()
|
||||
}
|
||||
|
||||
// AwaitWaitingFiles is like WaitingFiles but blocks while ctx is not done,
|
||||
// waiting for any files to be available.
|
||||
//
|
||||
// On return, exactly one of the results will be non-empty or non-nil,
|
||||
// respectively.
|
||||
func (b *LocalBackend) AwaitWaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
|
||||
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
|
||||
for {
|
||||
gotFile, gotFileCancel := context.WithCancel(context.Background())
|
||||
defer gotFileCancel()
|
||||
|
||||
handle := b.addFileWaiter(gotFileCancel)
|
||||
defer b.removeFileWaiter(handle)
|
||||
|
||||
// Now that we've registered ourselves, check again, in case
|
||||
// of race. Otherwise there's a small window where we could
|
||||
// miss a file arrival and wait forever.
|
||||
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-gotFile.Done():
|
||||
if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 {
|
||||
return ff, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DeleteFile(name string) error {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
@@ -3656,8 +4071,8 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
|
||||
b.mu.Lock()
|
||||
cc := b.ccAuto
|
||||
if prefs := b.pm.CurrentPrefs(); prefs.Valid() {
|
||||
req.NodeKey = prefs.Persist().PrivateNodeKey.Public()
|
||||
if prefs := b.pm.CurrentPrefs(); prefs.Valid() && prefs.Persist().Valid() {
|
||||
req.NodeKey = prefs.Persist().PrivateNodeKey().Public()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if cc == nil {
|
||||
@@ -4163,6 +4578,7 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
b.enterStateLockedOnEntry(ipn.NoState) // Reset state.
|
||||
health.SetLocalLogConfigHealth(nil)
|
||||
return b.Start(ipn.Options{})
|
||||
}
|
||||
|
||||
@@ -4205,11 +4621,3 @@ func (b *LocalBackend) ListProfiles() []ipn.LoginProfile {
|
||||
defer b.mu.Unlock()
|
||||
return b.pm.Profiles()
|
||||
}
|
||||
|
||||
// CurrentUser returns the current server mode user ID. It is only non-empty on
|
||||
// Windows where we have a multi-user system.
|
||||
func (b *LocalBackend) CurrentUser() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.pm.CurrentUser()
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -743,3 +745,78 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStatusWithoutPeers(t *testing.T) {
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
store := new(testStateStorage)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", store, "", nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
|
||||
t.Logf("ccGen: new mockControl.")
|
||||
cc.called("New")
|
||||
return cc, nil
|
||||
})
|
||||
b.Start(ipn.Options{})
|
||||
b.Login(nil)
|
||||
cc.send(nil, "", false, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
Addresses: ipps("100.101.101.101"),
|
||||
SelfNode: &tailcfg.Node{
|
||||
Addresses: ipps("100.101.101.101"),
|
||||
},
|
||||
})
|
||||
got := b.StatusWithoutPeers()
|
||||
if got.TailscaleIPs == nil {
|
||||
t.Errorf("got nil, expected TailscaleIPs value to not be nil")
|
||||
}
|
||||
if !reflect.DeepEqual(got.TailscaleIPs, got.Self.TailscaleIPs) {
|
||||
t.Errorf("got %v, expected %v", got.TailscaleIPs, got.Self.TailscaleIPs)
|
||||
}
|
||||
}
|
||||
|
||||
// legacyBackend was the interface between Tailscale frontends
|
||||
// (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale
|
||||
// backend (e.g. cmd/tailscaled) running on the same machine.
|
||||
// (It has nothing to do with the interface between the backends
|
||||
// and the cloud control plane.)
|
||||
type legacyBackend interface {
|
||||
// SetNotifyCallback sets the callback to be called on updates
|
||||
// from the backend to the client.
|
||||
SetNotifyCallback(func(ipn.Notify))
|
||||
// Start starts or restarts the backend, typically when a
|
||||
// frontend client connects.
|
||||
Start(ipn.Options) error
|
||||
// StartLoginInteractive requests to start a new interactive login
|
||||
// flow. This should trigger a new BrowseToURL notification
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *tailcfg.Oauth2Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
// SetPrefs installs a new set of user preferences, including
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*ipn.Prefs)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
// polling.
|
||||
RequestEngineStatus()
|
||||
}
|
||||
|
||||
// Verify that LocalBackend still implements the legacyBackend interface
|
||||
// for now, at least until the macOS and iOS clients move off of it.
|
||||
var _ legacyBackend = (*LocalBackend)(nil)
|
||||
|
||||
@@ -7,22 +7,28 @@ package ipnlocal
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
@@ -38,6 +44,14 @@ type tkaState struct {
|
||||
profile ipn.ProfileID
|
||||
authority *tka.Authority
|
||||
storage *tka.FS
|
||||
filtered []ipnstate.TKAFilteredPeer
|
||||
}
|
||||
|
||||
// permitTKAInitLocked returns true if tailnet lock initialization may
|
||||
// occur.
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) permitTKAInitLocked() bool {
|
||||
return envknob.UseWIPCode() || b.capTailnetLock
|
||||
}
|
||||
|
||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||
@@ -45,17 +59,20 @@ type tkaState struct {
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
if !envknob.UseWIPCode() {
|
||||
return // Feature-flag till network-lock is in Alpha.
|
||||
// TODO(tom): Remove this guard for 1.35 and later.
|
||||
if b.tka == nil && !b.permitTKAInitLocked() {
|
||||
health.SetTKAHealth(nil)
|
||||
return
|
||||
}
|
||||
if b.tka == nil {
|
||||
health.SetTKAHealth(nil)
|
||||
return // TKA not enabled.
|
||||
}
|
||||
|
||||
var toDelete map[int]bool // peer index => true
|
||||
for i, p := range nm.Peers {
|
||||
if p.UnsignedPeerAPIOnly {
|
||||
// Not subject to TKA.
|
||||
// Not subject to tailnet lock.
|
||||
continue
|
||||
}
|
||||
if len(p.KeySignature) == 0 {
|
||||
@@ -72,12 +89,37 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
// nm.Peers is ordered, so deletion must be order-preserving.
|
||||
if len(toDelete) > 0 {
|
||||
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
|
||||
filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete))
|
||||
for i, p := range nm.Peers {
|
||||
if !toDelete[i] {
|
||||
peers = append(peers, p)
|
||||
} else {
|
||||
// Record information about the node we filtered out.
|
||||
fp := ipnstate.TKAFilteredPeer{
|
||||
Name: p.Name,
|
||||
ID: p.ID,
|
||||
StableID: p.StableID,
|
||||
TailscaleIPs: make([]netip.Addr, len(p.Addresses)),
|
||||
}
|
||||
for i, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
|
||||
fp.TailscaleIPs[i] = addr.Addr()
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, fp)
|
||||
}
|
||||
}
|
||||
nm.Peers = peers
|
||||
b.tka.filtered = filtered
|
||||
} else {
|
||||
b.tka.filtered = nil
|
||||
}
|
||||
|
||||
// Check that we ourselves are not locked out, report a health issue if so.
|
||||
if nm.SelfNode != nil && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key, nm.SelfNode.KeySignature) != nil {
|
||||
health.SetTKAHealth(errors.New("this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"))
|
||||
} else {
|
||||
health.SetTKAHealth(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,22 +141,23 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
// tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
|
||||
// and may take b.mu as required.
|
||||
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
|
||||
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
|
||||
|
||||
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
|
||||
defer b.tkaSyncLock.Unlock()
|
||||
b.mu.Lock() // take mu to protect access to synchronized fields.
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// TODO(tom): Remove this guard for 1.35 and later.
|
||||
if b.tka == nil && !b.permitTKAInitLocked() {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
|
||||
|
||||
ourNodeKey := prefs.Persist().PublicNodeKey()
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
wantEnabled := nm.TKAEnabled
|
||||
didJustEnable := false
|
||||
if isEnabled != wantEnabled {
|
||||
var ourHead tka.AUMHash
|
||||
if b.tka != nil {
|
||||
@@ -131,10 +174,11 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
|
||||
}
|
||||
|
||||
if wantEnabled && !isEnabled {
|
||||
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
|
||||
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM, prefs.Persist()); err != nil {
|
||||
return fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
isEnabled = true
|
||||
didJustEnable = true
|
||||
} else if !wantEnabled && isEnabled {
|
||||
if err := b.tkaApplyDisablementLocked(bs.DisablementSecret); err != nil {
|
||||
// We log here instead of returning an error (which itself would be
|
||||
@@ -143,13 +187,17 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
|
||||
b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
|
||||
} else {
|
||||
isEnabled = false
|
||||
health.SetTKAHealth(nil)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
||||
}
|
||||
}
|
||||
|
||||
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
|
||||
// We always transmit the sync RPCs if TKA was just enabled.
|
||||
// This informs the control plane that our TKA state is now
|
||||
// initialized to the transmitted TKA head hash.
|
||||
if isEnabled && (b.tka.authority.Head() != nm.TKAHead || didJustEnable) {
|
||||
if err := b.tkaSyncLocked(ourNodeKey); err != nil {
|
||||
return fmt.Errorf("tka sync: %w", err)
|
||||
}
|
||||
@@ -263,6 +311,8 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
|
||||
|
||||
// chonkPathLocked returns the absolute path to the directory in which TKA
|
||||
// state (the 'tailchonk') is stored.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) chonkPathLocked() string {
|
||||
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
|
||||
}
|
||||
@@ -271,7 +321,7 @@ func (b *LocalBackend) chonkPathLocked() string {
|
||||
// tailnet key authority, based on the given genesis AUM.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
|
||||
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, persist persist.PersistView) error {
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -281,6 +331,20 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
|
||||
return fmt.Errorf("reading genesis: %v", err)
|
||||
}
|
||||
|
||||
if persist.Valid() && persist.DisallowedTKAStateIDs().Len() > 0 {
|
||||
if genesis.State == nil {
|
||||
return errors.New("invalid genesis: missing State")
|
||||
}
|
||||
bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
|
||||
|
||||
for i := 0; i < persist.DisallowedTKAStateIDs().Len(); i++ {
|
||||
stateID := persist.DisallowedTKAStateIDs().At(i)
|
||||
if stateID == bootstrapStateID {
|
||||
return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chonkDir := b.chonkPathLocked()
|
||||
if err := os.Mkdir(filepath.Dir(chonkDir), 0755); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating chonk root dir: %v", err)
|
||||
@@ -309,10 +373,6 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
|
||||
// CanSupportNetworkLock returns nil if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() error {
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("this feature is not yet complete, a later release may support this functionality")
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
// If the TKA is being used, it is supported.
|
||||
return nil
|
||||
@@ -338,10 +398,10 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
nodeKey *key.NodePublic
|
||||
nlPriv key.NLPrivate
|
||||
)
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
nkp := p.Persist().PublicNodeKey()
|
||||
nodeKey = &nkp
|
||||
nlPriv = p.Persist().NetworkLockKey
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
|
||||
if nlPriv.IsZero() {
|
||||
@@ -377,6 +437,11 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]*ipnstate.TKAFilteredPeer, len(b.tka.filtered))
|
||||
for i := 0; i < len(filtered); i++ {
|
||||
filtered[i] = b.tka.filtered[i].Clone()
|
||||
}
|
||||
|
||||
return &ipnstate.NetworkLockStatus{
|
||||
Enabled: true,
|
||||
Head: &head,
|
||||
@@ -384,6 +449,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
NodeKey: nodeKey,
|
||||
NodeKeySigned: selfAuthorized,
|
||||
TrustedKeys: outKeys,
|
||||
FilteredPeers: filtered,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +462,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
// needing signatures is returned as a response.
|
||||
// The Finish RPC submits signatures for all these nodes, at which point
|
||||
// Control has everything it needs to atomically enable network lock.
|
||||
func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte) error {
|
||||
func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) error {
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -404,15 +470,27 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
|
||||
var ourNodeKey key.NodePublic
|
||||
var nlPriv key.NLPrivate
|
||||
b.mu.Lock()
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
|
||||
|
||||
// TODO(tom): Remove this guard for 1.35 and later.
|
||||
if !b.permitTKAInitLocked() {
|
||||
b.mu.Unlock()
|
||||
return errors.New("this feature is not yet complete, a later release may support this functionality")
|
||||
}
|
||||
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
ourNodeKey = p.Persist().PublicNodeKey()
|
||||
nlPriv = p.Persist().NetworkLockKey
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if ourNodeKey.IsZero() || nlPriv.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
}
|
||||
|
||||
var entropy [16]byte
|
||||
if _, err := rand.Read(entropy[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generates a genesis AUM representing trust in the provided keys.
|
||||
// We use an in-memory tailchonk because we don't want to commit to
|
||||
// the filesystem until we've finished the initialization sequence,
|
||||
@@ -424,6 +502,9 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
|
||||
// - DisablementSecret: value needed to disable.
|
||||
// - DisablementValue: the KDF of the disablement secret, a public value.
|
||||
DisablementSecrets: disablementValues,
|
||||
|
||||
StateID1: binary.LittleEndian.Uint64(entropy[:8]),
|
||||
StateID2: binary.LittleEndian.Uint64(entropy[8:]),
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka.Create: %v", err)
|
||||
@@ -431,7 +512,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
|
||||
|
||||
b.logf("Generated genesis AUM to initialize network lock, trusting the following keys:")
|
||||
for i, k := range genesisAUM.State.Keys {
|
||||
b.logf(" - key[%d] = nlpub:%x with %d votes", i, k.Public, k.Votes)
|
||||
b.logf(" - key[%d] = tlpub:%x with %d votes", i, k.Public, k.Votes)
|
||||
}
|
||||
|
||||
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||
@@ -456,7 +537,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
|
||||
}
|
||||
|
||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||
_, err = b.tkaInitFinish(ourNodeKey, sigs)
|
||||
_, err = b.tkaInitFinish(ourNodeKey, sigs, supportDisablement)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -480,6 +561,31 @@ func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
|
||||
return b.tka.authority.KeyTrusted(keyID)
|
||||
}
|
||||
|
||||
// NetworkLockForceLocalDisable shuts down TKA locally, and denylists the current
|
||||
// TKA from being initialized locally in future.
|
||||
func (b *LocalBackend) NetworkLockForceLocalDisable() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return errNetworkLockNotActive
|
||||
}
|
||||
|
||||
id1, id2 := b.tka.authority.StateIDs()
|
||||
stateID := fmt.Sprintf("%d:%d", id1, id2)
|
||||
|
||||
newPrefs := b.pm.CurrentPrefs().AsStruct().Clone() // .Persist should always be initialized here.
|
||||
newPrefs.Persist.DisallowedTKAStateIDs = append(newPrefs.Persist.DisallowedTKAStateIDs, stateID)
|
||||
if err := b.pm.SetPrefs(newPrefs.View()); err != nil {
|
||||
return fmt.Errorf("saving prefs: %w", err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(b.chonkPathLocked()); err != nil {
|
||||
return fmt.Errorf("deleting TKA state: %w", err)
|
||||
}
|
||||
b.tka = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockSign signs the given node-key and submits it to the control plane.
|
||||
// rotationPublic, if specified, must be an ed25519 public key.
|
||||
func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []byte) error {
|
||||
@@ -488,8 +594,8 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var nlPriv key.NLPrivate
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil {
|
||||
nlPriv = p.Persist().NetworkLockKey
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
if nlPriv.IsZero() {
|
||||
return key.NodePublic{}, tka.NodeKeySignature{}, errMissingNetmap
|
||||
@@ -542,19 +648,16 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var ourNodeKey key.NodePublic
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
ourNodeKey = p.Persist().PublicNodeKey()
|
||||
}
|
||||
if ourNodeKey.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
}
|
||||
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
var nlPriv key.NLPrivate
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil {
|
||||
nlPriv = p.Persist().NetworkLockKey
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
if nlPriv.IsZero() {
|
||||
return errMissingNetmap
|
||||
@@ -562,6 +665,9 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||
if b.tka == nil {
|
||||
return errNetworkLockNotActive
|
||||
}
|
||||
if !b.tka.authority.KeyTrusted(nlPriv.KeyID()) {
|
||||
return errors.New("this node does not have a trusted tailnet lock key")
|
||||
}
|
||||
|
||||
updater := b.tka.authority.NewUpdater(nlPriv)
|
||||
|
||||
@@ -571,7 +677,11 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||
}
|
||||
}
|
||||
for _, removeKey := range removeKeys {
|
||||
if err := updater.RemoveKey(removeKey.ID()); err != nil {
|
||||
keyID, err := removeKey.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updater.RemoveKey(keyID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -608,10 +718,6 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||
|
||||
// NetworkLockDisable disables network-lock using the provided disablement secret.
|
||||
func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
ourNodeKey key.NodePublic
|
||||
head tka.AUMHash
|
||||
@@ -619,7 +725,7 @@ func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
|
||||
)
|
||||
|
||||
b.mu.Lock()
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
ourNodeKey = p.Persist().PublicNodeKey()
|
||||
}
|
||||
if b.tka == nil {
|
||||
@@ -642,6 +748,43 @@ func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// NetworkLockLog returns the changelog of TKA state up to maxEntries in size.
|
||||
func (b *LocalBackend) NetworkLockLog(maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.tka == nil {
|
||||
return nil, errNetworkLockNotActive
|
||||
}
|
||||
|
||||
var out []ipnstate.NetworkLockUpdate
|
||||
cursor := b.tka.authority.Head()
|
||||
for i := 0; i < maxEntries; i++ {
|
||||
aum, err := b.tka.storage.AUM(cursor)
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
break
|
||||
}
|
||||
return out, fmt.Errorf("reading AUM: %w", err)
|
||||
}
|
||||
|
||||
update := ipnstate.NetworkLockUpdate{
|
||||
Hash: cursor,
|
||||
Change: aum.MessageKind.String(),
|
||||
Raw: aum.Serialize(),
|
||||
}
|
||||
out = append(out, update)
|
||||
|
||||
parent, hasParent := aum.Parent()
|
||||
if !hasParent {
|
||||
break
|
||||
}
|
||||
cursor = parent
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||
if err != nil {
|
||||
@@ -696,12 +839,13 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature, supportDisablement []byte) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
Signatures: nks,
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
Signatures: nks,
|
||||
SupportDisablement: supportDisablement,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
@@ -108,8 +108,30 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
case "/machine/tka/sync/offer", "/machine/tka/sync/send":
|
||||
t.Error("node attempted to sync, but should have been up to date")
|
||||
// Sync offer/send endpoints are hit even though the node is up-to-date,
|
||||
// so we implement enough of a fake that the client doesn't explode.
|
||||
case "/machine/tka/sync/offer":
|
||||
head, err := a1.Head().MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncOfferResponse{
|
||||
Head: string(head),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "/machine/tka/sync/send":
|
||||
head, err := a1.Head().MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{
|
||||
Head: string(head),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
@@ -299,7 +321,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "control has an update",
|
||||
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.RemoveKey(someKey.ID()); err != nil {
|
||||
if err := b.RemoveKey(someKey.MustID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -314,7 +336,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "node has an update",
|
||||
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.RemoveKey(someKey.ID()); err != nil {
|
||||
if err := b.RemoveKey(someKey.MustID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -329,7 +351,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "node and control diverge",
|
||||
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swiggity"}); err != nil {
|
||||
if err := b.SetKeyMeta(someKey.MustID(), map[string]string{"ye": "swiggity"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -340,7 +362,7 @@ func TestTKASync(t *testing.T) {
|
||||
},
|
||||
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swooty"}); err != nil {
|
||||
if err := b.SetKeyMeta(someKey.MustID(), map[string]string{"ye": "swooty"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -756,3 +778,103 @@ func TestTKASign(t *testing.T) {
|
||||
t.Errorf("NetworkLockSign() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKAForceDisable(t *testing.T) {
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, to seed local state.
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
NetworkLockKey: nlPriv,
|
||||
},
|
||||
}).View()))
|
||||
|
||||
temp := t.TempDir()
|
||||
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
|
||||
os.Mkdir(tkaPath, 0755)
|
||||
chonk, err := tka.ChonkDir(tkaPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authority, genesis, err := tka.Create(chonk, tka.State{
|
||||
Keys: []tka.Key{key},
|
||||
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/bootstrap":
|
||||
body := new(tailcfg.TKABootstrapRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
GenesisAUM: genesis.Serialize(),
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
}
|
||||
|
||||
if err := b.NetworkLockForceLocalDisable(); err != nil {
|
||||
t.Fatalf("NetworkLockForceLocalDisable() failed: %v", err)
|
||||
}
|
||||
if b.tka != nil {
|
||||
t.Fatal("tka was not shut down")
|
||||
}
|
||||
if _, err := os.Stat(b.chonkPathLocked()); err == nil || !os.IsNotExist(err) {
|
||||
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
|
||||
}
|
||||
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: authority.Head(),
|
||||
}, pm.CurrentPrefs())
|
||||
if err != nil && err.Error() != "bootstrap: TKA with stateID of \"0:0\" is disallowed on this node" {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
t.Fatal("tka was re-initalized")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/adler32"
|
||||
"hash/crc32"
|
||||
"html"
|
||||
"io"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
@@ -46,6 +48,7 @@ import (
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -93,10 +96,6 @@ const (
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func (s *peerAPIServer) canReceiveFiles() bool {
|
||||
return s != nil && s.rootDir != ""
|
||||
}
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
@@ -166,13 +165,13 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
if name, ok := strs.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
|
||||
// After we're done looping over files, then try
|
||||
// to delete this file. Don't do it proactively,
|
||||
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
|
||||
// and we don't want to delete the ".deleted" file before
|
||||
// enumerating to the "foo.jpg" file.
|
||||
defer tryDeleteAgain(filepath.Join(s.rootDir, strings.TrimSuffix(name, deletedSuffix)))
|
||||
defer tryDeleteAgain(filepath.Join(s.rootDir, name))
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
@@ -225,11 +224,11 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
if name, ok := strs.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
|
||||
if deleted == nil {
|
||||
deleted = map[string]bool{}
|
||||
}
|
||||
deleted[strings.TrimSuffix(name, deletedSuffix)] = true
|
||||
deleted[name] = true
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
@@ -344,11 +343,68 @@ func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
// accidentally logging actual filenames anywhere.
|
||||
const redacted = "redacted"
|
||||
|
||||
func redactErr(err error) error {
|
||||
if pe, ok := err.(*os.PathError); ok {
|
||||
pe.Path = redacted
|
||||
type redactedErr struct {
|
||||
msg string
|
||||
inner error
|
||||
}
|
||||
|
||||
func (re *redactedErr) Error() string {
|
||||
return re.msg
|
||||
}
|
||||
|
||||
func (re *redactedErr) Unwrap() error {
|
||||
return re.inner
|
||||
}
|
||||
|
||||
func redactString(s string) string {
|
||||
hash := adler32.Checksum([]byte(s))
|
||||
|
||||
var buf [len(redacted) + len(".12345678")]byte
|
||||
b := append(buf[:0], []byte(redacted)...)
|
||||
b = append(b, '.')
|
||||
b = strconv.AppendUint(b, uint64(hash), 16)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func redactErr(root error) error {
|
||||
// redactStrings is a list of sensitive strings that were redacted.
|
||||
// It is not sufficient to just snub out sensitive fields in Go errors
|
||||
// since some wrapper errors like fmt.Errorf pre-cache the error string,
|
||||
// which would unfortunately remain unaffected.
|
||||
var redactStrings []string
|
||||
|
||||
// Redact sensitive fields in known Go error types.
|
||||
var unknownErrors int
|
||||
multierr.Range(root, func(err error) bool {
|
||||
switch err := err.(type) {
|
||||
case *os.PathError:
|
||||
redactStrings = append(redactStrings, err.Path)
|
||||
err.Path = redactString(err.Path)
|
||||
case *os.LinkError:
|
||||
redactStrings = append(redactStrings, err.New, err.Old)
|
||||
err.New = redactString(err.New)
|
||||
err.Old = redactString(err.Old)
|
||||
default:
|
||||
unknownErrors++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// If there are no redacted strings or no unknown error types,
|
||||
// then we can return the possibly modified root error verbatim.
|
||||
// Otherwise, we must replace redacted strings from any wrappers.
|
||||
if len(redactStrings) == 0 || unknownErrors == 0 {
|
||||
return root
|
||||
}
|
||||
return err
|
||||
|
||||
// Stringify and replace any paths that we found above, then return
|
||||
// the error wrapped in a type that uses the newly-redacted string
|
||||
// while also allowing Unwrap()-ing to the inner error type(s).
|
||||
s := root.Error()
|
||||
for _, toRedact := range redactStrings {
|
||||
s = strings.ReplaceAll(s, toRedact, redactString(toRedact))
|
||||
}
|
||||
return &redactedErr{msg: s, inner: root}
|
||||
}
|
||||
|
||||
func touchFile(path string) error {
|
||||
@@ -533,7 +589,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(netutil.NewOneConnListener(c, pln.ln.Addr()))
|
||||
go httpServer.Serve(netutil.NewOneConnListener(c, nil))
|
||||
}
|
||||
|
||||
// peerAPIHandler serves the PeerAPI for a source specific client.
|
||||
@@ -575,17 +631,58 @@ func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
|
||||
return h.validateHost(r)
|
||||
}
|
||||
|
||||
// peerAPIRequestShouldGetSecurityHeaders reports whether the PeerAPI request r
|
||||
// should get security response headers. It aims to report true for any request
|
||||
// from a browser and false for requests from tailscaled (Go) clients.
|
||||
//
|
||||
// PeerAPI is primarily an RPC mechanism between Tailscale instances. Some of
|
||||
// the HTTP handlers are useful for debugging with curl or browsers, but in
|
||||
// general the client is always tailscaled itself. Because PeerAPI only uses
|
||||
// HTTP/1 without HTTP/2 and its HPACK helping with repetitive headers, we try
|
||||
// to minimize header bytes sent in the common case when the client isn't a
|
||||
// browser. Minimizing bytes is important in particular with the ExitDNS service
|
||||
// provided by exit nodes, processing DNS clients from queries. We don't want to
|
||||
// waste bytes with security headers to non-browser clients. But if there's any
|
||||
// hint that the request is from a browser, then we do.
|
||||
func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
|
||||
// Accept-Encoding is a forbidden header
|
||||
// (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)
|
||||
// that Chrome, Firefox, Safari, etc send, but Go does not. So if we see it,
|
||||
// it's probably a browser and not a Tailscale PeerAPI (Go) client.
|
||||
if httpguts.HeaderValuesContainsToken(r.Header["Accept-Encoding"], "deflate") {
|
||||
return true
|
||||
}
|
||||
// Clients can mess with their User-Agent, but if they say Mozilla or have a bunch
|
||||
// of components (spaces) they're likely a browser.
|
||||
if ua := r.Header.Get("User-Agent"); strings.HasPrefix(ua, "Mozilla/") || strings.Count(ua, " ") > 2 {
|
||||
return true
|
||||
}
|
||||
// Tailscale/PeerAPI/Go clients don't have an Accept-Language.
|
||||
if r.Header.Get("Accept-Language") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.validatePeerAPIRequest(r); err != nil {
|
||||
metricInvalidRequests.Add(1)
|
||||
h.logf("invalid request from %v: %v", h.remoteAddr, err)
|
||||
http.Error(w, "invalid peerapi request", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if peerAPIRequestShouldGetSecurityHeaders(r) {
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
metricPutCalls.Add(1)
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/dns-query") {
|
||||
metricDNSCalls.Add(1)
|
||||
h.handleDNSQuery(w, r)
|
||||
return
|
||||
}
|
||||
@@ -606,12 +703,14 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
case "/v0/wol":
|
||||
metricWakeOnLANCalls.Add(1)
|
||||
h.handleWakeOnLAN(w, r)
|
||||
return
|
||||
case "/v0/interfaces":
|
||||
h.handleServeInterfaces(w, r)
|
||||
return
|
||||
case "/v0/ingress":
|
||||
metricIngressCalls.Add(1)
|
||||
h.handleServeIngress(w, r)
|
||||
return
|
||||
}
|
||||
@@ -690,18 +789,20 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
i, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
dr, err := interfaces.DefaultRoute()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintln(w, "<h1>Interfaces</h1>")
|
||||
fmt.Fprintf(w, "<h3>Default route is %q(%d)</h3>\n", dr.InterfaceName, dr.InterfaceIndex)
|
||||
|
||||
if dr, err := interfaces.DefaultRoute(); err == nil {
|
||||
fmt.Fprintf(w, "<h3>Default route is %q(%d)</h3>\n", html.EscapeString(dr.InterfaceName), dr.InterfaceIndex)
|
||||
} else {
|
||||
fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
|
||||
}
|
||||
|
||||
i, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Could not get interfaces: %s\n", html.EscapeString(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "<table>")
|
||||
fmt.Fprint(w, "<tr>")
|
||||
@@ -712,7 +813,7 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
|
||||
i.ForeachInterface(func(iface interfaces.Interface, ipps []netip.Prefix) {
|
||||
fmt.Fprint(w, "<tr>")
|
||||
for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} {
|
||||
fmt.Fprintf(w, "<td>%v</td> ", v)
|
||||
fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(fmt.Sprintf("%v", v)))
|
||||
}
|
||||
fmt.Fprint(w, "</tr>\n")
|
||||
})
|
||||
@@ -779,6 +880,10 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
|
||||
// canPutFile reports whether h can put a file ("Taildrop") to this node.
|
||||
func (h *peerAPIHandler) canPutFile() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
// Unsigned peers can't send files.
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityFileSharingSend)
|
||||
}
|
||||
|
||||
@@ -789,6 +894,10 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
// This node does not expose debug info.
|
||||
return false
|
||||
}
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
// Unsigned peers can't debug.
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
|
||||
}
|
||||
|
||||
@@ -814,6 +923,10 @@ func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !envknob.CanTaildrop() {
|
||||
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.canPutFile() {
|
||||
http.Error(w, "Taildrop access denied", http.StatusForbidden)
|
||||
return
|
||||
@@ -1321,3 +1434,13 @@ func (fl *fakePeerAPIListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
|
||||
func (fl *fakePeerAPIListener) Addr() net.Addr { return fl.addr }
|
||||
|
||||
var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
|
||||
|
||||
// Non-debug PeerAPI endpoints.
|
||||
metricPutCalls = clientmetric.NewCounter("peerapi_put")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user