mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 15:23:45 +00:00
TO DO:
-check if Context.getExternalFilesDirs works as is for private dir
This commit is contained in:
commit
6d5c7b1191
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -66,7 +66,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@ -80,4 +80,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||||
|
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1
|
uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0
|
||||||
with:
|
with:
|
||||||
version: v1.64
|
version: v1.64
|
||||||
|
|
||||||
|
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
token: ${{ secrets.GOVULNCHECK_BOT_TOKEN }}
|
token: ${{ secrets.GOVULNCHECK_BOT_TOKEN }}
|
||||||
payload: |
|
payload: |
|
||||||
{
|
{
|
||||||
"channel": "C05PXRM304B",
|
"channel": "C08FGKZCQTW",
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
"type": "section",
|
"type": "section",
|
||||||
|
27
.github/workflows/natlab-integrationtest.yml
vendored
Normal file
27
.github/workflows/natlab-integrationtest.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Run some natlab integration tests.
|
||||||
|
# See https://github.com/tailscale/tailscale/issues/13038
|
||||||
|
name: "natlab-integrationtest"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "tstest/integration/nat/nat_test.go"
|
||||||
|
jobs:
|
||||||
|
natlab-integrationtest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: Install qemu
|
||||||
|
run: |
|
||||||
|
sudo rm /var/lib/man-db/auto-update
|
||||||
|
sudo apt-get -y update
|
||||||
|
sudo apt-get -y remove man-db
|
||||||
|
sudo apt-get install -y qemu-system-x86 qemu-utils
|
||||||
|
- name: Run natlab integration tests
|
||||||
|
run: |
|
||||||
|
./tool/go test -v -run=^TestEasyEasy$ -timeout=3m -count=1 ./tstest/integration/nat --run-vm-tests
|
38
.github/workflows/test.yml
vendored
38
.github/workflows/test.yml
vendored
@ -79,7 +79,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
||||||
with:
|
with:
|
||||||
# Note: unlike the other setups, this is only grabbing the mod download
|
# Note: unlike the other setups, this is only grabbing the mod download
|
||||||
# cache, rather than the whole mod directory, as the download cache
|
# cache, rather than the whole mod directory, as the download cache
|
||||||
@ -139,7 +139,11 @@ jobs:
|
|||||||
echo "Build/test created untracked files in the repo (file names above)."
|
echo "Build/test created untracked files in the repo (file names above)."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
- name: Tidy cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
||||||
|
find $(go env GOMODCACHE)/cache -type f -mmin +90 -delete
|
||||||
windows:
|
windows:
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
@ -153,7 +157,7 @@ jobs:
|
|||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
||||||
with:
|
with:
|
||||||
# Note: unlike the other setups, this is only grabbing the mod download
|
# Note: unlike the other setups, this is only grabbing the mod download
|
||||||
# cache, rather than the whole mod directory, as the download cache
|
# cache, rather than the whole mod directory, as the download cache
|
||||||
@ -176,6 +180,11 @@ jobs:
|
|||||||
# Somewhere in the layers (powershell?)
|
# Somewhere in the layers (powershell?)
|
||||||
# the equals signs cause great confusion.
|
# the equals signs cause great confusion.
|
||||||
run: go test ./... -bench . -benchtime 1x -run "^$"
|
run: go test ./... -bench . -benchtime 1x -run "^$"
|
||||||
|
- name: Tidy cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
||||||
|
find $(go env GOMODCACHE)/cache -type f -mmin +90 -delete
|
||||||
|
|
||||||
privileged:
|
privileged:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@ -254,7 +263,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
||||||
with:
|
with:
|
||||||
# Note: unlike the other setups, this is only grabbing the mod download
|
# Note: unlike the other setups, this is only grabbing the mod download
|
||||||
# cache, rather than the whole mod directory, as the download cache
|
# cache, rather than the whole mod directory, as the download cache
|
||||||
@ -283,6 +292,11 @@ jobs:
|
|||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
|
- name: Tidy cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
||||||
|
find $(go env GOMODCACHE)/cache -type f -mmin +90 -delete
|
||||||
|
|
||||||
ios: # similar to cross above, but iOS can't build most of the repo. So, just
|
ios: # similar to cross above, but iOS can't build most of the repo. So, just
|
||||||
#make it build a few smoke packages.
|
#make it build a few smoke packages.
|
||||||
@ -319,7 +333,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
||||||
with:
|
with:
|
||||||
# Note: unlike the other setups, this is only grabbing the mod download
|
# Note: unlike the other setups, this is only grabbing the mod download
|
||||||
# cache, rather than the whole mod directory, as the download cache
|
# cache, rather than the whole mod directory, as the download cache
|
||||||
@ -342,6 +356,11 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
|
- name: Tidy cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
||||||
|
find $(go env GOMODCACHE)/cache -type f -mmin +90 -delete
|
||||||
|
|
||||||
android:
|
android:
|
||||||
# similar to cross above, but android fails to build a few pieces of the
|
# similar to cross above, but android fails to build a few pieces of the
|
||||||
@ -367,7 +386,7 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
||||||
with:
|
with:
|
||||||
# Note: unlike the other setups, this is only grabbing the mod download
|
# Note: unlike the other setups, this is only grabbing the mod download
|
||||||
# cache, rather than the whole mod directory, as the download cache
|
# cache, rather than the whole mod directory, as the download cache
|
||||||
@ -394,6 +413,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./tool/go run ./cmd/tsconnect --fast-compression build
|
./tool/go run ./cmd/tsconnect --fast-compression build
|
||||||
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
|
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
|
||||||
|
- name: Tidy cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find $(go env GOCACHE) -type f -mmin +90 -delete
|
||||||
|
find $(go env GOMODCACHE)/cache -type f -mmin +90 -delete
|
||||||
|
|
||||||
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
|
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@ -461,7 +485,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "artifacts_path=$(realpath .)" >> $GITHUB_ENV
|
echo "artifacts_path=$(realpath .)" >> $GITHUB_ENV
|
||||||
- name: upload crash
|
- name: upload crash
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
if: steps.run.outcome != 'success' && steps.build.outcome == 'success'
|
if: steps.run.outcome != 'success' && steps.build.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
name: artifacts
|
||||||
|
2
.github/workflows/update-flake.yml
vendored
2
.github/workflows/update-flake.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Send pull request
|
- name: Send pull request
|
||||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f #v7.0.6
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
author: Flakes Updater <noreply+flakes-updater@tailscale.com>
|
author: Flakes Updater <noreply+flakes-updater@tailscale.com>
|
||||||
|
@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Send pull request
|
- name: Send pull request
|
||||||
id: pull-request
|
id: pull-request
|
||||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f #v7.0.6
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
author: OSS Updater <noreply+oss-updater@tailscale.com>
|
author: OSS Updater <noreply+oss-updater@tailscale.com>
|
||||||
|
@ -26,16 +26,11 @@ issues:
|
|||||||
|
|
||||||
# Per-linter settings are contained in this top-level key
|
# Per-linter settings are contained in this top-level key
|
||||||
linters-settings:
|
linters-settings:
|
||||||
# Enable all rules by default; we don't use invisible unicode runes.
|
|
||||||
bidichk:
|
|
||||||
|
|
||||||
gofmt:
|
gofmt:
|
||||||
rewrite-rules:
|
rewrite-rules:
|
||||||
- pattern: 'interface{}'
|
- pattern: 'interface{}'
|
||||||
replacement: 'any'
|
replacement: 'any'
|
||||||
|
|
||||||
goimports:
|
|
||||||
|
|
||||||
govet:
|
govet:
|
||||||
# Matches what we use in corp as of 2023-12-07
|
# Matches what we use in corp as of 2023-12-07
|
||||||
enable:
|
enable:
|
||||||
@ -78,8 +73,6 @@ linters-settings:
|
|||||||
# analyzer doesn't support type declarations
|
# analyzer doesn't support type declarations
|
||||||
#- github.com/tailscale/tailscale/types/logger.Logf
|
#- github.com/tailscale/tailscale/types/logger.Logf
|
||||||
|
|
||||||
misspell:
|
|
||||||
|
|
||||||
revive:
|
revive:
|
||||||
enable-all-rules: false
|
enable-all-rules: false
|
||||||
ignore-generated-header: true
|
ignore-generated-header: true
|
||||||
|
@ -79,6 +79,13 @@ type Device struct {
|
|||||||
// Tailscale have attempted to collect this from the device but it has not
|
// Tailscale have attempted to collect this from the device but it has not
|
||||||
// opted in, PostureIdentity will have Disabled=true.
|
// opted in, PostureIdentity will have Disabled=true.
|
||||||
PostureIdentity *DevicePostureIdentity `json:"postureIdentity"`
|
PostureIdentity *DevicePostureIdentity `json:"postureIdentity"`
|
||||||
|
|
||||||
|
// TailnetLockKey is the tailnet lock public key of the node as a hex string.
|
||||||
|
TailnetLockKey string `json:"tailnetLockKey,omitempty"`
|
||||||
|
|
||||||
|
// TailnetLockErr indicates an issue with the tailnet lock node-key signature
|
||||||
|
// on this device. This field is only populated when tailnet lock is enabled.
|
||||||
|
TailnetLockErr string `json:"tailnetLockError,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DevicePostureIdentity struct {
|
type DevicePostureIdentity struct {
|
||||||
|
@ -203,35 +203,9 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
}
|
}
|
||||||
s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
|
s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
|
||||||
|
|
||||||
var metric string // clientmetric to report on startup
|
var metric string
|
||||||
|
s.apiHandler, metric = s.modeAPIHandler(s.mode)
|
||||||
// Create handler for "/api" requests with CSRF protection.
|
s.apiHandler = s.withCSRF(s.apiHandler)
|
||||||
// We don't require secure cookies, since the web client is regularly used
|
|
||||||
// on network appliances that are served on local non-https URLs.
|
|
||||||
// The client is secured by limiting the interface it listens on,
|
|
||||||
// or by authenticating requests before they reach the web client.
|
|
||||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
|
||||||
|
|
||||||
// signal to the CSRF middleware that the request is being served over
|
|
||||||
// plaintext HTTP to skip TLS-only header checks.
|
|
||||||
withSetPlaintext := func(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = csrf.PlaintextHTTPRequest(r)
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s.mode {
|
|
||||||
case LoginServerMode:
|
|
||||||
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveLoginAPI)))
|
|
||||||
metric = "web_login_client_initialization"
|
|
||||||
case ReadOnlyServerMode:
|
|
||||||
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveLoginAPI)))
|
|
||||||
metric = "web_readonly_client_initialization"
|
|
||||||
case ManageServerMode:
|
|
||||||
s.apiHandler = csrfProtect(withSetPlaintext(http.HandlerFunc(s.serveAPI)))
|
|
||||||
metric = "web_client_initialization"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't block startup on reporting metric.
|
// Don't block startup on reporting metric.
|
||||||
// Report in separate go routine with 5 second timeout.
|
// Report in separate go routine with 5 second timeout.
|
||||||
@ -244,6 +218,39 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) withCSRF(h http.Handler) http.Handler {
|
||||||
|
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||||
|
|
||||||
|
// ref https://github.com/tailscale/tailscale/pull/14822
|
||||||
|
// signal to the CSRF middleware that the request is being served over
|
||||||
|
// plaintext HTTP to skip TLS-only header checks.
|
||||||
|
withSetPlaintext := func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = csrf.PlaintextHTTPRequest(r)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: the order of the withSetPlaintext and csrfProtect calls is important
|
||||||
|
// to ensure that we signal to the CSRF middleware that the request is being
|
||||||
|
// served over plaintext HTTP and not over TLS as it presumes by default.
|
||||||
|
return withSetPlaintext(csrfProtect(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
|
||||||
|
switch mode {
|
||||||
|
case LoginServerMode:
|
||||||
|
return http.HandlerFunc(s.serveLoginAPI), "web_login_client_initialization"
|
||||||
|
case ReadOnlyServerMode:
|
||||||
|
return http.HandlerFunc(s.serveLoginAPI), "web_readonly_client_initialization"
|
||||||
|
case ManageServerMode:
|
||||||
|
return http.HandlerFunc(s.serveAPI), "web_client_initialization"
|
||||||
|
default: // invalid mode
|
||||||
|
log.Fatalf("invalid mode: %v", mode)
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Shutdown() {
|
func (s *Server) Shutdown() {
|
||||||
s.logf("web.Server: shutting down")
|
s.logf("web.Server: shutting down")
|
||||||
if s.assetsCleanup != nil {
|
if s.assetsCleanup != nil {
|
||||||
@ -328,7 +335,8 @@ func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (han
|
|||||||
ipv6ServiceHost = "[" + tsaddr.TailscaleServiceIPv6String + "]"
|
ipv6ServiceHost = "[" + tsaddr.TailscaleServiceIPv6String + "]"
|
||||||
)
|
)
|
||||||
// allow requests on quad-100 (or ipv6 equivalent)
|
// allow requests on quad-100 (or ipv6 equivalent)
|
||||||
if r.Host == ipv4ServiceHost || r.Host == ipv6ServiceHost {
|
host := strings.TrimSuffix(r.Host, ":80")
|
||||||
|
if host == ipv4ServiceHost || host == ipv6ServiceHost {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
@ -1175,6 +1177,16 @@ func TestRequireTailscaleIP(t *testing.T) {
|
|||||||
target: "http://[fd7a:115c:a1e0::53]/",
|
target: "http://[fd7a:115c:a1e0::53]/",
|
||||||
wantHandled: false,
|
wantHandled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "quad-100:80",
|
||||||
|
target: "http://100.100.100.100:80/",
|
||||||
|
wantHandled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ipv6-service-addr:80",
|
||||||
|
target: "http://[fd7a:115c:a1e0::53]:80/",
|
||||||
|
wantHandled: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -1477,3 +1489,83 @@ func mockWaitAuthURL(_ context.Context, id string, src tailcfg.NodeID) (*tailcfg
|
|||||||
return nil, errors.New("unknown id")
|
return nil, errors.New("unknown id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCSRFProtect(t *testing.T) {
|
||||||
|
s := &Server{}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /test/csrf-token", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := csrf.Token(r)
|
||||||
|
_, err := io.WriteString(w, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("POST /test/csrf-protected", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := io.WriteString(w, "ok")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
h := s.withCSRF(mux)
|
||||||
|
ser := httptest.NewServer(h)
|
||||||
|
defer ser.Close()
|
||||||
|
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to construct cookie jar: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := ser.Client()
|
||||||
|
client.Jar = jar
|
||||||
|
|
||||||
|
// make GET request to populate cookie jar
|
||||||
|
resp, err := client.Get(ser.URL + "/test/csrf-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %v", resp.Status)
|
||||||
|
}
|
||||||
|
tokenBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to read body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrfToken := strings.TrimSpace(string(tokenBytes))
|
||||||
|
if csrfToken == "" {
|
||||||
|
t.Fatal("empty csrf token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a POST request without the CSRF header; ensure it fails
|
||||||
|
resp, err = client.Post(ser.URL+"/test/csrf-protected", "text/plain", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make request: %v", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusForbidden {
|
||||||
|
t.Fatalf("unexpected status: %v", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a POST request with the CSRF header; ensure it succeeds
|
||||||
|
req, err := http.NewRequest("POST", ser.URL+"/test/csrf-protected", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error building request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-CSRF-Token", csrfToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make request: %v", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %v", resp.Status)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
out, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to read body: %v", err)
|
||||||
|
}
|
||||||
|
if string(out) != "ok" {
|
||||||
|
t.Fatalf("unexpected body: %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
147
cmd/containerboot/certs.go
Normal file
147
cmd/containerboot/certs.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/util/goroutines"
|
||||||
|
"tailscale.com/util/mak"
|
||||||
|
)
|
||||||
|
|
||||||
|
// certManager is responsible for issuing certificates for known domains and for
|
||||||
|
// maintaining a loop that re-attempts issuance daily.
|
||||||
|
// Currently cert manager logic is only run on ingress ProxyGroup replicas that are responsible for managing certs for
|
||||||
|
// HA Ingress HTTPS endpoints ('write' replicas).
|
||||||
|
type certManager struct {
|
||||||
|
lc localClient
|
||||||
|
tracker goroutines.Tracker // tracks running goroutines
|
||||||
|
mu sync.Mutex // guards the following
|
||||||
|
// certLoops contains a map of DNS names, for which we currently need to
|
||||||
|
// manage certs to cancel functions that allow stopping a goroutine when
|
||||||
|
// we no longer need to manage certs for the DNS name.
|
||||||
|
certLoops map[string]context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureCertLoops ensures that, for all currently managed Service HTTPS
|
||||||
|
// endpoints, there is a cert loop responsible for issuing and ensuring the
|
||||||
|
// renewal of the TLS certs.
|
||||||
|
// ServeConfig must not be nil.
|
||||||
|
func (cm *certManager) ensureCertLoops(ctx context.Context, sc *ipn.ServeConfig) error {
|
||||||
|
if sc == nil {
|
||||||
|
return fmt.Errorf("[unexpected] ensureCertLoops called with nil ServeConfig")
|
||||||
|
}
|
||||||
|
currentDomains := make(map[string]bool)
|
||||||
|
const httpsPort = "443"
|
||||||
|
for _, service := range sc.Services {
|
||||||
|
for hostPort := range service.Web {
|
||||||
|
domain, port, err := net.SplitHostPort(string(hostPort))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[unexpected] unable to parse HostPort %s", hostPort)
|
||||||
|
}
|
||||||
|
if port != httpsPort { // HA Ingress' HTTP endpoint
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentDomains[domain] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cm.mu.Lock()
|
||||||
|
defer cm.mu.Unlock()
|
||||||
|
for domain := range currentDomains {
|
||||||
|
if _, exists := cm.certLoops[domain]; !exists {
|
||||||
|
cancelCtx, cancel := context.WithCancel(ctx)
|
||||||
|
mak.Set(&cm.certLoops, domain, cancel)
|
||||||
|
cm.tracker.Go(func() { cm.runCertLoop(cancelCtx, domain) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop goroutines for domain names that are no longer in the config.
|
||||||
|
for domain, cancel := range cm.certLoops {
|
||||||
|
if !currentDomains[domain] {
|
||||||
|
cancel()
|
||||||
|
delete(cm.certLoops, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCertLoop:
|
||||||
|
// - calls localAPI certificate endpoint to ensure that certs are issued for the
|
||||||
|
// given domain name
|
||||||
|
// - calls localAPI certificate endpoint daily to ensure that certs are renewed
|
||||||
|
// - if certificate issuance failed retries after an exponential backoff period
|
||||||
|
// starting at 1 minute and capped at 24 hours. Reset the backoff once issuance succeeds.
|
||||||
|
// Note that renewal check also happens when the node receives an HTTPS request and it is possible that certs get
|
||||||
|
// renewed at that point. Renewal here is needed to prevent the shared certs from expiry in edge cases where the 'write'
|
||||||
|
// replica does not get any HTTPS requests.
|
||||||
|
// https://letsencrypt.org/docs/integration-guide/#retrying-failures
|
||||||
|
func (cm *certManager) runCertLoop(ctx context.Context, domain string) {
|
||||||
|
const (
|
||||||
|
normalInterval = 24 * time.Hour // regular renewal check
|
||||||
|
initialRetry = 1 * time.Minute // initial backoff after a failure
|
||||||
|
maxRetryInterval = 24 * time.Hour // max backoff period
|
||||||
|
)
|
||||||
|
timer := time.NewTimer(0) // fire off timer immediately
|
||||||
|
defer timer.Stop()
|
||||||
|
retryCount := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
// We call the certificate endpoint, but don't do anything
|
||||||
|
// with the returned certs here.
|
||||||
|
// The call to the certificate endpoint will ensure that
|
||||||
|
// certs are issued/renewed as needed and stored in the
|
||||||
|
// relevant state store. For example, for HA Ingress
|
||||||
|
// 'write' replica, the cert and key will be stored in a
|
||||||
|
// Kubernetes Secret named after the domain for which we
|
||||||
|
// are issuing.
|
||||||
|
// Note that renewals triggered by the call to the
|
||||||
|
// certificates endpoint here and by renewal check
|
||||||
|
// triggered during a call to node's HTTPS endpoint
|
||||||
|
// share the same state/renewal lock mechanism, so we
|
||||||
|
// should not run into redundant issuances during
|
||||||
|
// concurrent renewal checks.
|
||||||
|
// TODO(irbekrm): maybe it is worth adding a new
|
||||||
|
// issuance endpoint that explicitly only triggers
|
||||||
|
// issuance and stores certs in the relevant store, but
|
||||||
|
// does not return certs to the caller?
|
||||||
|
_, _, err := cm.lc.CertPair(ctx, domain)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error refreshing certificate for %s: %v", domain, err)
|
||||||
|
}
|
||||||
|
var nextInterval time.Duration
|
||||||
|
// TODO(irbekrm): distinguish between LE rate limit
|
||||||
|
// errors and other error types like transient network
|
||||||
|
// errors.
|
||||||
|
if err == nil {
|
||||||
|
retryCount = 0
|
||||||
|
nextInterval = normalInterval
|
||||||
|
} else {
|
||||||
|
retryCount++
|
||||||
|
// Calculate backoff: initialRetry * 2^(retryCount-1)
|
||||||
|
// For retryCount=1: 1min * 2^0 = 1min
|
||||||
|
// For retryCount=2: 1min * 2^1 = 2min
|
||||||
|
// For retryCount=3: 1min * 2^2 = 4min
|
||||||
|
backoff := initialRetry * time.Duration(1<<(retryCount-1))
|
||||||
|
if backoff > maxRetryInterval {
|
||||||
|
backoff = maxRetryInterval
|
||||||
|
}
|
||||||
|
nextInterval = backoff
|
||||||
|
log.Printf("Error refreshing certificate for %s (retry %d): %v. Will retry in %v\n",
|
||||||
|
domain, retryCount, err, nextInterval)
|
||||||
|
}
|
||||||
|
timer.Reset(nextInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
229
cmd/containerboot/certs_test.go
Normal file
229
cmd/containerboot/certs_test.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEnsureCertLoops tests that the certManager correctly starts and stops
|
||||||
|
// update loops for certs when the serve config changes. It tracks goroutine
|
||||||
|
// count and uses that as a validator that the expected number of cert loops are
|
||||||
|
// running.
|
||||||
|
func TestEnsureCertLoops(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialConfig *ipn.ServeConfig
|
||||||
|
updatedConfig *ipn.ServeConfig
|
||||||
|
initialGoroutines int64 // after initial serve config is applied
|
||||||
|
updatedGoroutines int64 // after updated serve config is applied
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty_serve_config",
|
||||||
|
initialConfig: &ipn.ServeConfig{},
|
||||||
|
initialGoroutines: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil_serve_config",
|
||||||
|
initialConfig: nil,
|
||||||
|
initialGoroutines: 0,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_to_one_service",
|
||||||
|
initialConfig: &ipn.ServeConfig{},
|
||||||
|
updatedConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 0,
|
||||||
|
updatedGoroutines: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single_service",
|
||||||
|
initialConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple_services",
|
||||||
|
initialConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:my-other-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 2, // one loop per domain across all services
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore_non_https_ports",
|
||||||
|
initialConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
"my-app.tailnetxyz.ts.net:80": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 1, // only one loop for the 443 endpoint
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove_domain",
|
||||||
|
initialConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:my-other-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatedConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 2, // initially two loops (one per service)
|
||||||
|
updatedGoroutines: 1, // one loop after removing service2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add_domain",
|
||||||
|
initialConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatedConfig: &ipn.ServeConfig{
|
||||||
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
||||||
|
"svc:my-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:my-other-app": {
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialGoroutines: 1,
|
||||||
|
updatedGoroutines: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cm := &certManager{
|
||||||
|
lc: &fakeLocalClient{},
|
||||||
|
certLoops: make(map[string]context.CancelFunc),
|
||||||
|
}
|
||||||
|
|
||||||
|
allDone := make(chan bool, 1)
|
||||||
|
defer cm.tracker.AddDoneCallback(func() {
|
||||||
|
cm.mu.Lock()
|
||||||
|
defer cm.mu.Unlock()
|
||||||
|
if cm.tracker.RunningGoroutines() > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case allDone <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
err := cm.ensureCertLoops(ctx, tt.initialConfig)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("ensureCertLoops() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := cm.tracker.RunningGoroutines(); got != tt.initialGoroutines {
|
||||||
|
t.Errorf("after initial config: got %d running goroutines, want %d", got, tt.initialGoroutines)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.updatedConfig != nil {
|
||||||
|
if err := cm.ensureCertLoops(ctx, tt.updatedConfig); err != nil {
|
||||||
|
t.Fatalf("ensureCertLoops() error on update = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although starting goroutines and cancelling
|
||||||
|
// the context happens in the main goroutine, it
|
||||||
|
// the actual goroutine exit when a context is
|
||||||
|
// cancelled does not- so wait for a bit for the
|
||||||
|
// running goroutine count to reach the expected
|
||||||
|
// number.
|
||||||
|
deadline := time.After(5 * time.Second)
|
||||||
|
for {
|
||||||
|
if got := cm.tracker.RunningGoroutines(); got == tt.updatedGoroutines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatalf("timed out waiting for goroutine count to reach %d, currently at %d",
|
||||||
|
tt.updatedGoroutines, cm.tracker.RunningGoroutines())
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.updatedGoroutines == 0 {
|
||||||
|
return // no goroutines to wait for
|
||||||
|
}
|
||||||
|
// cancel context to make goroutines exit
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for goroutine to finish")
|
||||||
|
case <-allDone:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -646,7 +646,7 @@ runLoop:
|
|||||||
|
|
||||||
if cfg.ServeConfigPath != "" {
|
if cfg.ServeConfigPath != "" {
|
||||||
triggerWatchServeConfigChanges.Do(func() {
|
triggerWatchServeConfigChanges.Do(func() {
|
||||||
go watchServeConfigChanges(ctx, cfg.ServeConfigPath, certDomainChanged, certDomain, client, kc)
|
go watchServeConfigChanges(ctx, certDomainChanged, certDomain, client, kc, cfg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,20 +28,23 @@ import (
|
|||||||
// applies it to lc. It exits when ctx is canceled. cdChanged is a channel that
|
// applies it to lc. It exits when ctx is canceled. cdChanged is a channel that
|
||||||
// is written to when the certDomain changes, causing the serve config to be
|
// is written to when the certDomain changes, causing the serve config to be
|
||||||
// re-read and applied.
|
// re-read and applied.
|
||||||
func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *local.Client, kc *kubeClient) {
|
func watchServeConfigChanges(ctx context.Context, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *local.Client, kc *kubeClient, cfg *settings) {
|
||||||
if certDomainAtomic == nil {
|
if certDomainAtomic == nil {
|
||||||
panic("certDomainAtomic must not be nil")
|
panic("certDomainAtomic must not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tickChan <-chan time.Time
|
var tickChan <-chan time.Time
|
||||||
var eventChan <-chan fsnotify.Event
|
var eventChan <-chan fsnotify.Event
|
||||||
if w, err := fsnotify.NewWatcher(); err != nil {
|
if w, err := fsnotify.NewWatcher(); err != nil {
|
||||||
|
// Creating a new fsnotify watcher would fail for example if inotify was not able to create a new file descriptor.
|
||||||
|
// See https://github.com/tailscale/tailscale/issues/15081
|
||||||
log.Printf("serve proxy: failed to create fsnotify watcher, timer-only mode: %v", err)
|
log.Printf("serve proxy: failed to create fsnotify watcher, timer-only mode: %v", err)
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
tickChan = ticker.C
|
tickChan = ticker.C
|
||||||
} else {
|
} else {
|
||||||
defer w.Close()
|
defer w.Close()
|
||||||
if err := w.Add(filepath.Dir(path)); err != nil {
|
if err := w.Add(filepath.Dir(cfg.ServeConfigPath)); err != nil {
|
||||||
log.Fatalf("serve proxy: failed to add fsnotify watch: %v", err)
|
log.Fatalf("serve proxy: failed to add fsnotify watch: %v", err)
|
||||||
}
|
}
|
||||||
eventChan = w.Events
|
eventChan = w.Events
|
||||||
@ -49,6 +52,12 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
|||||||
|
|
||||||
var certDomain string
|
var certDomain string
|
||||||
var prevServeConfig *ipn.ServeConfig
|
var prevServeConfig *ipn.ServeConfig
|
||||||
|
var cm certManager
|
||||||
|
if cfg.CertShareMode == "rw" {
|
||||||
|
cm = certManager{
|
||||||
|
lc: lc,
|
||||||
|
}
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@ -61,12 +70,12 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
|||||||
// k8s handles these mounts. So just re-read the file and apply it
|
// k8s handles these mounts. So just re-read the file and apply it
|
||||||
// if it's changed.
|
// if it's changed.
|
||||||
}
|
}
|
||||||
sc, err := readServeConfig(path, certDomain)
|
sc, err := readServeConfig(cfg.ServeConfigPath, certDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("serve proxy: failed to read serve config: %v", err)
|
log.Fatalf("serve proxy: failed to read serve config: %v", err)
|
||||||
}
|
}
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
log.Printf("serve proxy: no serve config at %q, skipping", path)
|
log.Printf("serve proxy: no serve config at %q, skipping", cfg.ServeConfigPath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if prevServeConfig != nil && reflect.DeepEqual(sc, prevServeConfig) {
|
if prevServeConfig != nil && reflect.DeepEqual(sc, prevServeConfig) {
|
||||||
@ -81,6 +90,12 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
prevServeConfig = sc
|
prevServeConfig = sc
|
||||||
|
if cfg.CertShareMode != "rw" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cm.ensureCertLoops(ctx, sc); err != nil {
|
||||||
|
log.Fatalf("serve proxy: error ensuring cert loops: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +109,7 @@ func certDomainFromNetmap(nm *netmap.NetworkMap) string {
|
|||||||
// localClient is a subset of [local.Client] that can be mocked for testing.
|
// localClient is a subset of [local.Client] that can be mocked for testing.
|
||||||
type localClient interface {
|
type localClient interface {
|
||||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||||
|
CertPair(context.Context, string) ([]byte, []byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateServeConfig(ctx context.Context, sc *ipn.ServeConfig, certDomain string, lc localClient) error {
|
func updateServeConfig(ctx context.Context, sc *ipn.ServeConfig, certDomain string, lc localClient) error {
|
||||||
|
@ -206,6 +206,10 @@ func (m *fakeLocalClient) SetServeConfig(ctx context.Context, cfg *ipn.ServeConf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *fakeLocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestHasHTTPSEndpoint(t *testing.T) {
|
func TestHasHTTPSEndpoint(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -74,6 +74,12 @@ type settings struct {
|
|||||||
HealthCheckEnabled bool
|
HealthCheckEnabled bool
|
||||||
DebugAddrPort string
|
DebugAddrPort string
|
||||||
EgressProxiesCfgPath string
|
EgressProxiesCfgPath string
|
||||||
|
// CertShareMode is set for Kubernetes Pods running cert share mode.
|
||||||
|
// Possible values are empty (containerboot doesn't run any certs
|
||||||
|
// logic), 'ro' (for Pods that shold never attempt to issue/renew
|
||||||
|
// certs) and 'rw' for Pods that should manage the TLS certs shared
|
||||||
|
// amongst the replicas.
|
||||||
|
CertShareMode string
|
||||||
}
|
}
|
||||||
|
|
||||||
func configFromEnv() (*settings, error) {
|
func configFromEnv() (*settings, error) {
|
||||||
@ -128,6 +134,17 @@ func configFromEnv() (*settings, error) {
|
|||||||
cfg.PodIPv6 = parsed.String()
|
cfg.PodIPv6 = parsed.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If cert share is enabled, set the replica as read or write. Only 0th
|
||||||
|
// replica should be able to write.
|
||||||
|
isInCertShareMode := defaultBool("TS_EXPERIMENTAL_CERT_SHARE", false)
|
||||||
|
if isInCertShareMode {
|
||||||
|
cfg.CertShareMode = "ro"
|
||||||
|
podName := os.Getenv("POD_NAME")
|
||||||
|
if strings.HasSuffix(podName, "-0") {
|
||||||
|
cfg.CertShareMode = "rw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := cfg.validate(); err != nil {
|
if err := cfg.validate(); err != nil {
|
||||||
return nil, fmt.Errorf("invalid configuration: %v", err)
|
return nil, fmt.Errorf("invalid configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,9 @@ func startTailscaled(ctx context.Context, cfg *settings) (*local.Client, *os.Pro
|
|||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
}
|
}
|
||||||
|
if cfg.CertShareMode != "" {
|
||||||
|
cmd.Env = append(os.Environ(), "TS_CERT_SHARE_MODE="+cfg.CertShareMode)
|
||||||
|
}
|
||||||
log.Printf("Starting tailscaled")
|
log.Printf("Starting tailscaled")
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return nil, nil, fmt.Errorf("starting tailscaled failed: %v", err)
|
return nil, nil, fmt.Errorf("starting tailscaled failed: %v", err)
|
||||||
@ -173,11 +176,14 @@ func tailscaleSet(ctx context.Context, cfg *settings) error {
|
|||||||
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Client, errCh chan<- error) {
|
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Client, errCh chan<- error) {
|
||||||
var (
|
var (
|
||||||
tickChan <-chan time.Time
|
tickChan <-chan time.Time
|
||||||
|
eventChan <-chan fsnotify.Event
|
||||||
|
errChan <-chan error
|
||||||
tailscaledCfgDir = filepath.Dir(path)
|
tailscaledCfgDir = filepath.Dir(path)
|
||||||
prevTailscaledCfg []byte
|
prevTailscaledCfg []byte
|
||||||
)
|
)
|
||||||
w, err := fsnotify.NewWatcher()
|
if w, err := fsnotify.NewWatcher(); err != nil {
|
||||||
if err != nil {
|
// Creating a new fsnotify watcher would fail for example if inotify was not able to create a new file descriptor.
|
||||||
|
// See https://github.com/tailscale/tailscale/issues/15081
|
||||||
log.Printf("tailscaled config watch: failed to create fsnotify watcher, timer-only mode: %v", err)
|
log.Printf("tailscaled config watch: failed to create fsnotify watcher, timer-only mode: %v", err)
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@ -188,6 +194,8 @@ func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Cl
|
|||||||
errCh <- fmt.Errorf("failed to add fsnotify watch: %w", err)
|
errCh <- fmt.Errorf("failed to add fsnotify watch: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
eventChan = w.Events
|
||||||
|
errChan = w.Errors
|
||||||
}
|
}
|
||||||
b, err := os.ReadFile(path)
|
b, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -205,11 +213,11 @@ func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Cl
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case err := <-w.Errors:
|
case err := <-errChan:
|
||||||
errCh <- fmt.Errorf("watcher error: %w", err)
|
errCh <- fmt.Errorf("watcher error: %w", err)
|
||||||
return
|
return
|
||||||
case <-tickChan:
|
case <-tickChan:
|
||||||
case event := <-w.Events:
|
case event := <-eventChan:
|
||||||
if event.Name != toWatch {
|
if event.Name != toWatch {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,28 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
var unsafeHostnameCharacters = regexp.MustCompile(`[^a-zA-Z0-9-\.]`)
|
||||||
@ -65,8 +77,18 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
|||||||
crtPath := filepath.Join(certdir, keyname+".crt")
|
crtPath := filepath.Join(certdir, keyname+".crt")
|
||||||
keyPath := filepath.Join(certdir, keyname+".key")
|
keyPath := filepath.Join(certdir, keyname+".key")
|
||||||
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
cert, err := tls.LoadX509KeyPair(crtPath, keyPath)
|
||||||
|
hostnameIP := net.ParseIP(hostname) // or nil if hostname isn't an IP address
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
// If the hostname is an IP address, automatically create a
|
||||||
|
// self-signed certificate for it.
|
||||||
|
var certp *tls.Certificate
|
||||||
|
if os.IsNotExist(err) && hostnameIP != nil {
|
||||||
|
certp, err = createSelfSignedIPCert(crtPath, keyPath, hostname)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can not load x509 key pair for hostname %q: %w", keyname, err)
|
||||||
|
}
|
||||||
|
cert = *certp
|
||||||
}
|
}
|
||||||
// ensure hostname matches with the certificate
|
// ensure hostname matches with the certificate
|
||||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
@ -76,6 +98,18 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
|||||||
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
||||||
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
||||||
}
|
}
|
||||||
|
if hostnameIP != nil {
|
||||||
|
// If the hostname is an IP address, print out information on how to
|
||||||
|
// confgure this in the derpmap.
|
||||||
|
dn := &tailcfg.DERPNode{
|
||||||
|
Name: "custom",
|
||||||
|
RegionID: 900,
|
||||||
|
HostName: hostname,
|
||||||
|
CertName: fmt.Sprintf("sha256-raw:%-02x", sha256.Sum256(x509Cert.Raw)),
|
||||||
|
}
|
||||||
|
dnJSON, _ := json.Marshal(dn)
|
||||||
|
log.Printf("Using self-signed certificate for IP address %q. Configure it in DERPMap using: (https://tailscale.com/s/custom-derp)\n %s", hostname, dnJSON)
|
||||||
|
}
|
||||||
return &manualCertManager{
|
return &manualCertManager{
|
||||||
cert: &cert,
|
cert: &cert,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
@ -109,3 +143,69 @@ func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certif
|
|||||||
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSelfSignedIPCert(crtPath, keyPath, ipStr string) (*tls.Certificate, error) {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate EC private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: ipStr,
|
||||||
|
},
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: now.AddDate(1, 0, 0), // expires in 1 year; a bit over that is rejected by macOS etc
|
||||||
|
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the IP as a SAN.
|
||||||
|
template.IPAddresses = []net.IP{ip}
|
||||||
|
|
||||||
|
// Create the self-signed certificate.
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
|
||||||
|
keyBytes, err := x509.MarshalECPrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to marshal EC private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(crtPath), 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create directory for certificate: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(crtPath, certPEM, 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write certificate to %s: %v", crtPath, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write key to %s: %v", keyPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create tls.Certificate: %v", err)
|
||||||
|
}
|
||||||
|
return &tlsCert, nil
|
||||||
|
}
|
||||||
|
@ -4,19 +4,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/derp"
|
||||||
|
"tailscale.com/derp/derphttp"
|
||||||
|
"tailscale.com/net/netmon"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify that in --certmode=manual mode, we can use a bare IP address
|
// Verify that in --certmode=manual mode, we can use a bare IP address
|
||||||
@ -95,3 +105,66 @@ func TestCertIP(t *testing.T) {
|
|||||||
t.Fatalf("GetCertificate returned nil")
|
t.Fatalf("GetCertificate returned nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that we can dial a raw IP without using a hostname and without a WebPKI
|
||||||
|
// cert, validating the cert against the signature of the cert in the DERP map's
|
||||||
|
// DERPNode.
|
||||||
|
//
|
||||||
|
// See https://github.com/tailscale/tailscale/issues/11776.
|
||||||
|
func TestPinnedCertRawIP(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
cp, err := NewManualCertManager(td, "127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewManualCertManager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := cp.TLSConfig().GetCertificate(&tls.ClientHelloInfo{
|
||||||
|
ServerName: "127.0.0.1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCertificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Listen: %v", err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
ds := derp.NewServer(key.NewNode(), t.Logf)
|
||||||
|
|
||||||
|
derpHandler := derphttp.Handler(ds)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/derp", derpHandler)
|
||||||
|
|
||||||
|
var hs http.Server
|
||||||
|
hs.Handler = mux
|
||||||
|
hs.TLSConfig = cp.TLSConfig()
|
||||||
|
go hs.ServeTLS(ln, "", "")
|
||||||
|
|
||||||
|
lnPort := ln.Addr().(*net.TCPAddr).Port
|
||||||
|
|
||||||
|
reg := &tailcfg.DERPRegion{
|
||||||
|
RegionID: 900,
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{
|
||||||
|
RegionID: 900,
|
||||||
|
HostName: "127.0.0.1",
|
||||||
|
CertName: fmt.Sprintf("sha256-raw:%-02x", sha256.Sum256(cert.Leaf.Raw)),
|
||||||
|
DERPPort: lnPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
netMon := netmon.NewStatic()
|
||||||
|
dc := derphttp.NewRegionClient(key.NewNode(), t.Logf, netMon, func() *tailcfg.DERPRegion {
|
||||||
|
return reg
|
||||||
|
})
|
||||||
|
defer dc.Close()
|
||||||
|
|
||||||
|
_, connClose, _, err := dc.DialRegionTLS(context.Background(), reg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DialRegionTLS: %v", err)
|
||||||
|
}
|
||||||
|
defer connClose.Close()
|
||||||
|
}
|
||||||
|
@ -96,6 +96,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
tailscale.com/disco from tailscale.com/derp
|
tailscale.com/disco from tailscale.com/derp
|
||||||
tailscale.com/drive from tailscale.com/client/local+
|
tailscale.com/drive from tailscale.com/client/local+
|
||||||
tailscale.com/envknob from tailscale.com/client/local+
|
tailscale.com/envknob from tailscale.com/client/local+
|
||||||
|
tailscale.com/feature from tailscale.com/tsweb
|
||||||
tailscale.com/health from tailscale.com/net/tlsdial+
|
tailscale.com/health from tailscale.com/net/tlsdial+
|
||||||
tailscale.com/hostinfo from tailscale.com/net/netmon+
|
tailscale.com/hostinfo from tailscale.com/net/netmon+
|
||||||
tailscale.com/ipn from tailscale.com/client/local
|
tailscale.com/ipn from tailscale.com/client/local
|
||||||
@ -128,8 +129,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
tailscale.com/tstime from tailscale.com/derp+
|
tailscale.com/tstime from tailscale.com/derp+
|
||||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||||
tailscale.com/tstime/rate from tailscale.com/derp
|
tailscale.com/tstime/rate from tailscale.com/derp
|
||||||
tailscale.com/tsweb from tailscale.com/cmd/derper
|
tailscale.com/tsweb from tailscale.com/cmd/derper+
|
||||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
|
tailscale.com/tsweb/promvarz from tailscale.com/cmd/derper
|
||||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||||
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/empty from tailscale.com/ipn
|
tailscale.com/types/empty from tailscale.com/ipn
|
||||||
@ -309,7 +310,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
html from net/http/pprof+
|
html from net/http/pprof+
|
||||||
html/template from tailscale.com/cmd/derper
|
html/template from tailscale.com/cmd/derper
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from syscall+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
internal/bytealg from bytes+
|
internal/bytealg from bytes+
|
||||||
internal/byteorder from crypto/cipher+
|
internal/byteorder from crypto/cipher+
|
||||||
@ -319,12 +320,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
internal/filepathlite from os+
|
internal/filepathlite from os+
|
||||||
internal/fmtsort from fmt+
|
internal/fmtsort from fmt+
|
||||||
internal/goarch from crypto/internal/fips140deps/cpu+
|
internal/goarch from crypto/internal/fips140deps/cpu+
|
||||||
internal/godebug from crypto/tls+
|
internal/godebug from crypto/internal/fips140deps/godebug+
|
||||||
internal/godebugs from internal/godebug+
|
internal/godebugs from internal/godebug+
|
||||||
internal/goexperiment from runtime+
|
internal/goexperiment from hash/maphash+
|
||||||
internal/goos from crypto/x509+
|
internal/goos from crypto/x509+
|
||||||
internal/itoa from internal/poll+
|
internal/itoa from internal/poll+
|
||||||
internal/msan from syscall+
|
internal/msan from internal/runtime/maps+
|
||||||
internal/nettrace from net+
|
internal/nettrace from net+
|
||||||
internal/oserror from io/fs+
|
internal/oserror from io/fs+
|
||||||
internal/poll from net+
|
internal/poll from net+
|
||||||
|
@ -49,6 +49,9 @@ import (
|
|||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
|
|
||||||
|
// Support for prometheus varz in tsweb
|
||||||
|
_ "tailscale.com/tsweb/promvarz"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -63,6 +66,7 @@ var (
|
|||||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443. When --certmode=manual, this can be an IP address to avoid SNI checks")
|
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443. When --certmode=manual, this can be an IP address to avoid SNI checks")
|
||||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||||
|
flagHome = flag.String("home", "", "what to serve at the root path. It may be left empty (the default, for a default homepage), \"blank\" for a blank page, or a URL to redirect to")
|
||||||
|
|
||||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list. If an entry contains a slash, the second part names a hostname to be used when dialing the target.")
|
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list. If an entry contains a slash, the second part names a hostname to be used when dialing the target.")
|
||||||
@ -71,10 +75,13 @@ var (
|
|||||||
secretsCacheDir = flag.String("secrets-cache-dir", defaultSetecCacheDir(), "directory to cache setec secrets in (required if --secrets-url is set)")
|
secretsCacheDir = flag.String("secrets-cache-dir", defaultSetecCacheDir(), "directory to cache setec secrets in (required if --secrets-url is set)")
|
||||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||||
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list. If an entry contains a slash, the second part names a DNS record to poll for its TXT record with a `0` to `100` value for rollout percentage.")
|
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list. If an entry contains a slash, the second part names a DNS record to poll for its TXT record with a `0` to `100` value for rollout percentage.")
|
||||||
|
|
||||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||||
verifyClientURL = flag.String("verify-client-url", "", "if non-empty, an admission controller URL for permitting client connections; see tailcfg.DERPAdmitClientRequest")
|
verifyClientURL = flag.String("verify-client-url", "", "if non-empty, an admission controller URL for permitting client connections; see tailcfg.DERPAdmitClientRequest")
|
||||||
verifyFailOpen = flag.Bool("verify-client-url-fail-open", true, "whether we fail open if --verify-client-url is unreachable")
|
verifyFailOpen = flag.Bool("verify-client-url-fail-open", true, "whether we fail open if --verify-client-url is unreachable")
|
||||||
|
|
||||||
|
socket = flag.String("socket", "", "optional alternate path to tailscaled socket (only relevant when using --verify-clients)")
|
||||||
|
|
||||||
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
|
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
|
||||||
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
|
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
|
||||||
|
|
||||||
@ -192,6 +199,7 @@ func main() {
|
|||||||
|
|
||||||
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
s := derp.NewServer(cfg.PrivateKey, log.Printf)
|
||||||
s.SetVerifyClient(*verifyClients)
|
s.SetVerifyClient(*verifyClients)
|
||||||
|
s.SetTailscaledSocketPath(*socket)
|
||||||
s.SetVerifyClientURL(*verifyClientURL)
|
s.SetVerifyClientURL(*verifyClientURL)
|
||||||
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
|
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
|
||||||
s.SetTCPWriteTimeout(*tcpWriteTimeout)
|
s.SetTCPWriteTimeout(*tcpWriteTimeout)
|
||||||
@ -250,6 +258,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
expvar.Publish("derp", s.ExpVar())
|
expvar.Publish("derp", s.ExpVar())
|
||||||
|
|
||||||
|
handleHome, ok := getHomeHandler(*flagHome)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("unknown --home value %q", *flagHome)
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
if *runDERP {
|
if *runDERP {
|
||||||
derpHandler := derphttp.Handler(s)
|
derpHandler := derphttp.Handler(s)
|
||||||
@ -270,19 +283,7 @@ func main() {
|
|||||||
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
|
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
|
||||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tsweb.AddBrowserHeaders(w)
|
tsweb.AddBrowserHeaders(w)
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
handleHome.ServeHTTP(w, r)
|
||||||
w.WriteHeader(200)
|
|
||||||
err := homePageTemplate.Execute(w, templateData{
|
|
||||||
ShowAbuseInfo: validProdHostname.MatchString(*hostname),
|
|
||||||
Disabled: !*runDERP,
|
|
||||||
AllowDebug: tsweb.AllowDebugAccess(r),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if r.Context().Err() == nil {
|
|
||||||
log.Printf("homePageTemplate.Execute: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tsweb.AddBrowserHeaders(w)
|
tsweb.AddBrowserHeaders(w)
|
||||||
@ -575,3 +576,35 @@ var homePageTemplate = template.Must(template.New("home").Parse(`<html><body>
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`))
|
`))
|
||||||
|
|
||||||
|
// getHomeHandler returns a handler for the home page based on a flag string
|
||||||
|
// as documented on the --home flag.
|
||||||
|
func getHomeHandler(val string) (_ http.Handler, ok bool) {
|
||||||
|
if val == "" {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
err := homePageTemplate.Execute(w, templateData{
|
||||||
|
ShowAbuseInfo: validProdHostname.MatchString(*hostname),
|
||||||
|
Disabled: !*runDERP,
|
||||||
|
AllowDebug: tsweb.AllowDebugAccess(r),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if r.Context().Err() == nil {
|
||||||
|
log.Printf("homePageTemplate.Execute: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
if val == "blank" {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") {
|
||||||
|
return http.RedirectHandler(val, http.StatusFound), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
@ -15,6 +15,9 @@ import (
|
|||||||
"tailscale.com/prober"
|
"tailscale.com/prober"
|
||||||
"tailscale.com/tsweb"
|
"tailscale.com/tsweb"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
|
|
||||||
|
// Support for prometheus varz in tsweb
|
||||||
|
_ "tailscale.com/tsweb/promvarz"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -134,6 +135,10 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
|
|||||||
if who == nil {
|
if who == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, tailcfg.NodeAttrNativeIPV4)
|
||||||
|
if err == nil && len(vals) > 0 {
|
||||||
|
return vals[0]
|
||||||
|
}
|
||||||
for _, nodeIP := range who.Node.Addresses {
|
for _, nodeIP := range who.Node.Addresses {
|
||||||
if nodeIP.Addr().Is4() && nodeIP.IsSingleIP() {
|
if nodeIP.Addr().Is4() && nodeIP.IsSingleIP() {
|
||||||
return nodeIP.Addr().String()
|
return nodeIP.Addr().String()
|
||||||
|
@ -814,6 +814,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
|
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
|
||||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||||
tailscale.com/ipn from tailscale.com/client/local+
|
tailscale.com/ipn from tailscale.com/client/local+
|
||||||
|
tailscale.com/ipn/auditlog from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+
|
tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+
|
||||||
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
|
||||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||||
@ -904,6 +905,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
tailscale.com/tstime/rate from tailscale.com/derp+
|
tailscale.com/tstime/rate from tailscale.com/derp+
|
||||||
tailscale.com/tsweb/varz from tailscale.com/util/usermetric
|
tailscale.com/tsweb/varz from tailscale.com/util/usermetric
|
||||||
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
|
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
|
||||||
|
tailscale.com/types/bools from tailscale.com/tsnet
|
||||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/types/empty from tailscale.com/ipn+
|
tailscale.com/types/empty from tailscale.com/ipn+
|
||||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||||
@ -1149,7 +1151,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
html from html/template+
|
html from html/template+
|
||||||
html/template from github.com/gorilla/csrf
|
html/template from github.com/gorilla/csrf
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from syscall+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
internal/bytealg from bytes+
|
internal/bytealg from bytes+
|
||||||
internal/byteorder from crypto/cipher+
|
internal/byteorder from crypto/cipher+
|
||||||
@ -1161,11 +1163,11 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
internal/goarch from crypto/internal/fips140deps/cpu+
|
internal/goarch from crypto/internal/fips140deps/cpu+
|
||||||
internal/godebug from archive/tar+
|
internal/godebug from archive/tar+
|
||||||
internal/godebugs from internal/godebug+
|
internal/godebugs from internal/godebug+
|
||||||
internal/goexperiment from runtime+
|
internal/goexperiment from hash/maphash+
|
||||||
internal/goos from crypto/x509+
|
internal/goos from crypto/x509+
|
||||||
internal/itoa from internal/poll+
|
internal/itoa from internal/poll+
|
||||||
internal/lazyregexp from go/doc
|
internal/lazyregexp from go/doc
|
||||||
internal/msan from syscall+
|
internal/msan from internal/runtime/maps+
|
||||||
internal/nettrace from net+
|
internal/nettrace from net+
|
||||||
internal/oserror from io/fs+
|
internal/oserror from io/fs+
|
||||||
internal/poll from net+
|
internal/poll from net+
|
||||||
|
@ -75,7 +75,7 @@ rules:
|
|||||||
verbs: ["get", "list", "watch", "create", "update", "deletecollection"]
|
verbs: ["get", "list", "watch", "create", "update", "deletecollection"]
|
||||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||||
resources: ["roles", "rolebindings"]
|
resources: ["roles", "rolebindings"]
|
||||||
verbs: ["get", "create", "patch", "update", "list", "watch"]
|
verbs: ["get", "create", "patch", "update", "list", "watch", "deletecollection"]
|
||||||
- apiGroups: ["monitoring.coreos.com"]
|
- apiGroups: ["monitoring.coreos.com"]
|
||||||
resources: ["servicemonitors"]
|
resources: ["servicemonitors"]
|
||||||
verbs: ["get", "list", "update", "create", "delete"]
|
verbs: ["get", "list", "update", "create", "delete"]
|
||||||
|
@ -4898,6 +4898,7 @@ rules:
|
|||||||
- update
|
- update
|
||||||
- list
|
- list
|
||||||
- watch
|
- watch
|
||||||
|
- deletecollection
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- monitoring.coreos.com
|
- monitoring.coreos.com
|
||||||
resources:
|
resources:
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
operatorutils "tailscale.com/k8s-operator"
|
operatorutils "tailscale.com/k8s-operator"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
)
|
)
|
||||||
@ -163,10 +164,10 @@ func headlessSvcForParent(o client.Object, typ string) *corev1.Service {
|
|||||||
Name: o.GetName(),
|
Name: o.GetName(),
|
||||||
Namespace: "tailscale",
|
Namespace: "tailscale",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentName: o.GetName(),
|
LabelParentName: o.GetName(),
|
||||||
LabelParentNamespace: o.GetNamespace(),
|
LabelParentNamespace: o.GetNamespace(),
|
||||||
LabelParentType: typ,
|
LabelParentType: typ,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.ServiceSpec{
|
Spec: corev1.ServiceSpec{
|
||||||
|
@ -112,9 +112,9 @@ func (er *egressPodsReconciler) Reconcile(ctx context.Context, req reconcile.Req
|
|||||||
}
|
}
|
||||||
// Get all ClusterIP Services for all egress targets exposed to cluster via this ProxyGroup.
|
// Get all ClusterIP Services for all egress targets exposed to cluster via this ProxyGroup.
|
||||||
lbls := map[string]string{
|
lbls := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
labelProxyGroup: proxyGroupName,
|
labelProxyGroup: proxyGroupName,
|
||||||
labelSvcType: typeEgress,
|
labelSvcType: typeEgress,
|
||||||
}
|
}
|
||||||
svcs := &corev1.ServiceList{}
|
svcs := &corev1.ServiceList{}
|
||||||
if err := er.List(ctx, svcs, client.InNamespace(er.tsNamespace), client.MatchingLabels(lbls)); err != nil {
|
if err := er.List(ctx, svcs, client.InNamespace(er.tsNamespace), client.MatchingLabels(lbls)); err != nil {
|
||||||
|
@ -450,9 +450,9 @@ func newSvc(name string, port int32) (*corev1.Service, string) {
|
|||||||
Namespace: "operator-ns",
|
Namespace: "operator-ns",
|
||||||
Name: name,
|
Name: name,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
labelProxyGroup: "dev",
|
labelProxyGroup: "dev",
|
||||||
labelSvcType: typeEgress,
|
labelSvcType: typeEgress,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: corev1.ServiceSpec{},
|
Spec: corev1.ServiceSpec{},
|
||||||
|
@ -630,7 +630,11 @@ func tailnetTargetFromSvc(svc *corev1.Service) egressservices.TailnetTarget {
|
|||||||
|
|
||||||
func portMap(p corev1.ServicePort) egressservices.PortMap {
|
func portMap(p corev1.ServicePort) egressservices.PortMap {
|
||||||
// TODO (irbekrm): out of bounds check?
|
// TODO (irbekrm): out of bounds check?
|
||||||
return egressservices.PortMap{Protocol: string(p.Protocol), MatchPort: uint16(p.TargetPort.IntVal), TargetPort: uint16(p.Port)}
|
return egressservices.PortMap{
|
||||||
|
Protocol: string(p.Protocol),
|
||||||
|
MatchPort: uint16(p.TargetPort.IntVal),
|
||||||
|
TargetPort: uint16(p.Port),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEgressSvcForProxyGroup(obj client.Object) bool {
|
func isEgressSvcForProxyGroup(obj client.Object) bool {
|
||||||
@ -676,12 +680,12 @@ func egressSvcsConfigs(ctx context.Context, cl client.Client, proxyGroupName, ts
|
|||||||
// should probably validate and truncate (?) the names is they are too long.
|
// should probably validate and truncate (?) the names is they are too long.
|
||||||
func egressSvcChildResourceLabels(svc *corev1.Service) map[string]string {
|
func egressSvcChildResourceLabels(svc *corev1.Service) map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentType: "svc",
|
LabelParentType: "svc",
|
||||||
LabelParentName: svc.Name,
|
LabelParentName: svc.Name,
|
||||||
LabelParentNamespace: svc.Namespace,
|
LabelParentNamespace: svc.Namespace,
|
||||||
labelProxyGroup: svc.Annotations[AnnotationProxyGroup],
|
labelProxyGroup: svc.Annotations[AnnotationProxyGroup],
|
||||||
labelSvcType: typeEgress,
|
labelSvcType: typeEgress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -17,13 +20,16 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
networkingv1 "k8s.io/api/networking/v1"
|
networkingv1 "k8s.io/api/networking/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
"tailscale.com/internal/client/tailscale"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
tsoperator "tailscale.com/k8s-operator"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
@ -63,6 +69,12 @@ func TestIngressPGReconciler(t *testing.T) {
|
|||||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
verifyServeConfig(t, fc, "svc:my-svc", false)
|
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||||
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||||
|
verifyTailscaledConfig(t, fc, []string{"svc:my-svc"})
|
||||||
|
|
||||||
|
// Verify cert resources were created for the first Ingress
|
||||||
|
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-svc.ts.net"))
|
||||||
|
expectEqual(t, fc, certSecretRole("test-pg", "operator-ns", "my-svc.ts.net"))
|
||||||
|
expectEqual(t, fc, certSecretRoleBinding("test-pg", "operator-ns", "my-svc.ts.net"))
|
||||||
|
|
||||||
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||||
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
|
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
|
||||||
@ -118,10 +130,17 @@ func TestIngressPGReconciler(t *testing.T) {
|
|||||||
verifyServeConfig(t, fc, "svc:my-other-svc", false)
|
verifyServeConfig(t, fc, "svc:my-other-svc", false)
|
||||||
verifyVIPService(t, ft, "svc:my-other-svc", []string{"443"})
|
verifyVIPService(t, ft, "svc:my-other-svc", []string{"443"})
|
||||||
|
|
||||||
|
// Verify cert resources were created for the second Ingress
|
||||||
|
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-other-svc.ts.net"))
|
||||||
|
expectEqual(t, fc, certSecretRole("test-pg", "operator-ns", "my-other-svc.ts.net"))
|
||||||
|
expectEqual(t, fc, certSecretRoleBinding("test-pg", "operator-ns", "my-other-svc.ts.net"))
|
||||||
|
|
||||||
// Verify first Ingress is still working
|
// Verify first Ingress is still working
|
||||||
verifyServeConfig(t, fc, "svc:my-svc", false)
|
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||||
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||||
|
|
||||||
|
verifyTailscaledConfig(t, fc, []string{"svc:my-svc", "svc:my-other-svc"})
|
||||||
|
|
||||||
// Delete second Ingress
|
// Delete second Ingress
|
||||||
if err := fc.Delete(context.Background(), ing2); err != nil {
|
if err := fc.Delete(context.Background(), ing2); err != nil {
|
||||||
t.Fatalf("deleting second Ingress: %v", err)
|
t.Fatalf("deleting second Ingress: %v", err)
|
||||||
@ -151,6 +170,11 @@ func TestIngressPGReconciler(t *testing.T) {
|
|||||||
t.Error("second Ingress service config was not cleaned up")
|
t.Error("second Ingress service config was not cleaned up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifyTailscaledConfig(t, fc, []string{"svc:my-svc"})
|
||||||
|
expectMissing[corev1.Secret](t, fc, "operator-ns", "my-other-svc.ts.net")
|
||||||
|
expectMissing[rbacv1.Role](t, fc, "operator-ns", "my-other-svc.ts.net")
|
||||||
|
expectMissing[rbacv1.RoleBinding](t, fc, "operator-ns", "my-other-svc.ts.net")
|
||||||
|
|
||||||
// Delete the first Ingress and verify cleanup
|
// Delete the first Ingress and verify cleanup
|
||||||
if err := fc.Delete(context.Background(), ing); err != nil {
|
if err := fc.Delete(context.Background(), ing); err != nil {
|
||||||
t.Fatalf("deleting Ingress: %v", err)
|
t.Fatalf("deleting Ingress: %v", err)
|
||||||
@ -175,6 +199,67 @@ func TestIngressPGReconciler(t *testing.T) {
|
|||||||
if len(cfg.Services) > 0 {
|
if len(cfg.Services) > 0 {
|
||||||
t.Error("serve config not cleaned up")
|
t.Error("serve config not cleaned up")
|
||||||
}
|
}
|
||||||
|
verifyTailscaledConfig(t, fc, nil)
|
||||||
|
|
||||||
|
// Add verification that cert resources were cleaned up
|
||||||
|
expectMissing[corev1.Secret](t, fc, "operator-ns", "my-svc.ts.net")
|
||||||
|
expectMissing[rbacv1.Role](t, fc, "operator-ns", "my-svc.ts.net")
|
||||||
|
expectMissing[rbacv1.RoleBinding](t, fc, "operator-ns", "my-svc.ts.net")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressPGReconciler_UpdateIngressHostname(t *testing.T) {
|
||||||
|
ingPGR, fc, ft := setupIngressTest(t)
|
||||||
|
|
||||||
|
ing := &networkingv1.Ingress{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
UID: types.UID("1234-UID"),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"tailscale.com/proxy-group": "test-pg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
DefaultBackend: &networkingv1.IngressBackend{
|
||||||
|
Service: &networkingv1.IngressServiceBackend{
|
||||||
|
Name: "test",
|
||||||
|
Port: networkingv1.ServiceBackendPort{
|
||||||
|
Number: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"my-svc.tailnetxyz.ts.net"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, ing)
|
||||||
|
|
||||||
|
// Verify initial reconciliation
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||||
|
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||||
|
verifyTailscaledConfig(t, fc, []string{"svc:my-svc"})
|
||||||
|
|
||||||
|
// Update the Ingress hostname and make sure the original VIPService is deleted.
|
||||||
|
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||||
|
ing.Spec.TLS[0].Hosts[0] = "updated-svc.tailnetxyz.ts.net"
|
||||||
|
})
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
verifyServeConfig(t, fc, "svc:updated-svc", false)
|
||||||
|
verifyVIPService(t, ft, "svc:updated-svc", []string{"443"})
|
||||||
|
verifyTailscaledConfig(t, fc, []string{"svc:updated-svc"})
|
||||||
|
|
||||||
|
_, err := ft.GetVIPService(context.Background(), tailcfg.ServiceName("svc:my-svc"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("svc:my-svc not cleaned up")
|
||||||
|
}
|
||||||
|
var errResp *tailscale.ErrResponse
|
||||||
|
if !errors.As(err, &errResp) || errResp.Status != http.StatusNotFound {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateIngress(t *testing.T) {
|
func TestValidateIngress(t *testing.T) {
|
||||||
@ -182,6 +267,15 @@ func TestValidateIngress(t *testing.T) {
|
|||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "test-ingress",
|
Name: "test-ingress",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationProxyGroup: "test-pg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"test"}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,10 +299,11 @@ func TestValidateIngress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ing *networkingv1.Ingress
|
ing *networkingv1.Ingress
|
||||||
pg *tsapi.ProxyGroup
|
pg *tsapi.ProxyGroup
|
||||||
wantErr string
|
existingIngs []networkingv1.Ingress
|
||||||
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid_ingress_with_hostname",
|
name: "valid_ingress_with_hostname",
|
||||||
@ -298,12 +393,38 @@ func TestValidateIngress(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "ProxyGroup \"test-pg\" is not ready",
|
wantErr: "ProxyGroup \"test-pg\" is not ready",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate_hostname",
|
||||||
|
ing: baseIngress,
|
||||||
|
pg: readyProxyGroup,
|
||||||
|
existingIngs: []networkingv1.Ingress{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "existing-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationProxyGroup: "test-pg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
wantErr: `found duplicate Ingress "existing-ingress" for hostname "test" - multiple Ingresses for the same hostname in the same cluster are not allowed`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := &IngressPGReconciler{}
|
fc := fake.NewClientBuilder().
|
||||||
err := r.validateIngress(tt.ing, tt.pg)
|
WithScheme(tsapi.GlobalScheme).
|
||||||
|
WithObjects(tt.ing).
|
||||||
|
WithLists(&networkingv1.IngressList{Items: tt.existingIngs}).
|
||||||
|
Build()
|
||||||
|
r := &HAIngressReconciler{Client: fc}
|
||||||
|
err := r.validateIngress(context.Background(), tt.ing, tt.pg)
|
||||||
if (err == nil && tt.wantErr != "") || (err != nil && err.Error() != tt.wantErr) {
|
if (err == nil && tt.wantErr != "") || (err != nil && err.Error() != tt.wantErr) {
|
||||||
t.Errorf("validateIngress() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("validateIngress() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
@ -359,6 +480,31 @@ func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status will be empty until the VIPService shows up in prefs.
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress, []networkingv1.IngressLoadBalancerIngress(nil)) {
|
||||||
|
t.Errorf("incorrect Ingress status: got %v, want empty",
|
||||||
|
ing.Status.LoadBalancer.Ingress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the VIPService to prefs to have the Ingress recognised as ready.
|
||||||
|
mustCreate(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pg-0",
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
Labels: pgSecretLabels("test-pg", "state"),
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"_current-profile": []byte("profile-foo"),
|
||||||
|
"profile-foo": []byte(`{"AdvertiseServices":["svc:my-svc"],"Config":{"NodeID":"node-foo"}}`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reconcile and re-fetch Ingress.
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKeyFromObject(ing), ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
wantStatus := []networkingv1.IngressPortStatus{
|
wantStatus := []networkingv1.IngressPortStatus{
|
||||||
{Port: 443, Protocol: "TCP"},
|
{Port: 443, Protocol: "TCP"},
|
||||||
{Port: 80, Protocol: "TCP"},
|
{Port: 80, Protocol: "TCP"},
|
||||||
@ -464,8 +610,28 @@ func verifyServeConfig(t *testing.T, fc client.Client, serviceName string, wantH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeTSClient) {
|
func verifyTailscaledConfig(t *testing.T, fc client.Client, expectedServices []string) {
|
||||||
t.Helper()
|
var expected string
|
||||||
|
if expectedServices != nil {
|
||||||
|
expectedServicesJSON, err := json.Marshal(expectedServices)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshaling expected services: %v", err)
|
||||||
|
}
|
||||||
|
expected = fmt.Sprintf(`,"AdvertiseServices":%s`, expectedServicesJSON)
|
||||||
|
}
|
||||||
|
expectEqual(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: pgConfigSecretName("test-pg", 0),
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
Labels: pgSecretLabels("test-pg", "config"),
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
tsoperator.TailscaledConfigFileName(106): []byte(fmt.Sprintf(`{"Version":""%s}`, expected)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupIngressTest(t *testing.T) (*HAIngressReconciler, client.Client, *fakeTSClient) {
|
||||||
|
|
||||||
tsIngressClass := &networkingv1.IngressClass{
|
tsIngressClass := &networkingv1.IngressClass{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
|
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
|
||||||
@ -494,9 +660,21 @@ func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeT
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-create a config Secret for the ProxyGroup
|
||||||
|
pgCfgSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: pgConfigSecretName("test-pg", 0),
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
Labels: pgSecretLabels("test-pg", "config"),
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
tsoperator.TailscaledConfigFileName(106): []byte("{}"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
fc := fake.NewClientBuilder().
|
fc := fake.NewClientBuilder().
|
||||||
WithScheme(tsapi.GlobalScheme).
|
WithScheme(tsapi.GlobalScheme).
|
||||||
WithObjects(pg, pgConfigMap, tsIngressClass).
|
WithObjects(pg, pgCfgSecret, pgConfigMap, tsIngressClass).
|
||||||
WithStatusSubresource(pg).
|
WithStatusSubresource(pg).
|
||||||
Build()
|
Build()
|
||||||
|
|
||||||
@ -511,9 +689,9 @@ func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeT
|
|||||||
if err := fc.Status().Update(context.Background(), pg); err != nil {
|
if err := fc.Status().Update(context.Background(), pg); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||||
|
|
||||||
ft := &fakeTSClient{}
|
ft := &fakeTSClient{}
|
||||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
|
||||||
zl, err := zap.NewDevelopment()
|
zl, err := zap.NewDevelopment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -527,12 +705,12 @@ func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeT
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ingPGR := &IngressPGReconciler{
|
ingPGR := &HAIngressReconciler{
|
||||||
Client: fc,
|
Client: fc,
|
||||||
tsClient: ft,
|
tsClient: ft,
|
||||||
tsnetServer: fakeTsnetServer,
|
|
||||||
defaultTags: []string{"tag:k8s"},
|
defaultTags: []string{"tag:k8s"},
|
||||||
tsNamespace: "operator-ns",
|
tsNamespace: "operator-ns",
|
||||||
|
tsnetServer: fakeTsnetServer,
|
||||||
logger: zl.Sugar(),
|
logger: zl.Sugar(),
|
||||||
recorder: record.NewFakeRecorder(10),
|
recorder: record.NewFakeRecorder(10),
|
||||||
lc: lc,
|
lc: lc,
|
||||||
@ -540,3 +718,87 @@ func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeT
|
|||||||
|
|
||||||
return ingPGR, fc, ft
|
return ingPGR, fc, ft
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIngressPGReconciler_MultiCluster(t *testing.T) {
|
||||||
|
ingPGR, fc, ft := setupIngressTest(t)
|
||||||
|
ingPGR.operatorID = "operator-1"
|
||||||
|
|
||||||
|
// Create initial Ingress
|
||||||
|
ing := &networkingv1.Ingress{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
UID: types.UID("1234-UID"),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"tailscale.com/proxy-group": "test-pg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"my-svc"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, ing)
|
||||||
|
|
||||||
|
// Simulate existing VIPService from another cluster
|
||||||
|
existingVIPSvc := &tailscale.VIPService{
|
||||||
|
Name: "svc:my-svc",
|
||||||
|
Comment: `{"ownerrefs":[{"operatorID":"operator-2"}]}`,
|
||||||
|
}
|
||||||
|
ft.vipServices = map[tailcfg.ServiceName]*tailscale.VIPService{
|
||||||
|
"svc:my-svc": existingVIPSvc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify reconciliation adds our operator reference
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
|
||||||
|
vipSvc, err := ft.GetVIPService(context.Background(), "svc:my-svc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getting VIPService: %v", err)
|
||||||
|
}
|
||||||
|
if vipSvc == nil {
|
||||||
|
t.Fatal("VIPService not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &comment{}
|
||||||
|
if err := json.Unmarshal([]byte(vipSvc.Comment), c); err != nil {
|
||||||
|
t.Fatalf("parsing comment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantOwnerRefs := []OwnerRef{
|
||||||
|
{OperatorID: "operator-2"},
|
||||||
|
{OperatorID: "operator-1"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(c.OwnerRefs, wantOwnerRefs) {
|
||||||
|
t.Errorf("incorrect owner refs\ngot: %+v\nwant: %+v", c.OwnerRefs, wantOwnerRefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the Ingress and verify VIPService still exists with one owner ref
|
||||||
|
if err := fc.Delete(context.Background(), ing); err != nil {
|
||||||
|
t.Fatalf("deleting Ingress: %v", err)
|
||||||
|
}
|
||||||
|
expectRequeue(t, ingPGR, "default", "test-ingress")
|
||||||
|
|
||||||
|
vipSvc, err = ft.GetVIPService(context.Background(), "svc:my-svc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getting VIPService after deletion: %v", err)
|
||||||
|
}
|
||||||
|
if vipSvc == nil {
|
||||||
|
t.Fatal("VIPService was incorrectly deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
c = &comment{}
|
||||||
|
if err := json.Unmarshal([]byte(vipSvc.Comment), c); err != nil {
|
||||||
|
t.Fatalf("parsing comment after deletion: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantOwnerRefs = []OwnerRef{
|
||||||
|
{OperatorID: "operator-2"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(c.OwnerRefs, wantOwnerRefs) {
|
||||||
|
t.Errorf("incorrect owner refs after deletion\ngot: %+v\nwant: %+v", c.OwnerRefs, wantOwnerRefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -73,6 +73,7 @@ func (a *IngressReconciler) Reconcile(ctx context.Context, req reconcile.Request
|
|||||||
return reconcile.Result{}, fmt.Errorf("failed to get ing: %w", err)
|
return reconcile.Result{}, fmt.Errorf("failed to get ing: %w", err)
|
||||||
}
|
}
|
||||||
if !ing.DeletionTimestamp.IsZero() || !a.shouldExpose(ing) {
|
if !ing.DeletionTimestamp.IsZero() || !a.shouldExpose(ing) {
|
||||||
|
// TODO(irbekrm): this message is confusing if the Ingress is an HA Ingress
|
||||||
logger.Debugf("ingress is being deleted or should not be exposed, cleaning up")
|
logger.Debugf("ingress is being deleted or should not be exposed, cleaning up")
|
||||||
return reconcile.Result{}, a.maybeCleanup(ctx, logger, ing)
|
return reconcile.Result{}, a.maybeCleanup(ctx, logger, ing)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -222,7 +223,7 @@ func metricsResourceName(stsName string) string {
|
|||||||
// proxy.
|
// proxy.
|
||||||
func metricsResourceLabels(opts *metricsOpts) map[string]string {
|
func metricsResourceLabels(opts *metricsOpts) map[string]string {
|
||||||
lbls := map[string]string{
|
lbls := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
labelMetricsTarget: opts.proxyStsName,
|
labelMetricsTarget: opts.proxyStsName,
|
||||||
labelPromProxyType: opts.proxyType,
|
labelPromProxyType: opts.proxyType,
|
||||||
labelPromProxyParentName: opts.proxyLabels[LabelParentName],
|
labelPromProxyParentName: opts.proxyLabels[LabelParentName],
|
||||||
|
@ -9,6 +9,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -39,6 +40,7 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
"tailscale.com/client/local"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
@ -335,14 +337,19 @@ func runReconcilers(opts reconcilerOpts) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
startlog.Fatalf("could not get local client: %v", err)
|
startlog.Fatalf("could not get local client: %v", err)
|
||||||
}
|
}
|
||||||
|
id, err := id(context.Background(), lc)
|
||||||
|
if err != nil {
|
||||||
|
startlog.Fatalf("error determining stable ID of the operator's Tailscale device: %v", err)
|
||||||
|
}
|
||||||
ingressProxyGroupFilter := handler.EnqueueRequestsFromMapFunc(ingressesFromIngressProxyGroup(mgr.GetClient(), opts.log))
|
ingressProxyGroupFilter := handler.EnqueueRequestsFromMapFunc(ingressesFromIngressProxyGroup(mgr.GetClient(), opts.log))
|
||||||
err = builder.
|
err = builder.
|
||||||
ControllerManagedBy(mgr).
|
ControllerManagedBy(mgr).
|
||||||
For(&networkingv1.Ingress{}).
|
For(&networkingv1.Ingress{}).
|
||||||
Named("ingress-pg-reconciler").
|
Named("ingress-pg-reconciler").
|
||||||
Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(serviceHandlerForIngressPG(mgr.GetClient(), startlog))).
|
Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(serviceHandlerForIngressPG(mgr.GetClient(), startlog))).
|
||||||
|
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(ingressesFromPGStateSecret(mgr.GetClient(), startlog))).
|
||||||
Watches(&tsapi.ProxyGroup{}, ingressProxyGroupFilter).
|
Watches(&tsapi.ProxyGroup{}, ingressProxyGroupFilter).
|
||||||
Complete(&IngressPGReconciler{
|
Complete(&HAIngressReconciler{
|
||||||
recorder: eventRecorder,
|
recorder: eventRecorder,
|
||||||
tsClient: opts.tsClient,
|
tsClient: opts.tsClient,
|
||||||
tsnetServer: opts.tsServer,
|
tsnetServer: opts.tsServer,
|
||||||
@ -350,6 +357,7 @@ func runReconcilers(opts reconcilerOpts) {
|
|||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
logger: opts.log.Named("ingress-pg-reconciler"),
|
logger: opts.log.Named("ingress-pg-reconciler"),
|
||||||
lc: lc,
|
lc: lc,
|
||||||
|
operatorID: id,
|
||||||
tsNamespace: opts.tailscaleNamespace,
|
tsNamespace: opts.tailscaleNamespace,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -629,8 +637,8 @@ func enqueueAllIngressEgressProxySvcsInNS(ns string, cl client.Client, logger *z
|
|||||||
|
|
||||||
// Get all headless Services for proxies configured using Service.
|
// Get all headless Services for proxies configured using Service.
|
||||||
svcProxyLabels := map[string]string{
|
svcProxyLabels := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentType: "svc",
|
LabelParentType: "svc",
|
||||||
}
|
}
|
||||||
svcHeadlessSvcList := &corev1.ServiceList{}
|
svcHeadlessSvcList := &corev1.ServiceList{}
|
||||||
if err := cl.List(ctx, svcHeadlessSvcList, client.InNamespace(ns), client.MatchingLabels(svcProxyLabels)); err != nil {
|
if err := cl.List(ctx, svcHeadlessSvcList, client.InNamespace(ns), client.MatchingLabels(svcProxyLabels)); err != nil {
|
||||||
@ -643,8 +651,8 @@ func enqueueAllIngressEgressProxySvcsInNS(ns string, cl client.Client, logger *z
|
|||||||
|
|
||||||
// Get all headless Services for proxies configured using Ingress.
|
// Get all headless Services for proxies configured using Ingress.
|
||||||
ingProxyLabels := map[string]string{
|
ingProxyLabels := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentType: "ingress",
|
LabelParentType: "ingress",
|
||||||
}
|
}
|
||||||
ingHeadlessSvcList := &corev1.ServiceList{}
|
ingHeadlessSvcList := &corev1.ServiceList{}
|
||||||
if err := cl.List(ctx, ingHeadlessSvcList, client.InNamespace(ns), client.MatchingLabels(ingProxyLabels)); err != nil {
|
if err := cl.List(ctx, ingHeadlessSvcList, client.InNamespace(ns), client.MatchingLabels(ingProxyLabels)); err != nil {
|
||||||
@ -711,7 +719,7 @@ func dnsRecordsReconcilerIngressHandler(ns string, isDefaultLoadBalancer bool, c
|
|||||||
|
|
||||||
func isManagedResource(o client.Object) bool {
|
func isManagedResource(o client.Object) bool {
|
||||||
ls := o.GetLabels()
|
ls := o.GetLabels()
|
||||||
return ls[LabelManaged] == "true"
|
return ls[kubetypes.LabelManaged] == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
func isManagedByType(o client.Object, typ string) bool {
|
func isManagedByType(o client.Object, typ string) bool {
|
||||||
@ -948,7 +956,7 @@ func egressPodsHandler(_ context.Context, o client.Object) []reconcile.Request {
|
|||||||
// returns reconciler requests for all egress EndpointSlices for that ProxyGroup.
|
// returns reconciler requests for all egress EndpointSlices for that ProxyGroup.
|
||||||
func egressEpsFromPGPods(cl client.Client, ns string) handler.MapFunc {
|
func egressEpsFromPGPods(cl client.Client, ns string) handler.MapFunc {
|
||||||
return func(_ context.Context, o client.Object) []reconcile.Request {
|
return func(_ context.Context, o client.Object) []reconcile.Request {
|
||||||
if v, ok := o.GetLabels()[LabelManaged]; !ok || v != "true" {
|
if v, ok := o.GetLabels()[kubetypes.LabelManaged]; !ok || v != "true" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO(irbekrm): for now this is good enough as all ProxyGroups are egress. Add a type check once we
|
// TODO(irbekrm): for now this is good enough as all ProxyGroups are egress. Add a type check once we
|
||||||
@ -968,15 +976,13 @@ func egressEpsFromPGPods(cl client.Client, ns string) handler.MapFunc {
|
|||||||
// returns reconciler requests for all egress EndpointSlices for that ProxyGroup.
|
// returns reconciler requests for all egress EndpointSlices for that ProxyGroup.
|
||||||
func egressEpsFromPGStateSecrets(cl client.Client, ns string) handler.MapFunc {
|
func egressEpsFromPGStateSecrets(cl client.Client, ns string) handler.MapFunc {
|
||||||
return func(_ context.Context, o client.Object) []reconcile.Request {
|
return func(_ context.Context, o client.Object) []reconcile.Request {
|
||||||
if v, ok := o.GetLabels()[LabelManaged]; !ok || v != "true" {
|
if v, ok := o.GetLabels()[kubetypes.LabelManaged]; !ok || v != "true" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO(irbekrm): for now this is good enough as all ProxyGroups are egress. Add a type check once we
|
|
||||||
// have ingress ProxyGroups.
|
|
||||||
if parentType := o.GetLabels()[LabelParentType]; parentType != "proxygroup" {
|
if parentType := o.GetLabels()[LabelParentType]; parentType != "proxygroup" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if secretType := o.GetLabels()[labelSecretType]; secretType != "state" {
|
if secretType := o.GetLabels()[kubetypes.LabelSecretType]; secretType != "state" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pg, ok := o.GetLabels()[LabelParentName]
|
pg, ok := o.GetLabels()[LabelParentName]
|
||||||
@ -993,7 +999,7 @@ func egressSvcFromEps(_ context.Context, o client.Object) []reconcile.Request {
|
|||||||
if typ := o.GetLabels()[labelSvcType]; typ != typeEgress {
|
if typ := o.GetLabels()[labelSvcType]; typ != typeEgress {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if v, ok := o.GetLabels()[LabelManaged]; !ok || v != "true" {
|
if v, ok := o.GetLabels()[kubetypes.LabelManaged]; !ok || v != "true" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
svcName, ok := o.GetLabels()[LabelParentName]
|
svcName, ok := o.GetLabels()[LabelParentName]
|
||||||
@ -1033,6 +1039,45 @@ func reconcileRequestsForPG(pg string, cl client.Client, ns string) []reconcile.
|
|||||||
return reqs
|
return reqs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ingressesFromPGStateSecret(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||||
|
return func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
secret, ok := o.(*corev1.Secret)
|
||||||
|
if !ok {
|
||||||
|
logger.Infof("[unexpected] ProxyGroup handler triggered for an object that is not a ProxyGroup")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if secret.ObjectMeta.Labels[kubetypes.LabelManaged] != "true" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if secret.ObjectMeta.Labels[LabelParentType] != "proxygroup" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if secret.ObjectMeta.Labels[kubetypes.LabelSecretType] != "state" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pgName, ok := secret.ObjectMeta.Labels[LabelParentName]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ingList := &networkingv1.IngressList{}
|
||||||
|
if err := cl.List(ctx, ingList, client.MatchingFields{indexIngressProxyGroup: pgName}); err != nil {
|
||||||
|
logger.Infof("error listing Ingresses, skipping a reconcile for event on Secret %s: %v", secret.Name, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reqs := make([]reconcile.Request, 0)
|
||||||
|
for _, ing := range ingList.Items {
|
||||||
|
reqs = append(reqs, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Namespace: ing.Namespace,
|
||||||
|
Name: ing.Name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return reqs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// egressSvcsFromEgressProxyGroup is an event handler for egress ProxyGroups. It returns reconcile requests for all
|
// egressSvcsFromEgressProxyGroup is an event handler for egress ProxyGroups. It returns reconcile requests for all
|
||||||
// user-created ExternalName Services that should be exposed on this ProxyGroup.
|
// user-created ExternalName Services that should be exposed on this ProxyGroup.
|
||||||
func egressSvcsFromEgressProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
func egressSvcsFromEgressProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||||
@ -1138,9 +1183,9 @@ func podsFromEgressEps(cl client.Client, logger *zap.SugaredLogger, ns string) h
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
podLabels := map[string]string{
|
podLabels := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentType: "proxygroup",
|
LabelParentType: "proxygroup",
|
||||||
LabelParentName: eps.Labels[labelProxyGroup],
|
LabelParentName: eps.Labels[labelProxyGroup],
|
||||||
}
|
}
|
||||||
podList := &corev1.PodList{}
|
podList := &corev1.PodList{}
|
||||||
if err := cl.List(ctx, podList, client.InNamespace(ns),
|
if err := cl.List(ctx, podList, client.InNamespace(ns),
|
||||||
@ -1262,3 +1307,14 @@ func hasProxyGroupAnnotation(obj client.Object) bool {
|
|||||||
ing := obj.(*networkingv1.Ingress)
|
ing := obj.(*networkingv1.Ingress)
|
||||||
return ing.Annotations[AnnotationProxyGroup] != ""
|
return ing.Annotations[AnnotationProxyGroup] != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func id(ctx context.Context, lc *local.Client) (string, error) {
|
||||||
|
st, err := lc.StatusWithoutPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting tailscale status: %w", err)
|
||||||
|
}
|
||||||
|
if st.Self == nil {
|
||||||
|
return "", fmt.Errorf("unexpected: device's status does not contain node's metadata")
|
||||||
|
}
|
||||||
|
return string(st.Self.ID), nil
|
||||||
|
}
|
||||||
|
@ -1387,10 +1387,10 @@ func Test_serviceHandlerForIngress(t *testing.T) {
|
|||||||
Name: "headless-1",
|
Name: "headless-1",
|
||||||
Namespace: "tailscale",
|
Namespace: "tailscale",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentName: "ing-1",
|
LabelParentName: "ing-1",
|
||||||
LabelParentNamespace: "ns-1",
|
LabelParentNamespace: "ns-1",
|
||||||
LabelParentType: "ingress",
|
LabelParentType: "ingress",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -452,7 +452,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
|
|||||||
for i := range pgReplicas(pg) {
|
for i := range pgReplicas(pg) {
|
||||||
cfgSecret := &corev1.Secret{
|
cfgSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: fmt.Sprintf("%s-%d-config", pg.Name, i),
|
Name: pgConfigSecretName(pg.Name, i),
|
||||||
Namespace: r.tsNamespace,
|
Namespace: r.tsNamespace,
|
||||||
Labels: pgSecretLabels(pg.Name, "config"),
|
Labels: pgSecretLabels(pg.Name, "config"),
|
||||||
OwnerReferences: pgOwnerReference(pg),
|
OwnerReferences: pgOwnerReference(pg),
|
||||||
@ -461,7 +461,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
|
|||||||
|
|
||||||
var existingCfgSecret *corev1.Secret // unmodified copy of secret
|
var existingCfgSecret *corev1.Secret // unmodified copy of secret
|
||||||
if err := r.Get(ctx, client.ObjectKeyFromObject(cfgSecret), cfgSecret); err == nil {
|
if err := r.Get(ctx, client.ObjectKeyFromObject(cfgSecret), cfgSecret); err == nil {
|
||||||
logger.Debugf("secret %s/%s already exists", cfgSecret.GetNamespace(), cfgSecret.GetName())
|
logger.Debugf("Secret %s/%s already exists", cfgSecret.GetNamespace(), cfgSecret.GetName())
|
||||||
existingCfgSecret = cfgSecret.DeepCopy()
|
existingCfgSecret = cfgSecret.DeepCopy()
|
||||||
} else if !apierrors.IsNotFound(err) {
|
} else if !apierrors.IsNotFound(err) {
|
||||||
return "", err
|
return "", err
|
||||||
@ -469,7 +469,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
|
|||||||
|
|
||||||
var authKey string
|
var authKey string
|
||||||
if existingCfgSecret == nil {
|
if existingCfgSecret == nil {
|
||||||
logger.Debugf("creating authkey for new ProxyGroup proxy")
|
logger.Debugf("Creating authkey for new ProxyGroup proxy")
|
||||||
tags := pg.Spec.Tags.Stringify()
|
tags := pg.Spec.Tags.Stringify()
|
||||||
if len(tags) == 0 {
|
if len(tags) == 0 {
|
||||||
tags = r.defaultTags
|
tags = r.defaultTags
|
||||||
@ -490,7 +490,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error marshalling tailscaled config: %w", err)
|
return "", fmt.Errorf("error marshalling tailscaled config: %w", err)
|
||||||
}
|
}
|
||||||
mak.Set(&cfgSecret.StringData, tsoperator.TailscaledConfigFileName(cap), string(cfgJSON))
|
mak.Set(&cfgSecret.Data, tsoperator.TailscaledConfigFileName(cap), cfgJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The config sha256 sum is a value for a hash annotation used to trigger
|
// The config sha256 sum is a value for a hash annotation used to trigger
|
||||||
@ -520,12 +520,14 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existingCfgSecret != nil {
|
if existingCfgSecret != nil {
|
||||||
logger.Debugf("patching the existing ProxyGroup config Secret %s", cfgSecret.Name)
|
if !apiequality.Semantic.DeepEqual(existingCfgSecret, cfgSecret) {
|
||||||
if err := r.Patch(ctx, cfgSecret, client.MergeFrom(existingCfgSecret)); err != nil {
|
logger.Debugf("Updating the existing ProxyGroup config Secret %s", cfgSecret.Name)
|
||||||
return "", err
|
if err := r.Update(ctx, cfgSecret); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("creating a new config Secret %s for the ProxyGroup", cfgSecret.Name)
|
logger.Debugf("Creating a new config Secret %s for the ProxyGroup", cfgSecret.Name)
|
||||||
if err := r.Create(ctx, cfgSecret); err != nil {
|
if err := r.Create(ctx, cfgSecret); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -596,10 +598,35 @@ func pgTailscaledConfig(pg *tsapi.ProxyGroup, class *tsapi.ProxyClass, idx int32
|
|||||||
conf.AuthKey = key
|
conf.AuthKey = key
|
||||||
}
|
}
|
||||||
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
||||||
|
|
||||||
|
// AdvertiseServices config is set by ingress-pg-reconciler, so make sure we
|
||||||
|
// don't overwrite it here.
|
||||||
|
if err := copyAdvertiseServicesConfig(conf, oldSecret, 106); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
capVerConfigs[106] = *conf
|
capVerConfigs[106] = *conf
|
||||||
return capVerConfigs, nil
|
return capVerConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyAdvertiseServicesConfig(conf *ipn.ConfigVAlpha, oldSecret *corev1.Secret, capVer tailcfg.CapabilityVersion) error {
|
||||||
|
if oldSecret == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldConfB := oldSecret.Data[tsoperator.TailscaledConfigFileName(capVer)]
|
||||||
|
if len(oldConfB) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldConf ipn.ConfigVAlpha
|
||||||
|
if err := json.Unmarshal(oldConfB, &oldConf); err != nil {
|
||||||
|
return fmt.Errorf("error unmarshalling existing config: %w", err)
|
||||||
|
}
|
||||||
|
conf.AdvertiseServices = oldConf.AdvertiseServices
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProxyGroupReconciler) validate(_ *tsapi.ProxyGroup) error {
|
func (r *ProxyGroupReconciler) validate(_ *tsapi.ProxyGroup) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -620,7 +647,7 @@ func (r *ProxyGroupReconciler) getNodeMetadata(ctx context.Context, pg *tsapi.Pr
|
|||||||
return nil, fmt.Errorf("unexpected secret %s was labelled as owned by the ProxyGroup %s: %w", secret.Name, pg.Name, err)
|
return nil, fmt.Errorf("unexpected secret %s was labelled as owned by the ProxyGroup %s: %w", secret.Name, pg.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, dnsName, ok, err := getNodeMetadata(ctx, &secret)
|
prefs, ok, err := getDevicePrefs(&secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -631,8 +658,8 @@ func (r *ProxyGroupReconciler) getNodeMetadata(ctx context.Context, pg *tsapi.Pr
|
|||||||
nm := nodeMetadata{
|
nm := nodeMetadata{
|
||||||
ordinal: ordinal,
|
ordinal: ordinal,
|
||||||
stateSecret: &secret,
|
stateSecret: &secret,
|
||||||
tsID: id,
|
tsID: prefs.Config.NodeID,
|
||||||
dnsName: dnsName,
|
dnsName: prefs.Config.UserProfile.LoginName,
|
||||||
}
|
}
|
||||||
pod := &corev1.Pod{}
|
pod := &corev1.Pod{}
|
||||||
if err := r.Get(ctx, client.ObjectKey{Namespace: r.tsNamespace, Name: secret.Name}, pod); err != nil && !apierrors.IsNotFound(err) {
|
if err := r.Get(ctx, client.ObjectKey{Namespace: r.tsNamespace, Name: secret.Name}, pod); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
@ -73,7 +73,7 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode string
|
|||||||
Name: fmt.Sprintf("tailscaledconfig-%d", i),
|
Name: fmt.Sprintf("tailscaledconfig-%d", i),
|
||||||
VolumeSource: corev1.VolumeSource{
|
VolumeSource: corev1.VolumeSource{
|
||||||
Secret: &corev1.SecretVolumeSource{
|
Secret: &corev1.SecretVolumeSource{
|
||||||
SecretName: fmt.Sprintf("%s-%d-config", pg.Name, i),
|
SecretName: pgConfigSecretName(pg.Name, i),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -178,7 +178,15 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode string
|
|||||||
corev1.EnvVar{
|
corev1.EnvVar{
|
||||||
Name: "TS_SERVE_CONFIG",
|
Name: "TS_SERVE_CONFIG",
|
||||||
Value: fmt.Sprintf("/etc/proxies/%s", serveConfigKey),
|
Value: fmt.Sprintf("/etc/proxies/%s", serveConfigKey),
|
||||||
})
|
},
|
||||||
|
corev1.EnvVar{
|
||||||
|
// Run proxies in cert share mode to
|
||||||
|
// ensure that only one TLS cert is
|
||||||
|
// issued for an HA Ingress.
|
||||||
|
Name: "TS_EXPERIMENTAL_CERT_SHARE",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return append(c.Env, envs...)
|
return append(c.Env, envs...)
|
||||||
}()
|
}()
|
||||||
@ -225,6 +233,13 @@ func pgRole(pg *tsapi.ProxyGroup, namespace string) *rbacv1.Role {
|
|||||||
OwnerReferences: pgOwnerReference(pg),
|
OwnerReferences: pgOwnerReference(pg),
|
||||||
},
|
},
|
||||||
Rules: []rbacv1.PolicyRule{
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{""},
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
Verbs: []string{
|
||||||
|
"list",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
APIGroups: []string{""},
|
APIGroups: []string{""},
|
||||||
Resources: []string{"secrets"},
|
Resources: []string{"secrets"},
|
||||||
@ -236,8 +251,8 @@ func pgRole(pg *tsapi.ProxyGroup, namespace string) *rbacv1.Role {
|
|||||||
ResourceNames: func() (secrets []string) {
|
ResourceNames: func() (secrets []string) {
|
||||||
for i := range pgReplicas(pg) {
|
for i := range pgReplicas(pg) {
|
||||||
secrets = append(secrets,
|
secrets = append(secrets,
|
||||||
fmt.Sprintf("%s-%d-config", pg.Name, i), // Config with auth key.
|
pgConfigSecretName(pg.Name, i), // Config with auth key.
|
||||||
fmt.Sprintf("%s-%d", pg.Name, i), // State.
|
fmt.Sprintf("%s-%d", pg.Name, i), // State.
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return secrets
|
return secrets
|
||||||
@ -318,9 +333,9 @@ func pgIngressCM(pg *tsapi.ProxyGroup, namespace string) *corev1.ConfigMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pgSecretLabels(pgName, typ string) map[string]string {
|
func pgSecretLabels(pgName, secretType string) map[string]string {
|
||||||
return pgLabels(pgName, map[string]string{
|
return pgLabels(pgName, map[string]string{
|
||||||
labelSecretType: typ, // "config" or "state".
|
kubetypes.LabelSecretType: secretType, // "config" or "state".
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +345,7 @@ func pgLabels(pgName string, customLabels map[string]string) map[string]string {
|
|||||||
l[k] = v
|
l[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
l[LabelManaged] = "true"
|
l[kubetypes.LabelManaged] = "true"
|
||||||
l[LabelParentType] = "proxygroup"
|
l[LabelParentType] = "proxygroup"
|
||||||
l[LabelParentName] = pgName
|
l[LabelParentName] = pgName
|
||||||
|
|
||||||
@ -349,6 +364,10 @@ func pgReplicas(pg *tsapi.ProxyGroup) int32 {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pgConfigSecretName(pgName string, i int32) string {
|
||||||
|
return fmt.Sprintf("%s-%d-config", pgName, i)
|
||||||
|
}
|
||||||
|
|
||||||
func pgEgressCMName(pg string) string {
|
func pgEgressCMName(pg string) string {
|
||||||
return fmt.Sprintf("%s-egress-config", pg)
|
return fmt.Sprintf("%s-egress-config", pg)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
|
"tailscale.com/ipn"
|
||||||
tsoperator "tailscale.com/k8s-operator"
|
tsoperator "tailscale.com/k8s-operator"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
"tailscale.com/kube/kubetypes"
|
"tailscale.com/kube/kubetypes"
|
||||||
@ -246,7 +247,6 @@ func TestProxyGroup(t *testing.T) {
|
|||||||
// The fake client does not clean up objects whose owner has been
|
// The fake client does not clean up objects whose owner has been
|
||||||
// deleted, so we can't test for the owned resources getting deleted.
|
// deleted, so we can't test for the owned resources getting deleted.
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyGroupTypes(t *testing.T) {
|
func TestProxyGroupTypes(t *testing.T) {
|
||||||
@ -416,6 +416,7 @@ func TestProxyGroupTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
verifyEnvVar(t, sts, "TS_INTERNAL_APP", kubetypes.AppProxyGroupIngress)
|
verifyEnvVar(t, sts, "TS_INTERNAL_APP", kubetypes.AppProxyGroupIngress)
|
||||||
verifyEnvVar(t, sts, "TS_SERVE_CONFIG", "/etc/proxies/serve-config.json")
|
verifyEnvVar(t, sts, "TS_SERVE_CONFIG", "/etc/proxies/serve-config.json")
|
||||||
|
verifyEnvVar(t, sts, "TS_EXPERIMENTAL_CERT_SHARE", "true")
|
||||||
|
|
||||||
// Verify ConfigMap volume mount
|
// Verify ConfigMap volume mount
|
||||||
cmName := fmt.Sprintf("%s-ingress-config", pg.Name)
|
cmName := fmt.Sprintf("%s-ingress-config", pg.Name)
|
||||||
@ -446,6 +447,77 @@ func TestProxyGroupTypes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIngressAdvertiseServicesConfigPreserved(t *testing.T) {
|
||||||
|
fc := fake.NewClientBuilder().
|
||||||
|
WithScheme(tsapi.GlobalScheme).
|
||||||
|
Build()
|
||||||
|
reconciler := &ProxyGroupReconciler{
|
||||||
|
tsNamespace: tsNamespace,
|
||||||
|
proxyImage: testProxyImage,
|
||||||
|
Client: fc,
|
||||||
|
l: zap.Must(zap.NewDevelopment()).Sugar(),
|
||||||
|
tsClient: &fakeTSClient{},
|
||||||
|
clock: tstest.NewClock(tstest.ClockOpts{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
existingServices := []string{"svc1", "svc2"}
|
||||||
|
existingConfigBytes, err := json.Marshal(ipn.ConfigVAlpha{
|
||||||
|
AdvertiseServices: existingServices,
|
||||||
|
Version: "should-get-overwritten",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pgName = "test-ingress"
|
||||||
|
mustCreate(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: pgConfigSecretName(pgName, 0),
|
||||||
|
Namespace: tsNamespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
tsoperator.TailscaledConfigFileName(106): existingConfigBytes,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
mustCreate(t, fc, &tsapi.ProxyGroup{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: pgName,
|
||||||
|
UID: "test-ingress-uid",
|
||||||
|
},
|
||||||
|
Spec: tsapi.ProxyGroupSpec{
|
||||||
|
Type: tsapi.ProxyGroupTypeIngress,
|
||||||
|
Replicas: ptr.To[int32](1),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectReconciled(t, reconciler, "", pgName)
|
||||||
|
|
||||||
|
expectedConfigBytes, err := json.Marshal(ipn.ConfigVAlpha{
|
||||||
|
// Preserved.
|
||||||
|
AdvertiseServices: existingServices,
|
||||||
|
|
||||||
|
// Everything else got updated in the reconcile:
|
||||||
|
Version: "alpha0",
|
||||||
|
AcceptDNS: "false",
|
||||||
|
AcceptRoutes: "false",
|
||||||
|
Locked: "false",
|
||||||
|
Hostname: ptr.To(fmt.Sprintf("%s-%d", pgName, 0)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expectEqual(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: pgConfigSecretName(pgName, 0),
|
||||||
|
Namespace: tsNamespace,
|
||||||
|
ResourceVersion: "2",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
tsoperator.TailscaledConfigFileName(106): expectedConfigBytes,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func verifyProxyGroupCounts(t *testing.T, r *ProxyGroupReconciler, wantIngress, wantEgress int) {
|
func verifyProxyGroupCounts(t *testing.T, r *ProxyGroupReconciler, wantIngress, wantEgress int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if r.ingressProxyGroups.Len() != wantIngress {
|
if r.ingressProxyGroups.Len() != wantIngress {
|
||||||
@ -501,7 +573,7 @@ func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.Prox
|
|||||||
for i := range pgReplicas(pg) {
|
for i := range pgReplicas(pg) {
|
||||||
expectedSecrets = append(expectedSecrets,
|
expectedSecrets = append(expectedSecrets,
|
||||||
fmt.Sprintf("%s-%d", pg.Name, i),
|
fmt.Sprintf("%s-%d", pg.Name, i),
|
||||||
fmt.Sprintf("%s-%d-config", pg.Name, i),
|
pgConfigSecretName(pg.Name, i),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,11 +44,9 @@ const (
|
|||||||
// Labels that the operator sets on StatefulSets and Pods. If you add a
|
// Labels that the operator sets on StatefulSets and Pods. If you add a
|
||||||
// new label here, do also add it to tailscaleManagedLabels var to
|
// new label here, do also add it to tailscaleManagedLabels var to
|
||||||
// ensure that it does not get overwritten by ProxyClass configuration.
|
// ensure that it does not get overwritten by ProxyClass configuration.
|
||||||
LabelManaged = "tailscale.com/managed"
|
|
||||||
LabelParentType = "tailscale.com/parent-resource-type"
|
LabelParentType = "tailscale.com/parent-resource-type"
|
||||||
LabelParentName = "tailscale.com/parent-resource"
|
LabelParentName = "tailscale.com/parent-resource"
|
||||||
LabelParentNamespace = "tailscale.com/parent-resource-ns"
|
LabelParentNamespace = "tailscale.com/parent-resource-ns"
|
||||||
labelSecretType = "tailscale.com/secret-type" // "config" or "state".
|
|
||||||
|
|
||||||
// LabelProxyClass can be set by users on tailscale Ingresses and Services that define cluster ingress or
|
// LabelProxyClass can be set by users on tailscale Ingresses and Services that define cluster ingress or
|
||||||
// cluster egress, to specify that configuration in this ProxyClass should be applied to resources created for
|
// cluster egress, to specify that configuration in this ProxyClass should be applied to resources created for
|
||||||
@ -108,7 +106,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// tailscaleManagedLabels are label keys that tailscale operator sets on StatefulSets and Pods.
|
// tailscaleManagedLabels are label keys that tailscale operator sets on StatefulSets and Pods.
|
||||||
tailscaleManagedLabels = []string{LabelManaged, LabelParentType, LabelParentName, LabelParentNamespace, "app"}
|
tailscaleManagedLabels = []string{kubetypes.LabelManaged, LabelParentType, LabelParentName, LabelParentNamespace, "app"}
|
||||||
// tailscaleManagedAnnotations are annotation keys that tailscale operator sets on StatefulSets and Pods.
|
// tailscaleManagedAnnotations are annotation keys that tailscale operator sets on StatefulSets and Pods.
|
||||||
tailscaleManagedAnnotations = []string{podAnnotationLastSetClusterIP, podAnnotationLastSetTailnetTargetIP, podAnnotationLastSetTailnetTargetFQDN, podAnnotationLastSetConfigFileHash}
|
tailscaleManagedAnnotations = []string{podAnnotationLastSetClusterIP, podAnnotationLastSetTailnetTargetIP, podAnnotationLastSetTailnetTargetFQDN, podAnnotationLastSetConfigFileHash}
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -156,8 +157,8 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
|
|||||||
// Set a couple additional fields so we can test that we don't
|
// Set a couple additional fields so we can test that we don't
|
||||||
// mistakenly override those.
|
// mistakenly override those.
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentName: "foo",
|
LabelParentName: "foo",
|
||||||
}
|
}
|
||||||
annots := map[string]string{
|
annots := map[string]string{
|
||||||
podAnnotationLastSetClusterIP: "1.2.3.4",
|
podAnnotationLastSetClusterIP: "1.2.3.4",
|
||||||
@ -303,28 +304,28 @@ func Test_mergeStatefulSetLabelsOrAnnots(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no custom labels specified and none present in current labels, return current labels",
|
name: "no custom labels specified and none present in current labels, return current labels",
|
||||||
current: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
current: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
want: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
want: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
managed: tailscaleManagedLabels,
|
managed: tailscaleManagedLabels,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no custom labels specified, but some present in current labels, return tailscale managed labels only from the current labels",
|
name: "no custom labels specified, but some present in current labels, return tailscale managed labels only from the current labels",
|
||||||
current: map[string]string{"foo": "bar", "something.io/foo": "bar", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
current: map[string]string{"foo": "bar", "something.io/foo": "bar", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
want: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
want: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
managed: tailscaleManagedLabels,
|
managed: tailscaleManagedLabels,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "custom labels specified, current labels only contain tailscale managed labels, return a union of both",
|
name: "custom labels specified, current labels only contain tailscale managed labels, return a union of both",
|
||||||
current: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
current: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
want: map[string]string{"foo": "bar", "something.io/foo": "bar", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
managed: tailscaleManagedLabels,
|
managed: tailscaleManagedLabels,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "custom labels specified, current labels contain tailscale managed labels and custom labels, some of which re not present in the new custom labels, return a union of managed labels and the desired custom labels",
|
name: "custom labels specified, current labels contain tailscale managed labels and custom labels, some of which re not present in the new custom labels, return a union of managed labels and the desired custom labels",
|
||||||
current: map[string]string{"foo": "bar", "bar": "baz", "app": "1234", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
current: map[string]string{"foo": "bar", "bar": "baz", "app": "1234", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar", "app": "1234", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
want: map[string]string{"foo": "bar", "something.io/foo": "bar", "app": "1234", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||||
managed: tailscaleManagedLabels,
|
managed: tailscaleManagedLabels,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -84,10 +84,10 @@ func childResourceLabels(name, ns, typ string) map[string]string {
|
|||||||
// proxying. Instead, we have to do our own filtering and tracking with
|
// proxying. Instead, we have to do our own filtering and tracking with
|
||||||
// labels.
|
// labels.
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentName: name,
|
LabelParentName: name,
|
||||||
LabelParentNamespace: ns,
|
LabelParentNamespace: ns,
|
||||||
LabelParentType: typ,
|
LabelParentType: typ,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
@ -563,10 +564,10 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
|
|||||||
func findGenName(t *testing.T, client client.Client, ns, name, typ string) (full, noSuffix string) {
|
func findGenName(t *testing.T, client client.Client, ns, name, typ string) (full, noSuffix string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
LabelManaged: "true",
|
kubetypes.LabelManaged: "true",
|
||||||
LabelParentName: name,
|
LabelParentName: name,
|
||||||
LabelParentNamespace: ns,
|
LabelParentNamespace: ns,
|
||||||
LabelParentType: typ,
|
LabelParentType: typ,
|
||||||
}
|
}
|
||||||
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
|
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -230,7 +230,7 @@ func (r *RecorderReconciler) maybeProvision(ctx context.Context, tsr *tsapi.Reco
|
|||||||
func (r *RecorderReconciler) maybeCleanup(ctx context.Context, tsr *tsapi.Recorder) (bool, error) {
|
func (r *RecorderReconciler) maybeCleanup(ctx context.Context, tsr *tsapi.Recorder) (bool, error) {
|
||||||
logger := r.logger(tsr.Name)
|
logger := r.logger(tsr.Name)
|
||||||
|
|
||||||
id, _, ok, err := r.getNodeMetadata(ctx, tsr.Name)
|
prefs, ok, err := r.getDevicePrefs(ctx, tsr.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -243,6 +243,7 @@ func (r *RecorderReconciler) maybeCleanup(ctx context.Context, tsr *tsapi.Record
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id := string(prefs.Config.NodeID)
|
||||||
logger.Debugf("deleting device %s from control", string(id))
|
logger.Debugf("deleting device %s from control", string(id))
|
||||||
if err := r.tsClient.DeleteDevice(ctx, string(id)); err != nil {
|
if err := r.tsClient.DeleteDevice(ctx, string(id)); err != nil {
|
||||||
errResp := &tailscale.ErrResponse{}
|
errResp := &tailscale.ErrResponse{}
|
||||||
@ -327,34 +328,33 @@ func (r *RecorderReconciler) getStateSecret(ctx context.Context, tsrName string)
|
|||||||
return secret, nil
|
return secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecorderReconciler) getNodeMetadata(ctx context.Context, tsrName string) (id tailcfg.StableNodeID, dnsName string, ok bool, err error) {
|
func (r *RecorderReconciler) getDevicePrefs(ctx context.Context, tsrName string) (prefs prefs, ok bool, err error) {
|
||||||
secret, err := r.getStateSecret(ctx, tsrName)
|
secret, err := r.getStateSecret(ctx, tsrName)
|
||||||
if err != nil || secret == nil {
|
if err != nil || secret == nil {
|
||||||
return "", "", false, err
|
return prefs, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNodeMetadata(ctx, secret)
|
return getDevicePrefs(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNodeMetadata returns 'ok == true' iff the node ID is found. The dnsName
|
// getDevicePrefs returns 'ok == true' iff the node ID is found. The dnsName
|
||||||
// is expected to always be non-empty if the node ID is, but not required.
|
// is expected to always be non-empty if the node ID is, but not required.
|
||||||
func getNodeMetadata(ctx context.Context, secret *corev1.Secret) (id tailcfg.StableNodeID, dnsName string, ok bool, err error) {
|
func getDevicePrefs(secret *corev1.Secret) (prefs prefs, ok bool, err error) {
|
||||||
// TODO(tomhjp): Should maybe use ipn to parse the following info instead.
|
// TODO(tomhjp): Should maybe use ipn to parse the following info instead.
|
||||||
currentProfile, ok := secret.Data[currentProfileKey]
|
currentProfile, ok := secret.Data[currentProfileKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", false, nil
|
return prefs, false, nil
|
||||||
}
|
}
|
||||||
profileBytes, ok := secret.Data[string(currentProfile)]
|
profileBytes, ok := secret.Data[string(currentProfile)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", false, nil
|
return prefs, false, nil
|
||||||
}
|
}
|
||||||
var profile profile
|
if err := json.Unmarshal(profileBytes, &prefs); err != nil {
|
||||||
if err := json.Unmarshal(profileBytes, &profile); err != nil {
|
return prefs, false, fmt.Errorf("failed to extract node profile info from state Secret %s: %w", secret.Name, err)
|
||||||
return "", "", false, fmt.Errorf("failed to extract node profile info from state Secret %s: %w", secret.Name, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = profile.Config.NodeID != ""
|
ok = prefs.Config.NodeID != ""
|
||||||
return tailcfg.StableNodeID(profile.Config.NodeID), profile.Config.UserProfile.LoginName, ok, nil
|
return prefs, ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecorderReconciler) getDeviceInfo(ctx context.Context, tsrName string) (d tsapi.RecorderTailnetDevice, ok bool, err error) {
|
func (r *RecorderReconciler) getDeviceInfo(ctx context.Context, tsrName string) (d tsapi.RecorderTailnetDevice, ok bool, err error) {
|
||||||
@ -367,14 +367,14 @@ func (r *RecorderReconciler) getDeviceInfo(ctx context.Context, tsrName string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDeviceInfo(ctx context.Context, tsClient tsClient, secret *corev1.Secret) (d tsapi.RecorderTailnetDevice, ok bool, err error) {
|
func getDeviceInfo(ctx context.Context, tsClient tsClient, secret *corev1.Secret) (d tsapi.RecorderTailnetDevice, ok bool, err error) {
|
||||||
nodeID, dnsName, ok, err := getNodeMetadata(ctx, secret)
|
prefs, ok, err := getDevicePrefs(secret)
|
||||||
if !ok || err != nil {
|
if !ok || err != nil {
|
||||||
return tsapi.RecorderTailnetDevice{}, false, err
|
return tsapi.RecorderTailnetDevice{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(tomhjp): The profile info doesn't include addresses, which is why we
|
// TODO(tomhjp): The profile info doesn't include addresses, which is why we
|
||||||
// need the API. Should we instead update the profile to include addresses?
|
// need the API. Should we instead update the profile to include addresses?
|
||||||
device, err := tsClient.Device(ctx, string(nodeID), nil)
|
device, err := tsClient.Device(ctx, string(prefs.Config.NodeID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tsapi.RecorderTailnetDevice{}, false, fmt.Errorf("failed to get device info from API: %w", err)
|
return tsapi.RecorderTailnetDevice{}, false, fmt.Errorf("failed to get device info from API: %w", err)
|
||||||
}
|
}
|
||||||
@ -383,20 +383,25 @@ func getDeviceInfo(ctx context.Context, tsClient tsClient, secret *corev1.Secret
|
|||||||
Hostname: device.Hostname,
|
Hostname: device.Hostname,
|
||||||
TailnetIPs: device.Addresses,
|
TailnetIPs: device.Addresses,
|
||||||
}
|
}
|
||||||
if dnsName != "" {
|
if dnsName := prefs.Config.UserProfile.LoginName; dnsName != "" {
|
||||||
d.URL = fmt.Sprintf("https://%s", dnsName)
|
d.URL = fmt.Sprintf("https://%s", dnsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return d, true, nil
|
return d, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type profile struct {
|
// [prefs] is a subset of the ipn.Prefs struct used for extracting information
|
||||||
|
// from the state Secret of Tailscale devices.
|
||||||
|
type prefs struct {
|
||||||
Config struct {
|
Config struct {
|
||||||
NodeID string `json:"NodeID"`
|
NodeID tailcfg.StableNodeID `json:"NodeID"`
|
||||||
UserProfile struct {
|
UserProfile struct {
|
||||||
|
// LoginName is the MagicDNS name of the device, e.g. foo.tail-scale.ts.net.
|
||||||
LoginName string `json:"LoginName"`
|
LoginName string `json:"LoginName"`
|
||||||
} `json:"UserProfile"`
|
} `json:"UserProfile"`
|
||||||
} `json:"Config"`
|
} `json:"Config"`
|
||||||
|
|
||||||
|
AdvertiseServices []string `json:"AdvertiseServices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func markedForDeletion(obj metav1.Object) bool {
|
func markedForDeletion(obj metav1.Object) bool {
|
||||||
|
@ -41,6 +41,8 @@ import (
|
|||||||
"tailscale.com/wgengine/netstack"
|
"tailscale.com/wgengine/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNoIPsAvailable = errors.New("no IPs available")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
hostinfo.SetApp("natc")
|
hostinfo.SetApp("natc")
|
||||||
if !envknob.UseWIPCode() {
|
if !envknob.UseWIPCode() {
|
||||||
@ -277,14 +279,14 @@ func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDP
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
who, err := c.lc.WhoIs(ctx, remoteAddr.String())
|
who, err := c.lc.WhoIs(ctx, remoteAddr.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: WhoIs failed: %v\n", err)
|
log.Printf("HandleDNS(remote=%s): WhoIs failed: %v\n", remoteAddr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg dnsmessage.Message
|
var msg dnsmessage.Message
|
||||||
err = msg.Unpack(buf)
|
err = msg.Unpack(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
|
log.Printf("HandleDNS(remote=%s): dnsmessage unpack failed: %v\n", remoteAddr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,19 +299,19 @@ func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDP
|
|||||||
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
|
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
|
||||||
dstAddrs, err := lookupDestinationIP(q.Name.String())
|
dstAddrs, err := lookupDestinationIP(q.Name.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: lookup destination failed: %v\n ", err)
|
log.Printf("HandleDNS(remote=%s): lookup destination failed: %v\n", remoteAddr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.ignoreDestination(dstAddrs) {
|
if c.ignoreDestination(dstAddrs) {
|
||||||
bs, err := dnsResponse(&msg, dstAddrs)
|
bs, err := dnsResponse(&msg, dstAddrs)
|
||||||
// TODO (fran): treat as SERVFAIL
|
// TODO (fran): treat as SERVFAIL
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: generate ignore response failed: %v\n", err)
|
log.Printf("HandleDNS(remote=%s): generate ignore response failed: %v\n", remoteAddr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = pc.WriteTo(bs, remoteAddr)
|
_, err = pc.WriteTo(bs, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: write failed: %v\n", err)
|
log.Printf("HandleDNS(remote=%s): write failed: %v\n", remoteAddr.String(), err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -322,7 +324,7 @@ func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDP
|
|||||||
resp, err := c.generateDNSResponse(&msg, who.Node.ID)
|
resp, err := c.generateDNSResponse(&msg, who.Node.ID)
|
||||||
// TODO (fran): treat as SERVFAIL
|
// TODO (fran): treat as SERVFAIL
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: connector handling failed: %v\n", err)
|
log.Printf("HandleDNS(remote=%s): connector handling failed: %v\n", remoteAddr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO (fran): treat as NXDOMAIN
|
// TODO (fran): treat as NXDOMAIN
|
||||||
@ -332,7 +334,7 @@ func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDP
|
|||||||
// This connector handled the DNS request
|
// This connector handled the DNS request
|
||||||
_, err = pc.WriteTo(resp, remoteAddr)
|
_, err = pc.WriteTo(resp, remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("HandleDNS: write failed: %v\n", err)
|
log.Printf("HandleDNS(remote=%s): write failed: %v\n", remoteAddr.String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,6 +531,9 @@ func (ps *perPeerState) ipForDomain(domain string) ([]netip.Addr, error) {
|
|||||||
return addrs, nil
|
return addrs, nil
|
||||||
}
|
}
|
||||||
addrs := ps.assignAddrsLocked(domain)
|
addrs := ps.assignAddrsLocked(domain)
|
||||||
|
if addrs == nil {
|
||||||
|
return nil, ErrNoIPsAvailable
|
||||||
|
}
|
||||||
return addrs, nil
|
return addrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,6 +580,9 @@ func (ps *perPeerState) assignAddrsLocked(domain string) []netip.Addr {
|
|||||||
ps.addrToDomain = &bart.Table[string]{}
|
ps.addrToDomain = &bart.Table[string]{}
|
||||||
}
|
}
|
||||||
v4 := ps.unusedIPv4Locked()
|
v4 := ps.unusedIPv4Locked()
|
||||||
|
if !v4.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
as16 := ps.c.v6ULA.Addr().As16()
|
as16 := ps.c.v6ULA.Addr().As16()
|
||||||
as4 := v4.As4()
|
as4 := v4.As4()
|
||||||
copy(as16[12:], as4[:])
|
copy(as16[12:], as4[:])
|
||||||
|
@ -19,8 +19,25 @@
|
|||||||
// header_property = username
|
// header_property = username
|
||||||
// auto_sign_up = true
|
// auto_sign_up = true
|
||||||
// whitelist = 127.0.0.1
|
// whitelist = 127.0.0.1
|
||||||
// headers = Name:X-WEBAUTH-NAME
|
// headers = Email:X-Webauth-User, Name:X-Webauth-Name, Role:X-Webauth-Role
|
||||||
// enable_login_token = true
|
// enable_login_token = true
|
||||||
|
//
|
||||||
|
// You can use grants in Tailscale ACL to give users different roles in Grafana.
|
||||||
|
// For example, to give group:eng the Editor role, add the following to your ACLs:
|
||||||
|
//
|
||||||
|
// "grants": [
|
||||||
|
// {
|
||||||
|
// "src": ["group:eng"],
|
||||||
|
// "dst": ["tag:grafana"],
|
||||||
|
// "app": {
|
||||||
|
// "tailscale.com/cap/proxy-to-grafana": [{
|
||||||
|
// "role": "editor",
|
||||||
|
// }],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// If multiple roles are specified, the most permissive role is used.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -49,6 +66,57 @@ var (
|
|||||||
loginServer = flag.String("login-server", "", "URL to alternative control server. If empty, the default Tailscale control is used.")
|
loginServer = flag.String("login-server", "", "URL to alternative control server. If empty, the default Tailscale control is used.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// aclCap is the Tailscale ACL capability used to configure proxy-to-grafana.
|
||||||
|
const aclCap tailcfg.PeerCapability = "tailscale.com/cap/proxy-to-grafana"
|
||||||
|
|
||||||
|
// aclGrant is an access control rule that assigns Grafana permissions
|
||||||
|
// while provisioning a user.
|
||||||
|
type aclGrant struct {
|
||||||
|
// Role is one of: "viewer", "editor", "admin".
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// grafanaRole defines possible Grafana roles.
|
||||||
|
type grafanaRole int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Roles are ordered by their permissions, with the least permissive role first.
|
||||||
|
// If a user has multiple roles, the most permissive role is used.
|
||||||
|
ViewerRole grafanaRole = iota
|
||||||
|
EditorRole
|
||||||
|
AdminRole
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of a grafanaRole.
|
||||||
|
// It is used as a header value in the HTTP request to Grafana.
|
||||||
|
func (r grafanaRole) String() string {
|
||||||
|
switch r {
|
||||||
|
case ViewerRole:
|
||||||
|
return "Viewer"
|
||||||
|
case EditorRole:
|
||||||
|
return "Editor"
|
||||||
|
case AdminRole:
|
||||||
|
return "Admin"
|
||||||
|
default:
|
||||||
|
// A safe default.
|
||||||
|
return "Viewer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// roleFromString converts a string to a grafanaRole.
|
||||||
|
// It is used to parse the role from the ACL grant.
|
||||||
|
func roleFromString(s string) (grafanaRole, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "viewer":
|
||||||
|
return ViewerRole, nil
|
||||||
|
case "editor":
|
||||||
|
return EditorRole, nil
|
||||||
|
case "admin":
|
||||||
|
return AdminRole, nil
|
||||||
|
}
|
||||||
|
return ViewerRole, fmt.Errorf("unknown role: %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *hostname == "" || strings.Contains(*hostname, ".") {
|
if *hostname == "" || strings.Contains(*hostname, ".") {
|
||||||
@ -134,7 +202,15 @@ func modifyRequest(req *http.Request, localClient *local.Client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := getTailscaleUser(req.Context(), localClient, req.RemoteAddr)
|
// Delete any existing X-Webauth-* headers to prevent possible spoofing
|
||||||
|
// if getting Tailnet identity fails.
|
||||||
|
for h := range req.Header {
|
||||||
|
if strings.HasPrefix(h, "X-Webauth-") {
|
||||||
|
req.Header.Del(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, role, err := getTailscaleIdentity(req.Context(), localClient, req.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error getting Tailscale user: %v", err)
|
log.Printf("error getting Tailscale user: %v", err)
|
||||||
return
|
return
|
||||||
@ -142,19 +218,33 @@ func modifyRequest(req *http.Request, localClient *local.Client) {
|
|||||||
|
|
||||||
req.Header.Set("X-Webauth-User", user.LoginName)
|
req.Header.Set("X-Webauth-User", user.LoginName)
|
||||||
req.Header.Set("X-Webauth-Name", user.DisplayName)
|
req.Header.Set("X-Webauth-Name", user.DisplayName)
|
||||||
|
req.Header.Set("X-Webauth-Role", role.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTailscaleUser(ctx context.Context, localClient *local.Client, ipPort string) (*tailcfg.UserProfile, error) {
|
func getTailscaleIdentity(ctx context.Context, localClient *local.Client, ipPort string) (*tailcfg.UserProfile, grafanaRole, error) {
|
||||||
whois, err := localClient.WhoIs(ctx, ipPort)
|
whois, err := localClient.WhoIs(ctx, ipPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
return nil, ViewerRole, fmt.Errorf("failed to identify remote host: %w", err)
|
||||||
}
|
}
|
||||||
if whois.Node.IsTagged() {
|
if whois.Node.IsTagged() {
|
||||||
return nil, fmt.Errorf("tagged nodes are not users")
|
return nil, ViewerRole, fmt.Errorf("tagged nodes are not users")
|
||||||
}
|
}
|
||||||
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
||||||
return nil, fmt.Errorf("failed to identify remote user")
|
return nil, ViewerRole, fmt.Errorf("failed to identify remote user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return whois.UserProfile, nil
|
role := ViewerRole
|
||||||
|
grants, err := tailcfg.UnmarshalCapJSON[aclGrant](whois.CapMap, aclCap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ViewerRole, fmt.Errorf("failed to unmarshal ACL grants: %w", err)
|
||||||
|
}
|
||||||
|
for _, g := range grants {
|
||||||
|
r, err := roleFromString(g.Role)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ViewerRole, fmt.Errorf("failed to parse role: %w", err)
|
||||||
|
}
|
||||||
|
role = max(role, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return whois.UserProfile, role, nil
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||||
tailscale.com from tailscale.com/version
|
tailscale.com from tailscale.com/version
|
||||||
tailscale.com/envknob from tailscale.com/tsweb+
|
tailscale.com/envknob from tailscale.com/tsweb+
|
||||||
|
tailscale.com/feature from tailscale.com/tsweb
|
||||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||||
tailscale.com/metrics from tailscale.com/net/stunserver+
|
tailscale.com/metrics from tailscale.com/net/stunserver+
|
||||||
tailscale.com/net/netaddr from tailscale.com/net/tsaddr
|
tailscale.com/net/netaddr from tailscale.com/net/tsaddr
|
||||||
@ -57,8 +58,8 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
tailscale.com/net/tsaddr from tailscale.com/tsweb
|
tailscale.com/net/tsaddr from tailscale.com/tsweb
|
||||||
tailscale.com/syncs from tailscale.com/metrics
|
tailscale.com/syncs from tailscale.com/metrics
|
||||||
tailscale.com/tailcfg from tailscale.com/version
|
tailscale.com/tailcfg from tailscale.com/version
|
||||||
tailscale.com/tsweb from tailscale.com/cmd/stund
|
tailscale.com/tsweb from tailscale.com/cmd/stund+
|
||||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
|
tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund
|
||||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||||
tailscale.com/types/ipproto from tailscale.com/tailcfg
|
tailscale.com/types/ipproto from tailscale.com/tailcfg
|
||||||
@ -194,7 +195,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from net/http/pprof+
|
html from net/http/pprof+
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from syscall+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
internal/bytealg from bytes+
|
internal/bytealg from bytes+
|
||||||
internal/byteorder from crypto/cipher+
|
internal/byteorder from crypto/cipher+
|
||||||
@ -204,12 +205,12 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
internal/filepathlite from os+
|
internal/filepathlite from os+
|
||||||
internal/fmtsort from fmt
|
internal/fmtsort from fmt
|
||||||
internal/goarch from crypto/internal/fips140deps/cpu+
|
internal/goarch from crypto/internal/fips140deps/cpu+
|
||||||
internal/godebug from crypto/tls+
|
internal/godebug from crypto/internal/fips140deps/godebug+
|
||||||
internal/godebugs from internal/godebug+
|
internal/godebugs from internal/godebug+
|
||||||
internal/goexperiment from runtime+
|
internal/goexperiment from hash/maphash+
|
||||||
internal/goos from crypto/x509+
|
internal/goos from crypto/x509+
|
||||||
internal/itoa from internal/poll+
|
internal/itoa from internal/poll+
|
||||||
internal/msan from syscall+
|
internal/msan from internal/runtime/maps+
|
||||||
internal/nettrace from net+
|
internal/nettrace from net+
|
||||||
internal/oserror from io/fs+
|
internal/oserror from io/fs+
|
||||||
internal/poll from net+
|
internal/poll from net+
|
||||||
|
@ -15,6 +15,9 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/net/stunserver"
|
"tailscale.com/net/stunserver"
|
||||||
"tailscale.com/tsweb"
|
"tailscale.com/tsweb"
|
||||||
|
|
||||||
|
// Support for prometheus varz in tsweb
|
||||||
|
_ "tailscale.com/tsweb/promvarz"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -333,7 +333,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
image/color from github.com/skip2/go-qrcode+
|
image/color from github.com/skip2/go-qrcode+
|
||||||
image/png from github.com/skip2/go-qrcode
|
image/png from github.com/skip2/go-qrcode
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from syscall+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
internal/bytealg from bytes+
|
internal/bytealg from bytes+
|
||||||
internal/byteorder from crypto/cipher+
|
internal/byteorder from crypto/cipher+
|
||||||
@ -345,10 +345,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
internal/goarch from crypto/internal/fips140deps/cpu+
|
internal/goarch from crypto/internal/fips140deps/cpu+
|
||||||
internal/godebug from archive/tar+
|
internal/godebug from archive/tar+
|
||||||
internal/godebugs from internal/godebug+
|
internal/godebugs from internal/godebug+
|
||||||
internal/goexperiment from runtime+
|
internal/goexperiment from hash/maphash+
|
||||||
internal/goos from crypto/x509+
|
internal/goos from crypto/x509+
|
||||||
internal/itoa from internal/poll+
|
internal/itoa from internal/poll+
|
||||||
internal/msan from syscall+
|
internal/msan from internal/runtime/maps+
|
||||||
internal/nettrace from net+
|
internal/nettrace from net+
|
||||||
internal/oserror from io/fs+
|
internal/oserror from io/fs+
|
||||||
internal/poll from net+
|
internal/poll from net+
|
||||||
|
@ -271,6 +271,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||||
tailscale.com/ipn from tailscale.com/client/local+
|
tailscale.com/ipn from tailscale.com/client/local+
|
||||||
|
tailscale.com/ipn/auditlog from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||||
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
|
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
|
||||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||||
@ -285,7 +286,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||||
L tailscale.com/kube/kubeapi from tailscale.com/ipn/store/kubestore+
|
L tailscale.com/kube/kubeapi from tailscale.com/ipn/store/kubestore+
|
||||||
L tailscale.com/kube/kubeclient from tailscale.com/ipn/store/kubestore
|
L tailscale.com/kube/kubeclient from tailscale.com/ipn/store/kubestore
|
||||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
tailscale.com/kube/kubetypes from tailscale.com/envknob+
|
||||||
tailscale.com/licenses from tailscale.com/client/web
|
tailscale.com/licenses from tailscale.com/client/web
|
||||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||||
@ -588,7 +589,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
html from html/template+
|
html from html/template+
|
||||||
html/template from github.com/gorilla/csrf
|
html/template from github.com/gorilla/csrf
|
||||||
internal/abi from crypto/x509/internal/macos+
|
internal/abi from crypto/x509/internal/macos+
|
||||||
internal/asan from syscall+
|
internal/asan from internal/runtime/maps+
|
||||||
internal/bisect from internal/godebug
|
internal/bisect from internal/godebug
|
||||||
internal/bytealg from bytes+
|
internal/bytealg from bytes+
|
||||||
internal/byteorder from crypto/cipher+
|
internal/byteorder from crypto/cipher+
|
||||||
@ -600,10 +601,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
internal/goarch from crypto/internal/fips140deps/cpu+
|
internal/goarch from crypto/internal/fips140deps/cpu+
|
||||||
internal/godebug from archive/tar+
|
internal/godebug from archive/tar+
|
||||||
internal/godebugs from internal/godebug+
|
internal/godebugs from internal/godebug+
|
||||||
internal/goexperiment from runtime+
|
internal/goexperiment from hash/maphash+
|
||||||
internal/goos from crypto/x509+
|
internal/goos from crypto/x509+
|
||||||
internal/itoa from internal/poll+
|
internal/itoa from internal/poll+
|
||||||
internal/msan from syscall+
|
internal/msan from internal/runtime/maps+
|
||||||
internal/nettrace from net+
|
internal/nettrace from net+
|
||||||
internal/oserror from io/fs+
|
internal/oserror from io/fs+
|
||||||
internal/poll from net+
|
internal/poll from net+
|
||||||
|
@ -141,7 +141,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
|
|||||||
}
|
}
|
||||||
outcome := goOutput.Action
|
outcome := goOutput.Action
|
||||||
if outcome == "build-fail" {
|
if outcome == "build-fail" {
|
||||||
outcome = "FAIL"
|
outcome = "fail"
|
||||||
}
|
}
|
||||||
pkgTests[""].logs.WriteString(goOutput.Output)
|
pkgTests[""].logs.WriteString(goOutput.Output)
|
||||||
ch <- &testAttempt{
|
ch <- &testAttempt{
|
||||||
@ -152,7 +152,15 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
|
|||||||
logs: pkgTests[""].logs,
|
logs: pkgTests[""].logs,
|
||||||
pkgFinished: true,
|
pkgFinished: true,
|
||||||
}
|
}
|
||||||
|
case "output":
|
||||||
|
// Capture all output from the package except for the final
|
||||||
|
// "FAIL tailscale.io/control 0.684s" line, as
|
||||||
|
// printPkgOutcome will output a similar line
|
||||||
|
if !strings.HasPrefix(goOutput.Output, fmt.Sprintf("FAIL\t%s\t", goOutput.Package)) {
|
||||||
|
pkgTests[""].logs.WriteString(goOutput.Output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
testName := goOutput.Test
|
testName := goOutput.Test
|
||||||
@ -251,6 +259,7 @@ func main() {
|
|||||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\nflakytest failures JSON: %s\n\n", thisRun.attempt, j)
|
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\nflakytest failures JSON: %s\n\n", thisRun.attempt, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fatalFailures := make(map[string]struct{}) // pkg.Test key
|
||||||
toRetry := make(map[string][]*testAttempt) // pkg -> tests to retry
|
toRetry := make(map[string][]*testAttempt) // pkg -> tests to retry
|
||||||
for _, pt := range thisRun.tests {
|
for _, pt := range thisRun.tests {
|
||||||
ch := make(chan *testAttempt)
|
ch := make(chan *testAttempt)
|
||||||
@ -276,7 +285,11 @@ func main() {
|
|||||||
// when a package times out.
|
// when a package times out.
|
||||||
failed = true
|
failed = true
|
||||||
}
|
}
|
||||||
os.Stdout.ReadFrom(&tr.logs)
|
if testingVerbose || tr.outcome == "fail" {
|
||||||
|
// Output package-level output which is where e.g.
|
||||||
|
// panics outside tests will be printed
|
||||||
|
io.Copy(os.Stdout, &tr.logs)
|
||||||
|
}
|
||||||
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start))
|
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -289,11 +302,24 @@ func main() {
|
|||||||
if tr.isMarkedFlaky {
|
if tr.isMarkedFlaky {
|
||||||
toRetry[tr.pkg] = append(toRetry[tr.pkg], tr)
|
toRetry[tr.pkg] = append(toRetry[tr.pkg], tr)
|
||||||
} else {
|
} else {
|
||||||
|
fatalFailures[tr.pkg+"."+tr.testName] = struct{}{}
|
||||||
failed = true
|
failed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if failed {
|
if failed {
|
||||||
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
||||||
|
|
||||||
|
// Print the list of non-flakytest failures.
|
||||||
|
// We will later analyze the retried GitHub Action runs to see
|
||||||
|
// if non-flakytest failures succeeded upon retry. This will
|
||||||
|
// highlight tests which are flaky but not yet flagged as such.
|
||||||
|
if len(fatalFailures) > 0 {
|
||||||
|
tests := slicesx.MapKeys(fatalFailures)
|
||||||
|
sort.Strings(tests)
|
||||||
|
j, _ := json.Marshal(tests)
|
||||||
|
fmt.Printf("non-flakytest failures: %s\n", j)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
cmd/tsidp/Dockerfile
Normal file
41
cmd/tsidp/Dockerfile
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy only go.mod and go.sum first to leverage Docker caching
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy the entire repository
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the tsidp binary
|
||||||
|
RUN go build -o /bin/tsidp ./cmd/tsidp
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /var/lib/tsidp
|
||||||
|
|
||||||
|
# Copy binary from builder stage
|
||||||
|
COPY --from=builder /bin/tsidp /app/tsidp
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV TAILSCALE_USE_WIP_CODE=1 \
|
||||||
|
TS_HOSTNAME=tsidp \
|
||||||
|
TS_STATE_DIR=/var/lib/tsidp
|
||||||
|
|
||||||
|
# Expose the default port
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
ENTRYPOINT ["/app/tsidp"]
|
100
cmd/tsidp/README.md
Normal file
100
cmd/tsidp/README.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# `tsidp` - Tailscale OpenID Connect (OIDC) Identity Provider
|
||||||
|
|
||||||
|
[](https://tailscale.com/kb/1167/release-stages/#experimental)
|
||||||
|
|
||||||
|
`tsidp` is an OIDC Identity Provider (IdP) server that integrates with your Tailscale network. It allows you to use Tailscale identities for authentication in applications that support OpenID Connect, enabling single sign-on (SSO) capabilities within your tailnet.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Tailscale network (tailnet) with magicDNS and HTTPS enabled
|
||||||
|
- A Tailscale authentication key from your tailnet
|
||||||
|
- Docker installed on your system
|
||||||
|
|
||||||
|
## Installation using Docker
|
||||||
|
|
||||||
|
1. **Build the Docker Image**
|
||||||
|
|
||||||
|
The Dockerfile uses a multi-stage build process to:
|
||||||
|
- Build the `tsidp` binary from source
|
||||||
|
- Create a minimal Alpine-based image with just the necessary components
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the Tailscale repository
|
||||||
|
git clone https://github.com/tailscale/tailscale.git
|
||||||
|
cd tailscale
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the Docker image
|
||||||
|
docker build -t tsidp:latest -f cmd/tsidp/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the Container**
|
||||||
|
|
||||||
|
Replace `YOUR_TAILSCALE_AUTHKEY` with your Tailscale authentication key.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name `tsidp` \
|
||||||
|
-p 443:443 \
|
||||||
|
-e TS_AUTHKEY=YOUR_TAILSCALE_AUTHKEY \
|
||||||
|
-e TS_HOSTNAME=tsidp \
|
||||||
|
-v tsidp-data:/var/lib/tsidp \
|
||||||
|
tsidp:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify Installation**
|
||||||
|
```bash
|
||||||
|
docker logs tsidp
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `https://tsidp.tailnet.ts.net` to confirm the service is running.
|
||||||
|
|
||||||
|
## Usage Example: Proxmox Integration
|
||||||
|
|
||||||
|
Here's how to configure Proxmox to use `tsidp` for authentication:
|
||||||
|
|
||||||
|
1. In Proxmox, navigate to Datacenter > Realms > Add OpenID Connect Server
|
||||||
|
|
||||||
|
2. Configure the following settings:
|
||||||
|
- Issuer URL: `https://idp.velociraptor.ts.net`
|
||||||
|
- Realm: `tailscale` (or your preferred name)
|
||||||
|
- Client ID: `unused`
|
||||||
|
- Client Key: `unused`
|
||||||
|
- Default: `true`
|
||||||
|
- Autocreate users: `true`
|
||||||
|
- Username claim: `email`
|
||||||
|
|
||||||
|
3. Set up user permissions:
|
||||||
|
- Go to Datacenter > Permissions > Groups
|
||||||
|
- Create a new group (e.g., "tsadmins")
|
||||||
|
- Click Permissions in the sidebar
|
||||||
|
- Add Group Permission
|
||||||
|
- Set Path to `/` for full admin access or scope as needed
|
||||||
|
- Set the group and role
|
||||||
|
- Add Tailscale-authenticated users to the group
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
The `tsidp` server supports several command-line flags:
|
||||||
|
|
||||||
|
- `--verbose`: Enable verbose logging
|
||||||
|
- `--port`: Port to listen on (default: 443)
|
||||||
|
- `--local-port`: Allow requests from localhost
|
||||||
|
- `--use-local-tailscaled`: Use local tailscaled instead of tsnet
|
||||||
|
- `--dir`: tsnet state directory
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `TS_AUTHKEY`: Your Tailscale authentication key (required)
|
||||||
|
- `TS_HOSTNAME`: Hostname for the `tsidp` server (default: "idp")
|
||||||
|
- `TS_STATE_DIR`: State directory (default: "/var/lib/tsidp")
|
||||||
|
- `TAILSCALE_USE_WIP_CODE`: Enable work-in-progress code (default: "1")
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
This is an [experimental](https://tailscale.com/kb/1167/release-stages#experimental), work in progress feature. For issues or questions, file issues on the [GitHub repository](https://github.com/tailscale/tailscale)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSD-3-Clause License. See [LICENSE](../../LICENSE) for details.
|
@ -11,6 +11,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -345,7 +346,9 @@ func (ar *authRequest) allowRelyingParty(r *http.Request, lc *local.Client) erro
|
|||||||
clientID = r.FormValue("client_id")
|
clientID = r.FormValue("client_id")
|
||||||
clientSecret = r.FormValue("client_secret")
|
clientSecret = r.FormValue("client_secret")
|
||||||
}
|
}
|
||||||
if ar.funnelRP.ID != clientID || ar.funnelRP.Secret != clientSecret {
|
clientIDcmp := subtle.ConstantTimeCompare([]byte(clientID), []byte(ar.funnelRP.ID))
|
||||||
|
clientSecretcmp := subtle.ConstantTimeCompare([]byte(clientSecret), []byte(ar.funnelRP.Secret))
|
||||||
|
if clientIDcmp != 1 || clientSecretcmp != 1 {
|
||||||
return fmt.Errorf("tsidp: invalid client credentials")
|
return fmt.Errorf("tsidp: invalid client credentials")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -762,6 +765,18 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *idpServer) serveOpenIDConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *idpServer) serveOpenIDConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("Access-Control-Allow-Origin", "*")
|
||||||
|
h.Set("Access-Control-Allow-Method", "GET, OPTIONS")
|
||||||
|
// allow all to prevent errors from client sending their own bespoke headers
|
||||||
|
// and having the server reject the request.
|
||||||
|
h.Set("Access-Control-Allow-Headers", "*")
|
||||||
|
|
||||||
|
// early return for pre-flight OPTIONS requests.
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
if r.URL.Path != oidcConfigPath {
|
if r.URL.Path != oidcConfigPath {
|
||||||
http.Error(w, "tsidp: not found", http.StatusNotFound)
|
http.Error(w, "tsidp: not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -18,6 +18,9 @@ import (
|
|||||||
"tailscale.com/derp/xdp"
|
"tailscale.com/derp/xdp"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/tsweb"
|
"tailscale.com/tsweb"
|
||||||
|
|
||||||
|
// Support for prometheus varz in tsweb
|
||||||
|
_ "tailscale.com/tsweb/promvarz"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -119,6 +119,7 @@ type Auto struct {
|
|||||||
updateCh chan struct{} // readable when we should inform the server of a change
|
updateCh chan struct{} // readable when we should inform the server of a change
|
||||||
observer Observer // called to update Client status; always non-nil
|
observer Observer // called to update Client status; always non-nil
|
||||||
observerQueue execqueue.ExecQueue
|
observerQueue execqueue.ExecQueue
|
||||||
|
shutdownFn func() // to be called prior to shutdown or nil
|
||||||
|
|
||||||
unregisterHealthWatch func()
|
unregisterHealthWatch func()
|
||||||
|
|
||||||
@ -189,6 +190,7 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
|||||||
mapDone: make(chan struct{}),
|
mapDone: make(chan struct{}),
|
||||||
updateDone: make(chan struct{}),
|
updateDone: make(chan struct{}),
|
||||||
observer: opts.Observer,
|
observer: opts.Observer,
|
||||||
|
shutdownFn: opts.Shutdown,
|
||||||
}
|
}
|
||||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||||
@ -755,6 +757,7 @@ func (c *Auto) Shutdown() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.logf("client.Shutdown ...")
|
c.logf("client.Shutdown ...")
|
||||||
|
shutdownFn := c.shutdownFn
|
||||||
|
|
||||||
direct := c.direct
|
direct := c.direct
|
||||||
c.closed = true
|
c.closed = true
|
||||||
@ -767,6 +770,10 @@ func (c *Auto) Shutdown() {
|
|||||||
c.unpauseWaiters = nil
|
c.unpauseWaiters = nil
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if shutdownFn != nil {
|
||||||
|
shutdownFn()
|
||||||
|
}
|
||||||
|
|
||||||
c.unregisterHealthWatch()
|
c.unregisterHealthWatch()
|
||||||
<-c.authDone
|
<-c.authDone
|
||||||
<-c.mapDone
|
<-c.mapDone
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
package controlclient
|
package controlclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@ -147,3 +149,42 @@ func TestCanSkipStatus(t *testing.T) {
|
|||||||
t.Errorf("Status fields = %q; this code was only written to handle fields %q", f, want)
|
t.Errorf("Status fields = %q; this code was only written to handle fields %q", f, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetryableErrors(t *testing.T) {
|
||||||
|
errorTests := []struct {
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{errNoNoiseClient, true},
|
||||||
|
{errNoNodeKey, true},
|
||||||
|
{fmt.Errorf("%w: %w", errNoNoiseClient, errors.New("no noise")), true},
|
||||||
|
{fmt.Errorf("%w: %w", errHTTPPostFailure, errors.New("bad post")), true},
|
||||||
|
{fmt.Errorf("%w: %w", errNoNodeKey, errors.New("not node key")), true},
|
||||||
|
{errBadHTTPResponse(429, "too may requests"), true},
|
||||||
|
{errBadHTTPResponse(500, "internal server eror"), true},
|
||||||
|
{errBadHTTPResponse(502, "bad gateway"), true},
|
||||||
|
{errBadHTTPResponse(503, "service unavailable"), true},
|
||||||
|
{errBadHTTPResponse(504, "gateway timeout"), true},
|
||||||
|
{errBadHTTPResponse(1234, "random error"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range errorTests {
|
||||||
|
t.Run(tt.err.Error(), func(t *testing.T) {
|
||||||
|
if isRetryableErrorForTest(tt.err) != tt.want {
|
||||||
|
t.Fatalf("retriable: got %v, want %v", tt.err, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryableForTest interface {
|
||||||
|
Retryable() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableErrorForTest(err error) bool {
|
||||||
|
var ae retryableForTest
|
||||||
|
if errors.As(err, &ae) {
|
||||||
|
return ae.Retryable()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -156,6 +156,11 @@ type Options struct {
|
|||||||
// If we receive a new DialPlan from the server, this value will be
|
// If we receive a new DialPlan from the server, this value will be
|
||||||
// updated.
|
// updated.
|
||||||
DialPlan ControlDialPlanner
|
DialPlan ControlDialPlanner
|
||||||
|
|
||||||
|
// Shutdown is an optional function that will be called before client shutdown is
|
||||||
|
// attempted. It is used to allow the client to clean up any resources or complete any
|
||||||
|
// tasks that are dependent on a live client.
|
||||||
|
Shutdown func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ControlDialPlanner is the interface optionally supplied when creating a
|
// ControlDialPlanner is the interface optionally supplied when creating a
|
||||||
@ -1255,6 +1260,7 @@ type devKnobs struct {
|
|||||||
DumpNetMapsVerbose func() bool
|
DumpNetMapsVerbose func() bool
|
||||||
ForceProxyDNS func() bool
|
ForceProxyDNS func() bool
|
||||||
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
|
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
|
||||||
|
StripHomeDERP func() bool // strip Home DERP from control
|
||||||
StripCaps func() bool // strip all local node's control-provided capabilities
|
StripCaps func() bool // strip all local node's control-provided capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1266,6 +1272,7 @@ func initDevKnob() devKnobs {
|
|||||||
DumpRegister: envknob.RegisterBool("TS_DEBUG_REGISTER"),
|
DumpRegister: envknob.RegisterBool("TS_DEBUG_REGISTER"),
|
||||||
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
|
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
|
||||||
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||||
|
StripHomeDERP: envknob.RegisterBool("TS_DEBUG_STRIP_HOME_DERP"),
|
||||||
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
|
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1660,11 +1667,11 @@ func (c *Auto) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) err
|
|||||||
func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
|
func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
|
||||||
nc, err := c.getNoiseClient()
|
nc, err := c.getNoiseClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w: %w", errNoNoiseClient, err)
|
||||||
}
|
}
|
||||||
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
|
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("no node key")
|
return errNoNodeKey
|
||||||
}
|
}
|
||||||
if c.panicOnUse {
|
if c.panicOnUse {
|
||||||
panic("tainted client")
|
panic("tainted client")
|
||||||
@ -1695,6 +1702,47 @@ func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendAuditLog implements [auditlog.Transport] by sending an audit log synchronously to the control plane.
|
||||||
|
//
|
||||||
|
// See docs on [tailcfg.AuditLogRequest] and [auditlog.Logger] for background.
|
||||||
|
func (c *Auto) SendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
|
||||||
|
return c.direct.sendAuditLog(ctx, auditLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Direct) sendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
|
||||||
|
nc, err := c.getNoiseClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", errNoNoiseClient, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
|
||||||
|
if !ok {
|
||||||
|
return errNoNodeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &tailcfg.AuditLogRequest{
|
||||||
|
Version: tailcfg.CurrentCapabilityVersion,
|
||||||
|
NodeKey: nodeKey,
|
||||||
|
Action: auditLog.Action,
|
||||||
|
Details: auditLog.Details,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.panicOnUse {
|
||||||
|
panic("tainted client")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := nc.post(ctx, "/machine/audit-log", nodeKey, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", errHTTPPostFailure, err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
all, _ := io.ReadAll(res.Body)
|
||||||
|
return errBadHTTPResponse(res.StatusCode, string(all))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||||
if !nodeKey.IsZero() {
|
if !nodeKey.IsZero() {
|
||||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||||
|
51
control/controlclient/errors.go
Normal file
51
control/controlclient/errors.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package controlclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiResponseError is an error type that can be returned by controlclient
|
||||||
|
// api requests.
|
||||||
|
//
|
||||||
|
// It wraps an underlying error and a flag for clients to query if the
|
||||||
|
// error is retryable via the Retryable() method.
|
||||||
|
type apiResponseError struct {
|
||||||
|
err error
|
||||||
|
retryable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements [error].
|
||||||
|
func (e *apiResponseError) Error() string {
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retryable reports whether the error is retryable.
|
||||||
|
func (e *apiResponseError) Retryable() bool {
|
||||||
|
return e.retryable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *apiResponseError) Unwrap() error { return e.err }
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoNodeKey = &apiResponseError{errors.New("no node key"), true}
|
||||||
|
errNoNoiseClient = &apiResponseError{errors.New("no noise client"), true}
|
||||||
|
errHTTPPostFailure = &apiResponseError{errors.New("http failure"), true}
|
||||||
|
)
|
||||||
|
|
||||||
|
func errBadHTTPResponse(code int, msg string) error {
|
||||||
|
retryable := false
|
||||||
|
switch code {
|
||||||
|
case http.StatusTooManyRequests,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
http.StatusBadGateway,
|
||||||
|
http.StatusServiceUnavailable,
|
||||||
|
http.StatusGatewayTimeout:
|
||||||
|
retryable = true
|
||||||
|
}
|
||||||
|
return &apiResponseError{fmt.Errorf("http error %d: %s", code, msg), retryable}
|
||||||
|
}
|
@ -240,6 +240,9 @@ func upgradeNode(n *tailcfg.Node) {
|
|||||||
}
|
}
|
||||||
n.LegacyDERPString = ""
|
n.LegacyDERPString = ""
|
||||||
}
|
}
|
||||||
|
if DevKnob.StripHomeDERP() {
|
||||||
|
n.HomeDERP = 0
|
||||||
|
}
|
||||||
|
|
||||||
if n.AllowedIPs == nil {
|
if n.AllowedIPs == nil {
|
||||||
n.AllowedIPs = slices.Clone(n.Addresses)
|
n.AllowedIPs = slices.Clone(n.Addresses)
|
||||||
|
@ -137,6 +137,7 @@ type Server struct {
|
|||||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||||
dupPolicy dupPolicy
|
dupPolicy dupPolicy
|
||||||
debug bool
|
debug bool
|
||||||
|
localClient local.Client
|
||||||
|
|
||||||
// Counters:
|
// Counters:
|
||||||
packetsSent, bytesSent expvar.Int
|
packetsSent, bytesSent expvar.Int
|
||||||
@ -485,6 +486,16 @@ func (s *Server) SetVerifyClientURLFailOpen(v bool) {
|
|||||||
s.verifyClientsURLFailOpen = v
|
s.verifyClientsURLFailOpen = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTailscaledSocketPath sets the unix socket path to use to talk to
|
||||||
|
// tailscaled if client verification is enabled.
|
||||||
|
//
|
||||||
|
// If unset or set to the empty string, the default path for the operating
|
||||||
|
// system is used.
|
||||||
|
func (s *Server) SetTailscaledSocketPath(path string) {
|
||||||
|
s.localClient.Socket = path
|
||||||
|
s.localClient.UseSocketOnly = path != ""
|
||||||
|
}
|
||||||
|
|
||||||
// SetTCPWriteTimeout sets the timeout for writing to connected clients.
|
// SetTCPWriteTimeout sets the timeout for writing to connected clients.
|
||||||
// This timeout does not apply to mesh connections.
|
// This timeout does not apply to mesh connections.
|
||||||
// Defaults to 2 seconds.
|
// Defaults to 2 seconds.
|
||||||
@ -1320,8 +1331,6 @@ func (c *sclient) requestMeshUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localClient local.Client
|
|
||||||
|
|
||||||
// isMeshPeer reports whether the client is a trusted mesh peer
|
// isMeshPeer reports whether the client is a trusted mesh peer
|
||||||
// node in the DERP region.
|
// node in the DERP region.
|
||||||
func (s *Server) isMeshPeer(info *clientInfo) bool {
|
func (s *Server) isMeshPeer(info *clientInfo) bool {
|
||||||
@ -1340,7 +1349,7 @@ func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, inf
|
|||||||
|
|
||||||
// tailscaled-based verification:
|
// tailscaled-based verification:
|
||||||
if s.verifyClientsLocalTailscaled {
|
if s.verifyClientsLocalTailscaled {
|
||||||
_, err := localClient.WhoIsNodeKey(ctx, clientKey)
|
_, err := s.localClient.WhoIsNodeKey(ctx, clientKey)
|
||||||
if err == tailscale.ErrPeerNotFound {
|
if err == tailscale.ErrPeerNotFound {
|
||||||
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
|
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
|
||||||
}
|
}
|
||||||
@ -2240,7 +2249,7 @@ func (s *Server) ConsistencyCheck() error {
|
|||||||
func (s *Server) checkVerifyClientsLocalTailscaled() error {
|
func (s *Server) checkVerifyClientsLocalTailscaled() error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
status, err := localClient.StatusWithoutPeers(ctx)
|
status, err := s.localClient.StatusWithoutPeers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("localClient.Status: %w", err)
|
return fmt.Errorf("localClient.Status: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -652,7 +652,11 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
|||||||
tlsConf.VerifyConnection = nil
|
tlsConf.VerifyConnection = nil
|
||||||
}
|
}
|
||||||
if node.CertName != "" {
|
if node.CertName != "" {
|
||||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
if suf, ok := strings.CutPrefix(node.CertName, "sha256-raw:"); ok {
|
||||||
|
tlsdial.SetConfigExpectedCertHash(tlsConf, suf)
|
||||||
|
} else {
|
||||||
|
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tls.Client(nc, tlsConf)
|
return tls.Client(nc, tlsConf)
|
||||||
@ -666,7 +670,7 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
|||||||
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
|
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
|
||||||
tcpConn, node, err := c.dialRegion(ctx, reg)
|
tcpConn, node, err := c.dialRegion(ctx, reg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, fmt.Errorf("dialRegion(%d): %w", reg.RegionID, err)
|
||||||
}
|
}
|
||||||
done := make(chan bool) // unbuffered
|
done := make(chan bool) // unbuffered
|
||||||
defer close(done)
|
defer close(done)
|
||||||
@ -741,6 +745,17 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
|||||||
|
|
||||||
nwait := 0
|
nwait := 0
|
||||||
startDial := func(dstPrimary, proto string) {
|
startDial := func(dstPrimary, proto string) {
|
||||||
|
dst := cmp.Or(dstPrimary, n.HostName)
|
||||||
|
|
||||||
|
// If dialing an IP address directly, check its address family
|
||||||
|
// and bail out before incrementing nwait.
|
||||||
|
if ip, err := netip.ParseAddr(dst); err == nil {
|
||||||
|
if proto == "tcp4" && ip.Is6() ||
|
||||||
|
proto == "tcp6" && ip.Is4() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nwait++
|
nwait++
|
||||||
go func() {
|
go func() {
|
||||||
if proto == "tcp4" && c.preferIPv6() {
|
if proto == "tcp4" && c.preferIPv6() {
|
||||||
@ -755,7 +770,6 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
|||||||
// Start v4 dial
|
// Start v4 dial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dst := cmp.Or(dstPrimary, n.HostName)
|
|
||||||
port := "443"
|
port := "443"
|
||||||
if !c.useHTTPS() {
|
if !c.useHTTPS() {
|
||||||
port = "3340"
|
port = "3340"
|
||||||
|
@ -417,6 +417,29 @@ func App() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCertShareReadOnlyMode returns true if this replica should never attempt to
|
||||||
|
// issue or renew TLS credentials for any of the HTTPS endpoints that it is
|
||||||
|
// serving. It should only return certs found in its cert store. Currently,
|
||||||
|
// this is used by the Kubernetes Operator's HA Ingress via VIPServices, where
|
||||||
|
// multiple Ingress proxy instances serve the same HTTPS endpoint with a shared
|
||||||
|
// TLS credentials. The TLS credentials should only be issued by one of the
|
||||||
|
// replicas.
|
||||||
|
// For HTTPS Ingress the operator and containerboot ensure
|
||||||
|
// that read-only replicas will not be serving the HTTPS endpoints before there
|
||||||
|
// is a shared cert available.
|
||||||
|
func IsCertShareReadOnlyMode() bool {
|
||||||
|
m := String("TS_CERT_SHARE_MODE")
|
||||||
|
return m == "ro"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCertShareReadWriteMode returns true if this instance is the replica
|
||||||
|
// responsible for issuing and renewing TLS certs in an HA setup with certs
|
||||||
|
// shared between multiple replicas.
|
||||||
|
func IsCertShareReadWriteMode() bool {
|
||||||
|
m := String("TS_CERT_SHARE_MODE")
|
||||||
|
return m == "rw"
|
||||||
|
}
|
||||||
|
|
||||||
// CrashOnUnexpected reports whether the Tailscale client should panic
|
// CrashOnUnexpected reports whether the Tailscale client should panic
|
||||||
// on unexpected conditions. If TS_DEBUG_CRASH_ON_UNEXPECTED is set, that's
|
// on unexpected conditions. If TS_DEBUG_CRASH_ON_UNEXPECTED is set, that's
|
||||||
// used. Otherwise the default value is true for unstable builds.
|
// used. Otherwise the default value is true for unstable builds.
|
||||||
|
9
go.mod
9
go.mod
@ -20,6 +20,7 @@ require (
|
|||||||
github.com/coder/websocket v1.8.12
|
github.com/coder/websocket v1.8.12
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
|
github.com/creachadair/taskgroup v0.13.2
|
||||||
github.com/creack/pty v1.1.23
|
github.com/creack/pty v1.1.23
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||||
@ -32,7 +33,7 @@ require (
|
|||||||
github.com/frankban/quicktest v1.14.6
|
github.com/frankban/quicktest v1.14.6
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0
|
github.com/fxamacker/cbor/v2 v2.7.0
|
||||||
github.com/gaissmai/bart v0.18.0
|
github.com/gaissmai/bart v0.18.0
|
||||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288
|
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874
|
||||||
github.com/go-logr/zapr v1.3.0
|
github.com/go-logr/zapr v1.3.0
|
||||||
github.com/go-ole/go-ole v1.3.0
|
github.com/go-ole/go-ole v1.3.0
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
||||||
@ -77,7 +78,7 @@ require (
|
|||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca
|
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca
|
||||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||||
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6
|
github.com/tailscale/mkctr v0.0.0-20250228050937-c75ea1476830
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc
|
||||||
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb
|
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb
|
||||||
@ -93,10 +94,10 @@ require (
|
|||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.33.0
|
golang.org/x/crypto v0.35.0
|
||||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||||
golang.org/x/mod v0.23.0
|
golang.org/x/mod v0.23.0
|
||||||
golang.org/x/net v0.35.0
|
golang.org/x/net v0.36.0
|
||||||
golang.org/x/oauth2 v0.26.0
|
golang.org/x/oauth2 v0.26.0
|
||||||
golang.org/x/sync v0.11.0
|
golang.org/x/sync v0.11.0
|
||||||
golang.org/x/sys v0.30.0
|
golang.org/x/sys v0.30.0
|
||||||
|
20
go.sum
20
go.sum
@ -231,6 +231,8 @@ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creachadair/mds v0.17.1 h1:lXQbTGKmb3nE3aK6OEp29L1gCx6B5ynzlQ6c1KOBurc=
|
github.com/creachadair/mds v0.17.1 h1:lXQbTGKmb3nE3aK6OEp29L1gCx6B5ynzlQ6c1KOBurc=
|
||||||
github.com/creachadair/mds v0.17.1/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjOmrk888eg=
|
github.com/creachadair/mds v0.17.1/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjOmrk888eg=
|
||||||
|
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||||
|
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
@ -298,6 +300,8 @@ github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phm
|
|||||||
github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI=
|
github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI=
|
||||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
@ -327,8 +331,8 @@ github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0q
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
|
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
|
||||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
@ -906,8 +910,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
|
|||||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||||
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6 h1:9SuADtKJAGQkIpnpg5znEJ86QaxacN25pHkiEXTDjzg=
|
github.com/tailscale/mkctr v0.0.0-20250228050937-c75ea1476830 h1:SwZ72kr1oRzzSPA5PYB4hzPh22UI0nm0dapn3bHaUPs=
|
||||||
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6/go.mod h1:qTslktI+Qh9hXo7ZP8xLkl5V8AxUMfxG0xLtkCFLxnw=
|
github.com/tailscale/mkctr v0.0.0-20250228050937-c75ea1476830/go.mod h1:qTslktI+Qh9hXo7ZP8xLkl5V8AxUMfxG0xLtkCFLxnw=
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||||
@ -1041,8 +1045,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -1131,8 +1135,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
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=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -1 +1 @@
|
|||||||
2b494987ff3c1a6a26e10570c490394ff0a77aa4
|
4fdaeeb8fe43bcdb4e8cc736433b9cd9c0ddd221
|
||||||
|
@ -11,7 +11,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@ -30,7 +29,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
app = flag.String("app", "tsapp", "appliance name; one of the subdirectories of gokrazy/")
|
app = flag.String("app", "tsapp", "appliance name; one of the subdirectories of gokrazy/")
|
||||||
bucket = flag.String("bucket", "tskrazy-import", "S3 bucket to upload disk image to while making AMI")
|
bucket = flag.String("bucket", "tskrazy-import", "S3 bucket to upload disk image to while making AMI")
|
||||||
goArch = flag.String("arch", cmp.Or(os.Getenv("GOARCH"), "amd64"), "GOARCH architecture to build for: arm64 or amd64")
|
|
||||||
build = flag.Bool("build", false, "if true, just build locally and stop, without uploading")
|
build = flag.Bool("build", false, "if true, just build locally and stop, without uploading")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,6 +52,26 @@ func findMkfsExt4() (string, error) {
|
|||||||
return "", errors.New("No mkfs.ext4 found on system")
|
return "", errors.New("No mkfs.ext4 found on system")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var conf gokrazyConfig
|
||||||
|
|
||||||
|
// gokrazyConfig is the subset of gokrazy/internal/config.Struct
|
||||||
|
// that we care about.
|
||||||
|
type gokrazyConfig struct {
|
||||||
|
// Environment is os.Environment pairs to use when
|
||||||
|
// building userspace.
|
||||||
|
// See https://gokrazy.org/userguide/instance-config/#environment
|
||||||
|
Environment []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *gokrazyConfig) GOARCH() string {
|
||||||
|
for _, e := range c.Environment {
|
||||||
|
if v, ok := strings.CutPrefix(e, "GOARCH="); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -61,6 +79,19 @@ func main() {
|
|||||||
log.Fatalf("--app must be non-empty name such as 'tsapp' or 'natlabapp'")
|
log.Fatalf("--app must be non-empty name such as 'tsapp' or 'natlabapp'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confJSON, err := os.ReadFile(filepath.Join(*app, "config.json"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading config.json: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(confJSON, &conf); err != nil {
|
||||||
|
log.Fatalf("unmarshaling config.json: %v", err)
|
||||||
|
}
|
||||||
|
switch conf.GOARCH() {
|
||||||
|
case "amd64", "arm64":
|
||||||
|
default:
|
||||||
|
log.Fatalf("config.json GOARCH %q must be amd64 or arm64", conf.GOARCH())
|
||||||
|
}
|
||||||
|
|
||||||
if err := buildImage(); err != nil {
|
if err := buildImage(); err != nil {
|
||||||
log.Fatalf("build image: %v", err)
|
log.Fatalf("build image: %v", err)
|
||||||
}
|
}
|
||||||
@ -106,7 +137,6 @@ func buildImage() error {
|
|||||||
// Build the tsapp.img
|
// Build the tsapp.img
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
cmd := exec.Command("go", "run",
|
cmd := exec.Command("go", "run",
|
||||||
"-exec=env GOOS=linux GOARCH="+*goArch+" ",
|
|
||||||
"github.com/gokrazy/tools/cmd/gok",
|
"github.com/gokrazy/tools/cmd/gok",
|
||||||
"--parent_dir="+dir,
|
"--parent_dir="+dir,
|
||||||
"--instance="+*app,
|
"--instance="+*app,
|
||||||
@ -253,13 +283,13 @@ func waitForImportSnapshot(importTaskID string) (snapID string, err error) {
|
|||||||
|
|
||||||
func makeAMI(name, ebsSnapID string) (ami string, err error) {
|
func makeAMI(name, ebsSnapID string) (ami string, err error) {
|
||||||
var arch string
|
var arch string
|
||||||
switch *goArch {
|
switch conf.GOARCH() {
|
||||||
case "arm64":
|
case "arm64":
|
||||||
arch = "arm64"
|
arch = "arm64"
|
||||||
case "amd64":
|
case "amd64":
|
||||||
arch = "x86_64"
|
arch = "x86_64"
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unknown arch %q", *goArch)
|
return "", fmt.Errorf("unknown arch %q", conf.GOARCH())
|
||||||
}
|
}
|
||||||
out, err := exec.Command("aws", "ec2", "register-image",
|
out, err := exec.Command("aws", "ec2", "register-image",
|
||||||
"--name", name,
|
"--name", name,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
module tailscale.com/gokrazy
|
module tailscale.com/gokrazy
|
||||||
|
|
||||||
go 1.23.1
|
go 1.23
|
||||||
|
|
||||||
require github.com/gokrazy/tools v0.0.0-20240730192548-9f81add3a91e
|
require github.com/gokrazy/tools v0.0.0-20250128200151-63160424957c
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.2.10 // indirect
|
github.com/breml/rootcerts v0.2.10 // indirect
|
||||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 // indirect
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 // indirect
|
||||||
github.com/gokrazy/internal v0.0.0-20240629150625-a0f1dee26ef5 // indirect
|
github.com/gokrazy/internal v0.0.0-20250126213949-423a5b587b57 // indirect
|
||||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 // indirect
|
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 // indirect
|
||||||
github.com/google/renameio/v2 v2.0.0 // indirect
|
github.com/google/renameio/v2 v2.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@ -15,9 +15,5 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/gokrazy/gokrazy => github.com/tailscale/gokrazy v0.0.0-20240812224643-6b21ddf64678
|
|
||||||
|
|
||||||
replace github.com/gokrazy/tools => github.com/tailscale/gokrazy-tools v0.0.0-20240730192548-9f81add3a91e
|
|
||||||
|
@ -3,8 +3,10 @@ github.com/breml/rootcerts v0.2.10/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDly
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
|
||||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
||||||
github.com/gokrazy/internal v0.0.0-20240629150625-a0f1dee26ef5 h1:XDklMxV0pE5jWiNaoo5TzvWfqdoiRRScmr4ZtDzE4Uw=
|
github.com/gokrazy/internal v0.0.0-20250126213949-423a5b587b57 h1:f5bEvO4we3fbfiBkECrrUgWQ8OH6J3SdB2Dwxid/Yx4=
|
||||||
github.com/gokrazy/internal v0.0.0-20240629150625-a0f1dee26ef5/go.mod h1:t3ZirVhcs9bH+fPAJuGh51rzT7sVCZ9yfXvszf0ZjF0=
|
github.com/gokrazy/internal v0.0.0-20250126213949-423a5b587b57/go.mod h1:SJG1KwuJQXFEoBgryaNCkMbdISyovDgZd0xmXJRZmiw=
|
||||||
|
github.com/gokrazy/tools v0.0.0-20250128200151-63160424957c h1:iEbS8GrNOn671ze8J/AfrYFEVzf8qMx8aR5K0VxPK2w=
|
||||||
|
github.com/gokrazy/tools v0.0.0-20250128200151-63160424957c/go.mod h1:f2vZhnaPzy92+Bjpx1iuZHK7VuaJx6SNCWQWmu23HZA=
|
||||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
|
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
|
||||||
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
|
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
@ -19,14 +21,12 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
|||||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/tailscale/gokrazy-tools v0.0.0-20240730192548-9f81add3a91e h1:3/xIc1QCvnKL7BCLng9od98HEvxCadjvqiI/bN+Twso=
|
|
||||||
github.com/tailscale/gokrazy-tools v0.0.0-20240730192548-9f81add3a91e/go.mod h1:eTZ0QsugEPFU5UAQ/87bKMkPxQuTNa7+iFAIahOFwRg=
|
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@ -4,32 +4,58 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
|
||||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||||
|
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||||
|
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
@ -46,10 +72,14 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
|||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
@ -62,6 +92,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
@ -70,6 +102,8 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso
|
|||||||
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
||||||
github.com/illarion/gonotify/v2 v2.0.2 h1:oDH5yvxq9oiQGWUeut42uShcWzOy/hsT9E7pvO95+kQ=
|
github.com/illarion/gonotify/v2 v2.0.2 h1:oDH5yvxq9oiQGWUeut42uShcWzOy/hsT9E7pvO95+kQ=
|
||||||
github.com/illarion/gonotify/v2 v2.0.2/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
|
github.com/illarion/gonotify/v2 v2.0.2/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
|
||||||
|
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
|
||||||
|
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||||
@ -84,6 +118,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
@ -96,6 +132,8 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5
|
|||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||||
@ -126,12 +164,18 @@ github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4
|
|||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||||
@ -144,6 +188,8 @@ github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
|
|||||||
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@ -152,42 +198,66 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07 h1:Z+Zg+aXJYq6f4TK2E4H+vZkQ4dJAWnInXDR6hM9znxo=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||||
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
|
||||||
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
||||||
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
||||||
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
||||||
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
||||||
|
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||||
|
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
@ -20,6 +20,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Environment": [
|
||||||
|
"GOOS=linux",
|
||||||
|
"GOARCH=arm64"
|
||||||
|
],
|
||||||
"KernelPackage": "github.com/gokrazy/kernel.arm64",
|
"KernelPackage": "github.com/gokrazy/kernel.arm64",
|
||||||
"FirmwarePackage": "github.com/gokrazy/kernel.arm64",
|
"FirmwarePackage": "github.com/gokrazy/kernel.arm64",
|
||||||
"EEPROMPackage": "",
|
"EEPROMPackage": "",
|
||||||
|
@ -4,32 +4,58 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
|
||||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||||
|
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||||
|
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
@ -46,10 +72,14 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
|||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
@ -62,6 +92,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
@ -86,6 +118,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
@ -98,6 +132,8 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5
|
|||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||||
@ -128,14 +164,20 @@ github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4
|
|||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc=
|
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||||
@ -148,6 +190,8 @@ github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
|
|||||||
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@ -156,42 +200,66 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07 h1:Z+Zg+aXJYq6f4TK2E4H+vZkQ4dJAWnInXDR6hM9znxo=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||||
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
|
||||||
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
||||||
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
||||||
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
||||||
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
||||||
|
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||||
|
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
@ -20,6 +20,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Environment": [
|
||||||
|
"GOOS=linux",
|
||||||
|
"GOARCH=amd64"
|
||||||
|
],
|
||||||
"KernelPackage": "github.com/tailscale/gokrazy-kernel",
|
"KernelPackage": "github.com/tailscale/gokrazy-kernel",
|
||||||
"FirmwarePackage": "",
|
"FirmwarePackage": "",
|
||||||
"EEPROMPackage": "",
|
"EEPROMPackage": "",
|
||||||
|
@ -4,48 +4,80 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
|
||||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||||
|
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||||
|
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
|
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||||
|
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
@ -58,12 +90,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||||
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||||
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
||||||
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
||||||
|
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
|
||||||
|
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||||
@ -78,6 +114,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
@ -90,6 +128,8 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5
|
|||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||||
@ -116,14 +156,22 @@ github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29X
|
|||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
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/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||||
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||||
@ -136,6 +184,8 @@ github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
|
|||||||
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
||||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
@ -144,42 +194,66 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07 h1:Z+Zg+aXJYq6f4TK2E4H+vZkQ4dJAWnInXDR6hM9znxo=
|
||||||
|
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||||
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4=
|
||||||
|
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
|
||||||
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
||||||
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
||||||
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
||||||
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
||||||
|
k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
|
||||||
|
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
|
||||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Environment": [
|
||||||
|
"GOOS=linux",
|
||||||
|
"GOARCH=amd64"
|
||||||
|
],
|
||||||
"KernelPackage": "github.com/tailscale/gokrazy-kernel",
|
"KernelPackage": "github.com/tailscale/gokrazy-kernel",
|
||||||
"FirmwarePackage": "github.com/tailscale/gokrazy-kernel",
|
"FirmwarePackage": "github.com/tailscale/gokrazy-kernel",
|
||||||
"InternalCompatibilityFlags": {}
|
"InternalCompatibilityFlags": {}
|
||||||
|
466
ipn/auditlog/auditlog.go
Normal file
466
ipn/auditlog/auditlog.go
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Package auditlog provides a mechanism for logging audit events.
|
||||||
|
package auditlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/rands"
|
||||||
|
"tailscale.com/util/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// transaction represents an audit log that has not yet been sent to the control plane.
|
||||||
|
type transaction struct {
|
||||||
|
// EventID is the unique identifier for the event being logged.
|
||||||
|
// This is used on the client side only and is not sent to control.
|
||||||
|
EventID string `json:",omitempty"`
|
||||||
|
// Retries is the number of times the logger has attempted to send this log.
|
||||||
|
// This is used on the client side only and is not sent to control.
|
||||||
|
Retries int `json:",omitempty"`
|
||||||
|
|
||||||
|
// Action is the action to be logged. It must correspond to a known action in the control plane.
|
||||||
|
Action tailcfg.ClientAuditAction `json:",omitempty"`
|
||||||
|
// Details is an opaque string specific to the action being logged. Empty strings may not
|
||||||
|
// be valid depending on the action being logged.
|
||||||
|
Details string `json:",omitempty"`
|
||||||
|
// TimeStamp is the time at which the audit log was generated on the node.
|
||||||
|
TimeStamp time.Time `json:",omitzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport provides a means for a client to send audit logs to a consumer (typically the control plane).
|
||||||
|
type Transport interface {
|
||||||
|
// SendAuditLog sends an audit log to a consumer of audit logs.
|
||||||
|
// Errors should be checked with [IsRetryableError] for retryability.
|
||||||
|
SendAuditLog(context.Context, tailcfg.AuditLogRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogStore provides a means for a [Logger] to persist logs to disk or memory.
|
||||||
|
type LogStore interface {
|
||||||
|
// Save saves the given data to a persistent store. Save will overwrite existing data
|
||||||
|
// for the given key.
|
||||||
|
save(key ipn.ProfileID, txns []*transaction) error
|
||||||
|
|
||||||
|
// Load retrieves the data from a persistent store. Returns a nil slice and
|
||||||
|
// no error if no data exists for the given key.
|
||||||
|
load(key ipn.ProfileID) ([]*transaction, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opts contains the configuration options for a [Logger].
|
||||||
|
type Opts struct {
|
||||||
|
// RetryLimit is the maximum number of attempts the logger will make to send a log before giving up.
|
||||||
|
RetryLimit int
|
||||||
|
// Store is the persistent store used to save logs to disk. Must be non-nil.
|
||||||
|
Store LogStore
|
||||||
|
// Logf is the logger used to log messages from the audit logger. Must be non-nil.
|
||||||
|
Logf logger.Logf
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRetryableError returns true if the given error is retryable
|
||||||
|
// See [controlclient.apiResponseError]. Potentially retryable errors implement the Retryable() method.
|
||||||
|
func IsRetryableError(err error) bool {
|
||||||
|
var retryable interface{ Retryable() bool }
|
||||||
|
return errors.As(err, &retryable) && retryable.Retryable()
|
||||||
|
}
|
||||||
|
|
||||||
|
type backoffOpts struct {
|
||||||
|
min, max time.Duration
|
||||||
|
multiplier float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// .5, 1, 2, 4, 8, 10, 10, 10, 10, 10...
|
||||||
|
var defaultBackoffOpts = backoffOpts{
|
||||||
|
min: time.Millisecond * 500,
|
||||||
|
max: 10 * time.Second,
|
||||||
|
multiplier: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger provides a queue-based mechanism for submitting audit logs to the control plane - or
|
||||||
|
// another suitable consumer. Logs are stored to disk and retried until they are successfully sent,
|
||||||
|
// or until they permanently fail.
|
||||||
|
//
|
||||||
|
// Each individual profile/controlclient tuple should construct and manage a unique [Logger] instance.
|
||||||
|
type Logger struct {
|
||||||
|
logf logger.Logf
|
||||||
|
retryLimit int // the maximum number of attempts to send a log before giving up.
|
||||||
|
flusher chan struct{} // channel used to signal a flush operation.
|
||||||
|
done chan struct{} // closed when the flush worker exits.
|
||||||
|
ctx context.Context // canceled when the logger is stopped.
|
||||||
|
ctxCancel context.CancelFunc // cancels ctx.
|
||||||
|
backoffOpts // backoff settings for retry operations.
|
||||||
|
|
||||||
|
// mu protects the fields below.
|
||||||
|
mu sync.Mutex
|
||||||
|
store LogStore // persistent storage for unsent logs.
|
||||||
|
profileID ipn.ProfileID // empty if [Logger.SetProfileID] has not been called.
|
||||||
|
transport Transport // nil until [Logger.Start] is called.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger creates a new [Logger] with the given options.
|
||||||
|
func NewLogger(opts Opts) *Logger {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
al := &Logger{
|
||||||
|
retryLimit: opts.RetryLimit,
|
||||||
|
logf: logger.WithPrefix(opts.Logf, "auditlog: "),
|
||||||
|
store: opts.Store,
|
||||||
|
flusher: make(chan struct{}, 1),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
ctx: ctx,
|
||||||
|
ctxCancel: cancel,
|
||||||
|
backoffOpts: defaultBackoffOpts,
|
||||||
|
}
|
||||||
|
al.logf("created")
|
||||||
|
return al
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushAndStop synchronously flushes all pending logs and stops the audit logger.
|
||||||
|
// This will block until a final flush operation completes or context is done.
|
||||||
|
// If the logger is already stopped, this will return immediately. All unsent
|
||||||
|
// logs will be persisted to the store.
|
||||||
|
func (al *Logger) FlushAndStop(ctx context.Context) {
|
||||||
|
al.stop()
|
||||||
|
al.flush(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProfileID sets the profileID for the logger. This must be called before any logs can be enqueued.
|
||||||
|
// The profileID of a logger cannot be changed once set.
|
||||||
|
func (al *Logger) SetProfileID(profileID ipn.ProfileID) error {
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
if al.profileID != "" {
|
||||||
|
return errors.New("profileID already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
al.profileID = profileID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the audit logger with the given transport.
|
||||||
|
// It returns an error if the logger is already started.
|
||||||
|
func (al *Logger) Start(t Transport) error {
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
|
||||||
|
if al.transport != nil {
|
||||||
|
return errors.New("already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
al.transport = t
|
||||||
|
pending, err := al.storedCountLocked()
|
||||||
|
if err != nil {
|
||||||
|
al.logf("[unexpected] failed to restore logs: %v", err)
|
||||||
|
}
|
||||||
|
go al.flushWorker()
|
||||||
|
if pending > 0 {
|
||||||
|
al.flushAsync()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAuditLogStorageFailure is returned when the logger fails to persist logs to the store.
|
||||||
|
var ErrAuditLogStorageFailure = errors.New("audit log storage failure")
|
||||||
|
|
||||||
|
// Enqueue queues an audit log to be sent to the control plane (or another suitable consumer/transport).
|
||||||
|
// This will return an error if the underlying store fails to save the log or we fail to generate a unique
|
||||||
|
// eventID for the log.
|
||||||
|
func (al *Logger) Enqueue(action tailcfg.ClientAuditAction, details string) error {
|
||||||
|
txn := &transaction{
|
||||||
|
Action: action,
|
||||||
|
Details: details,
|
||||||
|
TimeStamp: time.Now(),
|
||||||
|
}
|
||||||
|
// Generate a suitably random eventID for the transaction.
|
||||||
|
txn.EventID = fmt.Sprint(txn.TimeStamp, rands.HexString(16))
|
||||||
|
return al.enqueue(txn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushAsync requests an asynchronous flush.
|
||||||
|
// It is a no-op if a flush is already pending.
|
||||||
|
func (al *Logger) flushAsync() {
|
||||||
|
select {
|
||||||
|
case al.flusher <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *Logger) flushWorker() {
|
||||||
|
defer close(al.done)
|
||||||
|
|
||||||
|
var retryDelay time.Duration
|
||||||
|
retry := time.NewTimer(0)
|
||||||
|
retry.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-al.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-al.flusher:
|
||||||
|
err := al.flush(al.ctx)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
|
// The logger was stopped, no need to retry.
|
||||||
|
return
|
||||||
|
case err != nil:
|
||||||
|
retryDelay = max(al.backoffOpts.min, min(retryDelay*time.Duration(al.backoffOpts.multiplier), al.backoffOpts.max))
|
||||||
|
al.logf("retrying after %v, %v", retryDelay, err)
|
||||||
|
retry.Reset(retryDelay)
|
||||||
|
default:
|
||||||
|
retryDelay = 0
|
||||||
|
retry.Stop()
|
||||||
|
}
|
||||||
|
case <-retry.C:
|
||||||
|
al.flushAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush attempts to send all pending logs to the control plane.
|
||||||
|
// l.mu must not be held.
|
||||||
|
func (al *Logger) flush(ctx context.Context) error {
|
||||||
|
al.mu.Lock()
|
||||||
|
pending, err := al.store.load(al.profileID)
|
||||||
|
t := al.transport
|
||||||
|
al.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// This will catch nil profileIDs
|
||||||
|
return fmt.Errorf("failed to restore pending logs: %w", err)
|
||||||
|
}
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t == nil {
|
||||||
|
return errors.New("no transport")
|
||||||
|
}
|
||||||
|
|
||||||
|
complete, unsent := al.sendToTransport(ctx, pending, t)
|
||||||
|
al.markTransactionsDone(complete)
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
if err = al.appendToStoreLocked(unsent); err != nil {
|
||||||
|
al.logf("[unexpected] failed to persist logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unsent) != 0 {
|
||||||
|
return fmt.Errorf("failed to send %d logs", len(unsent))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(complete) != 0 {
|
||||||
|
al.logf("complete %d audit log transactions", len(complete))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendToTransport sends all pending logs to the control plane. Returns a pair of slices
|
||||||
|
// containing the logs that were successfully sent (or failed permanently) and those that were not.
|
||||||
|
//
|
||||||
|
// This may require multiple round trips to the control plane and can be a long running transaction.
|
||||||
|
func (al *Logger) sendToTransport(ctx context.Context, pending []*transaction, t Transport) (complete []*transaction, unsent []*transaction) {
|
||||||
|
for i, txn := range pending {
|
||||||
|
req := tailcfg.AuditLogRequest{
|
||||||
|
Action: tailcfg.ClientAuditAction(txn.Action),
|
||||||
|
Details: txn.Details,
|
||||||
|
Timestamp: txn.TimeStamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.SendAuditLog(ctx, req); err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
|
||||||
|
// The contex is done. All further attempts will fail.
|
||||||
|
unsent = append(unsent, pending[i:]...)
|
||||||
|
return complete, unsent
|
||||||
|
case IsRetryableError(err) && txn.Retries+1 < al.retryLimit:
|
||||||
|
// We permit a maximum number of retries for each log. All retriable
|
||||||
|
// errors should be transient and we should be able to send the log eventually, but
|
||||||
|
// we don't want logs to be persisted indefinitely.
|
||||||
|
txn.Retries++
|
||||||
|
unsent = append(unsent, txn)
|
||||||
|
default:
|
||||||
|
complete = append(complete, txn)
|
||||||
|
al.logf("failed permanently: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No error - we're done.
|
||||||
|
complete = append(complete, txn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return complete, unsent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *Logger) stop() {
|
||||||
|
al.mu.Lock()
|
||||||
|
t := al.transport
|
||||||
|
al.mu.Unlock()
|
||||||
|
|
||||||
|
if t == nil {
|
||||||
|
// No transport means no worker goroutine and done will not be
|
||||||
|
// closed if we cancel the context.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
al.ctxCancel()
|
||||||
|
<-al.done
|
||||||
|
al.logf("stopped for profileID: %v", al.profileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendToStoreLocked persists logs to the store. This will deduplicate
|
||||||
|
// logs so it is safe to call this with the same logs multiple time, to
|
||||||
|
// requeue failed transactions for example.
|
||||||
|
//
|
||||||
|
// l.mu must be held.
|
||||||
|
func (al *Logger) appendToStoreLocked(txns []*transaction) error {
|
||||||
|
if len(txns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if al.profileID == "" {
|
||||||
|
return errors.New("no logId set")
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := al.store.load(al.profileID)
|
||||||
|
if err != nil {
|
||||||
|
al.logf("[unexpected] append failed to restore logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The order is important here. We want the latest transactions first, which will
|
||||||
|
// ensure when we dedup, the new transactions are seen and the older transactions
|
||||||
|
// are discarded.
|
||||||
|
txnsOut := append(txns, persisted...)
|
||||||
|
txnsOut = deduplicateAndSort(txnsOut)
|
||||||
|
|
||||||
|
return al.store.save(al.profileID, txnsOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// storedCountLocked returns the number of logs persisted to the store.
|
||||||
|
// al.mu must be held.
|
||||||
|
func (al *Logger) storedCountLocked() (int, error) {
|
||||||
|
persisted, err := al.store.load(al.profileID)
|
||||||
|
return len(persisted), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// markTransactionsDone removes logs from the store that are complete (sent or failed permanently).
|
||||||
|
// al.mu must not be held.
|
||||||
|
func (al *Logger) markTransactionsDone(sent []*transaction) {
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
|
||||||
|
ids := set.Set[string]{}
|
||||||
|
for _, txn := range sent {
|
||||||
|
ids.Add(txn.EventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted, err := al.store.load(al.profileID)
|
||||||
|
if err != nil {
|
||||||
|
al.logf("[unexpected] markTransactionsDone failed to restore logs: %v", err)
|
||||||
|
}
|
||||||
|
var unsent []*transaction
|
||||||
|
for _, txn := range persisted {
|
||||||
|
if !ids.Contains(txn.EventID) {
|
||||||
|
unsent = append(unsent, txn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
al.store.save(al.profileID, unsent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deduplicateAndSort removes duplicate logs from the given slice and sorts them by timestamp.
|
||||||
|
// The first log entry in the slice will be retained, subsequent logs with the same EventID will be discarded.
|
||||||
|
func deduplicateAndSort(txns []*transaction) []*transaction {
|
||||||
|
seen := set.Set[string]{}
|
||||||
|
deduped := make([]*transaction, 0, len(txns))
|
||||||
|
for _, txn := range txns {
|
||||||
|
if !seen.Contains(txn.EventID) {
|
||||||
|
deduped = append(deduped, txn)
|
||||||
|
seen.Add(txn.EventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort logs by timestamp - oldest to newest. This will put the oldest logs at
|
||||||
|
// the front of the queue.
|
||||||
|
sort.Slice(deduped, func(i, j int) bool {
|
||||||
|
return deduped[i].TimeStamp.Before(deduped[j].TimeStamp)
|
||||||
|
})
|
||||||
|
return deduped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *Logger) enqueue(txn *transaction) error {
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
|
||||||
|
if err := al.appendToStoreLocked([]*transaction{txn}); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrAuditLogStorageFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a.transport is nil if the logger is stopped.
|
||||||
|
if al.transport != nil {
|
||||||
|
al.flushAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ LogStore = (*logStateStore)(nil)
|
||||||
|
|
||||||
|
// logStateStore is a concrete implementation of [LogStore]
|
||||||
|
// using [ipn.StateStore] as the underlying storage.
|
||||||
|
type logStateStore struct {
|
||||||
|
store ipn.StateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogStore creates a new LogStateStore with the given [ipn.StateStore].
|
||||||
|
func NewLogStore(store ipn.StateStore) LogStore {
|
||||||
|
return &logStateStore{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *logStateStore) generateKey(key ipn.ProfileID) string {
|
||||||
|
return "auditlog-" + string(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the given logs to an [ipn.StateStore]. This overwrites
|
||||||
|
// any existing entries for the given key.
|
||||||
|
func (s *logStateStore) save(key ipn.ProfileID, txns []*transaction) error {
|
||||||
|
if key == "" {
|
||||||
|
return errors.New("empty key")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(txns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k := ipn.StateKey(s.generateKey(key))
|
||||||
|
return s.store.WriteState(k, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load retrieves the logs from an [ipn.StateStore].
|
||||||
|
func (s *logStateStore) load(key ipn.ProfileID) ([]*transaction, error) {
|
||||||
|
if key == "" {
|
||||||
|
return nil, errors.New("empty key")
|
||||||
|
}
|
||||||
|
|
||||||
|
k := ipn.StateKey(s.generateKey(key))
|
||||||
|
data, err := s.store.ReadState(k)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ipn.ErrStateNotExist):
|
||||||
|
return nil, nil
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var txns []*transaction
|
||||||
|
err = json.Unmarshal(data, &txns)
|
||||||
|
return txns, err
|
||||||
|
}
|
481
ipn/auditlog/auditlog_test.go
Normal file
481
ipn/auditlog/auditlog_test.go
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package auditlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/tstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loggerForTest creates an auditLogger for you and cleans it up
|
||||||
|
// (and ensures no goroutines are leaked) when the test is done.
|
||||||
|
func loggerForTest(t *testing.T, opts Opts) *Logger {
|
||||||
|
t.Helper()
|
||||||
|
tstest.ResourceCheck(t)
|
||||||
|
|
||||||
|
if opts.Logf == nil {
|
||||||
|
opts.Logf = t.Logf
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Store == nil {
|
||||||
|
t.Fatalf("opts.Store must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
a := NewLogger(opts)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
a.FlushAndStop(context.Background())
|
||||||
|
})
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonRetryableErrors(t *testing.T) {
|
||||||
|
errorTests := []struct {
|
||||||
|
desc string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"DeadlineExceeded", context.DeadlineExceeded, false},
|
||||||
|
{"Canceled", context.Canceled, false},
|
||||||
|
{"Canceled wrapped", fmt.Errorf("%w: %w", context.Canceled, errors.New("ctx cancelled")), false},
|
||||||
|
{"Random error", errors.New("random error"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range errorTests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
if IsRetryableError(tt.err) != tt.want {
|
||||||
|
t.Fatalf("retriable: got %v, want %v", !tt.want, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnqueueAndFlush enqueues n logs and flushes them.
|
||||||
|
// We expect all logs to be flushed and for no
|
||||||
|
// logs to remain in the store once FlushAndStop returns.
|
||||||
|
func TestEnqueueAndFlush(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(nil)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 200,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
wantSent := 10
|
||||||
|
|
||||||
|
for i := range wantSent {
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent := mockTransport.sentCount(); gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnqueueAndFlushWithFlushCancel calls FlushAndCancel with a cancelled
|
||||||
|
// context. We expect nothing to be sent and all logs to be stored.
|
||||||
|
func TestEnqueueAndFlushWithFlushCancel(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(&retriableError)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 200,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the context before calling FlushAndStop - nothing should get sent.
|
||||||
|
// This mimics a timeout before flush() has a chance to execute.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
al.FlushAndStop(ctx)
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 10; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeduplicateAndSort tests that the most recent log is kept when deduplicating logs
|
||||||
|
func TestDeduplicateAndSort(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
|
||||||
|
logs := []*transaction{
|
||||||
|
{EventID: "1", Details: "log 1", TimeStamp: time.Now().Add(-time.Minute * 1), Retries: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
al.appendToStoreLocked(logs)
|
||||||
|
|
||||||
|
// Update the transaction and re-append it
|
||||||
|
logs[0].Retries = 2
|
||||||
|
al.appendToStoreLocked(logs)
|
||||||
|
|
||||||
|
fromStore, err := al.store.load("test")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
// We should see only one transaction
|
||||||
|
if wantStored, gotStored := len(logs), len(fromStore); gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should see the latest transaction
|
||||||
|
if wantRetryCount, gotRetryCount := 2, fromStore[0].Retries; gotRetryCount != wantRetryCount {
|
||||||
|
t.Fatalf("reties: got %d, want %d", gotRetryCount, wantRetryCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangeProfileId(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
|
||||||
|
// Changing a profile ID must fail
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendOnRestore pushes a n logs to the persistent store, and ensures they
|
||||||
|
// are sent as soon as Start is called then checks to ensure the sent logs no
|
||||||
|
// longer exist in the store.
|
||||||
|
func TestSendOnRestore(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(nil)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
al.SetProfileID("test")
|
||||||
|
|
||||||
|
wantTotal := 10
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent, wantSent := mockTransport.sentCount(), wantTotal; gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailureExhaustion enqueues n logs, with the transport in a failable state.
|
||||||
|
// We then set it to a non-failing state, call FlushAndStop and expect all logs to be sent.
|
||||||
|
func TestFailureExhaustion(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(&retriableError)
|
||||||
|
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 1,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnqueueAndFailNoRetry enqueues a set of logs, all of which will fail and are not
|
||||||
|
// retriable. We then call FlushAndStop and expect all to be unsent.
|
||||||
|
func TestEnqueueAndFailNoRetry(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(&nonRetriableError)
|
||||||
|
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnqueueAndRetry enqueues a set of logs, all of which will fail and are retriable.
|
||||||
|
// Mid-test, we set the transport to not-fail and expect the queue to flush properly
|
||||||
|
// We set the backoff parameters to 0 seconds so retries are immediate.
|
||||||
|
func TestEnqueueAndRetry(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockTransport := newMockTransport(&retriableError)
|
||||||
|
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
al.backoffOpts = backoffOpts{
|
||||||
|
min: 1 * time.Millisecond,
|
||||||
|
max: 4 * time.Millisecond,
|
||||||
|
multiplier: 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
c.Assert(al.Start(mockTransport), qt.IsNil)
|
||||||
|
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log 1"))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
// This will wait for at least 2 retries
|
||||||
|
gotRetried, wantRetried := mockTransport.waitForSendAttemptsToReach(3), true
|
||||||
|
if gotRetried != wantRetried {
|
||||||
|
t.Fatalf("retried: got %v, want %v", gotRetried, wantRetried)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockTransport.setErrorCondition(nil)
|
||||||
|
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotSent, wantSent := mockTransport.sentCount(), 1; gotSent != wantSent {
|
||||||
|
t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnqueueBeforeSetProfileID tests that logs enqueued before SetProfileId are not sent
|
||||||
|
func TestEnqueueBeforeSetProfileID(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
al := loggerForTest(t, Opts{
|
||||||
|
RetryLimit: 100,
|
||||||
|
Logf: t.Logf,
|
||||||
|
Store: NewLogStore(&mem.Store{}),
|
||||||
|
})
|
||||||
|
|
||||||
|
err := al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
|
||||||
|
c.Assert(err, qt.IsNotNil)
|
||||||
|
al.FlushAndStop(context.Background())
|
||||||
|
|
||||||
|
al.mu.Lock()
|
||||||
|
defer al.mu.Unlock()
|
||||||
|
gotStored, err := al.storedCountLocked()
|
||||||
|
c.Assert(err, qt.IsNotNil)
|
||||||
|
|
||||||
|
if wantStored := 0; gotStored != wantStored {
|
||||||
|
t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLogStoring tests that audit logs are persisted sorted by timestamp, oldest to newest
|
||||||
|
func TestLogSorting(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
mockStore := NewLogStore(&mem.Store{})
|
||||||
|
|
||||||
|
logs := []*transaction{
|
||||||
|
{EventID: "1", Details: "log 3", TimeStamp: time.Now().Add(-time.Minute * 1)},
|
||||||
|
{EventID: "1", Details: "log 3", TimeStamp: time.Now().Add(-time.Minute * 2)},
|
||||||
|
{EventID: "2", Details: "log 2", TimeStamp: time.Now().Add(-time.Minute * 3)},
|
||||||
|
{EventID: "3", Details: "log 1", TimeStamp: time.Now().Add(-time.Minute * 4)},
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLogs := []transaction{
|
||||||
|
{Details: "log 1"},
|
||||||
|
{Details: "log 2"},
|
||||||
|
{Details: "log 3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.save("test", logs)
|
||||||
|
|
||||||
|
gotLogs, err := mockStore.load("test")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
gotLogs = deduplicateAndSort(gotLogs)
|
||||||
|
|
||||||
|
for i := range gotLogs {
|
||||||
|
if want, got := wantLogs[i].Details, gotLogs[i].Details; want != got {
|
||||||
|
t.Fatalf("Details: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock implementations for testing
|
||||||
|
|
||||||
|
// newMockTransport returns a mock transport for testing
|
||||||
|
// If err is no nil, SendAuditLog will return this error if the send is attempted
|
||||||
|
// before the context is cancelled.
|
||||||
|
func newMockTransport(err error) *mockAuditLogTransport {
|
||||||
|
return &mockAuditLogTransport{
|
||||||
|
err: err,
|
||||||
|
attempts: make(chan int, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockAuditLogTransport struct {
|
||||||
|
attempts chan int // channel to notify of send attempts
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
sendAttmpts int // number of attempts to send logs
|
||||||
|
sendCount int // number of logs sent by the transport
|
||||||
|
err error // error to return when sending logs
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForSendAttemptsToReach blocks until the number of send attempts reaches n
|
||||||
|
// This should be use only in tests where the transport is expected to retry sending logs
|
||||||
|
func (t *mockAuditLogTransport) waitForSendAttemptsToReach(n int) bool {
|
||||||
|
for attempts := range t.attempts {
|
||||||
|
if attempts >= n {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockAuditLogTransport) setErrorCondition(err error) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
t.err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockAuditLogTransport) sentCount() int {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.sendCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockAuditLogTransport) SendAuditLog(ctx context.Context, _ tailcfg.AuditLogRequest) (err error) {
|
||||||
|
t.mu.Lock()
|
||||||
|
t.sendAttmpts += 1
|
||||||
|
defer func() {
|
||||||
|
a := t.sendAttmpts
|
||||||
|
t.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case t.attempts <- a:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.err != nil {
|
||||||
|
return t.err
|
||||||
|
}
|
||||||
|
t.sendCount += 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
retriableError = mockError{errors.New("retriable error")}
|
||||||
|
nonRetriableError = mockError{errors.New("permanent failure error")}
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e mockError) Retryable() bool {
|
||||||
|
return e == retriableError
|
||||||
|
}
|
@ -10,12 +10,11 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuditLogFunc is any function that can be used to log audit actions performed by an [Actor].
|
// AuditLogFunc is any function that can be used to log audit actions performed by an [Actor].
|
||||||
//
|
type AuditLogFunc func(action tailcfg.ClientAuditAction, details string) error
|
||||||
// TODO(nickkhyl,barnstar): define a named string type for the action (in tailcfg?) and use it here.
|
|
||||||
type AuditLogFunc func(action, details string)
|
|
||||||
|
|
||||||
// Actor is any actor using the [ipnlocal.LocalBackend].
|
// Actor is any actor using the [ipnlocal.LocalBackend].
|
||||||
//
|
//
|
||||||
@ -45,7 +44,7 @@ type Actor interface {
|
|||||||
//
|
//
|
||||||
// If the auditLogger is non-nil, it is used to write details about the action
|
// If the auditLogger is non-nil, it is used to write details about the action
|
||||||
// to the audit log when required by the policy.
|
// to the audit log when required by the policy.
|
||||||
CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogger AuditLogFunc) error
|
CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogFn AuditLogFunc) error
|
||||||
|
|
||||||
// IsLocalSystem reports whether the actor is the Windows' Local System account.
|
// IsLocalSystem reports whether the actor is the Windows' Local System account.
|
||||||
//
|
//
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/syspolicy"
|
"tailscale.com/util/syspolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ func (a actorWithPolicyChecks) CheckProfileAccess(profile ipn.LoginProfileView,
|
|||||||
//
|
//
|
||||||
// TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
|
// TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
|
||||||
// and corp to this package.
|
// and corp to this package.
|
||||||
func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditLogger AuditLogFunc) error {
|
func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditFn AuditLogFunc) error {
|
||||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -58,15 +59,16 @@ func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason str
|
|||||||
if reason == "" {
|
if reason == "" {
|
||||||
return errors.New("disconnect not allowed: reason required")
|
return errors.New("disconnect not allowed: reason required")
|
||||||
}
|
}
|
||||||
if auditLogger != nil {
|
if auditFn != nil {
|
||||||
var details string
|
var details string
|
||||||
if username, _ := actor.Username(); username != "" { // best-effort; we don't have it on all platforms
|
if username, _ := actor.Username(); username != "" { // best-effort; we don't have it on all platforms
|
||||||
details = fmt.Sprintf("%q is being disconnected by %q: %v", profile.Name(), username, reason)
|
details = fmt.Sprintf("%q is being disconnected by %q: %v", profile.Name(), username, reason)
|
||||||
} else {
|
} else {
|
||||||
details = fmt.Sprintf("%q is being disconnected: %v", profile.Name(), reason)
|
details = fmt.Sprintf("%q is being disconnected: %v", profile.Name(), reason)
|
||||||
}
|
}
|
||||||
// TODO(nickkhyl,barnstar): use a const for DISCONNECT_NODE.
|
if err := auditFn(tailcfg.AuditNodeDisconnect, details); err != nil {
|
||||||
auditLogger("DISCONNECT_NODE", details)
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,9 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||||
|
if envknob.IsCertShareReadOnlyMode() {
|
||||||
|
return pair, nil
|
||||||
|
}
|
||||||
// If we got here, we have a valid unexpired cert.
|
// If we got here, we have a valid unexpired cert.
|
||||||
// Check whether we should start an async renewal.
|
// Check whether we should start an async renewal.
|
||||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
||||||
@ -134,7 +137,7 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
|||||||
if minValidity == 0 {
|
if minValidity == 0 {
|
||||||
logf("starting async renewal")
|
logf("starting async renewal")
|
||||||
// Start renewal in the background, return current valid cert.
|
// Start renewal in the background, return current valid cert.
|
||||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
|
b.goTracker.Go(func() { getCertPEM(context.Background(), b, cs, logf, traceACME, domain, now, minValidity) })
|
||||||
return pair, nil
|
return pair, nil
|
||||||
}
|
}
|
||||||
// If the caller requested a specific validity duration, fall through
|
// If the caller requested a specific validity duration, fall through
|
||||||
@ -142,7 +145,11 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
|
|||||||
logf("starting sync renewal")
|
logf("starting sync renewal")
|
||||||
}
|
}
|
||||||
|
|
||||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
|
if envknob.IsCertShareReadOnlyMode() {
|
||||||
|
return nil, fmt.Errorf("retrieving cached TLS certificate failed and cert store is configured in read-only mode, not attempting to issue a new certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, err := getCertPEM(ctx, b, cs, logf, traceACME, domain, now, minValidity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("getCertPEM: %v", err)
|
logf("getCertPEM: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -250,15 +257,13 @@ type certStore interface {
|
|||||||
// for now. If they're expired, it returns errCertExpired.
|
// for now. If they're expired, it returns errCertExpired.
|
||||||
// If they don't exist, it returns ipn.ErrStateNotExist.
|
// If they don't exist, it returns ipn.ErrStateNotExist.
|
||||||
Read(domain string, now time.Time) (*TLSCertKeyPair, error)
|
Read(domain string, now time.Time) (*TLSCertKeyPair, error)
|
||||||
// WriteCert writes the cert for domain.
|
|
||||||
WriteCert(domain string, cert []byte) error
|
|
||||||
// WriteKey writes the key for domain.
|
|
||||||
WriteKey(domain string, key []byte) error
|
|
||||||
// ACMEKey returns the value previously stored via WriteACMEKey.
|
// ACMEKey returns the value previously stored via WriteACMEKey.
|
||||||
// It is a PEM encoded ECDSA key.
|
// It is a PEM encoded ECDSA key.
|
||||||
ACMEKey() ([]byte, error)
|
ACMEKey() ([]byte, error)
|
||||||
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
||||||
WriteACMEKey([]byte) error
|
WriteACMEKey([]byte) error
|
||||||
|
// WriteTLSCertAndKey writes the cert and key for domain.
|
||||||
|
WriteTLSCertAndKey(domain string, cert, key []byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var errCertExpired = errors.New("cert expired")
|
var errCertExpired = errors.New("cert expired")
|
||||||
@ -344,6 +349,13 @@ func (f certFileStore) WriteKey(domain string, key []byte) error {
|
|||||||
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f certFileStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
|
||||||
|
if err := f.WriteKey(domain, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.WriteCert(domain, cert)
|
||||||
|
}
|
||||||
|
|
||||||
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
||||||
type certStateStore struct {
|
type certStateStore struct {
|
||||||
ipn.StateStore
|
ipn.StateStore
|
||||||
@ -353,7 +365,29 @@ type certStateStore struct {
|
|||||||
testRoots *x509.CertPool
|
testRoots *x509.CertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSCertKeyReader is an interface implemented by state stores where it makes
|
||||||
|
// sense to read the TLS cert and key in a single operation that can be
|
||||||
|
// distinguished from generic state value reads. Currently this is only implemented
|
||||||
|
// by the kubestore.Store, which, in some cases, need to read cert and key from a
|
||||||
|
// non-cached TLS Secret.
|
||||||
|
type TLSCertKeyReader interface {
|
||||||
|
ReadTLSCertAndKey(domain string) ([]byte, []byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||||
|
// If we're using a store that supports atomic reads, use that
|
||||||
|
if kr, ok := s.StateStore.(TLSCertKeyReader); ok {
|
||||||
|
cert, key, err := kr.ReadTLSCertAndKey(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !validCertPEM(domain, key, cert, s.testRoots, now) {
|
||||||
|
return nil, errCertExpired
|
||||||
|
}
|
||||||
|
return &TLSCertKeyPair{CertPEM: cert, KeyPEM: key, Cached: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise fall back to separate reads
|
||||||
certPEM, err := s.ReadState(ipn.StateKey(domain + ".crt"))
|
certPEM, err := s.ReadState(ipn.StateKey(domain + ".crt"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -384,6 +418,27 @@ func (s certStateStore) WriteACMEKey(key []byte) error {
|
|||||||
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
|
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSCertKeyWriter is an interface implemented by state stores that can write the TLS
|
||||||
|
// cert and key in a single atomic operation. Currently this is only implemented
|
||||||
|
// by the kubestore.StoreKube.
|
||||||
|
type TLSCertKeyWriter interface {
|
||||||
|
WriteTLSCertAndKey(domain string, cert, key []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTLSCertAndKey writes the TLS cert and key for domain to the current
|
||||||
|
// LocalBackend's StateStore.
|
||||||
|
func (s certStateStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
|
||||||
|
// If we're using a store that supports atomic writes, use that.
|
||||||
|
if aw, ok := s.StateStore.(TLSCertKeyWriter); ok {
|
||||||
|
return aw.WriteTLSCertAndKey(domain, cert, key)
|
||||||
|
}
|
||||||
|
// Otherwise fall back to separate writes for cert and key.
|
||||||
|
if err := s.WriteKey(domain, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.WriteCert(domain, cert)
|
||||||
|
}
|
||||||
|
|
||||||
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
||||||
// from cache or freshly obtained.
|
// from cache or freshly obtained.
|
||||||
type TLSCertKeyPair struct {
|
type TLSCertKeyPair struct {
|
||||||
@ -420,7 +475,9 @@ func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKey
|
|||||||
return cs.Read(domain, now)
|
return cs.Read(domain, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
// getCertPem checks if a cert needs to be renewed and if so, renews it.
|
||||||
|
// It can be overridden in tests.
|
||||||
|
var getCertPEM = func(ctx context.Context, b *LocalBackend, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||||
acmeMu.Lock()
|
acmeMu.Lock()
|
||||||
defer acmeMu.Unlock()
|
defer acmeMu.Unlock()
|
||||||
|
|
||||||
@ -445,6 +502,10 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isDefaultDirectoryURL(ac.DirectoryURL) {
|
||||||
|
logf("acme: using Directory URL %q", ac.DirectoryURL)
|
||||||
|
}
|
||||||
|
|
||||||
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
@ -546,9 +607,6 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
|||||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
csr, err := certRequest(certPrivKey, domain, nil)
|
csr, err := certRequest(certPrivKey, domain, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -570,7 +628,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
|
if err := cs.WriteTLSCertAndKey(domain, certPEM.Bytes(), privPEM.Bytes()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
b.domainRenewed(domain)
|
b.domainRenewed(domain)
|
||||||
@ -714,7 +772,28 @@ func validateLeaf(leaf *x509.Certificate, intermediates *x509.CertPool, domain s
|
|||||||
// binary's baked-in roots (LetsEncrypt). See tailscale/tailscale#14690.
|
// binary's baked-in roots (LetsEncrypt). See tailscale/tailscale#14690.
|
||||||
return validateLeaf(leaf, intermediates, domain, now, bakedroots.Get())
|
return validateLeaf(leaf, intermediates, domain, now, bakedroots.Get())
|
||||||
}
|
}
|
||||||
return err == nil
|
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// When pointed at a non-prod ACME server, we don't expect to have the CA
|
||||||
|
// in our system or baked-in roots. Verify only throws UnknownAuthorityError
|
||||||
|
// after first checking the leaf cert's expiry, hostnames etc, so we know
|
||||||
|
// that the only reason for an error is to do with constructing a full chain.
|
||||||
|
// Allow this error so that cert caching still works in testing environments.
|
||||||
|
if errors.As(err, &x509.UnknownAuthorityError{}) {
|
||||||
|
acmeURL := envknob.String("TS_DEBUG_ACME_DIRECTORY_URL")
|
||||||
|
if !isDefaultDirectoryURL(acmeURL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDefaultDirectoryURL(u string) bool {
|
||||||
|
return u == "" || u == acme.LetsEncryptURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// validLookingCertDomain reports whether name looks like a valid domain name that
|
// validLookingCertDomain reports whether name looks like a valid domain name that
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@ -14,11 +15,17 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
|
"tailscale.com/tstest"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/must"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidLookingCertDomain(t *testing.T) {
|
func TestValidLookingCertDomain(t *testing.T) {
|
||||||
@ -47,10 +54,10 @@ var certTestFS embed.FS
|
|||||||
func TestCertStoreRoundTrip(t *testing.T) {
|
func TestCertStoreRoundTrip(t *testing.T) {
|
||||||
const testDomain = "example.com"
|
const testDomain = "example.com"
|
||||||
|
|
||||||
// Use a fixed verification timestamp so validity doesn't fall off when the
|
// Use fixed verification timestamps so validity doesn't change over time.
|
||||||
// cert expires. If you update the test data below, this may also need to be
|
// If you update the test data below, these may also need to be updated.
|
||||||
// updated.
|
|
||||||
testNow := time.Date(2023, time.February, 10, 0, 0, 0, 0, time.UTC)
|
testNow := time.Date(2023, time.February, 10, 0, 0, 0, 0, time.UTC)
|
||||||
|
testExpired := time.Date(2026, time.February, 10, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
// To re-generate a root certificate and domain certificate for testing,
|
// To re-generate a root certificate and domain certificate for testing,
|
||||||
// use:
|
// use:
|
||||||
@ -78,21 +85,23 @@ func TestCertStoreRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
store certStore
|
store certStore
|
||||||
|
debugACMEURL bool
|
||||||
}{
|
}{
|
||||||
{"FileStore", certFileStore{dir: t.TempDir(), testRoots: roots}},
|
{"FileStore", certFileStore{dir: t.TempDir(), testRoots: roots}, false},
|
||||||
{"StateStore", certStateStore{StateStore: new(mem.Store), testRoots: roots}},
|
{"FileStore_UnknownCA", certFileStore{dir: t.TempDir()}, true},
|
||||||
|
{"StateStore", certStateStore{StateStore: new(mem.Store), testRoots: roots}, false},
|
||||||
|
{"StateStore_UnknownCA", certStateStore{StateStore: new(mem.Store)}, true},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
if err := test.store.WriteCert(testDomain, testCert); err != nil {
|
if test.debugACMEURL {
|
||||||
t.Fatalf("WriteCert: unexpected error: %v", err)
|
t.Setenv("TS_DEBUG_ACME_DIRECTORY_URL", "https://acme-staging-v02.api.letsencrypt.org/directory")
|
||||||
}
|
}
|
||||||
if err := test.store.WriteKey(testDomain, testKey); err != nil {
|
if err := test.store.WriteTLSCertAndKey(testDomain, testCert, testKey); err != nil {
|
||||||
t.Fatalf("WriteKey: unexpected error: %v", err)
|
t.Fatalf("WriteTLSCertAndKey: unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
kp, err := test.store.Read(testDomain, testNow)
|
kp, err := test.store.Read(testDomain, testNow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Read: unexpected error: %v", err)
|
t.Fatalf("Read: unexpected error: %v", err)
|
||||||
@ -103,6 +112,10 @@ func TestCertStoreRoundTrip(t *testing.T) {
|
|||||||
if diff := cmp.Diff(kp.KeyPEM, testKey); diff != "" {
|
if diff := cmp.Diff(kp.KeyPEM, testKey); diff != "" {
|
||||||
t.Errorf("Key (-got, +want):\n%s", diff)
|
t.Errorf("Key (-got, +want):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
unexpected, err := test.store.Read(testDomain, testExpired)
|
||||||
|
if err != errCertExpired {
|
||||||
|
t.Fatalf("Read: expected expiry error: %v", string(unexpected.CertPEM))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,3 +228,151 @@ func TestDebugACMEDirectoryURL(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCertPEMWithValidity(t *testing.T) {
|
||||||
|
const testDomain = "example.com"
|
||||||
|
b := &LocalBackend{
|
||||||
|
store: &mem.Store{},
|
||||||
|
varRoot: t.TempDir(),
|
||||||
|
ctx: context.Background(),
|
||||||
|
logf: t.Logf,
|
||||||
|
}
|
||||||
|
certDir, err := b.certDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("certDir error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := b.getCertStore(); err != nil {
|
||||||
|
t.Fatalf("getCertStore error: %v", err)
|
||||||
|
}
|
||||||
|
testRoot, err := certTestFS.ReadFile("testdata/rootCA.pem")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM(testRoot) {
|
||||||
|
t.Fatal("Unable to add test CA to the cert pool")
|
||||||
|
}
|
||||||
|
testX509Roots = roots
|
||||||
|
defer func() { testX509Roots = nil }()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
now time.Time
|
||||||
|
// storeCerts is true if the test cert and key should be written to store.
|
||||||
|
storeCerts bool
|
||||||
|
readOnlyMode bool // TS_READ_ONLY_CERTS env var
|
||||||
|
wantAsyncRenewal bool // async issuance should be started
|
||||||
|
wantIssuance bool // sync issuance should be started
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid_no_renewal",
|
||||||
|
now: time.Date(2023, time.February, 20, 0, 0, 0, 0, time.UTC),
|
||||||
|
storeCerts: true,
|
||||||
|
wantAsyncRenewal: false,
|
||||||
|
wantIssuance: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuance_needed",
|
||||||
|
now: time.Date(2023, time.February, 20, 0, 0, 0, 0, time.UTC),
|
||||||
|
storeCerts: false,
|
||||||
|
wantAsyncRenewal: false,
|
||||||
|
wantIssuance: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "renewal_needed",
|
||||||
|
now: time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
storeCerts: true,
|
||||||
|
wantAsyncRenewal: true,
|
||||||
|
wantIssuance: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "renewal_needed_read_only_mode",
|
||||||
|
now: time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
storeCerts: true,
|
||||||
|
readOnlyMode: true,
|
||||||
|
wantAsyncRenewal: false,
|
||||||
|
wantIssuance: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_certs_read_only_mode",
|
||||||
|
now: time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
storeCerts: false,
|
||||||
|
readOnlyMode: true,
|
||||||
|
wantAsyncRenewal: false,
|
||||||
|
wantIssuance: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
if tt.readOnlyMode {
|
||||||
|
envknob.Setenv("TS_CERT_SHARE_MODE", "ro")
|
||||||
|
}
|
||||||
|
|
||||||
|
os.RemoveAll(certDir)
|
||||||
|
if tt.storeCerts {
|
||||||
|
os.MkdirAll(certDir, 0755)
|
||||||
|
if err := os.WriteFile(filepath.Join(certDir, "example.com.crt"),
|
||||||
|
must.Get(os.ReadFile("testdata/example.com.pem")), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(certDir, "example.com.key"),
|
||||||
|
must.Get(os.ReadFile("testdata/example.com-key.pem")), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.clock = tstest.NewClock(tstest.ClockOpts{Start: tt.now})
|
||||||
|
|
||||||
|
allDone := make(chan bool, 1)
|
||||||
|
defer b.goTracker.AddDoneCallback(func() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if b.goTracker.RunningGoroutines() > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case allDone <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Set to true if get getCertPEM is called. GetCertPEM can be called in a goroutine for async
|
||||||
|
// renewal or in the main goroutine if issuance is required to obtain valid TLS credentials.
|
||||||
|
getCertPemWasCalled := false
|
||||||
|
getCertPEM = func(ctx context.Context, b *LocalBackend, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||||
|
getCertPemWasCalled = true
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
prevGoRoutines := b.goTracker.StartedGoroutines()
|
||||||
|
_, err = b.GetCertPEMWithValidity(context.Background(), testDomain, 0)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("b.GetCertPemWithValidity got err %v, wants error: '%v'", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
// GetCertPEMWithValidity calls getCertPEM in a goroutine if async renewal is needed. That's the
|
||||||
|
// only goroutine it starts, so this can be used to test if async renewal was started.
|
||||||
|
gotAsyncRenewal := b.goTracker.StartedGoroutines()-prevGoRoutines != 0
|
||||||
|
if gotAsyncRenewal {
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for goroutines to finish")
|
||||||
|
case <-allDone:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify that async renewal was triggered if expected.
|
||||||
|
if tt.wantAsyncRenewal != gotAsyncRenewal {
|
||||||
|
t.Fatalf("wants getCertPem to be called async: %v, got called %v", tt.wantAsyncRenewal, gotAsyncRenewal)
|
||||||
|
}
|
||||||
|
// Verify that (non-async) issuance was started if expected.
|
||||||
|
gotIssuance := getCertPemWasCalled && !gotAsyncRenewal
|
||||||
|
if tt.wantIssuance != gotIssuance {
|
||||||
|
t.Errorf("wants getCertPem to be called: %v, got called %v", tt.wantIssuance, gotIssuance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -57,10 +57,12 @@ import (
|
|||||||
"tailscale.com/health/healthmsg"
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/auditlog"
|
||||||
"tailscale.com/ipn/conffile"
|
"tailscale.com/ipn/conffile"
|
||||||
"tailscale.com/ipn/ipnauth"
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/ipn/policy"
|
"tailscale.com/ipn/policy"
|
||||||
|
memstore "tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/log/sockstatlog"
|
"tailscale.com/log/sockstatlog"
|
||||||
"tailscale.com/logpolicy"
|
"tailscale.com/logpolicy"
|
||||||
"tailscale.com/net/captivedetection"
|
"tailscale.com/net/captivedetection"
|
||||||
@ -406,8 +408,8 @@ type LocalBackend struct {
|
|||||||
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
||||||
outgoingFiles map[string]*ipn.OutgoingFile
|
outgoingFiles map[string]*ipn.OutgoingFile
|
||||||
|
|
||||||
// getSafFd gets the Storage Access Framework file descriptor for writing Taildrop files to
|
// FileOps abstracts platform-specific file operations needed for file transfers.
|
||||||
GetSafFd func(filename string) int32
|
FileOps taildrop.FileOps
|
||||||
|
|
||||||
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
||||||
// avoid unnecessary churn between multiple equally-good options.
|
// avoid unnecessary churn between multiple equally-good options.
|
||||||
@ -453,6 +455,12 @@ type LocalBackend struct {
|
|||||||
// Each callback is called exactly once in unspecified order and without b.mu held.
|
// Each callback is called exactly once in unspecified order and without b.mu held.
|
||||||
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
|
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
|
||||||
shutdownCbs set.HandleSet[func() error]
|
shutdownCbs set.HandleSet[func() error]
|
||||||
|
|
||||||
|
// auditLogger, if non-nil, manages audit logging for the backend.
|
||||||
|
//
|
||||||
|
// It queues, persists, and sends audit logs
|
||||||
|
// to the control client. auditLogger has the same lifespan as b.cc.
|
||||||
|
auditLogger *auditlog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthTracker returns the health tracker for the backend.
|
// HealthTracker returns the health tracker for the backend.
|
||||||
@ -621,19 +629,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize Taildrive shares from saved state
|
|
||||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
|
||||||
if ok {
|
|
||||||
currentShares := b.pm.prefs.DriveShares()
|
|
||||||
if currentShares.Len() > 0 {
|
|
||||||
var shares []*drive.Share
|
|
||||||
for _, share := range currentShares.All() {
|
|
||||||
shares = append(shares, share.AsStruct())
|
|
||||||
}
|
|
||||||
fs.SetShares(shares)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, newFn := range registeredExtensions {
|
for name, newFn := range registeredExtensions {
|
||||||
ext, err := newFn(logf, sys)
|
ext, err := newFn(logf, sys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -813,6 +808,13 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
|
|||||||
b.directFileRoot = dir
|
b.directFileRoot = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFileOps sets the
|
||||||
|
func (b *LocalBackend) SetFileOps(fileOps taildrop.FileOps) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.FileOps = fileOps
|
||||||
|
}
|
||||||
|
|
||||||
// ReloadConfig reloads the backend's config from disk.
|
// ReloadConfig reloads the backend's config from disk.
|
||||||
//
|
//
|
||||||
// It returns (false, nil) if not running in declarative mode, (true, nil) on
|
// It returns (false, nil) if not running in declarative mode, (true, nil) on
|
||||||
@ -1695,6 +1697,15 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
|||||||
b.logf("Failed to save new controlclient state: %v", err)
|
b.logf("Failed to save new controlclient state: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the audit logger with the current profile ID.
|
||||||
|
if b.auditLogger != nil && prefsChanged {
|
||||||
|
pid := b.pm.CurrentProfile().ID()
|
||||||
|
if err := b.auditLogger.SetProfileID(pid); err != nil {
|
||||||
|
b.logf("Failed to set profile ID in audit logger: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
|
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
|
||||||
// (for new profiles) on the first call to b.pm.SetPrefs.
|
// (for new profiles) on the first call to b.pm.SetPrefs.
|
||||||
if err := b.initTKALocked(); err != nil {
|
if err := b.initTKALocked(); err != nil {
|
||||||
@ -2402,6 +2413,27 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
debugFlags = append([]string{"netstack"}, debugFlags...)
|
debugFlags = append([]string{"netstack"}, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var auditLogShutdown func()
|
||||||
|
// Audit logging is only available if the client has set up a proper persistent
|
||||||
|
// store for the logs in sys.
|
||||||
|
store, ok := b.sys.AuditLogStore.GetOK()
|
||||||
|
if !ok {
|
||||||
|
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
|
||||||
|
store = auditlog.NewLogStore(&memstore.Store{})
|
||||||
|
}
|
||||||
|
|
||||||
|
al := auditlog.NewLogger(auditlog.Opts{
|
||||||
|
Logf: b.logf,
|
||||||
|
RetryLimit: 32,
|
||||||
|
Store: store,
|
||||||
|
})
|
||||||
|
b.auditLogger = al
|
||||||
|
auditLogShutdown = func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
al.FlushAndStop(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(apenwarr): The only way to change the ServerURL is to
|
// TODO(apenwarr): The only way to change the ServerURL is to
|
||||||
// re-run b.Start, because this is the only place we create a
|
// re-run b.Start, because this is the only place we create a
|
||||||
// new controlclient. EditPrefs allows you to overwrite ServerURL,
|
// new controlclient. EditPrefs allows you to overwrite ServerURL,
|
||||||
@ -2427,6 +2459,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||||
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
||||||
ControlKnobs: b.sys.ControlKnobs(),
|
ControlKnobs: b.sys.ControlKnobs(),
|
||||||
|
Shutdown: auditLogShutdown,
|
||||||
|
|
||||||
// Don't warn about broken Linux IP forwarding when
|
// Don't warn about broken Linux IP forwarding when
|
||||||
// netstack is being used.
|
// netstack is being used.
|
||||||
@ -2461,6 +2494,16 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
||||||
b.sendToLocked(ipn.Notify{Prefs: &prefs}, allClients)
|
b.sendToLocked(ipn.Notify{Prefs: &prefs}, allClients)
|
||||||
|
|
||||||
|
// initialize Taildrive shares from saved state
|
||||||
|
if fs, ok := b.sys.DriveForRemote.GetOK(); ok {
|
||||||
|
currentShares := b.pm.CurrentPrefs().DriveShares()
|
||||||
|
var shares []*drive.Share
|
||||||
|
for _, share := range currentShares.All() {
|
||||||
|
shares = append(shares, share.AsStruct())
|
||||||
|
}
|
||||||
|
fs.SetShares(shares)
|
||||||
|
}
|
||||||
|
|
||||||
if !loggedOut && (b.hasNodeKeyLocked() || confWantRunning) {
|
if !loggedOut && (b.hasNodeKeyLocked() || confWantRunning) {
|
||||||
// If we know that we're either logged in or meant to be
|
// If we know that we're either logged in or meant to be
|
||||||
// running, tell the controlclient that it should also assume
|
// running, tell the controlclient that it should also assume
|
||||||
@ -4269,6 +4312,21 @@ func (b *LocalBackend) MaybeClearAppConnector(mp *ipn.MaskedPrefs) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errNoAuditLogger = errors.New("no audit logger configured")
|
||||||
|
|
||||||
|
func (b *LocalBackend) getAuditLoggerLocked() ipnauth.AuditLogFunc {
|
||||||
|
logger := b.auditLogger
|
||||||
|
return func(action tailcfg.ClientAuditAction, details string) error {
|
||||||
|
if logger == nil {
|
||||||
|
return errNoAuditLogger
|
||||||
|
}
|
||||||
|
if err := logger.Enqueue(action, details); err != nil {
|
||||||
|
return fmt.Errorf("failed to enqueue audit log %v %q: %w", action, details, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EditPrefs applies the changes in mp to the current prefs,
|
// EditPrefs applies the changes in mp to the current prefs,
|
||||||
// acting as the tailscaled itself rather than a specific user.
|
// acting as the tailscaled itself rather than a specific user.
|
||||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||||
@ -4294,9 +4352,8 @@ func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ip
|
|||||||
unlock := b.lockAndGetUnlock()
|
unlock := b.lockAndGetUnlock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
if mp.WantRunningSet && !mp.WantRunning && b.pm.CurrentPrefs().WantRunning() {
|
if mp.WantRunningSet && !mp.WantRunning && b.pm.CurrentPrefs().WantRunning() {
|
||||||
// TODO(barnstar,nickkhyl): replace loggerFn with the actual audit logger.
|
if err := actor.CheckProfileAccess(b.pm.CurrentProfile(), ipnauth.Disconnect, b.getAuditLoggerLocked()); err != nil {
|
||||||
loggerFn := func(action, details string) { b.logf("[audit]: %s: %s", action, details) }
|
b.logf("check profile access failed: %v", err)
|
||||||
if err := actor.CheckProfileAccess(b.pm.CurrentProfile(), ipnauth.Disconnect, loggerFn); err != nil {
|
|
||||||
return ipn.PrefsView{}, err
|
return ipn.PrefsView{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5296,7 +5353,6 @@ func (b *LocalBackend) initPeerAPIListener() {
|
|||||||
if fileRoot == "" {
|
if fileRoot == "" {
|
||||||
b.logf("peerapi starting without Taildrop directory configured")
|
b.logf("peerapi starting without Taildrop directory configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := &peerAPIServer{
|
ps := &peerAPIServer{
|
||||||
b: b,
|
b: b,
|
||||||
taildrop: taildrop.ManagerOptions{
|
taildrop: taildrop.ManagerOptions{
|
||||||
@ -5306,7 +5362,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
|||||||
Dir: fileRoot,
|
Dir: fileRoot,
|
||||||
DirectFileMode: b.directFileRoot != "",
|
DirectFileMode: b.directFileRoot != "",
|
||||||
SendFileNotify: b.sendFileNotify,
|
SendFileNotify: b.sendFileNotify,
|
||||||
}.New(b.getSafFd),
|
}.New(b.FileOps),
|
||||||
}
|
}
|
||||||
if dm, ok := b.sys.DNSManager.GetOK(); ok {
|
if dm, ok := b.sys.DNSManager.GetOK(); ok {
|
||||||
ps.resolver = dm.Resolver()
|
ps.resolver = dm.Resolver()
|
||||||
@ -5880,6 +5936,15 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
|||||||
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
|
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
|
||||||
b.cc = cc
|
b.cc = cc
|
||||||
b.ccAuto, _ = cc.(*controlclient.Auto)
|
b.ccAuto, _ = cc.(*controlclient.Auto)
|
||||||
|
if b.auditLogger != nil {
|
||||||
|
if err := b.auditLogger.SetProfileID(b.pm.CurrentProfile().ID()); err != nil {
|
||||||
|
b.logf("audit logger set profile ID failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.auditLogger.Start(b.ccAuto); err != nil {
|
||||||
|
b.logf("audit logger start failure: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetControlClientLocked sets b.cc to nil and returns the old value. If the
|
// resetControlClientLocked sets b.cc to nil and returns the old value. If the
|
||||||
@ -6712,7 +6777,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) taildropTargetStatus(p tailcfg.NodeView) ipnstate.TaildropTargetStatus {
|
func (b *LocalBackend) taildropTargetStatus(p tailcfg.NodeView) ipnstate.TaildropTargetStatus {
|
||||||
if b.netMap == nil || b.state != ipn.Running {
|
if b.state != ipn.Running {
|
||||||
return ipnstate.TaildropTargetIpnStateNotRunning
|
return ipnstate.TaildropTargetIpnStateNotRunning
|
||||||
}
|
}
|
||||||
if b.netMap == nil {
|
if b.netMap == nil {
|
||||||
@ -8225,15 +8290,13 @@ func (b *LocalBackend) vipServiceHash(services []*tailcfg.VIPService) string {
|
|||||||
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
|
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
|
||||||
// keyed by service name
|
// keyed by service name
|
||||||
var services map[tailcfg.ServiceName]*tailcfg.VIPService
|
var services map[tailcfg.ServiceName]*tailcfg.VIPService
|
||||||
if !b.serveConfig.Valid() {
|
if b.serveConfig.Valid() {
|
||||||
return nil
|
for svc, config := range b.serveConfig.Services().All() {
|
||||||
}
|
mak.Set(&services, svc, &tailcfg.VIPService{
|
||||||
|
Name: svc,
|
||||||
for svc, config := range b.serveConfig.Services().All() {
|
Ports: config.ServicePortRange(),
|
||||||
mak.Set(&services, svc, &tailcfg.VIPService{
|
})
|
||||||
Name: svc,
|
}
|
||||||
Ports: config.ServicePortRange(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range prefs.AdvertiseServices().All() {
|
for _, s := range prefs.AdvertiseServices().All() {
|
||||||
@ -8246,7 +8309,14 @@ func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcf
|
|||||||
services[sn].Active = true
|
services[sn].Active = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return slicesx.MapValues(services)
|
servicesList := slicesx.MapValues(services)
|
||||||
|
// [slicesx.MapValues] provides the values in an indeterminate order, but since we'll
|
||||||
|
// be hashing a representation of this list later we want it to be in a consistent
|
||||||
|
// order.
|
||||||
|
slices.SortFunc(servicesList, func(a, b *tailcfg.VIPService) int {
|
||||||
|
return strings.Compare(a.Name.String(), b.Name.String())
|
||||||
|
})
|
||||||
|
return servicesList
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -44,6 +44,7 @@ import (
|
|||||||
"tailscale.com/tsd"
|
"tailscale.com/tsd"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
|
"tailscale.com/types/ipproto"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/logid"
|
"tailscale.com/types/logid"
|
||||||
@ -60,6 +61,7 @@ import (
|
|||||||
"tailscale.com/util/syspolicy/source"
|
"tailscale.com/util/syspolicy/source"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
|
"tailscale.com/wgengine/filter/filtertype"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"tailscale.com/wgengine/wgcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5206,3 +5208,60 @@ func TestUpdateIngressLocked(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSrcCapPacketFilter tests that LocalBackend handles packet filters with
|
||||||
|
// SrcCaps instead of Srcs (IPs)
|
||||||
|
func TestSrcCapPacketFilter(t *testing.T) {
|
||||||
|
lb := newLocalBackendWithTestControl(t, false, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
|
||||||
|
return newClient(tb, opts)
|
||||||
|
})
|
||||||
|
if err := lb.Start(ipn.Options{}); err != nil {
|
||||||
|
t.Fatalf("(*LocalBackend).Start(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var k key.NodePublic
|
||||||
|
must.Do(k.UnmarshalText([]byte("nodekey:5c8f86d5fc70d924e55f02446165a5dae8f822994ad26bcf4b08fd841f9bf261")))
|
||||||
|
|
||||||
|
controlClient := lb.cc.(*mockControl)
|
||||||
|
controlClient.send(nil, "", false, &netmap.NetworkMap{
|
||||||
|
SelfNode: (&tailcfg.Node{
|
||||||
|
Addresses: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")},
|
||||||
|
}).View(),
|
||||||
|
Peers: []tailcfg.NodeView{
|
||||||
|
(&tailcfg.Node{
|
||||||
|
Addresses: []netip.Prefix{netip.MustParsePrefix("2.2.2.2/32")},
|
||||||
|
ID: 2,
|
||||||
|
Key: k,
|
||||||
|
CapMap: tailcfg.NodeCapMap{"cap-X": nil}, // node 2 has cap
|
||||||
|
}).View(),
|
||||||
|
(&tailcfg.Node{
|
||||||
|
Addresses: []netip.Prefix{netip.MustParsePrefix("3.3.3.3/32")},
|
||||||
|
ID: 3,
|
||||||
|
Key: k,
|
||||||
|
CapMap: tailcfg.NodeCapMap{}, // node 3 does not have the cap
|
||||||
|
}).View(),
|
||||||
|
},
|
||||||
|
PacketFilter: []filtertype.Match{{
|
||||||
|
IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}),
|
||||||
|
SrcCaps: []tailcfg.NodeCapability{"cap-X"}, // cap in packet filter rule
|
||||||
|
Dsts: []filtertype.NetPortRange{{
|
||||||
|
Net: netip.MustParsePrefix("1.1.1.1/32"),
|
||||||
|
Ports: filtertype.PortRange{
|
||||||
|
First: 22,
|
||||||
|
Last: 22,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
f := lb.GetFilterForTest()
|
||||||
|
res := f.Check(netip.MustParseAddr("2.2.2.2"), netip.MustParseAddr("1.1.1.1"), 22, ipproto.TCP)
|
||||||
|
if res != filter.Accept {
|
||||||
|
t.Errorf("Check(2.2.2.2, ...) = %s, want %s", res, filter.Accept)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = f.Check(netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("1.1.1.1"), 22, ipproto.TCP)
|
||||||
|
if !res.IsDrop() {
|
||||||
|
t.Error("IsDrop() for node without cap = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -216,6 +216,11 @@ type PeerStatusLite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PeerStatus describes a peer node and its current state.
|
// PeerStatus describes a peer node and its current state.
|
||||||
|
// WARNING: The fields in PeerStatus are merged by the AddPeer method in the StatusBuilder.
|
||||||
|
// When adding a new field to PeerStatus, you must update AddPeer to handle merging
|
||||||
|
// the new field. The AddPeer function is responsible for combining multiple updates
|
||||||
|
// to the same peer, and any new field that is not merged properly may lead to
|
||||||
|
// inconsistencies or lost data in the peer status.
|
||||||
type PeerStatus struct {
|
type PeerStatus struct {
|
||||||
ID tailcfg.StableNodeID
|
ID tailcfg.StableNodeID
|
||||||
PublicKey key.NodePublic
|
PublicKey key.NodePublic
|
||||||
@ -533,6 +538,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
|||||||
if v := st.Capabilities; v != nil {
|
if v := st.Capabilities; v != nil {
|
||||||
e.Capabilities = v
|
e.Capabilities = v
|
||||||
}
|
}
|
||||||
|
if v := st.TaildropTarget; v != TaildropTargetUnknown {
|
||||||
|
e.TaildropTarget = v
|
||||||
|
}
|
||||||
e.Location = st.Location
|
e.Location = st.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||||
@ -28,6 +30,14 @@ const (
|
|||||||
|
|
||||||
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
||||||
|
|
||||||
|
// Option defines a functional option type for configuring awsStore.
|
||||||
|
type Option func(*storeOptions)
|
||||||
|
|
||||||
|
// storeOptions holds optional settings for creating a new awsStore.
|
||||||
|
type storeOptions struct {
|
||||||
|
kmsKey string
|
||||||
|
}
|
||||||
|
|
||||||
// awsSSMClient is an interface allowing us to mock the couple of
|
// awsSSMClient is an interface allowing us to mock the couple of
|
||||||
// API calls we are leveraging with the AWSStore provider
|
// API calls we are leveraging with the AWSStore provider
|
||||||
type awsSSMClient interface {
|
type awsSSMClient interface {
|
||||||
@ -46,6 +56,10 @@ type awsStore struct {
|
|||||||
ssmClient awsSSMClient
|
ssmClient awsSSMClient
|
||||||
ssmARN arn.ARN
|
ssmARN arn.ARN
|
||||||
|
|
||||||
|
// kmsKey is optional. If empty, the parameter is stored in plaintext.
|
||||||
|
// If non-empty, the parameter is encrypted with this KMS key.
|
||||||
|
kmsKey string
|
||||||
|
|
||||||
memory mem.Store
|
memory mem.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,30 +71,80 @@ type awsStore struct {
|
|||||||
// Tailscaled to only only store new state in-memory and
|
// Tailscaled to only only store new state in-memory and
|
||||||
// restarting Tailscaled can fail until you delete your state
|
// restarting Tailscaled can fail until you delete your state
|
||||||
// from the AWS Parameter Store.
|
// from the AWS Parameter Store.
|
||||||
func New(_ logger.Logf, ssmARN string) (ipn.StateStore, error) {
|
//
|
||||||
return newStore(ssmARN, nil)
|
// If you want to specify an optional KMS key,
|
||||||
|
// pass one or more Option objects, e.g. awsstore.WithKeyID("alias/my-key").
|
||||||
|
func New(_ logger.Logf, ssmARN string, opts ...Option) (ipn.StateStore, error) {
|
||||||
|
// Apply all options to an empty storeOptions
|
||||||
|
var so storeOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&so)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStore(ssmARN, so, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyID sets the KMS key to be used for encryption. It can be
|
||||||
|
// a KeyID, an alias ("alias/my-key"), or a full ARN.
|
||||||
|
//
|
||||||
|
// If kmsKey is empty, the Option is a no-op.
|
||||||
|
func WithKeyID(kmsKey string) Option {
|
||||||
|
return func(o *storeOptions) {
|
||||||
|
o.kmsKey = kmsKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseARNAndOpts parses an ARN and optional URL-encoded parameters
|
||||||
|
// from arg.
|
||||||
|
func ParseARNAndOpts(arg string) (ssmARN string, opts []Option, err error) {
|
||||||
|
ssmARN = arg
|
||||||
|
|
||||||
|
// Support optional ?url-encoded-parameters.
|
||||||
|
if s, q, ok := strings.Cut(arg, "?"); ok {
|
||||||
|
ssmARN = s
|
||||||
|
q, err := url.ParseQuery(q)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range q {
|
||||||
|
switch k {
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("unknown arn option parameter %q", k)
|
||||||
|
case "kmsKey":
|
||||||
|
// We allow an ARN, a key ID, or an alias name for kmsKeyID.
|
||||||
|
// If it doesn't look like an ARN and doesn't have a '/',
|
||||||
|
// prepend "alias/" for KMS alias references.
|
||||||
|
kmsKey := q.Get(k)
|
||||||
|
if kmsKey != "" &&
|
||||||
|
!strings.Contains(kmsKey, "/") &&
|
||||||
|
!strings.HasPrefix(kmsKey, "arn:") {
|
||||||
|
kmsKey = "alias/" + kmsKey
|
||||||
|
}
|
||||||
|
if kmsKey != "" {
|
||||||
|
opts = append(opts, WithKeyID(kmsKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ssmARN, opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newStore is NewStore, but for tests. If client is non-nil, it's
|
// newStore is NewStore, but for tests. If client is non-nil, it's
|
||||||
// used instead of making one.
|
// used instead of making one.
|
||||||
func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
|
func newStore(ssmARN string, so storeOptions, client awsSSMClient) (ipn.StateStore, error) {
|
||||||
s := &awsStore{
|
s := &awsStore{
|
||||||
ssmClient: client,
|
ssmClient: client,
|
||||||
|
kmsKey: so.kmsKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Parse the ARN
|
|
||||||
if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
|
if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
|
return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the ARN corresponds to the SSM service
|
|
||||||
if s.ssmARN.Service != "ssm" {
|
if s.ssmARN.Service != "ssm" {
|
||||||
return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
|
return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the ARN corresponds to a parameter store resource
|
|
||||||
if !parameterNameRx.MatchString(s.ssmARN.Resource) {
|
if !parameterNameRx.MatchString(s.ssmARN.Resource) {
|
||||||
return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
|
return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
|
||||||
}
|
}
|
||||||
@ -96,12 +160,11 @@ func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
|
|||||||
s.ssmClient = ssm.NewFromConfig(cfg)
|
s.ssmClient = ssm.NewFromConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hydrate cache with the potentially current state
|
// Preload existing state, if any
|
||||||
if err := s.LoadState(); err != nil {
|
if err := s.LoadState(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadState attempts to read the state from AWS SSM parameter store key.
|
// LoadState attempts to read the state from AWS SSM parameter store key.
|
||||||
@ -172,15 +235,21 @@ func (s *awsStore) persistState() error {
|
|||||||
// which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
|
// which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
|
||||||
// doubling the capacity to 8kb per the following docs:
|
// doubling the capacity to 8kb per the following docs:
|
||||||
// https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
|
// https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
|
||||||
_, err = s.ssmClient.PutParameter(
|
in := &ssm.PutParameterInput{
|
||||||
context.TODO(),
|
Name: aws.String(s.ParameterName()),
|
||||||
&ssm.PutParameterInput{
|
Value: aws.String(string(bs)),
|
||||||
Name: aws.String(s.ParameterName()),
|
Overwrite: aws.Bool(true),
|
||||||
Value: aws.String(string(bs)),
|
Tier: ssmTypes.ParameterTierIntelligentTiering,
|
||||||
Overwrite: aws.Bool(true),
|
Type: ssmTypes.ParameterTypeSecureString,
|
||||||
Tier: ssmTypes.ParameterTierIntelligentTiering,
|
}
|
||||||
Type: ssmTypes.ParameterTypeSecureString,
|
|
||||||
},
|
// If kmsKey is specified, encrypt with that key
|
||||||
)
|
// NOTE: this input allows any alias, keyID or ARN
|
||||||
|
// If this isn't specified, AWS will use the default KMS key
|
||||||
|
if s.kmsKey != "" {
|
||||||
|
in.KeyId = aws.String(s.kmsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.ssmClient.PutParameter(context.TODO(), in)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build !linux || ts_omit_aws
|
|
||||||
|
|
||||||
package awsstore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"tailscale.com/ipn"
|
|
||||||
"tailscale.com/types/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(logger.Logf, string) (ipn.StateStore, error) {
|
|
||||||
return nil, fmt.Errorf("AWS store is not supported on %v", runtime.GOOS)
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux && !ts_omit_aws
|
||||||
|
|
||||||
package awsstore
|
package awsstore
|
||||||
|
|
||||||
@ -65,7 +65,11 @@ func TestNewAWSStore(t *testing.T) {
|
|||||||
Resource: "parameter/foo",
|
Resource: "parameter/foo",
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := newStore(storeParameterARN.String(), mc)
|
opts := storeOptions{
|
||||||
|
kmsKey: "arn:aws:kms:eu-west-1:123456789:key/MyCustomKey",
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := newStore(storeParameterARN.String(), opts, mc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating aws store failed: %v", err)
|
t.Fatalf("creating aws store failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -73,7 +77,7 @@ func TestNewAWSStore(t *testing.T) {
|
|||||||
|
|
||||||
// Build a brand new file store and check that both IDs written
|
// Build a brand new file store and check that both IDs written
|
||||||
// above are still there.
|
// above are still there.
|
||||||
s2, err := newStore(storeParameterARN.String(), mc)
|
s2, err := newStore(storeParameterARN.String(), opts, mc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating second aws store failed: %v", err)
|
t.Fatalf("creating second aws store failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -162,3 +166,54 @@ func testStoreSemantics(t *testing.T, store ipn.StateStore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseARNAndOpts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
arg string
|
||||||
|
wantARN string
|
||||||
|
wantKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-key",
|
||||||
|
arg: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam",
|
||||||
|
wantARN: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom-key",
|
||||||
|
arg: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam?kmsKey=alias/MyCustomKey",
|
||||||
|
wantARN: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam",
|
||||||
|
wantKey: "alias/MyCustomKey",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bare-name",
|
||||||
|
arg: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam?kmsKey=Bare",
|
||||||
|
wantARN: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam",
|
||||||
|
wantKey: "alias/Bare",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "arn-arg",
|
||||||
|
arg: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam?kmsKey=arn:foo",
|
||||||
|
wantARN: "arn:aws:ssm:us-east-1:123456789012:parameter/myTailscaleParam",
|
||||||
|
wantKey: "arn:foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
arn, opts, err := ParseARNAndOpts(tt.arg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New: %v", err)
|
||||||
|
}
|
||||||
|
if arn != tt.wantARN {
|
||||||
|
t.Errorf("ARN = %q; want %q", arn, tt.wantARN)
|
||||||
|
}
|
||||||
|
var got storeOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&got)
|
||||||
|
}
|
||||||
|
if got.kmsKey != tt.wantKey {
|
||||||
|
t.Errorf("kmsKey = %q; want %q", got.kmsKey, tt.wantKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,11 +13,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/kube/kubeapi"
|
"tailscale.com/kube/kubeapi"
|
||||||
"tailscale.com/kube/kubeclient"
|
"tailscale.com/kube/kubeclient"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/dnsname"
|
||||||
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -31,21 +35,37 @@ const (
|
|||||||
reasonTailscaleStateLoadFailed = "TailscaleStateLoadFailed"
|
reasonTailscaleStateLoadFailed = "TailscaleStateLoadFailed"
|
||||||
eventTypeWarning = "Warning"
|
eventTypeWarning = "Warning"
|
||||||
eventTypeNormal = "Normal"
|
eventTypeNormal = "Normal"
|
||||||
|
|
||||||
|
keyTLSCert = "tls.crt"
|
||||||
|
keyTLSKey = "tls.key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store is an ipn.StateStore that uses a Kubernetes Secret for persistence.
|
// Store is an ipn.StateStore that uses a Kubernetes Secret for persistence.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
client kubeclient.Client
|
client kubeclient.Client
|
||||||
canPatch bool
|
canPatch bool
|
||||||
secretName string
|
secretName string // state Secret
|
||||||
|
certShareMode string // 'ro', 'rw', or empty
|
||||||
|
podName string
|
||||||
|
|
||||||
// memory holds the latest tailscale state. Writes write state to a kube Secret and memory, Reads read from
|
// memory holds the latest tailscale state. Writes write state to a kube
|
||||||
// memory.
|
// Secret and memory, Reads read from memory.
|
||||||
memory mem.Store
|
memory mem.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Store that persists to the named Secret.
|
// New returns a new Store that persists state to Kubernets Secret(s).
|
||||||
func New(_ logger.Logf, secretName string) (*Store, error) {
|
// Tailscale state is stored in a Secret named by the secretName parameter.
|
||||||
|
// TLS certs are stored and retrieved from state Secret or separate Secrets
|
||||||
|
// named after TLS endpoints if running in cert share mode.
|
||||||
|
func New(logf logger.Logf, secretName string) (*Store, error) {
|
||||||
|
c, err := newClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newWithClient(logf, c, secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient() (kubeclient.Client, error) {
|
||||||
c, err := kubeclient.New("tailscale-state-store")
|
c, err := kubeclient.New("tailscale-state-store")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -54,6 +74,10 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
|
|||||||
// Derive the API server address from the environment variables
|
// Derive the API server address from the environment variables
|
||||||
c.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
c.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||||
}
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWithClient(logf logger.Logf, c kubeclient.Client, secretName string) (*Store, error) {
|
||||||
canPatch, _, err := c.CheckSecretPermissions(context.Background(), secretName)
|
canPatch, _, err := c.CheckSecretPermissions(context.Background(), secretName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -62,11 +86,30 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
|
|||||||
client: c,
|
client: c,
|
||||||
canPatch: canPatch,
|
canPatch: canPatch,
|
||||||
secretName: secretName,
|
secretName: secretName,
|
||||||
|
podName: os.Getenv("POD_NAME"),
|
||||||
}
|
}
|
||||||
|
if envknob.IsCertShareReadWriteMode() {
|
||||||
|
s.certShareMode = "rw"
|
||||||
|
} else if envknob.IsCertShareReadOnlyMode() {
|
||||||
|
s.certShareMode = "ro"
|
||||||
|
}
|
||||||
|
|
||||||
// Load latest state from kube Secret if it already exists.
|
// Load latest state from kube Secret if it already exists.
|
||||||
if err := s.loadState(); err != nil && err != ipn.ErrStateNotExist {
|
if err := s.loadState(); err != nil && err != ipn.ErrStateNotExist {
|
||||||
return nil, fmt.Errorf("error loading state from kube Secret: %w", err)
|
return nil, fmt.Errorf("error loading state from kube Secret: %w", err)
|
||||||
}
|
}
|
||||||
|
// If we are in cert share mode, pre-load existing shared certs.
|
||||||
|
if s.certShareMode == "rw" || s.certShareMode == "ro" {
|
||||||
|
sel := s.certSecretSelector()
|
||||||
|
if err := s.loadCerts(context.Background(), sel); err != nil {
|
||||||
|
// We will attempt to again retrieve the certs from Secrets when a request for an HTTPS endpoint
|
||||||
|
// is received.
|
||||||
|
log.Printf("[unexpected] error loading TLS certs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.certShareMode == "ro" {
|
||||||
|
go s.runCertReload(context.Background(), logf)
|
||||||
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +126,101 @@ func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
|
|||||||
|
|
||||||
// WriteState implements the StateStore interface.
|
// WriteState implements the StateStore interface.
|
||||||
func (s *Store) WriteState(id ipn.StateKey, bs []byte) (err error) {
|
func (s *Store) WriteState(id ipn.StateKey, bs []byte) (err error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.memory.WriteState(ipn.StateKey(sanitizeKey(id)), bs)
|
s.memory.WriteState(ipn.StateKey(sanitizeKey(id)), bs)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
return s.updateSecret(map[string][]byte{string(id): bs}, s.secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTLSCertAndKey writes a TLS cert and key to domain.crt, domain.key fields
|
||||||
|
// of a Tailscale Kubernetes node's state Secret.
|
||||||
|
func (s *Store) WriteTLSCertAndKey(domain string, cert, key []byte) (err error) {
|
||||||
|
if s.certShareMode == "ro" {
|
||||||
|
log.Printf("[unexpected] TLS cert and key write in read-only mode")
|
||||||
|
}
|
||||||
|
if err := dnsname.ValidHostname(domain); err != nil {
|
||||||
|
return fmt.Errorf("invalid domain name %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// TODO(irbekrm): a read between these two separate writes would
|
||||||
|
// get a mismatched cert and key. Allow writing both cert and
|
||||||
|
// key to the memory store in a single, lock-protected operation.
|
||||||
|
if err == nil {
|
||||||
|
s.memory.WriteState(ipn.StateKey(domain+".crt"), cert)
|
||||||
|
s.memory.WriteState(ipn.StateKey(domain+".key"), key)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
secretName := s.secretName
|
||||||
|
data := map[string][]byte{
|
||||||
|
domain + ".crt": cert,
|
||||||
|
domain + ".key": key,
|
||||||
|
}
|
||||||
|
// If we run in cert share mode, cert and key for a DNS name are written
|
||||||
|
// to a separate Secret.
|
||||||
|
if s.certShareMode == "rw" {
|
||||||
|
secretName = domain
|
||||||
|
data = map[string][]byte{
|
||||||
|
keyTLSCert: cert,
|
||||||
|
keyTLSKey: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.updateSecret(data, secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTLSCertAndKey reads a TLS cert and key from memory or from a
|
||||||
|
// domain-specific Secret. It first checks the in-memory store, if not found in
|
||||||
|
// memory and running cert store in read-only mode, looks up a Secret.
|
||||||
|
func (s *Store) ReadTLSCertAndKey(domain string) (cert, key []byte, err error) {
|
||||||
|
if err := dnsname.ValidHostname(domain); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("invalid domain name %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
certKey := domain + ".crt"
|
||||||
|
keyKey := domain + ".key"
|
||||||
|
|
||||||
|
cert, err = s.memory.ReadState(ipn.StateKey(certKey))
|
||||||
|
if err == nil {
|
||||||
|
key, err = s.memory.ReadState(ipn.StateKey(keyKey))
|
||||||
|
if err == nil {
|
||||||
|
return cert, key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.certShareMode != "ro" {
|
||||||
|
return nil, nil, ipn.ErrStateNotExist
|
||||||
|
}
|
||||||
|
// If we are in cert share read only mode, it is possible that a write
|
||||||
|
// replica just issued the TLS cert for this DNS name and it has not
|
||||||
|
// been loaded to store yet, so check the Secret.
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret, err := s.client.GetSecret(ctx, domain)
|
||||||
|
if err != nil {
|
||||||
|
if kubeclient.IsNotFoundErr(err) {
|
||||||
|
// TODO(irbekrm): we should return a more specific error
|
||||||
|
// that wraps ipn.ErrStateNotExist here.
|
||||||
|
return nil, nil, ipn.ErrStateNotExist
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("getting TLS Secret %q: %w", domain, err)
|
||||||
|
}
|
||||||
|
cert = secret.Data[keyTLSCert]
|
||||||
|
key = secret.Data[keyTLSKey]
|
||||||
|
if len(cert) == 0 || len(key) == 0 {
|
||||||
|
return nil, nil, ipn.ErrStateNotExist
|
||||||
|
}
|
||||||
|
// TODO(irbekrm): a read between these two separate writes would
|
||||||
|
// get a mismatched cert and key. Allow writing both cert and
|
||||||
|
// key to the memory store in a single lock-protected operation.
|
||||||
|
s.memory.WriteState(ipn.StateKey(certKey), cert)
|
||||||
|
s.memory.WriteState(ipn.StateKey(keyKey), key)
|
||||||
|
return cert, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) updateSecret(data map[string][]byte, secretName string) (err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := s.client.Event(ctx, eventTypeWarning, reasonTailscaleStateUpdateFailed, err.Error()); err != nil {
|
if err := s.client.Event(ctx, eventTypeWarning, reasonTailscaleStateUpdateFailed, err.Error()); err != nil {
|
||||||
log.Printf("kubestore: error creating tailscaled state update Event: %v", err)
|
log.Printf("kubestore: error creating tailscaled state update Event: %v", err)
|
||||||
@ -99,56 +232,69 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) (err error) {
|
|||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
secret, err := s.client.GetSecret(ctx, secretName)
|
||||||
secret, err := s.client.GetSecret(ctx, s.secretName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if kubeclient.IsNotFoundErr(err) {
|
// If the Secret does not exist, create it with the required data.
|
||||||
|
if kubeclient.IsNotFoundErr(err) && s.canCreateSecret(secretName) {
|
||||||
return s.client.CreateSecret(ctx, &kubeapi.Secret{
|
return s.client.CreateSecret(ctx, &kubeapi.Secret{
|
||||||
TypeMeta: kubeapi.TypeMeta{
|
TypeMeta: kubeapi.TypeMeta{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
Kind: "Secret",
|
Kind: "Secret",
|
||||||
},
|
},
|
||||||
ObjectMeta: kubeapi.ObjectMeta{
|
ObjectMeta: kubeapi.ObjectMeta{
|
||||||
Name: s.secretName,
|
Name: secretName,
|
||||||
},
|
|
||||||
Data: map[string][]byte{
|
|
||||||
sanitizeKey(id): bs,
|
|
||||||
},
|
},
|
||||||
|
Data: func(m map[string][]byte) map[string][]byte {
|
||||||
|
d := make(map[string][]byte, len(m))
|
||||||
|
for key, val := range m {
|
||||||
|
d[sanitizeKey(key)] = val
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}(data),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return err
|
return fmt.Errorf("error getting Secret %s: %w", secretName, err)
|
||||||
}
|
}
|
||||||
if s.canPatch {
|
if s.canPatchSecret(secretName) {
|
||||||
if len(secret.Data) == 0 { // if user has pre-created a blank Secret
|
var m []kubeclient.JSONPatch
|
||||||
m := []kubeclient.JSONPatch{
|
// If the user has pre-created a Secret with no data, we need to ensure the top level /data field.
|
||||||
|
if len(secret.Data) == 0 {
|
||||||
|
m = []kubeclient.JSONPatch{
|
||||||
{
|
{
|
||||||
Op: "add",
|
Op: "add",
|
||||||
Path: "/data",
|
Path: "/data",
|
||||||
Value: map[string][]byte{sanitizeKey(id): bs},
|
Value: func(m map[string][]byte) map[string][]byte {
|
||||||
|
d := make(map[string][]byte, len(m))
|
||||||
|
for key, val := range m {
|
||||||
|
d[sanitizeKey(key)] = val
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}(data),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := s.client.JSONPatchResource(ctx, s.secretName, kubeclient.TypeSecrets, m); err != nil {
|
// If the Secret has data, patch it with the new data.
|
||||||
return fmt.Errorf("error patching Secret %s with a /data field: %v", s.secretName, err)
|
} else {
|
||||||
|
for key, val := range data {
|
||||||
|
m = append(m, kubeclient.JSONPatch{
|
||||||
|
Op: "add",
|
||||||
|
Path: "/data/" + sanitizeKey(key),
|
||||||
|
Value: val,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
m := []kubeclient.JSONPatch{
|
if err := s.client.JSONPatchResource(ctx, secretName, kubeclient.TypeSecrets, m); err != nil {
|
||||||
{
|
return fmt.Errorf("error patching Secret %s: %w", secretName, err)
|
||||||
Op: "add",
|
|
||||||
Path: "/data/" + sanitizeKey(id),
|
|
||||||
Value: bs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := s.client.JSONPatchResource(ctx, s.secretName, kubeclient.TypeSecrets, m); err != nil {
|
|
||||||
return fmt.Errorf("error patching Secret %s with /data/%s field: %v", s.secretName, sanitizeKey(id), err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
secret.Data[sanitizeKey(id)] = bs
|
// No patch permissions, use UPDATE instead.
|
||||||
if err := s.client.UpdateSecret(ctx, secret); err != nil {
|
for key, val := range data {
|
||||||
return err
|
mak.Set(&secret.Data, sanitizeKey(key), val)
|
||||||
}
|
}
|
||||||
return err
|
if err := s.client.UpdateSecret(ctx, secret); err != nil {
|
||||||
|
return fmt.Errorf("error updating Secret %s: %w", s.secretName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) loadState() (err error) {
|
func (s *Store) loadState() (err error) {
|
||||||
@ -172,9 +318,100 @@ func (s *Store) loadState() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeKey(k ipn.StateKey) string {
|
// runCertReload relists and reloads all TLS certs for endpoints shared by this
|
||||||
// The only valid characters in a Kubernetes secret key are alphanumeric, -,
|
// node from Secrets other than the state Secret to ensure that renewed certs get eventually loaded.
|
||||||
// _, and .
|
// It is not critical to reload a cert immediately after
|
||||||
|
// renewal, so a daily check is acceptable.
|
||||||
|
// Currently (3/2025) this is only used for the shared HA Ingress certs on 'read' replicas.
|
||||||
|
// Note that if shared certs are not found in memory on an HTTPS request, we
|
||||||
|
// do a Secret lookup, so this mechanism does not need to ensure that newly
|
||||||
|
// added Ingresses' certs get loaded.
|
||||||
|
func (s *Store) runCertReload(ctx context.Context, logf logger.Logf) {
|
||||||
|
ticker := time.NewTicker(time.Hour * 24)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
sel := s.certSecretSelector()
|
||||||
|
if err := s.loadCerts(ctx, sel); err != nil {
|
||||||
|
logf("[unexpected] error reloading TLS certs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadCerts lists all Secrets matching the provided selector and loads TLS
|
||||||
|
// certs and keys from those.
|
||||||
|
func (s *Store) loadCerts(ctx context.Context, sel map[string]string) error {
|
||||||
|
ss, err := s.client.ListSecrets(ctx, sel)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error listing TLS Secrets: %w", err)
|
||||||
|
}
|
||||||
|
for _, secret := range ss.Items {
|
||||||
|
if !hasTLSData(&secret) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only load secrets that have valid domain names (ending in .ts.net)
|
||||||
|
if !strings.HasSuffix(secret.Name, ".ts.net") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.memory.WriteState(ipn.StateKey(secret.Name)+".crt", secret.Data[keyTLSCert])
|
||||||
|
s.memory.WriteState(ipn.StateKey(secret.Name)+".key", secret.Data[keyTLSKey])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// canCreateSecret returns true if this node should be allowed to create the given
|
||||||
|
// Secret in its namespace.
|
||||||
|
func (s *Store) canCreateSecret(secret string) bool {
|
||||||
|
// Only allow creating the state Secret (and not TLS Secrets).
|
||||||
|
return secret == s.secretName
|
||||||
|
}
|
||||||
|
|
||||||
|
// canPatchSecret returns true if this node should be allowed to patch the given
|
||||||
|
// Secret.
|
||||||
|
func (s *Store) canPatchSecret(secret string) bool {
|
||||||
|
// For backwards compatibility reasons, setups where the proxies are not
|
||||||
|
// given PATCH permissions for state Secrets are allowed. For TLS
|
||||||
|
// Secrets, we should always have PATCH permissions.
|
||||||
|
if secret == s.secretName {
|
||||||
|
return s.canPatch
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// certSecretSelector returns a label selector that can be used to list all
|
||||||
|
// Secrets that aren't Tailscale state Secrets and contain TLS certificates for
|
||||||
|
// HTTPS endpoints that this node serves.
|
||||||
|
// Currently (3/2025) this only applies to the Kubernetes Operator's ingress
|
||||||
|
// ProxyGroup.
|
||||||
|
func (s *Store) certSecretSelector() map[string]string {
|
||||||
|
if s.podName == "" {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
p := strings.LastIndex(s.podName, "-")
|
||||||
|
if p == -1 {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
pgName := s.podName[:p]
|
||||||
|
return map[string]string{
|
||||||
|
kubetypes.LabelSecretType: "certs",
|
||||||
|
kubetypes.LabelManaged: "true",
|
||||||
|
"tailscale.com/proxy-group": pgName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasTLSData returns true if the provided Secret contains non-empty TLS cert and key.
|
||||||
|
func hasTLSData(s *kubeapi.Secret) bool {
|
||||||
|
return len(s.Data[keyTLSCert]) != 0 && len(s.Data[keyTLSKey]) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeKey converts any value that can be converted to a string into a valid Kubernetes Secret key.
|
||||||
|
// Valid characters are alphanumeric, -, _, and .
|
||||||
|
// https://kubernetes.io/docs/concepts/configuration/secret/#restriction-names-data.
|
||||||
|
func sanitizeKey[T ~string](k T) string {
|
||||||
return strings.Map(func(r rune) rune {
|
return strings.Map(func(r rune) rune {
|
||||||
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
|
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
|
||||||
return r
|
return r
|
||||||
|
723
ipn/store/kubestore/store_kube_test.go
Normal file
723
ipn/store/kubestore/store_kube_test.go
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package kubestore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/store/mem"
|
||||||
|
"tailscale.com/kube/kubeapi"
|
||||||
|
"tailscale.com/kube/kubeclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteState(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initial map[string][]byte
|
||||||
|
key ipn.StateKey
|
||||||
|
value []byte
|
||||||
|
wantData map[string][]byte
|
||||||
|
allowPatch bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic_write",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"existing": []byte("old"),
|
||||||
|
},
|
||||||
|
key: "foo",
|
||||||
|
value: []byte("bar"),
|
||||||
|
wantData: map[string][]byte{
|
||||||
|
"existing": []byte("old"),
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
allowPatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_existing",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"foo": []byte("old"),
|
||||||
|
},
|
||||||
|
key: "foo",
|
||||||
|
value: []byte("new"),
|
||||||
|
wantData: map[string][]byte{
|
||||||
|
"foo": []byte("new"),
|
||||||
|
},
|
||||||
|
allowPatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_new_secret",
|
||||||
|
key: "foo",
|
||||||
|
value: []byte("bar"),
|
||||||
|
wantData: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
allowPatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "patch_denied",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"foo": []byte("old"),
|
||||||
|
},
|
||||||
|
key: "foo",
|
||||||
|
value: []byte("new"),
|
||||||
|
wantData: map[string][]byte{
|
||||||
|
"foo": []byte("new"),
|
||||||
|
},
|
||||||
|
allowPatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sanitize_key",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"clean-key": []byte("old"),
|
||||||
|
},
|
||||||
|
key: "dirty@key",
|
||||||
|
value: []byte("new"),
|
||||||
|
wantData: map[string][]byte{
|
||||||
|
"clean-key": []byte("old"),
|
||||||
|
"dirty_key": []byte("new"),
|
||||||
|
},
|
||||||
|
allowPatch: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
secret := tt.initial // track current state
|
||||||
|
client := &kubeclient.FakeClient{
|
||||||
|
GetSecretImpl: func(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
|
if secret == nil {
|
||||||
|
return nil, &kubeapi.Status{Code: 404}
|
||||||
|
}
|
||||||
|
return &kubeapi.Secret{Data: secret}, nil
|
||||||
|
},
|
||||||
|
CheckSecretPermissionsImpl: func(ctx context.Context, name string) (bool, bool, error) {
|
||||||
|
return tt.allowPatch, true, nil
|
||||||
|
},
|
||||||
|
CreateSecretImpl: func(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
|
secret = s.Data
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
UpdateSecretImpl: func(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
|
secret = s.Data
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
JSONPatchResourceImpl: func(ctx context.Context, name, resourceType string, patches []kubeclient.JSONPatch) error {
|
||||||
|
if !tt.allowPatch {
|
||||||
|
return &kubeapi.Status{Reason: "Forbidden"}
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
secret = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
for _, p := range patches {
|
||||||
|
if p.Op == "add" && p.Path == "/data" {
|
||||||
|
secret = p.Value.(map[string][]byte)
|
||||||
|
} else if p.Op == "add" && strings.HasPrefix(p.Path, "/data/") {
|
||||||
|
key := strings.TrimPrefix(p.Path, "/data/")
|
||||||
|
secret[key] = p.Value.([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Store{
|
||||||
|
client: client,
|
||||||
|
canPatch: tt.allowPatch,
|
||||||
|
secretName: "ts-state",
|
||||||
|
memory: mem.Store{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.WriteState(tt.key, tt.value)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WriteState() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secret data
|
||||||
|
if diff := cmp.Diff(secret, tt.wantData); diff != "" {
|
||||||
|
t.Errorf("secret data mismatch (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify memory store was updated
|
||||||
|
got, err := s.memory.ReadState(ipn.StateKey(sanitizeKey(string(tt.key))))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading from memory store: %v", err)
|
||||||
|
}
|
||||||
|
if !cmp.Equal(got, tt.value) {
|
||||||
|
t.Errorf("memory store key %q = %v, want %v", tt.key, got, tt.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteTLSCertAndKey(t *testing.T) {
|
||||||
|
const (
|
||||||
|
testDomain = "my-app.tailnetxyz.ts.net"
|
||||||
|
testCert = "fake-cert"
|
||||||
|
testKey = "fake-key"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initial map[string][]byte // pre-existing cert and key
|
||||||
|
certShareMode string
|
||||||
|
allowPatch bool // whether client can patch the Secret
|
||||||
|
wantSecretName string // name of the Secret where cert and key should be written
|
||||||
|
wantSecretData map[string][]byte
|
||||||
|
wantMemoryStore map[ipn.StateKey][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic_write",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"existing": []byte("old"),
|
||||||
|
},
|
||||||
|
allowPatch: true,
|
||||||
|
wantSecretName: "ts-state",
|
||||||
|
wantSecretData: map[string][]byte{
|
||||||
|
"existing": []byte("old"),
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_mode_write",
|
||||||
|
certShareMode: "rw",
|
||||||
|
allowPatch: true,
|
||||||
|
wantSecretName: "my-app.tailnetxyz.ts.net",
|
||||||
|
wantSecretData: map[string][]byte{
|
||||||
|
"tls.crt": []byte(testCert),
|
||||||
|
"tls.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_mode_write_update_existing",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"tls.crt": []byte("old-cert"),
|
||||||
|
"tls.key": []byte("old-key"),
|
||||||
|
},
|
||||||
|
certShareMode: "rw",
|
||||||
|
allowPatch: true,
|
||||||
|
wantSecretName: "my-app.tailnetxyz.ts.net",
|
||||||
|
wantSecretData: map[string][]byte{
|
||||||
|
"tls.crt": []byte(testCert),
|
||||||
|
"tls.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_existing",
|
||||||
|
initial: map[string][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte("old-cert"),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte("old-key"),
|
||||||
|
},
|
||||||
|
certShareMode: "",
|
||||||
|
allowPatch: true,
|
||||||
|
wantSecretName: "ts-state",
|
||||||
|
wantSecretData: map[string][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "patch_denied",
|
||||||
|
certShareMode: "",
|
||||||
|
allowPatch: false,
|
||||||
|
wantSecretName: "ts-state",
|
||||||
|
wantSecretData: map[string][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
// Set POD_NAME for testing selectors
|
||||||
|
envknob.Setenv("POD_NAME", "ingress-proxies-1")
|
||||||
|
defer envknob.Setenv("POD_NAME", "")
|
||||||
|
|
||||||
|
secret := tt.initial // track current state
|
||||||
|
client := &kubeclient.FakeClient{
|
||||||
|
GetSecretImpl: func(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
|
if secret == nil {
|
||||||
|
return nil, &kubeapi.Status{Code: 404}
|
||||||
|
}
|
||||||
|
return &kubeapi.Secret{Data: secret}, nil
|
||||||
|
},
|
||||||
|
CheckSecretPermissionsImpl: func(ctx context.Context, name string) (bool, bool, error) {
|
||||||
|
return tt.allowPatch, true, nil
|
||||||
|
},
|
||||||
|
CreateSecretImpl: func(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
|
if s.Name != tt.wantSecretName {
|
||||||
|
t.Errorf("CreateSecret called with wrong name, got %q, want %q", s.Name, tt.wantSecretName)
|
||||||
|
}
|
||||||
|
secret = s.Data
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
UpdateSecretImpl: func(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
|
if s.Name != tt.wantSecretName {
|
||||||
|
t.Errorf("UpdateSecret called with wrong name, got %q, want %q", s.Name, tt.wantSecretName)
|
||||||
|
}
|
||||||
|
secret = s.Data
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
JSONPatchResourceImpl: func(ctx context.Context, name, resourceType string, patches []kubeclient.JSONPatch) error {
|
||||||
|
if !tt.allowPatch {
|
||||||
|
return &kubeapi.Status{Reason: "Forbidden"}
|
||||||
|
}
|
||||||
|
if name != tt.wantSecretName {
|
||||||
|
t.Errorf("JSONPatchResource called with wrong name, got %q, want %q", name, tt.wantSecretName)
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
secret = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
for _, p := range patches {
|
||||||
|
if p.Op == "add" && p.Path == "/data" {
|
||||||
|
secret = p.Value.(map[string][]byte)
|
||||||
|
} else if p.Op == "add" && strings.HasPrefix(p.Path, "/data/") {
|
||||||
|
key := strings.TrimPrefix(p.Path, "/data/")
|
||||||
|
secret[key] = p.Value.([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Store{
|
||||||
|
client: client,
|
||||||
|
canPatch: tt.allowPatch,
|
||||||
|
secretName: tt.wantSecretName,
|
||||||
|
certShareMode: tt.certShareMode,
|
||||||
|
memory: mem.Store{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.WriteTLSCertAndKey(testDomain, []byte(testCert), []byte(testKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WriteTLSCertAndKey() error = '%v'", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify secret data
|
||||||
|
if diff := cmp.Diff(secret, tt.wantSecretData); diff != "" {
|
||||||
|
t.Errorf("secret data mismatch (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify memory store was updated
|
||||||
|
for key, want := range tt.wantMemoryStore {
|
||||||
|
got, err := s.memory.ReadState(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading from memory store: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !cmp.Equal(got, want) {
|
||||||
|
t.Errorf("memory store key %q = %v, want %v", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadTLSCertAndKey(t *testing.T) {
|
||||||
|
const (
|
||||||
|
testDomain = "my-app.tailnetxyz.ts.net"
|
||||||
|
testCert = "fake-cert"
|
||||||
|
testKey = "fake-key"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
memoryStore map[ipn.StateKey][]byte // pre-existing memory store state
|
||||||
|
certShareMode string
|
||||||
|
domain string
|
||||||
|
secretData map[string][]byte // data to return from mock GetSecret
|
||||||
|
secretGetErr error // error to return from mock GetSecret
|
||||||
|
wantCert []byte
|
||||||
|
wantKey []byte
|
||||||
|
wantErr error
|
||||||
|
// what should end up in memory store after the store is created
|
||||||
|
wantMemoryStore map[ipn.StateKey][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "found",
|
||||||
|
memoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
domain: testDomain,
|
||||||
|
wantCert: []byte(testCert),
|
||||||
|
wantKey: []byte(testKey),
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not_found",
|
||||||
|
domain: testDomain,
|
||||||
|
wantErr: ipn.ErrStateNotExist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_ro_mode_found_in_secret",
|
||||||
|
certShareMode: "ro",
|
||||||
|
domain: testDomain,
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
"tls.crt": []byte(testCert),
|
||||||
|
"tls.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantCert: []byte(testCert),
|
||||||
|
wantKey: []byte(testKey),
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_ro_mode_found_in_memory",
|
||||||
|
certShareMode: "ro",
|
||||||
|
memoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
domain: testDomain,
|
||||||
|
wantCert: []byte(testCert),
|
||||||
|
wantKey: []byte(testKey),
|
||||||
|
wantMemoryStore: map[ipn.StateKey][]byte{
|
||||||
|
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
|
||||||
|
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_ro_mode_not_found",
|
||||||
|
certShareMode: "ro",
|
||||||
|
domain: testDomain,
|
||||||
|
secretGetErr: &kubeapi.Status{Code: 404},
|
||||||
|
wantErr: ipn.ErrStateNotExist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_ro_mode_empty_cert_in_secret",
|
||||||
|
certShareMode: "ro",
|
||||||
|
domain: testDomain,
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
"tls.crt": {},
|
||||||
|
"tls.key": []byte(testKey),
|
||||||
|
},
|
||||||
|
wantErr: ipn.ErrStateNotExist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_share_ro_mode_kube_api_error",
|
||||||
|
certShareMode: "ro",
|
||||||
|
domain: testDomain,
|
||||||
|
secretGetErr: fmt.Errorf("api error"),
|
||||||
|
wantErr: fmt.Errorf("getting TLS Secret %q: api error", sanitizeKey(testDomain)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
client := &kubeclient.FakeClient{
|
||||||
|
GetSecretImpl: func(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
|
if tt.secretGetErr != nil {
|
||||||
|
return nil, tt.secretGetErr
|
||||||
|
}
|
||||||
|
return &kubeapi.Secret{Data: tt.secretData}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Store{
|
||||||
|
client: client,
|
||||||
|
secretName: "ts-state",
|
||||||
|
certShareMode: tt.certShareMode,
|
||||||
|
memory: mem.Store{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize memory store
|
||||||
|
for k, v := range tt.memoryStore {
|
||||||
|
s.memory.WriteState(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotCert, gotKey, err := s.ReadTLSCertAndKey(tt.domain)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ReadTLSCertAndKey() error = nil, want error containing %v", tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr.Error()) {
|
||||||
|
t.Errorf("ReadTLSCertAndKey() error = %v, want error containing %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadTLSCertAndKey() unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(gotCert, tt.wantCert) {
|
||||||
|
t.Errorf("ReadTLSCertAndKey() gotCert = %v, want %v", gotCert, tt.wantCert)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(gotKey, tt.wantKey) {
|
||||||
|
t.Errorf("ReadTLSCertAndKey() gotKey = %v, want %v", gotKey, tt.wantKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify memory store contents after operation
|
||||||
|
if tt.wantMemoryStore != nil {
|
||||||
|
for key, want := range tt.wantMemoryStore {
|
||||||
|
got, err := s.memory.ReadState(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading from memory store: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("memory store key %q = %v, want %v", key, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithClient(t *testing.T) {
|
||||||
|
const (
|
||||||
|
secretName = "ts-state"
|
||||||
|
testCert = "fake-cert"
|
||||||
|
testKey = "fake-key"
|
||||||
|
)
|
||||||
|
|
||||||
|
certSecretsLabels := map[string]string{
|
||||||
|
"tailscale.com/secret-type": "certs",
|
||||||
|
"tailscale.com/managed": "true",
|
||||||
|
"tailscale.com/proxy-group": "ingress-proxies",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create Secret objects for testing
|
||||||
|
makeSecret := func(name string, labels map[string]string, certSuffix string) kubeapi.Secret {
|
||||||
|
return kubeapi.Secret{
|
||||||
|
ObjectMeta: kubeapi.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"tls.crt": []byte(testCert + certSuffix),
|
||||||
|
"tls.key": []byte(testKey + certSuffix),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stateSecretContents map[string][]byte // data in state Secret
|
||||||
|
TLSSecrets []kubeapi.Secret // list of TLS cert Secrets
|
||||||
|
certMode string
|
||||||
|
secretGetErr error // error to return from GetSecret
|
||||||
|
secretsListErr error // error to return from ListSecrets
|
||||||
|
wantMemoryStoreContents map[ipn.StateKey][]byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty_state_secret",
|
||||||
|
stateSecretContents: map[string][]byte{},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "state_secret_not_found",
|
||||||
|
secretGetErr: &kubeapi.Status{Code: 404},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "state_secret_get_error",
|
||||||
|
secretGetErr: fmt.Errorf("some error"),
|
||||||
|
wantErr: fmt.Errorf("error loading state from kube Secret: some error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "load_existing_state",
|
||||||
|
stateSecretContents: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
"baz": []byte("qux"),
|
||||||
|
},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
"baz": []byte("qux"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "load_select_certs_in_read_only_mode",
|
||||||
|
certMode: "ro",
|
||||||
|
stateSecretContents: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
TLSSecrets: []kubeapi.Secret{
|
||||||
|
makeSecret("app1.tailnetxyz.ts.net", certSecretsLabels, "1"),
|
||||||
|
makeSecret("app2.tailnetxyz.ts.net", certSecretsLabels, "2"),
|
||||||
|
makeSecret("some-other-secret", nil, "3"),
|
||||||
|
makeSecret("app3.other-proxies.ts.net", map[string]string{
|
||||||
|
"tailscale.com/secret-type": "certs",
|
||||||
|
"tailscale.com/managed": "true",
|
||||||
|
"tailscale.com/proxy-group": "some-other-proxygroup",
|
||||||
|
}, "4"),
|
||||||
|
},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
"app1.tailnetxyz.ts.net.crt": []byte(testCert + "1"),
|
||||||
|
"app1.tailnetxyz.ts.net.key": []byte(testKey + "1"),
|
||||||
|
"app2.tailnetxyz.ts.net.crt": []byte(testCert + "2"),
|
||||||
|
"app2.tailnetxyz.ts.net.key": []byte(testKey + "2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "load_select_certs_in_read_write_mode",
|
||||||
|
certMode: "rw",
|
||||||
|
stateSecretContents: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
TLSSecrets: []kubeapi.Secret{
|
||||||
|
makeSecret("app1.tailnetxyz.ts.net", certSecretsLabels, "1"),
|
||||||
|
makeSecret("app2.tailnetxyz.ts.net", certSecretsLabels, "2"),
|
||||||
|
makeSecret("some-other-secret", nil, "3"),
|
||||||
|
makeSecret("app3.other-proxies.ts.net", map[string]string{
|
||||||
|
"tailscale.com/secret-type": "certs",
|
||||||
|
"tailscale.com/managed": "true",
|
||||||
|
"tailscale.com/proxy-group": "some-other-proxygroup",
|
||||||
|
}, "4"),
|
||||||
|
},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
"app1.tailnetxyz.ts.net.crt": []byte(testCert + "1"),
|
||||||
|
"app1.tailnetxyz.ts.net.key": []byte(testKey + "1"),
|
||||||
|
"app2.tailnetxyz.ts.net.crt": []byte(testCert + "2"),
|
||||||
|
"app2.tailnetxyz.ts.net.key": []byte(testKey + "2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_cert_secrets_fails",
|
||||||
|
certMode: "ro",
|
||||||
|
stateSecretContents: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
secretsListErr: fmt.Errorf("list error"),
|
||||||
|
// The error is logged but not returned, and state is still loaded
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert_secrets_not_loaded_when_not_in_share_mode",
|
||||||
|
certMode: "",
|
||||||
|
stateSecretContents: map[string][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
TLSSecrets: []kubeapi.Secret{
|
||||||
|
makeSecret("app1.tailnetxyz.ts.net", certSecretsLabels, "1"),
|
||||||
|
},
|
||||||
|
wantMemoryStoreContents: map[ipn.StateKey][]byte{
|
||||||
|
"foo": []byte("bar"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
envknob.Setenv("TS_CERT_SHARE_MODE", tt.certMode)
|
||||||
|
|
||||||
|
t.Setenv("POD_NAME", "ingress-proxies-1")
|
||||||
|
|
||||||
|
client := &kubeclient.FakeClient{
|
||||||
|
GetSecretImpl: func(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
|
if tt.secretGetErr != nil {
|
||||||
|
return nil, tt.secretGetErr
|
||||||
|
}
|
||||||
|
if name == secretName {
|
||||||
|
return &kubeapi.Secret{Data: tt.stateSecretContents}, nil
|
||||||
|
}
|
||||||
|
return nil, &kubeapi.Status{Code: 404}
|
||||||
|
},
|
||||||
|
CheckSecretPermissionsImpl: func(ctx context.Context, name string) (bool, bool, error) {
|
||||||
|
return true, true, nil
|
||||||
|
},
|
||||||
|
ListSecretsImpl: func(ctx context.Context, selector map[string]string) (*kubeapi.SecretList, error) {
|
||||||
|
if tt.secretsListErr != nil {
|
||||||
|
return nil, tt.secretsListErr
|
||||||
|
}
|
||||||
|
var matchingSecrets []kubeapi.Secret
|
||||||
|
for _, secret := range tt.TLSSecrets {
|
||||||
|
matches := true
|
||||||
|
for k, v := range selector {
|
||||||
|
if secret.Labels[k] != v {
|
||||||
|
matches = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches {
|
||||||
|
matchingSecrets = append(matchingSecrets, secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &kubeapi.SecretList{Items: matchingSecrets}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := newWithClient(t.Logf, client, secretName)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("NewWithClient() error = nil, want error containing %v", tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr.Error()) {
|
||||||
|
t.Errorf("NewWithClient() error = %v, want error containing %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewWithClient() unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify memory store contents
|
||||||
|
gotJSON, err := s.memory.ExportToJSON()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ExportToJSON failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var got map[ipn.StateKey][]byte
|
||||||
|
if err := json.Unmarshal(gotJSON, &got); err != nil {
|
||||||
|
t.Errorf("failed to unmarshal memory store JSON: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := tt.wantMemoryStoreContents
|
||||||
|
if want == nil {
|
||||||
|
want = map[ipn.StateKey][]byte{}
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(got, want); diff != "" {
|
||||||
|
t.Errorf("memory store contents mismatch (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,9 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/store/awsstore"
|
"tailscale.com/ipn/store/awsstore"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -14,5 +16,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerAWSStore() {
|
func registerAWSStore() {
|
||||||
Register("arn:", awsstore.New)
|
Register("arn:", func(logf logger.Logf, arg string) (ipn.StateStore, error) {
|
||||||
|
ssmARN, opts, err := awsstore.ParseARNAndOpts(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return awsstore.New(logf, ssmARN, opts...)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,14 @@ type Secret struct {
|
|||||||
Data map[string][]byte `json:"data,omitempty"`
|
Data map[string][]byte `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecretList is a list of Secret objects.
|
||||||
|
type SecretList struct {
|
||||||
|
TypeMeta `json:",inline"`
|
||||||
|
ObjectMeta `json:"metadata"`
|
||||||
|
|
||||||
|
Items []Secret `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Event contains a subset of fields from corev1.Event.
|
// Event contains a subset of fields from corev1.Event.
|
||||||
// https://github.com/kubernetes/api/blob/6cc44b8953ae704d6d9ec2adf32e7ae19199ea9f/core/v1/types.go#L7034
|
// https://github.com/kubernetes/api/blob/6cc44b8953ae704d6d9ec2adf32e7ae19199ea9f/core/v1/types.go#L7034
|
||||||
// It is copied here to avoid having to import kube libraries.
|
// It is copied here to avoid having to import kube libraries.
|
||||||
|
@ -60,6 +60,7 @@ func readFile(n string) ([]byte, error) {
|
|||||||
// It expects to be run inside a cluster.
|
// It expects to be run inside a cluster.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
GetSecret(context.Context, string) (*kubeapi.Secret, error)
|
GetSecret(context.Context, string) (*kubeapi.Secret, error)
|
||||||
|
ListSecrets(context.Context, map[string]string) (*kubeapi.SecretList, error)
|
||||||
UpdateSecret(context.Context, *kubeapi.Secret) error
|
UpdateSecret(context.Context, *kubeapi.Secret) error
|
||||||
CreateSecret(context.Context, *kubeapi.Secret) error
|
CreateSecret(context.Context, *kubeapi.Secret) error
|
||||||
// Event attempts to ensure an event with the specified options associated with the Pod in which we are
|
// Event attempts to ensure an event with the specified options associated with the Pod in which we are
|
||||||
@ -248,21 +249,35 @@ func (c *client) newRequest(ctx context.Context, method, url string, in any) (*h
|
|||||||
// GetSecret fetches the secret from the Kubernetes API.
|
// GetSecret fetches the secret from the Kubernetes API.
|
||||||
func (c *client) GetSecret(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
func (c *client) GetSecret(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
s := &kubeapi.Secret{Data: make(map[string][]byte)}
|
s := &kubeapi.Secret{Data: make(map[string][]byte)}
|
||||||
if err := c.kubeAPIRequest(ctx, "GET", c.resourceURL(name, TypeSecrets), nil, s); err != nil {
|
if err := c.kubeAPIRequest(ctx, "GET", c.resourceURL(name, TypeSecrets, ""), nil, s); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListSecrets fetches the secret from the Kubernetes API.
|
||||||
|
func (c *client) ListSecrets(ctx context.Context, selector map[string]string) (*kubeapi.SecretList, error) {
|
||||||
|
sl := new(kubeapi.SecretList)
|
||||||
|
s := make([]string, 0, len(selector))
|
||||||
|
for key, val := range selector {
|
||||||
|
s = append(s, key+"="+url.QueryEscape(val))
|
||||||
|
}
|
||||||
|
ss := strings.Join(s, ",")
|
||||||
|
if err := c.kubeAPIRequest(ctx, "GET", c.resourceURL("", TypeSecrets, ss), nil, sl); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sl, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateSecret creates a secret in the Kubernetes API.
|
// CreateSecret creates a secret in the Kubernetes API.
|
||||||
func (c *client) CreateSecret(ctx context.Context, s *kubeapi.Secret) error {
|
func (c *client) CreateSecret(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
s.Namespace = c.ns
|
s.Namespace = c.ns
|
||||||
return c.kubeAPIRequest(ctx, "POST", c.resourceURL("", TypeSecrets), s, nil)
|
return c.kubeAPIRequest(ctx, "POST", c.resourceURL("", TypeSecrets, ""), s, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSecret updates a secret in the Kubernetes API.
|
// UpdateSecret updates a secret in the Kubernetes API.
|
||||||
func (c *client) UpdateSecret(ctx context.Context, s *kubeapi.Secret) error {
|
func (c *client) UpdateSecret(ctx context.Context, s *kubeapi.Secret) error {
|
||||||
return c.kubeAPIRequest(ctx, "PUT", c.resourceURL(s.Name, TypeSecrets), s, nil)
|
return c.kubeAPIRequest(ctx, "PUT", c.resourceURL(s.Name, TypeSecrets, ""), s, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONPatch is a JSON patch operation.
|
// JSONPatch is a JSON patch operation.
|
||||||
@ -283,14 +298,14 @@ func (c *client) JSONPatchResource(ctx context.Context, name, typ string, patche
|
|||||||
return fmt.Errorf("unsupported JSON patch operation: %q", p.Op)
|
return fmt.Errorf("unsupported JSON patch operation: %q", p.Op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.kubeAPIRequest(ctx, "PATCH", c.resourceURL(name, typ), patches, nil, setHeader("Content-Type", "application/json-patch+json"))
|
return c.kubeAPIRequest(ctx, "PATCH", c.resourceURL(name, typ, ""), patches, nil, setHeader("Content-Type", "application/json-patch+json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StrategicMergePatchSecret updates a secret in the Kubernetes API using a
|
// StrategicMergePatchSecret updates a secret in the Kubernetes API using a
|
||||||
// strategic merge patch.
|
// strategic merge patch.
|
||||||
// If a fieldManager is provided, it will be used to track the patch.
|
// If a fieldManager is provided, it will be used to track the patch.
|
||||||
func (c *client) StrategicMergePatchSecret(ctx context.Context, name string, s *kubeapi.Secret, fieldManager string) error {
|
func (c *client) StrategicMergePatchSecret(ctx context.Context, name string, s *kubeapi.Secret, fieldManager string) error {
|
||||||
surl := c.resourceURL(name, TypeSecrets)
|
surl := c.resourceURL(name, TypeSecrets, "")
|
||||||
if fieldManager != "" {
|
if fieldManager != "" {
|
||||||
uv := url.Values{
|
uv := url.Values{
|
||||||
"fieldManager": {fieldManager},
|
"fieldManager": {fieldManager},
|
||||||
@ -342,7 +357,7 @@ func (c *client) Event(ctx context.Context, typ, reason, msg string) error {
|
|||||||
LastTimestamp: now,
|
LastTimestamp: now,
|
||||||
Count: 1,
|
Count: 1,
|
||||||
}
|
}
|
||||||
return c.kubeAPIRequest(ctx, "POST", c.resourceURL("", typeEvents), &ev, nil)
|
return c.kubeAPIRequest(ctx, "POST", c.resourceURL("", typeEvents, ""), &ev, nil)
|
||||||
}
|
}
|
||||||
// If the Event already exists, we patch its count and last timestamp. This ensures that when users run 'kubectl
|
// If the Event already exists, we patch its count and last timestamp. This ensures that when users run 'kubectl
|
||||||
// describe pod...', they see the event just once (but with a message of how many times it has appeared over
|
// describe pod...', they see the event just once (but with a message of how many times it has appeared over
|
||||||
@ -472,9 +487,13 @@ func (c *client) checkPermission(ctx context.Context, verb, typ, name string) (b
|
|||||||
// resourceURL returns a URL that can be used to interact with the given resource type and, if name is not empty string,
|
// resourceURL returns a URL that can be used to interact with the given resource type and, if name is not empty string,
|
||||||
// the named resource of that type.
|
// the named resource of that type.
|
||||||
// Note that this only works for core/v1 resource types.
|
// Note that this only works for core/v1 resource types.
|
||||||
func (c *client) resourceURL(name, typ string) string {
|
func (c *client) resourceURL(name, typ, sel string) string {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Sprintf("%s/api/v1/namespaces/%s/%s", c.url, c.ns, typ)
|
url := fmt.Sprintf("%s/api/v1/namespaces/%s/%s", c.url, c.ns, typ)
|
||||||
|
if sel != "" {
|
||||||
|
url += "?labelSelector=" + sel
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s/api/v1/namespaces/%s/%s/%s", c.url, c.ns, typ, name)
|
return fmt.Sprintf("%s/api/v1/namespaces/%s/%s/%s", c.url, c.ns, typ, name)
|
||||||
}
|
}
|
||||||
@ -487,7 +506,7 @@ func (c *client) nameForEvent(reason string) string {
|
|||||||
// getEvent fetches the event from the Kubernetes API.
|
// getEvent fetches the event from the Kubernetes API.
|
||||||
func (c *client) getEvent(ctx context.Context, name string) (*kubeapi.Event, error) {
|
func (c *client) getEvent(ctx context.Context, name string) (*kubeapi.Event, error) {
|
||||||
e := &kubeapi.Event{}
|
e := &kubeapi.Event{}
|
||||||
if err := c.kubeAPIRequest(ctx, "GET", c.resourceURL(name, typeEvents), nil, e); err != nil {
|
if err := c.kubeAPIRequest(ctx, "GET", c.resourceURL(name, typeEvents, ""), nil, e); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
|
@ -15,6 +15,10 @@ var _ Client = &FakeClient{}
|
|||||||
type FakeClient struct {
|
type FakeClient struct {
|
||||||
GetSecretImpl func(context.Context, string) (*kubeapi.Secret, error)
|
GetSecretImpl func(context.Context, string) (*kubeapi.Secret, error)
|
||||||
CheckSecretPermissionsImpl func(ctx context.Context, name string) (bool, bool, error)
|
CheckSecretPermissionsImpl func(ctx context.Context, name string) (bool, bool, error)
|
||||||
|
CreateSecretImpl func(context.Context, *kubeapi.Secret) error
|
||||||
|
UpdateSecretImpl func(context.Context, *kubeapi.Secret) error
|
||||||
|
JSONPatchResourceImpl func(context.Context, string, string, []JSONPatch) error
|
||||||
|
ListSecretsImpl func(context.Context, map[string]string) (*kubeapi.SecretList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fc *FakeClient) CheckSecretPermissions(ctx context.Context, name string) (bool, bool, error) {
|
func (fc *FakeClient) CheckSecretPermissions(ctx context.Context, name string) (bool, bool, error) {
|
||||||
@ -33,8 +37,18 @@ func (fc *FakeClient) Event(context.Context, string, string, string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fc *FakeClient) JSONPatchResource(context.Context, string, string, []JSONPatch) error {
|
func (fc *FakeClient) JSONPatchResource(ctx context.Context, resource, name string, patches []JSONPatch) error {
|
||||||
return nil
|
return fc.JSONPatchResourceImpl(ctx, resource, name, patches)
|
||||||
|
}
|
||||||
|
func (fc *FakeClient) UpdateSecret(ctx context.Context, secret *kubeapi.Secret) error {
|
||||||
|
return fc.UpdateSecretImpl(ctx, secret)
|
||||||
|
}
|
||||||
|
func (fc *FakeClient) CreateSecret(ctx context.Context, secret *kubeapi.Secret) error {
|
||||||
|
return fc.CreateSecretImpl(ctx, secret)
|
||||||
|
}
|
||||||
|
func (fc *FakeClient) ListSecrets(ctx context.Context, selector map[string]string) (*kubeapi.SecretList, error) {
|
||||||
|
if fc.ListSecretsImpl != nil {
|
||||||
|
return fc.ListSecretsImpl(ctx, selector)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (fc *FakeClient) UpdateSecret(context.Context, *kubeapi.Secret) error { return nil }
|
|
||||||
func (fc *FakeClient) CreateSecret(context.Context, *kubeapi.Secret) error { return nil }
|
|
||||||
|
@ -48,4 +48,7 @@ const (
|
|||||||
PodIPv4Header string = "Pod-IPv4"
|
PodIPv4Header string = "Pod-IPv4"
|
||||||
|
|
||||||
EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown"
|
EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown"
|
||||||
|
|
||||||
|
LabelManaged = "tailscale.com/managed"
|
||||||
|
LabelSecretType = "tailscale.com/secret-type" // "config", "state" "certs"
|
||||||
)
|
)
|
||||||
|
35
licenses/README.md
Normal file
35
licenses/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Licenses
|
||||||
|
|
||||||
|
This directory contains a list of dependencies, and their licenses, that are included in the Tailscale clients.
|
||||||
|
These lists are generated using the [go-licenses] tool to analyze all Go packages in the Tailscale binaries,
|
||||||
|
as well as a set of custom output templates that includes any additional non-Go dependencies.
|
||||||
|
For example, the clients for macOS and iOS include some additional Swift libraries.
|
||||||
|
|
||||||
|
These lists are updated roughly every week, so it is possible to see the dependencies in a given release by looking at the release tag.
|
||||||
|
For example, the dependences for the 1.80.0 release of the macOS client can be seen at
|
||||||
|
<https://github.com/tailscale/tailscale/blob/v1.80.0/licenses/apple.md>.
|
||||||
|
|
||||||
|
[go-licenses]: https://github.com/google/go-licenses
|
||||||
|
|
||||||
|
## Other formats
|
||||||
|
|
||||||
|
The go-licenses tool can output other formats like CSV, but that wouldn't include the non-Go dependencies.
|
||||||
|
We can generate a CSV file if that's really needed by running a regex over the markdown files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat apple.md | grep "^ -" | sed -E "s/- \[(.*)\]\(.*?\) \(\[(.*)\]\((.*)\)\)/\1,\2,\3/"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reviewer instructions
|
||||||
|
|
||||||
|
The majority of changes in this directory are from updating dependency versions.
|
||||||
|
In that case, only the URL for the license file will change to reflect the new version.
|
||||||
|
Occasionally, a dependency is added or removed, or the import path is changed.
|
||||||
|
|
||||||
|
New dependencies require the closest review to ensure the license is acceptable.
|
||||||
|
Because we generate the license reports **after** dependencies are changed,
|
||||||
|
the new dependency would have already gone through one review when it was initially added.
|
||||||
|
This is just a secondary review to double-check the license. If in doubt, ask legal.
|
||||||
|
|
||||||
|
Always do a normal GitHub code review on the license PR with a brief summary of what changed.
|
||||||
|
For example, see #13936 or #14064. Then approve and merge the PR.
|
@ -9,34 +9,33 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
|||||||
|
|
||||||
|
|
||||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.1.0/LICENSE))
|
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.1.0/LICENSE))
|
||||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.36.0/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.26.5/config/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.29.5/config/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.16.16/credentials/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.17.58/credentials/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.14.11/feature/ec2/imds/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.16.27/feature/ec2/imds/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.2.10/internal/configsources/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.3.31/internal/configsources/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.5.10/internal/endpoints/v2/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.6.31/internal/endpoints/v2/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.7.2/internal/ini/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.8.2/internal/ini/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/internal/sync/singleflight/LICENSE))
|
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.36.0/internal/sync/singleflight/LICENSE))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.10.4/service/internal/accept-encoding/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.12.2/service/internal/accept-encoding/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.10.10/service/internal/presigned-url/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.12.12/service/internal/presigned-url/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.44.7/service/ssm/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.44.7/service/ssm/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.18.7/service/sso/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.24.14/service/sso/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.21.7/service/ssooidc/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.28.13/service/ssooidc/LICENSE.txt))
|
||||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.26.7/service/sts/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.33.13/service/sts/LICENSE.txt))
|
||||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.19.0/LICENSE))
|
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.2/LICENSE))
|
||||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.19.0/internal/sync/singleflight/LICENSE))
|
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.22.2/internal/sync/singleflight/LICENSE))
|
||||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
|
||||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
||||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.18.0/LICENSE))
|
||||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/6a9a0fde9288/LICENSE))
|
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/d3c622f1b874/LICENSE))
|
||||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
||||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||||
- [github.com/illarion/gonotify/v2](https://pkg.go.dev/github.com/illarion/gonotify/v2) ([MIT](https://github.com/illarion/gonotify/blob/v2.0.3/LICENSE))
|
- [github.com/illarion/gonotify/v3](https://pkg.go.dev/github.com/illarion/gonotify/v3) ([MIT](https://github.com/illarion/gonotify/blob/v3.0.2/LICENSE))
|
||||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
||||||
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
||||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||||
@ -65,17 +64,17 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
|||||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||||
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
|
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
|
||||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/a8ea4be8:LICENSE))
|
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.35.0:LICENSE))
|
||||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b:LICENSE))
|
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/939b2ce7:LICENSE))
|
||||||
- [golang.org/x/mobile](https://pkg.go.dev/golang.org/x/mobile) ([BSD-3-Clause](https://cs.opensource.google/go/x/mobile/+/81131f64:LICENSE))
|
- [golang.org/x/mobile](https://pkg.go.dev/golang.org/x/mobile) ([BSD-3-Clause](https://cs.opensource.google/go/x/mobile/+/81131f64:LICENSE))
|
||||||
- [golang.org/x/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.22.0:LICENSE))
|
- [golang.org/x/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.23.0:LICENSE))
|
||||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.34.0:LICENSE))
|
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.36.0:LICENSE))
|
||||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.10.0:LICENSE))
|
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.11.0:LICENSE))
|
||||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad:LICENSE))
|
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.30.0:LICENSE))
|
||||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.28.0:LICENSE))
|
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.29.0:LICENSE))
|
||||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.21.0:LICENSE))
|
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.22.0:LICENSE))
|
||||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.9.0:LICENSE))
|
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.10.0:LICENSE))
|
||||||
- [golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools) ([BSD-3-Clause](https://cs.opensource.google/go/x/tools/+/v0.29.0:LICENSE))
|
- [golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools) ([BSD-3-Clause](https://cs.opensource.google/go/x/tools/+/v0.30.0:LICENSE))
|
||||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
|
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
||||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](Unknown))
|
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](Unknown))
|
||||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||||
|
@ -28,20 +28,19 @@ See also the dependencies in the [Tailscale CLI][].
|
|||||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.33.13/service/sts/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.33.13/service/sts/LICENSE.txt))
|
||||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.2/LICENSE))
|
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.2/LICENSE))
|
||||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.22.2/internal/sync/singleflight/LICENSE))
|
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.22.2/internal/sync/singleflight/LICENSE))
|
||||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
|
||||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
||||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.18.0/LICENSE))
|
||||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/6a9a0fde9288/LICENSE))
|
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/d3c622f1b874/LICENSE))
|
||||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
||||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
||||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||||
- [github.com/illarion/gonotify/v2](https://pkg.go.dev/github.com/illarion/gonotify/v2) ([MIT](https://github.com/illarion/gonotify/blob/v2.0.3/LICENSE))
|
- [github.com/illarion/gonotify/v3](https://pkg.go.dev/github.com/illarion/gonotify/v3) ([MIT](https://github.com/illarion/gonotify/blob/v3.0.2/LICENSE))
|
||||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/15c9b8791914/LICENSE))
|
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/15c9b8791914/LICENSE))
|
||||||
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
||||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||||
@ -69,14 +68,14 @@ See also the dependencies in the [Tailscale CLI][].
|
|||||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/a8ea4be8:LICENSE))
|
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.35.0:LICENSE))
|
||||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b:LICENSE))
|
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/939b2ce7:LICENSE))
|
||||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.34.0:LICENSE))
|
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.36.0:LICENSE))
|
||||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.10.0:LICENSE))
|
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.11.0:LICENSE))
|
||||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad:LICENSE))
|
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.30.0:LICENSE))
|
||||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.28.0:LICENSE))
|
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.29.0:LICENSE))
|
||||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.21.0:LICENSE))
|
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.22.0:LICENSE))
|
||||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.9.0:LICENSE))
|
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.10.0:LICENSE))
|
||||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
||||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ Some packages may only be included on certain architectures or operating systems
|
|||||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.33.13/service/sts/LICENSE.txt))
|
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.33.13/service/sts/LICENSE.txt))
|
||||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.2/LICENSE))
|
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.22.2/LICENSE))
|
||||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.22.2/internal/sync/singleflight/LICENSE))
|
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.22.2/internal/sync/singleflight/LICENSE))
|
||||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
|
||||||
- [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) ([ISC](https://github.com/coder/websocket/blob/v1.8.12/LICENSE.txt))
|
- [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) ([ISC](https://github.com/coder/websocket/blob/v1.8.12/LICENSE.txt))
|
||||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||||
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.23/LICENSE))
|
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.23/LICENSE))
|
||||||
@ -41,8 +40,8 @@ Some packages may only be included on certain architectures or operating systems
|
|||||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
||||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.18.0/LICENSE))
|
||||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/6a9a0fde9288/LICENSE))
|
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/d3c622f1b874/LICENSE))
|
||||||
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
|
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
|
||||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||||
@ -52,7 +51,7 @@ Some packages may only be included on certain architectures or operating systems
|
|||||||
- [github.com/gorilla/csrf](https://pkg.go.dev/github.com/gorilla/csrf) ([BSD-3-Clause](https://github.com/gorilla/csrf/blob/9dd6af1f6d30/LICENSE))
|
- [github.com/gorilla/csrf](https://pkg.go.dev/github.com/gorilla/csrf) ([BSD-3-Clause](https://github.com/gorilla/csrf/blob/9dd6af1f6d30/LICENSE))
|
||||||
- [github.com/gorilla/securecookie](https://pkg.go.dev/github.com/gorilla/securecookie) ([BSD-3-Clause](https://github.com/gorilla/securecookie/blob/v1.1.2/LICENSE))
|
- [github.com/gorilla/securecookie](https://pkg.go.dev/github.com/gorilla/securecookie) ([BSD-3-Clause](https://github.com/gorilla/securecookie/blob/v1.1.2/LICENSE))
|
||||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||||
- [github.com/illarion/gonotify/v2](https://pkg.go.dev/github.com/illarion/gonotify/v2) ([MIT](https://github.com/illarion/gonotify/blob/v2.0.3/LICENSE))
|
- [github.com/illarion/gonotify/v3](https://pkg.go.dev/github.com/illarion/gonotify/v3) ([MIT](https://github.com/illarion/gonotify/blob/v3.0.2/LICENSE))
|
||||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
||||||
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
- [github.com/jellydator/ttlcache/v3](https://pkg.go.dev/github.com/jellydator/ttlcache/v3) ([MIT](https://github.com/jellydator/ttlcache/blob/v3.1.0/LICENSE))
|
||||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||||
@ -91,15 +90,15 @@ Some packages may only be included on certain architectures or operating systems
|
|||||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/a8ea4be8:LICENSE))
|
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.35.0:LICENSE))
|
||||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b:LICENSE))
|
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/939b2ce7:LICENSE))
|
||||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.34.0:LICENSE))
|
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.36.0:LICENSE))
|
||||||
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.25.0:LICENSE))
|
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.26.0:LICENSE))
|
||||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.10.0:LICENSE))
|
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.11.0:LICENSE))
|
||||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad:LICENSE))
|
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.30.0:LICENSE))
|
||||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.28.0:LICENSE))
|
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.29.0:LICENSE))
|
||||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.21.0:LICENSE))
|
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.22.0:LICENSE))
|
||||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.9.0:LICENSE))
|
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.10.0:LICENSE))
|
||||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
||||||
|
@ -35,7 +35,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
|||||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/b75a8a7d7eb0/LICENSE))
|
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/b75a8a7d7eb0/LICENSE))
|
||||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.7.0/LICENSE))
|
||||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/6a9a0fde9288/LICENSE))
|
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/d3c622f1b874/LICENSE))
|
||||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
||||||
@ -62,23 +62,23 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
|||||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/4d49adab4de7/LICENSE))
|
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/4d49adab4de7/LICENSE))
|
||||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/72f92d5087d4/LICENSE))
|
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/b2c15a420186/LICENSE))
|
||||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/cfd3289ef17f/LICENSE))
|
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/5992cb43ca35/LICENSE))
|
||||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
|
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
|
||||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
|
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
|
||||||
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
|
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
|
||||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/a8ea4be8:LICENSE))
|
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.35.0:LICENSE))
|
||||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b:LICENSE))
|
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/939b2ce7:LICENSE))
|
||||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.23.0:LICENSE))
|
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.24.0:LICENSE))
|
||||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.22.0:LICENSE))
|
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.23.0:LICENSE))
|
||||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.34.0:LICENSE))
|
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.36.0:LICENSE))
|
||||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.10.0:LICENSE))
|
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.11.0:LICENSE))
|
||||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad:LICENSE))
|
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.30.0:LICENSE))
|
||||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.28.0:LICENSE))
|
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.29.0:LICENSE))
|
||||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.21.0:LICENSE))
|
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.22.0:LICENSE))
|
||||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||||
- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.35.1/LICENSE))
|
- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.35.1/LICENSE))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user