Compare commits
38 Commits
fran/appc-
...
enable-exi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78c36f53fe | ||
|
|
92d3f64e95 | ||
|
|
93618a3518 | ||
|
|
2409661a0d | ||
|
|
b9611461e5 | ||
|
|
262fa8a01e | ||
|
|
9eaa56df93 | ||
|
|
14683371ee | ||
|
|
1c259100b0 | ||
|
|
1535d0feca | ||
|
|
f384742375 | ||
|
|
92ca770b8d | ||
|
|
27038ee3c2 | ||
|
|
ec87e219ae | ||
|
|
e2586bc674 | ||
|
|
7558a1d594 | ||
|
|
e20ce7bf0c | ||
|
|
1d2af801fa | ||
|
|
e80b99cdd1 | ||
|
|
5aa4cfad06 | ||
|
|
e7599c1f7e | ||
|
|
5fb721d4ad | ||
|
|
af61179c2f | ||
|
|
b0941b79d6 | ||
|
|
354cac74a9 | ||
|
|
9401b09028 | ||
|
|
9b5176c4d9 | ||
|
|
9e2f58f846 | ||
|
|
b60c4664c7 | ||
|
|
3e6306a782 | ||
|
|
8f27520633 | ||
|
|
008676f76e | ||
|
|
66e4d843c1 | ||
|
|
bed818a978 | ||
|
|
0d8cd1645a | ||
|
|
eb42a16da9 | ||
|
|
5d41259a63 | ||
|
|
acb611f034 |
64
.github/workflows/go-licenses.yml
vendored
64
.github/workflows/go-licenses.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: go-licenses
|
||||
|
||||
on:
|
||||
# run action when a change lands in the main branch which updates go.mod or
|
||||
# our license template file. Also allow manual triggering.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- go.mod
|
||||
- .github/licenses.tmpl
|
||||
- .github/workflows/go-licenses.yml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install go-licenses
|
||||
run: |
|
||||
go install github.com/google/go-licenses@v1.2.2-0.20220825154955-5eedde1c6584
|
||||
|
||||
- name: Run go-licenses
|
||||
env:
|
||||
# include all build tags to include platform-specific dependencies
|
||||
GOFLAGS: "-tags=android,cgo,darwin,freebsd,ios,js,linux,openbsd,wasm,windows"
|
||||
run: |
|
||||
[ -d licenses ] || mkdir licenses
|
||||
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply+license-updater@tailscale.com>
|
||||
committer: License Updater <noreply+license-updater@tailscale.com>
|
||||
branch: licenses/cli
|
||||
commit-message: "licenses: update tailscale{,d} licenses"
|
||||
title: "licenses: update tailscale{,d} licenses"
|
||||
body: Triggered by ${{ github.repository }}@${{ github.sha }}
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
team-reviewers: opensource-license-reviewers
|
||||
8
Makefile
8
Makefile
@@ -100,6 +100,14 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
|
||||
|
||||
publishdevnameserver: ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/k8s-nameserver" || (echo "REPO=... must not be tailscale/k8s-nameserver" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-nameserver" && exit 1)
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
|
||||
|
||||
help: ## Show this help
|
||||
@echo "\nSpecify a command. The choices are:\n"
|
||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||
|
||||
@@ -70,6 +70,22 @@ case "$TARGET" in
|
||||
--target="${PLATFORM}" \
|
||||
/usr/local/bin/operator
|
||||
;;
|
||||
k8s-nameserver)
|
||||
DEFAULT_REPOS="tailscale/k8s-nameserver"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="tailscale.com/cmd/k8s-nameserver:/usr/local/bin/k8s-nameserver" \
|
||||
--ldflags=" \
|
||||
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
||||
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
/usr/local/bin/k8s-nameserver
|
||||
;;
|
||||
*)
|
||||
echo "unknown target: $TARGET"
|
||||
exit 1
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
@@ -1418,25 +1418,25 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
|
||||
return &cv, nil
|
||||
}
|
||||
|
||||
// TailFSSetFileServerAddr instructs TailFS to use the server at addr to access
|
||||
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
|
||||
// the filesystem. This is used on platforms like Windows and MacOS to let
|
||||
// TailFS know to use the file server running in the GUI app.
|
||||
func (lc *LocalClient) TailFSSetFileServerAddr(ctx context.Context, addr string) error {
|
||||
// Taildrive know to use the file server running in the GUI app.
|
||||
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareSet adds or updates the given share in the list of shares that
|
||||
// TailFS will serve to remote nodes. If a share with the same name already
|
||||
// DriveShareSet adds or updates the given share in the list of shares that
|
||||
// Taildrive will serve to remote nodes. If a share with the same name already
|
||||
// exists, the existing share is replaced/updated.
|
||||
func (lc *LocalClient) TailFSShareSet(ctx context.Context, share *tailfs.Share) error {
|
||||
func (lc *LocalClient) DriveShareSet(ctx context.Context, share *drive.Share) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareRemove removes the share with the given name from the list of
|
||||
// shares that TailFS will serve to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
|
||||
// DriveShareRemove removes the share with the given name from the list of
|
||||
// shares that Taildrive will serve to remote nodes.
|
||||
func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
"DELETE",
|
||||
@@ -1446,8 +1446,8 @@ func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareRename renames the share from old to new name.
|
||||
func (lc *LocalClient) TailFSShareRename(ctx context.Context, oldName, newName string) error {
|
||||
// DriveShareRename renames the share from old to new name.
|
||||
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
"POST",
|
||||
@@ -1457,14 +1457,14 @@ func (lc *LocalClient) TailFSShareRename(ctx context.Context, oldName, newName s
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareList returns the list of shares that TailFS is currently serving
|
||||
// DriveShareList returns the list of shares that drive is currently serving
|
||||
// to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareList(ctx context.Context) ([]*tailfs.Share, error) {
|
||||
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
|
||||
result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
err = json.Unmarshal(result, &shares)
|
||||
return shares, err
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
// Make sure we don't again accidentally bring in a dependency on
|
||||
// TailFS or its transitive dependencies
|
||||
"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
// drive or its transitive dependencies
|
||||
"tailscale.com/drive/driveimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
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/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
@@ -114,7 +115,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
|
||||
@@ -36,19 +36,21 @@ import (
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
|
||||
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
|
||||
versionFlag = flag.Bool("version", false, "print version and exit")
|
||||
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
|
||||
|
||||
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")
|
||||
@@ -129,6 +131,10 @@ func writeNewConfig() config {
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.Long())
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
|
||||
"tailscale.com/prober"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
versionFlag = flag.Bool("version", false, "print version and exit")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
||||
spread = flag.Bool("spread", true, "whether to spread probing over time")
|
||||
@@ -33,6 +35,10 @@ var (
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.Long())
|
||||
return
|
||||
}
|
||||
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
|
||||
opts := []prober.DERPOpt{
|
||||
|
||||
348
cmd/k8s-nameserver/main.go
Normal file
348
cmd/k8s-nameserver/main.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// k8s-nameserver is a simple nameserver implementation meant to be used with
|
||||
// k8s-operator to allow to resolve magicDNS names associated with tailnet
|
||||
// proxies in cluster.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/miekg/dns"
|
||||
operatorutils "tailscale.com/k8s-operator"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const (
|
||||
// tsNetDomain is the domain that this DNS nameserver has registered a handler for.
|
||||
tsNetDomain = "ts.net"
|
||||
// addr is the the address that the UDP and TCP listeners will listen on.
|
||||
addr = ":1053"
|
||||
|
||||
// The following constants are specific to the nameserver configuration
|
||||
// provided by a mounted Kubernetes Configmap. The Configmap mounted at
|
||||
// /config is the only supported way for configuring this nameserver.
|
||||
defaultDNSConfigDir = "/config"
|
||||
defaultDNSFile = "dns.json"
|
||||
kubeletMountedConfigLn = "..data"
|
||||
)
|
||||
|
||||
// nameserver is a simple nameserver that responds to DNS queries for A records
|
||||
// for ts.net domain names over UDP or TCP. It serves DNS responses from
|
||||
// in-memory IPv4 host records. It is intended to be deployed on Kubernetes with
|
||||
// a ConfigMap mounted at /config that should contain the host records. It
|
||||
// dynamically reconfigures its in-memory mappings as the contents of the
|
||||
// mounted ConfigMap changes.
|
||||
type nameserver struct {
|
||||
// configReader returns the latest desired configuration (host records)
|
||||
// for the nameserver. By default it gets set to a reader that reads
|
||||
// from a Kubernetes ConfigMap mounted at /config, but this can be
|
||||
// overridden in tests.
|
||||
configReader configReaderFunc
|
||||
// configWatcher is a watcher that returns an event when the desired
|
||||
// configuration has changed and the nameserver should update the
|
||||
// in-memory records.
|
||||
configWatcher <-chan string
|
||||
|
||||
mu sync.Mutex // protects following
|
||||
// ip4 are the in-memory hostname -> IP4 mappings that the nameserver
|
||||
// uses to respond to A record queries.
|
||||
ip4 map[dnsname.FQDN][]net.IP
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Ensure that we watch the kube Configmap mounted at /config for
|
||||
// nameserver configuration updates and send events when updates happen.
|
||||
c := ensureWatcherForKubeConfigMap(ctx)
|
||||
|
||||
ns := &nameserver{
|
||||
configReader: configMapConfigReader,
|
||||
configWatcher: c,
|
||||
}
|
||||
|
||||
// Ensure that in-memory records get set up to date now and will get
|
||||
// reset when the configuration changes.
|
||||
ns.runRecordsReconciler(ctx)
|
||||
|
||||
// Register a DNS server handle for ts.net domain names. Not having a
|
||||
// handle registered for any other domain names is how we enforce that
|
||||
// this nameserver can only be used for ts.net domains - querying any
|
||||
// other domain names returns Rcode Refused.
|
||||
dns.HandleFunc(tsNetDomain, ns.handleFunc())
|
||||
|
||||
// Listen for DNS queries over UDP and TCP.
|
||||
udpSig := make(chan os.Signal)
|
||||
tcpSig := make(chan os.Signal)
|
||||
go listenAndServe("udp", addr, udpSig)
|
||||
go listenAndServe("tcp", addr, tcpSig)
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
s := <-sig
|
||||
log.Printf("OS signal (%s) received, shutting down\n", s)
|
||||
cancel() // exit the records reconciler and configmap watcher goroutines
|
||||
udpSig <- s // stop the UDP listener
|
||||
tcpSig <- s // stop the TCP listener
|
||||
}
|
||||
|
||||
// handleFunc is a DNS query handler that can respond to A record queries from
|
||||
// the nameserver's in-memory records.
|
||||
// - If an A record query is received and the
|
||||
// nameserver's in-memory records contain records for the queried domain name,
|
||||
// return a success response.
|
||||
// - If an A record query is received, but the
|
||||
// nameserver's in-memory records do not contain records for the queried domain name,
|
||||
// return NXDOMAIN.
|
||||
// - If an A record query is received, but the queried domain name is not valid, return Format Error.
|
||||
// - If a query is received for any other record type than A, return Not Implemented.
|
||||
func (n *nameserver) handleFunc() func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
h := func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
defer func() {
|
||||
w.WriteMsg(m)
|
||||
}()
|
||||
if len(r.Question) < 1 {
|
||||
log.Print("[unexpected] nameserver received a request with no questions\n")
|
||||
m = r.SetRcodeFormatError(r)
|
||||
return
|
||||
}
|
||||
// TODO (irbekrm): maybe set message compression
|
||||
switch r.Question[0].Qtype {
|
||||
case dns.TypeA:
|
||||
q := r.Question[0].Name
|
||||
fqdn, err := dnsname.ToFQDN(q)
|
||||
if err != nil {
|
||||
m = r.SetRcodeFormatError(r)
|
||||
return
|
||||
}
|
||||
// The only supported use of this nameserver is as a
|
||||
// single source of truth for MagicDNS names by
|
||||
// non-tailnet Kubernetes workloads.
|
||||
m.Authoritative = true
|
||||
m.RecursionAvailable = false
|
||||
|
||||
ips := n.lookupIP4(fqdn)
|
||||
if ips == nil || len(ips) == 0 {
|
||||
// As we are the authoritative nameserver for MagicDNS
|
||||
// names, if we do not have a record for this MagicDNS
|
||||
// name, it does not exist.
|
||||
m = m.SetRcode(r, dns.RcodeNameError)
|
||||
return
|
||||
}
|
||||
// TODO (irbekrm): what TTL?
|
||||
for _, ip := range ips {
|
||||
rr := &dns.A{Hdr: dns.RR_Header{Name: q, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}, A: ip}
|
||||
m.SetRcode(r, dns.RcodeSuccess)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
// TODO (irbekrm): implement IPv6 support
|
||||
fallthrough
|
||||
default:
|
||||
log.Printf("[unexpected] nameserver received a query for an unsupported record type: %s\n", r.Question[0].String())
|
||||
m.SetRcode(r, dns.RcodeNotImplemented)
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// runRecordsReconciler ensures that nameserver's in-memory records are
|
||||
// reset when the provided configuration changes.
|
||||
func (n *nameserver) runRecordsReconciler(ctx context.Context) {
|
||||
log.Print("updating nameserver's records from the provided configuration...\n")
|
||||
if err := n.resetRecords(); err != nil { // ensure records are up to date before the nameserver starts
|
||||
log.Fatalf("error setting nameserver's records: %v\n", err)
|
||||
}
|
||||
log.Print("nameserver's records were updated\n")
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("context cancelled, exiting records reconciler\n")
|
||||
return
|
||||
case <-n.configWatcher:
|
||||
log.Print("configuration update detected, resetting records\n")
|
||||
if err := n.resetRecords(); err != nil {
|
||||
// TODO (irbekrm): this runs in a
|
||||
// container that will be thrown away,
|
||||
// so this should be ok. But maybe still
|
||||
// need to ensure that the DNS server
|
||||
// terminates connections more
|
||||
// gracefully.
|
||||
log.Fatalf("error resetting records: %v\n", err)
|
||||
}
|
||||
log.Print("nameserver records were reset\n")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// resetRecords sets the in-memory DNS records of this nameserver from the
|
||||
// provided configuration. It does not check for the diff, so the caller is
|
||||
// expected to ensure that this is only called when reset is needed.
|
||||
func (n *nameserver) resetRecords() error {
|
||||
dnsCfgBytes, err := n.configReader()
|
||||
if err != nil {
|
||||
log.Printf("error reading nameserver's configuration: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if dnsCfgBytes == nil || len(dnsCfgBytes) < 1 {
|
||||
log.Print("nameserver's configuration is empty, any in-memory records will be unset\n")
|
||||
n.mu.Lock()
|
||||
n.ip4 = make(map[dnsname.FQDN][]net.IP)
|
||||
n.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
dnsCfg := &operatorutils.Records{}
|
||||
err = json.Unmarshal(dnsCfgBytes, dnsCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshalling nameserver configuration: %v\n", err)
|
||||
}
|
||||
|
||||
if dnsCfg.Version != operatorutils.Alpha1Version {
|
||||
return fmt.Errorf("unsupported configuration version %s, supported versions are %s\n", dnsCfg.Version, operatorutils.Alpha1Version)
|
||||
}
|
||||
|
||||
ip4 := make(map[dnsname.FQDN][]net.IP)
|
||||
defer func() {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
n.ip4 = ip4
|
||||
}()
|
||||
|
||||
if dnsCfg.IP4 == nil || len(dnsCfg.IP4) == 0 {
|
||||
log.Print("nameserver's configuration contains no records, any in-memory records will be unset\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
for fqdn, ips := range dnsCfg.IP4 {
|
||||
fqdn, err := dnsname.ToFQDN(fqdn)
|
||||
if err != nil {
|
||||
log.Printf("invalid nameserver's configuration: %s is not a valid FQDN: %v; skipping this record\n", fqdn, err)
|
||||
continue // one invalid hostname should not break the whole nameserver
|
||||
}
|
||||
for _, ipS := range ips {
|
||||
ip := net.ParseIP(ipS).To4()
|
||||
if ip == nil { // To4 returns nil if IP is not a IPv4 address
|
||||
log.Printf("invalid nameserver's configuration: %v does not appear to be an IPv4 address; skipping this record\n", ipS)
|
||||
continue // one invalid IP address should not break the whole nameserver
|
||||
}
|
||||
ip4[fqdn] = []net.IP{ip}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listenAndServe starts a DNS server for the provided network and address.
|
||||
func listenAndServe(net, addr string, shutdown chan os.Signal) {
|
||||
s := &dns.Server{Addr: addr, Net: net}
|
||||
go func() {
|
||||
<-shutdown
|
||||
log.Printf("shutting down server for %s\n", net)
|
||||
s.Shutdown()
|
||||
}()
|
||||
log.Printf("listening for %s queries on %s\n", net, addr)
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Fatalf("error running %s server: %v\n", net, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureWatcherForKubeConfigMap sets up a new file watcher for the ConfigMap
|
||||
// that's expected to be mounted at /config. Returns a channel that receives an
|
||||
// event every time the contents get updated.
|
||||
func ensureWatcherForKubeConfigMap(ctx context.Context) chan string {
|
||||
c := make(chan string)
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatalf("error creating a new watcher for the mounted ConfigMap: %v\n", err)
|
||||
}
|
||||
// kubelet mounts configmap to a Pod using a series of symlinks, one of
|
||||
// which is <mount-dir>/..data that Kubernetes recommends consumers to
|
||||
// use if they need to monitor changes
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.28.1/pkg/volume/util/atomic_writer.go#L39-L61
|
||||
toWatch := filepath.Join(defaultDNSConfigDir, kubeletMountedConfigLn)
|
||||
go func() {
|
||||
defer watcher.Close()
|
||||
log.Printf("starting file watch for %s\n", defaultDNSConfigDir)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Print("context cancelled, exiting ConfigMap watcher\n")
|
||||
return
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
log.Fatal("watcher finished; exiting")
|
||||
}
|
||||
if event.Name == toWatch {
|
||||
msg := fmt.Sprintf("ConfigMap update received: %s\n", event)
|
||||
log.Print(msg)
|
||||
c <- msg
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
// TODO (irbekrm): this runs in a
|
||||
// container that will be thrown away,
|
||||
// so this should be ok. But maybe still
|
||||
// need to ensure that the DNS server
|
||||
// terminates connections more
|
||||
// gracefully.
|
||||
log.Fatalf("[unexpected] configuration watcher error: errors watcher finished: %v\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
// TODO (irbekrm): this runs in a
|
||||
// container that will be thrown away,
|
||||
// so this should be ok. But maybe still
|
||||
// need to ensure that the DNS server
|
||||
// terminates connections more
|
||||
// gracefully.
|
||||
log.Fatalf("[unexpected] error watching configuration: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err = watcher.Add(defaultDNSConfigDir); err != nil {
|
||||
log.Fatalf("failed setting up a watcher for the mounted ConfigMap: %v\n", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// configReaderFunc is a function that returns the desired nameserver configuration.
|
||||
type configReaderFunc func() ([]byte, error)
|
||||
|
||||
// configMapConfigReader reads the desired nameserver configuration from a
|
||||
// dns.json file in a ConfigMap mounted at /config.
|
||||
var configMapConfigReader configReaderFunc = func() ([]byte, error) {
|
||||
if contents, err := os.ReadFile(filepath.Join(defaultDNSConfigDir, defaultDNSFile)); err == nil {
|
||||
return contents, nil
|
||||
} else if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// lookupIP4 returns any IPv4 addresses for the given FQDN from nameserver's
|
||||
// in-memory records.
|
||||
func (n *nameserver) lookupIP4(fqdn dnsname.FQDN) []net.IP {
|
||||
if n.ip4 == nil {
|
||||
return nil
|
||||
}
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
f := n.ip4[fqdn]
|
||||
return f
|
||||
}
|
||||
227
cmd/k8s-nameserver/main_test.go
Normal file
227
cmd/k8s-nameserver/main_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/miekg/dns"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func TestNameserver(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ip4 map[dnsname.FQDN][]net.IP
|
||||
query *dns.Msg
|
||||
wantResp *dns.Msg
|
||||
}{
|
||||
{
|
||||
name: "A record query, record exists",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1, RecursionDesired: true},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.A{Hdr: dns.RR_Header{
|
||||
Name: "foo.bar.com", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0},
|
||||
A: net.IP{1, 2, 3, 4}}},
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
RecursionAvailable: false,
|
||||
RecursionDesired: true,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Authoritative: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "A record query, record does not exist",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeNameError,
|
||||
RecursionAvailable: false,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Authoritative: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "A record query, but the name is not a valid FQDN",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo..bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo..bar.com", Qtype: dns.TypeA}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeFormatError,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "AAAA record query",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeNotImplemented,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "AAAA record query",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeNotImplemented,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "CNAME record query",
|
||||
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
|
||||
query: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeCNAME}},
|
||||
MsgHdr: dns.MsgHdr{Id: 1},
|
||||
},
|
||||
wantResp: &dns.Msg{
|
||||
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeCNAME}},
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: 1,
|
||||
Rcode: dns.RcodeNotImplemented,
|
||||
Response: true,
|
||||
Opcode: dns.OpcodeQuery,
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ns := &nameserver{
|
||||
ip4: tt.ip4,
|
||||
}
|
||||
handler := ns.handleFunc()
|
||||
fakeRespW := &fakeResponseWriter{}
|
||||
handler(fakeRespW, tt.query)
|
||||
if diff := cmp.Diff(*fakeRespW.msg, *tt.wantResp); diff != "" {
|
||||
t.Fatalf("unexpected response (-got +want): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetRecords(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config []byte
|
||||
hasIp4 map[dnsname.FQDN][]net.IP
|
||||
wantsIp4 map[dnsname.FQDN][]net.IP
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "previously empty nameserver.ip4 gets set",
|
||||
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
||||
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
|
||||
},
|
||||
{
|
||||
name: "nameserver.ip4 gets reset",
|
||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
||||
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
||||
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
|
||||
},
|
||||
{
|
||||
name: "configuration with incompatible version",
|
||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
||||
config: []byte(`{"version": "v1beta1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
|
||||
wantsIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "nameserver.ip4 gets reset to empty config when no configuration is provided",
|
||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
||||
wantsIp4: make(map[dnsname.FQDN][]net.IP),
|
||||
},
|
||||
{
|
||||
name: "nameserver.ip4 gets reset to empty config when the provided configuration is empty",
|
||||
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
|
||||
config: []byte(`{"version": "v1alpha1", "ip4": {}}`),
|
||||
wantsIp4: make(map[dnsname.FQDN][]net.IP),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ns := &nameserver{
|
||||
ip4: tt.hasIp4,
|
||||
configReader: func() ([]byte, error) { return tt.config, nil },
|
||||
}
|
||||
if err := ns.resetRecords(); err == nil == tt.wantsErr {
|
||||
t.Errorf("resetRecords() returned err: %v, wantsErr: %v", err, tt.wantsErr)
|
||||
}
|
||||
if diff := cmp.Diff(ns.ip4, tt.wantsIp4); diff != "" {
|
||||
t.Fatalf("unexpected nameserver.ip4 contents (-got +want): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeResponseWriter is a faked out dns.ResponseWriter that can be used in
|
||||
// tests that need to read the response message that was written.
|
||||
type fakeResponseWriter struct {
|
||||
msg *dns.Msg
|
||||
}
|
||||
|
||||
var _ dns.ResponseWriter = &fakeResponseWriter{}
|
||||
|
||||
func (fr *fakeResponseWriter) WriteMsg(msg *dns.Msg) error {
|
||||
fr.msg = msg
|
||||
return nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) Write([]byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) TsigStatus() error {
|
||||
return nil
|
||||
}
|
||||
func (fr *fakeResponseWriter) TsigTimersOnly(bool) {}
|
||||
func (fr *fakeResponseWriter) Hijack() {}
|
||||
@@ -24,6 +24,9 @@ rules:
|
||||
- apiGroups: ["tailscale.com"]
|
||||
resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
|
||||
verbs: ["get", "list", "watch", "update"]
|
||||
- apiGroups: ["tailscale.com"]
|
||||
resources: ["dnsconfigs", "dnsconfigs/status"]
|
||||
verbs: ["get", "list", "watch", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
@@ -45,10 +48,10 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resources: ["secrets", "serviceaccounts", "configmaps"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["statefulsets"]
|
||||
resources: ["statefulsets", "deployments"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
96
cmd/k8s-operator/deploy/crds/tailscale.com_dnsconfigs.yaml
Normal file
96
cmd/k8s-operator/deploy/crds/tailscale.com_dnsconfigs.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.13.0
|
||||
name: dnsconfigs.tailscale.com
|
||||
spec:
|
||||
group: tailscale.com
|
||||
names:
|
||||
kind: DNSConfig
|
||||
listKind: DNSConfigList
|
||||
plural: dnsconfigs
|
||||
shortNames:
|
||||
- dc
|
||||
singular: dnsconfig
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Service IP address of the nameserver
|
||||
jsonPath: .status.nameserverStatus.ip
|
||||
name: NameserverIP
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- nameserver
|
||||
properties:
|
||||
nameserver:
|
||||
type: object
|
||||
properties:
|
||||
image:
|
||||
type: object
|
||||
properties:
|
||||
repo:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
description: ConnectorCondition contains condition information for a Connector.
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
||||
type: string
|
||||
format: date-time
|
||||
message:
|
||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
||||
type: integer
|
||||
format: int64
|
||||
reason:
|
||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
||||
type: string
|
||||
type:
|
||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
||||
type: string
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
nameserverStatus:
|
||||
type: object
|
||||
properties:
|
||||
ip:
|
||||
type: string
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
4
cmd/k8s-operator/deploy/manifests/nameserver/cm.yaml
Normal file
4
cmd/k8s-operator/deploy/manifests/nameserver/cm.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dnsconfig
|
||||
37
cmd/k8s-operator/deploy/manifests/nameserver/deploy.yaml
Normal file
37
cmd/k8s-operator/deploy/manifests/nameserver/deploy.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nameserver
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nameserver
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nameserver
|
||||
spec:
|
||||
containers:
|
||||
- imagePullPolicy: IfNotPresent
|
||||
name: nameserver
|
||||
ports:
|
||||
- name: tcp
|
||||
protocol: TCP
|
||||
containerPort: 1053
|
||||
- name: udp
|
||||
protocol: UDP
|
||||
containerPort: 1053
|
||||
volumeMounts:
|
||||
- name: dnsconfig
|
||||
mountPath: /config
|
||||
restartPolicy: Always
|
||||
serviceAccount: nameserver
|
||||
serviceAccountName: nameserver
|
||||
volumes:
|
||||
- name: dnsconfig
|
||||
configMap:
|
||||
name: dnsconfig
|
||||
6
cmd/k8s-operator/deploy/manifests/nameserver/sa.yaml
Normal file
6
cmd/k8s-operator/deploy/manifests/nameserver/sa.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: nameserver
|
||||
imagePullSecrets:
|
||||
- name: foo
|
||||
16
cmd/k8s-operator/deploy/manifests/nameserver/svc.yaml
Normal file
16
cmd/k8s-operator/deploy/manifests/nameserver/svc.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nameserver
|
||||
spec:
|
||||
selector:
|
||||
app: nameserver
|
||||
ports:
|
||||
- name: udp
|
||||
targetPort: 1053
|
||||
port: 53
|
||||
protocol: UDP
|
||||
- name: tcp
|
||||
targetPort: 1053
|
||||
port: 53
|
||||
protocol: TCP
|
||||
@@ -158,6 +158,103 @@ spec:
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.13.0
|
||||
name: dnsconfigs.tailscale.com
|
||||
spec:
|
||||
group: tailscale.com
|
||||
names:
|
||||
kind: DNSConfig
|
||||
listKind: DNSConfigList
|
||||
plural: dnsconfigs
|
||||
shortNames:
|
||||
- dc
|
||||
singular: dnsconfig
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Service IP address of the nameserver
|
||||
jsonPath: .status.nameserverStatus.ip
|
||||
name: NameserverIP
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
nameserver:
|
||||
properties:
|
||||
image:
|
||||
properties:
|
||||
repo:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- nameserver
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: ConnectorCondition contains condition information for a Connector.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
||||
format: int64
|
||||
type: integer
|
||||
reason:
|
||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
||||
type: string
|
||||
type:
|
||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
nameserverStatus:
|
||||
properties:
|
||||
ip:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.13.0
|
||||
@@ -691,6 +788,16 @@ rules:
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- apiGroups:
|
||||
- tailscale.com
|
||||
resources:
|
||||
- dnsconfigs
|
||||
- dnsconfigs/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
@@ -715,12 +822,15 @@ rules:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- serviceaccounts
|
||||
- configmaps
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets
|
||||
- deployments
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
|
||||
@@ -22,9 +22,11 @@ const (
|
||||
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
|
||||
connectorCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
|
||||
proxyClassCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxyclasses.yaml"
|
||||
dnsConfigCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_dnsconfigs.yaml"
|
||||
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
|
||||
connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml"
|
||||
proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml"
|
||||
dnsConfigCRDHelmTemplatePath = helmTemplatesPath + "/dnsconfig.yaml"
|
||||
|
||||
helmConditionalStart = "{{ if .Values.installCRDs -}}\n"
|
||||
helmConditionalEnd = "{{- end -}}"
|
||||
@@ -108,7 +110,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// generate places tailscale.com CRDs (currently Connector and ProxyClass) into
|
||||
// generate places tailscale.com CRDs (currently Connector, ProxyClass and DNSConfig) into
|
||||
// the Helm chart templates behind .Values.installCRDs=true condition (true by
|
||||
// default).
|
||||
func generate(baseDir string) error {
|
||||
@@ -140,6 +142,9 @@ func generate(baseDir string) error {
|
||||
if err := addCRDToHelm(proxyClassCRDPath, proxyClassCRDHelmTemplatePath); err != nil {
|
||||
return fmt.Errorf("error adding ProxyClass CRD to Helm templates: %w", err)
|
||||
}
|
||||
if err := addCRDToHelm(dnsConfigCRDPath, dnsConfigCRDHelmTemplatePath); err != nil {
|
||||
return fmt.Errorf("error adding DNSConfig CRD to Helm templates: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -151,5 +156,8 @@ func cleanup(baseDir string) error {
|
||||
if err := os.Remove(filepath.Join(baseDir, proxyClassCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error cleaning up ProxyClass CRD template: %w", err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(baseDir, dnsConfigCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error cleaning up DNSConfig CRD template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ func Test_generate(t *testing.T) {
|
||||
if !strings.Contains(installContentsWithCRD.String(), "name: proxyclasses.tailscale.com") {
|
||||
t.Errorf("ProxyClass CRD not found in default chart install")
|
||||
}
|
||||
if !strings.Contains(installContentsWithCRD.String(), "name: dnsconfigs.tailscale.com") {
|
||||
t.Errorf("DNSConfig CRD not found in default chart install")
|
||||
}
|
||||
|
||||
// Test that CRDs can be excluded from Helm chart install
|
||||
installContentsWithoutCRD := bytes.NewBuffer([]byte{})
|
||||
@@ -71,4 +74,7 @@ func Test_generate(t *testing.T) {
|
||||
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
|
||||
t.Errorf("ProxyClass CRD found in chart install that should not contain a CRD")
|
||||
}
|
||||
if strings.Contains(installContentsWithoutCRD.String(), "name: dnsconfigs.tailscale.com") {
|
||||
t.Errorf("DNSConfig CRD found in chart install that should not contain a CRD")
|
||||
}
|
||||
}
|
||||
|
||||
278
cmd/k8s-operator/nameserver.go
Normal file
278
cmd/k8s-operator/nameserver.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet and to make Tailscale nodes available to cluster
|
||||
// workloads
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
xslices "golang.org/x/exp/slices"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
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/reconcile"
|
||||
"sigs.k8s.io/yaml"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const (
|
||||
reasonNameserverCreationFailed = "NameserverCreationFailed"
|
||||
reasonMultipleDNSConfigsPresent = "MultipleDNSConfigsPresent"
|
||||
|
||||
reasonNameserverCreated = "NameserverCreated"
|
||||
|
||||
messageNameserverCreationFailed = "Failed creating nameserver resources: %v"
|
||||
messageMultipleDNSConfigsPresent = "Multiple DNSConfig resources found in cluster. Please ensure no more than one is present."
|
||||
)
|
||||
|
||||
// NameserverReconciler knows how to create nameserver resources in cluster in
|
||||
// response to users applying DNSConfig.
|
||||
type NameserverReconciler struct {
|
||||
client.Client
|
||||
logger *zap.SugaredLogger
|
||||
recorder record.EventRecorder
|
||||
clock tstime.Clock
|
||||
tsNamespace string
|
||||
|
||||
mu sync.Mutex // protects following
|
||||
managedNameservers set.Slice[types.UID] // one or none
|
||||
}
|
||||
|
||||
var (
|
||||
gaugeNameserverResources = clientmetric.NewGauge("k8s_nameserver_resources")
|
||||
)
|
||||
|
||||
func (a *NameserverReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
|
||||
logger := a.logger.With("dnsConfig", req.Name)
|
||||
logger.Debugf("starting reconcile")
|
||||
defer logger.Debugf("reconcile finished")
|
||||
|
||||
var dnsCfg tsapi.DNSConfig
|
||||
err = a.Get(ctx, req.NamespacedName, &dnsCfg)
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Request object not found, could have been deleted after reconcile request.
|
||||
logger.Debugf("dnsconfig not found, assuming it was deleted")
|
||||
return reconcile.Result{}, nil
|
||||
} else if err != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("failed to get dnsconfig: %w", err)
|
||||
}
|
||||
if !dnsCfg.DeletionTimestamp.IsZero() {
|
||||
ix := xslices.Index(dnsCfg.Finalizers, FinalizerName)
|
||||
if ix < 0 {
|
||||
logger.Debugf("no finalizer, nothing to do")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
logger.Info("Cleaning up DNSConfig resources")
|
||||
if err := a.maybeCleanup(ctx, &dnsCfg, logger); err != nil {
|
||||
logger.Errorf("error cleaning up reconciler resource: %v", err)
|
||||
return res, err
|
||||
}
|
||||
dnsCfg.Finalizers = append(dnsCfg.Finalizers[:ix], dnsCfg.Finalizers[ix+1:]...)
|
||||
if err := a.Update(ctx, &dnsCfg); err != nil {
|
||||
logger.Errorf("error removing finalizer: %v", err)
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
logger.Infof("Nameserver resources cleaned up")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
oldCnStatus := dnsCfg.Status.DeepCopy()
|
||||
setStatus := func(dnsCfg *tsapi.DNSConfig, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
|
||||
tsoperator.SetDNSConfigCondition(dnsCfg, tsapi.NameserverReady, status, reason, message, dnsCfg.Generation, a.clock, logger)
|
||||
if !apiequality.Semantic.DeepEqual(oldCnStatus, dnsCfg.Status) {
|
||||
// An error encountered here should get returned by the Reconcile function.
|
||||
if updateErr := a.Client.Status().Update(ctx, dnsCfg); updateErr != nil {
|
||||
err = errors.Wrap(err, updateErr.Error())
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
var dnsCfgs tsapi.DNSConfigList
|
||||
if err := a.List(ctx, &dnsCfgs); err != nil {
|
||||
return res, fmt.Errorf("error listing DNSConfigs: %w", err)
|
||||
}
|
||||
if len(dnsCfgs.Items) > 1 { // enforce DNSConfig to be a singleton
|
||||
msg := "invalid cluster configuration: more than one tailscale.com/dnsconfigs found. Please ensure that no more than one is created."
|
||||
logger.Error(msg)
|
||||
a.recorder.Event(&dnsCfg, corev1.EventTypeWarning, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
|
||||
setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionFalse, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
|
||||
}
|
||||
|
||||
if !slices.Contains(dnsCfg.Finalizers, FinalizerName) {
|
||||
logger.Infof("ensuring nameserver resources")
|
||||
dnsCfg.Finalizers = append(dnsCfg.Finalizers, FinalizerName)
|
||||
if err := a.Update(ctx, &dnsCfg); err != nil {
|
||||
msg := fmt.Sprintf(messageNameserverCreationFailed, err)
|
||||
logger.Error(msg)
|
||||
return setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionFalse, reasonNameserverCreationFailed, msg)
|
||||
}
|
||||
}
|
||||
if err := a.maybeProvision(ctx, &dnsCfg, logger); err != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("error provisioning nameserver resources: %w", err)
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.managedNameservers.Add(dnsCfg.UID)
|
||||
a.mu.Unlock()
|
||||
gaugeNameserverResources.Set(int64(a.managedNameservers.Len()))
|
||||
|
||||
svc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: a.tsNamespace},
|
||||
}
|
||||
if err := a.Client.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
|
||||
return res, fmt.Errorf("error getting Service: %w", err)
|
||||
}
|
||||
if ip := svc.Spec.ClusterIP; ip != "" && ip != "None" {
|
||||
dnsCfg.Status.NameserverStatus = &tsapi.NameserverStatus{
|
||||
IP: ip,
|
||||
}
|
||||
return setStatus(&dnsCfg, tsapi.NameserverReady, metav1.ConditionTrue, reasonNameserverCreated, reasonNameserverCreated)
|
||||
}
|
||||
logger.Info("nameserver Service does not have an IP address allocated, waiting...")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func nameserverResourceLabels(name, namespace string) map[string]string {
|
||||
labels := childResourceLabels(name, namespace, "nameserver")
|
||||
labels["app.kubernetes.io/name"] = "tailscale"
|
||||
labels["app.kubernetes.io/component"] = "nameserver"
|
||||
return labels
|
||||
}
|
||||
|
||||
func (a *NameserverReconciler) maybeProvision(ctx context.Context, tsDNSCfg *tsapi.DNSConfig, logger *zap.SugaredLogger) error {
|
||||
labels := nameserverResourceLabels(tsDNSCfg.Name, a.tsNamespace)
|
||||
dCfg := &deployConfig{
|
||||
ownerRefs: []metav1.OwnerReference{*metav1.NewControllerRef(tsDNSCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))},
|
||||
namespace: a.tsNamespace,
|
||||
labels: labels,
|
||||
}
|
||||
if tsDNSCfg.Spec.Nameserver.Image.Repo != "" {
|
||||
dCfg.imageRepo = tsDNSCfg.Spec.Nameserver.Image.Repo
|
||||
}
|
||||
if tsDNSCfg.Spec.Nameserver.Image.Tag != "" {
|
||||
dCfg.imageTag = tsDNSCfg.Spec.Nameserver.Image.Tag
|
||||
}
|
||||
for _, deployable := range []deployable{saDeployable, deployDeployable, svcDeployable, cmDeployable} {
|
||||
if err := deployable.updateObj(ctx, dCfg, a.Client); err != nil {
|
||||
return fmt.Errorf("error reconciling %s: %w", deployable.kind, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// maybeCleanup removes DNSConfig from being tracked. The cluster resources
|
||||
// created, will be automatically garbage collected as they are owned by the
|
||||
// DNSConfig.
|
||||
func (a *NameserverReconciler) maybeCleanup(ctx context.Context, dnsCfg *tsapi.DNSConfig, logger *zap.SugaredLogger) error {
|
||||
a.mu.Lock()
|
||||
a.managedNameservers.Remove(dnsCfg.UID)
|
||||
a.mu.Unlock()
|
||||
gaugeNameserverResources.Set(int64(a.managedNameservers.Len()))
|
||||
return nil
|
||||
}
|
||||
|
||||
type deployable struct {
|
||||
kind string
|
||||
updateObj func(context.Context, *deployConfig, client.Client) error
|
||||
}
|
||||
|
||||
type deployConfig struct {
|
||||
imageRepo string
|
||||
imageTag string
|
||||
labels map[string]string
|
||||
ownerRefs []metav1.OwnerReference
|
||||
namespace string
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed deploy/manifests/nameserver/cm.yaml
|
||||
cmYaml []byte
|
||||
//go:embed deploy/manifests/nameserver/deploy.yaml
|
||||
deployYaml []byte
|
||||
//go:embed deploy/manifests/nameserver/sa.yaml
|
||||
saYaml []byte
|
||||
//go:embed deploy/manifests/nameserver/svc.yaml
|
||||
svcYaml []byte
|
||||
|
||||
deployDeployable = deployable{
|
||||
kind: "Deployment",
|
||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
||||
d := new(appsv1.Deployment)
|
||||
if err := yaml.Unmarshal(deployYaml, &d); err != nil {
|
||||
return fmt.Errorf("error unmarshalling Deployment yaml: %w", err)
|
||||
}
|
||||
d.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("%s:%s", cfg.imageRepo, cfg.imageTag)
|
||||
d.ObjectMeta.Namespace = cfg.namespace
|
||||
d.ObjectMeta.Labels = cfg.labels
|
||||
d.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
||||
updateF := func(oldD *appsv1.Deployment) {
|
||||
oldD.Spec = d.Spec
|
||||
}
|
||||
_, err := createOrUpdate[appsv1.Deployment](ctx, kubeClient, cfg.namespace, d, updateF)
|
||||
return err
|
||||
},
|
||||
}
|
||||
saDeployable = deployable{
|
||||
kind: "ServiceAccount",
|
||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
||||
sa := new(corev1.ServiceAccount)
|
||||
if err := yaml.Unmarshal(saYaml, &sa); err != nil {
|
||||
return fmt.Errorf("error unmarshalling ServiceAccount yaml: %w", err)
|
||||
}
|
||||
sa.ObjectMeta.Labels = cfg.labels
|
||||
sa.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
||||
sa.ObjectMeta.Namespace = cfg.namespace
|
||||
_, err := createOrUpdate(ctx, kubeClient, cfg.namespace, sa, func(*corev1.ServiceAccount) {})
|
||||
return err
|
||||
},
|
||||
}
|
||||
svcDeployable = deployable{
|
||||
kind: "Service",
|
||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
||||
svc := new(corev1.Service)
|
||||
if err := yaml.Unmarshal(svcYaml, &svc); err != nil {
|
||||
return fmt.Errorf("error unmarshalling Service yaml: %w", err)
|
||||
}
|
||||
svc.ObjectMeta.Labels = cfg.labels
|
||||
svc.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
||||
svc.ObjectMeta.Namespace = cfg.namespace
|
||||
_, err := createOrUpdate[corev1.Service](ctx, kubeClient, cfg.namespace, svc, func(*corev1.Service) {})
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmDeployable = deployable{
|
||||
kind: "ConfigMap",
|
||||
updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
|
||||
cm := new(corev1.ConfigMap)
|
||||
if err := yaml.Unmarshal(cmYaml, &cm); err != nil {
|
||||
return fmt.Errorf("error unmarshalling ConfigMap yaml: %w", err)
|
||||
}
|
||||
cm.ObjectMeta.Labels = cfg.labels
|
||||
cm.ObjectMeta.OwnerReferences = cfg.ownerRefs
|
||||
cm.ObjectMeta.Namespace = cfg.namespace
|
||||
_, err := createOrUpdate[corev1.ConfigMap](ctx, kubeClient, cfg.namespace, cm, func(cm *corev1.ConfigMap) {})
|
||||
return err
|
||||
},
|
||||
}
|
||||
)
|
||||
118
cmd/k8s-operator/nameserver_test.go
Normal file
118
cmd/k8s-operator/nameserver_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet and to make Tailscale nodes available to cluster
|
||||
// workloads
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/yaml"
|
||||
operatorutils "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func TestNameserverReconciler(t *testing.T) {
|
||||
dnsCfg := &tsapi.DNSConfig{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "DNSConfig", APIVersion: "tailscale.com/v1alpha1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
Spec: tsapi.DNSConfigSpec{
|
||||
Nameserver: &tsapi.Nameserver{
|
||||
Image: &tsapi.Image{
|
||||
Repo: "test",
|
||||
Tag: "v0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(dnsCfg).
|
||||
WithStatusSubresource(dnsCfg).
|
||||
Build()
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||
nr := &NameserverReconciler{
|
||||
Client: fc,
|
||||
clock: cl,
|
||||
logger: zl.Sugar(),
|
||||
tsNamespace: "tailscale",
|
||||
}
|
||||
expectReconciled(t, nr, "", "test")
|
||||
// Verify that nameserver Deployment has been created and has the expected fields.
|
||||
wantsDeploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: "tailscale"}, TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: appsv1.SchemeGroupVersion.Identifier()}}
|
||||
if err := yaml.Unmarshal(deployYaml, wantsDeploy); err != nil {
|
||||
t.Fatalf("unmarshalling yaml: %v", err)
|
||||
}
|
||||
dnsCfgOwnerRef := metav1.NewControllerRef(dnsCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))
|
||||
wantsDeploy.OwnerReferences = []metav1.OwnerReference{*dnsCfgOwnerRef}
|
||||
wantsDeploy.Spec.Template.Spec.Containers[0].Image = "test:v0.0.1"
|
||||
wantsDeploy.Namespace = "tailscale"
|
||||
labels := nameserverResourceLabels("test", "tailscale")
|
||||
wantsDeploy.ObjectMeta.Labels = labels
|
||||
expectEqual(t, fc, wantsDeploy, nil)
|
||||
|
||||
// Verify that DNSConfig advertizes the nameserver's Service IP address,
|
||||
// has the ready status condition and tailscale finalizer.
|
||||
mustUpdate(t, fc, "tailscale", "nameserver", func(svc *corev1.Service) {
|
||||
svc.Spec.ClusterIP = "1.2.3.4"
|
||||
})
|
||||
expectReconciled(t, nr, "", "test")
|
||||
dnsCfg.Status.NameserverStatus = &tsapi.NameserverStatus{
|
||||
IP: "1.2.3.4",
|
||||
}
|
||||
dnsCfg.Finalizers = []string{FinalizerName}
|
||||
dnsCfg.Status.Conditions = append(dnsCfg.Status.Conditions, tsapi.ConnectorCondition{
|
||||
Type: tsapi.NameserverReady,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonNameserverCreated,
|
||||
Message: reasonNameserverCreated,
|
||||
LastTransitionTime: &metav1.Time{Time: cl.Now().Truncate(time.Second)},
|
||||
})
|
||||
expectEqual(t, fc, dnsCfg, nil)
|
||||
|
||||
// // Verify that nameserver image gets updated to match DNSConfig spec.
|
||||
mustUpdate(t, fc, "", "test", func(dnsCfg *tsapi.DNSConfig) {
|
||||
dnsCfg.Spec.Nameserver.Image.Tag = "v0.0.2"
|
||||
})
|
||||
expectReconciled(t, nr, "", "test")
|
||||
wantsDeploy.Spec.Template.Spec.Containers[0].Image = "test:v0.0.2"
|
||||
expectEqual(t, fc, wantsDeploy, nil)
|
||||
|
||||
// Verify that when another actor sets ConfigMap data, it does not get
|
||||
// overwritten by nameserver reconciler.
|
||||
dnsRecords := &operatorutils.Records{Version: "v1alpha1", IP4: map[string][]string{"foo.ts.net": {"1.2.3.4"}}}
|
||||
bs, err := json.Marshal(dnsRecords)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling ConfigMap contents: %v", err)
|
||||
}
|
||||
mustUpdate(t, fc, "tailscale", "dnsconfig", func(cm *corev1.ConfigMap) {
|
||||
mak.Set(&cm.Data, "dns.json", string(bs))
|
||||
})
|
||||
expectReconciled(t, nr, "", "test")
|
||||
wantCm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "dnsconfig",
|
||||
Namespace: "tailscale", Labels: labels, OwnerReferences: []metav1.OwnerReference{*dnsCfgOwnerRef}},
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
|
||||
Data: map[string]string{"dns.json": string(bs)},
|
||||
}
|
||||
expectEqual(t, fc, wantCm, nil)
|
||||
}
|
||||
@@ -223,8 +223,11 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
// resources that we GET via the controller manager's client.
|
||||
Cache: cache.Options{
|
||||
ByObject: map[client.Object]cache.ByObject{
|
||||
&corev1.Secret{}: nsFilter,
|
||||
&appsv1.StatefulSet{}: nsFilter,
|
||||
&corev1.Secret{}: nsFilter,
|
||||
&corev1.ServiceAccount{}: nsFilter,
|
||||
&corev1.ConfigMap{}: nsFilter,
|
||||
&appsv1.StatefulSet{}: nsFilter,
|
||||
&appsv1.Deployment{}: nsFilter,
|
||||
},
|
||||
},
|
||||
Scheme: tsapi.GlobalScheme,
|
||||
@@ -308,7 +311,28 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatal("could not create connector reconciler: %v", err)
|
||||
startlog.Fatalf("could not create connector reconciler: %v", err)
|
||||
}
|
||||
// TODO (irbekrm): switch to metadata-only watches for resources whose
|
||||
// spec we don't need to inspect to reduce memory consumption
|
||||
// https://github.com/kubernetes-sigs/controller-runtime/issues/1159
|
||||
nameserverFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("nameserver"))
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
For(&tsapi.DNSConfig{}).
|
||||
Watches(&appsv1.Deployment{}, nameserverFilter).
|
||||
Watches(&corev1.ConfigMap{}, nameserverFilter).
|
||||
Watches(&corev1.Service{}, nameserverFilter).
|
||||
Watches(&corev1.ServiceAccount{}, nameserverFilter).
|
||||
Complete(&NameserverReconciler{
|
||||
recorder: eventRecorder,
|
||||
tsNamespace: tsNamespace,
|
||||
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("nameserver-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create nameserver reconciler: %v", err)
|
||||
}
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
For(&tsapi.ProxyClass{}).
|
||||
|
||||
@@ -1194,7 +1194,6 @@ func TestTailscaledConfigfileHash(t *testing.T) {
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
|
||||
}
|
||||
|
||||
func Test_isMagicDNSName(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
@@ -829,7 +829,7 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
// Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
|
||||
// a CLI flag for this. The Pref is used by c2n.
|
||||
continue
|
||||
case "TailFSShares":
|
||||
case "DriveShares":
|
||||
// Handled by the tailscale share subcommand, we don't want a CLI
|
||||
// flag for this.
|
||||
continue
|
||||
|
||||
@@ -497,9 +497,9 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
}
|
||||
h.Text = text
|
||||
case filepath.IsAbs(target):
|
||||
if version.IsSandboxedMacOS() {
|
||||
// don't allow path serving for now on macOS (2022-11-15)
|
||||
return errors.New("path serving is not supported if sandboxed on macOS")
|
||||
if version.IsMacAppStore() || version.IsMacSys() {
|
||||
// The Tailscale network extension cannot serve arbitrary paths on macOS due to sandbox restrictions (2024-03-26)
|
||||
return errors.New("Path serving is not supported on macOS due to sandbox restrictions. To use Tailscale Serve on macOS, switch to the open-source tailscaled distribution. See https://tailscale.com/kb/1065/macos-variants for more information.")
|
||||
}
|
||||
|
||||
target = filepath.Clean(target)
|
||||
|
||||
@@ -38,24 +38,25 @@ Only settings explicitly mentioned will be set. There are no default values.`,
|
||||
}
|
||||
|
||||
type setArgsT struct {
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
runWebClient bool
|
||||
hostname string
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
advertiseConnector bool
|
||||
opUser string
|
||||
acceptedRisks string
|
||||
profileName string
|
||||
forceDaemon bool
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
exitDestinationFlowLogs bool
|
||||
shieldsUp bool
|
||||
runSSH bool
|
||||
runWebClient bool
|
||||
hostname string
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
advertiseConnector bool
|
||||
opUser string
|
||||
acceptedRisks string
|
||||
profileName string
|
||||
forceDaemon bool
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
@@ -66,6 +67,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
|
||||
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
setf.BoolVar(&setArgs.exitDestinationFlowLogs, "exit-destination-flow-logs", false, "Enable exit node destination in network flow logs")
|
||||
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
setf.BoolVar(&setArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
|
||||
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
@@ -106,16 +108,17 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
|
||||
maskedPrefs := &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ProfileName: setArgs.profileName,
|
||||
RouteAll: setArgs.acceptRoutes,
|
||||
CorpDNS: setArgs.acceptDNS,
|
||||
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
|
||||
ShieldsUp: setArgs.shieldsUp,
|
||||
RunSSH: setArgs.runSSH,
|
||||
RunWebClient: setArgs.runWebClient,
|
||||
Hostname: setArgs.hostname,
|
||||
OperatorUser: setArgs.opUser,
|
||||
ForceDaemon: setArgs.forceDaemon,
|
||||
ProfileName: setArgs.profileName,
|
||||
RouteAll: setArgs.acceptRoutes,
|
||||
CorpDNS: setArgs.acceptDNS,
|
||||
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
|
||||
ExitDestinationFlowLogs: setArgs.exitDestinationFlowLogs,
|
||||
ShieldsUp: setArgs.shieldsUp,
|
||||
RunSSH: setArgs.runSSH,
|
||||
RunWebClient: setArgs.runWebClient,
|
||||
Hostname: setArgs.hostname,
|
||||
OperatorUser: setArgs.opUser,
|
||||
ForceDaemon: setArgs.forceDaemon,
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: setArgs.updateCheck,
|
||||
Apply: opt.NewBool(setArgs.updateApply),
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/drive"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -69,7 +69,7 @@ func runShareSet(ctx context.Context, args []string) error {
|
||||
|
||||
name, path := args[0], args[1]
|
||||
|
||||
err := localClient.TailFSShareSet(ctx, &tailfs.Share{
|
||||
err := localClient.DriveShareSet(ctx, &drive.Share{
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
@@ -86,7 +86,7 @@ func runShareRemove(ctx context.Context, args []string) error {
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
err := localClient.TailFSShareRemove(ctx, name)
|
||||
err := localClient.DriveShareRemove(ctx, name)
|
||||
if err == nil {
|
||||
fmt.Printf("Removed share %q\n", name)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func runShareRename(ctx context.Context, args []string) error {
|
||||
oldName := args[0]
|
||||
newName := args[1]
|
||||
|
||||
err := localClient.TailFSShareRename(ctx, oldName, newName)
|
||||
err := localClient.DriveShareRename(ctx, oldName, newName)
|
||||
if err == nil {
|
||||
fmt.Printf("Renamed share %q to %q\n", oldName, newName)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func runShareList(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("usage: tailscale %v", shareListUsage)
|
||||
}
|
||||
|
||||
shares, err := localClient.TailFSShareList(ctx)
|
||||
shares, err := localClient.DriveShareList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func runShareList(ctx context.Context, args []string) error {
|
||||
|
||||
func buildShareLongHelp() string {
|
||||
longHelpAs := ""
|
||||
if tailfs.AllowShareAs() {
|
||||
if drive.AllowShareAs() {
|
||||
longHelpAs = shareLongHelpAs
|
||||
}
|
||||
return fmt.Sprintf(shareLongHelpBase, longHelpAs)
|
||||
|
||||
@@ -723,6 +723,7 @@ func init() {
|
||||
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
|
||||
addPrefFlagMapping("advertise-connector", "AppConnector")
|
||||
addPrefFlagMapping("posture-checking", "PostureChecking")
|
||||
addPrefFlagMapping("exit-destination-flow-logs", "ExitDestinationFlowLogs")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@@ -951,6 +952,8 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
set(exitNodeIPStr())
|
||||
case "exit-node-allow-lan-access":
|
||||
set(prefs.ExitNodeAllowLANAccess)
|
||||
case "exit-destination-flow-logs":
|
||||
set(prefs.ExitDestinationFlowLogs)
|
||||
case "advertise-tags":
|
||||
set(strings.Join(prefs.AdvertiseTags, ","))
|
||||
case "hostname":
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortHelp: "[BETA] Update Tailscale to the latest/different version",
|
||||
ShortHelp: "Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("update")
|
||||
|
||||
@@ -84,6 +84,7 @@ 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/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
@@ -118,7 +119,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
|
||||
@@ -88,7 +88,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
💣 github.com/djherbis/times from tailscale.com/tailfs/tailfsimpl
|
||||
💣 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
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
@@ -102,7 +102,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/clientupdate
|
||||
github.com/google/uuid from tailscale.com/clientupdate+
|
||||
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+
|
||||
@@ -111,7 +111,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/tailfsimpl/compositedav
|
||||
github.com/jellydator/ttlcache/v3 from tailscale.com/drive/driveimpl/compositedav
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
@@ -172,7 +172,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/xnet/webdav from tailscale.com/tailfs/tailfsimpl+
|
||||
github.com/tailscale/xnet/webdav from tailscale.com/drive/driveimpl+
|
||||
github.com/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
@@ -251,6 +251,11 @@ 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/tailscale+
|
||||
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/tailscale+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
@@ -320,11 +325,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/tailfs/tailfsimpl/compositedav from tailscale.com/tailfs/tailfsimpl
|
||||
tailscale.com/tailfs/tailfsimpl/dirfs from tailscale.com/tailfs/tailfsimpl+
|
||||
tailscale.com/tailfs/tailfsimpl/shared from tailscale.com/tailfs/tailfsimpl+
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
@@ -376,6 +376,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osuser from tailscale.com/ipn/localapi+
|
||||
tailscale.com/util/progresstracking from tailscale.com/ipn/localapi
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
@@ -522,7 +523,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand/v2 from tailscale.com/util/rands
|
||||
mime from github.com/tailscale/xnet/webdav+
|
||||
mime/multipart from net/http
|
||||
mime/multipart from net/http+
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
@@ -52,7 +53,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
@@ -145,7 +145,7 @@ var subCommands = map[string]*func([]string) error{
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
"be-child": &beChildFunc,
|
||||
"serve-tailfs": &serveTailFSFunc,
|
||||
"serve-tailfs": &serveDriveFunc,
|
||||
}
|
||||
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
@@ -407,7 +407,7 @@ func run() (err error) {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(logf))
|
||||
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||
}
|
||||
@@ -645,12 +645,12 @@ var tstunNew = tstun.New
|
||||
|
||||
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
|
||||
}
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
@@ -753,7 +753,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
tfs, _ := sys.TailFSForLocal.GetOK()
|
||||
tfs, _ := sys.DriveForLocal.GetOK()
|
||||
ret, err := netstack.Create(logf,
|
||||
sys.Tun.Get(),
|
||||
sys.Engine.Get(),
|
||||
@@ -831,23 +831,23 @@ func beChild(args []string) error {
|
||||
return f(args[1:])
|
||||
}
|
||||
|
||||
var serveTailFSFunc = serveTailFS
|
||||
var serveDriveFunc = serveDrive
|
||||
|
||||
// serveTailFS serves one or more tailfs on localhost using the WebDAV
|
||||
// serveDrive serves one or more tailfs on localhost using the WebDAV
|
||||
// protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
|
||||
// tailscaled processes in serve-tailfs mode in order to access the fliesystem
|
||||
// as specific (usually unprivileged) users.
|
||||
//
|
||||
// serveTailFS prints the address on which it's listening to stdout so that the
|
||||
// serveDrive prints the address on which it's listening to stdout so that the
|
||||
// parent process knows where to connect to.
|
||||
func serveTailFS(args []string) error {
|
||||
func serveDrive(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing shares")
|
||||
}
|
||||
if len(args)%2 != 0 {
|
||||
return errors.New("need <sharename> <path> pairs")
|
||||
}
|
||||
s, err := tailfsimpl.NewFileServer()
|
||||
s, err := driveimpl.NewFileServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start tailfs FileServer: %v", err)
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ import (
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
@@ -316,7 +316,7 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
sys.Set(netMon)
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(log.Printf))
|
||||
sys.Set(driveimpl.NewFileSystemForRemote(log.Printf))
|
||||
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
|
||||
|
||||
@@ -90,8 +90,11 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
c := logtail.Config{
|
||||
Collection: lpc.Collection,
|
||||
PrivateID: lpc.PrivateID,
|
||||
// NewZstdEncoder is intentionally not passed in, compressed requests
|
||||
// set HTTP headers that are not supported by the no-cors fetching mode.
|
||||
|
||||
// Compressed requests set HTTP headers that are not supported by the
|
||||
// no-cors fetching mode:
|
||||
CompressLogs: false,
|
||||
|
||||
HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
|
||||
}
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
|
||||
@@ -82,9 +82,9 @@ type Direct struct {
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverLegacyKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key; only used for signRegisterRequest on Windows now
|
||||
serverNoiseKey key.MachinePublic
|
||||
|
||||
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
|
||||
noiseClient *NoiseClient
|
||||
@@ -436,12 +436,6 @@ type loginOpt struct {
|
||||
OldNodeKeySignature tkatype.MarshaledSignature
|
||||
}
|
||||
|
||||
// httpClient provides a common interface for the noiseClient and
|
||||
// the NaCl box http.Client.
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// hostInfoLocked returns a Clone of c.hostinfo and c.netinfo.
|
||||
// It must only be called with c.mu held.
|
||||
func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo {
|
||||
@@ -454,7 +448,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.mu.Lock()
|
||||
persist := c.persist.AsStruct()
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverKey
|
||||
serverKey := c.serverLegacyKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
|
||||
hi := c.hostInfoLocked()
|
||||
@@ -494,20 +488,22 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
||||
|
||||
c.mu.Lock()
|
||||
c.serverKey = keys.LegacyPublicKey
|
||||
c.serverLegacyKey = keys.LegacyPublicKey
|
||||
c.serverNoiseKey = keys.PublicKey
|
||||
c.mu.Unlock()
|
||||
serverKey = keys.LegacyPublicKey
|
||||
serverNoiseKey = keys.PublicKey
|
||||
|
||||
// For servers supporting the Noise transport,
|
||||
// proactively shut down our TLS TCP connection.
|
||||
// Proactively shut down our TLS TCP connection.
|
||||
// We're not going to need it and it's nicer to the
|
||||
// server.
|
||||
if !serverNoiseKey.IsZero() {
|
||||
c.httpc.CloseIdleConnections()
|
||||
}
|
||||
c.httpc.CloseIdleConnections()
|
||||
}
|
||||
|
||||
if serverNoiseKey.IsZero() {
|
||||
return false, "", nil, errors.New("control server is too old; no noise key")
|
||||
}
|
||||
|
||||
var oldNodeKey key.NodePublic
|
||||
switch {
|
||||
case opt.Logout:
|
||||
@@ -594,7 +590,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
request.Auth.Provider = persist.Provider
|
||||
request.Auth.LoginName = persist.UserProfile.LoginName
|
||||
request.Auth.AuthKey = authKey
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverKey, machinePrivKey.Public())
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public())
|
||||
if err != nil {
|
||||
// If signing failed, clear all related fields
|
||||
request.SignatureType = tailcfg.SignatureNone
|
||||
@@ -614,21 +610,16 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
}
|
||||
|
||||
// URL and httpc are protocol specific.
|
||||
var url string
|
||||
var httpc httpClient
|
||||
if serverNoiseKey.IsZero() {
|
||||
httpc = c.httpc
|
||||
url = fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
} else {
|
||||
request.Version = tailcfg.CurrentCapabilityVersion
|
||||
httpc, err = c.getNoiseClient()
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url = fmt.Sprintf("%s/machine/register", c.serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
request.Version = tailcfg.CurrentCapabilityVersion
|
||||
httpc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
url := fmt.Sprintf("%s/machine/register", c.serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
bodyData, err := encode(request)
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
@@ -650,7 +641,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, serverKey, serverNoiseKey, machinePrivKey); err != nil {
|
||||
if err := decode(res, &resp); err != nil {
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
@@ -844,7 +835,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverKey := c.serverKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
@@ -858,6 +848,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if serverNoiseKey.IsZero() {
|
||||
return errors.New("control server is too old; no noise key")
|
||||
}
|
||||
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
@@ -914,7 +908,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
request.Compress = "zstd"
|
||||
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
bodyData, err := encode(request)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
@@ -926,20 +920,36 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
machinePubKey := machinePrivKey.Public()
|
||||
t0 := c.clock.Now()
|
||||
|
||||
// Url and httpc are protocol specific.
|
||||
var url string
|
||||
var httpc httpClient
|
||||
if serverNoiseKey.IsZero() {
|
||||
httpc = c.httpc
|
||||
url = fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.UntypedHexString())
|
||||
} else {
|
||||
httpc, err = c.getNoiseClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url = fmt.Sprintf("%s/machine/map", serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
httpc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url := fmt.Sprintf("%s/machine/map", serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
|
||||
// Create a watchdog timer that breaks the connection if we don't receive a
|
||||
// MapResponse from the network at least once every two minutes. The
|
||||
// watchdog timer is stopped every time we receive a MapResponse (so it
|
||||
// doesn't run when we're processing a MapResponse message, including any
|
||||
// long-running requested operations like Debug.Sleep) and is reset whenever
|
||||
// we go back to blocking on network reads.
|
||||
// The watchdog timer also covers the initial request (effectively the
|
||||
// pre-body and initial-body read timeouts) as we do not have any other
|
||||
// keep-alive mechanism for the initial request.
|
||||
watchdogTimer, watchdogTimedOut := c.clock.NewTimer(watchdogTimeout)
|
||||
defer watchdogTimer.Stop()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
case <-watchdogTimedOut:
|
||||
c.logf("map response long-poll timed out!")
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
@@ -962,6 +972,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
defer res.Body.Close()
|
||||
|
||||
health.NoteMapRequestHeard(request)
|
||||
watchdogTimer.Reset(watchdogTimeout)
|
||||
|
||||
if nu == nil {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
@@ -993,27 +1004,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.expiry = nm.Expiry
|
||||
}
|
||||
|
||||
// Create a watchdog timer that breaks the connection if we don't receive a
|
||||
// MapResponse from the network at least once every two minutes. The
|
||||
// watchdog timer is stopped every time we receive a MapResponse (so it
|
||||
// doesn't run when we're processing a MapResponse message, including any
|
||||
// long-running requested operations like Debug.Sleep) and is reset whenever
|
||||
// we go back to blocking on network reads.
|
||||
watchdogTimer, watchdogTimedOut := c.clock.NewTimer(watchdogTimeout)
|
||||
defer watchdogTimer.Stop()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
vlogf("netmap: ending timeout goroutine")
|
||||
return
|
||||
case <-watchdogTimedOut:
|
||||
c.logf("map response long-poll timed out!")
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// gotNonKeepAliveMessage is whether we've yet received a MapResponse message without
|
||||
// KeepAlive set.
|
||||
var gotNonKeepAliveMessage bool
|
||||
@@ -1043,7 +1033,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
|
||||
if err := c.decodeMsg(msg, &resp); err != nil {
|
||||
vlogf("netmap: decode error: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -1160,9 +1150,8 @@ func initDisplayNames(selfNode tailcfg.NodeView, resp *tailcfg.MapResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
// decode JSON decodes the res.Body into v. If serverNoiseKey is not specified,
|
||||
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
|
||||
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
|
||||
// decode JSON decodes the res.Body into v.
|
||||
func decode(res *http.Response, v any) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -1171,10 +1160,7 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("%d: %v", res.StatusCode, string(msg))
|
||||
}
|
||||
if !serverNoiseKey.IsZero() {
|
||||
return json.Unmarshal(msg, v)
|
||||
}
|
||||
return decodeMsg(msg, v, serverKey, mkey)
|
||||
return json.Unmarshal(msg, v)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1185,25 +1171,8 @@ var (
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
|
||||
// If c.serverNoiseKey is not specified, it uses the c.serverKey and mkey
|
||||
// to first the decrypt msg from the NaCl-crypto-box.
|
||||
func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
c.mu.Unlock()
|
||||
|
||||
var decrypted []byte
|
||||
if serverNoiseKey.IsZero() {
|
||||
var ok bool
|
||||
decrypted, ok = mkey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
} else {
|
||||
decrypted = msg
|
||||
}
|
||||
b, err := zstdframe.AppendDecode(nil, decrypted)
|
||||
func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
|
||||
b, err := zstdframe.AppendDecode(nil, compressedMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1220,26 +1189,11 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v any, serverKey key.MachinePublic, machinePrivKey key.MachinePrivate) error {
|
||||
decrypted, ok := machinePrivKey.OpenFrom(serverKey, msg)
|
||||
if !ok {
|
||||
return errors.New("cannot decrypt response")
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
}
|
||||
if err := json.Unmarshal(decrypted, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encode JSON encodes v. If serverNoiseKey is not specified, it uses the serverKey and mkey to
|
||||
// seal the message into a NaCl-crypto-box.
|
||||
func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) ([]byte, error) {
|
||||
// encode JSON encodes v as JSON, logging tailcfg.MapRequest values if
|
||||
// debugMap is set.
|
||||
func encode(v any) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1249,10 +1203,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
if !serverNoiseKey.IsZero() {
|
||||
return b, nil
|
||||
}
|
||||
return mkey.SealTo(serverKey, b), nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string) (*tailcfg.OverTLSPublicKeyResponse, error) {
|
||||
@@ -1349,7 +1300,7 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
|
||||
func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
httpc := c.httpc
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n" && c.noiseConfigured()
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n"
|
||||
if useNoise {
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
@@ -1550,14 +1501,6 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// noiseConfigured reports whether the client can communicate with Control
|
||||
// over Noise.
|
||||
func (c *Direct) noiseConfigured() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return !c.serverNoiseKey.IsZero()
|
||||
}
|
||||
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
|
||||
@@ -1567,53 +1510,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
||||
metricSetDNSError.Add(1)
|
||||
}
|
||||
}()
|
||||
if c.noiseConfigured() {
|
||||
return c.setDNSNoise(ctx, req)
|
||||
}
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
c.mu.Unlock()
|
||||
|
||||
if serverKey.IsZero() {
|
||||
return errors.New("zero serverKey")
|
||||
}
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
}
|
||||
if machinePrivKey.IsZero() {
|
||||
return errors.New("getMachinePrivKey returned zero key")
|
||||
}
|
||||
|
||||
// TODO(maisem): dedupe this codepath from SetDNSNoise.
|
||||
var serverNoiseKey key.MachinePublic
|
||||
bodyData, err := encode(req, serverKey, serverNoiseKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s/set-dns", c.serverURL, machinePrivKey.Public().UntypedHexString())
|
||||
hreq, err := http.NewRequestWithContext(ctx, "POST", u, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := c.httpc.Do(hreq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
if err := decode(res, &setDNSRes, serverKey, serverNoiseKey, machinePrivKey); err != nil {
|
||||
c.logf("error decoding SetDNSResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return fmt.Errorf("set-dns-response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.setDNSNoise(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -795,7 +795,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, pu.Hostname(), authHeader); err != nil {
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, target, authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
// Clone makes a deep copy of Share.
|
||||
// The result aliases no memory with the original.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
//go:build windows || darwin
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// Mkdir implements webdav.FileSystem. All attempts to Mkdir a directory that
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// OpenFile implements interface webdav.Filesystem.
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// Stat implements webdav.FileSystem.
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -38,10 +38,10 @@ const (
|
||||
func init() {
|
||||
// set AllowShareAs() to false so that we don't try to use sub-processes
|
||||
// for access files on disk.
|
||||
tailfs.DisallowShareAs = true
|
||||
drive.DisallowShareAs = true
|
||||
}
|
||||
|
||||
// The tests in this file simulate real-life TailFS scenarios, but without
|
||||
// The tests in this file simulate real-life Taildrive scenarios, but without
|
||||
// going over the Tailscale network stack.
|
||||
func TestDirectoryListing(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
@@ -51,9 +51,9 @@ func TestDirectoryListing(t *testing.T) {
|
||||
s.checkDirList("domain should contain its only remote", shared.Join(domain), remote1)
|
||||
s.checkDirList("remote with no shares should be empty", shared.Join(domain, remote1))
|
||||
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.checkDirList("remote with one share should contain that share", shared.Join(domain, remote1), share11)
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.addShare(remote1, share12, drive.PermissionReadOnly)
|
||||
s.checkDirList("remote with two shares should contain both in lexicographical order", shared.Join(domain, remote1), share12, share11)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkDirList("remote share should contain file", shared.Join(domain, remote1, share11), file111)
|
||||
@@ -76,12 +76,12 @@ func TestFileManipulation(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkFileStatus(remote1, share11, file111)
|
||||
s.checkFileContents(remote1, share11, file111)
|
||||
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.addShare(remote1, share12, drive.PermissionReadOnly)
|
||||
s.writeFile("writing file to read-only remote should fail", remote1, share12, file111, "hello world", false)
|
||||
|
||||
s.writeFile("writing file to non-existent remote should fail", "non-existent", share11, file111, "hello world", false)
|
||||
@@ -98,7 +98,7 @@ type remote struct {
|
||||
fs *FileSystemForRemote
|
||||
fileServer *FileServer
|
||||
shares map[string]string
|
||||
permissions map[string]tailfs.Permission
|
||||
permissions map[string]drive.Permission
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -175,15 +175,15 @@ func (s *system) addRemote(name string) {
|
||||
fileServer: fileServer,
|
||||
fs: NewFileSystemForRemote(log.Printf),
|
||||
shares: make(map[string]string),
|
||||
permissions: make(map[string]tailfs.Permission),
|
||||
permissions: make(map[string]drive.Permission),
|
||||
}
|
||||
r.fs.SetFileServerAddr(fileServer.Addr())
|
||||
go http.Serve(l, r)
|
||||
s.remotes[name] = r
|
||||
|
||||
remotes := make([]*tailfs.Remote, 0, len(s.remotes))
|
||||
remotes := make([]*drive.Remote, 0, len(s.remotes))
|
||||
for name, r := range s.remotes {
|
||||
remotes = append(remotes, &tailfs.Remote{
|
||||
remotes = append(remotes, &drive.Remote{
|
||||
Name: name,
|
||||
URL: fmt.Sprintf("http://%s", r.l.Addr()),
|
||||
})
|
||||
@@ -197,7 +197,7 @@ func (s *system) addRemote(name string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *system) addShare(remoteName, shareName string, permission tailfs.Permission) {
|
||||
func (s *system) addShare(remoteName, shareName string, permission drive.Permission) {
|
||||
r, ok := s.remotes[remoteName]
|
||||
if !ok {
|
||||
s.t.Fatalf("unknown remote %q", remoteName)
|
||||
@@ -207,14 +207,14 @@ func (s *system) addShare(remoteName, shareName string, permission tailfs.Permis
|
||||
r.shares[shareName] = f
|
||||
r.permissions[shareName] = permission
|
||||
|
||||
shares := make([]*tailfs.Share, 0, len(r.shares))
|
||||
shares := make([]*drive.Share, 0, len(r.shares))
|
||||
for shareName, folder := range r.shares {
|
||||
shares = append(shares, &tailfs.Share{
|
||||
shares = append(shares, &drive.Share{
|
||||
Name: shareName,
|
||||
Path: folder,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(shares, tailfs.CompareShares)
|
||||
slices.SortFunc(shares, drive.CompareShares)
|
||||
r.fs.SetShares(shares)
|
||||
r.fileServer.SetShares(r.shares)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
// FileServer is a standalone WebDAV server that dynamically serves up shares.
|
||||
// It's typically used in a separate process from the actual TailFS server to
|
||||
// It's typically used in a separate process from the actual Taildrive server to
|
||||
// serve up files as an unprivileged user.
|
||||
type FileServer struct {
|
||||
l net.Listener
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfsimpl provides an implementation of package tailfs.
|
||||
package tailfsimpl
|
||||
// Package driveimpl provides an implementation of package drive.
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositedav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/compositedav"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
// FileSystemForLocal is the Taildrive filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote Taildrive shares on other nodes.
|
||||
type FileSystemForLocal struct {
|
||||
logf logger.Logf
|
||||
h *compositedav.Handler
|
||||
@@ -69,7 +69,7 @@ func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) erro
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*tailfs.Remote, transport http.RoundTripper) {
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*drive.Remote, transport http.RoundTripper) {
|
||||
children := make([]*compositedav.Child, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
children = append(children, &compositedav.Child{
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/compositedav"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositedav"
|
||||
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote implements tailfs.FileSystemForRemote.
|
||||
// FileSystemForRemote implements drive.FileSystemForRemote.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
@@ -53,23 +53,23 @@ type FileSystemForRemote struct {
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
fileServerAddr string
|
||||
shares []*tailfs.Share
|
||||
shares []*drive.Share
|
||||
children map[string]*compositedav.Child
|
||||
userServers map[string]*userServer
|
||||
}
|
||||
|
||||
// SetFileServerAddr implements tailfs.FileSystemForRemote.
|
||||
// SetFileServerAddr implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
||||
s.mu.Lock()
|
||||
s.fileServerAddr = addr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetShares implements tailfs.FileSystemForRemote. Shares must be sorted
|
||||
// according to tailfs.CompareShares.
|
||||
func (s *FileSystemForRemote) SetShares(shares []*tailfs.Share) {
|
||||
// SetShares implements drive.FileSystemForRemote. Shares must be sorted
|
||||
// according to drive.CompareShares.
|
||||
func (s *FileSystemForRemote) SetShares(shares []*drive.Share) {
|
||||
userServers := make(map[string]*userServer)
|
||||
if tailfs.AllowShareAs() {
|
||||
if drive.AllowShareAs() {
|
||||
// Set up per-user server by running the current executable as an
|
||||
// unprivileged user in order to avoid privilege escalation.
|
||||
executable, err := os.Executable()
|
||||
@@ -112,7 +112,7 @@ func (s *FileSystemForRemote) SetShares(shares []*tailfs.Share) {
|
||||
s.closeChildren(oldChildren)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Child {
|
||||
func (s *FileSystemForRemote) buildChild(share *drive.Share) *compositedav.Child {
|
||||
return &compositedav.Child{
|
||||
Child: &dirfs.Child{
|
||||
Name: share.Name,
|
||||
@@ -133,8 +133,8 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
shareName := string(shareNameBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
var share *tailfs.Share
|
||||
i, shareFound := slices.BinarySearchFunc(s.shares, shareName, func(s *tailfs.Share, name string) int {
|
||||
var share *drive.Share
|
||||
i, shareFound := slices.BinarySearchFunc(s.shares, shareName, func(s *drive.Share, name string) int {
|
||||
return strings.Compare(s.Name, name)
|
||||
})
|
||||
if shareFound {
|
||||
@@ -149,7 +149,7 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
}
|
||||
|
||||
var addr string
|
||||
if !tailfs.AllowShareAs() {
|
||||
if !drive.AllowShareAs() {
|
||||
addr = fileServerAddr
|
||||
} else {
|
||||
userServer, found := userServers[share.As]
|
||||
@@ -176,18 +176,18 @@ func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Chil
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
// ServeHTTPWithPerms implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions drive.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
isWrite := writeMethods[r.Method]
|
||||
if isWrite {
|
||||
share := shared.CleanAndSplit(r.URL.Path)[0]
|
||||
switch permissions.For(share) {
|
||||
case tailfs.PermissionNone:
|
||||
case drive.PermissionNone:
|
||||
// If we have no permissions to this share, treat it as not found
|
||||
// to avoid leaking any information about the share's existence.
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
case tailfs.PermissionReadOnly:
|
||||
case drive.PermissionReadOnly:
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions,
|
||||
children := make([]*compositedav.Child, 0, len(childrenMap))
|
||||
// filter out shares to which the connecting principal has no access
|
||||
for name, child := range childrenMap {
|
||||
if permissions.For(name) == tailfs.PermissionNone {
|
||||
if permissions.For(name) == drive.PermissionNone {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ func (s *FileSystemForRemote) closeChildren(children map[string]*compositedav.Ch
|
||||
}
|
||||
}
|
||||
|
||||
// Close() implements tailfs.FileSystemForRemote.
|
||||
// Close() implements drive.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) Close() error {
|
||||
s.mu.Lock()
|
||||
userServers := s.userServers
|
||||
@@ -247,7 +247,7 @@ func (s *FileSystemForRemote) Close() error {
|
||||
// content is served as that Share.As user.
|
||||
type userServer struct {
|
||||
logf logger.Logf
|
||||
shares []*tailfs.Share
|
||||
shares []*drive.Share
|
||||
username string
|
||||
executable string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package shared contains types and functions shared by different tailfs
|
||||
// Package shared contains types and functions shared by different drive
|
||||
// packages.
|
||||
package shared
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfs provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV. The actual implementation of the core TailFS
|
||||
// functionality lives in package tailfsimpl. These packages are separated to
|
||||
// allow users of tailfs to refer to the interfaces without having a hard
|
||||
// dependency on tailfs, so that programs which don't actually use tailfs can
|
||||
// Package drive provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV. The actual implementation of the core Taildrive
|
||||
// functionality lives in package driveimpl. These packages are separated to
|
||||
// allow users of Taildrive to refer to the interfaces without having a hard
|
||||
// dependency on Taildrive, so that programs which don't actually use Taildrive can
|
||||
// avoid its transitive dependencies.
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Remote represents a remote TailFS node.
|
||||
// Remote represents a remote Taildrive node.
|
||||
type Remote struct {
|
||||
Name string
|
||||
URL string
|
||||
Available func() bool
|
||||
}
|
||||
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
// FileSystemForLocal is the Taildrive filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote Taildrive shares on other nodes.
|
||||
type FileSystemForLocal interface {
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
HandleConn(conn net.Conn, remoteAddr net.Addr) error
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=Share --clonefunc
|
||||
|
||||
@@ -22,7 +22,7 @@ func AllowShareAs() bool {
|
||||
return !DisallowShareAs && doAllowShareAs()
|
||||
}
|
||||
|
||||
// Share configures a folder to be shared through TailFS.
|
||||
// Share configures a folder to be shared through drive.
|
||||
type Share struct {
|
||||
// Name is how this share appears on remote nodes.
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -78,7 +78,7 @@ func CompareShares(a, b *Share) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
|
||||
// FileSystemForRemote is the TailFS filesystem exposed to remote nodes. It
|
||||
// FileSystemForRemote is the drive filesystem exposed to remote nodes. It
|
||||
// provides a unified WebDAV interface to local directories that have been
|
||||
// shared.
|
||||
type FileSystemForRemote interface {
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !unix
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
func doAllowShareAs() bool {
|
||||
// On non-UNIX platforms, we use the GUI application (e.g. Windows taskbar
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build unix
|
||||
|
||||
package tailfs
|
||||
package drive
|
||||
|
||||
import "tailscale.com/version"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -28,7 +28,7 @@ require (
|
||||
github.com/evanw/esbuild v0.19.11
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fxamacker/cbor/v2 v2.5.0
|
||||
github.com/gaissmai/bart v0.4.0
|
||||
github.com/gaissmai/bart v0.4.1
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -296,8 +296,8 @@ github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADi
|
||||
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
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.4.0 h1:ImIFoETsNMBzUr21tMGD82GQIwAb555fI6uxEyCHBTI=
|
||||
github.com/gaissmai/bart v0.4.0/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls=
|
||||
github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
|
||||
@@ -1 +1 @@
|
||||
f86d7c8ef64a0f8a2516fc23652eee28abc8d8e0
|
||||
48d71857bf5352daaa10b61dd3e9b1c0dd51e27a
|
||||
|
||||
@@ -52,7 +52,7 @@ func New() *tailcfg.Hostinfo {
|
||||
GoArchVar: lazyGoArchVar.Get(),
|
||||
GoVersion: runtime.Version(),
|
||||
Machine: condCall(unameMachine),
|
||||
DeviceModel: deviceModel(),
|
||||
DeviceModel: deviceModelCached(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
NoLogsNoSupport: envknob.NoLogsNoSupport(),
|
||||
AllowsUpdate: envknob.AllowsRemoteUpdate(),
|
||||
@@ -68,6 +68,7 @@ var (
|
||||
distroVersion func() string
|
||||
distroCodeName func() string
|
||||
unameMachine func() string
|
||||
deviceModel func() string
|
||||
)
|
||||
|
||||
func condCall[T any](fn func() T) T {
|
||||
@@ -176,6 +177,20 @@ var (
|
||||
// SetDeviceModel sets the device model for use in Hostinfo updates.
|
||||
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
|
||||
|
||||
func deviceModelCached() string {
|
||||
if v, _ := deviceModelAtomic.Load().(string); v != "" {
|
||||
return v
|
||||
}
|
||||
if deviceModel == nil {
|
||||
return ""
|
||||
}
|
||||
v := deviceModel()
|
||||
if v != "" {
|
||||
deviceModelAtomic.Store(v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// SetOSVersion sets the OS version.
|
||||
func SetOSVersion(v string) { osVersionAtomic.Store(v) }
|
||||
|
||||
@@ -193,11 +208,6 @@ func SetPackage(v string) { packagingType.Store(v) }
|
||||
// and "k8s-operator".
|
||||
func SetApp(v string) { appType.Store(v) }
|
||||
|
||||
func deviceModel() string {
|
||||
s, _ := deviceModelAtomic.Load().(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// FirewallMode returns the firewall mode for the app.
|
||||
// It is empty if unset.
|
||||
func FirewallMode() string {
|
||||
|
||||
@@ -22,9 +22,7 @@ func init() {
|
||||
distroName = distroNameLinux
|
||||
distroVersion = distroVersionLinux
|
||||
distroCodeName = distroCodeNameLinux
|
||||
if v := linuxDeviceModel(); v != "" {
|
||||
SetDeviceModel(v)
|
||||
}
|
||||
deviceModel = deviceModelLinux
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -50,7 +48,7 @@ func distroCodeNameLinux() string {
|
||||
return lazyVersionMeta.Get().DistroCodeName
|
||||
}
|
||||
|
||||
func linuxDeviceModel() string {
|
||||
func deviceModelLinux() string {
|
||||
for _, path := range []string{
|
||||
// First try the Synology-specific location.
|
||||
// Example: "DS916+-j"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -67,8 +67,9 @@ const (
|
||||
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
|
||||
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
|
||||
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyInitialTailFSShares // if set, the first Notify message (sent immediately) will contain the current TailFS Shares
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyInitialDriveShares // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
|
||||
NotifyInitialOutgoingFiles // if set, the first Notify message (sent immediately) will contain the current Taildrop OutgoingFiles
|
||||
)
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -114,6 +115,11 @@ type Notify struct {
|
||||
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
|
||||
IncomingFiles []PartialFile `json:",omitempty"`
|
||||
|
||||
// OutgoingFiles, if non-nil, tracks which files are in the process of
|
||||
// being sent via TailDrop, including files that finished, whether
|
||||
// successful or failed. This slice is sorted by Started time, then Name.
|
||||
OutgoingFiles []*OutgoingFile `json:",omitempty"`
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
// This is currently only used by Tailscale when run in the
|
||||
@@ -124,13 +130,13 @@ type Notify struct {
|
||||
// is available.
|
||||
ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
|
||||
|
||||
// TailFSShares tracks the full set of current TailFSShares that we're
|
||||
// DriveShares tracks the full set of current DriveShares that we're
|
||||
// publishing. Some client applications, like the MacOS and Windows clients,
|
||||
// will listen for updates to this and handle serving these shares under
|
||||
// the identity of the unprivileged user that is running the application. A
|
||||
// nil value here means that we're not broadcasting shares information, an
|
||||
// empty value means that there are no shares.
|
||||
TailFSShares views.SliceView[*tailfs.Share, tailfs.ShareView]
|
||||
DriveShares views.SliceView[*drive.Share, drive.ShareView]
|
||||
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
@@ -175,7 +181,7 @@ func (n Notify) String() string {
|
||||
return s[0:len(s)-1] + "}"
|
||||
}
|
||||
|
||||
// PartialFile represents an in-progress file transfer.
|
||||
// PartialFile represents an in-progress incoming file transfer.
|
||||
type PartialFile struct {
|
||||
Name string // e.g. "foo.jpg"
|
||||
Started time.Time // time transfer started
|
||||
@@ -194,6 +200,18 @@ type PartialFile struct {
|
||||
Done bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// OutgoingFile represents an in-progress outgoing file transfer.
|
||||
type OutgoingFile struct {
|
||||
ID string `json:",omitempty"` // unique identifier for this transfer (a type 4 UUID)
|
||||
PeerID tailcfg.StableNodeID `json:",omitempty"` // identifier for the peer to which this is being transferred
|
||||
Name string `json:",omitempty"` // e.g. "foo.jpg"
|
||||
Started time.Time // time transfer started
|
||||
DeclaredSize int64 // or -1 if unknown
|
||||
Sent int64 // bytes copied thus far
|
||||
Finished bool // indicates whether or not the transfer finished
|
||||
Succeeded bool // for a finished transfer, indicates whether or not it was successful
|
||||
}
|
||||
|
||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||
// (preferences, private keys, etc.). It is also used as a key for
|
||||
// the various LoginProfiles that the instance may be signed into.
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
@@ -25,10 +25,10 @@ func (src *Prefs) Clone() *Prefs {
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if src.TailFSShares != nil {
|
||||
dst.TailFSShares = make([]*tailfs.Share, len(src.TailFSShares))
|
||||
for i := range dst.TailFSShares {
|
||||
dst.TailFSShares[i] = src.TailFSShares[i].Clone()
|
||||
if src.DriveShares != nil {
|
||||
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
||||
for i := range dst.DriveShares {
|
||||
dst.DriveShares[i] = src.DriveShares[i].Clone()
|
||||
}
|
||||
}
|
||||
dst.Persist = src.Persist.Clone()
|
||||
@@ -63,7 +63,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
TailFSShares []*tailfs.Share
|
||||
DriveShares []*drive.Share
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
@@ -92,8 +92,8 @@ func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpda
|
||||
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
|
||||
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
|
||||
func (v PrefsView) TailFSShares() views.SliceView[*tailfs.Share, tailfs.ShareView] {
|
||||
return views.SliceOfViews[*tailfs.Share, tailfs.ShareView](v.ж.TailFSShares)
|
||||
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
|
||||
}
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
@@ -125,7 +125,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
TailFSShares []*tailfs.Share
|
||||
DriveShares []*drive.Share
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -11,69 +11,69 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
const (
|
||||
// TailFSLocalPort is the port on which the TailFS listens for location
|
||||
// DriveLocalPort is the port on which the Taildrive listens for location
|
||||
// connections on quad 100.
|
||||
TailFSLocalPort = 8080
|
||||
DriveLocalPort = 8080
|
||||
)
|
||||
|
||||
var (
|
||||
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
||||
ErrTailFSNotEnabled = errors.New("TailFS not enabled")
|
||||
ErrDriveNotEnabled = errors.New("TailFS not enabled")
|
||||
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
||||
)
|
||||
|
||||
// TailFSSharingEnabled reports whether sharing to remote nodes via tailfs is
|
||||
// DriveSharingEnabled reports whether sharing to remote nodes via Taildrive is
|
||||
// enabled. This is currently based on checking for the tailfs:share node
|
||||
// attribute.
|
||||
func (b *LocalBackend) TailFSSharingEnabled() bool {
|
||||
func (b *LocalBackend) DriveSharingEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.tailFSSharingEnabledLocked()
|
||||
return b.driveSharingEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSSharingEnabledLocked() bool {
|
||||
func (b *LocalBackend) driveSharingEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailFSShare)
|
||||
}
|
||||
|
||||
// TailFSAccessEnabled reports whether accessing TailFS shares on remote nodes
|
||||
// DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes
|
||||
// is enabled. This is currently based on checking for the tailfs:access node
|
||||
// attribute.
|
||||
func (b *LocalBackend) TailFSAccessEnabled() bool {
|
||||
func (b *LocalBackend) DriveAccessEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.tailFSAccessEnabledLocked()
|
||||
return b.driveAccessEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSAccessEnabledLocked() bool {
|
||||
func (b *LocalBackend) driveAccessEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailFSAccess)
|
||||
}
|
||||
|
||||
// TailFSSetFileServerAddr tells tailfs to use the given address for connecting
|
||||
// to the tailfs.FileServer that's exposing local files as an unprivileged
|
||||
// DriveSetServerAddr tells Taildrive to use the given address for connecting
|
||||
// to the drive.FileServer that's exposing local files as an unprivileged
|
||||
// user.
|
||||
func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
func (b *LocalBackend) DriveSetServerAddr(addr string) error {
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return ErrTailFSNotEnabled
|
||||
return ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
fs.SetFileServerAddr(addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TailFSSetShare adds the given share if no share with that name exists, or
|
||||
// DriveSetShare adds the given share if no share with that name exists, or
|
||||
// replaces the existing share if one with the same name already exists. To
|
||||
// avoid potential incompatibilities across file systems, share names are
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
func (b *LocalBackend) TailFSSetShare(share *tailfs.Share) error {
|
||||
func (b *LocalBackend) DriveSetShare(share *drive.Share) error {
|
||||
var err error
|
||||
share.Name, err = normalizeShareName(share.Name)
|
||||
if err != nil {
|
||||
@@ -81,13 +81,13 @@ func (b *LocalBackend) TailFSSetShare(share *tailfs.Share) error {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailFSSetShareLocked(share)
|
||||
shares, err := b.driveSetShareLocked(share)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.tailFSNotifyShares(shares)
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -108,16 +108,16 @@ func normalizeShareName(name string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSSetShareLocked(share *tailfs.Share) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
|
||||
existingShares := b.pm.prefs.TailFSShares()
|
||||
func (b *LocalBackend) driveSetShareLocked(share *drive.Share) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, ErrTailFSNotEnabled
|
||||
return existingShares, ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
addedShare := false
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() != share.Name {
|
||||
@@ -133,23 +133,23 @@ func (b *LocalBackend) tailFSSetShareLocked(share *tailfs.Share) (views.SliceVie
|
||||
shares = append(shares, share)
|
||||
}
|
||||
|
||||
err := b.tailFSSetSharesLocked(shares)
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.TailFSShares(), nil
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// TailFSRenameShare renames the share at old name to new name. To avoid
|
||||
// DriveRenameShare renames the share at old name to new name. To avoid
|
||||
// potential incompatibilities across file systems, the new share name is
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
// Any of the following will result in an error.
|
||||
// - no share found under old name
|
||||
// - new share name contains disallowed characters
|
||||
// - share already exists under new name
|
||||
func (b *LocalBackend) TailFSRenameShare(oldName, newName string) error {
|
||||
func (b *LocalBackend) DriveRenameShare(oldName, newName string) error {
|
||||
var err error
|
||||
newName, err = normalizeShareName(newName)
|
||||
if err != nil {
|
||||
@@ -157,26 +157,26 @@ func (b *LocalBackend) TailFSRenameShare(oldName, newName string) error {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailFSRenameShareLocked(oldName, newName)
|
||||
shares, err := b.driveRenameShareLocked(oldName, newName)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.tailFSNotifyShares(shares)
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSRenameShareLocked(oldName, newName string) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
|
||||
existingShares := b.pm.prefs.TailFSShares()
|
||||
func (b *LocalBackend) driveRenameShareLocked(oldName, newName string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, ErrTailFSNotEnabled
|
||||
return existingShares, ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() == newName {
|
||||
@@ -196,19 +196,19 @@ func (b *LocalBackend) tailFSRenameShareLocked(oldName, newName string) (views.S
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
slices.SortFunc(shares, tailfs.CompareShares)
|
||||
err := b.tailFSSetSharesLocked(shares)
|
||||
slices.SortFunc(shares, drive.CompareShares)
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.TailFSShares(), nil
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
// TailFSRemoveShare removes the named share. Share names are forced to
|
||||
// DriveRemoveShare removes the named share. Share names are forced to
|
||||
// lowercase.
|
||||
func (b *LocalBackend) TailFSRemoveShare(name string) error {
|
||||
func (b *LocalBackend) DriveRemoveShare(name string) error {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
var err error
|
||||
@@ -218,26 +218,26 @@ func (b *LocalBackend) TailFSRemoveShare(name string) error {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailFSRemoveShareLocked(name)
|
||||
shares, err := b.driveRemoveShareLocked(name)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.tailFSNotifyShares(shares)
|
||||
b.driveNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSRemoveShareLocked(name string) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
|
||||
existingShares := b.pm.prefs.TailFSShares()
|
||||
func (b *LocalBackend) driveRemoveShareLocked(name string) (views.SliceView[*drive.Share, drive.ShareView], error) {
|
||||
existingShares := b.pm.prefs.DriveShares()
|
||||
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
return existingShares, ErrTailFSNotEnabled
|
||||
return existingShares, ErrDriveNotEnabled
|
||||
}
|
||||
|
||||
found := false
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < existingShares.Len(); i++ {
|
||||
existing := existingShares.At(i)
|
||||
if existing.Name() != name {
|
||||
@@ -251,53 +251,53 @@ func (b *LocalBackend) tailFSRemoveShareLocked(name string) (views.SliceView[*ta
|
||||
return existingShares, os.ErrNotExist
|
||||
}
|
||||
|
||||
err := b.tailFSSetSharesLocked(shares)
|
||||
err := b.driveSetSharesLocked(shares)
|
||||
if err != nil {
|
||||
return existingShares, err
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return b.pm.prefs.TailFSShares(), nil
|
||||
return b.pm.prefs.DriveShares(), nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSSetSharesLocked(shares []*tailfs.Share) error {
|
||||
func (b *LocalBackend) driveSetSharesLocked(shares []*drive.Share) error {
|
||||
prefs := b.pm.prefs.AsStruct()
|
||||
prefs.ApplyEdits(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
TailFSShares: shares,
|
||||
DriveShares: shares,
|
||||
},
|
||||
TailFSSharesSet: true,
|
||||
DriveSharesSet: true,
|
||||
})
|
||||
return b.pm.setPrefsLocked(prefs.View())
|
||||
}
|
||||
|
||||
// tailFSNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||||
// driveNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||||
// about the latest list of shares.
|
||||
func (b *LocalBackend) tailFSNotifyShares(shares views.SliceView[*tailfs.Share, tailfs.ShareView]) {
|
||||
func (b *LocalBackend) driveNotifyShares(shares views.SliceView[*drive.Share, drive.ShareView]) {
|
||||
// Ensures shares is not nil to distinguish "no shares" from "not notifying shares"
|
||||
if shares.IsNil() {
|
||||
shares = views.SliceOfViews(make([]*tailfs.Share, 0))
|
||||
shares = views.SliceOfViews(make([]*drive.Share, 0))
|
||||
}
|
||||
b.send(ipn.Notify{TailFSShares: shares})
|
||||
b.send(ipn.Notify{DriveShares: shares})
|
||||
}
|
||||
|
||||
// tailFSNotifyCurrentSharesLocked sends an ipn.Notify if the current set of
|
||||
// driveNotifyCurrentSharesLocked sends an ipn.Notify if the current set of
|
||||
// shares has changed since the last notification.
|
||||
func (b *LocalBackend) tailFSNotifyCurrentSharesLocked() {
|
||||
var shares views.SliceView[*tailfs.Share, tailfs.ShareView]
|
||||
if b.tailFSSharingEnabledLocked() {
|
||||
func (b *LocalBackend) driveNotifyCurrentSharesLocked() {
|
||||
var shares views.SliceView[*drive.Share, drive.ShareView]
|
||||
if b.driveSharingEnabledLocked() {
|
||||
// Only populate shares if sharing is enabled.
|
||||
shares = b.pm.prefs.TailFSShares()
|
||||
shares = b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
lastNotified := b.lastNotifiedTailFSShares.Load()
|
||||
if lastNotified == nil || !tailFSShareViewsEqual(lastNotified, shares) {
|
||||
lastNotified := b.lastNotifiedDriveShares.Load()
|
||||
if lastNotified == nil || !driveShareViewsEqual(lastNotified, shares) {
|
||||
// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
|
||||
go b.tailFSNotifyShares(shares)
|
||||
go b.driveNotifyShares(shares)
|
||||
}
|
||||
}
|
||||
|
||||
func tailFSShareViewsEqual(a *views.SliceView[*tailfs.Share, tailfs.ShareView], b views.SliceView[*tailfs.Share, tailfs.ShareView]) bool {
|
||||
func driveShareViewsEqual(a *views.SliceView[*drive.Share, drive.ShareView], b views.SliceView[*drive.Share, drive.ShareView]) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func tailFSShareViewsEqual(a *views.SliceView[*tailfs.Share, tailfs.ShareView],
|
||||
}
|
||||
|
||||
for i := 0; i < a.Len(); i++ {
|
||||
if !tailfs.ShareViewsEqual(a.At(i), b.At(i)) {
|
||||
if !drive.ShareViewsEqual(a.At(i), b.At(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -315,35 +315,35 @@ func tailFSShareViewsEqual(a *views.SliceView[*tailfs.Share, tailfs.ShareView],
|
||||
return true
|
||||
}
|
||||
|
||||
// TailFSGetShares() gets the current list of TailFS shares, sorted by name.
|
||||
func (b *LocalBackend) TailFSGetShares() views.SliceView[*tailfs.Share, tailfs.ShareView] {
|
||||
// DriveGetShares gets the current list of Taildrive shares, sorted by name.
|
||||
func (b *LocalBackend) DriveGetShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.pm.prefs.TailFSShares()
|
||||
return b.pm.prefs.DriveShares()
|
||||
}
|
||||
|
||||
// updateTailFSPeersLocked sets all applicable peers from the netmap as tailfs
|
||||
// updateDrivePeersLocked sets all applicable peers from the netmap as Taildrive
|
||||
// remotes.
|
||||
func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
|
||||
fs, ok := b.sys.TailFSForLocal.GetOK()
|
||||
func (b *LocalBackend) updateDrivePeersLocked(nm *netmap.NetworkMap) {
|
||||
fs, ok := b.sys.DriveForLocal.GetOK()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var tailFSRemotes []*tailfs.Remote
|
||||
if b.tailFSAccessEnabledLocked() {
|
||||
var driveRemotes []*drive.Remote
|
||||
if b.driveAccessEnabledLocked() {
|
||||
// Only populate peers if access is enabled, otherwise leave blank.
|
||||
tailFSRemotes = b.tailFSRemotesFromPeers(nm)
|
||||
driveRemotes = b.driveRemotesFromPeers(nm)
|
||||
}
|
||||
|
||||
fs.SetRemotes(b.netMap.Domain, tailFSRemotes, &tailFSTransport{b: b})
|
||||
fs.SetRemotes(b.netMap.Domain, driveRemotes, &driveTransport{b: b})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSRemotesFromPeers(nm *netmap.NetworkMap) []*tailfs.Remote {
|
||||
tailFSRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
|
||||
func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Remote {
|
||||
driveRemotes := make([]*drive.Remote, 0, len(nm.Peers))
|
||||
for _, p := range nm.Peers {
|
||||
// Exclude mullvad exit nodes from list of TailFS peers
|
||||
// Exclude mullvad exit nodes from list of Taildrive peers
|
||||
// TODO(oxtoacart) - once we have a better mechanism for finding only accessible sharers
|
||||
// (see below) we can remove this logic.
|
||||
if strings.HasSuffix(p.Name(), ".mullvad.ts.net.") {
|
||||
@@ -352,7 +352,7 @@ func (b *LocalBackend) tailFSRemotesFromPeers(nm *netmap.NetworkMap) []*tailfs.R
|
||||
|
||||
peerID := p.ID()
|
||||
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailFSPrefix[1:])
|
||||
tailFSRemotes = append(tailFSRemotes, &tailfs.Remote{
|
||||
driveRemotes = append(driveRemotes, &drive.Remote{
|
||||
Name: p.DisplayName(false),
|
||||
URL: url,
|
||||
Available: func() bool {
|
||||
@@ -381,5 +381,5 @@ func (b *LocalBackend) tailFSRemotesFromPeers(nm *netmap.NetworkMap) []*tailfs.R
|
||||
},
|
||||
})
|
||||
}
|
||||
return tailFSRemotes
|
||||
return driveRemotes
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -42,6 +43,7 @@ import (
|
||||
"tailscale.com/doctor/ethtool"
|
||||
"tailscale.com/doctor/permissions"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/health/healthmsg"
|
||||
@@ -68,7 +70,6 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstime"
|
||||
@@ -86,6 +87,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
@@ -314,9 +316,12 @@ type LocalBackend struct {
|
||||
// Last ClientVersion received in MapResponse, guarded by mu.
|
||||
lastClientVersion *tailcfg.ClientVersion
|
||||
|
||||
// lastNotifiedTailFSShares keeps track of the last set of shares that we
|
||||
// lastNotifiedDriveShares keeps track of the last set of shares that we
|
||||
// notified about.
|
||||
lastNotifiedTailFSShares atomic.Pointer[views.SliceView[*tailfs.Share, tailfs.ShareView]]
|
||||
lastNotifiedDriveShares atomic.Pointer[views.SliceView[*drive.Share, drive.ShareView]]
|
||||
|
||||
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
||||
outgoingFiles map[string]*ipn.OutgoingFile
|
||||
}
|
||||
|
||||
type updateStatus struct {
|
||||
@@ -400,8 +405,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
if err != nil {
|
||||
log.Printf("error setting up sockstat logger: %v", err)
|
||||
}
|
||||
// Enable sockstats logs only on unstable builds
|
||||
if version.IsUnstableBuild() && b.sockstatLogger != nil {
|
||||
// Enable sockstats logs only on non-mobile unstable builds
|
||||
if version.IsUnstableBuild() && !version.IsMobile() && b.sockstatLogger != nil {
|
||||
b.sockstatLogger.SetLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@@ -437,12 +442,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
}
|
||||
|
||||
// initialize TailFS shares from saved state
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
// initialize Taildrive shares from saved state
|
||||
fs, ok := b.sys.DriveForRemote.GetOK()
|
||||
if ok {
|
||||
currentShares := b.pm.prefs.TailFSShares()
|
||||
currentShares := b.pm.prefs.DriveShares()
|
||||
if currentShares.Len() > 0 {
|
||||
var shares []*tailfs.Share
|
||||
var shares []*drive.Share
|
||||
for i := 0; i < currentShares.Len(); i++ {
|
||||
shares = append(shares, currentShares.At(i).AsStruct())
|
||||
}
|
||||
@@ -889,6 +894,14 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
ExitNode: p.StableID() != "" && p.StableID() == exitNodeID,
|
||||
SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(),
|
||||
Location: p.Hostinfo().Location(),
|
||||
Capabilities: p.Capabilities().AsSlice(),
|
||||
}
|
||||
if cm := p.CapMap(); cm.Len() > 0 {
|
||||
ps.CapMap = make(tailcfg.NodeCapMap, cm.Len())
|
||||
cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
|
||||
ps.CapMap[k] = v.AsSlice()
|
||||
return true
|
||||
})
|
||||
}
|
||||
peerStatusFromNode(ps, p)
|
||||
|
||||
@@ -1137,6 +1150,9 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
if setExitNodeID(prefs, st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if setExitDstFlowLogs(prefs) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if applySysPolicy(prefs) {
|
||||
prefsChanged = true
|
||||
}
|
||||
@@ -1322,6 +1338,15 @@ func applySysPolicy(prefs *ipn.Prefs) (anyChange bool) {
|
||||
return anyChange
|
||||
}
|
||||
|
||||
func setExitDstFlowLogs(prefs *ipn.Prefs) (anyChange bool) {
|
||||
fmt.Printf("set exit dst flow pref")
|
||||
if enable, err := syspolicy.GetBoolean(syspolicy.ExitDestinationFlowLogs, prefs.ExitDestinationFlowLogs); err == nil && prefs.ExitDestinationFlowLogs != enable {
|
||||
prefs.ExitDestinationFlowLogs = enable
|
||||
anyChange = true
|
||||
}
|
||||
return anyChange
|
||||
}
|
||||
|
||||
var _ controlclient.NetmapDeltaUpdater = (*LocalBackend)(nil)
|
||||
|
||||
// UpdateNetmapDelta implements controlclient.NetmapDeltaUpdater.
|
||||
@@ -2279,7 +2304,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialTailFSShares
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialDriveShares
|
||||
if mask&initialBits != 0 {
|
||||
ini = &ipn.Notify{Version: version.Long()}
|
||||
if mask&ipn.NotifyInitialState != 0 {
|
||||
@@ -2295,8 +2320,8 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
if mask&ipn.NotifyInitialNetMap != 0 {
|
||||
ini.NetMap = b.netMap
|
||||
}
|
||||
if mask&ipn.NotifyInitialTailFSShares != 0 && b.tailFSSharingEnabledLocked() {
|
||||
ini.TailFSShares = b.pm.prefs.TailFSShares()
|
||||
if mask&ipn.NotifyInitialDriveShares != 0 && b.driveSharingEnabledLocked() {
|
||||
ini.DriveShares = b.pm.prefs.DriveShares()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3234,6 +3259,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
setExitNodeID(newp, netMap)
|
||||
setExitDstFlowLogs(newp)
|
||||
// applySysPolicy does likewise so we can also ignore its return value.
|
||||
applySysPolicy(newp)
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
@@ -3369,8 +3395,8 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
return b.handleWebClientConn, opts
|
||||
}
|
||||
return b.HandleQuad100Port80Conn, opts
|
||||
case TailFSLocalPort:
|
||||
return b.handleTailFSConn, opts
|
||||
case DriveLocalPort:
|
||||
return b.handleDriveConn, opts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3404,9 +3430,9 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleTailFSConn(conn net.Conn) error {
|
||||
fs, ok := b.sys.TailFSForLocal.GetOK()
|
||||
if !ok || !b.TailFSAccessEnabled() {
|
||||
func (b *LocalBackend) handleDriveConn(conn net.Conn) error {
|
||||
fs, ok := b.sys.DriveForLocal.GetOK()
|
||||
if !ok || !b.DriveAccessEnabled() {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
@@ -3615,6 +3641,8 @@ func (b *LocalBackend) authReconfig() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.NetworkLogging.ExitDestinationFlowLogs = prefs.ExitDestinationFlowLogs()
|
||||
|
||||
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.ControlKnobs(), version.OS())
|
||||
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
|
||||
|
||||
@@ -4716,8 +4744,8 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
b.updateTailFSPeersLocked(nm)
|
||||
b.tailFSNotifyCurrentSharesLocked()
|
||||
b.updateDrivePeersLocked(nm)
|
||||
b.driveNotifyCurrentSharesLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
@@ -4744,19 +4772,123 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// tailFSTransport is an http.RoundTripper that uses the latest value of
|
||||
// driveTransport is an http.RoundTripper that uses the latest value of
|
||||
// b.Dialer().PeerAPITransport() for each round trip and imposes a short
|
||||
// dial timeout to avoid hanging on connecting to offline/unreachable hosts.
|
||||
type tailFSTransport struct {
|
||||
type driveTransport struct {
|
||||
b *LocalBackend
|
||||
}
|
||||
|
||||
func (t *tailFSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// responseBodyWrapper wraps an io.ReadCloser and stores
|
||||
// the number of bytesRead.
|
||||
type responseBodyWrapper struct {
|
||||
io.ReadCloser
|
||||
bytesRx int64
|
||||
bytesTx int64
|
||||
log logger.Logf
|
||||
method string
|
||||
statusCode int
|
||||
contentType string
|
||||
fileExtension string
|
||||
shareNodeKey string
|
||||
selfNodeKey string
|
||||
contentLength int64
|
||||
}
|
||||
|
||||
// logAccess logs the tailfs: access: log line. If the logger is nil,
|
||||
// the log will not be written.
|
||||
func (rbw *responseBodyWrapper) logAccess(err string) {
|
||||
if rbw.log == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Some operating systems create and copy lots of 0 length hidden files for
|
||||
// tracking various states. Omit these to keep logs from being too verbose.
|
||||
if rbw.contentLength > 0 {
|
||||
rbw.log("tailfs: access: %s from %s to %s: status-code=%d ext=%q content-type=%q content-length=%.f tx=%.f rx=%.f err=%q", rbw.method, rbw.selfNodeKey, rbw.shareNodeKey, rbw.statusCode, rbw.fileExtension, rbw.contentType, roundTraffic(rbw.contentLength), roundTraffic(rbw.bytesTx), roundTraffic(rbw.bytesRx), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface.
|
||||
func (rbw *responseBodyWrapper) Read(b []byte) (int, error) {
|
||||
n, err := rbw.ReadCloser.Read(b)
|
||||
rbw.bytesRx += int64(n)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
rbw.logAccess(err.Error())
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements the io.Close interface.
|
||||
func (rbw *responseBodyWrapper) Close() error {
|
||||
err := rbw.ReadCloser.Close()
|
||||
var errStr string
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
rbw.logAccess(errStr)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
bw := &requestBodyWrapper{}
|
||||
if req.Body != nil {
|
||||
bw.ReadCloser = req.Body
|
||||
req.Body = bw
|
||||
}
|
||||
|
||||
defer func() {
|
||||
contentType := "unknown"
|
||||
switch req.Method {
|
||||
case httpm.PUT:
|
||||
if ct := req.Header.Get("Content-Type"); ct != "" {
|
||||
contentType = ct
|
||||
}
|
||||
case httpm.GET:
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
contentType = ct
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
dt.b.mu.Lock()
|
||||
selfNodeKey := dt.b.netMap.SelfNode.Key().ShortString()
|
||||
dt.b.mu.Unlock()
|
||||
n, _, ok := dt.b.WhoIs(netip.MustParseAddrPort(req.URL.Host))
|
||||
shareNodeKey := "unknown"
|
||||
if ok {
|
||||
shareNodeKey = string(n.Key().ShortString())
|
||||
}
|
||||
|
||||
rbw := responseBodyWrapper{
|
||||
log: dt.b.logf,
|
||||
method: req.Method,
|
||||
bytesTx: int64(bw.bytesRead),
|
||||
selfNodeKey: selfNodeKey,
|
||||
shareNodeKey: shareNodeKey,
|
||||
contentType: contentType,
|
||||
contentLength: resp.ContentLength,
|
||||
fileExtension: parseDriveFileExtensionForLog(req.URL.Path),
|
||||
statusCode: resp.StatusCode,
|
||||
ReadCloser: resp.Body,
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
// in case of error response, just log immediately
|
||||
rbw.logAccess("")
|
||||
} else {
|
||||
resp.Body = &rbw
|
||||
}
|
||||
}()
|
||||
|
||||
// dialTimeout is fairly aggressive to avoid hangs on contacting offline or
|
||||
// unreachable hosts.
|
||||
dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this
|
||||
|
||||
tr := t.b.Dialer().PeerAPITransport().Clone()
|
||||
tr := dt.b.Dialer().PeerAPITransport().Clone()
|
||||
dialContext := tr.DialContext
|
||||
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
@@ -4766,6 +4898,32 @@ func (t *tailFSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return tr.RoundTrip(req)
|
||||
}
|
||||
|
||||
// roundTraffic rounds bytes. This is used to preserve user privacy within logs.
|
||||
func roundTraffic(bytes int64) float64 {
|
||||
var x float64
|
||||
switch {
|
||||
case bytes <= 5:
|
||||
return float64(bytes)
|
||||
case bytes < 1000:
|
||||
x = 10
|
||||
case bytes < 10_000:
|
||||
x = 100
|
||||
case bytes < 100_000:
|
||||
x = 1000
|
||||
case bytes < 1_000_000:
|
||||
x = 10_000
|
||||
case bytes < 10_000_000:
|
||||
x = 100_000
|
||||
case bytes < 100_000_000:
|
||||
x = 1_000_000
|
||||
case bytes < 1_000_000_000:
|
||||
x = 10_000_000
|
||||
default:
|
||||
x = 100_000_000
|
||||
}
|
||||
return math.Round(float64(bytes)/x) * x
|
||||
}
|
||||
|
||||
// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
|
||||
// capabilities in the provided NetMap.
|
||||
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
|
||||
|
||||
@@ -24,13 +24,13 @@ import (
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -736,6 +736,100 @@ func TestStatusWithoutPeers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusPeerCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []tailcfg.NodeView
|
||||
expectedPeerCapabilities map[tailcfg.StableNodeID][]tailcfg.NodeCapability
|
||||
expectedPeerCapMap map[tailcfg.StableNodeID]tailcfg.NodeCapMap
|
||||
}{
|
||||
{
|
||||
name: "peers-with-capabilities",
|
||||
peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 1,
|
||||
StableID: "foo",
|
||||
IsWireGuardOnly: true,
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilitySSH},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilitySSH: nil,
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "bar",
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityAdmin},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilityAdmin: {`{"test": "true}`},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
expectedPeerCapabilities: map[tailcfg.StableNodeID][]tailcfg.NodeCapability{
|
||||
tailcfg.StableNodeID("foo"): {tailcfg.CapabilitySSH},
|
||||
tailcfg.StableNodeID("bar"): {tailcfg.CapabilityAdmin},
|
||||
},
|
||||
expectedPeerCapMap: map[tailcfg.StableNodeID]tailcfg.NodeCapMap{
|
||||
tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilitySSH: nil,
|
||||
}),
|
||||
tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.CapabilityAdmin: {`{"test": "true}`},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "peers-without-capabilities",
|
||||
peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 1,
|
||||
StableID: "foo",
|
||||
IsWireGuardOnly: true,
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "bar",
|
||||
Hostinfo: (&tailcfg.Hostinfo{}).View(),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
}
|
||||
b := newTestLocalBackend(t)
|
||||
|
||||
var cc *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc = newClient(t, opts)
|
||||
|
||||
t.Logf("ccGen: new mockControl.")
|
||||
cc.called("New")
|
||||
return cc, nil
|
||||
})
|
||||
b.Start(ipn.Options{})
|
||||
b.Login(nil)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b.setNetMapLocked(&netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
MachineAuthorized: true,
|
||||
Addresses: ipps("100.101.101.101"),
|
||||
}).View(),
|
||||
Peers: tt.peers,
|
||||
})
|
||||
got := b.Status()
|
||||
for _, peer := range got.Peer {
|
||||
if !reflect.DeepEqual(peer.Capabilities, tt.expectedPeerCapabilities[peer.ID]) {
|
||||
t.Errorf("peer capabilities: expected %v got %v", tt.expectedPeerCapabilities, peer.Capabilities)
|
||||
}
|
||||
if !reflect.DeepEqual(peer.CapMap, tt.expectedPeerCapMap[peer.ID]) {
|
||||
t.Errorf("peer capmap: expected %v got %v", tt.expectedPeerCapMap, peer.CapMap)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// legacyBackend was the interface between Tailscale frontends
|
||||
// (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale
|
||||
// backend (e.g. cmd/tailscaled) running on the same machine.
|
||||
@@ -2201,12 +2295,12 @@ func TestTCPHandlerForDst(t *testing.T) {
|
||||
intercept: false,
|
||||
},
|
||||
{
|
||||
desc: "intercept port 8080 (TailFS) on quad100 IPv4",
|
||||
desc: "intercept port 8080 (Taildrive) on quad100 IPv4",
|
||||
dst: "100.100.100.100:8080",
|
||||
intercept: true,
|
||||
},
|
||||
{
|
||||
desc: "intercept port 8080 (TailFS) on quad100 IPv6",
|
||||
desc: "intercept port 8080 (Taildrive) on quad100 IPv6",
|
||||
dst: "[fd7a:115c:a1e0::53]:8080",
|
||||
intercept: true,
|
||||
},
|
||||
@@ -2246,24 +2340,24 @@ func TestTCPHandlerForDst(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailFSManageShares(t *testing.T) {
|
||||
func TestDriveManageShares(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disabled bool
|
||||
existing []*tailfs.Share
|
||||
add *tailfs.Share
|
||||
existing []*drive.Share
|
||||
add *drive.Share
|
||||
remove string
|
||||
rename [2]string
|
||||
expect any
|
||||
}{
|
||||
{
|
||||
name: "append",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " E "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " E "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
{Name: "e"},
|
||||
@@ -2271,12 +2365,12 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "prepend",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " A "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " A "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
@@ -2284,12 +2378,12 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "insert",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " C "},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " C "},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
{Name: "d"},
|
||||
@@ -2297,43 +2391,43 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "replace",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "b", Path: "i"},
|
||||
{Name: "d"},
|
||||
},
|
||||
add: &tailfs.Share{Name: " B ", Path: "ii"},
|
||||
expect: []*tailfs.Share{
|
||||
add: &drive.Share{Name: " B ", Path: "ii"},
|
||||
expect: []*drive.Share{
|
||||
{Name: "b", Path: "ii"},
|
||||
{Name: "d"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add_bad_name",
|
||||
add: &tailfs.Share{Name: "$"},
|
||||
add: &drive.Share{Name: "$"},
|
||||
expect: ErrInvalidShareName,
|
||||
},
|
||||
{
|
||||
name: "add_disabled",
|
||||
disabled: true,
|
||||
add: &tailfs.Share{Name: "a"},
|
||||
expect: ErrTailFSNotEnabled,
|
||||
add: &drive.Share{Name: "a"},
|
||||
expect: ErrDriveNotEnabled,
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
},
|
||||
remove: "b",
|
||||
expect: []*tailfs.Share{
|
||||
expect: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove_non_existing",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
@@ -2345,23 +2439,23 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
name: "remove_disabled",
|
||||
disabled: true,
|
||||
remove: "b",
|
||||
expect: ErrTailFSNotEnabled,
|
||||
expect: ErrDriveNotEnabled,
|
||||
},
|
||||
{
|
||||
name: "rename",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
rename: [2]string{"a", " C "},
|
||||
expect: []*tailfs.Share{
|
||||
expect: []*drive.Share{
|
||||
{Name: "b"},
|
||||
{Name: "c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rename_not_exist",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
@@ -2370,7 +2464,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "rename_exists",
|
||||
existing: []*tailfs.Share{
|
||||
existing: []*drive.Share{
|
||||
{Name: "a"},
|
||||
{Name: "b"},
|
||||
},
|
||||
@@ -2386,13 +2480,13 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
name: "rename_disabled",
|
||||
disabled: true,
|
||||
rename: [2]string{"a", "c"},
|
||||
expect: ErrTailFSNotEnabled,
|
||||
expect: ErrDriveNotEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
tailfs.DisallowShareAs = true
|
||||
drive.DisallowShareAs = true
|
||||
t.Cleanup(func() {
|
||||
tailfs.DisallowShareAs = false
|
||||
drive.DisallowShareAs = false
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -2400,20 +2494,20 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
b.mu.Lock()
|
||||
if tt.existing != nil {
|
||||
b.tailFSSetSharesLocked(tt.existing)
|
||||
b.driveSetSharesLocked(tt.existing)
|
||||
}
|
||||
if !tt.disabled {
|
||||
self := b.netMap.SelfNode.AsStruct()
|
||||
self.CapMap = tailcfg.NodeCapMap{tailcfg.NodeAttrsTailFSShare: nil}
|
||||
b.netMap.SelfNode = self.View()
|
||||
b.sys.Set(tailfsimpl.NewFileSystemForRemote(b.logf))
|
||||
b.sys.Set(driveimpl.NewFileSystemForRemote(b.logf))
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result := make(chan views.SliceView[*tailfs.Share, tailfs.ShareView], 1)
|
||||
result := make(chan views.SliceView[*drive.Share, drive.ShareView], 1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
@@ -2423,7 +2517,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
func() { wg.Done() },
|
||||
func(n *ipn.Notify) bool {
|
||||
select {
|
||||
case result <- n.TailFSShares:
|
||||
case result <- n.DriveShares:
|
||||
default:
|
||||
//
|
||||
}
|
||||
@@ -2435,11 +2529,11 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
var err error
|
||||
switch {
|
||||
case tt.add != nil:
|
||||
err = b.TailFSSetShare(tt.add)
|
||||
err = b.DriveSetShare(tt.add)
|
||||
case tt.remove != "":
|
||||
err = b.TailFSRemoveShare(tt.remove)
|
||||
err = b.DriveRemoveShare(tt.remove)
|
||||
default:
|
||||
err = b.TailFSRenameShare(tt.rename[0], tt.rename[1])
|
||||
err = b.DriveRenameShare(tt.rename[0], tt.rename[1])
|
||||
}
|
||||
|
||||
switch e := tt.expect.(type) {
|
||||
@@ -2447,7 +2541,7 @@ func TestTailFSManageShares(t *testing.T) {
|
||||
if !errors.Is(err, e) {
|
||||
t.Errorf("expected error, want: %v got: %v", e, err)
|
||||
}
|
||||
case []*tailfs.Share:
|
||||
case []*drive.Share:
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
} else {
|
||||
@@ -2507,3 +2601,29 @@ func TestValidPopBrowserURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTraffic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bytes int64
|
||||
want float64
|
||||
}{
|
||||
{name: "under 5 bytes", bytes: 4, want: 4},
|
||||
{name: "under 1000 bytes", bytes: 987, want: 990},
|
||||
{name: "under 10_000 bytes", bytes: 8875, want: 8900},
|
||||
{name: "under 100_000 bytes", bytes: 77777, want: 78000},
|
||||
{name: "under 1_000_000 bytes", bytes: 666523, want: 670000},
|
||||
{name: "under 10_000_000 bytes", bytes: 22556677, want: 23000000},
|
||||
{name: "under 1_000_000_000 bytes", bytes: 1234234234, want: 1200000000},
|
||||
{name: "under 1_000_000_000 bytes", bytes: 123423423499, want: 123400000000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if result := roundTraffic(tt.bytes); result != tt.want {
|
||||
t.Errorf("unexpected rounding got %v want %v", result, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -38,10 +40,10 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -323,7 +325,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, tailFSPrefix) {
|
||||
h.handleServeTailFS(w, r)
|
||||
h.handleServeDrive(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
@@ -1104,37 +1106,120 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeTailFS(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.TailFSSharingEnabled() {
|
||||
// httpResponseWrapper wraps an http.ResponseWrite and
|
||||
// stores the status code and content length.
|
||||
type httpResponseWrapper struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
contentLength int64
|
||||
}
|
||||
|
||||
// WriteHeader implements the WriteHeader interface.
|
||||
func (hrw *httpResponseWrapper) WriteHeader(status int) {
|
||||
hrw.statusCode = status
|
||||
hrw.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write implements the Write interface.
|
||||
func (hrw *httpResponseWrapper) Write(b []byte) (int, error) {
|
||||
n, err := hrw.ResponseWriter.Write(b)
|
||||
hrw.contentLength += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// requestBodyWrapper wraps an io.ReadCloser and stores
|
||||
// the number of bytesRead.
|
||||
type requestBodyWrapper struct {
|
||||
io.ReadCloser
|
||||
bytesRead int64
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface.
|
||||
func (rbw *requestBodyWrapper) Read(b []byte) (int, error) {
|
||||
n, err := rbw.ReadCloser.Read(b)
|
||||
rbw.bytesRead += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.DriveSharingEnabled() {
|
||||
h.logf("tailfs: not enabled")
|
||||
http.Error(w, "tailfs not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
capsMap := h.peerCaps()
|
||||
tailfsCaps, ok := capsMap[tailcfg.PeerCapabilityTailFS]
|
||||
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTailFS]
|
||||
if !ok {
|
||||
h.logf("tailfs: not permitted")
|
||||
http.Error(w, "tailfs not permitted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
rawPerms := make([][]byte, 0, len(tailfsCaps))
|
||||
for _, cap := range tailfsCaps {
|
||||
rawPerms := make([][]byte, 0, len(driveCaps))
|
||||
for _, cap := range driveCaps {
|
||||
rawPerms = append(rawPerms, []byte(cap))
|
||||
}
|
||||
|
||||
p, err := tailfs.ParsePermissions(rawPerms)
|
||||
p, err := drive.ParsePermissions(rawPerms)
|
||||
if err != nil {
|
||||
h.logf("tailfs: error parsing permissions: %w", err.Error())
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fs, ok := h.ps.b.sys.TailFSForRemote.GetOK()
|
||||
fs, ok := h.ps.b.sys.DriveForRemote.GetOK()
|
||||
if !ok {
|
||||
http.Error(w, "tailfs not enabled", http.StatusNotFound)
|
||||
h.logf("tailfs: not supported on platform")
|
||||
http.Error(w, "tailfs not supported on platform", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
wr := &httpResponseWrapper{
|
||||
ResponseWriter: w,
|
||||
}
|
||||
bw := &requestBodyWrapper{
|
||||
ReadCloser: r.Body,
|
||||
}
|
||||
r.Body = bw
|
||||
|
||||
if r.Method == httpm.PUT || r.Method == httpm.GET {
|
||||
defer func() {
|
||||
switch wr.statusCode {
|
||||
case 304:
|
||||
// 304s are particularly chatty so skip logging.
|
||||
default:
|
||||
contentType := "unknown"
|
||||
if ct := wr.Header().Get("Content-Type"); ct != "" {
|
||||
contentType = ct
|
||||
}
|
||||
|
||||
h.logf("tailfs: share: %s from %s to %s: status-code=%d ext=%q content-type=%q tx=%.f rx=%.f", r.Method, h.peerNode.Key().ShortString(), h.selfNode.Key().ShortString(), wr.statusCode, parseDriveFileExtensionForLog(r.URL.Path), contentType, roundTraffic(wr.contentLength), roundTraffic(bw.bytesRead))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, tailFSPrefix)
|
||||
fs.ServeHTTPWithPerms(p, w, r)
|
||||
fs.ServeHTTPWithPerms(p, wr, r)
|
||||
}
|
||||
|
||||
// parseDriveFileExtensionForLog parses the file extension, if available.
|
||||
// If a file extension is not present or parsable, the file extension is
|
||||
// set to "unknown". If the file extension contains a double quote, it is
|
||||
// replaced with "removed".
|
||||
// All whitespace is removed from a parsed file extension.
|
||||
// File extensions including the leading ., e.g. ".gif".
|
||||
func parseDriveFileExtensionForLog(path string) string {
|
||||
fileExt := "unknown"
|
||||
if fe := filepath.Ext(path); fe != "" {
|
||||
if strings.Contains(fe, "\"") {
|
||||
// Do not log include file extensions with quotes within them.
|
||||
return "removed"
|
||||
}
|
||||
// Remove white space from user defined inputs.
|
||||
fileExt = strings.ReplaceAll(fe, " ", "")
|
||||
}
|
||||
|
||||
return fileExt
|
||||
}
|
||||
|
||||
// newFakePeerAPIListener creates a new net.Listener that acts like
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/ipn"
|
||||
@@ -62,7 +64,7 @@ type serveHTTPContext struct {
|
||||
//
|
||||
// This is not used in userspace-networking mode.
|
||||
//
|
||||
// localListener is used by tailscale serve (TCP only), the built-in web client and tailfs.
|
||||
// localListener is used by tailscale serve (TCP only), the built-in web client and Taildrive.
|
||||
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
|
||||
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
|
||||
// so we need to be in the kernel's listening/routing tables.
|
||||
@@ -717,12 +719,27 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
|
||||
// Only currently set for nodes with user identities.
|
||||
return
|
||||
}
|
||||
r.Out.Header.Set("Tailscale-User-Login", user.LoginName)
|
||||
r.Out.Header.Set("Tailscale-User-Name", user.DisplayName)
|
||||
r.Out.Header.Set("Tailscale-User-Login", encTailscaleHeaderValue(user.LoginName))
|
||||
r.Out.Header.Set("Tailscale-User-Name", encTailscaleHeaderValue(user.DisplayName))
|
||||
r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL)
|
||||
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
|
||||
}
|
||||
|
||||
// encTailscaleHeaderValue cleans or encodes as necessary v, to be suitable in
|
||||
// an HTTP header value. See
|
||||
// https://github.com/tailscale/tailscale/issues/11603.
|
||||
//
|
||||
// If v is not a valid UTF-8 string, it returns an empty string.
|
||||
// If v is a valid ASCII string, it returns v unmodified.
|
||||
// If v is a valid UTF-8 string with non-ASCII characters, it returns a
|
||||
// RFC 2047 Q-encoded string.
|
||||
func encTailscaleHeaderValue(v string) string {
|
||||
if !utf8.ValidString(v) {
|
||||
return ""
|
||||
}
|
||||
return mime.QEncoding.Encode("utf-8", v)
|
||||
}
|
||||
|
||||
// serveWebHandler is an http.HandlerFunc that maps incoming requests to the
|
||||
// correct *http.
|
||||
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -823,3 +823,21 @@ func Test_isGRPCContentType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncTailscaleHeaderValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"Alice Smith", "Alice Smith"},
|
||||
{"Bad\xffUTF-8", ""},
|
||||
{"Krūmiņa", "=?utf-8?q?Kr=C5=ABmi=C5=86a?="},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := encTailscaleHeaderValue(tt.in)
|
||||
if got != tt.want {
|
||||
t.Errorf("encTailscaleHeaderValue(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
ipn/ipnlocal/taildrop.go
Normal file
35
ipn/ipnlocal/taildrop.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
// UpdateOutgoingFiles updates b.outgoingFiles to reflect the given updates and
|
||||
// sends an ipn.Notify with the full list of outgoingFiles.
|
||||
func (b *LocalBackend) UpdateOutgoingFiles(updates map[string]*ipn.OutgoingFile) {
|
||||
b.mu.Lock()
|
||||
if b.outgoingFiles == nil {
|
||||
b.outgoingFiles = make(map[string]*ipn.OutgoingFile, len(updates))
|
||||
}
|
||||
maps.Copy(b.outgoingFiles, updates)
|
||||
outgoingFiles := make([]*ipn.OutgoingFile, 0, len(b.outgoingFiles))
|
||||
for _, file := range b.outgoingFiles {
|
||||
outgoingFiles = append(outgoingFiles, file)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
slices.SortFunc(outgoingFiles, func(a, b *ipn.OutgoingFile) int {
|
||||
t := a.Started.Compare(b.Started)
|
||||
if t != 0 {
|
||||
return t
|
||||
}
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
b.send(ipn.Notify{OutgoingFiles: outgoingFiles})
|
||||
}
|
||||
@@ -496,6 +496,12 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if t := st.KeyExpiry; t != nil {
|
||||
e.KeyExpiry = ptr.To(*t)
|
||||
}
|
||||
if v := st.CapMap; v != nil {
|
||||
e.CapMap = v
|
||||
}
|
||||
if v := st.Capabilities; v != nil {
|
||||
e.Capabilities = v
|
||||
}
|
||||
e.Location = st.Location
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -28,8 +31,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -43,7 +48,6 @@ import (
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -57,6 +61,7 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/osdiag"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/util/progresstracking"
|
||||
"tailscale.com/util/rands"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
@@ -111,7 +116,7 @@ var handler = map[string]localAPIHandler{
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"set-gui-visible": (*Handler).serveSetGUIVisible,
|
||||
"tailfs/fileserver-address": (*Handler).serveTailFSFileServerAddr,
|
||||
"tailfs/fileserver-address": (*Handler).serveDriveServerAddr,
|
||||
"tailfs/shares": (*Handler).serveShares,
|
||||
"start": (*Handler).serveStart,
|
||||
"status": (*Handler).serveStatus,
|
||||
@@ -1529,9 +1534,16 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||
// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/)
|
||||
// directly, as the Windows GUI always runs in tun mode anyway.
|
||||
//
|
||||
// In addition to single file PUTs, this endpoint accepts multipart file
|
||||
// POSTS encoded as multipart/form-data.The first part should be an
|
||||
// application/json file that contains a manifest consisting of a JSON array of
|
||||
// OutgoingFiles which wecan use for tracking progress even before reading the
|
||||
// file parts.
|
||||
//
|
||||
// URL format:
|
||||
//
|
||||
// - PUT /localapi/v0/file-put/:stableID/:escaped-filename
|
||||
// - POST /localapi/v0/file-put/:stableID
|
||||
func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
metricFilePutCalls.Add(1)
|
||||
|
||||
@@ -1539,10 +1551,12 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
|
||||
if r.Method != "PUT" && r.Method != "POST" {
|
||||
http.Error(w, "want PUT to put file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fts, err := h.b.FileTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -1554,16 +1568,22 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "misconfigured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
stableIDStr, filenameEscaped, ok := strings.Cut(upath, "/")
|
||||
if !ok {
|
||||
http.Error(w, "bogus URL", http.StatusBadRequest)
|
||||
return
|
||||
var peerIDStr, filenameEscaped string
|
||||
if r.Method == "PUT" {
|
||||
ok := false
|
||||
peerIDStr, filenameEscaped, ok = strings.Cut(upath, "/")
|
||||
if !ok {
|
||||
http.Error(w, "bogus URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
peerIDStr = upath
|
||||
}
|
||||
stableID := tailcfg.StableNodeID(stableIDStr)
|
||||
peerID := tailcfg.StableNodeID(peerIDStr)
|
||||
|
||||
var ft *apitype.FileTarget
|
||||
for _, x := range fts {
|
||||
if x.Node.StableID == stableID {
|
||||
if x.Node.StableID == peerID {
|
||||
ft = x
|
||||
break
|
||||
}
|
||||
@@ -1578,20 +1598,175 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Periodically report progress of outgoing files.
|
||||
outgoingFiles := make(map[string]*ipn.OutgoingFile)
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
progressUpdates := make(chan ipn.OutgoingFile)
|
||||
defer close(progressUpdates)
|
||||
|
||||
go func() {
|
||||
defer t.Stop()
|
||||
defer h.b.UpdateOutgoingFiles(outgoingFiles)
|
||||
for {
|
||||
select {
|
||||
case u, ok := <-progressUpdates:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
outgoingFiles[u.ID] = &u
|
||||
case <-t.C:
|
||||
h.b.UpdateOutgoingFiles(outgoingFiles)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
file := ipn.OutgoingFile{
|
||||
ID: uuid.Must(uuid.NewRandom()).String(),
|
||||
PeerID: peerID,
|
||||
Name: filenameEscaped,
|
||||
DeclaredSize: r.ContentLength,
|
||||
}
|
||||
h.singleFilePut(r.Context(), progressUpdates, w, r.Body, dstURL, file)
|
||||
case "POST":
|
||||
h.multiFilePost(progressUpdates, w, r, peerID, dstURL)
|
||||
default:
|
||||
http.Error(w, "want PUT to put file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) multiFilePost(progressUpdates chan (ipn.OutgoingFile), w http.ResponseWriter, r *http.Request, peerID tailcfg.StableNodeID, dstURL *url.URL) {
|
||||
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid Content-Type for multipart POST: %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ww := &multiFilePostResponseWriter{}
|
||||
defer func() {
|
||||
if err := ww.Flush(w); err != nil {
|
||||
h.logf("error: multiFilePostResponseWriter.Flush(): %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
outgoingFilesByName := make(map[string]ipn.OutgoingFile)
|
||||
first := true
|
||||
mr := multipart.NewReader(r.Body, params["boundary"])
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
// No more parts.
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(ww, fmt.Sprintf("failed to decode multipart/form-data: %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if first {
|
||||
first = false
|
||||
if part.Header.Get("Content-Type") != "application/json" {
|
||||
http.Error(ww, "first MIME part must be a JSON map of filename -> size", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var manifest []ipn.OutgoingFile
|
||||
err := json.NewDecoder(part).Decode(&manifest)
|
||||
if err != nil {
|
||||
http.Error(ww, fmt.Sprintf("invalid manifest: %s", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range manifest {
|
||||
outgoingFilesByName[file.Name] = file
|
||||
progressUpdates <- file
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !h.singleFilePut(r.Context(), progressUpdates, ww, part, dstURL, outgoingFilesByName[part.FileName()]) {
|
||||
return
|
||||
}
|
||||
|
||||
if ww.statusCode >= 400 {
|
||||
// put failed, stop immediately
|
||||
h.logf("error: singleFilePut: failed with status %d", ww.statusCode)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multiFilePostResponseWriter is a buffering http.ResponseWriter that can be
|
||||
// reused across multiple singleFilePut calls and then flushed to the client
|
||||
// when all files have been PUT.
|
||||
type multiFilePostResponseWriter struct {
|
||||
header http.Header
|
||||
statusCode int
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (ww *multiFilePostResponseWriter) Header() http.Header {
|
||||
if ww.header == nil {
|
||||
ww.header = make(http.Header)
|
||||
}
|
||||
return ww.header
|
||||
}
|
||||
|
||||
func (ww *multiFilePostResponseWriter) WriteHeader(statusCode int) {
|
||||
ww.statusCode = statusCode
|
||||
}
|
||||
|
||||
func (ww *multiFilePostResponseWriter) Write(p []byte) (int, error) {
|
||||
if ww.body == nil {
|
||||
ww.body = bytes.NewBuffer(nil)
|
||||
}
|
||||
return ww.body.Write(p)
|
||||
}
|
||||
|
||||
func (ww *multiFilePostResponseWriter) Flush(w http.ResponseWriter) error {
|
||||
maps.Copy(w.Header(), ww.Header())
|
||||
w.WriteHeader(ww.statusCode)
|
||||
_, err := io.Copy(w, ww.body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) singleFilePut(
|
||||
ctx context.Context,
|
||||
progressUpdates chan (ipn.OutgoingFile),
|
||||
w http.ResponseWriter,
|
||||
body io.Reader,
|
||||
dstURL *url.URL,
|
||||
outgoingFile ipn.OutgoingFile,
|
||||
) bool {
|
||||
outgoingFile.Started = time.Now()
|
||||
body = progresstracking.NewReader(body, 1*time.Second, func(n int, err error) {
|
||||
outgoingFile.Sent = int64(n)
|
||||
progressUpdates <- outgoingFile
|
||||
})
|
||||
|
||||
fail := func() {
|
||||
outgoingFile.Finished = true
|
||||
outgoingFile.Succeeded = false
|
||||
progressUpdates <- outgoingFile
|
||||
}
|
||||
|
||||
// Before we PUT a file we check to see if there are any existing partial file and if so,
|
||||
// we resume the upload from where we left off by sending the remaining file instead of
|
||||
// the full file.
|
||||
var offset int64
|
||||
var resumeDuration time.Duration
|
||||
remainingBody := io.Reader(r.Body)
|
||||
remainingBody := io.Reader(body)
|
||||
client := &http.Client{
|
||||
Transport: h.b.Dialer().PeerAPITransport(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), "GET", dstURL.String()+"/v0/put/"+filenameEscaped, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", dstURL.String()+"/v0/put/"+outgoingFile.Name, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus peer URL", http.StatusInternalServerError)
|
||||
return
|
||||
fail()
|
||||
return false
|
||||
}
|
||||
switch resp, err := client.Do(req); {
|
||||
case err != nil:
|
||||
@@ -1603,7 +1778,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
resumeStart := time.Now()
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
offset, remainingBody, err = taildrop.ResumeReader(r.Body, func() (out taildrop.BlockChecksum, err error) {
|
||||
offset, remainingBody, err = taildrop.ResumeReader(body, func() (out taildrop.BlockChecksum, err error) {
|
||||
err = dec.Decode(&out)
|
||||
return out, err
|
||||
})
|
||||
@@ -1613,12 +1788,13 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
resumeDuration = time.Since(resumeStart).Round(time.Millisecond)
|
||||
}
|
||||
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, remainingBody)
|
||||
outReq, err := http.NewRequestWithContext(ctx, "PUT", "http://peer/v0/put/"+outgoingFile.Name, remainingBody)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus outreq", http.StatusInternalServerError)
|
||||
return
|
||||
fail()
|
||||
return false
|
||||
}
|
||||
outReq.ContentLength = r.ContentLength
|
||||
outReq.ContentLength = outgoingFile.DeclaredSize
|
||||
if offset > 0 {
|
||||
h.logf("resuming put at offset %d after %v", offset, resumeDuration)
|
||||
rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: 0}})
|
||||
@@ -1631,6 +1807,12 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
rp := httputil.NewSingleHostReverseProxy(dstURL)
|
||||
rp.Transport = h.b.Dialer().PeerAPITransport()
|
||||
rp.ServeHTTP(w, outReq)
|
||||
|
||||
outgoingFile.Finished = true
|
||||
outgoingFile.Succeeded = true
|
||||
progressUpdates <- outgoingFile
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2553,8 +2735,8 @@ func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(ups)
|
||||
}
|
||||
|
||||
// serveTailFSFileServerAddr handles updates of the tailfs file server address.
|
||||
func (h *Handler) serveTailFSFileServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||
// serveDriveServerAddr handles updates of the Taildrive file server address.
|
||||
func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -2566,24 +2748,24 @@ func (h *Handler) serveTailFSFileServerAddr(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
h.b.TailFSSetFileServerAddr(string(b))
|
||||
h.b.DriveSetServerAddr(string(b))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// serveShares handles the management of tailfs shares.
|
||||
// serveShares handles the management of Taildrive shares.
|
||||
//
|
||||
// PUT - adds or updates an existing share
|
||||
// DELETE - removes a share
|
||||
// GET - gets a list of all shares, sorted by name
|
||||
// POST - renames an existing share
|
||||
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.b.TailFSSharingEnabled() {
|
||||
if !h.b.DriveSharingEnabled() {
|
||||
http.Error(w, `tailfs sharing not enabled, please add the attribute "tailfs:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
var share tailfs.Share
|
||||
var share drive.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -2599,7 +2781,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not a directory", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if tailfs.AllowShareAs() {
|
||||
if drive.AllowShareAs() {
|
||||
// share as the connected user
|
||||
username, err := h.getUsername()
|
||||
if err != nil {
|
||||
@@ -2608,7 +2790,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
share.As = username
|
||||
}
|
||||
err = h.b.TailFSSetShare(&share)
|
||||
err = h.b.DriveSetShare(&share)
|
||||
if err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrInvalidShareName) {
|
||||
http.Error(w, "invalid share name", http.StatusBadRequest)
|
||||
@@ -2624,7 +2806,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.TailFSRemoveShare(string(b))
|
||||
err = h.b.DriveRemoveShare(string(b))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
@@ -2641,7 +2823,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.TailFSRenameShare(names[0], names[1])
|
||||
err = h.b.DriveRenameShare(names[0], names[1])
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
@@ -2660,7 +2842,7 @@ func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "GET":
|
||||
shares := h.b.TailFSGetShares()
|
||||
shares := h.b.DriveGetShares()
|
||||
err := json.NewEncoder(w).Encode(shares)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
70
ipn/prefs.go
70
ipn/prefs.go
@@ -18,11 +18,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
@@ -109,6 +109,9 @@ type Prefs struct {
|
||||
// routed directly or via the exit node.
|
||||
ExitNodeAllowLANAccess bool
|
||||
|
||||
// ExitDestinationFlowLogs indicates whether exit node destination is recorded in network flow logs.
|
||||
ExitDestinationFlowLogs bool
|
||||
|
||||
// CorpDNS specifies whether to install the Tailscale network's
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
@@ -225,9 +228,9 @@ type Prefs struct {
|
||||
// Linux-only.
|
||||
NetfilterKind string
|
||||
|
||||
// TailFSShares are the configured TailFSShares, stored in increasing order
|
||||
// DriveShares are the configured DriveShares, stored in increasing order
|
||||
// by name.
|
||||
TailFSShares []*tailfs.Share
|
||||
DriveShares []*drive.Share
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
@@ -274,33 +277,34 @@ type AppConnectorPrefs struct {
|
||||
type MaskedPrefs struct {
|
||||
Prefs
|
||||
|
||||
ControlURLSet bool `json:",omitempty"`
|
||||
RouteAllSet bool `json:",omitempty"`
|
||||
AllowSingleHostsSet bool `json:",omitempty"`
|
||||
ExitNodeIDSet bool `json:",omitempty"`
|
||||
ExitNodeIPSet bool `json:",omitempty"`
|
||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||
CorpDNSSet bool `json:",omitempty"`
|
||||
RunSSHSet bool `json:",omitempty"`
|
||||
RunWebClientSet bool `json:",omitempty"`
|
||||
WantRunningSet bool `json:",omitempty"`
|
||||
LoggedOutSet bool `json:",omitempty"`
|
||||
ShieldsUpSet bool `json:",omitempty"`
|
||||
AdvertiseTagsSet bool `json:",omitempty"`
|
||||
HostnameSet bool `json:",omitempty"`
|
||||
NotepadURLsSet bool `json:",omitempty"`
|
||||
ForceDaemonSet bool `json:",omitempty"`
|
||||
EggSet bool `json:",omitempty"`
|
||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||
NoSNATSet bool `json:",omitempty"`
|
||||
NetfilterModeSet bool `json:",omitempty"`
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
ProfileNameSet bool `json:",omitempty"`
|
||||
AutoUpdateSet AutoUpdatePrefsMask `json:",omitempty"`
|
||||
AppConnectorSet bool `json:",omitempty"`
|
||||
PostureCheckingSet bool `json:",omitempty"`
|
||||
NetfilterKindSet bool `json:",omitempty"`
|
||||
TailFSSharesSet bool `json:",omitempty"`
|
||||
ControlURLSet bool `json:",omitempty"`
|
||||
RouteAllSet bool `json:",omitempty"`
|
||||
AllowSingleHostsSet bool `json:",omitempty"`
|
||||
ExitDestinationFlowLogsSet bool `json:",omitempty"`
|
||||
ExitNodeIDSet bool `json:",omitempty"`
|
||||
ExitNodeIPSet bool `json:",omitempty"`
|
||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||
CorpDNSSet bool `json:",omitempty"`
|
||||
RunSSHSet bool `json:",omitempty"`
|
||||
RunWebClientSet bool `json:",omitempty"`
|
||||
WantRunningSet bool `json:",omitempty"`
|
||||
LoggedOutSet bool `json:",omitempty"`
|
||||
ShieldsUpSet bool `json:",omitempty"`
|
||||
AdvertiseTagsSet bool `json:",omitempty"`
|
||||
HostnameSet bool `json:",omitempty"`
|
||||
NotepadURLsSet bool `json:",omitempty"`
|
||||
ForceDaemonSet bool `json:",omitempty"`
|
||||
EggSet bool `json:",omitempty"`
|
||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||
NoSNATSet bool `json:",omitempty"`
|
||||
NetfilterModeSet bool `json:",omitempty"`
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
ProfileNameSet bool `json:",omitempty"`
|
||||
AutoUpdateSet AutoUpdatePrefsMask `json:",omitempty"`
|
||||
AppConnectorSet bool `json:",omitempty"`
|
||||
PostureCheckingSet bool `json:",omitempty"`
|
||||
NetfilterKindSet bool `json:",omitempty"`
|
||||
DriveSharesSet bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type AutoUpdatePrefsMask struct {
|
||||
@@ -475,6 +479,9 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if p.ShieldsUp {
|
||||
sb.WriteString("shields=true ")
|
||||
}
|
||||
if p.ExitDestinationFlowLogs {
|
||||
sb.WriteString("exitdestinationflowlogs=true ")
|
||||
}
|
||||
if p.ExitNodeIP.IsValid() {
|
||||
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
|
||||
} else if !p.ExitNodeID.IsZero() {
|
||||
@@ -545,6 +552,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.ExitNodeID == p2.ExitNodeID &&
|
||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||
p.ExitDestinationFlowLogs == p2.ExitDestinationFlowLogs &&
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.RunSSH == p2.RunSSH &&
|
||||
p.RunWebClient == p2.RunWebClient &&
|
||||
@@ -564,7 +572,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.AutoUpdate.Equals(p2.AutoUpdate) &&
|
||||
p.AppConnector == p2.AppConnector &&
|
||||
p.PostureChecking == p2.PostureChecking &&
|
||||
slices.EqualFunc(p.TailFSShares, p2.TailFSShares, tailfs.SharesEqual) &&
|
||||
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
|
||||
p.NetfilterKind == p2.NetfilterKind
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"AppConnector",
|
||||
"PostureChecking",
|
||||
"NetfilterKind",
|
||||
"TailFSShares",
|
||||
"DriveShares",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) {
|
||||
|
||||
@@ -10,6 +10,8 @@ Resource Types:
|
||||
|
||||
- [Connector](#connector)
|
||||
|
||||
- [DNSConfig](#dnsconfig)
|
||||
|
||||
- [ProxyClass](#proxyclass)
|
||||
|
||||
|
||||
@@ -259,6 +261,274 @@ ConnectorCondition contains condition information for a Connector.
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
## DNSConfig
|
||||
<sup><sup>[↩ Parent](#tailscalecomv1alpha1 )</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>apiVersion</b></td>
|
||||
<td>string</td>
|
||||
<td>tailscale.com/v1alpha1</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>kind</b></td>
|
||||
<td>string</td>
|
||||
<td>DNSConfig</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b><a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta">metadata</a></b></td>
|
||||
<td>object</td>
|
||||
<td>Refer to the Kubernetes API documentation for the fields of the `metadata` field.</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#dnsconfigspec">spec</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#dnsconfigstatus">status</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.spec
|
||||
<sup><sup>[↩ Parent](#dnsconfig)</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#dnsconfigspecnameserver">nameserver</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.spec.nameserver
|
||||
<sup><sup>[↩ Parent](#dnsconfigspec)</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#dnsconfigspecnameserverimage">image</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.spec.nameserver.image
|
||||
<sup><sup>[↩ Parent](#dnsconfigspecnameserver)</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>repo</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>tag</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.status
|
||||
<sup><sup>[↩ Parent](#dnsconfig)</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#dnsconfigstatusconditionsindex">conditions</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#dnsconfigstatusnameserverstatus">nameserverStatus</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.status.conditions[index]
|
||||
<sup><sup>[↩ Parent](#dnsconfigstatus)</sup></sup>
|
||||
|
||||
|
||||
|
||||
ConnectorCondition contains condition information for a Connector.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>status</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Status of the condition, one of ('True', 'False', 'Unknown').<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>type</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Type of the condition, known values are (`SubnetRouterReady`).<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>lastTransitionTime</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
LastTransitionTime is the timestamp corresponding to the last status change of this condition.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: date-time<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>message</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Message is a human readable description of the details of the last transition, complementing reason.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>reason</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Reason is a brief machine readable explanation for the condition's last transition.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
### DNSConfig.status.nameserverStatus
|
||||
<sup><sup>[↩ Parent](#dnsconfigstatus)</sup></sup>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>ip</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
## ProxyClass
|
||||
<sup><sup>[↩ Parent](#tailscalecomv1alpha1 )</sup></sup>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
|
||||
// Adds the list of known types to api.Scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{})
|
||||
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{}, &DNSConfig{}, &DNSConfigList{})
|
||||
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
71
k8s-operator/apis/v1alpha1/types_tsdnsconfig.go
Normal file
71
k8s-operator/apis/v1alpha1/types_tsdnsconfig.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Code comments on these types should be treated as user facing documentation-
|
||||
// they will appear on the DNSConfig CRD i.e if someone runs kubectl explain dnsconfig.
|
||||
|
||||
var DNSConfigKind = "DNSConfig"
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=dc
|
||||
// +kubebuilder:printcolumn:name="NameserverIP",type="string",JSONPath=`.status.nameserverStatus.ip`,description="Service IP address of the nameserver"
|
||||
|
||||
type DNSConfig struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec DNSConfigSpec `json:"spec"`
|
||||
|
||||
// +optional
|
||||
Status DNSConfigStatus `json:"status"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
type DNSConfigList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
|
||||
Items []DNSConfig `json:"items"`
|
||||
}
|
||||
|
||||
type DNSConfigSpec struct {
|
||||
Nameserver *Nameserver `json:"nameserver"`
|
||||
}
|
||||
|
||||
type Nameserver struct {
|
||||
// +optional
|
||||
Image *Image `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
// +optional
|
||||
Repo string `json:"repo,omitempty"`
|
||||
// +optional
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
type DNSConfigStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
Conditions []ConnectorCondition `json:"conditions"`
|
||||
// +optional
|
||||
NameserverStatus *NameserverStatus `json:"nameserverStatus"`
|
||||
}
|
||||
|
||||
type NameserverStatus struct {
|
||||
// +optional
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
const NameserverReady ConnectorConditionType = `NameserverReady`
|
||||
@@ -158,6 +158,162 @@ func (in *Container) DeepCopy() *Container {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DNSConfig) DeepCopyInto(out *DNSConfig) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfig.
|
||||
func (in *DNSConfig) DeepCopy() *DNSConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DNSConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DNSConfig) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DNSConfigList) DeepCopyInto(out *DNSConfigList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]DNSConfig, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigList.
|
||||
func (in *DNSConfigList) DeepCopy() *DNSConfigList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DNSConfigList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DNSConfigList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DNSConfigSpec) DeepCopyInto(out *DNSConfigSpec) {
|
||||
*out = *in
|
||||
if in.Nameserver != nil {
|
||||
in, out := &in.Nameserver, &out.Nameserver
|
||||
*out = new(Nameserver)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigSpec.
|
||||
func (in *DNSConfigSpec) DeepCopy() *DNSConfigSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DNSConfigSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DNSConfigStatus) DeepCopyInto(out *DNSConfigStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]ConnectorCondition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.NameserverStatus != nil {
|
||||
in, out := &in.NameserverStatus, &out.NameserverStatus
|
||||
*out = new(NameserverStatus)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSConfigStatus.
|
||||
func (in *DNSConfigStatus) DeepCopy() *DNSConfigStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DNSConfigStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Image) DeepCopyInto(out *Image) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Image.
|
||||
func (in *Image) DeepCopy() *Image {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Image)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Nameserver) DeepCopyInto(out *Nameserver) {
|
||||
*out = *in
|
||||
if in.Image != nil {
|
||||
in, out := &in.Image, &out.Image
|
||||
*out = new(Image)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Nameserver.
|
||||
func (in *Nameserver) DeepCopy() *Nameserver {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Nameserver)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NameserverStatus) DeepCopyInto(out *NameserverStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameserverStatus.
|
||||
func (in *NameserverStatus) DeepCopy() *NameserverStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(NameserverStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Pod) DeepCopyInto(out *Pod) {
|
||||
*out = *in
|
||||
|
||||
@@ -24,7 +24,7 @@ func SetConnectorCondition(cn *tsapi.Connector, conditionType tsapi.ConnectorCon
|
||||
cn.Status.Conditions = conds
|
||||
}
|
||||
|
||||
// RemoveConnectorCondition will remove condition of the given type.
|
||||
// RemoveConnectorCondition will remove condition of the given type if it exists.
|
||||
func RemoveConnectorCondition(conn *tsapi.Connector, conditionType tsapi.ConnectorConditionType) {
|
||||
conn.Status.Conditions = slices.DeleteFunc(conn.Status.Conditions, func(cond tsapi.ConnectorCondition) bool {
|
||||
return cond.Type == conditionType
|
||||
@@ -39,6 +39,14 @@ func SetProxyClassCondition(pc *tsapi.ProxyClass, conditionType tsapi.ConnectorC
|
||||
pc.Status.Conditions = conds
|
||||
}
|
||||
|
||||
// SetDNSConfigCondition ensures that DNSConfig status has a condition with the
|
||||
// given attributes. LastTransitionTime gets set every time condition's status
|
||||
// changes
|
||||
func SetDNSConfigCondition(dnsCfg *tsapi.DNSConfig, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) {
|
||||
conds := updateCondition(dnsCfg.Status.Conditions, conditionType, status, reason, message, gen, clock, logger)
|
||||
dnsCfg.Status.Conditions = conds
|
||||
}
|
||||
|
||||
func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string, gen int64, clock tstime.Clock, logger *zap.SugaredLogger) []tsapi.ConnectorCondition {
|
||||
newCondition := tsapi.ConnectorCondition{
|
||||
Type: conditionType,
|
||||
@@ -61,8 +69,9 @@ func updateCondition(conds []tsapi.ConnectorCondition, conditionType tsapi.Conne
|
||||
}
|
||||
|
||||
cond := conds[idx] // update the existing condition
|
||||
// If this update doesn't contain a state transition, we don't update
|
||||
// the conditions LastTransitionTime to Now().
|
||||
|
||||
// If this update doesn't contain a state transition, don't update last
|
||||
// transition time.
|
||||
if cond.Status == status {
|
||||
newCondition.LastTransitionTime = cond.LastTransitionTime
|
||||
} else {
|
||||
@@ -82,3 +91,14 @@ func ProxyClassIsReady(pc *tsapi.ProxyClass) bool {
|
||||
cond := pc.Status.Conditions[idx]
|
||||
return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == pc.Generation
|
||||
}
|
||||
|
||||
func DNSCfgIsReady(cfg *tsapi.DNSConfig) bool {
|
||||
idx := xslices.IndexFunc(cfg.Status.Conditions, func(cond tsapi.ConnectorCondition) bool {
|
||||
return cond.Type == tsapi.NameserverReady
|
||||
})
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
cond := cfg.Status.Conditions[idx]
|
||||
return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == cfg.Generation
|
||||
}
|
||||
|
||||
@@ -98,5 +98,4 @@ func TestSetConnectorCondition(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
17
k8s-operator/tsdns.go
Normal file
17
k8s-operator/tsdns.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package kube
|
||||
|
||||
const Alpha1Version = "v1alpha1"
|
||||
|
||||
type Records struct {
|
||||
// Version is the version of this Records configuration. Version is
|
||||
// intended to be used by ./cmd/k8s-nameserver to determine whether it
|
||||
// can read this records configuration.
|
||||
Version string `json:"version"`
|
||||
// IP4 contains a mapping of DNS names to IPv4 address(es).
|
||||
IP4 map[string][]string `json:"ip4"`
|
||||
}
|
||||
@@ -33,18 +33,17 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/benoitkugler/textlayout/fonts](https://pkg.go.dev/github.com/benoitkugler/textlayout/fonts) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/fonts/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/graphite](https://pkg.go.dev/github.com/benoitkugler/textlayout/graphite) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/graphite/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/harfbuzz](https://pkg.go.dev/github.com/benoitkugler/textlayout/harfbuzz) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/harfbuzz/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/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/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/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/go-text/typesetting](https://pkg.go.dev/github.com/go-text/typesetting) ([BSD-3-Clause](https://github.com/go-text/typesetting/blob/0399769901d5/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/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.5.0/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](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
||||
@@ -69,8 +68,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/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](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/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/tailscale-android/cmd](https://pkg.go.dev/github.com/tailscale/tailscale-android/cmd) ([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/cc193a0b3272/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))
|
||||
@@ -81,17 +79,19 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/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/+/v0.18.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.21.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/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/exp/shiny/iconvg](https://pkg.go.dev/golang.org/x/exp/shiny/iconvg) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/d852ddb8:shiny/LICENSE))
|
||||
- [golang.org/x/exp/shiny/materialdesign/icons](https://pkg.go.dev/golang.org/x/exp/shiny/materialdesign/icons) ([Apache-2.0](https://cs.opensource.google/go/x/exp/+/d852ddb8:shiny/materialdesign/icons/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.15.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.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.22.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.6.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.16.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.16.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.18.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.18.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.14.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/c9c1d4f9b186/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](Unknown))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
- [Gio UI](https://gioui.org/) ([MIT License](https://git.sr.ht/~eliasnaur/gio/tree/main/item/LICENSE))
|
||||
|
||||
@@ -13,41 +13,43 @@ 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.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/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.6/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/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.3/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/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.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/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/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.5.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/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/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.5.0/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/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.0/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.6/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.6/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.6/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [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.7/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.7/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.7/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/v1.7.2/LICENSE.md))
|
||||
@@ -75,11 +77,11 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [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.20.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.6.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.16.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.17.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.16.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.14.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/c9c1d4f9b186/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.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/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.21/LICENSE))
|
||||
@@ -40,6 +41,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/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.5.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/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))
|
||||
@@ -98,18 +100,17 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.20.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.6.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.16.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.17.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.16.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.14.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/c9c1d4f9b186/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/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.29.1/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [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))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart?tab=MIT-1-ov-file#readme))
|
||||
- [tailscale.com/tempfork/gliderlabs/ssh](https://pkg.go.dev/tailscale.com/tempfork/gliderlabs/ssh) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/gliderlabs/ssh/LICENSE))
|
||||
|
||||
@@ -14,38 +14,38 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.26.5/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.26.6/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/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.3/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/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.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/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [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/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/b75a8a7d7eb0/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/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/9aa6fdf5a28c/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.5.0/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
||||
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
|
||||
- [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/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.0/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.6/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.6/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.6/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [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.7/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.7/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.7/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [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/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))
|
||||
@@ -54,6 +54,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
|
||||
- [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/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/6a278000867c/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/d2e5cdeed6dc/LICENSE))
|
||||
@@ -70,7 +71,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.14.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.20.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.6.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.16.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.17.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.16.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.14.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))
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -46,11 +47,6 @@ const (
|
||||
CollectionNode = "tailnode.log.tailscale.io"
|
||||
)
|
||||
|
||||
type Encoder interface {
|
||||
EncodeAll(src, dst []byte) []byte
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Collection string // collection name, a domain name
|
||||
PrivateID logid.PrivateID // private ID for the primary log stream
|
||||
@@ -65,9 +61,6 @@ type Config struct {
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
CompressLogs bool // whether to compress the log uploads
|
||||
|
||||
// Deprecated: Use CompressUploads instead.
|
||||
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
|
||||
|
||||
// MetricsDelta, if non-nil, is a func that returns an encoding
|
||||
// delta in clientmetrics to upload alongside existing logs.
|
||||
// It can return either an empty string (for nothing) or a string
|
||||
@@ -161,9 +154,6 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
}
|
||||
l.SetSockstatsLabel(sockstats.LabelLogtailLogger)
|
||||
l.compressLogs = cfg.CompressLogs
|
||||
if cfg.NewZstdEncoder != nil {
|
||||
l.zstdEncoder = cfg.NewZstdEncoder()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
l.uploadCancel = cancel
|
||||
@@ -191,7 +181,6 @@ type Logger struct {
|
||||
sentinel chan int32
|
||||
clock tstime.Clock
|
||||
compressLogs bool
|
||||
zstdEncoder Encoder
|
||||
uploadCancel func()
|
||||
explainedRaw bool
|
||||
metricsDelta func() string // or nil
|
||||
@@ -272,9 +261,6 @@ func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
io.WriteString(l, "logger closing down\n")
|
||||
<-done
|
||||
|
||||
if l.zstdEncoder != nil {
|
||||
return l.zstdEncoder.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -378,17 +364,9 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
body := l.drainPending()
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if (l.compressLogs || l.zstdEncoder != nil) && len(body) > 256 {
|
||||
var zbody []byte
|
||||
switch {
|
||||
case l.zstdEncoder != nil:
|
||||
zbody = l.zstdEncoder.EncodeAll(body, nil)
|
||||
case l.lowMem:
|
||||
zbody = zstdframe.AppendEncode(nil, body,
|
||||
zstdframe.FastestCompression, zstdframe.LowMemory(true))
|
||||
default:
|
||||
zbody = zstdframe.AppendEncode(nil, body)
|
||||
}
|
||||
if l.compressLogs && len(body) > 256 {
|
||||
zbody := zstdframe.AppendEncode(nil, body,
|
||||
zstdframe.FastestCompression, zstdframe.LowMemory(true))
|
||||
|
||||
// Only send it compressed if the bandwidth savings are sufficient.
|
||||
// Just the extra headers associated with enabling compression
|
||||
@@ -491,6 +469,19 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAft
|
||||
req.Header.Add("Content-Encoding", "zstd")
|
||||
req.Header.Add("Orig-Content-Length", strconv.Itoa(origlen))
|
||||
}
|
||||
if runtime.GOOS == "js" {
|
||||
// We once advertised we'd accept optional client certs (for internal use)
|
||||
// on log.tailscale.io but then Tailscale SSH js/wasm clients prompted
|
||||
// users (on some browsers?) to pick a client cert. We'll fix the server's
|
||||
// TLS ServerHello, but we can also fix it client side for good measure.
|
||||
//
|
||||
// Corp details: https://github.com/tailscale/corp/issues/18177#issuecomment-2026598715
|
||||
// and https://github.com/tailscale/corp/pull/18775#issuecomment-2027505036
|
||||
//
|
||||
// See https://github.com/golang/go/wiki/WebAssembly#configuring-fetch-options-while-using-nethttp
|
||||
// and https://developer.mozilla.org/en-US/docs/Web/API/fetch#credentials
|
||||
req.Header.Set("js.fetch:credentials", "omit")
|
||||
}
|
||||
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
|
||||
|
||||
compressedNote := "not-compressed"
|
||||
|
||||
@@ -403,6 +403,8 @@ func (m *directManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
func (m *directManager) Close() error {
|
||||
m.ctxClose()
|
||||
|
||||
// We used to keep a file for the tailscale config and symlinked
|
||||
// to it, but then we stopped because /etc/resolv.conf being a
|
||||
// symlink to surprising places breaks snaps and other sandboxing
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user