Compare commits
3 Commits
icio/testw
...
percy/move
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b6a337039 | ||
|
|
e6a5c1ee16 | ||
|
|
58a312edca |
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.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -80,4 +80,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
|
||||
|
||||
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1
|
||||
uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6.2.0
|
||||
with:
|
||||
version: v1.60
|
||||
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- goarch: amd64
|
||||
coverflags: "-coverprofile=/tmp/coverage.out"
|
||||
- goarch: amd64
|
||||
buildflags: "-race"
|
||||
shard: '1/3'
|
||||
@@ -118,10 +119,15 @@ jobs:
|
||||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: test all
|
||||
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
|
||||
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ${{matrix.coverflags}} ./... ${{matrix.buildflags}}
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
TS_TEST_SHARD: ${{ matrix.shard }}
|
||||
- name: Publish to coveralls.io
|
||||
if: matrix.coverflags != '' # only publish results if we've tracked coverage
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
with:
|
||||
path-to-profile: /tmp/coverage.out
|
||||
- name: bench all
|
||||
run: ./tool/go test ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ $(for x in $(git grep -l "^func Benchmark" | xargs dirname | sort | uniq); do echo "./$x"; done)
|
||||
env:
|
||||
|
||||
@@ -26,9 +26,9 @@ import (
|
||||
"github.com/atotto/clipboard"
|
||||
dbus "github.com/godbus/dbus/v5"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/stringsx"
|
||||
@@ -67,7 +67,7 @@ func (menu *Menu) Run() {
|
||||
type Menu struct {
|
||||
mu sync.Mutex // protects the entire Menu
|
||||
|
||||
lc local.Client
|
||||
lc tailscale.LocalClient
|
||||
status *ipnstate.Status
|
||||
curProfile ipn.LoginProfile
|
||||
allProfiles []ipn.LoginProfile
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
)
|
||||
|
||||
// DNSNameServers is returned when retrieving the list of nameservers.
|
||||
|
||||
@@ -11,14 +11,13 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var lc local.Client
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "<h1>Hello from Tailscale!</h1> It works.")
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
// ErrPeerNotFound is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
var ErrPeerNotFound = local.ErrPeerNotFound
|
||||
|
||||
// LocalClient is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type LocalClient = local.Client
|
||||
|
||||
// IPNBusWatcher is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type IPNBusWatcher = local.IPNBusWatcher
|
||||
|
||||
// BugReportOpts is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type BugReportOpts = local.BugReportOpts
|
||||
|
||||
// DebugPortMapOpts is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type DebugPortmapOpts = local.DebugPortmapOpts
|
||||
|
||||
// PingOpts is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type PingOpts = local.PingOpts
|
||||
|
||||
// GetCertificate is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return local.GetCertificate(hi)
|
||||
}
|
||||
|
||||
// SetVersionMismatchHandler is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
||||
local.SetVersionMismatchHandler(f)
|
||||
}
|
||||
|
||||
// IsAccessDeniedError is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func IsAccessDeniedError(err error) bool {
|
||||
return local.IsAccessDeniedError(err)
|
||||
}
|
||||
|
||||
// IsPreconditionsFailedError is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func IsPreconditionsFailedError(err error) bool {
|
||||
return local.IsPreconditionsFailedError(err)
|
||||
}
|
||||
|
||||
// WhoIs is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
return local.WhoIs(ctx, remoteAddr)
|
||||
}
|
||||
|
||||
// Status is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return local.Status(ctx)
|
||||
}
|
||||
|
||||
// StatusWithoutPeers is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return local.StatusWithoutPeers(ctx)
|
||||
}
|
||||
|
||||
// CertPair is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return local.CertPair(ctx, domain)
|
||||
}
|
||||
|
||||
// ExpandSNIName is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return local.ExpandSNIName(ctx, name)
|
||||
}
|
||||
7
client/tailscale/localclient_stub.go
Normal file
7
client/tailscale/localclient_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailscale
|
||||
|
||||
// LocalClient has moved to tailscale.com/localclient/tailscale.
|
||||
type LocalClient struct{}
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
// Package tailscale contains a Go client for the Tailscale control plane API.
|
||||
//
|
||||
// This package is only intended for internal and transitional use.
|
||||
//
|
||||
// Deprecated: the official control plane client is available at
|
||||
// tailscale.com/client/tailscale/v2.
|
||||
// Deprecated: This package is no longer maintained. Use
|
||||
// tailscale.com/client/tailscale/v2 instead.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
@@ -20,7 +18,10 @@ import (
|
||||
)
|
||||
|
||||
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
||||
// for now. This package is being replaced by tailscale.com/client/tailscale/v2.
|
||||
// for now. It was added 2022-04-29 when it was moved to this git repo
|
||||
// and will be removed when the public API has settled.
|
||||
//
|
||||
// TODO(bradfitz): remove this after the we're happy with the public API.
|
||||
var I_Acknowledge_This_API_Is_Unstable = false
|
||||
|
||||
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
|
||||
@@ -98,8 +99,6 @@ func (c *Client) setAuth(r *http.Request) {
|
||||
// If httpClient is nil, then http.DefaultClient is used.
|
||||
// "api.tailscale.com" is set as the BaseURL for the returned client
|
||||
// and can be changed manually by the user.
|
||||
//
|
||||
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
||||
func NewClient(tailnet string, auth AuthMethod) *Client {
|
||||
return &Client{
|
||||
tailnet: tailnet,
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/envknob/featureknob"
|
||||
@@ -31,6 +29,8 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/licenses"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -50,7 +50,7 @@ type Server struct {
|
||||
mode ServerMode
|
||||
|
||||
logf logger.Logf
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
timeNow func() time.Time
|
||||
|
||||
// devMode indicates that the server run with frontend assets
|
||||
@@ -125,9 +125,9 @@ type ServerOpts struct {
|
||||
// PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
|
||||
PathPrefix string
|
||||
|
||||
// LocalClient is the local.Client to use for this web server.
|
||||
// LocalClient is the tailscale.LocalClient to use for this web server.
|
||||
// If nil, a new one will be created.
|
||||
LocalClient *local.Client
|
||||
LocalClient *tailscale.LocalClient
|
||||
|
||||
// TimeNow optionally provides a time function.
|
||||
// time.Now is used as default.
|
||||
@@ -166,7 +166,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
return nil, fmt.Errorf("invalid Mode provided")
|
||||
}
|
||||
if opts.LocalClient == nil {
|
||||
opts.LocalClient = &local.Client{}
|
||||
opts.LocalClient = &tailscale.LocalClient{}
|
||||
}
|
||||
s = &Server{
|
||||
mode: opts.Mode,
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/net/memnet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/views"
|
||||
@@ -120,7 +120,7 @@ func TestServeAPI(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
mode: ManageServerMode,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: time.Now,
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
timeNow: time.Now,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
}
|
||||
|
||||
// Add some browser sessions to cache state.
|
||||
@@ -457,7 +457,7 @@ func TestAuthorizeRequest(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
mode: ManageServerMode,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: time.Now,
|
||||
}
|
||||
validCookie := "ts-cookie"
|
||||
@@ -572,7 +572,7 @@ func TestServeAuth(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
mode: ManageServerMode,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: func() time.Time { return timeNow },
|
||||
newAuthURL: mockNewAuthURL,
|
||||
waitAuthURL: mockWaitAuthURL,
|
||||
@@ -914,7 +914,7 @@ func TestServeAPIAuthMetricLogging(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
mode: ManageServerMode,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: func() time.Time { return timeNow },
|
||||
newAuthURL: mockNewAuthURL,
|
||||
waitAuthURL: mockWaitAuthURL,
|
||||
@@ -1126,7 +1126,7 @@ func TestRequireTailscaleIP(t *testing.T) {
|
||||
|
||||
s := &Server{
|
||||
mode: ManageServerMode,
|
||||
lc: &local.Client{Dial: lal.Dial},
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: time.Now,
|
||||
logf: t.Logf,
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpver"
|
||||
"tailscale.com/version"
|
||||
@@ -170,12 +169,6 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
type updateFunction func() error
|
||||
|
||||
func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
hi := hostinfo.New()
|
||||
// We don't know how to update custom tsnet binaries, it's up to the user.
|
||||
if hi.Package == "tsnet" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return up.updateWindows, true
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
)
|
||||
|
||||
// metrics is a simple metrics HTTP server, if enabled it forwards requests to
|
||||
// the tailscaled's LocalAPI usermetrics endpoint at /localapi/v0/usermetrics.
|
||||
type metrics struct {
|
||||
debugEndpoint string
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
}
|
||||
|
||||
func proxy(w http.ResponseWriter, r *http.Request, url string, do func(*http.Request) (*http.Response, error)) {
|
||||
@@ -68,7 +68,7 @@ func (m *metrics) handleDebug(w http.ResponseWriter, r *http.Request) {
|
||||
// In 1.78.x and 1.80.x, it also proxies debug paths to tailscaled's debug
|
||||
// endpoint if configured to ease migration for a breaking change serving user
|
||||
// metrics instead of debug metrics on the "metrics" port.
|
||||
func metricsHandlers(mux *http.ServeMux, lc *local.Client, debugAddrPort string) {
|
||||
func metricsHandlers(mux *http.ServeMux, lc *tailscale.LocalClient, debugAddrPort string) {
|
||||
m := &metrics{
|
||||
lc: lc,
|
||||
debugEndpoint: debugAddrPort,
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
// 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
|
||||
// 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, path string, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *tailscale.LocalClient, kc *kubeClient) {
|
||||
if certDomainAtomic == nil {
|
||||
panic("certDomainAtomic must not be nil")
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func certDomainFromNetmap(nm *netmap.NetworkMap) string {
|
||||
return nm.DNS.CertDomains[0]
|
||||
}
|
||||
|
||||
// localClient is a subset of [local.Client] that can be mocked for testing.
|
||||
// localClient is a subset of tailscale.LocalClient that can be mocked for testing.
|
||||
type localClient interface {
|
||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
func TestUpdateServeConfig(t *testing.T) {
|
||||
@@ -197,7 +197,7 @@ func TestReadServeConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeLocalClient struct {
|
||||
*local.Client
|
||||
*tailscale.LocalClient
|
||||
setServeCalled bool
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/kube/egressservices"
|
||||
"tailscale.com/kube/kubeclient"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/httpm"
|
||||
@@ -50,7 +50,7 @@ type egressProxy struct {
|
||||
kc kubeclient.Client // never nil
|
||||
stateSecret string // name of the kube state Secret
|
||||
|
||||
tsClient *local.Client // never nil
|
||||
tsClient *tailscale.LocalClient // never nil
|
||||
|
||||
netmapChan chan ipn.Notify // chan to receive netmap updates on
|
||||
|
||||
@@ -131,7 +131,7 @@ type egressProxyRunOpts struct {
|
||||
cfgPath string
|
||||
nfr linuxfw.NetfilterRunner
|
||||
kc kubeclient.Client
|
||||
tsClient *local.Client
|
||||
tsClient *tailscale.LocalClient
|
||||
stateSecret string
|
||||
netmapChan chan ipn.Notify
|
||||
podIPv4 string
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
func startTailscaled(ctx context.Context, cfg *settings) (*local.Client, *os.Process, error) {
|
||||
func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, *os.Process, error) {
|
||||
args := tailscaledArgs(cfg)
|
||||
// tailscaled runs without context, since it needs to persist
|
||||
// beyond the startup timeout in ctx.
|
||||
@@ -54,7 +54,7 @@ func startTailscaled(ctx context.Context, cfg *settings) (*local.Client, *os.Pro
|
||||
break
|
||||
}
|
||||
|
||||
tsClient := &local.Client{
|
||||
tsClient := &tailscale.LocalClient{
|
||||
Socket: cfg.Socket,
|
||||
UseSocketOnly: true,
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func tailscaleSet(ctx context.Context, cfg *settings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Client, errCh chan<- error) {
|
||||
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *tailscale.LocalClient, errCh chan<- error) {
|
||||
var (
|
||||
tickChan <-chan time.Time
|
||||
tailscaledCfgDir = filepath.Dir(path)
|
||||
|
||||
@@ -51,11 +51,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
|
||||
github.com/tailscale/setec/client/setec from tailscale.com/cmd/derper
|
||||
github.com/tailscale/setec/types/api from github.com/tailscale/setec/client/setec
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/local+
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/netipx from tailscale.com/net/tsaddr
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
|
||||
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
|
||||
@@ -88,19 +86,18 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
tailscale.com from tailscale.com/version
|
||||
💣 tailscale.com/atomicfile from tailscale.com/cmd/derper+
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/derp from tailscale.com/cmd/derper+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/derper
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/drive from tailscale.com/client/local+
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/drive from tailscale.com/ipn+
|
||||
tailscale.com/envknob from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial+
|
||||
tailscale.com/hostinfo from tailscale.com/net/netmon+
|
||||
tailscale.com/ipn from tailscale.com/client/local
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn from tailscale.com/localclient/tailscale
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
tailscale.com/localclient/tailscale from tailscale.com/derp
|
||||
tailscale.com/localclient/tailscale/apitype from tailscale.com/localclient/tailscale
|
||||
tailscale.com/metrics from tailscale.com/cmd/derper+
|
||||
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
@@ -109,7 +106,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/local
|
||||
tailscale.com/net/netutil from tailscale.com/localclient/tailscale
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/net/stunserver
|
||||
tailscale.com/net/stunserver from tailscale.com/cmd/derper
|
||||
@@ -119,11 +116,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/cmd/derper
|
||||
tailscale.com/paths from tailscale.com/client/local
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/local
|
||||
tailscale.com/paths from tailscale.com/localclient/tailscale
|
||||
💣 tailscale.com/safesocket from tailscale.com/localclient/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/tka from tailscale.com/client/local+
|
||||
tailscale.com/tailcfg from tailscale.com/derp+
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnstate+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
@@ -134,17 +131,17 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/tailcfg+
|
||||
tailscale.com/types/key from tailscale.com/client/local+
|
||||
tailscale.com/types/key from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/opt from tailscale.com/envknob+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/client/local+
|
||||
tailscale.com/types/tkatype from tailscale.com/localclient/tailscale+
|
||||
tailscale.com/types/views from tailscale.com/ipn+
|
||||
tailscale.com/util/cibuild from tailscale.com/health
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netmon+
|
||||
@@ -155,7 +152,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/health+
|
||||
@@ -210,7 +206,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sync/singleflight from github.com/tailscale/setec/client/setec
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/argon2+
|
||||
LD golang.org/x/sys/unix from github.com/google/nftables+
|
||||
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -37,7 +36,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/setec/client/setec"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
@@ -66,9 +64,6 @@ var (
|
||||
|
||||
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.")
|
||||
secretsURL = flag.String("secrets-url", "", "SETEC server URL for secrets retrieval of mesh key")
|
||||
secretPrefix = flag.String("secrets-path-prefix", "prod/derp", "setec path prefix for \""+setecMeshKeyName+"\" secret for DERP mesh key")
|
||||
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")
|
||||
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.")
|
||||
@@ -89,14 +84,8 @@ var (
|
||||
var (
|
||||
tlsRequestVersion = &metrics.LabelMap{Label: "version"}
|
||||
tlsActiveVersion = &metrics.LabelMap{Label: "version"}
|
||||
|
||||
// Exactly 64 hexadecimal lowercase digits.
|
||||
validMeshKey = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
)
|
||||
|
||||
const setecMeshKeyName = "meshkey"
|
||||
const meshKeyEnvVar = "TAILSCALE_DERPER_MESH_KEY"
|
||||
|
||||
func init() {
|
||||
expvar.Publish("derper_tls_request_version", tlsRequestVersion)
|
||||
expvar.Publish("gauge_derper_tls_active_version", tlsActiveVersion)
|
||||
@@ -152,14 +141,6 @@ func writeNewConfig() config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func checkMeshKey(key string) (string, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if !validMeshKey.MatchString(key) {
|
||||
return "", errors.New("key must contain exactly 64 hex digits")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
@@ -196,55 +177,18 @@ func main() {
|
||||
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
|
||||
s.SetTCPWriteTimeout(*tcpWriteTimeout)
|
||||
|
||||
var meshKey string
|
||||
if *dev {
|
||||
meshKey = os.Getenv(meshKeyEnvVar)
|
||||
if meshKey == "" {
|
||||
log.Printf("No mesh key specified for dev via %s\n", meshKeyEnvVar)
|
||||
} else {
|
||||
log.Printf("Set mesh key from %s\n", meshKeyEnvVar)
|
||||
}
|
||||
} else if *secretsURL != "" {
|
||||
meshKeySecret := path.Join(*secretPrefix, setecMeshKeyName)
|
||||
fc, err := setec.NewFileCache(*secretsCacheDir)
|
||||
if *meshPSKFile != "" {
|
||||
b, err := os.ReadFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatalf("NewFileCache: %v", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Setting up setec store from %q", *secretsURL)
|
||||
st, err := setec.NewStore(ctx,
|
||||
setec.StoreConfig{
|
||||
Client: setec.Client{Server: *secretsURL},
|
||||
Secrets: []string{
|
||||
meshKeySecret,
|
||||
},
|
||||
Cache: fc,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("NewStore: %v", err)
|
||||
key := strings.TrimSpace(string(b))
|
||||
if matched, _ := regexp.MatchString(`(?i)^[0-9a-f]{64,}$`, key); !matched {
|
||||
log.Fatalf("key in %s must contain 64+ hex digits", *meshPSKFile)
|
||||
}
|
||||
meshKey = st.Secret(meshKeySecret).GetString()
|
||||
log.Println("Got mesh key from setec store")
|
||||
st.Close()
|
||||
} else if *meshPSKFile != "" {
|
||||
b, err := setec.StaticFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatalf("StaticFile failed to get key: %v", err)
|
||||
}
|
||||
log.Println("Got mesh key from static file")
|
||||
meshKey = b.GetString()
|
||||
}
|
||||
|
||||
if meshKey == "" && *dev {
|
||||
log.Printf("No mesh key configured for --dev mode")
|
||||
} else if meshKey == "" {
|
||||
log.Printf("No mesh key configured")
|
||||
} else if key, err := checkMeshKey(meshKey); err != nil {
|
||||
log.Fatalf("invalid mesh key: %v", err)
|
||||
} else {
|
||||
s.SetMeshKey(key)
|
||||
log.Println("DERP mesh key configured")
|
||||
log.Printf("DERP mesh key configured")
|
||||
}
|
||||
|
||||
if err := startMesh(s); err != nil {
|
||||
log.Fatalf("startMesh: %v", err)
|
||||
}
|
||||
@@ -438,10 +382,6 @@ func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
return errors.New("invalid hostname")
|
||||
}
|
||||
|
||||
func defaultSetecCacheDir() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".cache", "derper-secrets")
|
||||
}
|
||||
|
||||
func defaultMeshPSKFile() string {
|
||||
try := []string{
|
||||
"/home/derp/keys/derp-mesh.key",
|
||||
|
||||
@@ -138,46 +138,3 @@ func TestTemplate(t *testing.T) {
|
||||
t.Error("Output is missing debug info")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMeshKey(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "KeyOkay",
|
||||
input: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
|
||||
want: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TrimKeyOkay",
|
||||
input: " f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6 ",
|
||||
want: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "NotAKey",
|
||||
input: "zzthisisnotakey",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k, err := checkMeshKey(tt.input)
|
||||
if err != nil && !tt.wantErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if k != tt.want && err == nil {
|
||||
t.Errorf("want: %s doesn't match expected: %s", tt.want, k)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
// client lives in the same repo as this code.
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
reusable := flag.Bool("reusable", false, "allocate a reusable authkey")
|
||||
ephemeral := flag.Bool("ephemeral", false, "allocate an ephemeral authkey")
|
||||
preauth := flag.Bool("preauth", true, "set the authkey as pre-authorized")
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,7 +31,7 @@ var (
|
||||
//go:embed hello.tmpl.html
|
||||
var embeddedTemplate string
|
||||
|
||||
var localClient local.Client
|
||||
var localClient tailscale.LocalClient
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
@@ -9,6 +9,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
@@ -30,12 +31,10 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
L github.com/aws/aws-sdk-go-v2/internal/auth from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/auth/smithy from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/context from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/internal/middleware from github.com/aws/aws-sdk-go-v2/service/sso+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||
@@ -70,17 +69,16 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/private/requestcompression from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/tracing from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
|
||||
@@ -98,8 +96,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
💣 github.com/fsnotify/fsnotify from sigs.k8s.io/controller-runtime/pkg/certwatcher
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka+
|
||||
github.com/gaissmai/bart from tailscale.com/net/ipset+
|
||||
github.com/gaissmai/bart/internal/bitset from github.com/gaissmai/bart+
|
||||
github.com/gaissmai/bart/internal/sparse from github.com/gaissmai/bart
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt+
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
@@ -143,8 +139,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
||||
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
||||
L 💣 github.com/illarion/gonotify/v2 from tailscale.com/net/dns
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/feature/tap
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -237,7 +232,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
go.uber.org/zap/internal/pool from go.uber.org/zap+
|
||||
go.uber.org/zap/internal/stacktrace from go.uber.org/zap
|
||||
go.uber.org/zap/zapcore from github.com/go-logr/zapr+
|
||||
💣 go4.org/mem from tailscale.com/client/local+
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
|
||||
@@ -298,7 +293,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
@@ -782,9 +777,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
LW tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
@@ -800,8 +793,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/drive from tailscale.com/client/local+
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/drive from tailscale.com/ipn+
|
||||
tailscale.com/envknob from tailscale.com/client/web+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
||||
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
||||
@@ -811,14 +804,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
|
||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||
tailscale.com/ipn from tailscale.com/client/local+
|
||||
tailscale.com/ipn from tailscale.com/client/web+
|
||||
tailscale.com/ipn/conffile 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/ipnlocal from tailscale.com/ipn/localapi+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/web+
|
||||
tailscale.com/ipn/localapi from tailscale.com/tsnet+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
@@ -837,6 +828,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/kube/kubeclient from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/kube/kubetypes from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/licenses from tailscale.com/client/web
|
||||
tailscale.com/localclient/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/localclient/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/ipn/ipnlocal+
|
||||
@@ -865,7 +858,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/client/local+
|
||||
tailscale.com/net/netutil from tailscale.com/client/web+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
@@ -883,19 +876,19 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/tstun from tailscale.com/tsd+
|
||||
tailscale.com/omit from tailscale.com/ipn/conffile
|
||||
tailscale.com/paths from tailscale.com/client/local+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/local+
|
||||
💣 tailscale.com/safesocket from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/sessionrecording from tailscale.com/k8s-operator/sessionrecording+
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/tailcfg from tailscale.com/client/web+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tempfork/httprec from tailscale.com/control/controlclient
|
||||
tailscale.com/tka from tailscale.com/client/local+
|
||||
tailscale.com/tka from tailscale.com/control/controlclient+
|
||||
tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tsd from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tsnet from tailscale.com/cmd/k8s-operator+
|
||||
@@ -907,7 +900,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/ipn+
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/client/local+
|
||||
tailscale.com/types/key from tailscale.com/control/controlbase+
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
@@ -920,7 +913,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/tkatype from tailscale.com/client/local+
|
||||
tailscale.com/types/tkatype from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/views from tailscale.com/appc+
|
||||
tailscale.com/util/cibuild from tailscale.com/health
|
||||
tailscale.com/util/clientmetric from tailscale.com/cmd/k8s-operator+
|
||||
@@ -1019,7 +1012,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from github.com/prometheus-community/pro-bing+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
|
||||
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socks from golang.org/x/net/proxy
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
kzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,6 +64,7 @@ func TestMain(m *testing.M) {
|
||||
func runTests(m *testing.M) (int, error) {
|
||||
zlog := kzap.NewRaw([]kzap.Opts{kzap.UseDevMode(true), kzap.Level(zapcore.DebugLevel)}...).Sugar()
|
||||
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
if clientID := os.Getenv("TS_API_CLIENT_ID"); clientID != "" {
|
||||
cleanup, err := setupClientAndACLs()
|
||||
|
||||
@@ -46,9 +46,6 @@ const (
|
||||
FinalizerNamePG = "tailscale.com/ingress-pg-finalizer"
|
||||
|
||||
indexIngressProxyGroup = ".metadata.annotations.ingress-proxy-group"
|
||||
// annotationHTTPEndpoint can be used to configure the Ingress to expose an HTTP endpoint to tailnet (as
|
||||
// well as the default HTTPS endpoint).
|
||||
annotationHTTPEndpoint = "tailscale.com/http-endpoint"
|
||||
)
|
||||
|
||||
var gaugePGIngressResources = clientmetric.NewGauge(kubetypes.MetricIngressPGResourceCount)
|
||||
@@ -205,16 +202,16 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
|
||||
// 3. Ensure that the serve config for the ProxyGroup contains the VIPService
|
||||
cm, cfg, err := a.proxyGroupServeConfig(ctx, pgName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting Ingress serve config: %w", err)
|
||||
return fmt.Errorf("error getting ingress serve config: %w", err)
|
||||
}
|
||||
if cm == nil {
|
||||
logger.Infof("no Ingress serve config ConfigMap found, unable to update serve config. Ensure that ProxyGroup is healthy.")
|
||||
logger.Infof("no ingress serve config ConfigMap found, unable to update serve config. Ensure that ProxyGroup is healthy.")
|
||||
return nil
|
||||
}
|
||||
ep := ipn.HostPort(fmt.Sprintf("%s:443", dnsName))
|
||||
handlers, err := handlersForIngress(ctx, ing, a.Client, a.recorder, dnsName, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get handlers for Ingress: %w", err)
|
||||
return fmt.Errorf("failed to get handlers for ingress: %w", err)
|
||||
}
|
||||
ingCfg := &ipn.ServiceConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
@@ -228,19 +225,6 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add HTTP endpoint if configured.
|
||||
if isHTTPEndpointEnabled(ing) {
|
||||
logger.Infof("exposing Ingress over HTTP")
|
||||
epHTTP := ipn.HostPort(fmt.Sprintf("%s:80", dnsName))
|
||||
ingCfg.TCP[80] = &ipn.TCPPortHandler{
|
||||
HTTP: true,
|
||||
}
|
||||
ingCfg.Web[epHTTP] = &ipn.WebServerConfig{
|
||||
Handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
var gotCfg *ipn.ServiceConfig
|
||||
if cfg != nil && cfg.Services != nil {
|
||||
gotCfg = cfg.Services[serviceName]
|
||||
@@ -264,23 +248,16 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
|
||||
tags = strings.Split(tstr, ",")
|
||||
}
|
||||
|
||||
vipPorts := []string{"443"} // always 443 for Ingress
|
||||
if isHTTPEndpointEnabled(ing) {
|
||||
vipPorts = append(vipPorts, "80")
|
||||
}
|
||||
|
||||
vipSvc := &VIPService{
|
||||
Name: serviceName,
|
||||
Tags: tags,
|
||||
Ports: vipPorts,
|
||||
Ports: []string{"443"}, // always 443 for Ingress
|
||||
Comment: fmt.Sprintf(VIPSvcOwnerRef, ing.UID),
|
||||
}
|
||||
if existingVIPSvc != nil {
|
||||
vipSvc.Addrs = existingVIPSvc.Addrs
|
||||
}
|
||||
if existingVIPSvc == nil ||
|
||||
!reflect.DeepEqual(vipSvc.Tags, existingVIPSvc.Tags) ||
|
||||
!reflect.DeepEqual(vipSvc.Ports, existingVIPSvc.Ports) {
|
||||
if existingVIPSvc == nil || !reflect.DeepEqual(vipSvc.Tags, existingVIPSvc.Tags) {
|
||||
logger.Infof("Ensuring VIPService %q exists and is up to date", hostname)
|
||||
if err := a.tsClient.createOrUpdateVIPService(ctx, vipSvc); err != nil {
|
||||
logger.Infof("error creating VIPService: %v", err)
|
||||
@@ -290,22 +267,16 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
|
||||
|
||||
// 5. Update Ingress status
|
||||
oldStatus := ing.Status.DeepCopy()
|
||||
ports := []networkingv1.IngressPortStatus{
|
||||
{
|
||||
Protocol: "TCP",
|
||||
Port: 443,
|
||||
},
|
||||
}
|
||||
if isHTTPEndpointEnabled(ing) {
|
||||
ports = append(ports, networkingv1.IngressPortStatus{
|
||||
Protocol: "TCP",
|
||||
Port: 80,
|
||||
})
|
||||
}
|
||||
// TODO(irbekrm): once we have ingress ProxyGroup, we can determine if instances are ready to route traffic to the VIPService
|
||||
ing.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{
|
||||
{
|
||||
Hostname: dnsName,
|
||||
Ports: ports,
|
||||
Ports: []networkingv1.IngressPortStatus{
|
||||
{
|
||||
Protocol: "TCP",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if apiequality.Semantic.DeepEqual(oldStatus, ing.Status) {
|
||||
@@ -598,11 +569,3 @@ func (a *IngressPGReconciler) deleteVIPServiceIfExists(ctx context.Context, name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isHTTPEndpointEnabled returns true if the Ingress has been configured to expose an HTTP endpoint to tailnet.
|
||||
func isHTTPEndpointEnabled(ing *networkingv1.Ingress) bool {
|
||||
if ing == nil {
|
||||
return false
|
||||
}
|
||||
return ing.Annotations[annotationHTTPEndpoint] == "enabled"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"slices"
|
||||
@@ -20,18 +18,81 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestIngressPGReconciler(t *testing.T) {
|
||||
ingPGR, fc, ft := setupIngressTest(t)
|
||||
tsIngressClass := &networkingv1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
|
||||
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
|
||||
}
|
||||
|
||||
// Pre-create the ProxyGroup
|
||||
pg := &tsapi.ProxyGroup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pg",
|
||||
Generation: 1,
|
||||
},
|
||||
Spec: tsapi.ProxyGroupSpec{
|
||||
Type: tsapi.ProxyGroupTypeIngress,
|
||||
},
|
||||
}
|
||||
|
||||
// Pre-create the ConfigMap for the ProxyGroup
|
||||
pgConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pg-ingress-config",
|
||||
Namespace: "operator-ns",
|
||||
},
|
||||
BinaryData: map[string][]byte{
|
||||
"serve-config.json": []byte(`{"Services":{}}`),
|
||||
},
|
||||
}
|
||||
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pg, pgConfigMap, tsIngressClass).
|
||||
WithStatusSubresource(pg).
|
||||
Build()
|
||||
mustUpdateStatus(t, fc, "", pg.Name, func(pg *tsapi.ProxyGroup) {
|
||||
pg.Status.Conditions = []metav1.Condition{
|
||||
{
|
||||
Type: string(tsapi.ProxyGroupReady),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: 1,
|
||||
},
|
||||
}
|
||||
})
|
||||
ft := &fakeTSClient{}
|
||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lc := &fakeLocalClient{
|
||||
status: &ipnstate.Status{
|
||||
CurrentTailnet: &ipnstate.TailnetStatus{
|
||||
MagicDNSSuffix: "ts.net",
|
||||
},
|
||||
},
|
||||
}
|
||||
ingPGR := &IngressPGReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
tsnetServer: fakeTsnetServer,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
tsNamespace: "operator-ns",
|
||||
logger: zl.Sugar(),
|
||||
recorder: record.NewFakeRecorder(10),
|
||||
lc: lc,
|
||||
}
|
||||
|
||||
// Test 1: Default tags
|
||||
ing := &networkingv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -61,74 +122,8 @@ func TestIngressPGReconciler(t *testing.T) {
|
||||
|
||||
// Verify initial reconciliation
|
||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||
|
||||
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
|
||||
})
|
||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||
|
||||
// Verify VIPService uses custom tags
|
||||
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 created")
|
||||
}
|
||||
wantTags := []string{"tag:custom", "tag:test"} // custom tags only
|
||||
gotTags := slices.Clone(vipSvc.Tags)
|
||||
slices.Sort(gotTags)
|
||||
slices.Sort(wantTags)
|
||||
if !slices.Equal(gotTags, wantTags) {
|
||||
t.Errorf("incorrect VIPService tags: got %v, want %v", gotTags, wantTags)
|
||||
}
|
||||
|
||||
// Create second Ingress
|
||||
ing2 := &networkingv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-other-ingress",
|
||||
Namespace: "default",
|
||||
UID: types.UID("5678-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-other-svc.tailnetxyz.ts.net"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
mustCreate(t, fc, ing2)
|
||||
|
||||
// Verify second Ingress reconciliation
|
||||
expectReconciled(t, ingPGR, "default", "my-other-ingress")
|
||||
verifyServeConfig(t, fc, "svc:my-other-svc", false)
|
||||
verifyVIPService(t, ft, "svc:my-other-svc", []string{"443"})
|
||||
|
||||
// Verify first Ingress is still working
|
||||
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||
|
||||
// Delete second Ingress
|
||||
if err := fc.Delete(context.Background(), ing2); err != nil {
|
||||
t.Fatalf("deleting second Ingress: %v", err)
|
||||
}
|
||||
expectReconciled(t, ingPGR, "default", "my-other-ingress")
|
||||
|
||||
// Verify second Ingress cleanup
|
||||
// Get and verify the ConfigMap was updated
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||
Name: "test-pg-ingress-config",
|
||||
@@ -142,16 +137,46 @@ func TestIngressPGReconciler(t *testing.T) {
|
||||
t.Fatalf("unmarshaling serve config: %v", err)
|
||||
}
|
||||
|
||||
// Verify first Ingress is still configured
|
||||
if cfg.Services["svc:my-svc"] == nil {
|
||||
t.Error("first Ingress service config was incorrectly removed")
|
||||
}
|
||||
// Verify second Ingress was cleaned up
|
||||
if cfg.Services["svc:my-other-svc"] != nil {
|
||||
t.Error("second Ingress service config was not cleaned up")
|
||||
t.Error("expected serve config to contain VIPService configuration")
|
||||
}
|
||||
|
||||
// Delete the first Ingress and verify cleanup
|
||||
// Verify VIPService uses default tags
|
||||
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 created")
|
||||
}
|
||||
wantTags := []string{"tag:k8s"} // default tags
|
||||
if !slices.Equal(vipSvc.Tags, wantTags) {
|
||||
t.Errorf("incorrect VIPService tags: got %v, want %v", vipSvc.Tags, wantTags)
|
||||
}
|
||||
|
||||
// Test 2: Custom tags
|
||||
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
|
||||
})
|
||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||
|
||||
// Verify VIPService uses custom tags
|
||||
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 created")
|
||||
}
|
||||
wantTags = []string{"tag:custom", "tag:test"} // custom tags only
|
||||
gotTags := slices.Clone(vipSvc.Tags)
|
||||
slices.Sort(gotTags)
|
||||
slices.Sort(wantTags)
|
||||
if !slices.Equal(gotTags, wantTags) {
|
||||
t.Errorf("incorrect VIPService tags: got %v, want %v", gotTags, wantTags)
|
||||
}
|
||||
|
||||
// Delete the Ingress and verify cleanup
|
||||
if err := fc.Delete(context.Background(), ing); err != nil {
|
||||
t.Fatalf("deleting Ingress: %v", err)
|
||||
}
|
||||
@@ -310,233 +335,3 @@ func TestValidateIngress(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
|
||||
ingPGR, fc, ft := setupIngressTest(t)
|
||||
|
||||
// Create test Ingress with HTTP endpoint enabled
|
||||
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",
|
||||
"tailscale.com/http-endpoint": "enabled",
|
||||
},
|
||||
},
|
||||
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"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := fc.Create(context.Background(), ing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify initial reconciliation with HTTP enabled
|
||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||
verifyVIPService(t, ft, "svc:my-svc", []string{"80", "443"})
|
||||
verifyServeConfig(t, fc, "svc:my-svc", true)
|
||||
|
||||
// Verify Ingress status
|
||||
ing = &networkingv1.Ingress{}
|
||||
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
}, ing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantStatus := []networkingv1.IngressPortStatus{
|
||||
{Port: 443, Protocol: "TCP"},
|
||||
{Port: 80, Protocol: "TCP"},
|
||||
}
|
||||
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
|
||||
t.Errorf("incorrect status ports: got %v, want %v",
|
||||
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
|
||||
}
|
||||
|
||||
// Remove HTTP endpoint annotation
|
||||
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||
delete(ing.Annotations, "tailscale.com/http-endpoint")
|
||||
})
|
||||
|
||||
// Verify reconciliation after removing HTTP
|
||||
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
|
||||
verifyServeConfig(t, fc, "svc:my-svc", false)
|
||||
|
||||
// Verify Ingress status
|
||||
ing = &networkingv1.Ingress{}
|
||||
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
}, ing); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantStatus = []networkingv1.IngressPortStatus{
|
||||
{Port: 443, Protocol: "TCP"},
|
||||
}
|
||||
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
|
||||
t.Errorf("incorrect status ports: got %v, want %v",
|
||||
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyVIPService(t *testing.T, ft *fakeTSClient, serviceName string, wantPorts []string) {
|
||||
t.Helper()
|
||||
vipSvc, err := ft.getVIPService(context.Background(), tailcfg.ServiceName(serviceName))
|
||||
if err != nil {
|
||||
t.Fatalf("getting VIPService %q: %v", serviceName, err)
|
||||
}
|
||||
if vipSvc == nil {
|
||||
t.Fatalf("VIPService %q not created", serviceName)
|
||||
}
|
||||
gotPorts := slices.Clone(vipSvc.Ports)
|
||||
slices.Sort(gotPorts)
|
||||
slices.Sort(wantPorts)
|
||||
if !slices.Equal(gotPorts, wantPorts) {
|
||||
t.Errorf("incorrect ports for VIPService %q: got %v, want %v", serviceName, gotPorts, wantPorts)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyServeConfig(t *testing.T, fc client.Client, serviceName string, wantHTTP bool) {
|
||||
t.Helper()
|
||||
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||
Name: "test-pg-ingress-config",
|
||||
Namespace: "operator-ns",
|
||||
}, cm); err != nil {
|
||||
t.Fatalf("getting ConfigMap: %v", err)
|
||||
}
|
||||
|
||||
cfg := &ipn.ServeConfig{}
|
||||
if err := json.Unmarshal(cm.BinaryData["serve-config.json"], cfg); err != nil {
|
||||
t.Fatalf("unmarshaling serve config: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Looking for service %q in config: %+v", serviceName, cfg)
|
||||
|
||||
svc := cfg.Services[tailcfg.ServiceName(serviceName)]
|
||||
if svc == nil {
|
||||
t.Fatalf("service %q not found in serve config, services: %+v", serviceName, maps.Keys(cfg.Services))
|
||||
}
|
||||
|
||||
wantHandlers := 1
|
||||
if wantHTTP {
|
||||
wantHandlers = 2
|
||||
}
|
||||
|
||||
// Check TCP handlers
|
||||
if len(svc.TCP) != wantHandlers {
|
||||
t.Errorf("incorrect number of TCP handlers for service %q: got %d, want %d", serviceName, len(svc.TCP), wantHandlers)
|
||||
}
|
||||
if wantHTTP {
|
||||
if h, ok := svc.TCP[uint16(80)]; !ok {
|
||||
t.Errorf("HTTP (port 80) handler not found for service %q", serviceName)
|
||||
} else if !h.HTTP {
|
||||
t.Errorf("HTTP not enabled for port 80 handler for service %q", serviceName)
|
||||
}
|
||||
}
|
||||
if h, ok := svc.TCP[uint16(443)]; !ok {
|
||||
t.Errorf("HTTPS (port 443) handler not found for service %q", serviceName)
|
||||
} else if !h.HTTPS {
|
||||
t.Errorf("HTTPS not enabled for port 443 handler for service %q", serviceName)
|
||||
}
|
||||
|
||||
// Check Web handlers
|
||||
if len(svc.Web) != wantHandlers {
|
||||
t.Errorf("incorrect number of Web handlers for service %q: got %d, want %d", serviceName, len(svc.Web), wantHandlers)
|
||||
}
|
||||
}
|
||||
|
||||
func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeTSClient) {
|
||||
t.Helper()
|
||||
|
||||
tsIngressClass := &networkingv1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
|
||||
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
|
||||
}
|
||||
|
||||
// Pre-create the ProxyGroup
|
||||
pg := &tsapi.ProxyGroup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pg",
|
||||
Generation: 1,
|
||||
},
|
||||
Spec: tsapi.ProxyGroupSpec{
|
||||
Type: tsapi.ProxyGroupTypeIngress,
|
||||
},
|
||||
}
|
||||
|
||||
// Pre-create the ConfigMap for the ProxyGroup
|
||||
pgConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pg-ingress-config",
|
||||
Namespace: "operator-ns",
|
||||
},
|
||||
BinaryData: map[string][]byte{
|
||||
"serve-config.json": []byte(`{"Services":{}}`),
|
||||
},
|
||||
}
|
||||
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pg, pgConfigMap, tsIngressClass).
|
||||
WithStatusSubresource(pg).
|
||||
Build()
|
||||
|
||||
// Set ProxyGroup status to ready
|
||||
pg.Status.Conditions = []metav1.Condition{
|
||||
{
|
||||
Type: string(tsapi.ProxyGroupReady),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: 1,
|
||||
},
|
||||
}
|
||||
if err := fc.Status().Update(context.Background(), pg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ft := &fakeTSClient{}
|
||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lc := &fakeLocalClient{
|
||||
status: &ipnstate.Status{
|
||||
CurrentTailnet: &ipnstate.TailnetStatus{
|
||||
MagicDNSSuffix: "ts.net",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ingPGR := &IngressPGReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
tsnetServer: fakeTsnetServer,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
tsNamespace: "operator-ns",
|
||||
logger: zl.Sugar(),
|
||||
recorder: record.NewFakeRecorder(10),
|
||||
lc: lc,
|
||||
}
|
||||
|
||||
return ingPGR, fc, ft
|
||||
}
|
||||
|
||||
@@ -39,12 +39,13 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"tailscale.com/client/tailscale"
|
||||
remoteclient "tailscale.com/client/tailscale"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/kubestore"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -189,9 +190,9 @@ waitOnline:
|
||||
if loginDone {
|
||||
break
|
||||
}
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
caps := remoteclient.KeyCapabilities{
|
||||
Devices: remoteclient.KeyDeviceCapabilities{
|
||||
Create: remoteclient.KeyDeviceCreateCapabilities{
|
||||
Reusable: false,
|
||||
Preauthorized: true,
|
||||
Tags: strings.Split(operatorTags, ","),
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
ksr "tailscale.com/k8s-operator/sessionrecording"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -189,7 +189,7 @@ func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredL
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type apiserverProxy struct {
|
||||
log *zap.SugaredLogger
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
rp *httputil.ReverseProxy
|
||||
|
||||
mode apiServerProxyMode
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
@@ -27,10 +27,12 @@ import (
|
||||
"github.com/inetaf/tcpproxy"
|
||||
"github.com/peterbourgon/ff/v3"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/client/local"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -138,6 +140,26 @@ func main() {
|
||||
}
|
||||
// TODO(raggi): this is not a public interface or guarantee.
|
||||
ns := ts.Sys().Netstack.Get().(*netstack.Impl)
|
||||
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
|
||||
Min: tcp.MinBufferSize,
|
||||
Default: tcp.DefaultReceiveBufferSize,
|
||||
Max: tcp.MaxBufferSize,
|
||||
}
|
||||
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt); err != nil {
|
||||
log.Fatalf("could not set TCP RX buf size: %v", err)
|
||||
}
|
||||
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
|
||||
Min: tcp.MinBufferSize,
|
||||
Default: tcp.DefaultSendBufferSize,
|
||||
Max: tcp.MaxBufferSize,
|
||||
}
|
||||
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt); err != nil {
|
||||
log.Fatalf("could not set TCP TX buf size: %v", err)
|
||||
}
|
||||
mslOpt := tcpip.TCPTimeWaitTimeoutOption(5 * time.Second)
|
||||
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &mslOpt); err != nil {
|
||||
log.Fatalf("could not set TCP MSL: %v", err)
|
||||
}
|
||||
if *debugPort != 0 {
|
||||
expvar.Publish("netstack", ns.ExpVar())
|
||||
}
|
||||
@@ -164,9 +186,9 @@ func main() {
|
||||
type connector struct {
|
||||
// ts is the tsnet.Server used to host the connector.
|
||||
ts *tsnet.Server
|
||||
// lc is the local.Client used to interact with the tsnet.Server hosting this
|
||||
// lc is the LocalClient used to interact with the tsnet.Server hosting this
|
||||
// connector.
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
|
||||
// dnsAddr is the IPv4 address to listen on for DNS requests. It is used to
|
||||
// prevent the app connector from assigning it to a domain.
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-systemd/activation"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tsweb"
|
||||
@@ -105,7 +105,7 @@ type proxy struct {
|
||||
upstreamHost string // "my.database.com"
|
||||
upstreamCertPool *x509.CertPool
|
||||
downstreamCert []tls.Certificate
|
||||
client *local.Client
|
||||
client *tailscale.LocalClient
|
||||
|
||||
activeSessions expvar.Int
|
||||
startedSessions expvar.Int
|
||||
@@ -115,7 +115,7 @@ type proxy struct {
|
||||
// newProxy returns a proxy that forwards connections to
|
||||
// upstreamAddr. The upstream's TLS session is verified using the CA
|
||||
// cert(s) in upstreamCAPath.
|
||||
func newProxy(upstreamAddr, upstreamCAPath string, client *local.Client) (*proxy, error) {
|
||||
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
|
||||
bs, err := os.ReadFile(upstreamCAPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
@@ -127,7 +127,7 @@ func main() {
|
||||
log.Fatal(http.Serve(ln, proxy))
|
||||
}
|
||||
|
||||
func modifyRequest(req *http.Request, localClient *local.Client) {
|
||||
func modifyRequest(req *http.Request, localClient *tailscale.LocalClient) {
|
||||
// with enable_login_token set to true, we get a cookie that handles
|
||||
// auth for paths that are not /login
|
||||
if req.URL.Path != "/login" {
|
||||
@@ -144,7 +144,7 @@ func modifyRequest(req *http.Request, localClient *local.Client) {
|
||||
req.Header.Set("X-Webauth-Name", user.DisplayName)
|
||||
}
|
||||
|
||||
func getTailscaleUser(ctx context.Context, localClient *local.Client, ipPort string) (*tailcfg.UserProfile, error) {
|
||||
func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, ipPort string) (*tailcfg.UserProfile, error) {
|
||||
whois, err := localClient.WhoIs(ctx, ipPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tsweb"
|
||||
@@ -183,7 +183,7 @@ func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, pro
|
||||
type sniproxy struct {
|
||||
srv Server
|
||||
ts *tsnet.Server
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
}
|
||||
|
||||
func (s *sniproxy) advertiseRoutesFromConfig(ctx context.Context, c *appctype.AppConnectorConfig) error {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
var bugReportCmd = &ffcli.Command{
|
||||
|
||||
@@ -21,10 +21,9 @@ import (
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -80,7 +79,7 @@ func CleanUpArgs(args []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
var localClient = local.Client{
|
||||
var localClient = tailscale.LocalClient{
|
||||
Socket: paths.DefaultTailscaledSocket(),
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ import (
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
|
||||
@@ -25,10 +25,9 @@ import (
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -269,77 +268,46 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
fts, err := localClient.FileTargets(ctx)
|
||||
if err != nil {
|
||||
// This likely means tailscaled is unreachable or returned an error on /localapi/v0/status.
|
||||
return "", false, fmt.Errorf("failed to get local status: %w", err)
|
||||
return "", false, err
|
||||
}
|
||||
if st == nil {
|
||||
// Handle the case if the daemon returns nil with no error.
|
||||
return "", false, errors.New("no status available")
|
||||
}
|
||||
if st.Self == nil {
|
||||
// We have a status structure, but it doesn’t include Self info. Probably not connected.
|
||||
return "", false, errors.New("local node is not configured or missing Self information")
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.Addr() != ip {
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return n.StableID, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
}
|
||||
|
||||
// Find the PeerStatus that corresponds to ip.
|
||||
var foundPeer *ipnstate.PeerStatus
|
||||
peerLoop:
|
||||
for _, ps := range st.Peer {
|
||||
for _, pip := range ps.TailscaleIPs {
|
||||
if pip == ip {
|
||||
foundPeer = ps
|
||||
break peerLoop
|
||||
// fileTargetErrorDetail returns a non-nil error saying why ip is an
|
||||
// invalid file sharing target.
|
||||
func fileTargetErrorDetail(ctx context.Context, ip netip.Addr) error {
|
||||
found := false
|
||||
if st, err := localClient.Status(ctx); err == nil && st.Self != nil {
|
||||
for _, peer := range st.Peer {
|
||||
for _, pip := range peer.TailscaleIPs {
|
||||
if pip == ip {
|
||||
found = true
|
||||
if peer.UserID != st.Self.UserID {
|
||||
return errors.New("owned by different user; can only send files to your own devices")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn’t find a matching peer at all:
|
||||
if foundPeer == nil {
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return "", false, fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
|
||||
}
|
||||
return "", false, errors.New("unknown target; not in your Tailnet")
|
||||
if found {
|
||||
return errors.New("target seems to be running an old Tailscale version")
|
||||
}
|
||||
|
||||
// We found a peer. Decide whether we can send files to it:
|
||||
isOffline = !foundPeer.Online
|
||||
|
||||
switch foundPeer.TaildropTarget {
|
||||
case ipnstate.TaildropTargetAvailable:
|
||||
return foundPeer.ID, isOffline, nil
|
||||
|
||||
case ipnstate.TaildropTargetNoNetmapAvailable:
|
||||
return "", isOffline, errors.New("cannot send files: no netmap available on this node")
|
||||
|
||||
case ipnstate.TaildropTargetIpnStateNotRunning:
|
||||
return "", isOffline, errors.New("cannot send files: local Tailscale is not connected to the tailnet")
|
||||
|
||||
case ipnstate.TaildropTargetMissingCap:
|
||||
return "", isOffline, errors.New("cannot send files: missing required Taildrop capability")
|
||||
|
||||
case ipnstate.TaildropTargetOffline:
|
||||
return "", isOffline, errors.New("cannot send files: peer is offline")
|
||||
|
||||
case ipnstate.TaildropTargetNoPeerInfo:
|
||||
return "", isOffline, errors.New("cannot send files: invalid or unrecognized peer")
|
||||
|
||||
case ipnstate.TaildropTargetUnsupportedOS:
|
||||
return "", isOffline, errors.New("cannot send files: target's OS does not support Taildrop")
|
||||
|
||||
case ipnstate.TaildropTargetNoPeerAPI:
|
||||
return "", isOffline, errors.New("cannot send files: target is not advertising a file sharing API")
|
||||
|
||||
case ipnstate.TaildropTargetOwnedByOtherUser:
|
||||
return "", isOffline, errors.New("cannot send files: peer is owned by a different user")
|
||||
|
||||
case ipnstate.TaildropTargetUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return "", isOffline, fmt.Errorf("cannot send files: unknown or indeterminate reason")
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
|
||||
}
|
||||
return errors.New("unknown target; not in your Tailnet")
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/version"
|
||||
@@ -130,7 +130,7 @@ func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.Fla
|
||||
}
|
||||
|
||||
// localServeClient is an interface conforming to the subset of
|
||||
// local.Client. It includes only the methods used by the
|
||||
// tailscale.LocalClient. It includes only the methods used by the
|
||||
// serve command.
|
||||
//
|
||||
// The purpose of this interface is to allow tests to provide a mock.
|
||||
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -850,7 +850,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeLocalServeClient is a fake local.Client for tests.
|
||||
// fakeLocalServeClient is a fake tailscale.LocalClient for tests.
|
||||
// It's not a full implementation, just enough to test the serve command.
|
||||
//
|
||||
// The fake client is stateful, and is used to test manipulating
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/slicesx"
|
||||
|
||||
@@ -84,6 +84,10 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
// of failing. But for now:
|
||||
return fmt.Errorf("no system 'ssh' command found: %w", err)
|
||||
}
|
||||
tailscaleBin, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
knownHostsFile, err := writeKnownHosts(st)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -112,9 +116,7 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
|
||||
argv = append(argv,
|
||||
"-o", fmt.Sprintf("ProxyCommand %q %s nc %%h %%p",
|
||||
// os.Executable() would return the real running binary but in case tailscale is built with the ts_include_cli tag,
|
||||
// we need to return the started symlink instead
|
||||
os.Args[0],
|
||||
tailscaleBin,
|
||||
socketArg,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/internal/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -1097,6 +1097,12 @@ func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
// client lives in the same repo as this code.
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
}
|
||||
|
||||
// resolveAuthKey either returns v unchanged (in the common case) or, if it
|
||||
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
|
||||
//
|
||||
|
||||
@@ -60,7 +60,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/local+
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/net/tsaddr
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
@@ -70,9 +70,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
||||
tailscale.com from tailscale.com/version
|
||||
💣 tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
LW tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
@@ -86,19 +84,20 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/drive from tailscale.com/client/local+
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/drive from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/envknob from tailscale.com/client/web+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web
|
||||
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/health from tailscale.com/net/tlsdial+
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/internal/client/tailscale from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/internal/noiseconn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn from tailscale.com/client/local+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn from tailscale.com/client/web+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/web+
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
tailscale.com/licenses from tailscale.com/client/web+
|
||||
tailscale.com/localclient/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/localclient/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/captivedetection from tailscale.com/net/netcheck
|
||||
@@ -111,7 +110,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/local+
|
||||
tailscale.com/net/netutil from tailscale.com/client/web+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
@@ -121,12 +120,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/paths from tailscale.com/client/local+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/local+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/tailcfg from tailscale.com/client/web+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tka from tailscale.com/client/local+
|
||||
tailscale.com/tka from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
@@ -135,7 +134,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/ipn+
|
||||
tailscale.com/types/key from tailscale.com/client/local+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/lazy from tailscale.com/util/testenv+
|
||||
tailscale.com/types/logger from tailscale.com/client/web+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn+
|
||||
@@ -213,7 +212,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/net/http2/hpack from net/http+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
|
||||
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socks from golang.org/x/net/proxy
|
||||
|
||||
@@ -10,6 +10,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
@@ -31,12 +32,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/internal/auth from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/auth/smithy from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/context from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/internal/middleware from github.com/aws/aws-sdk-go-v2/service/sso+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||
@@ -71,16 +70,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/private/requestcompression from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/tracing from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
|
||||
@@ -92,8 +90,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun+
|
||||
github.com/gaissmai/bart/internal/bitset from github.com/gaissmai/bart+
|
||||
github.com/gaissmai/bart/internal/sparse from github.com/gaissmai/bart
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt+
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
@@ -115,8 +111,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
|
||||
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
|
||||
L 💣 github.com/illarion/gonotify/v2 from tailscale.com/net/dns
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/feature/tap
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -186,7 +181,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/local+
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from github.com/tailscale/wf+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
@@ -210,7 +205,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
@@ -235,9 +230,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/local from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
LW tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
@@ -254,12 +246,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/drive from tailscale.com/client/local+
|
||||
tailscale.com/drive from tailscale.com/drive/driveimpl+
|
||||
tailscale.com/drive/driveimpl from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/drive/driveimpl/compositedav from tailscale.com/drive/driveimpl
|
||||
tailscale.com/drive/driveimpl/dirfs from tailscale.com/drive/driveimpl+
|
||||
tailscale.com/drive/driveimpl/shared from tailscale.com/drive/driveimpl+
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob from tailscale.com/client/web+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
||||
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
||||
@@ -270,13 +262,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||
tailscale.com/ipn from tailscale.com/client/local+
|
||||
tailscale.com/ipn from tailscale.com/client/web+
|
||||
tailscale.com/ipn/conffile 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/ipnlocal from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/web+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
@@ -287,6 +278,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L tailscale.com/kube/kubeclient from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
tailscale.com/licenses from tailscale.com/client/web
|
||||
tailscale.com/localclient/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/localclient/tailscale/apitype from tailscale.com/client/web+
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
@@ -314,7 +307,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/client/local+
|
||||
tailscale.com/net/netutil from tailscale.com/client/web+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
@@ -332,21 +325,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/omit from tailscale.com/ipn/conffile
|
||||
tailscale.com/paths from tailscale.com/client/local+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/local+
|
||||
💣 tailscale.com/safesocket from tailscale.com/cmd/tailscaled+
|
||||
LD tailscale.com/sessionrecording from tailscale.com/ssh/tailssh
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||
tailscale.com/tailcfg from tailscale.com/client/web+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tempfork/httprec from tailscale.com/control/controlclient
|
||||
tailscale.com/tka from tailscale.com/client/local+
|
||||
tailscale.com/tka from tailscale.com/control/controlclient+
|
||||
tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/control/controlclient+
|
||||
@@ -358,14 +351,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/empty from tailscale.com/ipn+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/client/local+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/ipn/localapi+
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/ptr from tailscale.com/control/controlclient+
|
||||
@@ -386,7 +379,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web+
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/httpm from tailscale.com/client/web+
|
||||
tailscale.com/util/lineiter from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
@@ -470,7 +463,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
|
||||
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
|
||||
golang.org/x/net/internal/socks from golang.org/x/net/proxy
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
@@ -42,6 +41,7 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
@@ -621,7 +621,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
if root := lb.TailscaleVarRoot(); root != "" {
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
||||
}
|
||||
lb.ConfigureWebClient(&local.Client{
|
||||
lb.ConfigureWebClient(&tailscale.LocalClient{
|
||||
Socket: args.socketpath,
|
||||
UseSocketOnly: args.socketpath != paths.DefaultTailscaledSocket(),
|
||||
})
|
||||
|
||||
@@ -44,7 +44,6 @@ import (
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/desktop"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
@@ -336,13 +335,6 @@ func beWindowsSubprocess() bool {
|
||||
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(log.Printf))
|
||||
|
||||
if sessionManager, err := desktop.NewSessionManager(log.Printf); err == nil {
|
||||
sys.Set(sessionManager)
|
||||
} else {
|
||||
// Errors creating the session manager are unexpected, but not fatal.
|
||||
log.Printf("[unexpected]: error creating a desktop session manager: %v", err)
|
||||
}
|
||||
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
package flakytest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -38,14 +38,7 @@ func Mark(t testing.TB, issue string) {
|
||||
// We're being run under cmd/testwrapper so send our sentinel message
|
||||
// to stderr. (We avoid doing this when the env is absent to avoid
|
||||
// spamming people running tests without the wrapper)
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
// FIXME: this won't catch panics because t.Failed() won't yet
|
||||
// be correctly set. https://github.com/golang/go/issues/49929
|
||||
root, _, _ := strings.Cut(t.Name(), "/")
|
||||
t.Logf("flakytest: retry: %s %s", root, strings.Join(os.Args, " "))
|
||||
}
|
||||
})
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
|
||||
}
|
||||
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
|
||||
}
|
||||
|
||||
@@ -41,32 +41,3 @@ func TestFlakeRun(t *testing.T) {
|
||||
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlakePanic is a test that panics when run in the testwrapper
|
||||
// for the first time, but succeeds on the second run.
|
||||
// It's used to test whether the testwrapper retries flaky tests.
|
||||
func TestFlakeExit(t *testing.T) {
|
||||
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
|
||||
e := os.Getenv(FlakeAttemptEnv)
|
||||
if e == "" {
|
||||
t.Skip("not running in testwrapper")
|
||||
}
|
||||
if e == "1" {
|
||||
t.Log("First run in testwrapper, failing so exiting so test is retried. This is expected.")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlakePanic is a test that panics when run in the testwrapper
|
||||
// for the first time, but succeeds on the second run.
|
||||
// It's used to test whether the testwrapper retries flaky tests.
|
||||
func TestFlakePanic(t *testing.T) {
|
||||
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
|
||||
e := os.Getenv(FlakeAttemptEnv)
|
||||
if e == "" {
|
||||
t.Skip("not running in testwrapper")
|
||||
}
|
||||
if e == "1" {
|
||||
panic("First run in testwrapper, failing so that test is retried. This is expected.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/dave/courtney/scanner"
|
||||
"github.com/dave/courtney/shared"
|
||||
"github.com/dave/courtney/tester"
|
||||
"github.com/dave/patsy"
|
||||
"github.com/dave/patsy/vos"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
@@ -191,7 +197,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
|
||||
return nil
|
||||
}
|
||||
|
||||
func _main() {
|
||||
func main() {
|
||||
goTestArgs, packages, testArgs, err := splitArgs(os.Args[1:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -232,6 +238,30 @@ func _main() {
|
||||
fmt.Printf("%s\t%s\t%.3fs\n", outcome, pkg, runtime.Seconds())
|
||||
}
|
||||
|
||||
// Check for -coverprofile argument and filter it out
|
||||
combinedCoverageFilename := ""
|
||||
filteredGoTestArgs := make([]string, 0, len(goTestArgs))
|
||||
preceededByCoverProfile := false
|
||||
for _, arg := range goTestArgs {
|
||||
if arg == "-coverprofile" {
|
||||
preceededByCoverProfile = true
|
||||
} else if preceededByCoverProfile {
|
||||
combinedCoverageFilename = strings.TrimSpace(arg)
|
||||
preceededByCoverProfile = false
|
||||
} else {
|
||||
filteredGoTestArgs = append(filteredGoTestArgs, arg)
|
||||
}
|
||||
}
|
||||
goTestArgs = filteredGoTestArgs
|
||||
|
||||
runningWithCoverage := combinedCoverageFilename != ""
|
||||
if runningWithCoverage {
|
||||
fmt.Printf("Will log coverage to %v\n", combinedCoverageFilename)
|
||||
}
|
||||
|
||||
// Keep track of all test coverage files. With each retry, we'll end up
|
||||
// with additional coverage files that will be combined when we finish.
|
||||
coverageFiles := make([]string, 0)
|
||||
for len(toRun) > 0 {
|
||||
var thisRun *nextRun
|
||||
thisRun, toRun = toRun[0], toRun[1:]
|
||||
@@ -245,13 +275,27 @@ func _main() {
|
||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\nflakytest failures JSON: %s\n\n", thisRun.attempt, j)
|
||||
}
|
||||
|
||||
goTestArgsWithCoverage := testArgs
|
||||
if runningWithCoverage {
|
||||
coverageFile := fmt.Sprintf("/tmp/coverage_%d.out", thisRun.attempt)
|
||||
coverageFiles = append(coverageFiles, coverageFile)
|
||||
goTestArgsWithCoverage = make([]string, len(goTestArgs), len(goTestArgs)+2)
|
||||
copy(goTestArgsWithCoverage, goTestArgs)
|
||||
goTestArgsWithCoverage = append(
|
||||
goTestArgsWithCoverage,
|
||||
fmt.Sprintf("-coverprofile=%v", coverageFile),
|
||||
"-covermode=set",
|
||||
"-coverpkg=./...",
|
||||
)
|
||||
}
|
||||
|
||||
toRetry := make(map[string][]*testAttempt) // pkg -> tests to retry
|
||||
for _, pt := range thisRun.tests {
|
||||
ch := make(chan *testAttempt)
|
||||
runErr := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(runErr)
|
||||
runErr <- runTests(ctx, thisRun.attempt, pt, goTestArgs, testArgs, ch)
|
||||
runErr <- runTests(ctx, thisRun.attempt, pt, goTestArgsWithCoverage, testArgs, ch)
|
||||
}()
|
||||
|
||||
var failed bool
|
||||
@@ -328,4 +372,107 @@ func _main() {
|
||||
}
|
||||
toRun = append(toRun, nextRun)
|
||||
}
|
||||
|
||||
if runningWithCoverage {
|
||||
intermediateCoverageFilename := "/tmp/coverage.out_intermediate"
|
||||
if err := combineCoverageFiles(intermediateCoverageFilename, coverageFiles); err != nil {
|
||||
fmt.Printf("error combining coverage files: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err := processCoverageWithCourtney(intermediateCoverageFilename, combinedCoverageFilename, testArgs); err != nil {
|
||||
fmt.Printf("error processing coverage with courtney: %v\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
fmt.Printf("Wrote combined coverage to %v\n", combinedCoverageFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func combineCoverageFiles(intermediateCoverageFilename string, coverageFiles []string) error {
|
||||
combinedCoverageFile, err := os.OpenFile(intermediateCoverageFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create /tmp/coverage.out: %w", err)
|
||||
}
|
||||
defer combinedCoverageFile.Close()
|
||||
w := bufio.NewWriter(combinedCoverageFile)
|
||||
defer w.Flush()
|
||||
|
||||
for fileNumber, coverageFile := range coverageFiles {
|
||||
f, err := os.Open(coverageFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %v: %w", coverageFile, err)
|
||||
}
|
||||
defer f.Close()
|
||||
in := bufio.NewReader(f)
|
||||
line := 0
|
||||
for {
|
||||
r, _, err := in.ReadRune()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return fmt.Errorf("read %v: %w", coverageFile, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// On all but the first coverage file, skip the coverage file header
|
||||
if fileNumber > 0 && line == 0 {
|
||||
continue
|
||||
}
|
||||
if r == '\n' {
|
||||
line++
|
||||
}
|
||||
|
||||
// filter for only printable characters because coverage file sometimes includes junk on 2nd line
|
||||
if unicode.IsPrint(r) || r == '\n' {
|
||||
if _, err := w.WriteRune(r); err != nil {
|
||||
return fmt.Errorf("write %v: %w", combinedCoverageFile.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processCoverageWithCourtney post-processes code coverage to exclude less
|
||||
// meaningful sections like 'if err != nil { return err}', as well as
|
||||
// anything marked with a '// notest' comment.
|
||||
//
|
||||
// instead of running the courtney as a separate program, this embeds
|
||||
// courtney for easier integration.
|
||||
func processCoverageWithCourtney(intermediateCoverageFilename, combinedCoverageFilename string, testArgs []string) error {
|
||||
env := vos.Os()
|
||||
|
||||
setup := &shared.Setup{
|
||||
Env: vos.Os(),
|
||||
Paths: patsy.NewCache(env),
|
||||
TestArgs: testArgs,
|
||||
Load: intermediateCoverageFilename,
|
||||
Output: combinedCoverageFilename,
|
||||
}
|
||||
if err := setup.Parse(testArgs); err != nil {
|
||||
return fmt.Errorf("parse args: %w", err)
|
||||
}
|
||||
|
||||
s := scanner.New(setup)
|
||||
if err := s.LoadProgram(); err != nil {
|
||||
return fmt.Errorf("load program: %w", err)
|
||||
}
|
||||
if err := s.ScanPackages(); err != nil {
|
||||
return fmt.Errorf("scan packages: %w", err)
|
||||
}
|
||||
|
||||
t := tester.New(setup)
|
||||
if err := t.Load(); err != nil {
|
||||
return fmt.Errorf("load: %w", err)
|
||||
}
|
||||
if err := t.ProcessExcludes(s.Excludes); err != nil {
|
||||
return fmt.Errorf("process excludes: %w", err)
|
||||
}
|
||||
if err := t.Save(); err != nil {
|
||||
return fmt.Errorf("save: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Lshortfile)
|
||||
log.SetPrefix("testwrapper: ")
|
||||
|
||||
// Build go args: test [-work] ...
|
||||
var workdir string
|
||||
var args = []string{"test"}
|
||||
if !slices.Contains(args, "-work") && !slices.Contains(args, "--work") {
|
||||
args = append(args, "-work")
|
||||
defer func() {
|
||||
if workdir != "" {
|
||||
// Clean up the WORK directory as the user didn't want it.
|
||||
if err := os.RemoveAll(workdir); err != nil {
|
||||
log.Printf("error removing workdir: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
args = append(args, os.Args[1:]...)
|
||||
|
||||
// Run go test.
|
||||
attempt := 1
|
||||
r, xerr := run("go", args, []string{attemptenv(attempt)}, os.Stdout, os.Stderr)
|
||||
if nonexecerr(xerr) {
|
||||
log.Fatal("go test: ", xerr)
|
||||
}
|
||||
|
||||
// Check whether anything needs retried.
|
||||
log.Printf("failures: builds=%d tests=%d retryable=%d", r.buildFailures, r.testFailures, r.testFailuresRetryable)
|
||||
if r.buildFailures > 0 || r.testFailuresRetryable == 0 || r.testFailures > r.testFailuresRetryable {
|
||||
exit(xerr)
|
||||
}
|
||||
|
||||
// Retry tests we found.
|
||||
const maxAttempts = 3
|
||||
for cmd := range r.retryCmds {
|
||||
pkg := strings.TrimSuffix(cmdPkg(cmd), ".test")
|
||||
for {
|
||||
attempt++
|
||||
p := r.retryCmds[cmd]
|
||||
log.Printf("attempt %d: %s %s", attempt, pkg, strings.Join(p.tests, " "))
|
||||
|
||||
// Retry the test by invoking the built pkg.test binary directly.
|
||||
pr, xerr := run(
|
||||
cmd,
|
||||
append(p.args, "-test.run=^"+strings.Join(p.tests, "$|^")+"$"),
|
||||
[]string{attemptenv(attempt)},
|
||||
os.Stdout, os.Stdout, // go test copies all underlying pkg.test output to stdout
|
||||
)
|
||||
if nonexecerr(xerr) {
|
||||
log.Fatalf("%s: %s", cmd, xerr)
|
||||
}
|
||||
if code, _ := exitcode(xerr); code == 0 {
|
||||
break // all tests passed.
|
||||
}
|
||||
|
||||
if attempt == maxAttempts {
|
||||
log.Fatalf("failed %d times: %s %s", attempt, pkg, strings.Join(p.tests, " "))
|
||||
}
|
||||
|
||||
// Try again with the new failure instructions. Hopefully with fewer
|
||||
// failed tests...
|
||||
r.retryCmds[cmd] = pr.retryCmds[cmd]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attemptenv returns the environment variable value K=V used to signal
|
||||
// [flakytest] that it's in a test environment.
|
||||
func attemptenv(attempt int) string {
|
||||
return flakytest.FlakeAttemptEnv + "=" + strconv.Itoa(attempt)
|
||||
}
|
||||
|
||||
type testRun struct {
|
||||
workDir string
|
||||
|
||||
buildFailures int
|
||||
testFailures int
|
||||
testFailuresRetryable int
|
||||
|
||||
retryCmds map[string]pkgRetry // cmd path => retry instructions
|
||||
}
|
||||
|
||||
type pkgRetry struct {
|
||||
cmd string
|
||||
args []string
|
||||
tests []string
|
||||
}
|
||||
|
||||
// run executes prog with args and environ, writing output to stdout and stderr
|
||||
// and returns the error from [exec.Cmd.Wait], along with information parsed
|
||||
// from the output about how many builds or tests failed and how to retry them.
|
||||
func run(prog string, args []string, environ []string, stdout, stderr io.Writer) (r testRun, _ error) {
|
||||
cmd := exec.Command(prog, args...)
|
||||
cmd.Env = append(os.Environ(), environ...)
|
||||
cmdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("StdoutPipe: %s", err)
|
||||
}
|
||||
cmderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("StderrPipe: %s", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Start: %s", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Read WORK= from first line of stderr. We retain this so we can clean it
|
||||
// when testwrapper ends.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := readthrulines(cmderr, stderr, func(line string) {
|
||||
if r.workDir == "" {
|
||||
if w, ok := strings.CutPrefix(line, "WORK="); ok {
|
||||
r.workDir = w
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("reading stderr: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := readthrulines(cmdout, stdout, func(line string) {
|
||||
if strings.HasPrefix(line, "--- FAIL: Test") {
|
||||
r.testFailures++
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(line, "FAIL\t") && strings.HasSuffix(line, "[build failed]") {
|
||||
r.buildFailures++
|
||||
return
|
||||
}
|
||||
if _, args, ok := strings.Cut(line, "flakytest: retry:"); ok {
|
||||
wargs := strings.Split(strings.TrimSpace(args), " ")
|
||||
if len(wargs) < 2 {
|
||||
log.Printf("failed to retry log line %q", line)
|
||||
return
|
||||
}
|
||||
test, cmd, args := wargs[0], wargs[1], wargs[2:]
|
||||
|
||||
p := r.retryCmds[cmd]
|
||||
p.cmd = cmd
|
||||
p.args = args
|
||||
p.tests = append(p.tests, test)
|
||||
mak.Set(&r.retryCmds, cmd, p)
|
||||
r.testFailuresRetryable++
|
||||
return
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("reading stdout: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
xerr := cmd.Wait()
|
||||
return r, xerr
|
||||
}
|
||||
|
||||
// exit calls os.Exit with the exit code for err.
|
||||
func exit(err error) {
|
||||
code, _ := exitcode(err)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// nonexecerr reports whether err is an error which prevented a program executing.
|
||||
func nonexecerr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
xe := &exec.ExitError{}
|
||||
return !errors.As(err, &xe) || xe.ExitCode() < 0
|
||||
}
|
||||
|
||||
// exitcode returns a representative error code for err. If err has an
|
||||
// ExitCode() int method, its exit code is returned.
|
||||
func exitcode(err error) (code int, ok bool) {
|
||||
if xe := (interface{ ExitCode() int })(nil); errors.As(err, &xe) {
|
||||
return xe.ExitCode(), true
|
||||
}
|
||||
if err != nil {
|
||||
return 1, false
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// readthrulines copies r to w, calling f with each line of text.
|
||||
func readthrulines(r io.Reader, w io.Writer, f func(line string)) error {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
f(line)
|
||||
io.WriteString(w, line)
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
// cmdPkg will return the package of the binary that was built. From Go 1.24 on,
|
||||
// this will return the full package path followed by the ".test" from the
|
||||
// autogenerated main test pkg. For earlier Go versions return base(exe).
|
||||
func cmdPkg(exe string) string {
|
||||
v, _ := exec.Command("go", "version", "-m", exe).Output()
|
||||
_, vp, ok := bytes.Cut(v, []byte("\n\tpath\t"))
|
||||
if ok {
|
||||
p, _, _ := bytes.Cut(vp, []byte("\n"))
|
||||
p = bytes.TrimSpace(p)
|
||||
if len(p) > 0 {
|
||||
return string(p)
|
||||
}
|
||||
}
|
||||
return filepath.Base(exe)
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -37,7 +37,7 @@ var (
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
lc := local.Client{Socket: *flagSocket}
|
||||
lc := tailscale.LocalClient{Socket: *flagSocket}
|
||||
if lc.Socket != "" {
|
||||
lc.UseSocketOnly = true
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ import (
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/key"
|
||||
@@ -75,7 +75,7 @@ func main() {
|
||||
}
|
||||
|
||||
var (
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
st *ipnstate.Status
|
||||
err error
|
||||
watcherChan chan error
|
||||
@@ -84,7 +84,7 @@ func main() {
|
||||
lns []net.Listener
|
||||
)
|
||||
if *flagUseLocalTailscaled {
|
||||
lc = &local.Client{}
|
||||
lc = &tailscale.LocalClient{}
|
||||
st, err = lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("getting status: %v", err)
|
||||
@@ -212,7 +212,7 @@ func main() {
|
||||
// serveOnLocalTailscaled starts a serve session using an already-running
|
||||
// tailscaled instead of starting a fresh tsnet server, making something
|
||||
// listening on clientDNSName:dstPort accessible over serve/funnel.
|
||||
func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.Status, dstPort uint16, shouldFunnel bool) (cleanup func(), watcherChan chan error, err error) {
|
||||
func serveOnLocalTailscaled(ctx context.Context, lc *tailscale.LocalClient, st *ipnstate.Status, dstPort uint16, shouldFunnel bool) (cleanup func(), watcherChan chan error, err error) {
|
||||
// In order to support funneling out in local tailscaled mode, we need
|
||||
// to add a serve config to forward the listeners we bound above and
|
||||
// allow those forwarders to be funneled out.
|
||||
@@ -275,7 +275,7 @@ func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.
|
||||
}
|
||||
|
||||
type idpServer struct {
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
loopbackURL string
|
||||
serverURL string // "https://foo.bar.ts.net"
|
||||
funnel bool
|
||||
@@ -328,7 +328,7 @@ type authRequest struct {
|
||||
// allowRelyingParty validates that a relying party identified either by a
|
||||
// known remoteAddr or a valid client ID/secret pair is allowed to proceed
|
||||
// with the authorization flow associated with this authRequest.
|
||||
func (ar *authRequest) allowRelyingParty(r *http.Request, lc *local.Client) error {
|
||||
func (ar *authRequest) allowRelyingParty(r *http.Request, lc *tailscale.LocalClient) error {
|
||||
if ar.localRP {
|
||||
ra, err := netip.ParseAddrPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/set"
|
||||
@@ -64,7 +64,7 @@ func serveCmd(w http.ResponseWriter, cmd string, args ...string) {
|
||||
}
|
||||
|
||||
type localClientRoundTripper struct {
|
||||
lc local.Client
|
||||
lc tailscale.LocalClient
|
||||
}
|
||||
|
||||
func (rt *localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -1003,9 +1003,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
if persist == c.persist {
|
||||
newPersist := persist.AsStruct()
|
||||
newPersist.NodeID = nm.SelfNode.StableID()
|
||||
if up, ok := nm.UserProfiles[nm.User()]; ok {
|
||||
newPersist.UserProfile = *up.AsStruct()
|
||||
}
|
||||
newPersist.UserProfile = nm.UserProfiles[nm.User()]
|
||||
|
||||
c.persist = newPersist.View()
|
||||
persist = c.persist
|
||||
|
||||
@@ -77,7 +77,7 @@ type mapSession struct {
|
||||
peers map[tailcfg.NodeID]tailcfg.NodeView
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastPacketFilterRules views.Slice[tailcfg.FilterRule] // concatenation of all namedPacketFilters
|
||||
namedPacketFilters map[string]views.Slice[tailcfg.FilterRule]
|
||||
lastParsedPacketFilter []filter.Match
|
||||
@@ -89,6 +89,7 @@ type mapSession struct {
|
||||
lastPopBrowserURL string
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||
lastMaxExpiry time.Duration
|
||||
}
|
||||
|
||||
// newMapSession returns a mostly unconfigured new mapSession.
|
||||
@@ -103,7 +104,7 @@ func newMapSession(privateNodeKey key.NodePrivate, nu NetmapUpdater, controlKnob
|
||||
privateNodeKey: privateNodeKey,
|
||||
publicNodeKey: privateNodeKey.Public(),
|
||||
lastDNSConfig: new(tailcfg.DNSConfig),
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfileView{},
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
|
||||
|
||||
// Non-nil no-op defaults, to be optionally overridden by the caller.
|
||||
logf: logger.Discard,
|
||||
@@ -194,6 +195,10 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
|
||||
ms.updateStateFromResponse(resp)
|
||||
|
||||
// Occasionally clean up old userprofile if it grows too much
|
||||
// from e.g. ephemeral tagged nodes.
|
||||
ms.cleanLastUserProfile()
|
||||
|
||||
if ms.tryHandleIncrementally(resp) {
|
||||
ms.occasionallyPrintSummary(ms.lastNetmapSummary)
|
||||
return nil
|
||||
@@ -289,9 +294,8 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
}
|
||||
|
||||
for _, up := range resp.UserProfiles {
|
||||
ms.lastUserProfile[up.ID] = up.View()
|
||||
ms.lastUserProfile[up.ID] = up
|
||||
}
|
||||
// TODO(bradfitz): clean up old user profiles? maybe not worth it.
|
||||
|
||||
if dm := resp.DERPMap; dm != nil {
|
||||
ms.vlogf("netmap: new map contains DERP map")
|
||||
@@ -383,6 +387,9 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
if resp.MaxKeyDuration > 0 {
|
||||
ms.lastMaxExpiry = resp.MaxKeyDuration
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -537,6 +544,32 @@ func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserI
|
||||
}
|
||||
}
|
||||
|
||||
// cleanLastUserProfile deletes any entries from lastUserProfile
|
||||
// that are not referenced by any peer or the self node.
|
||||
//
|
||||
// This is expensive enough that we don't do this on every message
|
||||
// from the server, but only when it's grown enough to matter.
|
||||
func (ms *mapSession) cleanLastUserProfile() {
|
||||
if len(ms.lastUserProfile) < len(ms.peers)*2 {
|
||||
// Hasn't grown enough to be worth cleaning.
|
||||
return
|
||||
}
|
||||
|
||||
keep := set.Set[tailcfg.UserID]{}
|
||||
if node := ms.lastNode; node.Valid() {
|
||||
keep.Add(node.User())
|
||||
}
|
||||
for _, n := range ms.peers {
|
||||
keep.Add(n.User())
|
||||
keep.Add(n.Sharer())
|
||||
}
|
||||
for userID := range ms.lastUserProfile {
|
||||
if !keep.Contains(userID) {
|
||||
delete(ms.lastUserProfile, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var debugPatchifyPeer = envknob.RegisterBool("TS_DEBUG_PATCHIFY_PEER")
|
||||
|
||||
// patchifyPeersChanged mutates resp to promote PeersChanged entries to PeersChangedPatch
|
||||
@@ -804,7 +837,7 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: peerViews,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfileView),
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
@@ -815,6 +848,7 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
DERPMap: ms.lastDERPMap,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
MaxKeyDuration: ms.lastMaxExpiry,
|
||||
}
|
||||
|
||||
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
||||
|
||||
@@ -36,10 +36,9 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -1320,7 +1319,7 @@ func (c *sclient) requestMeshUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
var localClient local.Client
|
||||
var localClient tailscale.LocalClient
|
||||
|
||||
// isMeshPeer reports whether the client is a trusted mesh peer
|
||||
// node in the DERP region.
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
<string id="SINCE_V1_62">Tailscale version 1.62.0 and later</string>
|
||||
<string id="SINCE_V1_74">Tailscale version 1.74.0 and later</string>
|
||||
<string id="SINCE_V1_78">Tailscale version 1.78.0 and later</string>
|
||||
<string id="SINCE_V1_82">Tailscale version 1.82.0 and later</string>
|
||||
<string id="Tailscale_Category">Tailscale</string>
|
||||
<string id="UI_Category">UI customization</string>
|
||||
<string id="Settings_Category">Settings</string>
|
||||
<string id="AllowedWithAudit">Allowed (with audit)</string>
|
||||
<string id="NotAllowed">Not Allowed</string>
|
||||
<string id="LoginURL">Require using a specific Tailscale coordination server</string>
|
||||
<string id="LoginURL_Help"><![CDATA[This policy can be used to require the use of a particular Tailscale coordination server.
|
||||
|
||||
@@ -101,14 +98,6 @@ If you disable this policy, then Run Unattended is always disabled and the menu
|
||||
If you do not configure this policy, then Run Unattended depends on what is selected in the Preferences submenu.
|
||||
|
||||
See https://tailscale.com/kb/1315/mdm-keys#set-unattended-mode and https://tailscale.com/kb/1088/run-unattended for more details.]]></string>
|
||||
<string id="AlwaysOn">Restrict users from disconnecting Tailscale (always-on mode)</string>
|
||||
<string id="AlwaysOn_Help"><![CDATA[This policy setting controls whether a user can disconnect Tailscale.
|
||||
|
||||
If you enable this policy setting, users will not be allowed to disconnect Tailscale, and it will remain in a connected state as long as they are logged in, even if they close or terminate the GUI. Optionally, you can allow users to temporarily disconnect Tailscale by requiring them to provide a reason, which will be logged for auditing purposes.
|
||||
|
||||
If necessary, it can be used along with Unattended Mode to keep Tailscale connected regardless of whether a user is logged in. This can be used to facilitate remote access to a device or ensure connectivity to a Domain Controller before a user logs in.
|
||||
|
||||
If you disable or don't configure this policy setting, users will be allowed to disconnect Tailscale at their will.]]></string>
|
||||
<string id="ExitNodeAllowLANAccess">Allow Local Network Access when an Exit Node is in use</string>
|
||||
<string id="ExitNodeAllowLANAccess_Help"><![CDATA[This policy can be used to require that the Allow Local Network Access setting is configured a certain way.
|
||||
|
||||
@@ -276,10 +265,6 @@ See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more d
|
||||
<label>Auth Key:</label>
|
||||
</textBox>
|
||||
</presentation>
|
||||
<presentation id="AlwaysOn">
|
||||
<text>The options below allow configuring exceptions where disconnecting Tailscale is permitted.</text>
|
||||
<dropdownList refId="AlwaysOn_OverrideWithReason" noSort="true" defaultItem="0">Disconnects with reason:</dropdownList>
|
||||
</presentation>
|
||||
<presentation id="ExitNodeID">
|
||||
<textBox refId="ExitNodeIDPrompt">
|
||||
<label>Exit Node:</label>
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
displayName="$(string.SINCE_V1_78)">
|
||||
<and><reference ref="TAILSCALE_PRODUCT"/></and>
|
||||
</definition>
|
||||
<definition name="SINCE_V1_82"
|
||||
displayName="$(string.SINCE_V1_82)">
|
||||
<and><reference ref="TAILSCALE_PRODUCT"/></and>
|
||||
</definition>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
<categories>
|
||||
@@ -102,7 +98,7 @@
|
||||
<parentCategory ref="Settings_Category" />
|
||||
<supportedOn ref="SINCE_V1_56" />
|
||||
<elements>
|
||||
<text id="ExitNodeIDPrompt" valueName="ExitNodeID" required="true" />
|
||||
<text id="ExitNodeIDPrompt" valueName="ExitNodeID" required="true" />>
|
||||
</elements>
|
||||
</policy>
|
||||
<policy name="AllowedSuggestedExitNodes" class="Machine" displayName="$(string.AllowedSuggestedExitNodes)" explainText="$(string.AllowedSuggestedExitNodes_Help)" presentation="$(presentation.AllowedSuggestedExitNodes)" key="Software\Policies\Tailscale\AllowedSuggestedExitNodes">
|
||||
@@ -132,30 +128,6 @@
|
||||
<string>never</string>
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="AlwaysOn" class="Machine" displayName="$(string.AlwaysOn)" explainText="$(string.AlwaysOn_Help)" presentation="$(presentation.AlwaysOn)" key="Software\Policies\Tailscale" valueName="AlwaysOn.Enabled">
|
||||
<parentCategory ref="Settings_Category" />
|
||||
<supportedOn ref="SINCE_V1_82" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
<elements>
|
||||
<enum id="AlwaysOn_OverrideWithReason" valueName="AlwaysOn.OverrideWithReason">
|
||||
<item displayName="$(string.NotAllowed)">
|
||||
<value>
|
||||
<decimal value="0" />
|
||||
</value>
|
||||
</item>
|
||||
<item displayName="$(string.AllowedWithAudit)">
|
||||
<value>
|
||||
<decimal value="1" />
|
||||
</value>
|
||||
</item>
|
||||
</enum>
|
||||
</elements>
|
||||
</policy>
|
||||
<policy name="ExitNodeAllowLANAccess" class="Machine" displayName="$(string.ExitNodeAllowLANAccess)" explainText="$(string.ExitNodeAllowLANAccess_Help)" key="Software\Policies\Tailscale" valueName="ExitNodeAllowLANAccess">
|
||||
<parentCategory ref="Settings_Category" />
|
||||
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
|
||||
|
||||
76
go.mod
76
go.mod
@@ -1,6 +1,6 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.23.6
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
@@ -10,10 +10,10 @@ require (
|
||||
github.com/andybalholm/brotli v1.1.0
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.5
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7
|
||||
github.com/bramvdbogaerde/go-scp v1.4.0
|
||||
github.com/cilium/ebpf v0.15.0
|
||||
@@ -21,6 +21,8 @@ require (
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/dave/courtney v0.4.0
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/distribution/reference v0.6.0
|
||||
@@ -31,7 +33,7 @@ require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fxamacker/cbor/v2 v2.7.0
|
||||
github.com/gaissmai/bart v0.18.0
|
||||
github.com/gaissmai/bart v0.11.1
|
||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
@@ -46,7 +48,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/goreleaser/nfpm/v2 v2.33.1
|
||||
github.com/hdevalence/ed25519consensus v0.2.0
|
||||
github.com/illarion/gonotify/v3 v3.0.2
|
||||
github.com/illarion/gonotify/v2 v2.0.3
|
||||
github.com/inetaf/tcpproxy v0.0.0-20250203165043-ded522cbd03f
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0
|
||||
@@ -80,7 +82,6 @@ require (
|
||||
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc
|
||||
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
|
||||
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19
|
||||
@@ -93,20 +94,20 @@ require (
|
||||
go.uber.org/zap v1.27.0
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/mod v0.23.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/term v0.29.0
|
||||
golang.org/x/time v0.10.0
|
||||
golang.org/x/tools v0.30.0
|
||||
golang.org/x/crypto v0.32.1-0.20250118192723-a8ea4be81f07
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
||||
golang.org/x/mod v0.22.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/time v0.9.0
|
||||
golang.org/x/tools v0.29.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
|
||||
honnef.co/go/tools v0.5.1
|
||||
k8s.io/api v0.32.0
|
||||
k8s.io/apimachinery v0.32.0
|
||||
@@ -127,12 +128,15 @@ require (
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.2.1 // indirect
|
||||
github.com/butuzov/mirror v1.1.0 // indirect
|
||||
github.com/catenacyber/perfsprint v0.7.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
|
||||
github.com/ckaznocha/intrange v0.1.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
|
||||
github.com/dave/brenda v1.1.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -184,21 +188,21 @@ require (
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.6.0 // indirect
|
||||
github.com/ashanbrown/makezero v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.1 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
@@ -380,8 +384,8 @@ require (
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
|
||||
165
go.sum
165
go.sum
@@ -123,50 +123,65 @@ github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5Fc
|
||||
github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
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/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
|
||||
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.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.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/feature/s3/manager v1.17.58 h1:/BsEGAyMai+KdXS+CMHlLhB5miAO19wOqE6tj8azWPM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58/go.mod h1:KHM3lfl/sAJBCoLI1Lsg5w4SD2VDYWwQi7vxbKhw7TI=
|
||||
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.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.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/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw=
|
||||
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/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw=
|
||||
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/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
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/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.22/go.mod h1:mN7Li1wxaPxSSy4Xkr6stFuinJGf3VZW3ZSNvO0q6sI=
|
||||
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/credentials v1.13.21/go.mod h1:90Dk1lJoMyspa/EDUrldTxsPns0wn6+KpRKpdAWc0uA=
|
||||
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/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
|
||||
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/s3/manager v1.11.64 h1:9QJQs36z61YB8nxGwRDfWXEDYbU6H7jdI6zFiAX1vag=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64/go.mod h1:4Q7R9MFpXRdjO3YnAfUTdnuENs32WzBkASt6VxSYDYQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
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/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
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/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
|
||||
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/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
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/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
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/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0 h1:L5h2fymEdVJYvn6hYO8Jx48YmC6xVmjmgHJV3oGKgmc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
|
||||
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/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.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.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.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.9/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
|
||||
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/ssooidc v1.14.9/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
|
||||
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/sts v1.18.10/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
|
||||
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/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY=
|
||||
github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
@@ -229,8 +244,6 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
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/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjOmrk888eg=
|
||||
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/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
@@ -240,6 +253,14 @@ github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18C
|
||||
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/daixiang0/gci v0.12.3 h1:yOZI7VAxAGPQmkb1eqt5g/11SUlwoat1fSblGLmdiQc=
|
||||
github.com/daixiang0/gci v0.12.3/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI=
|
||||
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4=
|
||||
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms=
|
||||
github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw=
|
||||
github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM=
|
||||
github.com/dave/courtney v0.4.0 h1:Vb8hi+k3O0h5++BR96FIcX0x3NovRbnhGd/dRr8inBk=
|
||||
github.com/dave/courtney v0.4.0/go.mod h1:3WSU3yaloZXYAxRuWt8oRyVb9SaRiMBt5Kz/2J227tM=
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs=
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -306,8 +327,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
|
||||
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
|
||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/ghostiam/protogetter v0.3.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbxOK4Ug=
|
||||
github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
@@ -544,8 +565,8 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI=
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
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/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
@@ -912,8 +933,6 @@ 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/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/setec v0.0.0-20250205144240-8898a29c3fbb h1:Rtklwm6HUlCtf/MR2MB9iY4FoA16acWWlC5pLrTVa90=
|
||||
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb/go.mod h1:R8iCVJnbOB05pGexHK/bKHneIRHpZ3jLl7wMQ0OM/jw=
|
||||
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/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
@@ -938,8 +957,6 @@ github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+n
|
||||
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ=
|
||||
github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4=
|
||||
github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg=
|
||||
github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok=
|
||||
github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||
@@ -1041,8 +1058,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.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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1053,16 +1070,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
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/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1090,8 +1107,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1131,16 +1148,16 @@ 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.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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
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.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1154,8 +1171,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.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.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1214,16 +1231,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1234,13 +1251,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.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=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1305,8 +1322,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1436,8 +1453,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
||||
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
|
||||
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=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -1 +1 @@
|
||||
65c3f5f3fc9d96f56a37a79cad4ebbd7ff985801
|
||||
64f7854906c3121fe3ada3d05f1936d3420d6ffa
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailscale provides a minimal control plane API client for internal
|
||||
// use. A full client for 3rd party use is available at
|
||||
// tailscale.com/client/tailscale/v2. The internal client is provided to avoid
|
||||
// having to import that whole package.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
tsclient "tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tsclient.I_Acknowledge_This_API_Is_Unstable = true
|
||||
}
|
||||
|
||||
// AuthMethod is an alias to tailscale.com/client/tailscale.
|
||||
type AuthMethod = tsclient.AuthMethod
|
||||
|
||||
// Device is an alias to tailscale.com/client/tailscale.
|
||||
type Device = tsclient.Device
|
||||
|
||||
// DeviceFieldsOpts is an alias to tailscale.com/client/tailscale.
|
||||
type DeviceFieldsOpts = tsclient.DeviceFieldsOpts
|
||||
|
||||
// Key is an alias to tailscale.com/client/tailscale.
|
||||
type Key = tsclient.Key
|
||||
|
||||
// KeyCapabilities is an alias to tailscale.com/client/tailscale.
|
||||
type KeyCapabilities = tsclient.KeyCapabilities
|
||||
|
||||
// KeyDeviceCapabilities is an alias to tailscale.com/client/tailscale.
|
||||
type KeyDeviceCapabilities = tsclient.KeyDeviceCapabilities
|
||||
|
||||
// KeyDeviceCreateCapabilities is an alias to tailscale.com/client/tailscale.
|
||||
type KeyDeviceCreateCapabilities = tsclient.KeyDeviceCreateCapabilities
|
||||
|
||||
// ErrResponse is an alias to tailscale.com/client/tailscale.
|
||||
type ErrResponse = tsclient.ErrResponse
|
||||
|
||||
// NewClient is an alias to tailscale.com/client/tailscale.
|
||||
func NewClient(tailnet string, auth AuthMethod) *Client {
|
||||
return &Client{
|
||||
Client: tsclient.NewClient(tailnet, auth),
|
||||
}
|
||||
}
|
||||
|
||||
// Client is a wrapper of tailscale.com/client/tailscale.
|
||||
type Client struct {
|
||||
*tsclient.Client
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package desktop facilitates interaction with the desktop environment
|
||||
// and user sessions. As of 2025-02-06, it is only implemented for Windows.
|
||||
package desktop
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package desktop
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
|
||||
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
|
||||
|
||||
//sys setLastError(dwErrorCode uint32) = kernel32.SetLastError
|
||||
|
||||
//sys registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) [atom==0] = user32.RegisterClassExW
|
||||
//sys createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) [hWnd==0] = user32.CreateWindowExW
|
||||
//sys defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.DefWindowProcW
|
||||
//sys setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) [res==0 && e1!=0] = user32.SetWindowLongPtrW
|
||||
//sys getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) [res==0 && e1!=0] = user32.GetWindowLongPtrW
|
||||
//sys sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.SendMessageW
|
||||
//sys getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) = user32.GetMessageW
|
||||
//sys translateMessage(lpMsg *_MSG) (res bool) = user32.TranslateMessage
|
||||
//sys dispatchMessage(lpMsg *_MSG) (res uintptr) = user32.DispatchMessageW
|
||||
//sys destroyWindow(hwnd windows.HWND) (err error) [int32(failretval)==0] = user32.DestroyWindow
|
||||
//sys postQuitMessage(exitCode int32) = user32.PostQuitMessage
|
||||
|
||||
//sys registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) [int32(failretval)==0] = wtsapi32.WTSRegisterSessionNotificationEx
|
||||
//sys unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) [int32(failretval)==0] = wtsapi32.WTSUnRegisterSessionNotificationEx
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
)
|
||||
|
||||
// SessionID is a unique identifier of a desktop session.
|
||||
type SessionID uint
|
||||
|
||||
// SessionStatus is the status of a desktop session.
|
||||
type SessionStatus int
|
||||
|
||||
const (
|
||||
// ClosedSession is a session that does not exist, is not yet initialized by the OS,
|
||||
// or has been terminated.
|
||||
ClosedSession SessionStatus = iota
|
||||
// ForegroundSession is a session that a user can interact with,
|
||||
// such as when attached to a physical console or an active,
|
||||
// unlocked RDP connection.
|
||||
ForegroundSession
|
||||
// BackgroundSession indicates that the session is locked, disconnected,
|
||||
// or otherwise running without user presence or interaction.
|
||||
BackgroundSession
|
||||
)
|
||||
|
||||
// String implements [fmt.Stringer].
|
||||
func (s SessionStatus) String() string {
|
||||
switch s {
|
||||
case ClosedSession:
|
||||
return "Closed"
|
||||
case ForegroundSession:
|
||||
return "Foreground"
|
||||
case BackgroundSession:
|
||||
return "Background"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// Session is a state of a desktop session at a given point in time.
|
||||
type Session struct {
|
||||
ID SessionID // Identifier of the session; can be reused after the session is closed.
|
||||
Status SessionStatus // The status of the session, such as foreground or background.
|
||||
User ipnauth.Actor // User logged into the session.
|
||||
}
|
||||
|
||||
// Description returns a human-readable description of the session.
|
||||
func (s *Session) Description() string {
|
||||
if maybeUsername, _ := s.User.Username(); maybeUsername != "" { // best effort
|
||||
return fmt.Sprintf("Session %d - %q (%s)", s.ID, maybeUsername, s.Status)
|
||||
}
|
||||
return fmt.Sprintf("Session %d (%s)", s.ID, s.Status)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// ErrNotImplemented is returned by [NewSessionManager] when it is not
|
||||
// implemented for the current GOOS.
|
||||
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
// SessionInitCallback is a function that is called once per [Session].
|
||||
// It returns an optional cleanup function that is called when the session
|
||||
// is about to be destroyed, or nil if no cleanup is needed.
|
||||
// It is not safe to call SessionManager methods from within the callback.
|
||||
type SessionInitCallback func(session *Session) (cleanup func())
|
||||
|
||||
// SessionStateCallback is a function that reports the initial or updated
|
||||
// state of a [Session], such as when it transitions between foreground and background.
|
||||
// It is guaranteed to be called after all registered [SessionInitCallback] functions
|
||||
// have completed, and before any cleanup functions are called for the same session.
|
||||
// It is not safe to call SessionManager methods from within the callback.
|
||||
type SessionStateCallback func(session *Session)
|
||||
|
||||
// SessionManager is an interface that provides access to desktop sessions on the current platform.
|
||||
// It is safe for concurrent use.
|
||||
type SessionManager interface {
|
||||
// Init explicitly initializes the receiver.
|
||||
// Unless the receiver is explicitly initialized, it will be lazily initialized
|
||||
// on the first call to any other method.
|
||||
// It is safe to call Init multiple times.
|
||||
Init() error
|
||||
|
||||
// Sessions returns a session snapshot taken at the time of the call.
|
||||
// Since sessions can be created or destroyed at any time, it may become
|
||||
// outdated as soon as it is returned.
|
||||
//
|
||||
// It is primarily intended for logging and debugging.
|
||||
// Prefer registering a [SessionInitCallback] or [SessionStateCallback]
|
||||
// in contexts requiring stronger guarantees.
|
||||
Sessions() (map[SessionID]*Session, error)
|
||||
|
||||
// RegisterInitCallback registers a [SessionInitCallback] that is called for each existing session
|
||||
// and for each new session that is created, until the returned unregister function is called.
|
||||
// If the specified [SessionInitCallback] returns a cleanup function, it is called when the session
|
||||
// is about to be destroyed. The callback function is guaranteed to be called once and only once
|
||||
// for each existing and new session.
|
||||
RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error)
|
||||
|
||||
// RegisterStateCallback registers a [SessionStateCallback] that is called for each existing session
|
||||
// and every time the state of a session changes, until the returned unregister function is called.
|
||||
RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error)
|
||||
|
||||
// Close waits for all registered callbacks to complete
|
||||
// and releases resources associated with the receiver.
|
||||
Close() error
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package desktop
|
||||
|
||||
import "tailscale.com/types/logger"
|
||||
|
||||
// NewSessionManager returns a new [SessionManager] for the current platform,
|
||||
// [ErrNotImplemented] if the platform is not supported, or an error if the
|
||||
// session manager could not be created.
|
||||
func NewSessionManager(logger.Logf) (SessionManager, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
@@ -1,672 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// wtsManager is a [SessionManager] implementation for Windows.
|
||||
type wtsManager struct {
|
||||
logf logger.Logf
|
||||
ctx context.Context // cancelled when the manager is closed
|
||||
ctxCancel context.CancelFunc
|
||||
|
||||
initOnce func() error
|
||||
watcher *sessionWatcher
|
||||
|
||||
mu sync.Mutex
|
||||
sessions map[SessionID]*wtsSession
|
||||
initCbs set.HandleSet[SessionInitCallback]
|
||||
stateCbs set.HandleSet[SessionStateCallback]
|
||||
}
|
||||
|
||||
// NewSessionManager returns a new [SessionManager] for the current platform,
|
||||
func NewSessionManager(logf logger.Logf) (SessionManager, error) {
|
||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
m := &wtsManager{
|
||||
logf: logf,
|
||||
ctx: ctx,
|
||||
ctxCancel: ctxCancel,
|
||||
sessions: make(map[SessionID]*wtsSession),
|
||||
}
|
||||
m.watcher = newSessionWatcher(m.ctx, m.logf, m.sessionEventHandler)
|
||||
|
||||
m.initOnce = sync.OnceValue(func() error {
|
||||
if err := waitUntilWTSReady(m.ctx); err != nil {
|
||||
return fmt.Errorf("WTS is not ready: %w", err)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if err := m.watcher.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start session watcher: %w", err)
|
||||
}
|
||||
|
||||
var err error
|
||||
m.sessions, err = enumerateSessions()
|
||||
return err // may be nil or non-nil
|
||||
})
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Init implements [SessionManager].
|
||||
func (m *wtsManager) Init() error {
|
||||
return m.initOnce()
|
||||
}
|
||||
|
||||
// Sessions implements [SessionManager].
|
||||
func (m *wtsManager) Sessions() (map[SessionID]*Session, error) {
|
||||
if err := m.initOnce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
sessions := make(map[SessionID]*Session, len(m.sessions))
|
||||
for _, s := range m.sessions {
|
||||
sessions[s.id] = s.AsSession()
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// RegisterInitCallback implements [SessionManager].
|
||||
func (m *wtsManager) RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error) {
|
||||
if err := m.initOnce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cb == nil {
|
||||
return nil, errors.New("nil callback")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
handle := m.initCbs.Add(cb)
|
||||
|
||||
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
||||
for _, s := range m.sessions {
|
||||
if cleanup := cb(s.AsSession()); cleanup != nil {
|
||||
s.cleanup = append(s.cleanup, cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
return func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.initCbs, handle)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RegisterStateCallback implements [SessionManager].
|
||||
func (m *wtsManager) RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error) {
|
||||
if err := m.initOnce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cb == nil {
|
||||
return nil, errors.New("nil callback")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
handle := m.stateCbs.Add(cb)
|
||||
|
||||
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
||||
for _, s := range m.sessions {
|
||||
cb(s.AsSession())
|
||||
}
|
||||
|
||||
return func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.stateCbs, handle)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *wtsManager) sessionEventHandler(id SessionID, event uint32) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
switch event {
|
||||
case windows.WTS_SESSION_LOGON:
|
||||
// The session may have been created after we started watching,
|
||||
// but before the initial enumeration was performed.
|
||||
// Do not create a new session if it already exists.
|
||||
if _, _, err := m.getOrCreateSessionLocked(id); err != nil {
|
||||
m.logf("[unexpected] getOrCreateSessionLocked(%d): %v", id, err)
|
||||
}
|
||||
case windows.WTS_SESSION_LOCK:
|
||||
if err := m.setSessionStatusLocked(id, BackgroundSession); err != nil {
|
||||
m.logf("[unexpected] setSessionStatusLocked(%d, BackgroundSession): %v", id, err)
|
||||
}
|
||||
case windows.WTS_SESSION_UNLOCK:
|
||||
if err := m.setSessionStatusLocked(id, ForegroundSession); err != nil {
|
||||
m.logf("[unexpected] setSessionStatusLocked(%d, ForegroundSession): %v", id, err)
|
||||
}
|
||||
case windows.WTS_SESSION_LOGOFF:
|
||||
if err := m.deleteSessionLocked(id); err != nil {
|
||||
m.logf("[unexpected] deleteSessionLocked(%d): %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *wtsManager) getOrCreateSessionLocked(id SessionID) (_ *wtsSession, created bool, err error) {
|
||||
if s, ok := m.sessions[id]; ok {
|
||||
return s, false, nil
|
||||
}
|
||||
|
||||
s, err := newWTSSession(id, ForegroundSession)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
m.sessions[id] = s
|
||||
|
||||
session := s.AsSession()
|
||||
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
||||
for _, cb := range m.initCbs {
|
||||
if cleanup := cb(session); cleanup != nil {
|
||||
s.cleanup = append(s.cleanup, cleanup)
|
||||
}
|
||||
}
|
||||
for _, cb := range m.stateCbs {
|
||||
cb(session)
|
||||
}
|
||||
|
||||
return s, true, err
|
||||
}
|
||||
|
||||
func (m *wtsManager) setSessionStatusLocked(id SessionID, status SessionStatus) error {
|
||||
s, _, err := m.getOrCreateSessionLocked(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.status == status {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.status = status
|
||||
session := s.AsSession()
|
||||
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
||||
for _, cb := range m.stateCbs {
|
||||
cb(session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *wtsManager) deleteSessionLocked(id SessionID) error {
|
||||
s, ok := m.sessions[id]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.status = ClosedSession
|
||||
session := s.AsSession()
|
||||
// TODO(nickkhyl): enqueue callbacks (and [wtsSession.close]!) in a separate goroutine?
|
||||
for _, cb := range m.stateCbs {
|
||||
cb(session)
|
||||
}
|
||||
|
||||
delete(m.sessions, id)
|
||||
return s.close()
|
||||
}
|
||||
|
||||
func (m *wtsManager) Close() error {
|
||||
m.ctxCancel()
|
||||
|
||||
if m.watcher != nil {
|
||||
err := m.watcher.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.watcher = nil
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.initCbs = nil
|
||||
m.stateCbs = nil
|
||||
errs := make([]error, 0, len(m.sessions))
|
||||
for _, s := range m.sessions {
|
||||
errs = append(errs, s.close())
|
||||
}
|
||||
m.sessions = nil
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
type wtsSession struct {
|
||||
id SessionID
|
||||
user *ipnauth.WindowsActor
|
||||
|
||||
status SessionStatus
|
||||
|
||||
cleanup []func()
|
||||
}
|
||||
|
||||
func newWTSSession(id SessionID, status SessionStatus) (*wtsSession, error) {
|
||||
var token windows.Token
|
||||
if err := windows.WTSQueryUserToken(uint32(id), &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := ipnauth.NewWindowsActorWithToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &wtsSession{id, user, status, nil}, nil
|
||||
}
|
||||
|
||||
// enumerateSessions returns a map of all active WTS sessions.
|
||||
func enumerateSessions() (map[SessionID]*wtsSession, error) {
|
||||
const reserved, version uint32 = 0, 1
|
||||
var numSessions uint32
|
||||
var sessionInfos *windows.WTS_SESSION_INFO
|
||||
if err := windows.WTSEnumerateSessions(_WTS_CURRENT_SERVER_HANDLE, reserved, version, &sessionInfos, &numSessions); err != nil {
|
||||
return nil, fmt.Errorf("WTSEnumerateSessions failed: %w", err)
|
||||
}
|
||||
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessionInfos)))
|
||||
|
||||
sessions := make(map[SessionID]*wtsSession, numSessions)
|
||||
for _, si := range unsafe.Slice(sessionInfos, numSessions) {
|
||||
status := _WTS_CONNECTSTATE_CLASS(si.State).ToSessionStatus()
|
||||
if status == ClosedSession {
|
||||
// The session does not exist as far as we're concerned.
|
||||
// It may be in the process of being created or destroyed,
|
||||
// or be a special "listener" session, etc.
|
||||
continue
|
||||
}
|
||||
id := SessionID(si.SessionID)
|
||||
session, err := newWTSSession(id, status)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sessions[id] = session
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (s *wtsSession) AsSession() *Session {
|
||||
return &Session{
|
||||
ID: s.id,
|
||||
Status: s.status,
|
||||
// wtsSession owns the user; don't let the caller close it
|
||||
User: ipnauth.WithoutClose(s.user),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *wtsSession) close() error {
|
||||
for _, cleanup := range m.cleanup {
|
||||
cleanup()
|
||||
}
|
||||
m.cleanup = nil
|
||||
|
||||
if m.user != nil {
|
||||
if err := m.user.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.user = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sessionEventHandler func(id SessionID, event uint32)
|
||||
|
||||
// TODO(nickkhyl): implement a sessionWatcher that does not use the message queue.
|
||||
// One possible approach is to have the tailscaled service register a HandlerEx function
|
||||
// and stream SERVICE_CONTROL_SESSIONCHANGE events to the tailscaled subprocess
|
||||
// (the actual tailscaled backend), exposing these events via [sessionWatcher]/[wtsManager].
|
||||
//
|
||||
// See tailscale/corp#26477 for details and tracking.
|
||||
type sessionWatcher struct {
|
||||
logf logger.Logf
|
||||
ctx context.Context // canceled to stop the watcher
|
||||
ctxCancel context.CancelFunc // cancels the watcher
|
||||
hWnd windows.HWND // window handle for receiving session change notifications
|
||||
handler sessionEventHandler // called on session events
|
||||
|
||||
mu sync.Mutex
|
||||
doneCh chan error // written to when the watcher exits; nil if not started
|
||||
}
|
||||
|
||||
func newSessionWatcher(ctx context.Context, logf logger.Logf, handler sessionEventHandler) *sessionWatcher {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &sessionWatcher{logf: logf, ctx: ctx, ctxCancel: cancel, handler: handler}
|
||||
}
|
||||
|
||||
func (sw *sessionWatcher) Start() error {
|
||||
sw.mu.Lock()
|
||||
defer sw.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-sw.ctx.Done():
|
||||
return fmt.Errorf("sessionWatcher already stopped: %w", sw.ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
if sw.doneCh != nil {
|
||||
// Already started.
|
||||
return nil
|
||||
}
|
||||
sw.doneCh = make(chan error, 1)
|
||||
|
||||
startedCh := make(chan error, 1)
|
||||
go sw.run(startedCh, sw.doneCh)
|
||||
if err := <-startedCh; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Signal the window to unsubscribe from session notifications
|
||||
// and shut down gracefully when the sessionWatcher is stopped.
|
||||
context.AfterFunc(sw.ctx, func() {
|
||||
sendMessage(sw.hWnd, _WM_CLOSE, 0, 0)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sw *sessionWatcher) run(started, done chan<- error) {
|
||||
runtime.LockOSThread()
|
||||
defer func() {
|
||||
runtime.UnlockOSThread()
|
||||
close(done)
|
||||
}()
|
||||
err := sw.createMessageWindow()
|
||||
started <- err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pumpThreadMessages()
|
||||
}
|
||||
|
||||
// Stop stops the session watcher and waits for it to exit.
|
||||
func (sw *sessionWatcher) Stop() error {
|
||||
sw.ctxCancel()
|
||||
|
||||
sw.mu.Lock()
|
||||
doneCh := sw.doneCh
|
||||
sw.doneCh = nil
|
||||
sw.mu.Unlock()
|
||||
|
||||
if doneCh != nil {
|
||||
return <-doneCh
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const watcherWindowClassName = "Tailscale-SessionManager"
|
||||
|
||||
var watcherWindowClassName16 = sync.OnceValue(func() *uint16 {
|
||||
return must.Get(syscall.UTF16PtrFromString(watcherWindowClassName))
|
||||
})
|
||||
|
||||
var registerSessionManagerWindowClass = sync.OnceValue(func() error {
|
||||
var hInst windows.Handle
|
||||
if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
|
||||
return fmt.Errorf("GetModuleHandle: %w", err)
|
||||
}
|
||||
wc := _WNDCLASSEX{
|
||||
CbSize: uint32(unsafe.Sizeof(_WNDCLASSEX{})),
|
||||
HInstance: hInst,
|
||||
LpfnWndProc: syscall.NewCallback(sessionWatcherWndProc),
|
||||
LpszClassName: watcherWindowClassName16(),
|
||||
}
|
||||
if _, err := registerClassEx(&wc); err != nil {
|
||||
return fmt.Errorf("RegisterClassEx(%q): %w", watcherWindowClassName, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
func (sw *sessionWatcher) createMessageWindow() error {
|
||||
if err := registerSessionManagerWindowClass(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := createWindowEx(
|
||||
0, // dwExStyle
|
||||
watcherWindowClassName16(), // lpClassName
|
||||
nil, // lpWindowName
|
||||
0, // dwStyle
|
||||
0, // x
|
||||
0, // y
|
||||
0, // nWidth
|
||||
0, // nHeight
|
||||
_HWND_MESSAGE, // hWndParent; message-only window
|
||||
0, // hMenu
|
||||
0, // hInstance
|
||||
unsafe.Pointer(sw), // lpParam
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateWindowEx: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sw *sessionWatcher) wndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
|
||||
switch msg {
|
||||
case _WM_CREATE:
|
||||
err := registerSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd, _NOTIFY_FOR_ALL_SESSIONS)
|
||||
if err != nil {
|
||||
sw.logf("[unexpected] failed to register for session notifications: %v", err)
|
||||
return ^uintptr(0)
|
||||
}
|
||||
sw.logf("registered for session notifications")
|
||||
case _WM_WTSSESSION_CHANGE:
|
||||
sw.handler(SessionID(lParam), uint32(wParam))
|
||||
return 0
|
||||
case _WM_CLOSE:
|
||||
if err := destroyWindow(hWnd); err != nil {
|
||||
sw.logf("[unexpected] failed to destroy window: %v", err)
|
||||
}
|
||||
return 0
|
||||
case _WM_DESTROY:
|
||||
err := unregisterSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd)
|
||||
if err != nil {
|
||||
sw.logf("[unexpected] failed to unregister session notifications callback: %v", err)
|
||||
}
|
||||
sw.logf("unregistered from session notifications")
|
||||
return 0
|
||||
case _WM_NCDESTROY:
|
||||
sw.hWnd = 0
|
||||
postQuitMessage(0) // quit the message loop for this thread
|
||||
}
|
||||
return defWindowProc(hWnd, msg, wParam, lParam)
|
||||
}
|
||||
|
||||
func (sw *sessionWatcher) setHandle(hwnd windows.HWND) error {
|
||||
sw.hWnd = hwnd
|
||||
setLastError(0)
|
||||
_, err := setWindowLongPtr(sw.hWnd, _GWLP_USERDATA, uintptr(unsafe.Pointer(sw)))
|
||||
return err // may be nil or non-nil
|
||||
}
|
||||
|
||||
func sessionWatcherByHandle(hwnd windows.HWND) *sessionWatcher {
|
||||
val, _ := getWindowLongPtr(hwnd, _GWLP_USERDATA)
|
||||
return (*sessionWatcher)(unsafe.Pointer(val))
|
||||
}
|
||||
|
||||
func sessionWatcherWndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
|
||||
if msg == _WM_NCCREATE {
|
||||
cs := (*_CREATESTRUCT)(unsafe.Pointer(lParam))
|
||||
sw := (*sessionWatcher)(unsafe.Pointer(cs.CreateParams))
|
||||
if sw == nil {
|
||||
return 0
|
||||
}
|
||||
if err := sw.setHandle(hWnd); err != nil {
|
||||
return 0
|
||||
}
|
||||
return defWindowProc(hWnd, msg, wParam, lParam)
|
||||
}
|
||||
if sw := sessionWatcherByHandle(hWnd); sw != nil {
|
||||
return sw.wndProc(hWnd, msg, wParam, lParam)
|
||||
}
|
||||
return defWindowProc(hWnd, msg, wParam, lParam)
|
||||
}
|
||||
|
||||
func pumpThreadMessages() {
|
||||
var msg _MSG
|
||||
for getMessage(&msg, 0, 0, 0) != 0 {
|
||||
translateMessage(&msg)
|
||||
dispatchMessage(&msg)
|
||||
}
|
||||
}
|
||||
|
||||
// waitUntilWTSReady waits until the Windows Terminal Services (WTS) is ready.
|
||||
// This is necessary because the WTS API functions may fail if called before
|
||||
// the WTS is ready.
|
||||
//
|
||||
// https://web.archive.org/web/20250207011738/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotificationex
|
||||
func waitUntilWTSReady(ctx context.Context) error {
|
||||
eventName16, err := windows.UTF16PtrFromString(`Global\TermSrvReadyEvent`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event, err := windows.OpenEvent(windows.SYNCHRONIZE, false, eventName16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.CloseHandle(event)
|
||||
return waitForContextOrHandle(ctx, event)
|
||||
}
|
||||
|
||||
// waitForContextOrHandle waits for either the context to be done or a handle to be signaled.
|
||||
func waitForContextOrHandle(ctx context.Context, handle windows.Handle) error {
|
||||
contextDoneEvent, cleanup, err := channelToEvent(ctx.Done())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
handles := []windows.Handle{contextDoneEvent, handle}
|
||||
waitCode, err := windows.WaitForMultipleObjects(handles, false, windows.INFINITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
waitCode -= windows.WAIT_OBJECT_0
|
||||
if waitCode == 0 { // contextDoneEvent
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// channelToEvent returns an auto-reset event that is set when the channel
|
||||
// becomes receivable, including when the channel is closed.
|
||||
func channelToEvent[T any](c <-chan T) (evt windows.Handle, cleanup func(), err error) {
|
||||
evt, err = windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
cancel := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-cancel:
|
||||
return
|
||||
case <-c:
|
||||
}
|
||||
windows.SetEvent(evt)
|
||||
}()
|
||||
|
||||
cleanup = func() {
|
||||
close(cancel)
|
||||
windows.CloseHandle(evt)
|
||||
}
|
||||
|
||||
return evt, cleanup, nil
|
||||
}
|
||||
|
||||
type _WNDCLASSEX struct {
|
||||
CbSize uint32
|
||||
Style uint32
|
||||
LpfnWndProc uintptr
|
||||
CbClsExtra int32
|
||||
CbWndExtra int32
|
||||
HInstance windows.Handle
|
||||
HIcon windows.Handle
|
||||
HCursor windows.Handle
|
||||
HbrBackground windows.Handle
|
||||
LpszMenuName *uint16
|
||||
LpszClassName *uint16
|
||||
HIconSm windows.Handle
|
||||
}
|
||||
|
||||
type _CREATESTRUCT struct {
|
||||
CreateParams uintptr
|
||||
Instance windows.Handle
|
||||
Menu windows.Handle
|
||||
Parent windows.HWND
|
||||
Cy int32
|
||||
Cx int32
|
||||
Y int32
|
||||
X int32
|
||||
Style int32
|
||||
Name *uint16
|
||||
ClassName *uint16
|
||||
ExStyle uint32
|
||||
}
|
||||
|
||||
type _POINT struct {
|
||||
X, Y int32
|
||||
}
|
||||
|
||||
type _MSG struct {
|
||||
HWnd windows.HWND
|
||||
Message uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt _POINT
|
||||
}
|
||||
|
||||
const (
|
||||
_WM_CREATE = 1
|
||||
_WM_DESTROY = 2
|
||||
_WM_CLOSE = 16
|
||||
_WM_NCCREATE = 129
|
||||
_WM_QUIT = 18
|
||||
_WM_NCDESTROY = 130
|
||||
|
||||
// _WM_WTSSESSION_CHANGE is a message sent to windows that have registered
|
||||
// for session change notifications, informing them of changes in session state.
|
||||
//
|
||||
// https://web.archive.org/web/20250207012421/https://learn.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
|
||||
_WM_WTSSESSION_CHANGE = 0x02B1
|
||||
)
|
||||
|
||||
const _GWLP_USERDATA = -21
|
||||
|
||||
const _HWND_MESSAGE = ^windows.HWND(2)
|
||||
|
||||
// _NOTIFY_FOR_ALL_SESSIONS indicates that the window should receive
|
||||
// session change notifications for all sessions on the specified server.
|
||||
const _NOTIFY_FOR_ALL_SESSIONS = 1
|
||||
|
||||
// _WTS_CURRENT_SERVER_HANDLE indicates that the window should receive
|
||||
// session change notifications for the host itself rather than a remote server.
|
||||
const _WTS_CURRENT_SERVER_HANDLE = windows.Handle(0)
|
||||
|
||||
// _WTS_CONNECTSTATE_CLASS represents the connection state of a session.
|
||||
//
|
||||
// https://web.archive.org/web/20250206082427/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ne-wtsapi32-wts_connectstate_class
|
||||
type _WTS_CONNECTSTATE_CLASS int32
|
||||
|
||||
// ToSessionStatus converts cs to a [SessionStatus].
|
||||
func (cs _WTS_CONNECTSTATE_CLASS) ToSessionStatus() SessionStatus {
|
||||
switch cs {
|
||||
case windows.WTSActive:
|
||||
return ForegroundSession
|
||||
case windows.WTSDisconnected:
|
||||
return BackgroundSession
|
||||
default:
|
||||
// The session does not exist as far as we're concerned.
|
||||
return ClosedSession
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
moduser32 = windows.NewLazySystemDLL("user32.dll")
|
||||
modwtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
|
||||
|
||||
procSetLastError = modkernel32.NewProc("SetLastError")
|
||||
procCreateWindowExW = moduser32.NewProc("CreateWindowExW")
|
||||
procDefWindowProcW = moduser32.NewProc("DefWindowProcW")
|
||||
procDestroyWindow = moduser32.NewProc("DestroyWindow")
|
||||
procDispatchMessageW = moduser32.NewProc("DispatchMessageW")
|
||||
procGetMessageW = moduser32.NewProc("GetMessageW")
|
||||
procGetWindowLongPtrW = moduser32.NewProc("GetWindowLongPtrW")
|
||||
procPostQuitMessage = moduser32.NewProc("PostQuitMessage")
|
||||
procRegisterClassExW = moduser32.NewProc("RegisterClassExW")
|
||||
procSendMessageW = moduser32.NewProc("SendMessageW")
|
||||
procSetWindowLongPtrW = moduser32.NewProc("SetWindowLongPtrW")
|
||||
procTranslateMessage = moduser32.NewProc("TranslateMessage")
|
||||
procWTSRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSRegisterSessionNotificationEx")
|
||||
procWTSUnRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSUnRegisterSessionNotificationEx")
|
||||
)
|
||||
|
||||
func setLastError(dwErrorCode uint32) {
|
||||
syscall.Syscall(procSetLastError.Addr(), 1, uintptr(dwErrorCode), 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) {
|
||||
r0, _, e1 := syscall.Syscall12(procCreateWindowExW.Addr(), 12, uintptr(dwExStyle), uintptr(unsafe.Pointer(lpClassName)), uintptr(unsafe.Pointer(lpWindowName)), uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(nWidth), uintptr(nHeight), uintptr(hWndParent), uintptr(hMenu), uintptr(hInstance), uintptr(lpParam))
|
||||
hWnd = windows.HWND(r0)
|
||||
if hWnd == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
|
||||
r0, _, _ := syscall.Syscall6(procDefWindowProcW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
|
||||
res = uintptr(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func destroyWindow(hwnd windows.HWND) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procDestroyWindow.Addr(), 1, uintptr(hwnd), 0, 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dispatchMessage(lpMsg *_MSG) (res uintptr) {
|
||||
r0, _, _ := syscall.Syscall(procDispatchMessageW.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
|
||||
res = uintptr(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) {
|
||||
r0, _, _ := syscall.Syscall6(procGetMessageW.Addr(), 4, uintptr(unsafe.Pointer(lpMsg)), uintptr(hwnd), uintptr(msgMin), uintptr(msgMax), 0, 0)
|
||||
ret = int32(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetWindowLongPtrW.Addr(), 2, uintptr(hwnd), uintptr(index), 0)
|
||||
res = uintptr(r0)
|
||||
if res == 0 && e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func postQuitMessage(exitCode int32) {
|
||||
syscall.Syscall(procPostQuitMessage.Addr(), 1, uintptr(exitCode), 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procRegisterClassExW.Addr(), 1, uintptr(unsafe.Pointer(windowClass)), 0, 0)
|
||||
atom = uint16(r0)
|
||||
if atom == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
|
||||
r0, _, _ := syscall.Syscall6(procSendMessageW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
|
||||
res = uintptr(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procSetWindowLongPtrW.Addr(), 3, uintptr(hwnd), uintptr(index), uintptr(newLong))
|
||||
res = uintptr(r0)
|
||||
if res == 0 && e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func translateMessage(lpMsg *_MSG) (res bool) {
|
||||
r0, _, _ := syscall.Syscall(procTranslateMessage.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
|
||||
res = r0 != 0
|
||||
return
|
||||
}
|
||||
|
||||
func registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWTSRegisterSessionNotificationEx.Addr(), 3, uintptr(hServer), uintptr(hwnd), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWTSUnRegisterSessionNotificationEx.Addr(), 2, uintptr(hServer), uintptr(hwnd), 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -4,11 +4,9 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
@@ -34,11 +32,6 @@ type Actor interface {
|
||||
// a connected LocalAPI client. Otherwise, it returns a zero value and false.
|
||||
ClientID() (_ ClientID, ok bool)
|
||||
|
||||
// Context returns the context associated with the actor.
|
||||
// It carries additional information about the actor
|
||||
// and is canceled when the actor is done.
|
||||
Context() context.Context
|
||||
|
||||
// CheckProfileAccess checks whether the actor has the necessary access rights
|
||||
// to perform a given action on the specified Tailscale profile.
|
||||
// It returns an error if access is denied.
|
||||
@@ -109,27 +102,3 @@ func (id ClientID) MarshalJSON() ([]byte, error) {
|
||||
func (id *ClientID) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, &id.v)
|
||||
}
|
||||
|
||||
type actorWithRequestReason struct {
|
||||
Actor
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// WithRequestReason returns an [Actor] that wraps the given actor and
|
||||
// carries the specified request reason in its context.
|
||||
func WithRequestReason(actor Actor, requestReason string) Actor {
|
||||
ctx := apitype.RequestReasonKey.WithValue(actor.Context(), requestReason)
|
||||
return &actorWithRequestReason{Actor: actor, ctx: ctx}
|
||||
}
|
||||
|
||||
// Context implements [Actor].
|
||||
func (a *actorWithRequestReason) Context() context.Context { return a.ctx }
|
||||
|
||||
type withoutCloseActor struct{ Actor }
|
||||
|
||||
// WithoutClose returns an [Actor] that does not expose the [ActorCloser] interface.
|
||||
// In other words, _, ok := WithoutClose(actor).(ActorCloser) will always be false,
|
||||
// even if the original actor implements [ActorCloser].
|
||||
func WithoutClose(actor Actor) Actor {
|
||||
return withoutCloseActor{actor}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/lazy"
|
||||
)
|
||||
|
||||
// WindowsActor implements [Actor].
|
||||
var _ Actor = (*WindowsActor)(nil)
|
||||
|
||||
// WindowsActor represents a logged in Windows user.
|
||||
type WindowsActor struct {
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
token WindowsToken
|
||||
uid ipn.WindowsUserID
|
||||
username lazy.SyncValue[string]
|
||||
}
|
||||
|
||||
// NewWindowsActorWithToken returns a new [WindowsActor] for the user
|
||||
// represented by the given [windows.Token].
|
||||
// It takes ownership of the token.
|
||||
func NewWindowsActorWithToken(t windows.Token) (_ *WindowsActor, err error) {
|
||||
tok := newToken(t)
|
||||
uid, err := tok.UID()
|
||||
if err != nil {
|
||||
t.Close()
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
return &WindowsActor{ctx: ctx, cancelCtx: cancelCtx, token: tok, uid: uid}, nil
|
||||
}
|
||||
|
||||
// UserID implements [Actor].
|
||||
func (a *WindowsActor) UserID() ipn.WindowsUserID {
|
||||
return a.uid
|
||||
}
|
||||
|
||||
// Username implements [Actor].
|
||||
func (a *WindowsActor) Username() (string, error) {
|
||||
return a.username.GetErr(a.token.Username)
|
||||
}
|
||||
|
||||
// ClientID implements [Actor].
|
||||
func (a *WindowsActor) ClientID() (_ ClientID, ok bool) {
|
||||
// TODO(nickkhyl): assign and return a client ID when the actor
|
||||
// represents a connected LocalAPI client.
|
||||
return NoClientID, false
|
||||
}
|
||||
|
||||
// Context implements [Actor].
|
||||
func (a *WindowsActor) Context() context.Context {
|
||||
return a.ctx
|
||||
}
|
||||
|
||||
// CheckProfileAccess implements [Actor].
|
||||
func (a *WindowsActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
|
||||
if profile.LocalUserID() != a.UserID() {
|
||||
// TODO(nickkhyl): return errors of more specific types and have them
|
||||
// translated to the appropriate HTTP status codes in the API handler.
|
||||
return errors.New("the target profile does not belong to the user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLocalSystem implements [Actor].
|
||||
//
|
||||
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
|
||||
// permission model and will be removed as we progress on tailscale/corp#18342.
|
||||
func (a *WindowsActor) IsLocalSystem() bool {
|
||||
// https://web.archive.org/web/2024/https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
|
||||
const systemUID = ipn.WindowsUserID("S-1-5-18")
|
||||
return a.uid == systemUID
|
||||
}
|
||||
|
||||
// IsLocalAdmin implements [Actor].
|
||||
//
|
||||
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
|
||||
// permission model and will be removed as we progress on tailscale/corp#18342.
|
||||
func (a *WindowsActor) IsLocalAdmin(operatorUID string) bool {
|
||||
return a.token.IsElevated()
|
||||
}
|
||||
|
||||
// Close releases resources associated with the actor
|
||||
// and cancels its context.
|
||||
func (a *WindowsActor) Close() error {
|
||||
if a.token != nil {
|
||||
if err := a.token.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
a.token = nil
|
||||
}
|
||||
a.cancelCtx()
|
||||
return nil
|
||||
}
|
||||
@@ -36,12 +36,6 @@ type token struct {
|
||||
t windows.Token
|
||||
}
|
||||
|
||||
func newToken(t windows.Token) *token {
|
||||
tok := &token{t: t}
|
||||
runtime.SetFinalizer(tok, func(t *token) { t.Close() })
|
||||
return tok
|
||||
}
|
||||
|
||||
func (t *token) UID() (ipn.WindowsUserID, error) {
|
||||
sid, err := t.uid()
|
||||
if err != nil {
|
||||
@@ -190,5 +184,7 @@ func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newToken(windows.Token(h)), nil
|
||||
result := &token{t: windows.Token(h)}
|
||||
runtime.SetFinalizer(result, func(t *token) { t.Close() })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -7,36 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
type actorWithPolicyChecks struct{ Actor }
|
||||
|
||||
// WithPolicyChecks returns an [Actor] that wraps the given actor and
|
||||
// performs additional policy checks on top of the access checks
|
||||
// implemented by the wrapped actor.
|
||||
func WithPolicyChecks(actor Actor) Actor {
|
||||
// TODO(nickkhyl): We should probably exclude the Windows Local System
|
||||
// account from policy checks as well.
|
||||
switch actor.(type) {
|
||||
case unrestricted:
|
||||
return actor
|
||||
default:
|
||||
return &actorWithPolicyChecks{Actor: actor}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckProfileAccess implements [Actor].
|
||||
func (a actorWithPolicyChecks) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogger AuditLogFunc) error {
|
||||
if err := a.Actor.CheckProfileAccess(profile, requestedAccess, auditLogger); err != nil {
|
||||
return err
|
||||
}
|
||||
requestReason := apitype.RequestReasonKey.Value(a.Context())
|
||||
return CheckDisconnectPolicy(a.Actor, profile, requestReason, auditLogger)
|
||||
}
|
||||
|
||||
// CheckDisconnectPolicy checks if the policy allows the specified actor to disconnect
|
||||
// Tailscale with the given optional reason. It returns nil if the operation is allowed,
|
||||
// or an error if it is not. If auditLogger is non-nil, it is called to log the action
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
@@ -19,21 +17,18 @@ var Self Actor = unrestricted{}
|
||||
type unrestricted struct{}
|
||||
|
||||
// UserID implements [Actor].
|
||||
func (unrestricted) UserID() ipn.WindowsUserID { return "" }
|
||||
func (u unrestricted) UserID() ipn.WindowsUserID { return "" }
|
||||
|
||||
// Username implements [Actor].
|
||||
func (unrestricted) Username() (string, error) { return "", nil }
|
||||
|
||||
// Context implements [Actor].
|
||||
func (unrestricted) Context() context.Context { return context.Background() }
|
||||
func (u unrestricted) Username() (string, error) { return "", nil }
|
||||
|
||||
// ClientID implements [Actor].
|
||||
// It always returns (NoClientID, false) because the tailscaled itself
|
||||
// is not a connected LocalAPI client.
|
||||
func (unrestricted) ClientID() (_ ClientID, ok bool) { return NoClientID, false }
|
||||
func (u unrestricted) ClientID() (_ ClientID, ok bool) { return NoClientID, false }
|
||||
|
||||
// CheckProfileAccess implements [Actor].
|
||||
func (unrestricted) CheckProfileAccess(_ ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
|
||||
func (u unrestricted) CheckProfileAccess(_ ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
|
||||
// Unrestricted access to all profiles.
|
||||
return nil
|
||||
}
|
||||
@@ -42,10 +37,10 @@ func (unrestricted) CheckProfileAccess(_ ipn.LoginProfileView, _ ProfileAccess,
|
||||
//
|
||||
// Deprecated: this method exists for compatibility with the current (as of 2025-01-28)
|
||||
// permission model and will be removed as we progress on tailscale/corp#18342.
|
||||
func (unrestricted) IsLocalSystem() bool { return false }
|
||||
func (u unrestricted) IsLocalSystem() bool { return false }
|
||||
|
||||
// IsLocalAdmin implements [Actor].
|
||||
//
|
||||
// Deprecated: this method exists for compatibility with the current (as of 2025-01-28)
|
||||
// permission model and will be removed as we progress on tailscale/corp#18342.
|
||||
func (unrestricted) IsLocalAdmin(operatorUID string) bool { return false }
|
||||
func (u unrestricted) IsLocalAdmin(operatorUID string) bool { return false }
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
package ipnauth
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
@@ -19,7 +17,6 @@ type TestActor struct {
|
||||
Name string // username associated with the actor, or ""
|
||||
NameErr error // error to be returned by [TestActor.Username]
|
||||
CID ClientID // non-zero if the actor represents a connected LocalAPI client
|
||||
Ctx context.Context // context associated with the actor
|
||||
LocalSystem bool // whether the actor represents the special Local System account on Windows
|
||||
LocalAdmin bool // whether the actor has local admin access
|
||||
}
|
||||
@@ -33,9 +30,6 @@ func (a *TestActor) Username() (string, error) { return a.Name, a.NameErr }
|
||||
// ClientID implements [Actor].
|
||||
func (a *TestActor) ClientID() (_ ClientID, ok bool) { return a.CID, a.CID != NoClientID }
|
||||
|
||||
// Context implements [Actor].
|
||||
func (a *TestActor) Context() context.Context { return cmp.Or(a.Ctx, context.Background()) }
|
||||
|
||||
// CheckProfileAccess implements [Actor].
|
||||
func (a *TestActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
|
||||
return errors.New("profile access denied")
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Both the desktop session manager and multi-user support
|
||||
// are currently available only on Windows.
|
||||
// This file does not need to be built for other platforms.
|
||||
|
||||
//go:build windows && !ts_omit_desktop_sessions
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/desktop"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.Register("desktop-sessions")
|
||||
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
|
||||
}
|
||||
|
||||
// desktopSessionsExt implements [localBackendExtension].
|
||||
var _ localBackendExtension = (*desktopSessionsExt)(nil)
|
||||
|
||||
// desktopSessionsExt extends [LocalBackend] with desktop session management.
|
||||
// It keeps Tailscale running in the background if Always-On mode is enabled,
|
||||
// and switches to an appropriate profile when a user signs in or out,
|
||||
// locks their screen, or disconnects a remote session.
|
||||
type desktopSessionsExt struct {
|
||||
logf logger.Logf
|
||||
sm desktop.SessionManager
|
||||
|
||||
*LocalBackend // or nil, until Init is called
|
||||
cleanup []func() // cleanup functions to call on shutdown
|
||||
|
||||
// mu protects all following fields.
|
||||
// When both mu and [LocalBackend.mu] need to be taken,
|
||||
// [LocalBackend.mu] must be taken before mu.
|
||||
mu sync.Mutex
|
||||
id2sess map[desktop.SessionID]*desktop.Session
|
||||
}
|
||||
|
||||
// newDesktopSessionsExt returns a new [desktopSessionsExt],
|
||||
// or an error if [desktop.SessionManager] is not available.
|
||||
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (localBackendExtension, error) {
|
||||
sm, ok := sys.SessionManager.GetOK()
|
||||
if !ok {
|
||||
return nil, errors.New("session manager is not available")
|
||||
}
|
||||
return &desktopSessionsExt{logf: logf, sm: sm, id2sess: make(map[desktop.SessionID]*desktop.Session)}, nil
|
||||
}
|
||||
|
||||
// Init implements [localBackendExtension].
|
||||
func (e *desktopSessionsExt) Init(lb *LocalBackend) (err error) {
|
||||
e.LocalBackend = lb
|
||||
unregisterResolver := lb.RegisterBackgroundProfileResolver(e.getBackgroundProfile)
|
||||
unregisterSessionCb, err := e.sm.RegisterStateCallback(e.updateDesktopSessionState)
|
||||
if err != nil {
|
||||
unregisterResolver()
|
||||
return fmt.Errorf("session callback registration failed: %w", err)
|
||||
}
|
||||
e.cleanup = []func(){unregisterResolver, unregisterSessionCb}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDesktopSessionState is a [desktop.SessionStateCallback]
|
||||
// invoked by [desktop.SessionManager] once for each existing session
|
||||
// and whenever the session state changes. It updates the session map
|
||||
// and switches to the best profile if necessary.
|
||||
func (e *desktopSessionsExt) updateDesktopSessionState(session *desktop.Session) {
|
||||
e.mu.Lock()
|
||||
if session.Status != desktop.ClosedSession {
|
||||
e.id2sess[session.ID] = session
|
||||
} else {
|
||||
delete(e.id2sess, session.ID)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
var action string
|
||||
switch session.Status {
|
||||
case desktop.ForegroundSession:
|
||||
// The user has either signed in or unlocked their session.
|
||||
// For remote sessions, this may also mean the user has connected.
|
||||
// The distinction isn't important for our purposes,
|
||||
// so let's always say "signed in".
|
||||
action = "signed in to"
|
||||
case desktop.BackgroundSession:
|
||||
action = "locked"
|
||||
case desktop.ClosedSession:
|
||||
action = "signed out from"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
maybeUsername, _ := session.User.Username()
|
||||
userIdentifier := cmp.Or(maybeUsername, string(session.User.UserID()), "user")
|
||||
reason := fmt.Sprintf("%s %s session %v", userIdentifier, action, session.ID)
|
||||
|
||||
e.SwitchToBestProfile(reason)
|
||||
}
|
||||
|
||||
// getBackgroundProfile is a [profileResolver] that works as follows:
|
||||
//
|
||||
// If Always-On mode is disabled, it returns no profile ("","",false).
|
||||
//
|
||||
// If AlwaysOn mode is enabled, it returns the current profile unless:
|
||||
// - The current user has signed out.
|
||||
// - Another user has a foreground (i.e. active/unlocked) session.
|
||||
//
|
||||
// If the current user's session runs in the background and no other user
|
||||
// has a foreground session, it returns the current profile. This applies
|
||||
// when a locally signed-in user locks their screen or when a remote user
|
||||
// disconnects without signing out.
|
||||
//
|
||||
// In all other cases, it returns no profile ("","",false).
|
||||
//
|
||||
// It is called with [LocalBackend.mu] locked.
|
||||
func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
isCurrentUserSingedIn := false
|
||||
var foregroundUIDs []ipn.WindowsUserID
|
||||
for _, s := range e.id2sess {
|
||||
switch uid := s.User.UserID(); uid {
|
||||
case e.pm.CurrentUserID():
|
||||
isCurrentUserSingedIn = true
|
||||
if s.Status == desktop.ForegroundSession {
|
||||
// Keep the current profile if the user has a foreground session.
|
||||
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
||||
}
|
||||
default:
|
||||
if s.Status == desktop.ForegroundSession {
|
||||
foregroundUIDs = append(foregroundUIDs, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no current user (e.g., tailscaled just started), or if the current
|
||||
// user has no foreground session, switch to the default profile of the first user
|
||||
// with a foreground session, if any.
|
||||
for _, uid := range foregroundUIDs {
|
||||
if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" {
|
||||
return uid, profileID, true
|
||||
}
|
||||
}
|
||||
|
||||
// If no user has a foreground session but the current user is still signed in,
|
||||
// keep the current profile even if the session is not in the foreground,
|
||||
// such as when the screen is locked or a remote session is disconnected.
|
||||
if len(foregroundUIDs) == 0 && isCurrentUserSingedIn {
|
||||
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Shutdown implements [localBackendExtension].
|
||||
func (e *desktopSessionsExt) Shutdown() error {
|
||||
for _, f := range e.cleanup {
|
||||
f()
|
||||
}
|
||||
e.cleanup = nil
|
||||
e.LocalBackend = nil
|
||||
return nil
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/control/controlknobs"
|
||||
@@ -61,6 +60,7 @@ import (
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/log/sockstatlog"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/captivedetection"
|
||||
@@ -168,49 +168,6 @@ type watchSession struct {
|
||||
cancel context.CancelFunc // to shut down the session
|
||||
}
|
||||
|
||||
// localBackendExtension extends [LocalBackend] with additional functionality.
|
||||
type localBackendExtension interface {
|
||||
// Init is called to initialize the extension when the [LocalBackend] is created
|
||||
// and before it starts running. If the extension cannot be initialized,
|
||||
// it must return an error, and the Shutdown method will not be called.
|
||||
// Any returned errors are not fatal; they are used for logging.
|
||||
// TODO(nickkhyl): should we allow returning a fatal error?
|
||||
Init(*LocalBackend) error
|
||||
|
||||
// Shutdown is called when the [LocalBackend] is shutting down,
|
||||
// if the extension was initialized. Any returned errors are not fatal;
|
||||
// they are used for logging.
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
// newLocalBackendExtension is a function that instantiates a [localBackendExtension].
|
||||
type newLocalBackendExtension func(logger.Logf, *tsd.System) (localBackendExtension, error)
|
||||
|
||||
// registeredExtensions is a map of registered local backend extensions,
|
||||
// where the key is the name of the extension and the value is the function
|
||||
// that instantiates the extension.
|
||||
var registeredExtensions map[string]newLocalBackendExtension
|
||||
|
||||
// RegisterExtension registers a function that creates a [localBackendExtension].
|
||||
// It panics if newExt is nil or if an extension with the same name has already been registered.
|
||||
func RegisterExtension(name string, newExt newLocalBackendExtension) {
|
||||
if newExt == nil {
|
||||
panic(fmt.Sprintf("lb: newExt is nil: %q", name))
|
||||
}
|
||||
if _, ok := registeredExtensions[name]; ok {
|
||||
panic(fmt.Sprintf("lb: duplicate extensions: %q", name))
|
||||
}
|
||||
mak.Set(®isteredExtensions, name, newExt)
|
||||
}
|
||||
|
||||
// profileResolver is any function that returns user and profile IDs
|
||||
// along with a flag indicating whether it succeeded. Since an empty
|
||||
// profile ID ("") represents an empty profile, the ok return parameter
|
||||
// distinguishes between an empty profile and no profile.
|
||||
//
|
||||
// It is called with [LocalBackend.mu] held.
|
||||
type profileResolver func() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool)
|
||||
|
||||
// LocalBackend is the glue between the major pieces of the Tailscale
|
||||
// network software: the cloud control plane (via controlclient), the
|
||||
// network data plane (via wgengine), and the user-facing UIs and CLIs
|
||||
@@ -345,12 +302,8 @@ type LocalBackend struct {
|
||||
directFileRoot string
|
||||
componentLogUntil map[string]componentLogState
|
||||
// c2nUpdateStatus is the status of c2n-triggered client update.
|
||||
c2nUpdateStatus updateStatus
|
||||
currentUser ipnauth.Actor
|
||||
|
||||
// backgroundProfileResolvers are optional background profile resolvers.
|
||||
backgroundProfileResolvers set.HandleSet[profileResolver]
|
||||
|
||||
c2nUpdateStatus updateStatus
|
||||
currentUser ipnauth.Actor
|
||||
selfUpdateProgress []ipnstate.UpdateProgress
|
||||
lastSelfUpdateState ipnstate.SelfUpdateStatus
|
||||
// capForcedNetfilter is the netfilter that control instructs Linux clients
|
||||
@@ -441,11 +394,6 @@ type LocalBackend struct {
|
||||
// and the user has disconnected with a reason.
|
||||
// See tailscale/corp#26146.
|
||||
overrideAlwaysOn bool
|
||||
|
||||
// shutdownCbs are the callbacks to be called when the backend is shutting down.
|
||||
// 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.
|
||||
shutdownCbs set.HandleSet[func() error]
|
||||
}
|
||||
|
||||
// HealthTracker returns the health tracker for the backend.
|
||||
@@ -627,19 +575,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
}
|
||||
|
||||
for name, newFn := range registeredExtensions {
|
||||
ext, err := newFn(logf, sys)
|
||||
if err != nil {
|
||||
b.logf("lb: failed to create %q extension: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := ext.Init(b); err != nil {
|
||||
b.logf("lb: failed to initialize %q extension: %v", name, err)
|
||||
continue
|
||||
}
|
||||
b.shutdownCbs.Add(ext.Shutdown)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -1098,17 +1033,9 @@ func (b *LocalBackend) Shutdown() {
|
||||
if b.notifyCancel != nil {
|
||||
b.notifyCancel()
|
||||
}
|
||||
shutdownCbs := slices.Collect(maps.Values(b.shutdownCbs))
|
||||
b.shutdownCbs = nil
|
||||
b.mu.Unlock()
|
||||
b.webClientShutdown()
|
||||
|
||||
for _, cb := range shutdownCbs {
|
||||
if err := cb(); err != nil {
|
||||
b.logf("shutdown callback failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if b.sockstatLogger != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -1329,7 +1256,6 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(),
|
||||
Location: p.Hostinfo().Location().AsStruct(),
|
||||
Capabilities: p.Capabilities().AsSlice(),
|
||||
TaildropTarget: b.taildropTargetStatus(p),
|
||||
}
|
||||
if cm := p.CapMap(); cm.Len() > 0 {
|
||||
ps.CapMap = make(tailcfg.NodeCapMap, cm.Len())
|
||||
@@ -1379,18 +1305,6 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) {
|
||||
}
|
||||
}
|
||||
|
||||
func profileFromView(v tailcfg.UserProfileView) tailcfg.UserProfile {
|
||||
if v.Valid() {
|
||||
return tailcfg.UserProfile{
|
||||
ID: v.ID(),
|
||||
LoginName: v.LoginName(),
|
||||
DisplayName: v.DisplayName(),
|
||||
ProfilePicURL: v.ProfilePicURL(),
|
||||
}
|
||||
}
|
||||
return tailcfg.UserProfile{}
|
||||
}
|
||||
|
||||
// WhoIsNodeKey returns the peer info of given public key, if it exists.
|
||||
func (b *LocalBackend) WhoIsNodeKey(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
b.mu.Lock()
|
||||
@@ -1400,12 +1314,11 @@ func (b *LocalBackend) WhoIsNodeKey(k key.NodePublic) (n tailcfg.NodeView, u tai
|
||||
return n, u, false
|
||||
}
|
||||
if self := b.netMap.SelfNode; self.Valid() && self.Key() == k {
|
||||
return self, profileFromView(b.netMap.UserProfiles[self.User()]), true
|
||||
return self, b.netMap.UserProfiles[self.User()], true
|
||||
}
|
||||
for _, n := range b.peers {
|
||||
if n.Key() == k {
|
||||
up, ok := b.netMap.UserProfiles[n.User()]
|
||||
u = profileFromView(up)
|
||||
u, ok = b.netMap.UserProfiles[n.User()]
|
||||
return n, u, ok
|
||||
}
|
||||
}
|
||||
@@ -1475,11 +1388,11 @@ func (b *LocalBackend) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeVi
|
||||
}
|
||||
n = b.netMap.SelfNode
|
||||
}
|
||||
up, ok := b.netMap.UserProfiles[n.User()]
|
||||
u, ok = b.netMap.UserProfiles[n.User()]
|
||||
if !ok {
|
||||
return failf("no userprofile for node %v", n.Key())
|
||||
}
|
||||
return n, profileFromView(up), true
|
||||
return n, u, true
|
||||
}
|
||||
|
||||
// PeerCaps returns the capabilities that remote src IP has to
|
||||
@@ -1512,10 +1425,6 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetFilterForTest() *filter.Filter {
|
||||
return b.filterAtomic.Load()
|
||||
}
|
||||
|
||||
// SetControlClientStatus is the callback invoked by the control client whenever it posts a new status.
|
||||
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
|
||||
func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st controlclient.Status) {
|
||||
@@ -3644,6 +3553,23 @@ func (b *LocalBackend) State() ipn.State {
|
||||
return b.state
|
||||
}
|
||||
|
||||
// InServerMode reports whether the Tailscale backend is explicitly running in
|
||||
// "server mode" where it continues to run despite whatever the platform's
|
||||
// default is. In practice, this is only used on Windows, where the default
|
||||
// tailscaled behavior is to shut down whenever the GUI disconnects.
|
||||
//
|
||||
// On non-Windows platforms, this usually returns false (because people don't
|
||||
// set unattended mode on other platforms) and also isn't checked on other
|
||||
// platforms.
|
||||
//
|
||||
// TODO(bradfitz): rename to InWindowsUnattendedMode or something? Or make this
|
||||
// return true on Linux etc and always be called? It's kinda messy now.
|
||||
func (b *LocalBackend) InServerMode() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.pm.CurrentPrefs().ForceDaemon()
|
||||
}
|
||||
|
||||
// CheckIPNConnectionAllowed returns an error if the specified actor should not
|
||||
// be allowed to connect or make requests to the LocalAPI currently.
|
||||
//
|
||||
@@ -3653,10 +3579,16 @@ func (b *LocalBackend) State() ipn.State {
|
||||
func (b *LocalBackend) CheckIPNConnectionAllowed(actor ipnauth.Actor) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.pm.CurrentUserID() == "" {
|
||||
// There's no "current user" yet; allow the connection.
|
||||
serverModeUid := b.pm.CurrentUserID()
|
||||
if serverModeUid == "" {
|
||||
// Either this platform isn't a "multi-user" platform or we're not yet
|
||||
// running as one.
|
||||
return nil
|
||||
}
|
||||
if !b.pm.CurrentPrefs().ForceDaemon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Always allow Windows SYSTEM user to connect,
|
||||
// even if Tailscale is currently being used by another user.
|
||||
if actor.IsLocalSystem() {
|
||||
@@ -3667,21 +3599,10 @@ func (b *LocalBackend) CheckIPNConnectionAllowed(actor ipnauth.Actor) error {
|
||||
if uid == "" {
|
||||
return errors.New("empty user uid in connection identity")
|
||||
}
|
||||
if uid == b.pm.CurrentUserID() {
|
||||
// The connection is from the current user; allow it.
|
||||
return nil
|
||||
if uid != serverModeUid {
|
||||
return fmt.Errorf("Tailscale running in server mode (%q); connection from %q not allowed", b.tryLookupUserName(string(serverModeUid)), b.tryLookupUserName(string(uid)))
|
||||
}
|
||||
|
||||
// The connection is from a different user; block it.
|
||||
var reason string
|
||||
if b.pm.CurrentPrefs().ForceDaemon() {
|
||||
reason = "running in server mode"
|
||||
} else {
|
||||
reason = "already in use"
|
||||
}
|
||||
return fmt.Errorf("Tailscale %s (%q); connection from %q not allowed",
|
||||
reason, b.tryLookupUserName(string(b.pm.CurrentUserID())),
|
||||
b.tryLookupUserName(string(uid)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryLookupUserName tries to look up the username for the uid.
|
||||
@@ -3873,15 +3794,14 @@ func (b *LocalBackend) shouldUploadServices() bool {
|
||||
//
|
||||
// On non-multi-user systems, the actor should be set to nil.
|
||||
func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) {
|
||||
var uid ipn.WindowsUserID
|
||||
if actor != nil {
|
||||
uid = actor.UserID()
|
||||
}
|
||||
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
var userIdentifier string
|
||||
if user := cmp.Or(actor, b.currentUser); user != nil {
|
||||
maybeUsername, _ := user.Username()
|
||||
userIdentifier = cmp.Or(maybeUsername, string(user.UserID()))
|
||||
}
|
||||
|
||||
if actor != b.currentUser {
|
||||
if c, ok := b.currentUser.(ipnauth.ActorCloser); ok {
|
||||
c.Close()
|
||||
@@ -3889,149 +3809,10 @@ func (b *LocalBackend) SetCurrentUser(actor ipnauth.Actor) {
|
||||
b.currentUser = actor
|
||||
}
|
||||
|
||||
var action string
|
||||
if actor == nil {
|
||||
action = "disconnected"
|
||||
} else {
|
||||
action = "connected"
|
||||
if b.pm.CurrentUserID() != uid {
|
||||
b.pm.SetCurrentUserID(uid)
|
||||
b.resetForProfileChangeLockedOnEntry(unlock)
|
||||
}
|
||||
reason := fmt.Sprintf("client %s (%s)", action, userIdentifier)
|
||||
b.switchToBestProfileLockedOnEntry(reason, unlock)
|
||||
}
|
||||
|
||||
// SwitchToBestProfile selects the best profile to use,
|
||||
// as reported by [LocalBackend.resolveBestProfileLocked], and switches
|
||||
// to it, unless it's already the current profile. The reason indicates
|
||||
// why the profile is being switched, such as due to a client connecting
|
||||
// or disconnecting, or a change in the desktop session state, and is used
|
||||
// for logging.
|
||||
func (b *LocalBackend) SwitchToBestProfile(reason string) {
|
||||
b.switchToBestProfileLockedOnEntry(reason, b.lockAndGetUnlock())
|
||||
}
|
||||
|
||||
// switchToBestProfileLockedOnEntry is like [LocalBackend.SwitchToBestProfile],
|
||||
// but b.mu must held on entry. It is released on exit.
|
||||
func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock unlockOnce) {
|
||||
defer unlock()
|
||||
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
||||
uid, profileID, background := b.resolveBestProfileLocked()
|
||||
cp, switched := b.pm.SetCurrentUserAndProfile(uid, profileID)
|
||||
switch {
|
||||
case !switched && cp.ID() == "":
|
||||
b.logf("%s: staying on empty profile", reason)
|
||||
case !switched:
|
||||
b.logf("%s: staying on profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID())
|
||||
case cp.ID() == "":
|
||||
b.logf("%s: disconnecting Tailscale", reason)
|
||||
case background:
|
||||
b.logf("%s: switching to background profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID())
|
||||
default:
|
||||
b.logf("%s: switching to profile %q (%s)", reason, cp.UserProfile().LoginName, cp.ID())
|
||||
}
|
||||
if !switched {
|
||||
return
|
||||
}
|
||||
// As an optimization, only reset the dialPlan if the control URL changed.
|
||||
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(); oldControlURL != newControlURL {
|
||||
b.resetDialPlan()
|
||||
}
|
||||
if err := b.resetForProfileChangeLockedOnEntry(unlock); err != nil {
|
||||
// TODO(nickkhyl): The actual reset cannot fail. However,
|
||||
// the TKA initialization or [LocalBackend.Start] can fail.
|
||||
// These errors are not critical as far as we're concerned.
|
||||
// But maybe we should post a notification to the API watchers?
|
||||
b.logf("failed switching profile to %q: %v", profileID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveBestProfileLocked returns the best profile to use based on the current
|
||||
// state of the backend, such as whether a GUI/CLI client is connected, whether
|
||||
// the unattended mode is enabled, the current state of the desktop sessions,
|
||||
// and other factors.
|
||||
//
|
||||
// It returns the user ID, profile ID, and whether the returned profile is
|
||||
// considered a background profile. A background profile is used when no OS user
|
||||
// is actively using Tailscale, such as when no GUI/CLI client is connected
|
||||
// and Unattended Mode is enabled (see also [LocalBackend.getBackgroundProfileLocked]).
|
||||
// An empty profile ID indicates that Tailscale should switch to an empty profile.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) resolveBestProfileLocked() (userID ipn.WindowsUserID, profileID ipn.ProfileID, isBackground bool) {
|
||||
// If a GUI/CLI client is connected, use the connected user's profile, which means
|
||||
// either the current profile if owned by the user, or their default profile.
|
||||
if b.currentUser != nil {
|
||||
cp := b.pm.CurrentProfile()
|
||||
uid := b.currentUser.UserID()
|
||||
|
||||
var profileID ipn.ProfileID
|
||||
// TODO(nickkhyl): check if the current profile is allowed on the device,
|
||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||
// See tailscale/corp#26249.
|
||||
if cp.LocalUserID() == uid {
|
||||
profileID = cp.ID()
|
||||
} else {
|
||||
profileID = b.pm.DefaultUserProfileID(uid)
|
||||
}
|
||||
return uid, profileID, false
|
||||
}
|
||||
|
||||
// Otherwise, if on Windows, use the background profile if one is set.
|
||||
// This includes staying on the current profile if Unattended Mode is enabled
|
||||
// or if AlwaysOn mode is enabled and the current user is still signed in.
|
||||
// If the returned background profileID is "", Tailscale will disconnect
|
||||
// and remain idle until a GUI or CLI client connects.
|
||||
if goos := envknob.GOOS(); goos == "windows" {
|
||||
uid, profileID := b.getBackgroundProfileLocked()
|
||||
return uid, profileID, true
|
||||
}
|
||||
|
||||
// On other platforms, however, Tailscale continues to run in the background
|
||||
// using the current profile.
|
||||
//
|
||||
// TODO(nickkhyl): check if the current profile is allowed on the device,
|
||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||
// See tailscale/corp#26249.
|
||||
return b.pm.CurrentUserID(), b.pm.CurrentProfile().ID(), false
|
||||
}
|
||||
|
||||
// RegisterBackgroundProfileResolver registers a function to be used when
|
||||
// resolving the background profile, until the returned unregister function is called.
|
||||
func (b *LocalBackend) RegisterBackgroundProfileResolver(resolver profileResolver) (unregister func()) {
|
||||
// TODO(nickkhyl): should we allow specifying some kind of priority/altitude for the resolver?
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
handle := b.backgroundProfileResolvers.Add(resolver)
|
||||
return func() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
delete(b.backgroundProfileResolvers, handle)
|
||||
}
|
||||
}
|
||||
|
||||
// getBackgroundProfileLocked returns the user and profile ID to use when no GUI/CLI
|
||||
// client is connected, or "","" if Tailscale should not run in the background.
|
||||
// As of 2025-02-07, it is only used on Windows.
|
||||
func (b *LocalBackend) getBackgroundProfileLocked() (ipn.WindowsUserID, ipn.ProfileID) {
|
||||
// TODO(nickkhyl): check if the returned profile is allowed on the device,
|
||||
// such as when [syspolicy.Tailnet] policy setting requires a specific Tailnet.
|
||||
// See tailscale/corp#26249.
|
||||
|
||||
// If Unattended Mode is enabled for the current profile, keep using it.
|
||||
if b.pm.CurrentPrefs().ForceDaemon() {
|
||||
return b.pm.CurrentProfile().LocalUserID(), b.pm.CurrentProfile().ID()
|
||||
}
|
||||
|
||||
// Otherwise, attempt to resolve the background profile using the background
|
||||
// profile resolvers available on the current platform.
|
||||
for _, resolver := range b.backgroundProfileResolvers {
|
||||
if uid, profileID, ok := resolver(); ok {
|
||||
return uid, profileID
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, switch to an empty profile and disconnect Tailscale
|
||||
// until a GUI or CLI client connects.
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// CurrentUserForTest returns the current user and the associated WindowsUserID.
|
||||
@@ -4372,12 +4153,6 @@ func (b *LocalBackend) hasIngressEnabledLocked() bool {
|
||||
return b.serveConfig.Valid() && b.serveConfig.IsFunnelOn()
|
||||
}
|
||||
|
||||
// shouldWireInactiveIngressLocked reports whether the node is in a state where funnel is not actively enabled, but it
|
||||
// seems that it is intended to be used with funnel.
|
||||
func (b *LocalBackend) shouldWireInactiveIngressLocked() bool {
|
||||
return b.serveConfig.Valid() && !b.hasIngressEnabledLocked() && b.wantIngressLocked()
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
// It returns a read-only copy of the new prefs.
|
||||
@@ -4418,7 +4193,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
||||
}
|
||||
}
|
||||
if netMap != nil {
|
||||
newProfile := profileFromView(netMap.UserProfiles[netMap.User()])
|
||||
newProfile := netMap.UserProfiles[netMap.User()]
|
||||
if newLoginName := newProfile.LoginName; newLoginName != "" {
|
||||
if !oldp.Persist().Valid() {
|
||||
b.logf("active login: %s", newLoginName)
|
||||
@@ -5485,18 +5260,18 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
||||
|
||||
hi.ServicesHash = b.vipServiceHash(b.vipServicesFromPrefsLocked(prefs))
|
||||
|
||||
// The Hostinfo.WantIngress field tells control whether this node wants to
|
||||
// be wired up for ingress connections. If harmless if it's accidentally
|
||||
// true; the actual policy is controlled in tailscaled by ServeConfig. But
|
||||
// if this is accidentally false, then control may not configure DNS
|
||||
// properly. This exists as an optimization to control to program fewer DNS
|
||||
// records that have ingress enabled but are not actually being used.
|
||||
// TODO(irbekrm): once control knows that if hostinfo.IngressEnabled is true,
|
||||
// then wireIngress can be considered true, don't send wireIngress in that case.
|
||||
hi.WireIngress = b.wantIngressLocked()
|
||||
// The Hostinfo.IngressEnabled field is used to communicate to control whether
|
||||
// the node has funnel enabled.
|
||||
// the funnel is actually enabled.
|
||||
hi.IngressEnabled = b.hasIngressEnabledLocked()
|
||||
// The Hostinfo.WantIngress field tells control whether the user intends
|
||||
// to use funnel with this node even though it is not currently enabled.
|
||||
// This is an optimization to control- Funnel requires creation of DNS
|
||||
// records and because DNS propagation can take time, we want to ensure
|
||||
// that the records exist for any node that intends to use funnel even
|
||||
// if it's not enabled. If hi.IngressEnabled is true, control knows that
|
||||
// DNS records are needed, so we can save bandwidth and not send
|
||||
// WireIngress.
|
||||
hi.WireIngress = b.shouldWireInactiveIngressLocked()
|
||||
hi.AppConnector.Set(prefs.AppConnector().Advertise)
|
||||
}
|
||||
|
||||
@@ -5840,6 +5615,41 @@ func (b *LocalBackend) resetAuthURLLocked() {
|
||||
b.authActor = nil
|
||||
}
|
||||
|
||||
// ResetForClientDisconnect resets the backend for GUI clients running
|
||||
// in interactive (non-headless) mode. This is currently used only by
|
||||
// Windows. This causes all state to be cleared, lest an unrelated user
|
||||
// connect to tailscaled next. But it does not trigger a logout; we
|
||||
// don't want to the user to have to reauthenticate in the future
|
||||
// when they restart the GUI.
|
||||
func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.logf("LocalBackend.ResetForClientDisconnect")
|
||||
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
prevCC := b.resetControlClientLocked()
|
||||
if prevCC != nil {
|
||||
// Needs to happen without b.mu held.
|
||||
defer prevCC.Shutdown()
|
||||
}
|
||||
|
||||
b.setNetMapLocked(nil)
|
||||
b.pm.Reset()
|
||||
if b.currentUser != nil {
|
||||
if c, ok := b.currentUser.(ipnauth.ActorCloser); ok {
|
||||
c.Close()
|
||||
}
|
||||
b.currentUser = nil
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.resetAuthURLLocked()
|
||||
b.activeLogin = ""
|
||||
b.resetDialPlan()
|
||||
b.resetAlwaysOnOverrideLocked()
|
||||
b.setAtomicValuesFromPrefsLocked(ipn.PrefsView{})
|
||||
b.enterStateLockedOnEntry(ipn.Stopped, unlock)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
|
||||
|
||||
// ShouldRunWebClient reports whether the web client is being run
|
||||
@@ -6028,7 +5838,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = cmp.Or(profileFromView(nm.UserProfiles[nm.User()]).LoginName, "<missing-profile>")
|
||||
login = cmp.Or(nm.UserProfiles[nm.User()].LoginName, "<missing-profile>")
|
||||
}
|
||||
b.netMap = nm
|
||||
b.updatePeersFromNetmapLocked(nm)
|
||||
@@ -6410,6 +6220,8 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
|
||||
// updateIngressLocked updates the hostinfo.WireIngress and hostinfo.IngressEnabled fields and kicks off a Hostinfo
|
||||
// update if the values have changed.
|
||||
// TODO(irbekrm): once control knows that if hostinfo.IngressEnabled is true, then wireIngress can be considered true,
|
||||
// we can stop sending hostinfo.WireIngress in that case.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) updateIngressLocked() {
|
||||
@@ -6417,16 +6229,16 @@ func (b *LocalBackend) updateIngressLocked() {
|
||||
return
|
||||
}
|
||||
hostInfoChanged := false
|
||||
if wire := b.wantIngressLocked(); b.hostinfo.WireIngress != wire {
|
||||
b.logf("Hostinfo.WireIngress changed to %v", wire)
|
||||
b.hostinfo.WireIngress = wire
|
||||
hostInfoChanged = true
|
||||
}
|
||||
if ie := b.hasIngressEnabledLocked(); b.hostinfo.IngressEnabled != ie {
|
||||
b.logf("Hostinfo.IngressEnabled changed to %v", ie)
|
||||
b.hostinfo.IngressEnabled = ie
|
||||
hostInfoChanged = true
|
||||
}
|
||||
if wire := b.shouldWireInactiveIngressLocked(); b.hostinfo.WireIngress != wire {
|
||||
b.logf("Hostinfo.WireIngress changed to %v", wire)
|
||||
b.hostinfo.WireIngress = wire
|
||||
hostInfoChanged = true
|
||||
}
|
||||
// Kick off a Hostinfo update to control if ingress status has changed.
|
||||
if hostInfoChanged {
|
||||
b.goTracker.Go(b.doSetHostinfoFilterServices)
|
||||
@@ -6634,41 +6446,6 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) taildropTargetStatus(p tailcfg.NodeView) ipnstate.TaildropTargetStatus {
|
||||
if b.netMap == nil || b.state != ipn.Running {
|
||||
return ipnstate.TaildropTargetIpnStateNotRunning
|
||||
}
|
||||
if b.netMap == nil {
|
||||
return ipnstate.TaildropTargetNoNetmapAvailable
|
||||
}
|
||||
if !b.capFileSharing {
|
||||
return ipnstate.TaildropTargetMissingCap
|
||||
}
|
||||
|
||||
if !p.Online().Get() {
|
||||
return ipnstate.TaildropTargetOffline
|
||||
}
|
||||
|
||||
if !p.Valid() {
|
||||
return ipnstate.TaildropTargetNoPeerInfo
|
||||
}
|
||||
if b.netMap.User() != p.User() {
|
||||
// Different user must have the explicit file sharing target capability
|
||||
if p.Addresses().Len() == 0 ||
|
||||
!b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
return ipnstate.TaildropTargetOwnedByOtherUser
|
||||
}
|
||||
}
|
||||
|
||||
if p.Hostinfo().OS() == "tvOS" {
|
||||
return ipnstate.TaildropTargetUnsupportedOS
|
||||
}
|
||||
if peerAPIBase(b.netMap, p) == "" {
|
||||
return ipnstate.TaildropTargetNoPeerAPI
|
||||
}
|
||||
return ipnstate.TaildropTargetAvailable
|
||||
}
|
||||
|
||||
// peerIsTaildropTargetLocked reports whether p is a valid Taildrop file
|
||||
// recipient from this node according to its ownership and the capabilities in
|
||||
// the netmap.
|
||||
@@ -7307,20 +7084,21 @@ func (b *LocalBackend) ShouldInterceptVIPServiceTCPPort(ap netip.AddrPort) bool
|
||||
// It will restart the backend on success.
|
||||
// If the profile is not known, it returns an errProfileNotFound.
|
||||
func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if b.pm.CurrentProfile().ID() == profile {
|
||||
if b.CurrentProfile().ID() == profile {
|
||||
return nil
|
||||
}
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
||||
if err := b.pm.SwitchProfile(profile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// As an optimization, only reset the dialPlan if the control URL changed.
|
||||
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(); oldControlURL != newControlURL {
|
||||
// As an optimization, only reset the dialPlan if the control URL
|
||||
// changed; we treat an empty URL as "unknown" and always reset.
|
||||
newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
|
||||
if oldControlURL != newControlURL || oldControlURL == "" || newControlURL == "" {
|
||||
b.resetDialPlan()
|
||||
}
|
||||
|
||||
@@ -7400,24 +7178,15 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err
|
||||
b.setNetMapLocked(nil) // Reset netmap.
|
||||
// Reset the NetworkMap in the engine
|
||||
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||
if prevCC := b.resetControlClientLocked(); prevCC != nil {
|
||||
// Needs to happen without b.mu held.
|
||||
defer prevCC.Shutdown()
|
||||
if err := b.initTKALocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
// TKA errors should not prevent resetting the backend state.
|
||||
// However, we should still return the error to the caller.
|
||||
tkaErr := b.initTKALocked()
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
b.lastSuggestedExitNode = ""
|
||||
b.keyExpired = false
|
||||
b.resetAlwaysOnOverrideLocked()
|
||||
b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs())
|
||||
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
||||
b.health.SetLocalLogConfigHealth(nil)
|
||||
if tkaErr != nil {
|
||||
return tkaErr
|
||||
}
|
||||
return b.Start(ipn.Options{})
|
||||
}
|
||||
|
||||
|
||||
@@ -1052,13 +1052,13 @@ func TestWhoIs(t *testing.T) {
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.200.200.200/32")},
|
||||
}).View(),
|
||||
},
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
|
||||
10: (&tailcfg.UserProfile{
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||
10: {
|
||||
DisplayName: "Myself",
|
||||
}).View(),
|
||||
20: (&tailcfg.UserProfile{
|
||||
},
|
||||
20: {
|
||||
DisplayName: "Peer",
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
})
|
||||
tests := []struct {
|
||||
@@ -2754,12 +2754,12 @@ func TestTCPHandlerForDstWithVIPService(t *testing.T) {
|
||||
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)},
|
||||
},
|
||||
}).View(),
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
|
||||
tailcfg.UserID(1): (&tailcfg.UserProfile{
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||
tailcfg.UserID(1): {
|
||||
LoginName: "someone@example.com",
|
||||
DisplayName: "Some One",
|
||||
ProfilePicURL: "https://example.com/photo.jpg",
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -5084,7 +5084,7 @@ func TestUpdateIngressLocked(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantIngress: true,
|
||||
wantWireIngress: false, // implied by wantIngress
|
||||
wantWireIngress: true,
|
||||
wantControlUpdate: true,
|
||||
},
|
||||
{
|
||||
@@ -5111,6 +5111,7 @@ func TestUpdateIngressLocked(t *testing.T) {
|
||||
name: "funnel_enabled_no_change",
|
||||
hi: &tailcfg.Hostinfo{
|
||||
IngressEnabled: true,
|
||||
WireIngress: true,
|
||||
},
|
||||
sc: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{
|
||||
@@ -5118,7 +5119,7 @@ func TestUpdateIngressLocked(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantIngress: true,
|
||||
wantWireIngress: false, // implied by wantIngress
|
||||
wantWireIngress: true,
|
||||
},
|
||||
{
|
||||
name: "funnel_disabled_no_change",
|
||||
@@ -5136,6 +5137,7 @@ func TestUpdateIngressLocked(t *testing.T) {
|
||||
name: "funnel_changes_to_disabled",
|
||||
hi: &tailcfg.Hostinfo{
|
||||
IngressEnabled: true,
|
||||
WireIngress: true,
|
||||
},
|
||||
sc: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{
|
||||
@@ -5155,8 +5157,8 @@ func TestUpdateIngressLocked(t *testing.T) {
|
||||
"tailnet.xyz:443": true,
|
||||
},
|
||||
},
|
||||
wantWireIngress: true,
|
||||
wantIngress: true,
|
||||
wantWireIngress: false, // implied by wantIngress
|
||||
wantControlUpdate: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tstest"
|
||||
|
||||
@@ -77,49 +77,6 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetCurrentUserAndProfile sets the current user ID and switches the specified
|
||||
// profile, if it is accessible to the user. If the profile does not exist,
|
||||
// or is not accessible, it switches to the user's default profile,
|
||||
// creating a new one if necessary.
|
||||
//
|
||||
// It is a shorthand for [profileManager.SetCurrentUserID] followed by
|
||||
// [profileManager.SwitchProfile], but it is more efficient as it switches
|
||||
// directly to the specified profile rather than switching to the user's
|
||||
// default profile first.
|
||||
//
|
||||
// As a special case, if the specified profile ID "", it creates a new
|
||||
// profile for the user and switches to it, unless the current profile
|
||||
// is already a new, empty profile owned by the user.
|
||||
//
|
||||
// It returns the current profile and whether the call resulted
|
||||
// in a profile switch.
|
||||
func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) {
|
||||
pm.currentUserID = uid
|
||||
|
||||
if profileID == "" {
|
||||
if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid {
|
||||
return pm.currentProfile, false
|
||||
}
|
||||
pm.NewProfileForUser(uid)
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
|
||||
if profile, err := pm.ProfileByID(profileID); err == nil {
|
||||
if pm.CurrentProfile().ID() == profileID {
|
||||
return pm.currentProfile, false
|
||||
}
|
||||
if err := pm.SwitchProfile(profile.ID()); err == nil {
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
}
|
||||
|
||||
if err := pm.SwitchToDefaultProfile(); err != nil {
|
||||
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
|
||||
pm.NewProfile()
|
||||
}
|
||||
return pm.currentProfile, true
|
||||
}
|
||||
|
||||
// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user,
|
||||
// or an empty string if the specified user does not have a default profile.
|
||||
func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID {
|
||||
@@ -140,7 +97,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
|
||||
}
|
||||
|
||||
pk := ipn.StateKey(string(b))
|
||||
prof := pm.findProfileByKey(uid, pk)
|
||||
prof := pm.findProfileByKey(pk)
|
||||
if !prof.Valid() {
|
||||
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
|
||||
return ""
|
||||
@@ -151,24 +108,17 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
|
||||
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
|
||||
// does not have access to the specified profile.
|
||||
func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
|
||||
return pm.checkProfileAccessAs(pm.currentUserID, profile)
|
||||
}
|
||||
|
||||
// checkProfileAccessAs returns an [errProfileAccessDenied] if the specified user
|
||||
// does not have access to the specified profile.
|
||||
func (pm *profileManager) checkProfileAccessAs(uid ipn.WindowsUserID, profile ipn.LoginProfileView) error {
|
||||
if uid != "" && profile.LocalUserID() != uid {
|
||||
if pm.currentUserID != "" && profile.LocalUserID() != pm.currentUserID {
|
||||
return errProfileAccessDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// allProfilesFor returns all profiles accessible to the specified user.
|
||||
// allProfiles returns all profiles accessible to the current user.
|
||||
// The returned profiles are sorted by Name.
|
||||
func (pm *profileManager) allProfilesFor(uid ipn.WindowsUserID) []ipn.LoginProfileView {
|
||||
out := make([]ipn.LoginProfileView, 0, len(pm.knownProfiles))
|
||||
func (pm *profileManager) allProfiles() (out []ipn.LoginProfileView) {
|
||||
for _, p := range pm.knownProfiles {
|
||||
if pm.checkProfileAccessAs(uid, p) == nil {
|
||||
if pm.checkProfileAccess(p) == nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
@@ -178,10 +128,10 @@ func (pm *profileManager) allProfilesFor(uid ipn.WindowsUserID) []ipn.LoginProfi
|
||||
return out
|
||||
}
|
||||
|
||||
// matchingProfiles is like [profileManager.allProfilesFor], but returns only profiles
|
||||
// matchingProfiles is like [profileManager.allProfiles], but returns only profiles
|
||||
// matching the given predicate.
|
||||
func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
|
||||
all := pm.allProfilesFor(uid)
|
||||
func (pm *profileManager) matchingProfiles(f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
|
||||
all := pm.allProfiles()
|
||||
out = all[:0]
|
||||
for _, p := range all {
|
||||
if f(p) {
|
||||
@@ -194,8 +144,8 @@ func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.Log
|
||||
// findMatchingProfiles returns all profiles accessible to the current user
|
||||
// that represent the same node/user as prefs.
|
||||
// The returned profiles are sorted by Name.
|
||||
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
|
||||
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []ipn.LoginProfileView {
|
||||
return pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
|
||||
return p.ControlURL() == prefs.ControlURL() &&
|
||||
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
|
||||
p.NodeID() == prefs.Persist().NodeID())
|
||||
@@ -206,16 +156,16 @@ func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.
|
||||
// given name. It returns "" if no such profile exists among profiles
|
||||
// accessible to the current user.
|
||||
func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
|
||||
p := pm.findProfileByName(pm.currentUserID, name)
|
||||
p := pm.findProfileByName(name)
|
||||
if !p.Valid() {
|
||||
return ""
|
||||
}
|
||||
return p.ID()
|
||||
}
|
||||
|
||||
func (pm *profileManager) findProfileByName(uid ipn.WindowsUserID, name string) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.Name() == name && pm.checkProfileAccessAs(uid, p) == nil
|
||||
func (pm *profileManager) findProfileByName(name string) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
|
||||
return p.Name() == name
|
||||
})
|
||||
if len(out) == 0 {
|
||||
return ipn.LoginProfileView{}
|
||||
@@ -226,9 +176,9 @@ func (pm *profileManager) findProfileByName(uid ipn.WindowsUserID, name string)
|
||||
return out[0]
|
||||
}
|
||||
|
||||
func (pm *profileManager) findProfileByKey(uid ipn.WindowsUserID, key ipn.StateKey) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
|
||||
return p.Key() == key && pm.checkProfileAccessAs(uid, p) == nil
|
||||
func (pm *profileManager) findProfileByKey(key ipn.StateKey) ipn.LoginProfileView {
|
||||
out := pm.matchingProfiles(func(p ipn.LoginProfileView) bool {
|
||||
return p.Key() == key
|
||||
})
|
||||
if len(out) == 0 {
|
||||
return ipn.LoginProfileView{}
|
||||
@@ -272,7 +222,7 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
|
||||
}
|
||||
|
||||
// Check if we already have an existing profile that matches the user/node.
|
||||
if existing := pm.findMatchingProfiles(pm.currentUserID, prefsIn); len(existing) > 0 {
|
||||
if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 {
|
||||
// We already have a profile for this user/node we should reuse it. Also
|
||||
// cleanup any other duplicate profiles.
|
||||
cp = existing[0]
|
||||
@@ -426,7 +376,12 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
|
||||
|
||||
// Profiles returns the list of known profiles accessible to the current user.
|
||||
func (pm *profileManager) Profiles() []ipn.LoginProfileView {
|
||||
return pm.allProfilesFor(pm.currentUserID)
|
||||
allProfiles := pm.allProfiles()
|
||||
out := make([]ipn.LoginProfileView, len(allProfiles))
|
||||
for i, p := range allProfiles {
|
||||
out[i] = p
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ProfileByID returns a profile with the given id, if it is accessible to the current user.
|
||||
|
||||
@@ -129,10 +129,10 @@ func TestProfileList(t *testing.T) {
|
||||
|
||||
pm.SetCurrentUserID("user1")
|
||||
checkProfiles(t, "alice", "bob")
|
||||
if lp := pm.findProfileByKey("user1", carol.Key()); lp.Valid() {
|
||||
if lp := pm.findProfileByKey(carol.Key()); lp.Valid() {
|
||||
t.Fatalf("found profile for user2 in user1's profile list")
|
||||
}
|
||||
if lp := pm.findProfileByName("user1", carol.Name()); lp.Valid() {
|
||||
if lp := pm.findProfileByName(carol.Name()); lp.Valid() {
|
||||
t.Fatalf("found profile for user2 in user1's profile list")
|
||||
}
|
||||
|
||||
|
||||
@@ -327,12 +327,12 @@ func TestServeConfigServices(t *testing.T) {
|
||||
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)},
|
||||
},
|
||||
}).View(),
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
|
||||
tailcfg.UserID(1): (&tailcfg.UserProfile{
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||
tailcfg.UserID(1): {
|
||||
LoginName: "someone@example.com",
|
||||
DisplayName: "Some One",
|
||||
ProfilePicURL: "https://example.com/photo.jpg",
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -905,12 +905,12 @@ func newTestBackend(t *testing.T) *LocalBackend {
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "example.ts.net",
|
||||
}).View(),
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{
|
||||
tailcfg.UserID(1): (&tailcfg.UserProfile{
|
||||
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||
tailcfg.UserID(1): {
|
||||
LoginName: "someone@example.com",
|
||||
DisplayName: "Some One",
|
||||
ProfilePicURL: "https://example.com/photo.jpg",
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
}
|
||||
b.peers = map[tailcfg.NodeID]tailcfg.NodeView{
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/web"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -36,16 +36,16 @@ type webClient struct {
|
||||
|
||||
server *web.Server // or nil, initialized lazily
|
||||
|
||||
// lc optionally specifies a local.Client to use to connect
|
||||
// lc optionally specifies a LocalClient to use to connect
|
||||
// to the localapi for this tailscaled instance.
|
||||
// If nil, a default is used.
|
||||
lc *local.Client
|
||||
lc *tailscale.LocalClient
|
||||
}
|
||||
|
||||
// ConfigureWebClient configures b.web prior to use.
|
||||
// Specifially, it sets b.web.lc to the provided local.Client.
|
||||
// Specifially, it sets b.web.lc to the provided LocalClient.
|
||||
// If provided as nil, b.web.lc is cleared out.
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {
|
||||
b.webClient.mu.Lock()
|
||||
defer b.webClient.mu.Unlock()
|
||||
b.webClient.lc = lc
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
)
|
||||
|
||||
const webClientPort = 5252
|
||||
|
||||
type webClient struct{}
|
||||
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {}
|
||||
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {}
|
||||
|
||||
func (b *LocalBackend) webClientGetOrInit() error {
|
||||
return errors.New("not implemented")
|
||||
|
||||
@@ -32,7 +32,6 @@ type actor struct {
|
||||
ci *ipnauth.ConnIdentity
|
||||
|
||||
clientID ipnauth.ClientID
|
||||
userID ipn.WindowsUserID // cached Windows user ID of the connected client process.
|
||||
// accessOverrideReason specifies the reason for overriding certain access restrictions,
|
||||
// such as permitting a user to disconnect when the always-on mode is enabled,
|
||||
// provided that such justification is allowed by the policy.
|
||||
@@ -60,14 +59,7 @@ func newActor(logf logger.Logf, c net.Conn) (*actor, error) {
|
||||
// connectivity on domain-joined devices and/or be slow.
|
||||
clientID = ipnauth.ClientIDFrom(pid)
|
||||
}
|
||||
return &actor{
|
||||
logf: logf,
|
||||
ci: ci,
|
||||
clientID: clientID,
|
||||
userID: ci.WindowsUserID(),
|
||||
isLocalSystem: connIsLocalSystem(ci),
|
||||
},
|
||||
nil
|
||||
return &actor{logf: logf, ci: ci, clientID: clientID, isLocalSystem: connIsLocalSystem(ci)}, nil
|
||||
}
|
||||
|
||||
// actorWithAccessOverride returns a new actor that carries the specified
|
||||
@@ -81,7 +73,6 @@ func actorWithAccessOverride(baseActor *actor, reason string) *actor {
|
||||
logf: baseActor.logf,
|
||||
ci: baseActor.ci,
|
||||
clientID: baseActor.clientID,
|
||||
userID: baseActor.userID,
|
||||
accessOverrideReason: reason,
|
||||
isLocalSystem: baseActor.isLocalSystem,
|
||||
}
|
||||
@@ -115,7 +106,7 @@ func (a *actor) IsLocalAdmin(operatorUID string) bool {
|
||||
|
||||
// UserID implements [ipnauth.Actor].
|
||||
func (a *actor) UserID() ipn.WindowsUserID {
|
||||
return a.userID
|
||||
return a.ci.WindowsUserID()
|
||||
}
|
||||
|
||||
func (a *actor) pid() int {
|
||||
@@ -127,9 +118,6 @@ func (a *actor) ClientID() (_ ipnauth.ClientID, ok bool) {
|
||||
return a.clientID, a.clientID != ipnauth.NoClientID
|
||||
}
|
||||
|
||||
// Context implements [ipnauth.Actor].
|
||||
func (a *actor) Context() context.Context { return context.Background() }
|
||||
|
||||
// Username implements [ipnauth.Actor].
|
||||
func (a *actor) Username() (string, error) {
|
||||
if a.ci == nil {
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"sync/atomic"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
@@ -42,6 +42,12 @@ type Server struct {
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // must be non-nil
|
||||
backendLogID logid.PublicID
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 active HTTP requests. That is, this is whether the backend is
|
||||
// being run in "client mode" that requires an active GUI
|
||||
// connection (such as on Windows by default). Even if this
|
||||
// is true, the ForceDaemon pref can override this.
|
||||
resetOnZero bool
|
||||
|
||||
// mu guards the fields that follow.
|
||||
// lock order: mu, then LocalBackend.mu
|
||||
@@ -190,22 +196,22 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer onDone()
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.backendLogID)
|
||||
if actor, ok := ci.(*actor); ok {
|
||||
lah.PermitRead, lah.PermitWrite = actor.Permissions(lb.OperatorUserID())
|
||||
lah.PermitCert = actor.CanFetchCerts()
|
||||
reason, err := base64.StdEncoding.DecodeString(r.Header.Get(apitype.RequestReasonHeader))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid reason header", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ci = actorWithAccessOverride(actor, string(reason))
|
||||
}
|
||||
|
||||
lah := localapi.NewHandler(ci, lb, s.logf, s.backendLogID)
|
||||
if actor, ok := ci.(*actor); ok {
|
||||
lah.PermitRead, lah.PermitWrite = actor.Permissions(lb.OperatorUserID())
|
||||
lah.PermitCert = actor.CanFetchCerts()
|
||||
lah.Actor = actorWithAccessOverride(actor, string(reason))
|
||||
} else if testenv.InTest() {
|
||||
lah.PermitRead, lah.PermitWrite = true, true
|
||||
}
|
||||
if lah.Actor == nil {
|
||||
lah.Actor = ci
|
||||
}
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -312,13 +318,6 @@ func (s *Server) blockWhileIdentityInUse(ctx context.Context, actor ipnauth.Acto
|
||||
// Unix-like platforms and specifies the ID of a local user
|
||||
// (in the os/user.User.Uid string form) who is allowed
|
||||
// to operate tailscaled without being root or using sudo.
|
||||
//
|
||||
// Sandboxed macos clients must directly supply, or be able to read,
|
||||
// an explicit token. Permission is inferred by validating that
|
||||
// token. Sandboxed macos clients also don't use ipnserver.actor at all
|
||||
// (and prior to that, they didn't use ipnauth.ConnIdentity)
|
||||
//
|
||||
// See safesocket and safesocket_darwin.
|
||||
func (a *actor) Permissions(operatorUID string) (read, write bool) {
|
||||
switch envknob.GOOS() {
|
||||
case "windows":
|
||||
@@ -423,8 +422,13 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, actor ipnauth.Actor) (o
|
||||
return
|
||||
}
|
||||
|
||||
if envknob.GOOS() == "windows" && !actor.IsLocalSystem() {
|
||||
lb.SetCurrentUser(nil)
|
||||
if s.resetOnZero {
|
||||
if lb.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
lb.ResetForClientDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Wake up callers waiting for the server to be idle:
|
||||
@@ -448,6 +452,7 @@ func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server
|
||||
backendLogID: logID,
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
resetOnZero: envknob.GOOS() == "windows",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,14 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/localclient/tailscale"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -331,7 +330,7 @@ func newTestIPNServer(tb testing.TB, lb *ipnlocal.LocalBackend, enableLogging bo
|
||||
|
||||
type testIPNClient struct {
|
||||
tb testing.TB
|
||||
*local.Client
|
||||
*tailscale.LocalClient
|
||||
User *ipnauth.TestActor
|
||||
}
|
||||
|
||||
@@ -339,7 +338,7 @@ func (c *testIPNClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt
|
||||
c.tb.Helper()
|
||||
ctx, cancelWatcher := context.WithCancel(ctx)
|
||||
c.tb.Cleanup(cancelWatcher)
|
||||
watcher, err := c.Client.WatchIPNBus(ctx, mask)
|
||||
watcher, err := c.LocalClient.WatchIPNBus(ctx, mask)
|
||||
if err != nil {
|
||||
c.tb.Fatalf("WatchIPNBus(%q): %v", c.User.Name, err)
|
||||
}
|
||||
@@ -360,7 +359,7 @@ type testIPNServer struct {
|
||||
tb testing.TB
|
||||
*Server
|
||||
clientID atomic.Int64
|
||||
getClient func(*ipnauth.TestActor) *local.Client
|
||||
getClient func(*ipnauth.TestActor) *tailscale.LocalClient
|
||||
|
||||
actorsMu sync.Mutex
|
||||
actors map[string]*ipnauth.TestActor
|
||||
@@ -370,9 +369,9 @@ func (s *testIPNServer) getClientAs(name string) *testIPNClient {
|
||||
clientID := fmt.Sprintf("Client-%d", 1+s.clientID.Add(1))
|
||||
user := s.makeTestUser(name, clientID)
|
||||
return &testIPNClient{
|
||||
tb: s.tb,
|
||||
Client: s.getClient(user),
|
||||
User: user,
|
||||
tb: s.tb,
|
||||
LocalClient: s.getClient(user),
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +427,7 @@ func (s *testIPNServer) checkCurrentUser(want *ipnauth.TestActor) {
|
||||
|
||||
// startTestIPNServer starts a [httptest.Server] that hosts the specified IPN server for the
|
||||
// duration of the test, using the specified base context for incoming requests.
|
||||
// It returns a function that creates a [local.Client] as a given [ipnauth.TestActor].
|
||||
// It returns a function that creates a [tailscale.LocalClient] as a given [ipnauth.TestActor].
|
||||
func startTestIPNServer(tb testing.TB, baseContext context.Context, server *Server) *testIPNServer {
|
||||
tb.Helper()
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -449,8 +448,8 @@ func startTestIPNServer(tb testing.TB, baseContext context.Context, server *Serv
|
||||
return &testIPNServer{
|
||||
tb: tb,
|
||||
Server: server,
|
||||
getClient: func(actor *ipnauth.TestActor) *local.Client {
|
||||
return &local.Client{Transport: newTestRoundTripper(ts, actor)}
|
||||
getClient: func(actor *ipnauth.TestActor) *tailscale.LocalClient {
|
||||
return &tailscale.LocalClient{Transport: newTestRoundTripper(ts, actor)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,12 +270,6 @@ type PeerStatus struct {
|
||||
// PeerAPIURL are the URLs of the node's PeerAPI servers.
|
||||
PeerAPIURL []string
|
||||
|
||||
// TaildropTargetStatus represents the node's eligibility to have files shared to it.
|
||||
TaildropTarget TaildropTargetStatus
|
||||
|
||||
// Reason why this peer cannot receive files. Empty if CanReceiveFiles=true
|
||||
NoFileSharingReason string
|
||||
|
||||
// Capabilities are capabilities that the node has.
|
||||
// They're free-form strings, but should be in the form of URLs/URIs
|
||||
// such as:
|
||||
@@ -324,21 +318,6 @@ type PeerStatus struct {
|
||||
Location *tailcfg.Location `json:",omitempty"`
|
||||
}
|
||||
|
||||
type TaildropTargetStatus int
|
||||
|
||||
const (
|
||||
TaildropTargetUnknown TaildropTargetStatus = iota
|
||||
TaildropTargetAvailable
|
||||
TaildropTargetNoNetmapAvailable
|
||||
TaildropTargetIpnStateNotRunning
|
||||
TaildropTargetMissingCap
|
||||
TaildropTargetOffline
|
||||
TaildropTargetNoPeerInfo
|
||||
TaildropTargetUnsupportedOS
|
||||
TaildropTargetNoPeerAPI
|
||||
TaildropTargetOwnedByOtherUser
|
||||
)
|
||||
|
||||
// HasCap reports whether ps has the given capability.
|
||||
func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
|
||||
return ps.CapMap.Contains(cap)
|
||||
@@ -388,7 +367,7 @@ func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfileView) {
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
if sb.locked {
|
||||
log.Printf("[unexpected] ipnstate: AddUser after Locked")
|
||||
return
|
||||
@@ -398,7 +377,7 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfileView)
|
||||
sb.st.User = make(map[tailcfg.UserID]tailcfg.UserProfile)
|
||||
}
|
||||
|
||||
sb.st.User[id] = *up.AsStruct()
|
||||
sb.st.User[id] = up
|
||||
}
|
||||
|
||||
// AddIP adds a Tailscale IP address to the status.
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
@@ -41,6 +40,7 @@ import (
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -169,9 +169,10 @@ var (
|
||||
metrics = map[string]*clientmetric.Metric{}
|
||||
)
|
||||
|
||||
// NewHandler creates a new LocalAPI HTTP handler. All parameters are required.
|
||||
func NewHandler(actor ipnauth.Actor, b *ipnlocal.LocalBackend, logf logger.Logf, logID logid.PublicID) *Handler {
|
||||
return &Handler{Actor: actor, b: b, logf: logf, backendLogID: logID, clock: tstime.StdClock{}}
|
||||
// NewHandler creates a new LocalAPI HTTP handler. All parameters except netMon
|
||||
// are required (if non-nil it's used to do faster interface lookups).
|
||||
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID logid.PublicID) *Handler {
|
||||
return &Handler{b: b, logf: logf, backendLogID: logID, clock: tstime.StdClock{}}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/k8s-operator/sessionrecording/spdy"
|
||||
"tailscale.com/k8s-operator/sessionrecording/tsrecorder"
|
||||
"tailscale.com/k8s-operator/sessionrecording/ws"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/k8s-operator/sessionrecording/fakes"
|
||||
"tailscale.com/localclient/tailscale/apitype"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
|
||||
@@ -26,11 +26,12 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [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/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/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/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/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.6.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/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/2e55bd4e08b0/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/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
@@ -40,42 +41,45 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [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/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/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.11/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.11/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.11/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.4/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.4/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.4/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/fbb4dce95f42/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.58/LICENSE))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/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/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/35a0c7bd7edc/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android/libtailscale](https://pkg.go.dev/github.com/tailscale/tailscale-android/libtailscale) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/0b8b35511f19/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/799c1978fafc/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/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/d2acac8f3701/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/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))
|
||||
- [go4.org/intern](https://pkg.go.dev/go4.org/intern) ([BSD-3-Clause](https://github.com/go4org/intern/blob/ae77deb06f29/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/4f986261bf13/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))
|
||||
- [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/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.26.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713: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/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/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad: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/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/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/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/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.20.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.28.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.8.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.23.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.23.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.17.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.5.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.24.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))
|
||||
- [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))
|
||||
|
||||
@@ -12,29 +12,29 @@ 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))
|
||||
- [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.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.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.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.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.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.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.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.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.12.12/service/internal/presigned-url/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.32.4/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.27.28/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.17.28/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.16.12/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.3.23/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.6.23/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.8.1/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.32.4/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.12.0/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.11.18/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.45.0/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.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.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.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/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/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.22.5/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.26.5/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.30.4/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.0/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.0/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/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/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.6.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/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/2e55bd4e08b0/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/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
@@ -45,13 +45,14 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [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/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/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.4.1/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.11/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.11/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.11/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/fbb4dce95f42/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.58/LICENSE))
|
||||
@@ -59,25 +60,27 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/prometheus-community/pro-bing](https://pkg.go.dev/github.com/prometheus-community/pro-bing) ([MIT](https://github.com/prometheus-community/pro-bing/blob/v0.4.0/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/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/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/35a0c7bd7edc/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/0b8b35511f19/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/799c1978fafc/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/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/d2acac8f3701/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/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))
|
||||
- [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))
|
||||
- [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/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b: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/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad: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/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/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))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/9414b50a5633/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.28.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fc45aab8:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.30.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.9.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.27.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.25.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.20.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.5.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))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
@@ -17,22 +17,22 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/akutz/memconn](https://pkg.go.dev/github.com/akutz/memconn) ([Apache-2.0](https://github.com/akutz/memconn/blob/v0.1.0/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/1a75b4708caa/LICENSE))
|
||||
- [github.com/anmitsu/go-shlex](https://pkg.go.dev/github.com/anmitsu/go-shlex) ([MIT](https://github.com/anmitsu/go-shlex/blob/38f4b401e2be/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.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.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.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.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.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.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.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.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.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.12.12/service/internal/presigned-url/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.24.1/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/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/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/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/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/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/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/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/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/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.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.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.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/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/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/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/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/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/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/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/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))
|
||||
@@ -40,22 +40,23 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/a09d6be7affa/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/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.6.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/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/2e55bd4e08b0/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/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/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/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/v1.7.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/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/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/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/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.11/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.11/internal/snapref/LICENSE))
|
||||
@@ -65,7 +66,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.13/LICENSE))
|
||||
- [github.com/mattn/go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty) ([MIT](https://github.com/mattn/go-isatty/blob/v0.0.20/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/fbb4dce95f42/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.58/LICENSE))
|
||||
@@ -78,32 +79,34 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/d3fa0460f47e/LICENSE.md))
|
||||
- [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/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/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/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/35a0c7bd7edc/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/d4cd19a26976/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5db17b287bf1/LICENSE))
|
||||
- [github.com/tailscale/wf](https://pkg.go.dev/github.com/tailscale/wf) ([BSD-3-Clause](https://github.com/tailscale/wf/blob/6fbb0a674ee6/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/0b8b35511f19/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/4e883d38c8d3/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/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/v0.12.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/d2acac8f3701/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/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))
|
||||
- [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/4f986261bf13/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/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/7588d65b: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/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/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/1c14dcad: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/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/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/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.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.16.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.9.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.27.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.22.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.16.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.5.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/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))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.32.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))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.3/LICENSE))
|
||||
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/LICENSE))
|
||||
- [sigs.k8s.io/yaml/goyaml.v2](https://pkg.go.dev/sigs.k8s.io/yaml/goyaml.v2) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/goyaml.v2/LICENSE))
|
||||
- [software.sslmate.com/src/go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) ([BSD-3-Clause](https://github.com/SSLMate/go-pkcs12/blob/v0.4.0/LICENSE))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user