Compare commits
78 Commits
bradfitz/s
...
bradfitz/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8ce6685e | ||
|
|
4899c2c1f4 | ||
|
|
b949e208bb | ||
|
|
18bd98d35b | ||
|
|
71271e41d6 | ||
|
|
95faefd1f6 | ||
|
|
8a5b02133d | ||
|
|
51078b6486 | ||
|
|
7fd6cc3caa | ||
|
|
feabb34ea0 | ||
|
|
e06f2f1873 | ||
|
|
97ee3891f1 | ||
|
|
56ebcd1ed4 | ||
|
|
e89927de2b | ||
|
|
18e2936d25 | ||
|
|
c363b9055d | ||
|
|
a6270826a3 | ||
|
|
5297bd2cff | ||
|
|
5c555cdcbb | ||
|
|
8c7169105e | ||
|
|
9cb6c5bb78 | ||
|
|
af5a586463 | ||
|
|
754fb9a8a8 | ||
|
|
8f948638c5 | ||
|
|
b1867eb23f | ||
|
|
24f322bc43 | ||
|
|
1a78f240b5 | ||
|
|
7783a960e8 | ||
|
|
ce0830837d | ||
|
|
37c646d9d3 | ||
|
|
1294b89792 | ||
|
|
2d4f808a4c | ||
|
|
4abd470322 | ||
|
|
96f01a73b1 | ||
|
|
d62af8e643 | ||
|
|
1cb9e33a95 | ||
|
|
c1ef55249a | ||
|
|
319607625f | ||
|
|
9d96e05267 | ||
|
|
8b630c91bc | ||
|
|
0a412eba40 | ||
|
|
11348fbe72 | ||
|
|
fbfee6a8c0 | ||
|
|
7a0de2997e | ||
|
|
aad3584319 | ||
|
|
fffafc65d6 | ||
|
|
9f05018419 | ||
|
|
04a8b8bb8e | ||
|
|
4e083e4548 | ||
|
|
78a083e144 | ||
|
|
05a1f5bf71 | ||
|
|
56c0a75ea9 | ||
|
|
ba6ec42f6d | ||
|
|
677d486830 | ||
|
|
7f08bddfe1 | ||
|
|
00977f6de9 | ||
|
|
0ccfcb515c | ||
|
|
3749a3bbbb | ||
|
|
6b1ed732df | ||
|
|
70de16bda7 | ||
|
|
7f540042d5 | ||
|
|
d0b8bdf8f7 | ||
|
|
9eedf86563 | ||
|
|
249edaa349 | ||
|
|
893bdd729c | ||
|
|
b4e587c3bd | ||
|
|
9593cd3871 | ||
|
|
623926a25d | ||
|
|
886917c42b | ||
|
|
553f657248 | ||
|
|
6f36f8842c | ||
|
|
13767e5108 | ||
|
|
f991c8a61f | ||
|
|
498f7ec663 | ||
|
|
e4cb83b18b | ||
|
|
e6aa7b815d | ||
|
|
b7988b3825 | ||
|
|
557ddced6c |
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -39,6 +39,16 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
race-root-integration:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: integration tests as root
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper --sudo ./tstest/integration/ -race
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false # don't abort the entire matrix if one element fails
|
||||
@@ -70,6 +80,7 @@ jobs:
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
|
||||
- name: build all
|
||||
if: matrix.buildflags == '' # skip on race builder
|
||||
run: ./tool/go build ${{matrix.buildflags}} ./...
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
@@ -162,7 +173,17 @@ jobs:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDB_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
|
||||
race-build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build all
|
||||
run: ./tool/go install -race ./cmd/...
|
||||
- name: build tests
|
||||
run: ./tool/go test -race -exec=true ./...
|
||||
|
||||
cross: # cross-compile checks, build only.
|
||||
strategy:
|
||||
fail-fast: false # don't abort the entire matrix if one element fails
|
||||
|
||||
4
api.md
4
api.md
@@ -209,10 +209,6 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
|
||||
"192.68.0.21:59128"
|
||||
],
|
||||
|
||||
// derp (string) is the IP:port of the DERP server currently being used.
|
||||
// Learn about DERP servers at https://tailscale.com/kb/1232/.
|
||||
"derp":"",
|
||||
|
||||
// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
|
||||
// vary based on the destination IP.
|
||||
"mappingVariesByDestIP":false,
|
||||
|
||||
59
appctype/appconnector.go
Normal file
59
appctype/appconnector.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package appcfg contains an experimental configuration structure for
|
||||
// "tailscale.com/app-connector" capmap extensions.
|
||||
package appctype
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// ConfigID is an opaque identifier for a configuration.
|
||||
type ConfigID string
|
||||
|
||||
// AppConnectorConfig is the configuration structure for an application
|
||||
// connection proxy service.
|
||||
type AppConnectorConfig struct {
|
||||
// DNAT is a map of destination NAT configurations.
|
||||
DNAT map[ConfigID]DNATConfig `json:",omitempty"`
|
||||
// SNIProxy is a map of SNI proxy configurations.
|
||||
SNIProxy map[ConfigID]SNIProxyConfig `json:",omitempty"`
|
||||
|
||||
// AdvertiseRoutes indicates that the node should advertise routes for each
|
||||
// of the addresses in service configuration address lists. If false, the
|
||||
// routes have already been advertised.
|
||||
AdvertiseRoutes bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// DNATConfig is the configuration structure for a destination NAT service, also
|
||||
// known as a "port forward" or "port proxy".
|
||||
type DNATConfig struct {
|
||||
// Addrs is a list of addresses to listen on.
|
||||
Addrs []netip.Addr `json:",omitempty"`
|
||||
|
||||
// To is a list of destination addresses to forward traffic to. It should
|
||||
// only contain one domain, or a list of IP addresses.
|
||||
To []string `json:",omitempty"`
|
||||
|
||||
// IP is a list of IP specifications to forward. If omitted, all protocols are
|
||||
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
|
||||
IP []tailcfg.ProtoPortRange `json:",omitempty"`
|
||||
}
|
||||
|
||||
// SNIPRoxyConfig is the configuration structure for an SNI proxy service,
|
||||
// forwarding TLS connections based on the hostname field in SNI.
|
||||
type SNIProxyConfig struct {
|
||||
// Addrs is a list of addresses to listen on.
|
||||
Addrs []netip.Addr `json:",omitempty"`
|
||||
|
||||
// IP is a list of IP specifications to forward. If omitted, all protocols are
|
||||
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
|
||||
IP []tailcfg.ProtoPortRange `json:",omitempty"`
|
||||
|
||||
// AllowedDomains is a list of domains that are allowed to be proxied. If
|
||||
// the domain starts with a `.` that means any subdomain of the suffix.
|
||||
AllowedDomains []string `json:",omitempty"`
|
||||
}
|
||||
78
appctype/appconnector_test.go
Normal file
78
appctype/appconnector_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appctype
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
var golden = `{
|
||||
"dnat": {
|
||||
"opaqueid1": {
|
||||
"addrs": ["100.64.0.1", "fd7a:115c:a1e0::1"],
|
||||
"to": ["example.org"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
},
|
||||
"sniProxy": {
|
||||
"opaqueid2": {
|
||||
"addrs": ["::"],
|
||||
"ip": ["tcp:443"],
|
||||
"allowedDomains": ["*"]
|
||||
}
|
||||
},
|
||||
"advertiseRoutes": true
|
||||
}`
|
||||
|
||||
func TestGolden(t *testing.T) {
|
||||
wantDNAT := map[ConfigID]DNATConfig{"opaqueid1": {
|
||||
Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
|
||||
To: []string{"example.org"},
|
||||
IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
|
||||
}}
|
||||
|
||||
wantSNI := map[ConfigID]SNIProxyConfig{"opaqueid2": {
|
||||
Addrs: []netip.Addr{netip.MustParseAddr("::")},
|
||||
IP: []tailcfg.ProtoPortRange{{Proto: 6, Ports: tailcfg.PortRange{First: 443, Last: 443}}},
|
||||
AllowedDomains: []string{"*"},
|
||||
}}
|
||||
|
||||
var config AppConnectorConfig
|
||||
if err := json.NewDecoder(strings.NewReader(golden)).Decode(&config); err != nil {
|
||||
t.Fatalf("failed to decode golden config: %v", err)
|
||||
}
|
||||
|
||||
if !config.AdvertiseRoutes {
|
||||
t.Fatalf("expected AdvertiseRoutes to be true, got false")
|
||||
}
|
||||
|
||||
assertEqual(t, "DNAT", config.DNAT, wantDNAT)
|
||||
assertEqual(t, "SNI", config.SNIProxy, wantSNI)
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
var config AppConnectorConfig
|
||||
must.Do(json.NewDecoder(strings.NewReader(golden)).Decode(&config))
|
||||
b := must.Get(json.Marshal(config))
|
||||
var config2 AppConnectorConfig
|
||||
must.Do(json.Unmarshal(b, &config2))
|
||||
assertEqual(t, "DNAT", config.DNAT, config2.DNAT)
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, name string, a, b any) {
|
||||
var addrComparer = cmp.Comparer(func(a, b netip.Addr) bool {
|
||||
return a.Compare(b) == 0
|
||||
})
|
||||
t.Helper()
|
||||
if diff := cmp.Diff(a, b, addrComparer); diff != "" {
|
||||
t.Fatalf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -40,3 +40,12 @@ type SetPushDeviceTokenRequest struct {
|
||||
// PushDeviceToken is the iOS/macOS APNs device token (and any future Android equivalent).
|
||||
PushDeviceToken string
|
||||
}
|
||||
|
||||
// ReloadConfigResponse is the response to a LocalAPI reload-config request.
|
||||
//
|
||||
// There are three possible outcomes: (false, "") if no config mode in use,
|
||||
// (true, "") on success, or (false, "error message") on failure.
|
||||
type ReloadConfigResponse struct {
|
||||
Reloaded bool // whether the config was reloaded
|
||||
Err string // any error message
|
||||
}
|
||||
|
||||
@@ -1244,6 +1244,25 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
|
||||
return current, all, err
|
||||
}
|
||||
|
||||
// ReloadConfig reloads the config file, if possible.
|
||||
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := decodeJSON[apitype.ReloadConfigResponse](body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if res.Err != "" {
|
||||
return false, errors.New(res.Err)
|
||||
}
|
||||
return res.Reloaded, nil
|
||||
}
|
||||
|
||||
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
|
||||
// profile is not assigned an ID until it is persisted after a successful login.
|
||||
// In order to login to the new profile, the user must call LoginInteractive.
|
||||
|
||||
@@ -66,6 +66,11 @@ const (
|
||||
sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
|
||||
)
|
||||
|
||||
var (
|
||||
exitNodeRouteV4 = netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 = netip.MustParsePrefix("::/0")
|
||||
)
|
||||
|
||||
// browserSession holds data about a user's browser session
|
||||
// on the full management web client.
|
||||
type browserSession struct {
|
||||
@@ -125,8 +130,7 @@ type ServerOpts struct {
|
||||
}
|
||||
|
||||
// NewServer constructs a new Tailscale web client server.
|
||||
// The provided context should live for the duration of the Server's lifetime.
|
||||
func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) {
|
||||
func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
|
||||
if opts.LocalClient == nil {
|
||||
opts.LocalClient = &tailscale.LocalClient{}
|
||||
}
|
||||
@@ -190,7 +194,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if !s.devMode {
|
||||
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_page_load", 1)
|
||||
}
|
||||
s.assetsHandler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -428,8 +432,6 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
IPNVersion: versionShort,
|
||||
DebugMode: s.tsDebugMode,
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
||||
data.AdvertiseExitNode = true
|
||||
@@ -474,6 +476,22 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
prefs, err := s.lc.GetPrefs(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
isCurrentlyExitNode := slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV4) || slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV6)
|
||||
|
||||
if postData.AdvertiseExitNode != isCurrentlyExitNode {
|
||||
if postData.AdvertiseExitNode {
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_enable", 1)
|
||||
} else {
|
||||
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_disable", 1)
|
||||
}
|
||||
}
|
||||
|
||||
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"tailscale.com/clientupdate/distsign"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpver"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -77,6 +78,10 @@ type Arguments struct {
|
||||
AppStore bool
|
||||
// Logf is a logger for update progress messages.
|
||||
Logf logger.Logf
|
||||
// Stdout and Stderr should be used for output instead of os.Stdout and
|
||||
// os.Stderr.
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
// Confirm is called when a new version is available and should return true
|
||||
// if this new version should be installed. When Confirm returns false, the
|
||||
// update is aborted.
|
||||
@@ -108,6 +113,12 @@ func NewUpdater(args Arguments) (*Updater, error) {
|
||||
up := Updater{
|
||||
Arguments: args,
|
||||
}
|
||||
if up.Stdout == nil {
|
||||
up.Stdout = os.Stdout
|
||||
}
|
||||
if up.Stderr == nil {
|
||||
up.Stderr = os.Stderr
|
||||
}
|
||||
up.Update = up.getUpdateFunction()
|
||||
if up.Update == nil {
|
||||
return nil, errors.ErrUnsupported
|
||||
@@ -174,7 +185,9 @@ func (up *Updater) getUpdateFunction() updateFunction {
|
||||
case !up.Arguments.AppStore && !version.IsSandboxedMacOS():
|
||||
return nil
|
||||
case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
return up.updateMacSys
|
||||
// TODO(noncombatant): return up.updateMacSys when we figure out why
|
||||
// Sparkle update doesn't work when running "tailscale update".
|
||||
return nil
|
||||
default:
|
||||
return up.updateMacAppStore
|
||||
}
|
||||
@@ -201,9 +214,13 @@ func Update(args Arguments) error {
|
||||
}
|
||||
|
||||
func (up *Updater) confirm(ver string) bool {
|
||||
if version.Short() == ver {
|
||||
switch cmpver.Compare(version.Short(), ver) {
|
||||
case 0:
|
||||
up.Logf("already running %v; no update needed", ver)
|
||||
return false
|
||||
case 1:
|
||||
up.Logf("installed version %v is newer than the latest available version %v; no update needed", version.Short(), ver)
|
||||
return false
|
||||
}
|
||||
if up.Confirm != nil {
|
||||
return up.Confirm(ver)
|
||||
@@ -256,9 +273,9 @@ func (up *Updater) updateSynology() error {
|
||||
// connected over tailscale ssh and this parent process dies. Otherwise, if
|
||||
// you abort synopkg install mid-way, tailscaled is not restarted.
|
||||
cmd := exec.Command("nohup", "synopkg", "install", spkPath)
|
||||
// Don't attach cmd.Stdout to os.Stdout because nohup will redirect that
|
||||
// into nohup.out file. synopkg doesn't have any progress output anyway, it
|
||||
// just spits out a JSON result when done.
|
||||
// Don't attach cmd.Stdout to Stdout because nohup will redirect that into
|
||||
// nohup.out file. synopkg doesn't have any progress output anyway, it just
|
||||
// spits out a JSON result when done.
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
|
||||
@@ -369,15 +386,15 @@ func (up *Updater) updateDebLike() error {
|
||||
// we're not updating them:
|
||||
"-o", "APT::Get::List-Cleanup=0",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -491,8 +508,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -577,8 +594,8 @@ func (up *Updater) updateAlpineLike() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("apk", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using apk: %w", err)
|
||||
}
|
||||
@@ -634,8 +651,8 @@ func (up *Updater) updateMacAppStore() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
|
||||
}
|
||||
@@ -726,8 +743,8 @@ func (up *Updater) updateWindows() error {
|
||||
|
||||
cmd := exec.Command(selfCopy, "update")
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stderr
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
@@ -743,8 +760,8 @@ func (up *Updater) installMSI(msi string) error {
|
||||
for tries := 0; tries < 2; tries++ {
|
||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
|
||||
cmd.Dir = filepath.Dir(msi)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
@@ -757,8 +774,8 @@ func (up *Updater) installMSI(msi string) error {
|
||||
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
|
||||
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
|
||||
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
up.Logf("msiexec uninstall: %v", err)
|
||||
@@ -846,8 +863,8 @@ func (up *Updater) updateFreeBSD() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("pkg", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using pkg: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
-----BEGIN ROOT PUBLIC KEY-----
|
||||
Muw5GkO5mASsJ7k6kS+svfuanr6XcW9I7fPGtyqOTeI=
|
||||
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
|
||||
-----END ROOT PUBLIC KEY-----
|
||||
@@ -19,8 +19,7 @@
|
||||
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
|
||||
// destination.
|
||||
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
|
||||
// - TS_EXTRA_ARGS: extra arguments to 'tailscale login', these are not
|
||||
// reset on restart.
|
||||
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
|
||||
// - TS_USERSPACE: run with userspace networking (the default)
|
||||
// instead of kernel networking.
|
||||
// - TS_STATE_DIR: the directory in which to store tailscaled
|
||||
@@ -36,15 +35,9 @@
|
||||
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
|
||||
// be created.
|
||||
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
|
||||
// logged in. If false, forcibly log in every time the container starts.
|
||||
// The default until 1.50.0 was false, but that was misleading: until
|
||||
// 1.50, containerboot used `tailscale up` which would ignore an authkey
|
||||
// argument if there was already a node key. Effectively, this behaved
|
||||
// as though TS_AUTH_ONCE were always true.
|
||||
// In 1.50.0 the change was made to use `tailscale login` instead of `up`,
|
||||
// and login will reauthenticate every time it is given an authkey.
|
||||
// In 1.50.1 we set the TS_AUTH_ONCE to true, to match the previously
|
||||
// observed behavior.
|
||||
// logged in. If false (the default, for backwards
|
||||
// compatibility), forcibly log in every time the
|
||||
// container starts.
|
||||
// - TS_SERVE_CONFIG: if specified, is the file path where the ipn.ServeConfig is located.
|
||||
// It will be applied once tailscaled is up and running. If the file contains
|
||||
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
|
||||
@@ -84,10 +77,19 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/linuxfw"
|
||||
)
|
||||
|
||||
func newNetfilterRunner(logf logger.Logf) (linuxfw.NetfilterRunner, error) {
|
||||
if defaultBool("TS_TEST_FAKE_NETFILTER", false) {
|
||||
return linuxfw.NewFakeIPTablesRunner(), nil
|
||||
}
|
||||
return linuxfw.New(logf)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("boot: ")
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
@@ -109,7 +111,7 @@ func main() {
|
||||
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
|
||||
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
|
||||
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", true),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
|
||||
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
|
||||
}
|
||||
|
||||
@@ -203,7 +205,7 @@ func main() {
|
||||
}
|
||||
didLogin = true
|
||||
w.Close()
|
||||
if err := tailscaleLogin(bootCtx, cfg); err != nil {
|
||||
if err := tailscaleUp(bootCtx, cfg); err != nil {
|
||||
return fmt.Errorf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
w, err = client.WatchIPNBus(bootCtx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
|
||||
@@ -253,10 +255,12 @@ authLoop:
|
||||
ctx, cancel := context.WithCancel(context.Background()) // no deadline now that we're in steady state
|
||||
defer cancel()
|
||||
|
||||
// Now that we are authenticated, we can set/reset any of the
|
||||
// settings that we need to.
|
||||
if err := tailscaleSet(ctx, cfg); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
if cfg.AuthOnce {
|
||||
// Now that we are authenticated, we can set/reset any of the
|
||||
// settings that we need to.
|
||||
if err := tailscaleSet(ctx, cfg); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ServeConfigPath != "" {
|
||||
@@ -295,6 +299,13 @@ authLoop:
|
||||
if cfg.ServeConfigPath != "" {
|
||||
go watchServeConfigChanges(ctx, cfg.ServeConfigPath, certDomainChanged, certDomain, client)
|
||||
}
|
||||
var nfr linuxfw.NetfilterRunner
|
||||
if wantProxy {
|
||||
nfr, err = newNetfilterRunner(log.Printf)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating new netfilter runner: %v", err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
n, err := w.Next()
|
||||
if err != nil {
|
||||
@@ -315,7 +326,7 @@ authLoop:
|
||||
ipsHaveChanged := newCurrentIPs != currentIPs
|
||||
if cfg.ProxyTo != "" && len(addrs) > 0 && ipsHaveChanged {
|
||||
log.Printf("Installing proxy rules")
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs); err != nil {
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs, nfr); err != nil {
|
||||
log.Fatalf("installing ingress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -330,7 +341,7 @@ authLoop:
|
||||
}
|
||||
}
|
||||
if cfg.TailnetTargetIP != "" && ipsHaveChanged && len(addrs) > 0 {
|
||||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs); err != nil {
|
||||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs, nfr); err != nil {
|
||||
log.Fatalf("installing egress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -385,19 +396,20 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
||||
panic("cd must not be nil")
|
||||
}
|
||||
var tickChan <-chan time.Time
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
var eventChan <-chan fsnotify.Event
|
||||
if w, err := fsnotify.NewWatcher(); err != nil {
|
||||
log.Printf("failed to create fsnotify watcher, timer-only mode: %v", err)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
tickChan = ticker.C
|
||||
} else {
|
||||
defer w.Close()
|
||||
if err := w.Add(filepath.Dir(path)); err != nil {
|
||||
log.Fatalf("failed to add fsnotify watch: %v", err)
|
||||
}
|
||||
eventChan = w.Events
|
||||
}
|
||||
|
||||
if err := w.Add(filepath.Dir(path)); err != nil {
|
||||
log.Fatalf("failed to add fsnotify watch: %v", err)
|
||||
}
|
||||
var certDomain string
|
||||
var prevServeConfig *ipn.ServeConfig
|
||||
for {
|
||||
@@ -407,7 +419,7 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
|
||||
case <-cdChanged:
|
||||
certDomain = *certDomainAtomic.Load()
|
||||
case <-tickChan:
|
||||
case <-w.Events:
|
||||
case <-eventChan:
|
||||
// We can't do any reasonable filtering on the event because of how
|
||||
// k8s handles these mounts. So just re-read the file and apply it
|
||||
// if it's changed.
|
||||
@@ -528,29 +540,40 @@ func tailscaledArgs(cfg *settings) []string {
|
||||
return args
|
||||
}
|
||||
|
||||
// tailscaleLogin uses cfg to run 'tailscale login' everytime containerboot
|
||||
// starts, or if TS_AUTH_ONCE is set, only the first time containerboot starts.
|
||||
func tailscaleLogin(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "login"}
|
||||
// tailscaleUp uses cfg to run 'tailscale up' everytime containerboot starts, or
|
||||
// if TS_AUTH_ONCE is set, only the first time containerboot starts.
|
||||
func tailscaleUp(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "up"}
|
||||
if cfg.AcceptDNS {
|
||||
args = append(args, "--accept-dns=true")
|
||||
} else {
|
||||
args = append(args, "--accept-dns=false")
|
||||
}
|
||||
if cfg.AuthKey != "" {
|
||||
args = append(args, "--authkey="+cfg.AuthKey)
|
||||
}
|
||||
if cfg.Routes != "" {
|
||||
args = append(args, "--advertise-routes="+cfg.Routes)
|
||||
}
|
||||
if cfg.Hostname != "" {
|
||||
args = append(args, "--hostname="+cfg.Hostname)
|
||||
}
|
||||
if cfg.ExtraArgs != "" {
|
||||
args = append(args, strings.Fields(cfg.ExtraArgs)...)
|
||||
}
|
||||
log.Printf("Running 'tailscale login'")
|
||||
log.Printf("Running 'tailscale up'")
|
||||
cmd := exec.CommandContext(ctx, "tailscale", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("tailscale login failed: %v", err)
|
||||
return fmt.Errorf("tailscale up failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tailscaleSet uses cfg to run 'tailscale set' to set any known configuration
|
||||
// options that are passed in via environment variables. This is run after the
|
||||
// node is in Running state.
|
||||
// node is in Running state and only if TS_AUTH_ONCE is set.
|
||||
func tailscaleSet(ctx context.Context, cfg *settings) error {
|
||||
args := []string{"--socket=" + cfg.Socket, "set"}
|
||||
if cfg.AcceptDNS {
|
||||
@@ -662,16 +685,12 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, routes string
|
||||
return nil
|
||||
}
|
||||
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argv0 := "iptables"
|
||||
if dst.Is6() {
|
||||
argv0 = "ip6tables"
|
||||
}
|
||||
var local string
|
||||
var local netip.Addr
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
@@ -679,52 +698,30 @@ func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []net
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr().String()
|
||||
local = pfx.Addr()
|
||||
break
|
||||
}
|
||||
if local == "" {
|
||||
if !local.IsValid() {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
// Technically, if the control server ever changes the IPs assigned to this
|
||||
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
|
||||
// for now we'll live with it.
|
||||
// Set up a rule that ensures that all packets
|
||||
// except for those received on tailscale0 interface is forwarded to
|
||||
// destination address
|
||||
cmdDNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "!", "-i", "tailscale0", "-j", "DNAT", "--to-destination", dstStr)
|
||||
cmdDNAT.Stdout = os.Stdout
|
||||
cmdDNAT.Stderr = os.Stderr
|
||||
if err := cmdDNAT.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
if err := nfr.DNATNonTailscaleTraffic("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
}
|
||||
// Set up a rule that ensures that all packets sent to the destination
|
||||
// address will have the proxy's IP set as source IP
|
||||
cmdSNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "POSTROUTING", "1", "--destination", dstStr, "-j", "SNAT", "--to-source", local)
|
||||
cmdSNAT.Stdout = os.Stdout
|
||||
cmdSNAT.Stderr = os.Stderr
|
||||
if err := cmdSNAT.Run(); err != nil {
|
||||
return fmt.Errorf("setting up SNAT via iptables failed: %w", err)
|
||||
if err := nfr.AddSNATRuleForDst(local, dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
}
|
||||
|
||||
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
cmdClamp.Stdout = os.Stdout
|
||||
cmdClamp.Stderr = os.Stderr
|
||||
if err := cmdClamp.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing egress proxy rules: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
|
||||
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argv0 := "iptables"
|
||||
if dst.Is6() {
|
||||
argv0 = "ip6tables"
|
||||
}
|
||||
var local string
|
||||
var local netip.Addr
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
@@ -732,26 +729,17 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr().String()
|
||||
local = pfx.Addr()
|
||||
break
|
||||
}
|
||||
if local == "" {
|
||||
if !local.IsValid() {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
// Technically, if the control server ever changes the IPs assigned to this
|
||||
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
|
||||
// for now we'll live with it.
|
||||
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
if err := nfr.AddDNATRule(local, dst); err != nil {
|
||||
return fmt.Errorf("installing ingress proxy rules: %w", err)
|
||||
}
|
||||
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
cmdClamp.Stdout = os.Stdout
|
||||
cmdClamp.Stderr = os.Stderr
|
||||
if err := cmdClamp.Run(); err != nil {
|
||||
return fmt.Errorf("executing iptables failed: %w", err)
|
||||
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
|
||||
return fmt.Errorf("installing ingress proxy rules: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,22 +129,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
|
||||
Name: "no_args",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
|
||||
Env: nil,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -152,21 +146,17 @@ func TestContainerBoot(t *testing.T) {
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -174,21 +164,17 @@ func TestContainerBoot(t *testing.T) {
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey-old-flag",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -197,35 +183,30 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "routes",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -234,9 +215,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -246,13 +224,12 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -261,9 +238,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -273,13 +247,12 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1::/64",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -288,9 +261,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "0",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1::/64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -300,13 +270,12 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1.2.3.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -315,9 +284,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "1",
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1.2.3.0/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -327,22 +293,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_DEST_IP": "1.2.3.4",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
|
||||
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -352,23 +312,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_TAILNET_TARGET_IP": "100.99.99.99",
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
"/usr/bin/iptables -t nat -I PREROUTING 1 ! -i tailscale0 -j DNAT --to-destination 100.99.99.99",
|
||||
"/usr/bin/iptables -t nat -I POSTROUTING 1 --destination 100.99.99.99 -j SNAT --to-source 100.64.0.1",
|
||||
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -389,7 +342,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -405,7 +358,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -414,7 +366,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -422,9 +374,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
@@ -443,22 +392,18 @@ func TestContainerBoot(t *testing.T) {
|
||||
"TS_KUBE_SECRET": "",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
@@ -469,7 +414,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
KubeDenyPatch: true,
|
||||
@@ -477,15 +421,12 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
Notify: runningNotify,
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
@@ -515,7 +456,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
State: ptr.To(ipn.NeedsLogin),
|
||||
},
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -539,7 +480,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
KubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -548,7 +488,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
@@ -556,9 +496,6 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
WantKubeSecret: map[string]string{
|
||||
"authkey": "tskey-key",
|
||||
"device_fqdn": "test-node.test.ts.net",
|
||||
@@ -591,20 +528,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_SOCKS5_SERVER": "localhost:1080",
|
||||
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -612,20 +545,16 @@ func TestContainerBoot(t *testing.T) {
|
||||
Name: "dns",
|
||||
Env: map[string]string{
|
||||
"TS_ACCEPT_DNS": "true",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -634,41 +563,31 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"TS_EXTRA_ARGS": "--widget=rotated",
|
||||
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --widget=rotated",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
|
||||
},
|
||||
},
|
||||
{
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hostname",
|
||||
Env: map[string]string{
|
||||
"TS_HOSTNAME": "my-server",
|
||||
"TS_AUTH_ONCE": "false",
|
||||
"TS_HOSTNAME": "my-server",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
|
||||
},
|
||||
},
|
||||
{
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --hostname=my-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -694,6 +613,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
|
||||
fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
|
||||
fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
|
||||
fmt.Sprint("TS_TEST_FAKE_NETFILTER=true"),
|
||||
}
|
||||
for k, v := range test.Env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
|
||||
@@ -17,7 +17,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
@@ -79,22 +78,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
|
||||
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
nhooyr.io/websocket from tailscale.com/cmd/derper+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
@@ -164,10 +147,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/multierr from tailscale.com/health+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb+
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestProdAutocertHostPolicy(t *testing.T) {
|
||||
@@ -128,3 +129,14 @@ func TestNoContent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -192,8 +192,15 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
}
|
||||
}
|
||||
addIngressBackend(ing.Spec.DefaultBackend, "/")
|
||||
|
||||
var tlsHost string // hostname or FQDN or empty
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
tlsHost = ing.Spec.TLS[0].Hosts[0]
|
||||
}
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
if rule.Host != "" {
|
||||
// Host is optional, but if it's present it must match the TLS host
|
||||
// otherwise we ignore the rule.
|
||||
if rule.Host != "" && rule.Host != tlsHost {
|
||||
a.recorder.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "rule with host %q ignored, unsupported", rule.Host)
|
||||
continue
|
||||
}
|
||||
@@ -208,8 +215,8 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
tags = strings.Split(tstr, ",")
|
||||
}
|
||||
hostname := ing.Namespace + "-" + ing.Name + "-ingress"
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
hostname, _, _ = strings.Cut(ing.Spec.TLS[0].Hosts[0], ".")
|
||||
if tlsHost != "" {
|
||||
hostname, _, _ = strings.Cut(tlsHost, ".")
|
||||
}
|
||||
|
||||
sts := &tailscaleSTSConfig{
|
||||
|
||||
@@ -151,7 +151,7 @@ spec:
|
||||
value: tailscale/tailscale:unstable
|
||||
- name: PROXY_TAGS
|
||||
value: tag:k8s
|
||||
- name: AUTH_PROXY
|
||||
- name: APISERVER_PROXY
|
||||
value: "false"
|
||||
volumeMounts:
|
||||
- name: oauth
|
||||
|
||||
@@ -75,6 +75,7 @@ func main() {
|
||||
wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
|
||||
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
|
||||
hostname = fs.String("hostname", "", "Hostname to register the service under")
|
||||
)
|
||||
|
||||
err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
|
||||
@@ -89,6 +90,7 @@ func main() {
|
||||
|
||||
var s server
|
||||
s.ts.Port = uint16(*wgPort)
|
||||
s.ts.Hostname = *hostname
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
|
||||
@@ -139,11 +139,22 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: localAPIAction("break-derp-conns"),
|
||||
ShortHelp: "break any open DERP connections from the daemon",
|
||||
},
|
||||
{
|
||||
Name: "pick-new-derp",
|
||||
Exec: localAPIAction("pick-new-derp"),
|
||||
ShortHelp: "switch to some other random DERP home region for a short time",
|
||||
},
|
||||
{
|
||||
Name: "force-netmap-update",
|
||||
Exec: localAPIAction("force-netmap-update"),
|
||||
ShortHelp: "force a full no-op netmap update (for load testing)",
|
||||
},
|
||||
{
|
||||
// TODO(bradfitz,maisem): eventually promote this out of debug
|
||||
Name: "reload-config",
|
||||
Exec: reloadConfig,
|
||||
ShortHelp: "reload config",
|
||||
},
|
||||
{
|
||||
Name: "control-knobs",
|
||||
Exec: debugControlKnobs,
|
||||
@@ -446,6 +457,20 @@ func localAPIAction(action string) func(context.Context, []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func reloadConfig(ctx context.Context, args []string) error {
|
||||
ok, err := localClient.ReloadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
printf("config reloaded\n")
|
||||
return nil
|
||||
}
|
||||
printf("config mode not in use\n")
|
||||
os.Exit(1)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func runEnv(ctx context.Context, args []string) error {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
|
||||
@@ -49,6 +49,7 @@ type setArgsT struct {
|
||||
forceDaemon bool
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
@@ -66,6 +67,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
|
||||
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
|
||||
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
@@ -108,6 +111,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
Check: setArgs.updateCheck,
|
||||
Apply: setArgs.updateApply,
|
||||
},
|
||||
PostureChecking: setArgs.postureChecking,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
@@ -725,6 +726,7 @@ func init() {
|
||||
addPrefFlagMapping("nickname", "ProfileName")
|
||||
addPrefFlagMapping("update-check", "AutoUpdate")
|
||||
addPrefFlagMapping("auto-update", "AutoUpdate")
|
||||
addPrefFlagMapping("posture-checking", "PostureChecking")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
|
||||
@@ -63,7 +63,9 @@ func runUpdate(ctx context.Context, args []string) error {
|
||||
err := clientupdate.Update(clientupdate.Arguments{
|
||||
Version: ver,
|
||||
AppStore: updateArgs.appStore,
|
||||
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
|
||||
Logf: func(f string, a ...any) { printf(f+"\n", a...) },
|
||||
Stdout: Stdout,
|
||||
Stderr: Stderr,
|
||||
Confirm: confirmUpdate,
|
||||
})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
|
||||
@@ -80,7 +80,7 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
webServer, cleanup := web.NewServer(ctx, web.ServerOpts{
|
||||
webServer, cleanup := web.NewServer(web.ServerOpts{
|
||||
DevMode: webArgs.dev,
|
||||
CGIMode: webArgs.cgi,
|
||||
PathPrefix: webArgs.prefix,
|
||||
|
||||
@@ -17,7 +17,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
|
||||
@@ -65,22 +64,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
@@ -158,7 +141,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -169,11 +152,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
|
||||
21
cmd/tailscale/tailscale_test.go
Normal file
21
cmd/tailscale/tailscale_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
@@ -86,6 +86,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
|
||||
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/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
@@ -147,6 +148,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
@@ -238,6 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
@@ -275,6 +278,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
@@ -292,13 +296,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/control/controlclient+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
@@ -329,7 +334,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
@@ -337,12 +342,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
@@ -354,11 +361,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
W tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag+
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
@@ -386,7 +394,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
|
||||
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store"
|
||||
@@ -127,6 +128,7 @@ var args struct {
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
confFile string
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
@@ -172,6 +174,7 @@ func main() {
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||
flag.StringVar(&args.confFile, "config", "", "path to config file")
|
||||
|
||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
||||
beCLI()
|
||||
@@ -339,6 +342,17 @@ func run() error {
|
||||
|
||||
sys := new(tsd.System)
|
||||
|
||||
// Parse config, if specified, to fail early if it's invalid.
|
||||
var conf *conffile.Config
|
||||
if args.confFile != "" {
|
||||
var err error
|
||||
conf, err = conffile.Load(args.confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
sys.InitialConfig = conf
|
||||
}
|
||||
|
||||
netMon, err := netmon.New(func(format string, args ...any) {
|
||||
logf(format, args...)
|
||||
})
|
||||
@@ -540,6 +554,10 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
}
|
||||
sys.Set(store)
|
||||
|
||||
if w, ok := sys.Tun.GetOK(); ok {
|
||||
w.Start()
|
||||
}
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
|
||||
@@ -73,11 +73,15 @@ var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
// It calls close(ch) when it's done.
|
||||
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error {
|
||||
defer close(ch)
|
||||
args := []string{"test", "-json", pt.Pattern}
|
||||
args := []string{"test", "--json"}
|
||||
if *flagSudo {
|
||||
args = append(args, "--exec", "sudo -E")
|
||||
}
|
||||
args = append(args, pt.Pattern)
|
||||
args = append(args, otherArgs...)
|
||||
if len(pt.Tests) > 0 {
|
||||
runArg := strings.Join(pt.Tests, "|")
|
||||
args = append(args, "-run", runArg)
|
||||
args = append(args, "--run", runArg)
|
||||
}
|
||||
if debug {
|
||||
fmt.Println("running", strings.Join(args, " "))
|
||||
@@ -177,6 +181,11 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
flagVerbose = flag.Bool("v", false, "verbose")
|
||||
flagSudo = flag.Bool("sudo", false, "run tests with -exec=sudo")
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -187,7 +196,6 @@ func main() {
|
||||
// We run `go test -json` which returns the same information as `go test -v`,
|
||||
// but in a machine-readable format. So this flag is only for testwrapper's
|
||||
// output.
|
||||
v := flag.Bool("v", false, "verbose")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
|
||||
@@ -285,7 +293,7 @@ func main() {
|
||||
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt)
|
||||
continue
|
||||
}
|
||||
if *v || tr.outcome == "fail" {
|
||||
if *flagVerbose || tr.outcome == "fail" {
|
||||
io.Copy(os.Stdout, &tr.logs)
|
||||
}
|
||||
if tr.outcome != "fail" {
|
||||
|
||||
@@ -55,6 +55,7 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/systemd"
|
||||
)
|
||||
|
||||
@@ -566,6 +567,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
|
||||
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
|
||||
if err != nil {
|
||||
c.logf("unable to provide Tailnet field in register request. err: %v", err)
|
||||
}
|
||||
now := c.clock.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
@@ -577,6 +583,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
Timestamp: &now,
|
||||
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
|
||||
NodeKeySignature: nodeKeySignature,
|
||||
Tailnet: tailnet,
|
||||
}
|
||||
if opt.Logout {
|
||||
request.Expiry = time.Unix(123, 0) // far in the past
|
||||
|
||||
@@ -261,7 +261,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
|
||||
func MessageSummary(m Message) string {
|
||||
switch m := m.(type) {
|
||||
case *Ping:
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case *CallMeMaybe:
|
||||
|
||||
63
docs/sysv/tailscale.init
Executable file
63
docs/sysv/tailscale.init
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: tailscaled
|
||||
# Required-Start:
|
||||
# Required-Stop:
|
||||
# Default-Start:
|
||||
# Default-Stop:
|
||||
# Short-Description: Tailscale Mesh Wireguard VPN
|
||||
### END INIT INFO
|
||||
|
||||
set -e
|
||||
|
||||
# /etc/init.d/tailscale: start and stop the Tailscale VPN service
|
||||
|
||||
test -x /usr/sbin/tailscaled || exit 0
|
||||
|
||||
umask 022
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Are we running from init?
|
||||
run_by_init() {
|
||||
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
|
||||
}
|
||||
|
||||
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
log_daemon_msg "Starting Tailscale VPN" "tailscaled" || true
|
||||
if start-stop-daemon --start --oknodo --name tailscaled -m --pidfile /run/tailscaled.pid --background \
|
||||
--exec /usr/sbin/tailscaled -- \
|
||||
--state=/var/lib/tailscale/tailscaled.state \
|
||||
--socket=/run/tailscale/tailscaled.sock \
|
||||
--port 41641;
|
||||
then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping Tailscale VPN" "tailscaled" || true
|
||||
if start-stop-daemon --stop --remove-pidfile --pidfile /run/tailscaled.pid --exec /usr/sbin/tailscaled; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p /run/tailscaled.pid /usr/sbin/tailscaled tailscaled && exit 0 || exit $?
|
||||
;;
|
||||
|
||||
*)
|
||||
log_action_msg "Usage: /etc/init.d/tailscaled {start|stop|status}" || true
|
||||
exit 1
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -115,4 +115,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
|
||||
11
go.mod
11
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||
github.com/andybalholm/brotli v1.0.5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aws/aws-sdk-go-v2 v1.21.0
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/dave/jennifer v1.7.0
|
||||
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/dsnet/try v0.0.3
|
||||
github.com/evanw/esbuild v0.19.4
|
||||
github.com/frankban/quicktest v1.14.5
|
||||
@@ -76,14 +77,14 @@ require (
|
||||
go.uber.org/zap v1.26.0
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/mod v0.12.0
|
||||
golang.org/x/net v0.15.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.12.0
|
||||
golang.org/x/term v0.12.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/time v0.3.0
|
||||
golang.org/x/tools v0.13.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
|
||||
22
go.sum
22
go.sum
@@ -93,8 +93,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||
@@ -233,6 +233,8 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0
|
||||
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
|
||||
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
|
||||
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
|
||||
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
@@ -984,8 +986,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1080,8 +1082,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1177,8 +1179,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1187,8 +1189,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -1 +1 @@
|
||||
f242beecd311476f6e6b9fa3052e253e2301e170
|
||||
d1c91593484a1db2d4de2564f2ef2669814af9c8
|
||||
|
||||
122
ipn/conf.go
Normal file
122
ipn/conf.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// ConfigVAlpha is the config file format for the "alpha0" version.
|
||||
type ConfigVAlpha struct {
|
||||
Locked opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true
|
||||
|
||||
ServerURL *string `json:",omitempty"` // defaults to https://controlplane.tailscale.com
|
||||
AuthKey *string `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if it contains a slash)
|
||||
Enabled opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true
|
||||
|
||||
OperatorUser *string `json:",omitempty"` // local user name who is allowed to operate tailscaled without being root or using sudo
|
||||
Hostname *string `json:",omitempty"`
|
||||
|
||||
AcceptDNS opt.Bool `json:"acceptDNS,omitempty"` // --accept-dns
|
||||
AcceptRoutes opt.Bool `json:"acceptRoutes,omitempty"`
|
||||
|
||||
ExitNode *string `json:"exitNode,omitempty"` // IP, StableID, or MagicDNS base name
|
||||
AllowLANWhileUsingExitNode opt.Bool `json:"allowLANWhileUsingExitNode,omitempty"`
|
||||
|
||||
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
|
||||
DisableSNAT opt.Bool `json:",omitempty"`
|
||||
|
||||
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
|
||||
|
||||
PostureChecking opt.Bool `json:",omitempty"`
|
||||
RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH
|
||||
ShieldsUp opt.Bool `json:",omitempty"`
|
||||
AutoUpdate *AutoUpdatePrefs `json:",omitempty"`
|
||||
ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this
|
||||
|
||||
// TODO(bradfitz,maisem): future something like:
|
||||
// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
|
||||
}
|
||||
|
||||
func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
|
||||
var mp MaskedPrefs
|
||||
if c == nil {
|
||||
return mp, nil
|
||||
}
|
||||
if c.ServerURL != nil {
|
||||
mp.ControlURL = *c.ServerURL
|
||||
mp.ControlURLSet = true
|
||||
}
|
||||
if c.Enabled != "" {
|
||||
mp.WantRunning = c.Enabled.EqualBool(true)
|
||||
mp.WantRunningSet = true
|
||||
}
|
||||
if c.OperatorUser != nil {
|
||||
mp.OperatorUser = *c.OperatorUser
|
||||
mp.OperatorUserSet = true
|
||||
}
|
||||
if c.Hostname != nil {
|
||||
mp.Hostname = *c.Hostname
|
||||
mp.HostnameSet = true
|
||||
}
|
||||
if c.AcceptDNS != "" {
|
||||
mp.CorpDNS = c.AcceptDNS.EqualBool(true)
|
||||
mp.CorpDNSSet = true
|
||||
}
|
||||
if c.AcceptRoutes != "" {
|
||||
mp.RouteAll = c.AcceptRoutes.EqualBool(true)
|
||||
mp.RouteAllSet = true
|
||||
}
|
||||
if c.ExitNode != nil {
|
||||
ip, err := netip.ParseAddr(*c.ExitNode)
|
||||
if err == nil {
|
||||
mp.ExitNodeIP = ip
|
||||
mp.ExitNodeIPSet = true
|
||||
} else {
|
||||
mp.ExitNodeID = tailcfg.StableNodeID(*c.ExitNode)
|
||||
mp.ExitNodeIDSet = true
|
||||
}
|
||||
}
|
||||
if c.AllowLANWhileUsingExitNode != "" {
|
||||
mp.ExitNodeAllowLANAccess = c.AllowLANWhileUsingExitNode.EqualBool(true)
|
||||
mp.ExitNodeAllowLANAccessSet = true
|
||||
}
|
||||
if c.AdvertiseRoutes != nil {
|
||||
mp.AdvertiseRoutes = c.AdvertiseRoutes
|
||||
mp.AdvertiseRoutesSet = true
|
||||
}
|
||||
if c.DisableSNAT != "" {
|
||||
mp.NoSNAT = c.DisableSNAT.EqualBool(true)
|
||||
mp.NoSNAT = true
|
||||
}
|
||||
if c.NetfilterMode != nil {
|
||||
m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
|
||||
if err != nil {
|
||||
return mp, err
|
||||
}
|
||||
mp.NetfilterMode = m
|
||||
mp.NetfilterModeSet = true
|
||||
}
|
||||
if c.PostureChecking != "" {
|
||||
mp.PostureChecking = c.PostureChecking.EqualBool(true)
|
||||
mp.PostureCheckingSet = true
|
||||
}
|
||||
if c.RunSSHServer != "" {
|
||||
mp.RunSSH = c.RunSSHServer.EqualBool(true)
|
||||
mp.RunSSHSet = true
|
||||
}
|
||||
if c.ShieldsUp != "" {
|
||||
mp.ShieldsUp = c.ShieldsUp.EqualBool(true)
|
||||
mp.ShieldsUpSet = true
|
||||
}
|
||||
if c.AutoUpdate != nil {
|
||||
mp.AutoUpdate = *c.AutoUpdate
|
||||
mp.AutoUpdateSet = true
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
66
ipn/conffile/conffile.go
Normal file
66
ipn/conffile/conffile.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package conffile contains code to load, manipulate, and access config file
|
||||
// settings.
|
||||
package conffile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
// Config describes a config file
|
||||
type Config struct {
|
||||
Path string // disk path of HuJSON
|
||||
Raw []byte // raw bytes from disk, in HuJSON form
|
||||
Std []byte // standardized JSON form
|
||||
Version string // "alpha0" for now
|
||||
|
||||
// Parsed is the parsed config, converted from its on-disk version to the
|
||||
// latest known format.
|
||||
//
|
||||
// As of 2023-10-15 there is exactly one format ("alpha0") so this is both
|
||||
// the on-disk format and the in-memory upgraded format.
|
||||
Parsed ipn.ConfigVAlpha
|
||||
}
|
||||
|
||||
// Load reads and parses the config file at the provided path on disk.
|
||||
func Load(path string) (*Config, error) {
|
||||
var c Config
|
||||
c.Path = path
|
||||
|
||||
var err error
|
||||
c.Raw, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Std, err = hujson.Standardize(c.Raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s HuJSON/JSON: %w", path, err)
|
||||
}
|
||||
var ver struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Std, &ver); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
||||
}
|
||||
switch ver.Version {
|
||||
case "":
|
||||
return nil, fmt.Errorf("error parsing config file %s: no \"version\" field defined", path)
|
||||
case "alpha0":
|
||||
default:
|
||||
return nil, fmt.Errorf("error parsing config file %s: unsupported \"version\" value %q; want \"alpha0\" for now", path, ver.Version)
|
||||
}
|
||||
c.Version = ver.Version
|
||||
|
||||
err = json.Unmarshal(c.Std, &c.Parsed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
@@ -52,6 +52,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
21
ipn/ipn_test.go
Normal file
21
ipn/ipn_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
@@ -87,6 +87,7 @@ func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.Netfilte
|
||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
||||
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
|
||||
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
@@ -113,6 +114,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ import (
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/posture"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -67,6 +69,14 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
|
||||
}
|
||||
case "/posture/identity":
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
b.handleC2NPostureIdentityGet(w, r)
|
||||
default:
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
@@ -215,10 +225,42 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /posture/identity received")
|
||||
|
||||
res := tailcfg.C2NPostureIdentityResponse{}
|
||||
|
||||
// Only collect serial numbers if enabled on the client,
|
||||
// this will first check syspolicy, MDM settings like Registry
|
||||
// on Windows or defaults on macOS. If they are not set, it falls
|
||||
// back to the cli-flag, `--posture-checking`.
|
||||
choice, err := syspolicy.GetPreferenceOption(syspolicy.PostureChecking)
|
||||
if err != nil {
|
||||
b.logf(
|
||||
"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
|
||||
b.Prefs().PostureChecking(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
|
||||
sns, err := posture.GetSerialNumbers(b.logf)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.SerialNumbers = sns
|
||||
} else {
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
// If NewUpdater does not return an error, we can update the installation.
|
||||
// Exception: When version.IsMacSysExt returns true, we don't support that
|
||||
// yet. TODO(cpalmer, #6995): Implement it.
|
||||
//
|
||||
// Note that we create the Updater solely to check for errors; we do not
|
||||
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
||||
@@ -226,7 +268,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
|
||||
Supported: err == nil && !version.IsMacSysExt(),
|
||||
Supported: err == nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +84,9 @@ var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
// ACME process. ACME process is used for new domain certs, existing expired
|
||||
// certs or existing certs that should get renewed due to upcoming expiry.
|
||||
//
|
||||
// syncRenewal changes renewal behavior for existing certs that are still valid
|
||||
// but need renewal. When syncRenewal is set, the method blocks until a new
|
||||
// cert is issued. When syncRenewal is not set, existing cert is returned right
|
||||
// away and renewal is kicked off in a background goroutine.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
|
||||
// If a cert is expired, it will be renewed synchronously otherwise it will be
|
||||
// renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
@@ -108,18 +106,16 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair)
|
||||
if err != nil {
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
if shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair); err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if !shouldRenew {
|
||||
return pair, nil
|
||||
}
|
||||
if !syncRenewal {
|
||||
} else if shouldRenew {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
|
||||
}
|
||||
// Synchronous renewal happens below.
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
@@ -130,6 +126,8 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
|
||||
// based on the current time, the cert's expiry, and the ARI check.
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
@@ -365,8 +363,9 @@ type TLSCertKeyPair struct {
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
// getCertPEMCached returns a non-nil keyPair and true if a cached keypair for
|
||||
// domain exists on disk in dir that is valid at the provided now time.
|
||||
// getCertPEMCached returns a non-nil keyPair if a cached keypair for domain
|
||||
// exists on disk in dir that is valid at the provided now time.
|
||||
//
|
||||
// If the keypair is expired, it returns errCertExpired.
|
||||
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
|
||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
|
||||
@@ -12,6 +12,6 @@ type TLSCertKeyPair struct {
|
||||
CertPEM, KeyPEM []byte
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
return nil, errors.New("not implemented for js/wasm")
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -198,7 +199,8 @@ type LocalBackend struct {
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
pm *profileManager // mu guards access
|
||||
conf *conffile.Config // latest parsed config, or nil if not in declarative mode
|
||||
pm *profileManager // mu guards access
|
||||
filterHash deephash.Sum
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
@@ -239,7 +241,6 @@ type LocalBackend struct {
|
||||
peerAPIServer *peerAPIServer // or nil
|
||||
peerAPIListeners []*peerAPIListener
|
||||
loginFlags controlclient.LoginFlags
|
||||
incomingFiles map[*incomingFile]bool
|
||||
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
|
||||
notifyWatchers set.HandleSet[*watchSession]
|
||||
lastStatusTime time.Time // status.AsOf value of the last processed status update
|
||||
@@ -341,6 +342,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
sys: sys,
|
||||
conf: sys.InitialConfig,
|
||||
e: e,
|
||||
dialer: dialer,
|
||||
store: store,
|
||||
@@ -519,6 +521,25 @@ func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
|
||||
b.directFileDoFinalRename = v
|
||||
}
|
||||
|
||||
// ReloadCOnfig reloads the backend's config from disk.
|
||||
//
|
||||
// It returns (false, nil) if not running in declarative mode, (true, nil) on
|
||||
// success, or (false, error) on failure.
|
||||
func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.conf == nil {
|
||||
return false, nil
|
||||
}
|
||||
conf, err := conffile.Load(b.conf.Path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b.conf = conf
|
||||
// TODO(bradfitz): apply things
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// pauseOrResumeControlClientLocked pauses b.cc if there is no network available
|
||||
// or if the LocalBackend is in Stopped state with a valid NetMap. In all other
|
||||
// cases, it unpauses it. It is a no-op if b.cc is nil.
|
||||
@@ -2157,6 +2178,12 @@ func (b *LocalBackend) DebugForceNetmapUpdate() {
|
||||
b.setNetMapLocked(nm)
|
||||
}
|
||||
|
||||
// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP.
|
||||
// See its docs.
|
||||
func (b *LocalBackend) DebugPickNewDERP() error {
|
||||
return b.sys.MagicSock.Get().DebugPickNewDERP()
|
||||
}
|
||||
|
||||
// send delivers n to the connected frontend and any API watchers from
|
||||
// LocalBackend.WatchNotifications (via the LocalAPI).
|
||||
//
|
||||
@@ -2213,10 +2240,7 @@ func (b *LocalBackend) sendFileNotify() {
|
||||
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
|
||||
// in JSON to clients. They distinguish between empty and non-nil
|
||||
// to know whether a Notify should be able about files.
|
||||
n.IncomingFiles = make([]ipn.PartialFile, 0)
|
||||
for f := range b.incomingFiles {
|
||||
n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
|
||||
}
|
||||
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
|
||||
b.mu.Unlock()
|
||||
|
||||
sort.Slice(n.IncomingFiles, func(i, j int) bool {
|
||||
@@ -3548,12 +3572,13 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: b.logf,
|
||||
Clock: b.clock,
|
||||
RootDir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
DirectFileDoFinalRename: b.directFileDoFinalRename,
|
||||
taildrop: &taildrop.Manager{
|
||||
Logf: b.logf,
|
||||
Clock: tstime.DefaultClock{b.clock},
|
||||
Dir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
AvoidFinalRename: !b.directFileDoFinalRename,
|
||||
SendFileNotify: b.sendFileNotify,
|
||||
},
|
||||
}
|
||||
if dm, ok := b.sys.DNSManager.GetOK(); ok {
|
||||
@@ -3768,6 +3793,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
||||
hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
|
||||
hi.RequestTags = prefs.AdvertiseTags().AsSlice()
|
||||
hi.ShieldsUp = prefs.ShieldsUp()
|
||||
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply
|
||||
|
||||
var sshHostKeys []string
|
||||
if prefs.RunSSH() && envknob.CanSSHD() {
|
||||
@@ -4590,19 +4616,6 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
|
||||
return cc.SetDNS(ctx, req)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.incomingFiles == nil {
|
||||
b.incomingFiles = make(map[*incomingFile]bool)
|
||||
}
|
||||
if active {
|
||||
b.incomingFiles[inf] = true
|
||||
} else {
|
||||
delete(b.incomingFiles, inf)
|
||||
}
|
||||
}
|
||||
|
||||
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
|
||||
svcs := peer.Hostinfo().Services()
|
||||
for i := range svcs.LenIter() {
|
||||
|
||||
@@ -39,10 +39,9 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -56,7 +55,7 @@ type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
resolver *resolver.Resolver
|
||||
|
||||
taildrop *taildrop.Handler
|
||||
taildrop *taildrop.Manager
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -306,6 +305,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/partial-files/") {
|
||||
h.handlePartialFileGet(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
metricPutCalls.Add(1)
|
||||
h.handlePeerPut(w, r)
|
||||
@@ -586,64 +589,6 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
|
||||
fmt.Fprintln(w, "</pre>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
clock tstime.Clock
|
||||
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
sendFileNotify func() // called when done
|
||||
partialPath string // non-empty in direct mode
|
||||
|
||||
mu sync.Mutex
|
||||
copied int64
|
||||
done bool
|
||||
lastNotify time.Time
|
||||
}
|
||||
|
||||
func (f *incomingFile) markAndNotifyDone() {
|
||||
f.mu.Lock()
|
||||
f.done = true
|
||||
f.mu.Unlock()
|
||||
f.sendFileNotify()
|
||||
}
|
||||
|
||||
func (f *incomingFile) Write(p []byte) (n int, err error) {
|
||||
n, err = f.w.Write(p)
|
||||
|
||||
var needNotify bool
|
||||
defer func() {
|
||||
if needNotify {
|
||||
f.sendFileNotify()
|
||||
}
|
||||
}()
|
||||
if n > 0 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.copied += int64(n)
|
||||
now := f.clock.Now()
|
||||
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
|
||||
f.lastNotify = now
|
||||
needNotify = true
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return ipn.PartialFile{
|
||||
Name: f.name,
|
||||
Started: f.started,
|
||||
DeclaredSize: f.size,
|
||||
Received: f.copied,
|
||||
PartialPath: f.partialPath,
|
||||
Done: f.done,
|
||||
}
|
||||
}
|
||||
|
||||
// canPutFile reports whether h can put a file ("Taildrop") to this node.
|
||||
func (h *peerAPIHandler) canPutFile() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
@@ -686,13 +631,71 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
||||
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !envknob.CanTaildrop() {
|
||||
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
|
||||
var errMisconfiguredInternals = errors.New("misconfigured internals")
|
||||
|
||||
func (h *peerAPIHandler) extractBaseName(rawPath, prefix string) (ret string, err error) {
|
||||
prefix, ok := strings.CutPrefix(rawPath, prefix)
|
||||
if !ok {
|
||||
return "", errMisconfiguredInternals
|
||||
}
|
||||
if prefix == "" {
|
||||
return "", taildrop.ErrInvalidFileName
|
||||
}
|
||||
if strings.Contains(prefix, "/") {
|
||||
return "", taildrop.ErrInvalidFileName
|
||||
}
|
||||
baseName, err := url.PathUnescape(prefix)
|
||||
if err == errMisconfiguredInternals {
|
||||
return "", errMisconfiguredInternals
|
||||
} else if err != nil {
|
||||
return "", taildrop.ErrInvalidFileName
|
||||
}
|
||||
return baseName, nil
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePartialFileGet(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.hasCapFileSharing() {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "expected method GET", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var resp any
|
||||
var err error
|
||||
id := taildrop.ClientID(h.peerNode.StableID())
|
||||
|
||||
if r.URL.Path == "/v0/partial-files/" {
|
||||
resp, err = h.ps.taildrop.PartialFiles(id)
|
||||
} else {
|
||||
baseName, _ := h.extractBaseName(r.URL.EscapedPath(), "/v0/partial-files/")
|
||||
ranges, ok := httphdr.ParseRange(r.Header.Get("Range"))
|
||||
if !ok || len(ranges) != 1 || ranges[0].Length < 0 {
|
||||
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
offset := ranges[0].Start
|
||||
length := ranges[0].Length
|
||||
if length == 0 {
|
||||
length = -1 // httphdr.Range.Length == 0 implies reading the rest of file
|
||||
}
|
||||
resp, err = h.ps.taildrop.HashPartialFile(id, baseName, offset, length)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canPutFile() {
|
||||
http.Error(w, "Taildrop access denied", http.StatusForbidden)
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.ps.b.hasCapFileSharing() {
|
||||
@@ -703,113 +706,38 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if mayDeref(h.ps.taildrop).RootDir == "" {
|
||||
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if distro.Get() == distro.Unraid && !h.ps.taildrop.DirectFileMode {
|
||||
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured internals", 500)
|
||||
return
|
||||
}
|
||||
if suffix == "" {
|
||||
http.Error(w, "empty filename", 400)
|
||||
return
|
||||
}
|
||||
if strings.Contains(suffix, "/") {
|
||||
http.Error(w, "directories not supported", 400)
|
||||
return
|
||||
}
|
||||
baseName, err := url.PathUnescape(suffix)
|
||||
baseName, err := h.extractBaseName(r.URL.EscapedPath(), "/v0/put/")
|
||||
if err != nil {
|
||||
http.Error(w, "bad path encoding", 400)
|
||||
return
|
||||
}
|
||||
dstFile, ok := h.ps.taildrop.DiskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t0 := h.ps.b.clock.Now()
|
||||
// TODO(bradfitz): prevent same filename being sent by two peers at once
|
||||
id := taildrop.ClientID(h.peerNode.StableID())
|
||||
|
||||
// prevent same filename being sent twice
|
||||
if _, err := os.Stat(dstFile); err == nil {
|
||||
http.Error(w, "file exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
partialFile := dstFile + taildrop.PartialSuffix
|
||||
f, err := os.Create(partialFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", taildrop.RedactErr(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
os.Remove(partialFile)
|
||||
}
|
||||
}()
|
||||
var finalSize int64
|
||||
var inFile *incomingFile
|
||||
if r.ContentLength != 0 {
|
||||
inFile = &incomingFile{
|
||||
clock: h.ps.b.clock,
|
||||
name: baseName,
|
||||
started: h.ps.b.clock.Now(),
|
||||
size: r.ContentLength,
|
||||
w: f,
|
||||
sendFileNotify: h.ps.b.sendFileNotify,
|
||||
}
|
||||
if h.ps.taildrop.DirectFileMode {
|
||||
inFile.partialPath = partialFile
|
||||
}
|
||||
h.ps.b.registerIncomingFile(inFile, true)
|
||||
defer h.ps.b.registerIncomingFile(inFile, false)
|
||||
n, err := io.Copy(inFile, r.Body)
|
||||
if err != nil {
|
||||
err = taildrop.RedactErr(err)
|
||||
f.Close()
|
||||
h.logf("put Copy error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
var offset int64
|
||||
if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
|
||||
ranges, ok := httphdr.ParseRange(rangeHdr)
|
||||
if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
|
||||
http.Error(w, "invalid Range header", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
finalSize = n
|
||||
offset = ranges[0].Start
|
||||
}
|
||||
if err := taildrop.RedactErr(f.Close()); err != nil {
|
||||
h.logf("put Close error: %v", err)
|
||||
n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
|
||||
switch err {
|
||||
case nil:
|
||||
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
||||
io.WriteString(w, "{}\n")
|
||||
case taildrop.ErrNoTaildrop:
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
case taildrop.ErrInvalidFileName:
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
case taildrop.ErrFileExists:
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
default:
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.taildrop.DirectFileMode && !h.ps.taildrop.DirectFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(partialFile, dstFile); err != nil {
|
||||
err = taildrop.RedactErr(err)
|
||||
h.logf("put final rename: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
success = true
|
||||
io.WriteString(w, "{}\n")
|
||||
h.ps.taildrop.KnownEmpty.Store(false)
|
||||
h.ps.b.sendFileNotify()
|
||||
}
|
||||
|
||||
func approxSize(n int64) string {
|
||||
@@ -882,7 +810,7 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
dh := health.DebugHandler("dnsfwd")
|
||||
if dh == nil {
|
||||
http.Error(w, "not wired up", 500)
|
||||
http.Error(w, "not wired up", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dh.ServeHTTP(w, r)
|
||||
@@ -1020,9 +948,9 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
h.logf("handleDNS fwd error: %v", err)
|
||||
if err := ctx.Err(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
http.Error(w, "DNS forwarding error", 500)
|
||||
http.Error(w, "DNS forwarding error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -15,11 +14,12 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -68,7 +68,7 @@ func bodyNotContains(sub string) check {
|
||||
|
||||
func fileHasSize(name string, size int) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.taildrop.RootDir
|
||||
root := e.ph.ps.taildrop.Dir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
|
||||
return
|
||||
@@ -84,7 +84,7 @@ func fileHasSize(name string, size int) check {
|
||||
|
||||
func fileHasContents(name string, want string) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.taildrop.RootDir
|
||||
root := e.ph.ps.taildrop.Dir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check contents of %q", name)
|
||||
return
|
||||
@@ -173,7 +173,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("Taildrop access denied"),
|
||||
bodyContains("Taildrop disabled"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -193,7 +193,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
checks: checks(
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("Taildrop disabled; no storage directory"),
|
||||
),
|
||||
},
|
||||
@@ -250,7 +250,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.partial", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -260,7 +260,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -270,7 +270,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/.", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -280,7 +280,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("empty filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -290,7 +290,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo/bar", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("directories not supported"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -300,7 +300,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -310,7 +310,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -320,7 +320,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -330,7 +330,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -340,7 +340,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -372,7 +372,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -382,7 +382,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%00", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -392,7 +392,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%01", nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -402,7 +402,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -412,7 +412,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil)},
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
bodyContains("invalid filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -443,23 +443,69 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_duplicate_zero_length",
|
||||
name: "duplicate_zero_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil), httptest.NewRequest("PUT", "/v0/put/foo", nil)},
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
},
|
||||
checks: checks(
|
||||
httpStatus(409),
|
||||
bodyContains("file exists"),
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 0}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_duplicate_non_zero_length_content_length",
|
||||
name: "duplicate_non_zero_length_content_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")), httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
|
||||
},
|
||||
checks: checks(
|
||||
httpStatus(409),
|
||||
bodyContains("file exists"),
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 8}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate_different_files",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
reqs: []*http.Request{
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("fizz")),
|
||||
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("buzz")),
|
||||
},
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
func(t *testing.T, env *peerAPITestEnv) {
|
||||
got, err := env.ph.ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("WaitingFiles error: %v", err)
|
||||
}
|
||||
want := []apitype.WaitingFile{{Name: "foo", Size: 4}, {Name: "foo (1)", Size: 4}}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -494,9 +540,11 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
if !tt.omitRoot {
|
||||
rootDir = t.TempDir()
|
||||
if e.ph.ps.taildrop == nil {
|
||||
e.ph.ps.taildrop = &taildrop.Handler{}
|
||||
e.ph.ps.taildrop = &taildrop.Manager{
|
||||
Logf: e.logBuf.Logf,
|
||||
}
|
||||
}
|
||||
e.ph.ps.taildrop.RootDir = rootDir
|
||||
e.ph.ps.taildrop.Dir = rootDir
|
||||
}
|
||||
for _, req := range tt.reqs {
|
||||
e.rr = httptest.NewRecorder()
|
||||
@@ -535,10 +583,9 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
capFileSharing: true,
|
||||
clock: &tstest.Clock{},
|
||||
},
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: t.Logf,
|
||||
Clock: &tstest.Clock{},
|
||||
RootDir: dir,
|
||||
taildrop: &taildrop.Manager{
|
||||
Logf: t.Logf,
|
||||
Dir: dir,
|
||||
},
|
||||
}
|
||||
ph := &peerAPIHandler{
|
||||
@@ -579,92 +626,6 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests "foo.jpg.deleted" marks (for Windows).
|
||||
func TestDeletedMarkers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ps := &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
logf: t.Logf,
|
||||
capFileSharing: true,
|
||||
},
|
||||
taildrop: &taildrop.Handler{
|
||||
RootDir: dir,
|
||||
},
|
||||
}
|
||||
|
||||
nothingWaiting := func() {
|
||||
t.Helper()
|
||||
ps.taildrop.KnownEmpty.Store(false)
|
||||
if ps.taildrop.HasFilesWaiting() {
|
||||
t.Fatal("unexpected files waiting")
|
||||
}
|
||||
}
|
||||
touch := func(base string) {
|
||||
t.Helper()
|
||||
if err := taildrop.TouchFile(filepath.Join(dir, base)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wantEmptyTempDir := func() {
|
||||
t.Helper()
|
||||
if fis, err := os.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(fis) > 0 && runtime.GOOS != "windows" {
|
||||
for _, fi := range fis {
|
||||
t.Errorf("unexpected file in tempdir: %q", fi.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
wf, err := ps.taildrop.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wf) != 0 {
|
||||
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err == nil {
|
||||
rc.Close()
|
||||
t.Fatal("unexpected foo.jpg open")
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
// And verify basics still work in non-deleted cases.
|
||||
touch("foo.jpg")
|
||||
touch("bar.jpg.deleted")
|
||||
if wf, err := ps.taildrop.WaitingFiles(); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(wf) != 1 {
|
||||
t.Errorf("WaitingFiles = %d; want 1", len(wf))
|
||||
} else if wf[0].Name != "foo.jpg" {
|
||||
t.Errorf("unexpected waiting file %+v", wf[0])
|
||||
}
|
||||
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
|
||||
@@ -719,67 +680,3 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err func() error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "PathError",
|
||||
err: func() error {
|
||||
return &os.PathError{
|
||||
Op: "open",
|
||||
Path: "/tmp/sensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `open redacted.41360718: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "LinkError",
|
||||
err: func() error {
|
||||
return &os.LinkError{
|
||||
Op: "symlink",
|
||||
Old: "/tmp/sensitive.txt",
|
||||
New: "/tmp/othersensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "something else",
|
||||
err: func() error { return errors.New("i am another error type") },
|
||||
want: `i am another error type`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// For debugging
|
||||
var i int
|
||||
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
|
||||
t.Logf("%d: %T @ %p", i, err, err)
|
||||
i++
|
||||
}
|
||||
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
got := taildrop.RedactErr(tc.err()).Error()
|
||||
if got != tc.want {
|
||||
t.Errorf("err = %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
t.Run("Wrapped", func(t *testing.T) {
|
||||
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
|
||||
want := "wrapped error: " + tc.want
|
||||
|
||||
got := taildrop.RedactErr(wrapped).Error()
|
||||
if got != want {
|
||||
t.Errorf("err = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +160,9 @@ func (s *serveListener) shouldWarnAboutListenError(err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// handleServeListenersAccept accepts connections for the Listener.
|
||||
// Calls incoming handler in a new goroutine for each accepted connection.
|
||||
// handleServeListenersAccept accepts connections for the Listener. It calls the
|
||||
// handler in a new goroutine for each accepted connection. This is used to
|
||||
// handle local "tailscale serve" traffic originating from the machine itself.
|
||||
func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
@@ -171,7 +172,7 @@ func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
||||
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
|
||||
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr)
|
||||
if handler == nil {
|
||||
s.b.logf("serve RST for %v", srcAddr)
|
||||
s.b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, s.ap.Port())
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
@@ -325,32 +326,43 @@ func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
|
||||
return b.setServeConfigLocked(sc, "")
|
||||
}
|
||||
|
||||
// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer
|
||||
// proxied to the local node over the PeerAPI.
|
||||
// Target represents the destination HostPort of the conn.
|
||||
// srcAddr represents the source AddrPort and not that of the ingressPeer.
|
||||
// getConnOrReset is a callback to get the connection, or reset if the connection
|
||||
// is no longer available.
|
||||
// sendRST is a callback to send a TCP RST to the ingressPeer indicating that
|
||||
// the connection was not accepted.
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
// TODO(maisem,bradfitz): make this not alloc for every conn.
|
||||
logf := logger.WithPrefix(b.logf, "handleIngress: ")
|
||||
|
||||
if !sc.Valid() {
|
||||
b.logf("localbackend: got ingress conn w/o serveConfig; rejecting")
|
||||
logf("got ingress conn w/o serveConfig; rejecting")
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
if !sc.HasFunnelForTarget(target) {
|
||||
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
|
||||
logf("got ingress conn for unconfigured %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(string(target))
|
||||
if err != nil {
|
||||
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
|
||||
logf("got ingress conn for bad target %q; rejecting", target)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
@@ -360,7 +372,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
if handler != nil {
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
@@ -371,12 +383,13 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
// extend serveHTTPContext or similar.
|
||||
handler := b.tcpHandlerForServe(dport, srcAddr)
|
||||
if handler == nil {
|
||||
logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
@@ -390,13 +403,11 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
b.mu.Unlock()
|
||||
|
||||
if !sc.Valid() {
|
||||
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
|
||||
return nil
|
||||
}
|
||||
|
||||
tcph, ok := sc.FindTCP(dport)
|
||||
if !ok {
|
||||
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -440,7 +451,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni, false)
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -468,7 +479,6 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -747,7 +757,7 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, hi.ServerName, false)
|
||||
pair, err := b.GetCertPEM(ctx, hi.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
pair, err := h.b.GetCertPEM(r.Context(), domain, true)
|
||||
pair, err := h.b.GetCertPEM(r.Context(), domain)
|
||||
if err != nil {
|
||||
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
|
||||
// GetCertPEM (and everywhere) should carry info info to get whether
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -45,6 +46,7 @@ import (
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/osdiag"
|
||||
@@ -79,6 +81,7 @@ var handler = map[string]localAPIHandler{
|
||||
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
|
||||
"debug-capture": (*Handler).serveDebugCapture,
|
||||
"debug-log": (*Handler).serveDebugLog,
|
||||
"debug-web-client": (*Handler).serveDebugWebClient,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
||||
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
|
||||
@@ -93,6 +96,7 @@ var handler = map[string]localAPIHandler{
|
||||
"ping": (*Handler).servePing,
|
||||
"prefs": (*Handler).servePrefs,
|
||||
"pprof": (*Handler).servePprof,
|
||||
"reload-config": (*Handler).reloadConfig,
|
||||
"reset-auth": (*Handler).serveResetAuth,
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
@@ -279,23 +283,23 @@ func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
httpReq, err := http.NewRequest("POST", "https://unused/machine/id-token", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
resp, err := h.b.DoNoiseRequest(httpReq)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -408,36 +412,52 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveWhoIsWithBackend(w, r, h.b)
|
||||
}
|
||||
|
||||
// localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
|
||||
// by the localapi WhoIs method.
|
||||
type localBackendWhoIsMethods interface {
|
||||
WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
PeerCaps(netip.Addr) tailcfg.PeerCapMap
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "whois access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
b := h.b
|
||||
var ipp netip.AddrPort
|
||||
if v := r.FormValue("addr"); v != "" {
|
||||
var err error
|
||||
ipp, err = netip.ParseAddrPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'addr' parameter", 400)
|
||||
return
|
||||
if ip, err := netip.ParseAddr(v); err == nil {
|
||||
ipp = netip.AddrPortFrom(ip, 0)
|
||||
} else {
|
||||
var err error
|
||||
ipp, err = netip.ParseAddrPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'addr' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'addr' parameter", 400)
|
||||
http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ipp)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP:port", 404)
|
||||
http.Error(w, "no match for IP:port", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
res := &apitype.WhoIsResponse{
|
||||
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
|
||||
UserProfile: &u, // always non-nil per WhoIsResponse contract
|
||||
CapMap: b.PeerCaps(ipp.Addr()),
|
||||
}
|
||||
if n.Addresses().Len() > 0 {
|
||||
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -566,13 +586,15 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
case "pick-new-derp":
|
||||
err = h.b.DebugPickNewDERP()
|
||||
case "":
|
||||
err = fmt.Errorf("missing parameter 'action'")
|
||||
default:
|
||||
err = fmt.Errorf("unknown action %q", action)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -589,7 +611,7 @@ func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -817,6 +839,26 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
|
||||
servePprofFunc(w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ok, err := h.b.ReloadConfig()
|
||||
var res apitype.ReloadConfigResponse
|
||||
res.Reloaded = ok
|
||||
if err != nil {
|
||||
res.Err = err.Error()
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&res)
|
||||
}
|
||||
|
||||
func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "reset-auth modify access denied", http.StatusForbidden)
|
||||
@@ -919,18 +961,18 @@ func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.R
|
||||
|
||||
ipStr := r.FormValue("ip")
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid IP", 400)
|
||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1007,7 +1049,7 @@ func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.b.StartLoginInteractive()
|
||||
@@ -1021,7 +1063,7 @@ func (h *Handler) serveStart(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var o ipn.Options
|
||||
@@ -1044,7 +1086,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := h.b.Logout(r.Context())
|
||||
@@ -1052,7 +1094,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1069,7 +1111,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
if err := json.NewDecoder(r.Body).Decode(mp); err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
@@ -1107,7 +1149,7 @@ func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
p := new(ipn.Prefs)
|
||||
if err := json.NewDecoder(r.Body).Decode(p); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := h.b.CheckPrefs(p)
|
||||
@@ -1131,7 +1173,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if suffix == "" {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list files", 400)
|
||||
http.Error(w, "want GET to list files", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
@@ -1148,7 +1190,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
wfs, err := h.b.AwaitWaitingFiles(ctx)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1157,12 +1199,12 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
name, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, "bad filename", 400)
|
||||
http.Error(w, "bad filename", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.Method == "DELETE" {
|
||||
if err := h.b.DeleteFile(name); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@@ -1170,7 +1212,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
rc, size, err := h.b.OpenFile(name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
@@ -1184,7 +1226,7 @@ func writeErrorJSON(w http.ResponseWriter, err error) {
|
||||
err = errors.New("unexpected nil error")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
type E struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -1197,7 +1239,7 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list targets", 400)
|
||||
http.Error(w, "want GET to list targets", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fts, err := h.b.FileTargets()
|
||||
@@ -1237,12 +1279,12 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "want PUT to put file", 400)
|
||||
http.Error(w, "want PUT to put file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fts, err := h.b.FileTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1253,7 +1295,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
stableIDStr, filenameEscaped, ok := strings.Cut(upath, "/")
|
||||
if !ok {
|
||||
http.Error(w, "bogus URL", 400)
|
||||
http.Error(w, "bogus URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
stableID := tailcfg.StableNodeID(stableIDStr)
|
||||
@@ -1266,20 +1308,60 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if ft == nil {
|
||||
http.Error(w, "node not found", 404)
|
||||
http.Error(w, "node not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
dstURL, err := url.Parse(ft.PeerAPIURL)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus peer URL", 500)
|
||||
http.Error(w, "bogus peer URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, r.Body)
|
||||
|
||||
// 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.
|
||||
offset, remainingBody, err := taildrop.ResumeReader(r.Body, func(offset, length int64) (taildrop.FileChecksums, error) {
|
||||
client := &http.Client{
|
||||
Transport: h.b.Dialer().PeerAPITransport(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), "GET", "http://peer/v0/partial-files/"+filenameEscaped, nil)
|
||||
if err != nil {
|
||||
return taildrop.FileChecksums{}, err
|
||||
}
|
||||
|
||||
rangeHdr, ok := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: length}})
|
||||
if !ok {
|
||||
return taildrop.FileChecksums{}, fmt.Errorf("invalid offset and length")
|
||||
}
|
||||
req.Header.Set("Range", rangeHdr)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return taildrop.FileChecksums{}, err
|
||||
}
|
||||
|
||||
var checksums taildrop.FileChecksums
|
||||
err = json.NewDecoder(resp.Body).Decode(&checksums)
|
||||
return checksums, err
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "bogus outreq", 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, remainingBody)
|
||||
if err != nil {
|
||||
http.Error(w, "bogus outreq", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
outReq.ContentLength = r.ContentLength
|
||||
if offset > 0 {
|
||||
rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{offset, 0}})
|
||||
outReq.Header.Set("Range", rangeHdr)
|
||||
if outReq.ContentLength >= 0 {
|
||||
outReq.ContentLength -= offset
|
||||
}
|
||||
}
|
||||
|
||||
rp := httputil.NewSingleHostReverseProxy(dstURL)
|
||||
rp.Transport = h.b.Dialer().PeerAPITransport()
|
||||
@@ -1292,7 +1374,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
@@ -1307,7 +1389,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET", 400)
|
||||
http.Error(w, "want GET", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1352,22 +1434,22 @@ func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "want POST", 400)
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ipStr := r.FormValue("ip")
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid IP", 400)
|
||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
pingTypeStr := r.FormValue("type")
|
||||
if pingTypeStr == "" {
|
||||
http.Error(w, "missing 'type' parameter", 400)
|
||||
http.Error(w, "missing 'type' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
size := 0
|
||||
@@ -1375,15 +1457,15 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
||||
if sizeStr != "" {
|
||||
size, err = strconv.Atoi(sizeStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'size' parameter", 400)
|
||||
http.Error(w, "invalid 'size' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if size != 0 && tailcfg.PingType(pingTypeStr) != tailcfg.PingDisco {
|
||||
http.Error(w, "'size' parameter is only supported with disco pings", 400)
|
||||
http.Error(w, "'size' parameter is only supported with disco pings", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if size > magicsock.MaxDiscoPingSize {
|
||||
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), 400)
|
||||
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1464,7 +1546,7 @@ func (h *Handler) serveSetPushDeviceToken(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
var params apitype.SetPushDeviceTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hostinfo.SetPushDeviceToken(params.PushDeviceToken)
|
||||
@@ -1485,7 +1567,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
var clientMetrics []clientMetricJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1497,7 +1579,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
metric.Add(int64(m.Value))
|
||||
} else {
|
||||
if clientmetric.HasPublished(m.Name) {
|
||||
http.Error(w, "Already have a metric named "+m.Name, 400)
|
||||
http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var metric *clientmetric.Metric
|
||||
@@ -1507,7 +1589,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
case "gauge":
|
||||
metric = clientmetric.NewGauge(m.Name)
|
||||
default:
|
||||
http.Error(w, "Unknown metric type "+m.Type, 400)
|
||||
http.Error(w, "Unknown metric type "+m.Type, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
metrics[m.Name] = metric
|
||||
@@ -1531,7 +1613,7 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1583,7 +1665,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var req initRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1594,7 +1676,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1617,7 +1699,7 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var req modifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1677,14 +1759,14 @@ func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
var req verifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON for verifyRequest body", 400)
|
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1704,7 +1786,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
body := io.LimitReader(r.Body, 1024*1024)
|
||||
secret, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading secret", 400)
|
||||
http.Error(w, "reading secret", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1728,7 +1810,7 @@ func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||
// Require a JSON stanza for the body as an additional CSRF protection.
|
||||
var req struct{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1763,7 +1845,7 @@ func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j, err := json.MarshalIndent(updates, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1826,7 +1908,7 @@ func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Req
|
||||
|
||||
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
@@ -2037,6 +2119,65 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// serveDebugWebClient is for use by the web client to communicate with
|
||||
// the control server for browser auth sessions.
|
||||
//
|
||||
// This is an unsupported localapi endpoint and restricted to flagged
|
||||
// domains on the control side. TODO(tailscale/#14335): Rename this handler.
|
||||
func (h *Handler) serveDebugWebClient(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type reqData struct {
|
||||
ID string
|
||||
Src tailcfg.NodeID
|
||||
}
|
||||
var data reqData
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil || !nm.SelfNode.Valid() {
|
||||
http.Error(w, "[unexpected] no self node", 400)
|
||||
return
|
||||
}
|
||||
dst := nm.SelfNode.ID()
|
||||
|
||||
var noiseURL string
|
||||
if data.ID != "" {
|
||||
noiseURL = fmt.Sprintf("https://unused/machine/webclient/wait/%d/to/%d/%s", data.Src, dst, data.ID)
|
||||
} else {
|
||||
noiseURL = fmt.Sprintf("https://unused/machine/webclient/init/%d/to/%d", data.Src, dst)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(r.Context(), "POST", noiseURL, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.b.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
@@ -2081,7 +2222,7 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var logRequest logRequestJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,15 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -77,3 +81,68 @@ func TestSetPushDeviceToken(t *testing.T) {
|
||||
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type whoIsBackend struct {
|
||||
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
peerCaps map[netip.Addr]tailcfg.PeerCapMap
|
||||
}
|
||||
|
||||
func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
return b.whoIs(ipp)
|
||||
}
|
||||
|
||||
func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
|
||||
return b.peerCaps[ip]
|
||||
}
|
||||
|
||||
// Tests that the WhoIs handler accepts either IPs or IP:ports.
|
||||
//
|
||||
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
|
||||
func TestWhoIsJustIP(t *testing.T) {
|
||||
h := &Handler{
|
||||
PermitRead: true,
|
||||
}
|
||||
for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
|
||||
rec := httptest.NewRecorder()
|
||||
t.Run(input, func(t *testing.T) {
|
||||
b := whoIsBackend{
|
||||
whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
if !strings.Contains(input, ":") {
|
||||
want := netip.MustParseAddrPort("100.101.102.103:0")
|
||||
if ipp != want {
|
||||
t.Fatalf("backend called with %v; want %v", ipp, want)
|
||||
}
|
||||
}
|
||||
return (&tailcfg.Node{
|
||||
ID: 123,
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.101.102.103/32"),
|
||||
},
|
||||
}).View(),
|
||||
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
|
||||
true
|
||||
},
|
||||
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
|
||||
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
|
||||
"foo": {`"bar"`},
|
||||
},
|
||||
},
|
||||
}
|
||||
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
|
||||
|
||||
var res apitype.WhoIsResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
|
||||
t.Errorf("res.Node.ID=%v, want %v", got, want)
|
||||
}
|
||||
if got, want := res.UserProfile.DisplayName, "foo"; got != want {
|
||||
t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
|
||||
}
|
||||
if got, want := len(res.CapMap), 1; got != want {
|
||||
t.Errorf("capmap size=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,10 @@ type Prefs struct {
|
||||
// AutoUpdatePrefs docs for more details.
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
|
||||
// PostureChecking enables the collection of information used for device
|
||||
// posture checks.
|
||||
PostureChecking bool
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
@@ -246,6 +250,7 @@ type MaskedPrefs struct {
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
ProfileNameSet bool `json:",omitempty"`
|
||||
AutoUpdateSet bool `json:",omitempty"`
|
||||
PostureCheckingSet bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
|
||||
@@ -439,7 +444,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
||||
p.Persist.Equals(p2.Persist) &&
|
||||
p.ProfileName == p2.ProfileName &&
|
||||
p.AutoUpdate == p2.AutoUpdate
|
||||
p.AutoUpdate == p2.AutoUpdate &&
|
||||
p.PostureChecking == p2.PostureChecking
|
||||
}
|
||||
|
||||
func (au AutoUpdatePrefs) Pretty() string {
|
||||
|
||||
@@ -57,6 +57,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"OperatorUser",
|
||||
"ProfileName",
|
||||
"AutoUpdate",
|
||||
"PostureChecking",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
@@ -304,6 +305,16 @@ func TestPrefsEqual(t *testing.T) {
|
||||
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{PostureChecking: true},
|
||||
&Prefs{PostureChecking: true},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{PostureChecking: true},
|
||||
&Prefs{PostureChecking: false},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equals(tt.b)
|
||||
|
||||
@@ -37,6 +37,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [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.18/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/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/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))
|
||||
@@ -85,13 +86,13 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [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/6213f710f925/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.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.14.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/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.17.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.12.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.3.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.12.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.12.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.13.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.13.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.13.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.3.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))
|
||||
|
||||
@@ -265,6 +265,13 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
if err := env.nmIsUsingResolved(); err != nil {
|
||||
// If systemd-resolved is not running at all, then we don't have any
|
||||
// other choice: we take direct control of DNS.
|
||||
dbg("nm-resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
|
||||
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
|
||||
@@ -270,6 +270,18 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/9687
|
||||
name: "networkmanager_endeavouros",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"search example.com localdomain",
|
||||
"nameserver 10.0.0.1"),
|
||||
nmRunning("1.44.2", false)),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
|
||||
"dns: [rc=nm resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@@ -13,13 +14,17 @@ func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
|
||||
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
// Debian resolvconf doesn't understand --version, and
|
||||
// exits with a specific error code.
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
|
||||
return "debian"
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
|
||||
return "debian"
|
||||
}
|
||||
// Treat everything else as openresolv, by far the more popular implementation.
|
||||
return "openresolv"
|
||||
}
|
||||
|
||||
@@ -39,14 +39,22 @@ import (
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
var disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER")
|
||||
var (
|
||||
optRecursiveResolver = envknob.RegisterOptBool("TS_DNSFALLBACK_RECURSIVE_RESOLVER")
|
||||
disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER") // legacy pre-1.52 env knob name
|
||||
)
|
||||
|
||||
// MakeLookupFunc creates a function that can be used to resolve hostnames
|
||||
// (e.g. as a LookupIPFallback from dnscache.Resolver).
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
return func(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
if disableRecursiveResolver() {
|
||||
// If they've explicitly disabled the recursive resolver with the legacy
|
||||
// TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER envknob or not set the
|
||||
// newer TS_DNSFALLBACK_RECURSIVE_RESOLVER to true, then don't use the
|
||||
// recursive resolver. (tailscale/corp#15261) In the future, we might
|
||||
// change the default (the opt.Bool being unset) to mean enabled.
|
||||
if disableRecursiveResolver() || !optRecursiveResolver().EqualBool(true) {
|
||||
return lookup(ctx, host, logf, netMon)
|
||||
}
|
||||
|
||||
|
||||
197
net/packet/checksum/checksum.go
Normal file
197
net/packet/checksum/checksum.go
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package checksum provides functions for updating checksums in parsed packets.
|
||||
package checksum
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
|
||||
// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateSrcAddr(q *packet.Parsed, src netip.Addr) {
|
||||
if src.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if src.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidSNAT = true
|
||||
q.CaptureMeta.OriginalSrc = q.Src
|
||||
|
||||
old := q.Src.Addr()
|
||||
q.Src = netip.AddrPortFrom(src, q.Src.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if src.Is6() {
|
||||
v6 := src.As16()
|
||||
copy(b[8:24], v6[:])
|
||||
updateV6PacketChecksums(q, old, src)
|
||||
} else {
|
||||
v4 := src.As4()
|
||||
copy(b[12:16], v4[:])
|
||||
updateV4PacketChecksums(q, old, src)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
|
||||
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func UpdateDstAddr(q *packet.Parsed, dst netip.Addr) {
|
||||
if dst.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if dst.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidDNAT = true
|
||||
q.CaptureMeta.OriginalDst = q.Dst
|
||||
|
||||
old := q.Dst.Addr()
|
||||
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if dst.Is6() {
|
||||
v6 := dst.As16()
|
||||
copy(b[24:36], v6[:])
|
||||
updateV6PacketChecksums(q, old, dst)
|
||||
} else {
|
||||
v4 := dst.As4()
|
||||
copy(b[16:20], v4[:])
|
||||
updateV4PacketChecksums(q, old, dst)
|
||||
}
|
||||
}
|
||||
|
||||
// updateV4PacketChecksums updates the checksums in the packet buffer.
|
||||
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, only the IP header checksum is updated.
|
||||
func updateV4PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 12 {
|
||||
// Not enough space for an IPv4 header.
|
||||
return
|
||||
}
|
||||
o4, n4 := old.As4(), new.As4()
|
||||
|
||||
// First update the checksum in the IP header.
|
||||
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:])
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
// Not enough space for a UDP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[6:8], o4[:], n4[:])
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
// Not enough space for a TCP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[16:18], o4[:], n4[:])
|
||||
case ipproto.GRE:
|
||||
if len(tr) < 6 {
|
||||
// Not enough space for a GRE header.
|
||||
return
|
||||
}
|
||||
if tr[0] == 1 { // checksum present
|
||||
updateV4Checksum(tr[4:6], o4[:], n4[:])
|
||||
}
|
||||
case ipproto.SCTP, ipproto.ICMPv4:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV6PacketChecksums updates the checksums in the packet buffer.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, no checksums are updated.
|
||||
func updateV6PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 40 {
|
||||
// Not enough space for an IPv6 header.
|
||||
return
|
||||
}
|
||||
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.ICMPv6:
|
||||
if len(tr) < header.ICMPv6MinimumSize {
|
||||
return
|
||||
}
|
||||
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.SCTP:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV4Checksum calculates and updates the checksum in the packet buffer for
|
||||
// a change between old and new. The oldSum must point to the 16-bit checksum
|
||||
// field in the packet buffer that holds the old checksum value, it will be
|
||||
// updated in place.
|
||||
//
|
||||
// The old and new must be the same length, and must be an even number of bytes.
|
||||
func updateV4Checksum(oldSum, old, new []byte) {
|
||||
if len(old) != len(new) {
|
||||
panic("old and new must be the same length")
|
||||
}
|
||||
if len(old)%2 != 0 {
|
||||
panic("old and new must be of even length")
|
||||
}
|
||||
/*
|
||||
RFC 1624
|
||||
Given the following notation:
|
||||
|
||||
HC - old checksum in header
|
||||
C - one's complement sum of old header
|
||||
HC' - new checksum in header
|
||||
C' - one's complement sum of new header
|
||||
m - old value of a 16-bit field
|
||||
m' - new value of a 16-bit field
|
||||
|
||||
HC' = ~(C + (-m) + m') -- [Eqn. 3]
|
||||
HC' = ~(~HC + ~m + m')
|
||||
|
||||
This can be simplified to:
|
||||
HC' = ~(C + ~m + m') -- [Eqn. 3]
|
||||
HC' = ~C'
|
||||
C' = C + ~m + m'
|
||||
*/
|
||||
|
||||
c := uint32(^binary.BigEndian.Uint16(oldSum))
|
||||
|
||||
cPrime := c
|
||||
for len(new) > 0 {
|
||||
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
|
||||
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
|
||||
cPrime += mPrime + mNot
|
||||
new, old = new[2:], old[2:]
|
||||
}
|
||||
|
||||
// Account for overflows by adding the carry bits back into the sum.
|
||||
for (cPrime >> 16) > 0 {
|
||||
cPrime = cPrime&0xFFFF + cPrime>>16
|
||||
}
|
||||
hcPrime := ^uint16(cPrime)
|
||||
binary.BigEndian.PutUint16(oldSum, hcPrime)
|
||||
}
|
||||
196
net/packet/checksum/checksum_test.go
Normal file
196
net/packet/checksum/checksum_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package checksum
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
func fullHeaderChecksumV4(b []byte) uint16 {
|
||||
s := uint32(0)
|
||||
for i := 0; i < len(b); i += 2 {
|
||||
if i == 10 {
|
||||
// Skip checksum field.
|
||||
continue
|
||||
}
|
||||
s += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
}
|
||||
for s>>16 > 0 {
|
||||
s = s&0xFFFF + s>>16
|
||||
}
|
||||
return ^uint16(s)
|
||||
}
|
||||
|
||||
func TestHeaderChecksumsV4(t *testing.T) {
|
||||
// This is not a good enough test, because it doesn't
|
||||
// check the various packet types or the many edge cases
|
||||
// of the checksum algorithm. But it's a start.
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
packet []byte
|
||||
}{
|
||||
{
|
||||
name: "ICMPv4",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x54, 0xb7, 0x96, 0x40, 0x00, 0x40, 0x01, 0x7a, 0x06, 0x64, 0x7f, 0x3f, 0x4c, 0x64, 0x40, 0x01, 0x01, 0x08, 0x00, 0x47, 0x1a, 0x00, 0x11, 0x01, 0xac, 0xcc, 0xf5, 0x95, 0x63, 0x00, 0x00, 0x00, 0x00, 0x8d, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TLS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x3c, 0x54, 0x29, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xac, 0x64, 0x42, 0xd4, 0x33, 0x64, 0x61, 0x98, 0x0f, 0xb1, 0x94, 0x01, 0xbb, 0x0a, 0x51, 0xce, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfb, 0xe0, 0x38, 0xf6, 0x00, 0x00, 0x02, 0x04, 0x04, 0xd8, 0x04, 0x02, 0x08, 0x0a, 0x86, 0x2b, 0xcc, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DNS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x74, 0xe2, 0x85, 0x00, 0x00, 0x40, 0x11, 0x96, 0xb5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x42, 0xd4, 0x33, 0x00, 0x35, 0xec, 0x55, 0x00, 0x60, 0xd9, 0x19, 0xed, 0xfd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x0c, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x01, 0x6c, 0xc0, 0x15, 0xc0, 0x31, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x04, 0x8e, 0xfa, 0xbd, 0xce, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DCCP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x28, 0x15, 0x06, 0x40, 0x00, 0x40, 0x21, 0x5f, 0x2f, 0xc0, 0xa8, 0x01, 0x1f, 0xc9, 0x0b, 0x3b, 0xad, 0x80, 0x04, 0x13, 0x89, 0x05, 0x00, 0x08, 0xdb, 0x01, 0x00, 0x00, 0x04, 0x29, 0x01, 0x6d, 0xdc, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SCTP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x30, 0x09, 0xd9, 0x40, 0x00, 0xff, 0x84, 0x50, 0xe2, 0x0a, 0x1c, 0x06, 0x2c, 0x0a, 0x1c, 0x06, 0x2b, 0x0b, 0x80, 0x40, 0x00, 0x21, 0x44, 0x15, 0x23, 0x2b, 0xf2, 0x02, 0x4e, 0x03, 0x00, 0x00, 0x10, 0x28, 0x02, 0x43, 0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
// TODO(maisem): add test for GRE.
|
||||
}
|
||||
var p packet.Parsed
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p.Decode(tt.packet)
|
||||
t.Log(p.String())
|
||||
UpdateSrcAddr(&p, netip.MustParseAddr("100.64.0.1"))
|
||||
|
||||
got := binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want := fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
|
||||
UpdateDstAddr(&p, netip.MustParseAddr("100.64.0.2"))
|
||||
got = binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want = fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6UDP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.UDPMinimumSize + 32,
|
||||
TransportProtocol: header.UDPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
udp := header.UDP(b[header.IPv6MinimumSize:])
|
||||
udp.Encode(&header.UDPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
Length: header.UDPMinimumSize + 32,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.UDPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.UDPMinimumSize+32),
|
||||
)
|
||||
xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
|
||||
udp.SetChecksum(^udp.CalculateChecksum(xsum))
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p packet.Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
UpdateSrcAddr(&p, a2)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
UpdateDstAddr(&p, a1)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6TCP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake TCP packet with no payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.TCPMinimumSize,
|
||||
TransportProtocol: header.TCPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
tcp := header.TCP(b[header.IPv6MinimumSize:])
|
||||
tcp.Encode(&header.TCPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
SeqNum: 1,
|
||||
AckNum: 2,
|
||||
DataOffset: header.TCPMinimumSize,
|
||||
Flags: 3,
|
||||
WindowSize: 4,
|
||||
Checksum: 0,
|
||||
UrgentPointer: 5,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.TCPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.TCPMinimumSize),
|
||||
)
|
||||
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
|
||||
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p packet.Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
UpdateSrcAddr(&p, a2)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
UpdateDstAddr(&p, a1)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
@@ -454,62 +452,6 @@ func (q *Parsed) IsEchoResponse() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
|
||||
// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func (q *Parsed) UpdateSrcAddr(src netip.Addr) {
|
||||
if src.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if src.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidSNAT = true
|
||||
q.CaptureMeta.OriginalSrc = q.Src
|
||||
|
||||
old := q.Src.Addr()
|
||||
q.Src = netip.AddrPortFrom(src, q.Src.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if src.Is6() {
|
||||
v6 := src.As16()
|
||||
copy(b[8:24], v6[:])
|
||||
updateV6PacketChecksums(q, old, src)
|
||||
} else {
|
||||
v4 := src.As4()
|
||||
copy(b[12:16], v4[:])
|
||||
updateV4PacketChecksums(q, old, src)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
|
||||
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
|
||||
// is supported. It panics if provided with an address in a different
|
||||
// family to the parsed packet.
|
||||
func (q *Parsed) UpdateDstAddr(dst netip.Addr) {
|
||||
if dst.Is6() && q.IPVersion != 6 {
|
||||
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
|
||||
} else if dst.Is4() && q.IPVersion != 4 {
|
||||
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
|
||||
}
|
||||
q.CaptureMeta.DidDNAT = true
|
||||
q.CaptureMeta.OriginalDst = q.Dst
|
||||
|
||||
old := q.Dst.Addr()
|
||||
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
|
||||
|
||||
b := q.Buffer()
|
||||
if dst.Is6() {
|
||||
v6 := dst.As16()
|
||||
copy(b[24:36], v6[:])
|
||||
updateV6PacketChecksums(q, old, dst)
|
||||
} else {
|
||||
v4 := dst.As4()
|
||||
copy(b[16:20], v4[:])
|
||||
updateV4PacketChecksums(q, old, dst)
|
||||
}
|
||||
}
|
||||
|
||||
// EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response,
|
||||
// and returns them as a uint32, used to lookup internally routed ICMP echo
|
||||
// responses. This function is intentionally lightweight as it is called on
|
||||
@@ -572,129 +514,3 @@ func withIP(ap netip.AddrPort, ip netip.Addr) netip.AddrPort {
|
||||
func withPort(ap netip.AddrPort, port uint16) netip.AddrPort {
|
||||
return netip.AddrPortFrom(ap.Addr(), port)
|
||||
}
|
||||
|
||||
// updateV4PacketChecksums updates the checksums in the packet buffer.
|
||||
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, only the IP header checksum is updated.
|
||||
func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 12 {
|
||||
// Not enough space for an IPv4 header.
|
||||
return
|
||||
}
|
||||
o4, n4 := old.As4(), new.As4()
|
||||
|
||||
// First update the checksum in the IP header.
|
||||
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:])
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
// Not enough space for a UDP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[6:8], o4[:], n4[:])
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
// Not enough space for a TCP header.
|
||||
return
|
||||
}
|
||||
updateV4Checksum(tr[16:18], o4[:], n4[:])
|
||||
case ipproto.GRE:
|
||||
if len(tr) < 6 {
|
||||
// Not enough space for a GRE header.
|
||||
return
|
||||
}
|
||||
if tr[0] == 1 { // checksum present
|
||||
updateV4Checksum(tr[4:6], o4[:], n4[:])
|
||||
}
|
||||
case ipproto.SCTP, ipproto.ICMPv4:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV6PacketChecksums updates the checksums in the packet buffer.
|
||||
// p is modified in place.
|
||||
// If p.IPProto is unknown, no checksums are updated.
|
||||
func updateV6PacketChecksums(p *Parsed, old, new netip.Addr) {
|
||||
if len(p.Buffer()) < 40 {
|
||||
// Not enough space for an IPv6 header.
|
||||
return
|
||||
}
|
||||
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
|
||||
|
||||
// Now update the transport layer checksums, where applicable.
|
||||
tr := p.Transport()
|
||||
switch p.IPProto {
|
||||
case ipproto.ICMPv6:
|
||||
if len(tr) < header.ICMPv6MinimumSize {
|
||||
return
|
||||
}
|
||||
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
|
||||
case ipproto.UDP, ipproto.DCCP:
|
||||
if len(tr) < header.UDPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.TCP:
|
||||
if len(tr) < header.TCPMinimumSize {
|
||||
return
|
||||
}
|
||||
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
|
||||
case ipproto.SCTP:
|
||||
// No transport layer update required.
|
||||
}
|
||||
}
|
||||
|
||||
// updateV4Checksum calculates and updates the checksum in the packet buffer for
|
||||
// a change between old and new. The oldSum must point to the 16-bit checksum
|
||||
// field in the packet buffer that holds the old checksum value, it will be
|
||||
// updated in place.
|
||||
//
|
||||
// The old and new must be the same length, and must be an even number of bytes.
|
||||
func updateV4Checksum(oldSum, old, new []byte) {
|
||||
if len(old) != len(new) {
|
||||
panic("old and new must be the same length")
|
||||
}
|
||||
if len(old)%2 != 0 {
|
||||
panic("old and new must be of even length")
|
||||
}
|
||||
/*
|
||||
RFC 1624
|
||||
Given the following notation:
|
||||
|
||||
HC - old checksum in header
|
||||
C - one's complement sum of old header
|
||||
HC' - new checksum in header
|
||||
C' - one's complement sum of new header
|
||||
m - old value of a 16-bit field
|
||||
m' - new value of a 16-bit field
|
||||
|
||||
HC' = ~(C + (-m) + m') -- [Eqn. 3]
|
||||
HC' = ~(~HC + ~m + m')
|
||||
|
||||
This can be simplified to:
|
||||
HC' = ~(C + ~m + m') -- [Eqn. 3]
|
||||
HC' = ~C'
|
||||
C' = C + ~m + m'
|
||||
*/
|
||||
|
||||
c := uint32(^binary.BigEndian.Uint16(oldSum))
|
||||
|
||||
cPrime := c
|
||||
for len(new) > 0 {
|
||||
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
|
||||
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
|
||||
cPrime += mPrime + mNot
|
||||
new, old = new[2:], old[2:]
|
||||
}
|
||||
|
||||
// Account for overflows by adding the carry bits back into the sum.
|
||||
for (cPrime >> 16) > 0 {
|
||||
cPrime = cPrime&0xFFFF + cPrime>>16
|
||||
}
|
||||
hcPrime := ^uint16(cPrime)
|
||||
binary.BigEndian.PutUint16(oldSum, hcPrime)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
@@ -13,9 +12,6 @@ import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/must"
|
||||
@@ -33,187 +29,6 @@ const (
|
||||
Fragment = ipproto.Fragment
|
||||
)
|
||||
|
||||
func fullHeaderChecksumV4(b []byte) uint16 {
|
||||
s := uint32(0)
|
||||
for i := 0; i < len(b); i += 2 {
|
||||
if i == 10 {
|
||||
// Skip checksum field.
|
||||
continue
|
||||
}
|
||||
s += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
}
|
||||
for s>>16 > 0 {
|
||||
s = s&0xFFFF + s>>16
|
||||
}
|
||||
return ^uint16(s)
|
||||
}
|
||||
|
||||
func TestHeaderChecksumsV4(t *testing.T) {
|
||||
// This is not a good enough test, because it doesn't
|
||||
// check the various packet types or the many edge cases
|
||||
// of the checksum algorithm. But it's a start.
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
packet []byte
|
||||
}{
|
||||
{
|
||||
name: "ICMPv4",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x54, 0xb7, 0x96, 0x40, 0x00, 0x40, 0x01, 0x7a, 0x06, 0x64, 0x7f, 0x3f, 0x4c, 0x64, 0x40, 0x01, 0x01, 0x08, 0x00, 0x47, 0x1a, 0x00, 0x11, 0x01, 0xac, 0xcc, 0xf5, 0x95, 0x63, 0x00, 0x00, 0x00, 0x00, 0x8d, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TLS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x3c, 0x54, 0x29, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xac, 0x64, 0x42, 0xd4, 0x33, 0x64, 0x61, 0x98, 0x0f, 0xb1, 0x94, 0x01, 0xbb, 0x0a, 0x51, 0xce, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfb, 0xe0, 0x38, 0xf6, 0x00, 0x00, 0x02, 0x04, 0x04, 0xd8, 0x04, 0x02, 0x08, 0x0a, 0x86, 0x2b, 0xcc, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DNS",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x74, 0xe2, 0x85, 0x00, 0x00, 0x40, 0x11, 0x96, 0xb5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x42, 0xd4, 0x33, 0x00, 0x35, 0xec, 0x55, 0x00, 0x60, 0xd9, 0x19, 0xed, 0xfd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x0c, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x01, 0x6c, 0xc0, 0x15, 0xc0, 0x31, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x04, 0x8e, 0xfa, 0xbd, 0xce, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DCCP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x28, 0x15, 0x06, 0x40, 0x00, 0x40, 0x21, 0x5f, 0x2f, 0xc0, 0xa8, 0x01, 0x1f, 0xc9, 0x0b, 0x3b, 0xad, 0x80, 0x04, 0x13, 0x89, 0x05, 0x00, 0x08, 0xdb, 0x01, 0x00, 0x00, 0x04, 0x29, 0x01, 0x6d, 0xdc, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SCTP",
|
||||
packet: []byte{
|
||||
0x45, 0x00, 0x00, 0x30, 0x09, 0xd9, 0x40, 0x00, 0xff, 0x84, 0x50, 0xe2, 0x0a, 0x1c, 0x06, 0x2c, 0x0a, 0x1c, 0x06, 0x2b, 0x0b, 0x80, 0x40, 0x00, 0x21, 0x44, 0x15, 0x23, 0x2b, 0xf2, 0x02, 0x4e, 0x03, 0x00, 0x00, 0x10, 0x28, 0x02, 0x43, 0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
},
|
||||
// TODO(maisem): add test for GRE.
|
||||
}
|
||||
var p Parsed
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p.Decode(tt.packet)
|
||||
t.Log(p.String())
|
||||
p.UpdateSrcAddr(netip.MustParseAddr("100.64.0.1"))
|
||||
|
||||
got := binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want := fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
|
||||
p.UpdateDstAddr(netip.MustParseAddr("100.64.0.2"))
|
||||
got = binary.BigEndian.Uint16(tt.packet[10:12])
|
||||
want = fullHeaderChecksumV4(tt.packet[:20])
|
||||
if got != want {
|
||||
t.Fatalf("got %x want %x", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6UDP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.UDPMinimumSize + 32,
|
||||
TransportProtocol: header.UDPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
udp := header.UDP(b[header.IPv6MinimumSize:])
|
||||
udp.Encode(&header.UDPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
Length: header.UDPMinimumSize + 32,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.UDPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.UDPMinimumSize+32),
|
||||
)
|
||||
xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
|
||||
udp.SetChecksum(^udp.CalculateChecksum(xsum))
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
p.UpdateSrcAddr(a2)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
p.UpdateDstAddr(a1)
|
||||
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatChecksumsV6TCP(t *testing.T) {
|
||||
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
|
||||
|
||||
// Make a fake TCP packet with no payload.
|
||||
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
|
||||
b.Encode(&header.IPv6Fields{
|
||||
PayloadLength: header.TCPMinimumSize,
|
||||
TransportProtocol: header.TCPProtocolNumber,
|
||||
HopLimit: 16,
|
||||
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
})
|
||||
tcp := header.TCP(b[header.IPv6MinimumSize:])
|
||||
tcp.Encode(&header.TCPFields{
|
||||
SrcPort: 42,
|
||||
DstPort: 43,
|
||||
SeqNum: 1,
|
||||
AckNum: 2,
|
||||
DataOffset: header.TCPMinimumSize,
|
||||
Flags: 3,
|
||||
WindowSize: 4,
|
||||
Checksum: 0,
|
||||
UrgentPointer: 5,
|
||||
})
|
||||
xsum := header.PseudoHeaderChecksum(
|
||||
header.TCPProtocolNumber,
|
||||
tcpip.AddrFrom16Slice(a1.AsSlice()),
|
||||
tcpip.AddrFrom16Slice(a2.AsSlice()),
|
||||
uint16(header.TCPMinimumSize),
|
||||
)
|
||||
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
|
||||
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("test broken; initial packet has incorrect checksum")
|
||||
}
|
||||
|
||||
// Parse the packet.
|
||||
var p Parsed
|
||||
p.Decode(b)
|
||||
t.Log(p.String())
|
||||
|
||||
// Update the source address of the packet to be the same as the dest.
|
||||
p.UpdateSrcAddr(a2)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating source address")
|
||||
}
|
||||
|
||||
// Update the dest address of the packet to be the original source address.
|
||||
p.UpdateDstAddr(a1)
|
||||
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
|
||||
t.Fatal("incorrect checksum after updating destination address")
|
||||
}
|
||||
}
|
||||
|
||||
func mustIPPort(s string) netip.AddrPort {
|
||||
ipp, err := netip.ParseAddrPort(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,3 +55,4 @@ func (t *fakeTUN) MTU() (int, error) { return 1500, nil }
|
||||
func (t *fakeTUN) Name() (string, error) { return FakeTUNName, nil }
|
||||
func (t *fakeTUN) Events() <-chan tun.Event { return t.evchan }
|
||||
func (t *fakeTUN) BatchSize() int { return 1 }
|
||||
func (t *fakeTUN) IsFakeTun() bool { return true }
|
||||
|
||||
@@ -79,14 +79,16 @@ const (
|
||||
safeTUNMTU TUNMTU = 1280
|
||||
)
|
||||
|
||||
// MaxProbedWireMTU is the largest MTU we will test for path MTU
|
||||
// discovery.
|
||||
var MaxProbedWireMTU WireMTU = 9000
|
||||
|
||||
func init() {
|
||||
if MaxProbedWireMTU > WireMTU(maxTUNMTU) {
|
||||
MaxProbedWireMTU = WireMTU(maxTUNMTU)
|
||||
}
|
||||
// WireMTUsToProbe is a list of the on-the-wire MTUs we want to probe. Each time
|
||||
// magicsock discovery begins, it will send a set of pings, one of each size
|
||||
// listed below.
|
||||
var WireMTUsToProbe = []WireMTU{
|
||||
WireMTU(safeTUNMTU), // Tailscale over Tailscale :)
|
||||
TUNToWireMTU(safeTUNMTU), // Smallest MTU allowed for IPv6, current default
|
||||
1400, // Most common MTU minus a few bytes for tunnels
|
||||
1500, // Most common MTU
|
||||
8000, // Should fit inside all jumbo frame sizes
|
||||
9000, // Most jumbo frames are this size or larger
|
||||
}
|
||||
|
||||
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
|
||||
@@ -125,7 +127,7 @@ func WireToTUNMTU(w WireMTU) TUNMTU {
|
||||
// MTU. It is also the path MTU that we default to if we have no
|
||||
// information about the path to a peer.
|
||||
//
|
||||
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTunMTU
|
||||
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTUNMTU
|
||||
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg overhead
|
||||
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
|
||||
func DefaultTUNMTU() TUNMTU {
|
||||
@@ -135,12 +137,23 @@ func DefaultTUNMTU() TUNMTU {
|
||||
|
||||
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
|
||||
if debugPMTUD {
|
||||
return WireToTUNMTU(MaxProbedWireMTU)
|
||||
// TODO: While we are just probing MTU but not generating PTB,
|
||||
// this has to continue to return the safe MTU. When we add the
|
||||
// code to generate PTB, this will be:
|
||||
//
|
||||
// return WireToTUNMTU(maxProbedWireMTU)
|
||||
return safeTUNMTU
|
||||
}
|
||||
|
||||
return safeTUNMTU
|
||||
}
|
||||
|
||||
// SafeWireMTU returns the wire MTU that is safe to use if we have no
|
||||
// information about the path MTU to this peer.
|
||||
func SafeWireMTU() WireMTU {
|
||||
return TUNToWireMTU(safeTUNMTU)
|
||||
}
|
||||
|
||||
// DefaultWireMTU returns the default TUN MTU, adjusted for wireguard
|
||||
// overhead.
|
||||
func DefaultWireMTU() WireMTU {
|
||||
|
||||
@@ -39,15 +39,18 @@ func TestDefaultTunMTU(t *testing.T) {
|
||||
t.Errorf("default TUN MTU = %d, want %d, clamping failed", DefaultTUNMTU(), maxTUNMTU)
|
||||
}
|
||||
|
||||
// If PMTUD is enabled, the MTU should default to the largest probed
|
||||
// MTU, but only if the user hasn't requested a specific MTU.
|
||||
// If PMTUD is enabled, the MTU should default to the safe MTU, but only
|
||||
// if the user hasn't requested a specific MTU.
|
||||
//
|
||||
// TODO: When PMTUD is generating PTB responses, this will become the
|
||||
// largest MTU we probe.
|
||||
os.Setenv("TS_DEBUG_MTU", "")
|
||||
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
|
||||
if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
|
||||
if DefaultTUNMTU() != safeTUNMTU {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
|
||||
}
|
||||
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
|
||||
mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
|
||||
mtu = WireToTUNMTU(MaxPacketSize - 1)
|
||||
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
|
||||
if DefaultTUNMTU() != mtu {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/net/connstats"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/packet/checksum"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun/table"
|
||||
"tailscale.com/syncs"
|
||||
@@ -77,6 +78,9 @@ var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
|
||||
type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
|
||||
|
||||
// Wrapper augments a tun.Device with packet filtering and injection.
|
||||
//
|
||||
// A Wrapper starts in a "corked" mode where Read calls are blocked
|
||||
// until the Wrapper's Start method is called.
|
||||
type Wrapper struct {
|
||||
logf logger.Logf
|
||||
limitedLogf logger.Logf // aggressively rate-limited logf used for potentially high volume errors
|
||||
@@ -84,6 +88,9 @@ type Wrapper struct {
|
||||
tdev tun.Device
|
||||
isTAP bool // whether tdev is a TAP device
|
||||
|
||||
started atomic.Bool // whether Start has been called
|
||||
startCh chan struct{} // closed in Start
|
||||
|
||||
closeOnce sync.Once
|
||||
|
||||
// lastActivityAtomic is read/written atomically.
|
||||
@@ -218,6 +225,16 @@ type setWrapperer interface {
|
||||
setWrapper(*Wrapper)
|
||||
}
|
||||
|
||||
// Start unblocks any Wrapper.Read calls that have already started
|
||||
// and makes the Wrapper functional.
|
||||
//
|
||||
// Start must be called exactly once after the various Tailscale
|
||||
// subsystems have been wired up to each other.
|
||||
func (w *Wrapper) Start() {
|
||||
w.started.Store(true)
|
||||
close(w.startCh)
|
||||
}
|
||||
|
||||
func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
return wrap(logf, tdev, true)
|
||||
}
|
||||
@@ -243,6 +260,7 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
|
||||
eventsOther: make(chan tun.Event),
|
||||
// TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets.
|
||||
filterFlags: filter.LogAccepts | filter.LogDrops,
|
||||
startCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
w.vectorBuffer = make([][]byte, tdev.BatchSize())
|
||||
@@ -308,6 +326,9 @@ func (t *Wrapper) isSelfDisco(p *packet.Parsed) bool {
|
||||
func (t *Wrapper) Close() error {
|
||||
var err error
|
||||
t.closeOnce.Do(func() {
|
||||
if t.started.CompareAndSwap(false, true) {
|
||||
close(t.startCh)
|
||||
}
|
||||
close(t.closed)
|
||||
t.bufferConsumedMu.Lock()
|
||||
t.bufferConsumedClosed = true
|
||||
@@ -487,7 +508,7 @@ func (t *Wrapper) snat(p *packet.Parsed) {
|
||||
oldSrc := p.Src.Addr()
|
||||
newSrc := nc.selectSrcIP(oldSrc, p.Dst.Addr())
|
||||
if oldSrc != newSrc {
|
||||
p.UpdateSrcAddr(newSrc)
|
||||
checksum.UpdateSrcAddr(p, newSrc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +518,7 @@ func (t *Wrapper) dnat(p *packet.Parsed) {
|
||||
oldDst := p.Dst.Addr()
|
||||
newDst := nc.mapDstIP(oldDst)
|
||||
if newDst != oldDst {
|
||||
p.UpdateDstAddr(newDst)
|
||||
checksum.UpdateDstAddr(p, newDst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,16 +694,16 @@ func (c *natFamilyConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
|
||||
// natConfigFromWGConfig generates a natFamilyConfig from nm,
|
||||
// for the indicated address family.
|
||||
// If NAT is not required for that address family, it returns nil.
|
||||
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *natFamilyConfig {
|
||||
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFamilyConfig {
|
||||
if wcfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nativeAddr netip.Addr
|
||||
switch addrFam {
|
||||
case ipproto.IPProtoVersion4:
|
||||
case ipproto.Version4:
|
||||
nativeAddr = findV4(wcfg.Addresses)
|
||||
case ipproto.IPProtoVersion6:
|
||||
case ipproto.Version6:
|
||||
nativeAddr = findV6(wcfg.Addresses)
|
||||
}
|
||||
if !nativeAddr.IsValid() {
|
||||
@@ -703,8 +724,8 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
|
||||
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
|
||||
if isExitNode {
|
||||
hasMasqAddrsForFamily := false ||
|
||||
(addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
|
||||
(addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
|
||||
(addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
|
||||
(addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
|
||||
if hasMasqAddrsForFamily {
|
||||
exitNodeRequiresMasq = true
|
||||
}
|
||||
@@ -714,10 +735,10 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
|
||||
for i := range wcfg.Peers {
|
||||
p := &wcfg.Peers[i]
|
||||
var addrToUse netip.Addr
|
||||
if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
if addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
addrToUse = *p.V4MasqAddr
|
||||
mak.Set(&listenAddrs, addrToUse, struct{}{})
|
||||
} else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
|
||||
} else if addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
|
||||
addrToUse = *p.V6MasqAddr
|
||||
mak.Set(&listenAddrs, addrToUse, struct{}{})
|
||||
} else if exitNodeRequiresMasq {
|
||||
@@ -741,7 +762,7 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
|
||||
|
||||
// SetNetMap is called when a new NetworkMap is received.
|
||||
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
|
||||
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
|
||||
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.Version4), natConfigFromWGConfig(wcfg, ipproto.Version6)
|
||||
var cfg *natConfig
|
||||
if v4 != nil || v6 != nil {
|
||||
cfg = &natConfig{v4: v4, v6: v6}
|
||||
@@ -835,6 +856,9 @@ func (t *Wrapper) IdleDuration() time.Duration {
|
||||
}
|
||||
|
||||
func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
if !t.started.Load() {
|
||||
<-t.startCh
|
||||
}
|
||||
// packet from OS read and sent to WG
|
||||
res, ok := <-t.vectorOutbound
|
||||
if !ok {
|
||||
|
||||
@@ -178,6 +178,7 @@ func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper
|
||||
} else {
|
||||
tun.disableFilter = true
|
||||
}
|
||||
tun.Start()
|
||||
return chtun, tun
|
||||
}
|
||||
|
||||
@@ -617,7 +618,7 @@ func TestNATCfg(t *testing.T) {
|
||||
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
|
||||
return p
|
||||
}
|
||||
test := func(addrFam ipproto.IPProtoVersion) {
|
||||
test := func(addrFam ipproto.Version) {
|
||||
var (
|
||||
noIP netip.Addr
|
||||
|
||||
@@ -635,7 +636,7 @@ func TestNATCfg(t *testing.T) {
|
||||
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
|
||||
publicIP = netip.MustParseAddr("8.8.8.8")
|
||||
)
|
||||
if addrFam == ipproto.IPProtoVersion6 {
|
||||
if addrFam == ipproto.Version6 {
|
||||
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
|
||||
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
|
||||
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
|
||||
@@ -817,8 +818,8 @@ func TestNATCfg(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
test(ipproto.IPProtoVersion4)
|
||||
test(ipproto.IPProtoVersion6)
|
||||
test(ipproto.Version4)
|
||||
test(ipproto.Version6)
|
||||
}
|
||||
|
||||
// TestCaptureHook verifies that the Wrapper.captureHook callback is called
|
||||
|
||||
74
posture/serialnumber_macos.go
Normal file
74
posture/serialnumber_macos.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build cgo && darwin && !ios
|
||||
|
||||
package posture
|
||||
|
||||
// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
|
||||
// #include <CoreFoundation/CoreFoundation.h>
|
||||
// #include <IOKit/IOKitLib.h>
|
||||
//
|
||||
// #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
|
||||
// #define kIOMainPortDefault kIOMasterPortDefault
|
||||
// #endif
|
||||
//
|
||||
// const char *
|
||||
// getSerialNumber()
|
||||
// {
|
||||
// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice");
|
||||
// if (!matching) {
|
||||
// return "err: failed to create dictionary to match IOServices";
|
||||
// }
|
||||
//
|
||||
// io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching);
|
||||
// if (!service) {
|
||||
// return "err: failed to look up registered IOService objects that match a matching dictionary";
|
||||
// }
|
||||
//
|
||||
// CFStringRef serialNumberRef = IORegistryEntryCreateCFProperty(
|
||||
// service,
|
||||
// CFSTR("IOPlatformSerialNumber"),
|
||||
// kCFAllocatorDefault,
|
||||
// 0
|
||||
// );
|
||||
// if (!serialNumberRef) {
|
||||
// return "err: failed to look up serial number in IORegistry";
|
||||
// }
|
||||
//
|
||||
// CFIndex length = CFStringGetLength(serialNumberRef);
|
||||
// CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
// char *serialNumberBuf = (char *)malloc(max_size);
|
||||
//
|
||||
// bool result = CFStringGetCString(serialNumberRef, serialNumberBuf, max_size, kCFStringEncodingUTF8);
|
||||
//
|
||||
// CFRelease(serialNumberRef);
|
||||
// IOObjectRelease(service);
|
||||
//
|
||||
// if (!result) {
|
||||
// free(serialNumberBuf);
|
||||
//
|
||||
// return "err: failed to convert serial number reference to string";
|
||||
// }
|
||||
//
|
||||
// return serialNumberBuf;
|
||||
// }
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// GetSerialNumber returns the platform serial sumber as reported by IOKit.
|
||||
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
|
||||
csn := C.getSerialNumber()
|
||||
serialNumber := C.GoString(csn)
|
||||
|
||||
if err, ok := strings.CutPrefix(serialNumber, "err: "); ok {
|
||||
return nil, fmt.Errorf("failed to get serial number from IOKit: %s", err)
|
||||
}
|
||||
|
||||
return []string{serialNumber}, nil
|
||||
}
|
||||
37
posture/serialnumber_macos_test.go
Normal file
37
posture/serialnumber_macos_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build cgo && darwin && !ios
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cibuild"
|
||||
)
|
||||
|
||||
func TestGetSerialNumberMac(t *testing.T) {
|
||||
// Do not run this test on CI, it can only be ran on macOS
|
||||
// and we currenty only use Linux runners.
|
||||
if cibuild.On() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
sns, err := GetSerialNumbers(logger.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get serial number: %s", err)
|
||||
}
|
||||
|
||||
if len(sns) != 1 {
|
||||
t.Errorf("expected list of one serial number, got %v", sns)
|
||||
}
|
||||
|
||||
if len(sns[0]) <= 0 {
|
||||
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
|
||||
}
|
||||
|
||||
fmt.Printf("serials: %v\n", sns)
|
||||
}
|
||||
143
posture/serialnumber_notmacos.go
Normal file
143
posture/serialnumber_notmacos.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Build on Windows, Linux and *BSD
|
||||
|
||||
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/digitalocean/go-smbios/smbios"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
|
||||
func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
|
||||
// the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
|
||||
// so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
|
||||
index := specOffset - 4
|
||||
if index >= len(s.Formatted) || index < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return s.Formatted[index]
|
||||
}
|
||||
|
||||
// getStringFromSmbiosStructure retrieves a string at the given specOffset.
|
||||
// Returns an empty string if no string was present.
|
||||
func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) {
|
||||
index := getByteFromSmbiosStructure(s, specOffset)
|
||||
|
||||
if index == 0 || int(index) > len(s.Strings) {
|
||||
return "", errors.New("specified offset does not exist in smbios structure")
|
||||
}
|
||||
|
||||
str := s.Strings[index-1]
|
||||
trimmed := strings.TrimSpace(str)
|
||||
|
||||
return trimmed, nil
|
||||
}
|
||||
|
||||
// Product Table (Type 1) structure
|
||||
// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
|
||||
// Page 34 and onwards.
|
||||
const (
|
||||
// Serial is present at the same offset in all IDs
|
||||
serialNumberOffset = 0x07
|
||||
|
||||
productID = 1
|
||||
baseboardID = 2
|
||||
chassisID = 3
|
||||
)
|
||||
|
||||
var (
|
||||
idToTableName = map[int]string{
|
||||
1: "product",
|
||||
2: "baseboard",
|
||||
3: "chassis",
|
||||
}
|
||||
validTables []string
|
||||
numOfTables int
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, table := range idToTableName {
|
||||
validTables = append(validTables, table)
|
||||
}
|
||||
numOfTables = len(validTables)
|
||||
|
||||
}
|
||||
|
||||
// serialFromSmbiosStructure extracts a serial number from a product,
|
||||
// baseboard or chassis SMBIOS table.
|
||||
func serialFromSmbiosStructure(s *smbios.Structure) (string, error) {
|
||||
id := s.Header.Type
|
||||
if (id != productID) && (id != baseboardID) && (id != chassisID) {
|
||||
return "", fmt.Errorf(
|
||||
"cannot get serial table type %d, supported tables are %v",
|
||||
id,
|
||||
validTables,
|
||||
)
|
||||
}
|
||||
|
||||
serial, err := getStringFromSmbiosStructure(s, serialNumberOffset)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to get serial from %s table: %w",
|
||||
idToTableName[int(s.Header.Type)],
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return serial, nil
|
||||
}
|
||||
|
||||
func GetSerialNumbers(logf logger.Logf) ([]string, error) {
|
||||
// Find SMBIOS data in operating system-specific location.
|
||||
rc, _, err := smbios.Stream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Decode SMBIOS structures from the stream.
|
||||
d := smbios.NewDecoder(rc)
|
||||
ss, err := d.Decode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
|
||||
}
|
||||
|
||||
serials := make([]string, 0, numOfTables)
|
||||
errs := make([]error, 0, numOfTables)
|
||||
|
||||
for _, s := range ss {
|
||||
switch s.Header.Type {
|
||||
case productID, baseboardID, chassisID:
|
||||
serial, err := serialFromSmbiosStructure(s)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
serials = append(serials, serial)
|
||||
}
|
||||
}
|
||||
|
||||
err = multierr.New(errs...)
|
||||
|
||||
// if there were no serial numbers, check if any errors were
|
||||
// returned and combine them.
|
||||
if len(serials) == 0 && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logf("got serial numbers %v (errors: %s)", serials, err)
|
||||
|
||||
return serials, nil
|
||||
}
|
||||
38
posture/serialnumber_notmacos_test.go
Normal file
38
posture/serialnumber_notmacos_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Build on Windows, Linux and *BSD
|
||||
|
||||
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestGetSerialNumberNotMac(t *testing.T) {
|
||||
// This test is intentionally skipped as it will
|
||||
// require root on Linux to get access to the serials.
|
||||
// The test case is intended for local testing.
|
||||
// Comment out skip for local testing.
|
||||
t.Skip()
|
||||
|
||||
sns, err := GetSerialNumbers(logger.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get serial number: %s", err)
|
||||
}
|
||||
|
||||
if len(sns) == 0 {
|
||||
t.Fatalf("expected at least one serial number, got %v", sns)
|
||||
}
|
||||
|
||||
if len(sns[0]) <= 0 {
|
||||
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
|
||||
}
|
||||
|
||||
fmt.Printf("serials: %v\n", sns)
|
||||
}
|
||||
24
posture/serialnumber_stub.go
Normal file
24
posture/serialnumber_stub.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// ios: Apple does not allow getting serials on iOS
|
||||
// android: not implemented
|
||||
// js: not implemented
|
||||
// plan9: not implemented
|
||||
// solaris: currently unsupported by go-smbios:
|
||||
// https://github.com/digitalocean/go-smbios/pull/21
|
||||
|
||||
//go:build ios || android || solaris || plan9 || js || wasm || (darwin && !cgo)
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// GetSerialNumber returns client machine serial number(s).
|
||||
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
16
posture/serialnumber_test.go
Normal file
16
posture/serialnumber_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestGetSerialNumber(t *testing.T) {
|
||||
// ensure GetSerialNumbers is implemented
|
||||
// or covered by a stub on a given platform.
|
||||
_, _ = GetSerialNumbers(logger.Discard)
|
||||
}
|
||||
@@ -16,4 +16,4 @@
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=
|
||||
|
||||
@@ -192,6 +192,26 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
|
||||
return actual, loaded
|
||||
}
|
||||
|
||||
// LoadOrInit returns the value for the given key if it exists
|
||||
// otherwise f is called to construct the value to be set.
|
||||
// The lock is held for the duration to prevent duplicate initialization.
|
||||
func (m *Map[K, V]) LoadOrInit(key K, f func() V) (actual V, loaded bool) {
|
||||
if actual, loaded := m.Load(key); loaded {
|
||||
return actual, loaded
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if actual, loaded = m.m[key]; loaded {
|
||||
return actual, loaded
|
||||
}
|
||||
|
||||
loaded = false
|
||||
actual = f()
|
||||
mak.Set(&m.m, key, actual)
|
||||
return actual, loaded
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -91,8 +91,11 @@ func TestMap(t *testing.T) {
|
||||
if v, ok := m.LoadOrStore("two", 2); v != 2 || ok {
|
||||
t.Errorf(`LoadOrStore("two", 2) = (%v, %v), want (2, false)`, v, ok)
|
||||
}
|
||||
if v, ok := m.LoadOrInit("three", func() int { return 3 }); v != 3 || ok {
|
||||
t.Errorf(`LoadOrInit("three", 3) = (%v, %v), want (3, true)`, v, ok)
|
||||
}
|
||||
got := map[string]int{}
|
||||
want := map[string]int{"one": 1, "two": 2}
|
||||
want := map[string]int{"one": 1, "two": 2, "three": 3}
|
||||
m.Range(func(k string, v int) bool {
|
||||
got[k] = v
|
||||
return true
|
||||
@@ -106,6 +109,7 @@ func TestMap(t *testing.T) {
|
||||
if v, ok := m.LoadAndDelete("two"); v != 0 || ok {
|
||||
t.Errorf(`LoadAndDelete("two) = (%v, %v), want (0, false)`, v, ok)
|
||||
}
|
||||
m.Delete("three")
|
||||
m.Delete("one")
|
||||
m.Delete("noexist")
|
||||
got = map[string]int{}
|
||||
|
||||
@@ -52,3 +52,15 @@ type C2NUpdateResponse struct {
|
||||
// Started indicates whether the update has started.
|
||||
Started bool
|
||||
}
|
||||
|
||||
// C2NPostureIdentityResponse contains either a set of identifying serial number
|
||||
// from the client or a boolean indicating that the machine has opted out of
|
||||
// posture collection.
|
||||
type C2NPostureIdentityResponse struct {
|
||||
// SerialNumbers is a list of serial numbers of the client machine.
|
||||
SerialNumbers []string `json:",omitempty"`
|
||||
|
||||
// PostureDisabled indicates if the machine has opted out of
|
||||
// device posture collection.
|
||||
PostureDisabled bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
187
tailcfg/proto_port_range.go
Normal file
187
tailcfg/proto_port_range.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailcfg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/vizerror"
|
||||
)
|
||||
|
||||
var (
|
||||
errEmptyProtocol = errors.New("empty protocol")
|
||||
errEmptyString = errors.New("empty string")
|
||||
)
|
||||
|
||||
// ProtoPortRange is used to encode "proto:port" format.
|
||||
// The following formats are supported:
|
||||
//
|
||||
// "*" allows all TCP, UDP and ICMP traffic on all ports.
|
||||
// "<ports>" allows all TCP, UDP and ICMP traffic on the specified ports.
|
||||
// "proto:*" allows traffic of the specified proto on all ports.
|
||||
// "proto:<port>" allows traffic of the specified proto on the specified port.
|
||||
//
|
||||
// Ports are either a single port number or a range of ports (e.g. "80-90").
|
||||
// String named protocols support names that ipproto.Proto accepts.
|
||||
type ProtoPortRange struct {
|
||||
// Proto is the IP protocol number.
|
||||
// If Proto is 0, it means TCP+UDP+ICMP(4+6).
|
||||
Proto int
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface. See
|
||||
// ProtoPortRange for the format.
|
||||
func (ppr *ProtoPortRange) UnmarshalText(text []byte) error {
|
||||
ppr2, err := parseProtoPortRange(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*ppr = *ppr2
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface. See
|
||||
// ProtoPortRange for the format.
|
||||
func (ppr *ProtoPortRange) MarshalText() ([]byte, error) {
|
||||
if ppr.Proto == 0 && ppr.Ports == (PortRange{}) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return []byte(ppr.String()), nil
|
||||
}
|
||||
|
||||
// String implements the stringer interface. See ProtoPortRange for the
|
||||
// format.
|
||||
func (ppr ProtoPortRange) String() string {
|
||||
if ppr.Proto == 0 {
|
||||
if ppr.Ports == PortRangeAny {
|
||||
return "*"
|
||||
}
|
||||
}
|
||||
var buf strings.Builder
|
||||
if ppr.Proto != 0 {
|
||||
// Proto.MarshalText is infallible.
|
||||
text, _ := ipproto.Proto(ppr.Proto).MarshalText()
|
||||
buf.Write(text)
|
||||
buf.Write([]byte(":"))
|
||||
}
|
||||
pr := ppr.Ports
|
||||
if pr.First == pr.Last {
|
||||
fmt.Fprintf(&buf, "%d", pr.First)
|
||||
} else if pr == PortRangeAny {
|
||||
buf.WriteByte('*')
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%d-%d", pr.First, pr.Last)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ParseProtoPortRanges parses a slice of IP port range fields.
|
||||
func ParseProtoPortRanges(ips []string) ([]ProtoPortRange, error) {
|
||||
var out []ProtoPortRange
|
||||
for _, p := range ips {
|
||||
ppr, err := parseProtoPortRange(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *ppr)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseProtoPortRange(ipProtoPort string) (*ProtoPortRange, error) {
|
||||
if ipProtoPort == "" {
|
||||
return nil, errEmptyString
|
||||
}
|
||||
if ipProtoPort == "*" {
|
||||
return &ProtoPortRange{Ports: PortRangeAny}, nil
|
||||
}
|
||||
if !strings.Contains(ipProtoPort, ":") {
|
||||
ipProtoPort = "*:" + ipProtoPort
|
||||
}
|
||||
protoStr, portRange, err := parseHostPortRange(ipProtoPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if protoStr == "" {
|
||||
return nil, errEmptyProtocol
|
||||
}
|
||||
|
||||
ppr := &ProtoPortRange{
|
||||
Ports: portRange,
|
||||
}
|
||||
if protoStr == "*" {
|
||||
return ppr, nil
|
||||
}
|
||||
var ipProto ipproto.Proto
|
||||
if err := ipProto.UnmarshalText([]byte(protoStr)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ppr.Proto = int(ipProto)
|
||||
return ppr, nil
|
||||
}
|
||||
|
||||
// parseHostPortRange parses hostport as HOST:PORTS where HOST is
|
||||
// returned unchanged and PORTS is is either "*" or PORTLOW-PORTHIGH ranges.
|
||||
func parseHostPortRange(hostport string) (host string, ports PortRange, err error) {
|
||||
hostport = strings.ToLower(hostport)
|
||||
colon := strings.LastIndexByte(hostport, ':')
|
||||
if colon < 0 {
|
||||
return "", ports, vizerror.New("hostport must contain a colon (\":\")")
|
||||
}
|
||||
host = hostport[:colon]
|
||||
portlist := hostport[colon+1:]
|
||||
|
||||
if strings.Contains(host, ",") {
|
||||
return "", ports, vizerror.New("host cannot contain a comma (\",\")")
|
||||
}
|
||||
|
||||
if portlist == "*" {
|
||||
// Special case: permit hostname:* as a port wildcard.
|
||||
return host, PortRangeAny, nil
|
||||
}
|
||||
|
||||
if len(portlist) == 0 {
|
||||
return "", ports, vizerror.Errorf("invalid port list: %#v", portlist)
|
||||
}
|
||||
|
||||
if strings.Count(portlist, "-") > 1 {
|
||||
return "", ports, vizerror.Errorf("port range %#v: too many dashes(-)", portlist)
|
||||
}
|
||||
|
||||
firstStr, lastStr, isRange := strings.Cut(portlist, "-")
|
||||
|
||||
var first, last uint64
|
||||
first, err = strconv.ParseUint(firstStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", ports, vizerror.Errorf("port range %#v: invalid first integer", portlist)
|
||||
}
|
||||
|
||||
if isRange {
|
||||
last, err = strconv.ParseUint(lastStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", ports, vizerror.Errorf("port range %#v: invalid last integer", portlist)
|
||||
}
|
||||
} else {
|
||||
last = first
|
||||
}
|
||||
|
||||
if first == 0 {
|
||||
return "", ports, vizerror.Errorf("port range %#v: first port must be >0, or use '*' for wildcard", portlist)
|
||||
}
|
||||
|
||||
if first > last {
|
||||
return "", ports, vizerror.Errorf("port range %#v: first port must be >= last port", portlist)
|
||||
}
|
||||
|
||||
return host, newPortRange(uint16(first), uint16(last)), nil
|
||||
}
|
||||
|
||||
func newPortRange(first, last uint16) PortRange {
|
||||
return PortRange{First: first, Last: last}
|
||||
}
|
||||
131
tailcfg/proto_port_range_test.go
Normal file
131
tailcfg/proto_port_range_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailcfg
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/vizerror"
|
||||
)
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*ProtoPortRange)(nil)
|
||||
|
||||
func TestProtoPortRangeParsing(t *testing.T) {
|
||||
pr := func(s, e uint16) PortRange {
|
||||
return PortRange{First: s, Last: e}
|
||||
}
|
||||
tests := []struct {
|
||||
in string
|
||||
out ProtoPortRange
|
||||
err error
|
||||
}{
|
||||
{in: "tcp:80", out: ProtoPortRange{Proto: int(ipproto.TCP), Ports: pr(80, 80)}},
|
||||
{in: "80", out: ProtoPortRange{Ports: pr(80, 80)}},
|
||||
{in: "*", out: ProtoPortRange{Ports: PortRangeAny}},
|
||||
{in: "*:*", out: ProtoPortRange{Ports: PortRangeAny}},
|
||||
{in: "tcp:*", out: ProtoPortRange{Proto: int(ipproto.TCP), Ports: PortRangeAny}},
|
||||
{
|
||||
in: "tcp:",
|
||||
err: vizerror.Errorf("invalid port list: %#v", ""),
|
||||
},
|
||||
{
|
||||
in: ":80",
|
||||
err: errEmptyProtocol,
|
||||
},
|
||||
{
|
||||
in: "",
|
||||
err: errEmptyString,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.in, func(t *testing.T) {
|
||||
var ppr ProtoPortRange
|
||||
err := ppr.UnmarshalText([]byte(tc.in))
|
||||
if tc.err != err {
|
||||
if err == nil || tc.err.Error() != err.Error() {
|
||||
t.Fatalf("want err=%v, got %v", tc.err, err)
|
||||
}
|
||||
}
|
||||
if ppr != tc.out {
|
||||
t.Fatalf("got %v; want %v", ppr, tc.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtoPortRangeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input ProtoPortRange
|
||||
want string
|
||||
}{
|
||||
{ProtoPortRange{}, "0"},
|
||||
|
||||
// Zero protocol.
|
||||
{ProtoPortRange{Ports: PortRangeAny}, "*"},
|
||||
{ProtoPortRange{Ports: PortRange{23, 23}}, "23"},
|
||||
{ProtoPortRange{Ports: PortRange{80, 120}}, "80-120"},
|
||||
|
||||
// Non-zero unnamed protocol.
|
||||
{ProtoPortRange{Proto: 100, Ports: PortRange{80, 80}}, "100:80"},
|
||||
{ProtoPortRange{Proto: 200, Ports: PortRange{101, 105}}, "200:101-105"},
|
||||
|
||||
// Non-zero named protocol.
|
||||
{ProtoPortRange{Proto: 1, Ports: PortRangeAny}, "icmp:*"},
|
||||
{ProtoPortRange{Proto: 2, Ports: PortRangeAny}, "igmp:*"},
|
||||
{ProtoPortRange{Proto: 6, Ports: PortRange{10, 13}}, "tcp:10-13"},
|
||||
{ProtoPortRange{Proto: 17, Ports: PortRangeAny}, "udp:*"},
|
||||
{ProtoPortRange{Proto: 0x84, Ports: PortRange{999, 999}}, "sctp:999"},
|
||||
{ProtoPortRange{Proto: 0x3a, Ports: PortRangeAny}, "ipv6-icmp:*"},
|
||||
{ProtoPortRange{Proto: 0x21, Ports: PortRangeAny}, "dccp:*"},
|
||||
{ProtoPortRange{Proto: 0x2f, Ports: PortRangeAny}, "gre:*"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := tc.input.String(); got != tc.want {
|
||||
t.Errorf("String for %v: got %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtoPortRangeRoundTrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
input ProtoPortRange
|
||||
text string
|
||||
}{
|
||||
{ProtoPortRange{Ports: PortRangeAny}, "*"},
|
||||
{ProtoPortRange{Ports: PortRange{23, 23}}, "23"},
|
||||
{ProtoPortRange{Ports: PortRange{80, 120}}, "80-120"},
|
||||
{ProtoPortRange{Proto: 100, Ports: PortRange{80, 80}}, "100:80"},
|
||||
{ProtoPortRange{Proto: 200, Ports: PortRange{101, 105}}, "200:101-105"},
|
||||
{ProtoPortRange{Proto: 1, Ports: PortRangeAny}, "icmp:*"},
|
||||
{ProtoPortRange{Proto: 2, Ports: PortRangeAny}, "igmp:*"},
|
||||
{ProtoPortRange{Proto: 6, Ports: PortRange{10, 13}}, "tcp:10-13"},
|
||||
{ProtoPortRange{Proto: 17, Ports: PortRangeAny}, "udp:*"},
|
||||
{ProtoPortRange{Proto: 0x84, Ports: PortRange{999, 999}}, "sctp:999"},
|
||||
{ProtoPortRange{Proto: 0x3a, Ports: PortRangeAny}, "ipv6-icmp:*"},
|
||||
{ProtoPortRange{Proto: 0x21, Ports: PortRangeAny}, "dccp:*"},
|
||||
{ProtoPortRange{Proto: 0x2f, Ports: PortRangeAny}, "gre:*"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
out, err := tc.input.MarshalText()
|
||||
if err != nil {
|
||||
t.Errorf("MarshalText for %v: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
if got := string(out); got != tc.text {
|
||||
t.Errorf("MarshalText for %#v: got %q, want %q", tc.input, got, tc.text)
|
||||
}
|
||||
var ppr ProtoPortRange
|
||||
if err := ppr.UnmarshalText(out); err != nil {
|
||||
t.Errorf("UnmarshalText for %q: err=%v", tc.text, err)
|
||||
continue
|
||||
}
|
||||
if ppr != tc.input {
|
||||
t.Errorf("round trip error for %q: got %v, want %#v", tc.text, ppr, tc.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1100,6 +1100,17 @@ type RegisterRequest struct {
|
||||
Timestamp *time.Time `json:",omitempty"` // creation time of request to prevent replay
|
||||
DeviceCert []byte `json:",omitempty"` // X.509 certificate for client device
|
||||
Signature []byte `json:",omitempty"` // as described by SignatureType
|
||||
|
||||
// Tailnet is an optional identifier specifying the name of the recommended or required
|
||||
// network that the node should join. Its exact form should not be depended on; new
|
||||
// forms are coming later. The identifier is generally a domain name (for an organization)
|
||||
// or e-mail address (for a personal account on a shared e-mail provider). It is the same name
|
||||
// used by the API, as described in /api.md#tailnet.
|
||||
// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different
|
||||
// network than the one specified. Otherwise, the server should recommend the specified network
|
||||
// but still permit logging in to other networks.
|
||||
// If empty, no recommendation is offered to the server and the login page should show all options.
|
||||
Tailnet string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// RegisterResponse is returned by the server in response to a RegisterRequest.
|
||||
@@ -2436,6 +2447,22 @@ type QueryFeatureResponse struct {
|
||||
ShouldWait bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// WebClientAuthResponse is the response to a web client authentication request
|
||||
// sent to "/machine/webclient/action" or "/machine/webclient/wait".
|
||||
// See client/web for usage.
|
||||
type WebClientAuthResponse struct {
|
||||
// Message, if non-empty, provides a message for the user.
|
||||
Message string `json:",omitempty"`
|
||||
|
||||
// Complete is true when the session authentication has been completed.
|
||||
Complete bool `json:",omitempty"`
|
||||
|
||||
// URL is the link for the user to visit to authenticate the session.
|
||||
//
|
||||
// When empty, there is no action for the user to take.
|
||||
URL string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// OverTLSPublicKeyResponse is the JSON response to /key?v=<n>
|
||||
// over HTTPS (regular TLS) to the Tailscale control plane server,
|
||||
// where the 'v' argument is the client's current capability version
|
||||
|
||||
@@ -362,6 +362,7 @@ var _RegisterRequestCloneNeedsRegeneration = RegisterRequest(struct {
|
||||
Timestamp *time.Time
|
||||
DeviceCert []byte
|
||||
Signature []byte
|
||||
Tailnet string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of DERPHomeParams.
|
||||
|
||||
@@ -792,6 +792,7 @@ func (v RegisterRequestView) DeviceCert() views.ByteSlice[[]byte] {
|
||||
func (v RegisterRequestView) Signature() views.ByteSlice[[]byte] {
|
||||
return views.ByteSliceOf(v.ж.Signature)
|
||||
}
|
||||
func (v RegisterRequestView) Tailnet() string { return v.ж.Tailnet }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
|
||||
@@ -810,6 +811,7 @@ var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
|
||||
Timestamp *time.Time
|
||||
DeviceCert []byte
|
||||
Signature []byte
|
||||
Tailnet string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DERPHomeParams.
|
||||
|
||||
214
taildrop/resume.go
Normal file
214
taildrop/resume.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
blockSize = int64(64 << 10)
|
||||
hashAlgorithm = "sha256"
|
||||
)
|
||||
|
||||
// FileChecksums represents checksums into partially received file.
|
||||
type FileChecksums struct {
|
||||
// Offset is the offset into the file.
|
||||
Offset int64 `json:"offset"`
|
||||
// Length is the length of content being hashed in the file.
|
||||
Length int64 `json:"length"`
|
||||
// Checksums is a list of checksums of BlockSize-sized blocks
|
||||
// starting from Offset. The number of checksums is the Length
|
||||
// divided by BlockSize rounded up to the nearest integer.
|
||||
// All blocks except for the last one are guaranteed to be checksums
|
||||
// over BlockSize-sized blocks.
|
||||
Checksums []Checksum `json:"checksums"`
|
||||
// Algorithm is the hashing algorithm used to compute checksums.
|
||||
Algorithm string `json:"algorithm"` // always "sha256" for now
|
||||
// BlockSize is the size of each block.
|
||||
// The last block may be smaller than this, but never zero.
|
||||
BlockSize int64 `json:"blockSize"` // always (64<<10) for now
|
||||
}
|
||||
|
||||
// Checksum is an opaque checksum that is comparable.
|
||||
type Checksum struct{ cs [sha256.Size]byte }
|
||||
|
||||
func hash(b []byte) Checksum {
|
||||
return Checksum{sha256.Sum256(b)}
|
||||
}
|
||||
func (cs Checksum) String() string {
|
||||
return hex.EncodeToString(cs.cs[:])
|
||||
}
|
||||
func (cs Checksum) AppendText(b []byte) ([]byte, error) {
|
||||
return hexAppendEncode(b, cs.cs[:]), nil
|
||||
}
|
||||
func (cs Checksum) MarshalText() ([]byte, error) {
|
||||
return hexAppendEncode(nil, cs.cs[:]), nil
|
||||
}
|
||||
func (cs *Checksum) UnmarshalText(b []byte) error {
|
||||
if len(b) != 2*len(cs.cs) {
|
||||
return fmt.Errorf("invalid hex length: %d", len(b))
|
||||
}
|
||||
_, err := hex.Decode(cs.cs[:], b)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(https://go.dev/issue/53693): Use hex.AppendEncode instead.
|
||||
func hexAppendEncode(dst, src []byte) []byte {
|
||||
n := hex.EncodedLen(len(src))
|
||||
dst = slices.Grow(dst, n)
|
||||
hex.Encode(dst[len(dst):][:n], src)
|
||||
return dst[:len(dst)+n]
|
||||
}
|
||||
|
||||
// PartialFiles returns a list of partial files in [Handler.Dir]
|
||||
// that were sent (or is actively being sent) by the provided id.
|
||||
func (m *Manager) PartialFiles(id ClientID) (ret []string, err error) {
|
||||
if m == nil || m.Dir == "" {
|
||||
return nil, ErrNoTaildrop
|
||||
}
|
||||
if m.DirectFileMode && m.AvoidFinalRename {
|
||||
return nil, nil // resuming is not supported for users that peek at our file structure
|
||||
}
|
||||
|
||||
f, err := os.Open(m.Dir)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
suffix := id.partialSuffix()
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
for _, de := range des {
|
||||
if name := de.Name(); strings.HasSuffix(name, suffix) {
|
||||
ret = append(ret, name)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HashPartialFile hashes the contents of a partial file sent by id,
|
||||
// starting at the specified offset and for the specified length.
|
||||
// If length is negative, it hashes the entire file.
|
||||
// If the length exceeds the remaining file length, then it hashes until EOF.
|
||||
// If [FileHashes.Length] is less than length and no error occurred,
|
||||
// then it implies that all remaining content in the file has been hashed.
|
||||
func (m *Manager) HashPartialFile(id ClientID, baseName string, offset, length int64) (FileChecksums, error) {
|
||||
if m == nil || m.Dir == "" {
|
||||
return FileChecksums{}, ErrNoTaildrop
|
||||
}
|
||||
if m.DirectFileMode && m.AvoidFinalRename {
|
||||
return FileChecksums{}, nil // resuming is not supported for users that peek at our file structure
|
||||
}
|
||||
|
||||
dstFile, err := m.joinDir(baseName)
|
||||
if err != nil {
|
||||
return FileChecksums{}, err
|
||||
}
|
||||
f, err := os.Open(dstFile + id.partialSuffix())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return FileChecksums{}, nil
|
||||
}
|
||||
return FileChecksums{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Seek(offset, io.SeekStart); err != nil {
|
||||
return FileChecksums{}, err
|
||||
}
|
||||
checksums := FileChecksums{
|
||||
Offset: offset,
|
||||
Algorithm: hashAlgorithm,
|
||||
BlockSize: blockSize,
|
||||
}
|
||||
b := make([]byte, blockSize) // TODO: Pool this?
|
||||
r := io.Reader(f)
|
||||
if length >= 0 {
|
||||
r = io.LimitReader(f, length)
|
||||
}
|
||||
for {
|
||||
switch n, err := io.ReadFull(r, b); {
|
||||
case err != nil && err != io.EOF && err != io.ErrUnexpectedEOF:
|
||||
return checksums, err
|
||||
case n == 0:
|
||||
return checksums, nil
|
||||
default:
|
||||
checksums.Checksums = append(checksums.Checksums, hash(b[:n]))
|
||||
checksums.Length += int64(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResumeReader reads and discards the leading content of r
|
||||
// that matches the content based on the checksums that exist.
|
||||
// It returns the number of bytes consumed,
|
||||
// and returns an [io.Reader] representing the remaining content.
|
||||
func ResumeReader(r io.Reader, hashFile func(offset, length int64) (FileChecksums, error)) (int64, io.Reader, error) {
|
||||
if hashFile == nil {
|
||||
return 0, r, nil
|
||||
}
|
||||
|
||||
// Ask for checksums of a particular content length,
|
||||
// where the amount of memory needed to represent the checksums themselves
|
||||
// is exactly equal to the blockSize.
|
||||
numBlocks := blockSize / sha256.Size
|
||||
hashLength := blockSize * numBlocks
|
||||
|
||||
var offset int64
|
||||
b := make([]byte, 0, blockSize)
|
||||
for {
|
||||
// Request a list of checksums for the partial file starting at offset.
|
||||
checksums, err := hashFile(offset, hashLength)
|
||||
if len(checksums.Checksums) == 0 || err != nil {
|
||||
return offset, io.MultiReader(bytes.NewReader(b), r), err
|
||||
} else if checksums.BlockSize != blockSize || checksums.Algorithm != hashAlgorithm {
|
||||
return offset, io.MultiReader(bytes.NewReader(b), r), fmt.Errorf("invalid block size or hashing algorithm")
|
||||
}
|
||||
|
||||
// Read from r, comparing each block with the provided checksums.
|
||||
for _, want := range checksums.Checksums {
|
||||
// Read a block from r.
|
||||
n, err := io.ReadFull(r, b[:blockSize])
|
||||
b = b[:n]
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
err = nil
|
||||
}
|
||||
if len(b) == 0 || err != nil {
|
||||
// This should not occur in practice.
|
||||
// It implies that an error occurred reading r,
|
||||
// or that the partial file on the remote side is fully complete.
|
||||
return offset, io.MultiReader(bytes.NewReader(b), r), err
|
||||
}
|
||||
|
||||
// Compare the local and remote block checksums.
|
||||
// If it mismatches, then resume from this point.
|
||||
got := hash(b)
|
||||
if got != want {
|
||||
return offset, io.MultiReader(bytes.NewReader(b), r), nil
|
||||
}
|
||||
offset += int64(len(b))
|
||||
b = b[:0]
|
||||
}
|
||||
|
||||
// We hashed the remainder of the partial file, so stop.
|
||||
if checksums.Length < hashLength {
|
||||
return offset, io.MultiReader(bytes.NewReader(b), r), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
63
taildrop/resume_test.go
Normal file
63
taildrop/resume_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestResume(t *testing.T) {
|
||||
oldBlockSize := blockSize
|
||||
defer func() { blockSize = oldBlockSize }()
|
||||
blockSize = 256
|
||||
|
||||
m := Manager{Logf: t.Logf, Dir: t.TempDir()}
|
||||
|
||||
rn := rand.New(rand.NewSource(0))
|
||||
want := make([]byte, 12345)
|
||||
must.Get(io.ReadFull(rn, want))
|
||||
|
||||
t.Run("resume-noop", func(t *testing.T) {
|
||||
r := io.Reader(bytes.NewReader(want))
|
||||
offset, r, err := ResumeReader(r, func(offset, length int64) (FileChecksums, error) {
|
||||
return m.HashPartialFile("", "foo", offset, length)
|
||||
})
|
||||
must.Do(err)
|
||||
must.Get(m.PutFile("", "foo", r, offset, -1))
|
||||
got := must.Get(os.ReadFile(must.Get(m.joinDir("foo"))))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("content mismatches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume-retry", func(t *testing.T) {
|
||||
rn := rand.New(rand.NewSource(0))
|
||||
for {
|
||||
r := io.Reader(bytes.NewReader(want))
|
||||
offset, r, err := ResumeReader(r, func(offset, length int64) (FileChecksums, error) {
|
||||
return m.HashPartialFile("", "foo", offset, length)
|
||||
})
|
||||
must.Do(err)
|
||||
numWant := rn.Int63n(min(int64(len(want))-offset, 1000) + 1)
|
||||
if offset < int64(len(want)) {
|
||||
r = io.MultiReader(io.LimitReader(r, numWant), iotest.ErrReader(io.ErrClosedPipe))
|
||||
}
|
||||
if _, err := m.PutFile("", "foo", r, offset, -1); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
got := must.Get(os.ReadFile(must.Get(m.joinDir("foo"))))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("content mismatches")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
)
|
||||
|
||||
// HasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
func (s *Handler) HasFilesWaiting() bool {
|
||||
if s == nil || s.RootDir == "" || s.DirectFileMode {
|
||||
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
|
||||
// This always returns false when [Handler.DirectFileMode] is false.
|
||||
func (m *Manager) HasFilesWaiting() bool {
|
||||
if m == nil || m.Dir == "" || m.DirectFileMode {
|
||||
return false
|
||||
}
|
||||
if s.KnownEmpty.Load() {
|
||||
if m.knownEmpty.Load() {
|
||||
// Optimization: this is usually empty, so avoid opening
|
||||
// the directory and checking. We can't cache the actual
|
||||
// has-files-or-not values as the macOS/iOS client might
|
||||
@@ -33,7 +33,7 @@ func (s *Handler) HasFilesWaiting() bool {
|
||||
// keep this negative cache.
|
||||
return false
|
||||
}
|
||||
f, err := os.Open(s.RootDir)
|
||||
f, err := os.Open(m.Dir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func (s *Handler) HasFilesWaiting() bool {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, PartialSuffix) {
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
|
||||
@@ -51,22 +51,22 @@ func (s *Handler) HasFilesWaiting() bool {
|
||||
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
|
||||
// and we don't want to delete the ".deleted" file before
|
||||
// enumerating to the "foo.jpg" file.
|
||||
defer tryDeleteAgain(filepath.Join(s.RootDir, name))
|
||||
defer tryDeleteAgain(filepath.Join(m.Dir, name))
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
_, err := os.Stat(filepath.Join(s.RootDir, name+deletedSuffix))
|
||||
_, err := os.Stat(filepath.Join(m.Dir, name+deletedSuffix))
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
tryDeleteAgain(filepath.Join(s.RootDir, name))
|
||||
tryDeleteAgain(filepath.Join(m.Dir, name))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
s.KnownEmpty.Store(true)
|
||||
m.knownEmpty.Store(true)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
@@ -76,22 +76,16 @@ func (s *Handler) HasFilesWaiting() bool {
|
||||
}
|
||||
|
||||
// WaitingFiles returns the list of files that have been sent by a
|
||||
// peer that are waiting in the buffered "pick up" directory owned by
|
||||
// the Tailscale daemon.
|
||||
//
|
||||
// As a side effect, it also does any lazy deletion of files as
|
||||
// required by Windows.
|
||||
func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s == nil {
|
||||
return nil, errNilHandler
|
||||
}
|
||||
if s.RootDir == "" {
|
||||
// peer that are waiting in [Handler.Dir].
|
||||
// This always returns nil when [Handler.DirectFileMode] is false.
|
||||
func (m *Manager) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if m == nil || m.Dir == "" {
|
||||
return nil, ErrNoTaildrop
|
||||
}
|
||||
if s.DirectFileMode {
|
||||
if m.DirectFileMode {
|
||||
return nil, nil
|
||||
}
|
||||
f, err := os.Open(s.RootDir)
|
||||
f, err := os.Open(m.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -101,7 +95,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, PartialSuffix) {
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
|
||||
@@ -143,7 +137,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
// Maybe Windows is done virus scanning the file we tried
|
||||
// to delete a long time ago and will let us delete it now.
|
||||
for name := range deleted {
|
||||
tryDeleteAgain(filepath.Join(s.RootDir, name))
|
||||
tryDeleteAgain(filepath.Join(m.Dir, name))
|
||||
}
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
||||
@@ -164,27 +158,26 @@ func tryDeleteAgain(fullPath string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Handler) DeleteFile(baseName string) error {
|
||||
if s == nil {
|
||||
return errNilHandler
|
||||
}
|
||||
if s.RootDir == "" {
|
||||
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
|
||||
// This method is only allowed when [Handler.DirectFileMode] is false.
|
||||
func (m *Manager) DeleteFile(baseName string) error {
|
||||
if m == nil || m.Dir == "" {
|
||||
return ErrNoTaildrop
|
||||
}
|
||||
if s.DirectFileMode {
|
||||
if m.DirectFileMode {
|
||||
return errors.New("deletes not allowed in direct mode")
|
||||
}
|
||||
path, ok := s.DiskPath(baseName)
|
||||
if !ok {
|
||||
return errors.New("bad filename")
|
||||
path, err := m.joinDir(baseName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var bo *backoff.Backoff
|
||||
logf := s.Logf
|
||||
t0 := s.Clock.Now()
|
||||
logf := m.Logf
|
||||
t0 := m.Clock.Now()
|
||||
for {
|
||||
err := os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
err = RedactErr(err)
|
||||
err = redactErr(err)
|
||||
// Put a retry loop around deletes on Windows. Windows
|
||||
// file descriptor closes are effectively asynchronous,
|
||||
// as a bunch of hooks run on/after close, and we can't
|
||||
@@ -199,11 +192,11 @@ func (s *Handler) DeleteFile(baseName string) error {
|
||||
if bo == nil {
|
||||
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
|
||||
}
|
||||
if s.Clock.Since(t0) < 5*time.Second {
|
||||
if m.Clock.Since(t0) < 5*time.Second {
|
||||
bo.BackOff(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err := TouchFile(path + deletedSuffix); err != nil {
|
||||
if err := touchFile(path + deletedSuffix); err != nil {
|
||||
logf("peerapi: failed to leave deleted marker: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -214,27 +207,26 @@ func (s *Handler) DeleteFile(baseName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func TouchFile(path string) error {
|
||||
func touchFile(path string) error {
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return RedactErr(err)
|
||||
return redactErr(err)
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if s == nil {
|
||||
return nil, 0, errNilHandler
|
||||
}
|
||||
if s.RootDir == "" {
|
||||
// OpenFile opens a file of the given baseName from [Handler.Dir].
|
||||
// This method is only allowed when [Handler.DirectFileMode] is false.
|
||||
func (m *Manager) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if m == nil || m.Dir == "" {
|
||||
return nil, 0, ErrNoTaildrop
|
||||
}
|
||||
if s.DirectFileMode {
|
||||
if m.DirectFileMode {
|
||||
return nil, 0, errors.New("opens not allowed in direct mode")
|
||||
}
|
||||
path, ok := s.DiskPath(baseName)
|
||||
if !ok {
|
||||
return nil, 0, errors.New("bad filename")
|
||||
path, err := m.joinDir(baseName)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
|
||||
tryDeleteAgain(path)
|
||||
@@ -242,12 +234,12 @@ func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err e
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, RedactErr(err)
|
||||
return nil, 0, redactErr(err)
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, 0, RedactErr(err)
|
||||
return nil, 0, redactErr(err)
|
||||
}
|
||||
return f, fi.Size(), nil
|
||||
}
|
||||
255
taildrop/send.go
Normal file
255
taildrop/send.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
type incomingFileKey struct {
|
||||
id ClientID
|
||||
name string // e.g., "foo.jpeg"
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
clock tstime.DefaultClock
|
||||
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
sendFileNotify func() // called when done
|
||||
partialPath string // non-empty in direct mode
|
||||
|
||||
mu sync.Mutex
|
||||
copied int64
|
||||
done bool
|
||||
lastNotify time.Time
|
||||
}
|
||||
|
||||
func (f *incomingFile) Write(p []byte) (n int, err error) {
|
||||
n, err = f.w.Write(p)
|
||||
|
||||
var needNotify bool
|
||||
defer func() {
|
||||
if needNotify {
|
||||
f.sendFileNotify()
|
||||
}
|
||||
}()
|
||||
if n > 0 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.copied += int64(n)
|
||||
now := f.clock.Now()
|
||||
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
|
||||
f.lastNotify = now
|
||||
needNotify = true
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// PutFile stores a file into [Manager.Dir] from a given client id.
|
||||
// The baseName must be a base filename without any slashes.
|
||||
// The length is the expected length of content to read from r,
|
||||
// it may be negative to indicate that it is unknown.
|
||||
// It returns the length of the entire file.
|
||||
//
|
||||
// If there is a failure reading from r, then the partial file is not deleted
|
||||
// for some period of time. The [Manager.PartialFiles] and [Manager.HashPartialFile]
|
||||
// methods may be used to list all partial files and to compute the hash for a
|
||||
// specific partial file. This allows the client to determine whether to resume
|
||||
// a partial file. While resuming, PutFile may be called again with a non-zero
|
||||
// offset to specify where to resume receiving data at.
|
||||
func (m *Manager) PutFile(id ClientID, baseName string, r io.Reader, offset, length int64) (int64, error) {
|
||||
switch {
|
||||
case m == nil || m.Dir == "":
|
||||
return 0, ErrNoTaildrop
|
||||
case !envknob.CanTaildrop():
|
||||
return 0, ErrNoTaildrop
|
||||
case distro.Get() == distro.Unraid && !m.DirectFileMode:
|
||||
return 0, ErrNotAccessible
|
||||
}
|
||||
dstPath, err := m.joinDir(baseName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
redactAndLogError := func(action string, err error) error {
|
||||
err = redactErr(err)
|
||||
m.Logf("put %v error: %v", action, err)
|
||||
return err
|
||||
}
|
||||
|
||||
avoidPartialRename := m.DirectFileMode && m.AvoidFinalRename
|
||||
if avoidPartialRename {
|
||||
// Users using AvoidFinalRename are depending on the exact filename
|
||||
// of the partial files. So avoid injecting the id into it.
|
||||
id = ""
|
||||
}
|
||||
|
||||
// Check whether there is an in-progress transfer for the file.
|
||||
sendFileNotify := m.SendFileNotify
|
||||
if sendFileNotify == nil {
|
||||
sendFileNotify = func() {} // avoid nil panics below
|
||||
}
|
||||
partialPath := dstPath + id.partialSuffix()
|
||||
inFileKey := incomingFileKey{id, baseName}
|
||||
inFile, loaded := m.incomingFiles.LoadOrInit(inFileKey, func() *incomingFile {
|
||||
inFile := &incomingFile{
|
||||
clock: m.Clock,
|
||||
started: m.Clock.Now(),
|
||||
size: length,
|
||||
sendFileNotify: sendFileNotify,
|
||||
}
|
||||
if m.DirectFileMode {
|
||||
inFile.partialPath = partialPath
|
||||
}
|
||||
return inFile
|
||||
})
|
||||
if loaded {
|
||||
return 0, ErrFileExists
|
||||
}
|
||||
defer m.incomingFiles.Delete(inFileKey)
|
||||
|
||||
// Create (if not already) the partial file with read-write permissions.
|
||||
f, err := os.OpenFile(partialPath, os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Create", err)
|
||||
}
|
||||
defer func() {
|
||||
f.Close() // best-effort to cleanup dangling file handles
|
||||
if err != nil {
|
||||
if avoidPartialRename {
|
||||
os.Remove(partialPath) // best-effort
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: We need to delete partialPath eventually.
|
||||
// However, this must be done after some period of time.
|
||||
}
|
||||
}()
|
||||
inFile.w = f
|
||||
|
||||
// A positive offset implies that we are resuming an existing file.
|
||||
// Seek to the appropriate offset and truncate the file.
|
||||
if offset != 0 {
|
||||
currLength, err := f.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Seek", err)
|
||||
}
|
||||
if offset < 0 || offset > currLength {
|
||||
return 0, redactAndLogError("Seek", err)
|
||||
}
|
||||
if _, err := f.Seek(offset, io.SeekStart); err != nil {
|
||||
return 0, redactAndLogError("Seek", err)
|
||||
}
|
||||
if err := f.Truncate(offset); err != nil {
|
||||
return 0, redactAndLogError("Truncate", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the contents of the file.
|
||||
copyLength, err := io.Copy(inFile, r)
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Copy", err)
|
||||
}
|
||||
if length >= 0 && copyLength != length {
|
||||
return 0, redactAndLogError("Copy", errors.New("copied an unexpected number of bytes"))
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return 0, redactAndLogError("Close", err)
|
||||
}
|
||||
fileLength := offset + copyLength
|
||||
|
||||
// Return early for avoidPartialRename since users of AvoidFinalRename
|
||||
// are depending on the exact naming of partial files.
|
||||
if avoidPartialRename {
|
||||
inFile.mu.Lock()
|
||||
inFile.done = true
|
||||
inFile.mu.Unlock()
|
||||
m.knownEmpty.Store(false)
|
||||
sendFileNotify()
|
||||
return fileLength, nil
|
||||
}
|
||||
|
||||
// File has been successfully received, rename the partial file
|
||||
// to the final destination filename. If a file of that name already exists,
|
||||
// then try multiple times with variations of the filename.
|
||||
computePartialSum := sync.OnceValues(func() ([sha256.Size]byte, error) {
|
||||
return sha256File(partialPath)
|
||||
})
|
||||
maxRetries := 10
|
||||
for ; maxRetries > 0; maxRetries-- {
|
||||
// Atomically rename the partial file as the destination file if it doesn't exist.
|
||||
// Otherwise, it returns the length of the current destination file.
|
||||
// The operation is atomic.
|
||||
dstLength, err := func() (int64, error) {
|
||||
m.renameMu.Lock()
|
||||
defer m.renameMu.Unlock()
|
||||
switch fi, err := os.Stat(dstPath); {
|
||||
case os.IsNotExist(err):
|
||||
return -1, os.Rename(partialPath, dstPath)
|
||||
case err != nil:
|
||||
return -1, err
|
||||
default:
|
||||
return fi.Size(), nil
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Rename", err)
|
||||
}
|
||||
if dstLength < 0 {
|
||||
break // we successfully renamed; so stop
|
||||
}
|
||||
|
||||
// Avoid the final rename if a destination file has the same contents.
|
||||
if dstLength == fileLength {
|
||||
partialSum, err := computePartialSum()
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Rename", err)
|
||||
}
|
||||
dstSum, err := sha256File(dstPath)
|
||||
if err != nil {
|
||||
return 0, redactAndLogError("Rename", err)
|
||||
}
|
||||
if dstSum == partialSum {
|
||||
if err := os.Remove(partialPath); err != nil {
|
||||
return 0, redactAndLogError("Remove", err)
|
||||
}
|
||||
break // we successfully found a content match; so stop
|
||||
}
|
||||
}
|
||||
|
||||
// Choose a new destination filename and try again.
|
||||
dstPath = NextFilename(dstPath)
|
||||
}
|
||||
if maxRetries <= 0 {
|
||||
return 0, errors.New("too many retries trying to rename partial file")
|
||||
}
|
||||
m.knownEmpty.Store(false)
|
||||
sendFileNotify()
|
||||
return fileLength, nil
|
||||
}
|
||||
|
||||
func sha256File(file string) (out [sha256.Size]byte, err error) {
|
||||
h := sha256.New()
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return [sha256.Size]byte(h.Sum(nil)), nil
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package taildrop contains the implementation of the Taildrop
|
||||
// functionality including sending and retrieving files.
|
||||
// This package does not validate permissions, the caller should
|
||||
// be responsible for ensuring correct authorization.
|
||||
//
|
||||
// For related documentation see: http://go/taildrop-how-does-it-work
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
@@ -9,50 +15,91 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
// ClientID is an opaque identifier for file resumption.
|
||||
// A client can only list and resume partial files for its own ID.
|
||||
// It must contain any filesystem specific characters (e.g., slashes).
|
||||
type ClientID string // e.g., "n12345CNTRL"
|
||||
|
||||
func (id ClientID) partialSuffix() string {
|
||||
if id == "" {
|
||||
return partialSuffix
|
||||
}
|
||||
return "." + string(id) + partialSuffix // e.g., ".n12345CNTRL.partial"
|
||||
}
|
||||
|
||||
// Manager manages the state for receiving and managing taildropped files.
|
||||
type Manager struct {
|
||||
Logf logger.Logf
|
||||
Clock tstime.Clock
|
||||
Clock tstime.DefaultClock
|
||||
|
||||
RootDir string // empty means file receiving unavailable
|
||||
// Dir is the directory to store received files.
|
||||
// This main either be the final location for the files
|
||||
// or just a temporary staging directory (see DirectFileMode).
|
||||
Dir string
|
||||
|
||||
// DirectFileMode is whether we're writing files directly to a
|
||||
// download directory (as *.partial files), rather than making
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on the GUI macOS versions
|
||||
// and on Synology.
|
||||
// In DirectFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg" unless
|
||||
// directFileDoFinalRename is set.
|
||||
// DirectFileMode reports whether we are writing files
|
||||
// directly to a download directory, rather than writing them to
|
||||
// a temporary staging directory.
|
||||
//
|
||||
// The following methods:
|
||||
// - HasFilesWaiting
|
||||
// - WaitingFiles
|
||||
// - DeleteFile
|
||||
// - OpenFile
|
||||
// have no purpose in DirectFileMode.
|
||||
// They are only used to check whether files are in the staging directory,
|
||||
// copy them out, and then delete them.
|
||||
DirectFileMode bool
|
||||
|
||||
// DirectFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
DirectFileDoFinalRename bool
|
||||
// AvoidFinalRename specifies whether in DirectFileMode
|
||||
// we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
|
||||
//
|
||||
// TODO(joetsai,rhea): Delete this. This is currently depended upon
|
||||
// in the Apple platforms since it violates the abstraction layer
|
||||
// and directly assumes how taildrop represents partial files.
|
||||
// Right now, file resumption does not work on Apple.
|
||||
AvoidFinalRename bool
|
||||
|
||||
KnownEmpty atomic.Bool
|
||||
// SendFileNotify is called periodically while a file is actively
|
||||
// receiving the contents for the file. There is a final call
|
||||
// to the function when reception completes.
|
||||
// It is not called if nil.
|
||||
SendFileNotify func()
|
||||
|
||||
knownEmpty atomic.Bool
|
||||
|
||||
incomingFiles syncs.Map[incomingFileKey, *incomingFile]
|
||||
|
||||
// renameMu is used to protect os.Rename calls so that they are atomic.
|
||||
renameMu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
errNilHandler = errors.New("handler unavailable; not listening")
|
||||
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
|
||||
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
|
||||
ErrInvalidFileName = errors.New("invalid filename")
|
||||
ErrFileExists = errors.New("file already exists")
|
||||
ErrNotAccessible = errors.New("Taildrop folder not configured or accessible")
|
||||
)
|
||||
|
||||
const (
|
||||
// PartialSuffix is the suffix appended to files while they're
|
||||
// partialSuffix is the suffix appended to files while they're
|
||||
// still in the process of being transferred.
|
||||
PartialSuffix = ".partial"
|
||||
partialSuffix = ".partial"
|
||||
|
||||
// deletedSuffix is the suffix for a deleted marker file
|
||||
// that's placed next to a file (without the suffix) that we
|
||||
@@ -84,33 +131,55 @@ func validFilenameRune(r rune) bool {
|
||||
return unicode.IsPrint(r)
|
||||
}
|
||||
|
||||
func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
|
||||
func (m *Manager) joinDir(baseName string) (fullPath string, err error) {
|
||||
if !utf8.ValidString(baseName) {
|
||||
return "", false
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
if strings.TrimSpace(baseName) != baseName {
|
||||
return "", false
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
if len(baseName) > 255 {
|
||||
return "", false
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
// TODO: validate unicode normalization form too? Varies by platform.
|
||||
clean := path.Clean(baseName)
|
||||
if clean != baseName ||
|
||||
clean == "." || clean == ".." ||
|
||||
strings.HasSuffix(clean, deletedSuffix) ||
|
||||
strings.HasSuffix(clean, PartialSuffix) {
|
||||
return "", false
|
||||
strings.HasSuffix(clean, partialSuffix) {
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
for _, r := range baseName {
|
||||
if !validFilenameRune(r) {
|
||||
return "", false
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
}
|
||||
if !filepath.IsLocal(baseName) {
|
||||
return "", false
|
||||
return "", ErrInvalidFileName
|
||||
}
|
||||
return filepath.Join(s.RootDir, baseName), true
|
||||
return filepath.Join(m.Dir, baseName), nil
|
||||
}
|
||||
|
||||
// IncomingFiles returns a list of active incoming files.
|
||||
func (m *Manager) IncomingFiles() []ipn.PartialFile {
|
||||
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
|
||||
// in JSON to clients. They distinguish between empty and non-nil
|
||||
// to know whether a Notify should be able about files.
|
||||
files := make([]ipn.PartialFile, 0)
|
||||
m.incomingFiles.Range(func(k incomingFileKey, f *incomingFile) bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
files = append(files, ipn.PartialFile{
|
||||
Name: k.name,
|
||||
Started: f.started,
|
||||
DeclaredSize: f.size,
|
||||
Received: f.copied,
|
||||
PartialPath: f.partialPath,
|
||||
Done: f.done,
|
||||
})
|
||||
return true
|
||||
})
|
||||
return files
|
||||
}
|
||||
|
||||
type redactedErr struct {
|
||||
@@ -136,7 +205,7 @@ func redactString(s string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func RedactErr(root error) error {
|
||||
func redactErr(root error) error {
|
||||
// redactStrings is a list of sensitive strings that were redacted.
|
||||
// It is not sufficient to just snub out sensitive fields in Go errors
|
||||
// since some wrapper errors like fmt.Errorf pre-cache the error string,
|
||||
@@ -176,3 +245,26 @@ func RedactErr(root error) error {
|
||||
}
|
||||
return &redactedErr{msg: s, inner: root}
|
||||
}
|
||||
|
||||
var (
|
||||
rxExtensionSuffix = regexp.MustCompile(`(\.[a-zA-Z0-9]{0,3}[a-zA-Z][a-zA-Z0-9]{0,3})*$`)
|
||||
rxNumberSuffix = regexp.MustCompile(` \([0-9]+\)`)
|
||||
)
|
||||
|
||||
// NextFilename returns the next filename in a sequence.
|
||||
// It is used for construction a new filename if there is a conflict.
|
||||
//
|
||||
// For example, "Foo.jpg" becomes "Foo (1).jpg" and
|
||||
// "Foo (1).jpg" becomes "Foo (2).jpg".
|
||||
func NextFilename(name string) string {
|
||||
ext := rxExtensionSuffix.FindString(strings.TrimPrefix(name, "."))
|
||||
name = strings.TrimSuffix(name, ext)
|
||||
var n uint64
|
||||
if rxNumberSuffix.MatchString(name) {
|
||||
i := strings.LastIndex(name, " (")
|
||||
if n, _ = strconv.ParseUint(name[i+len("( "):len(name)-len(")")], 10, 64); n > 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
}
|
||||
return name + " (" + strconv.FormatUint(n+1, 10) + ")" + ext
|
||||
}
|
||||
|
||||
184
taildrop/taildrop_test.go
Normal file
184
taildrop/taildrop_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests "foo.jpg.deleted" marks (for Windows).
|
||||
func TestDeletedMarkers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
h := &Manager{Dir: dir}
|
||||
|
||||
nothingWaiting := func() {
|
||||
t.Helper()
|
||||
h.knownEmpty.Store(false)
|
||||
if h.HasFilesWaiting() {
|
||||
t.Fatal("unexpected files waiting")
|
||||
}
|
||||
}
|
||||
touch := func(base string) {
|
||||
t.Helper()
|
||||
if err := touchFile(filepath.Join(dir, base)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wantEmptyTempDir := func() {
|
||||
t.Helper()
|
||||
if fis, err := os.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(fis) > 0 && runtime.GOOS != "windows" {
|
||||
for _, fi := range fis {
|
||||
t.Errorf("unexpected file in tempdir: %q", fi.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
wf, err := h.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wf) != 0 {
|
||||
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
if rc, _, err := h.OpenFile("foo.jpg"); err == nil {
|
||||
rc.Close()
|
||||
t.Fatal("unexpected foo.jpg open")
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
// And verify basics still work in non-deleted cases.
|
||||
touch("foo.jpg")
|
||||
touch("bar.jpg.deleted")
|
||||
if wf, err := h.WaitingFiles(); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(wf) != 1 {
|
||||
t.Errorf("WaitingFiles = %d; want 1", len(wf))
|
||||
} else if wf[0].Name != "foo.jpg" {
|
||||
t.Errorf("unexpected waiting file %+v", wf[0])
|
||||
}
|
||||
if rc, _, err := h.OpenFile("foo.jpg"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err func() error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "PathError",
|
||||
err: func() error {
|
||||
return &os.PathError{
|
||||
Op: "open",
|
||||
Path: "/tmp/sensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `open redacted.41360718: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "LinkError",
|
||||
err: func() error {
|
||||
return &os.LinkError{
|
||||
Op: "symlink",
|
||||
Old: "/tmp/sensitive.txt",
|
||||
New: "/tmp/othersensitive.txt",
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
},
|
||||
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
|
||||
},
|
||||
{
|
||||
name: "something else",
|
||||
err: func() error { return errors.New("i am another error type") },
|
||||
want: `i am another error type`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// For debugging
|
||||
var i int
|
||||
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
|
||||
t.Logf("%d: %T @ %p", i, err, err)
|
||||
i++
|
||||
}
|
||||
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
got := redactErr(tc.err()).Error()
|
||||
if got != tc.want {
|
||||
t.Errorf("err = %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
t.Run("Wrapped", func(t *testing.T) {
|
||||
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
|
||||
want := "wrapped error: " + tc.want
|
||||
|
||||
got := redactErr(wrapped).Error()
|
||||
if got != want {
|
||||
t.Errorf("err = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
want2 string
|
||||
}{
|
||||
{"foo", "foo (1)", "foo (2)"},
|
||||
{"foo(1)", "foo(1) (1)", "foo(1) (2)"},
|
||||
{"foo.tar", "foo (1).tar", "foo (2).tar"},
|
||||
{"foo.tar.gz", "foo (1).tar.gz", "foo (2).tar.gz"},
|
||||
{".bashrc", ".bashrc (1)", ".bashrc (2)"},
|
||||
{"fizz buzz.torrent", "fizz buzz (1).torrent", "fizz buzz (2).torrent"},
|
||||
{"rawr 2023.12.15.txt", "rawr 2023.12.15 (1).txt", "rawr 2023.12.15 (2).txt"},
|
||||
{"IMG_7934.JPEG", "IMG_7934 (1).JPEG", "IMG_7934 (2).JPEG"},
|
||||
{"my song.mp3", "my song (1).mp3", "my song (2).mp3"},
|
||||
{"archive.7z", "archive (1).7z", "archive (2).7z"},
|
||||
{"foo/bar/fizz", "foo/bar/fizz (1)", "foo/bar/fizz (2)"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := NextFilename(tt.in); got != tt.want {
|
||||
t.Errorf("NextFilename(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
if got2 := NextFilename(tt.want); got2 != tt.want2 {
|
||||
t.Errorf("NextFilename(%q) = %q, want %q", tt.want, got2, tt.want2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,10 @@ if [[ -d "$toolchain" ]]; then
|
||||
# A toolchain exists, but is it recent enough to compile gocross? If not,
|
||||
# wipe it out so that the next if block fetches a usable one.
|
||||
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
|
||||
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
|
||||
have_go_minor=""
|
||||
if [[ -f "$toolchain/VERSION" ]]; then
|
||||
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
|
||||
fi
|
||||
# Shortly before stable releases, we run release candidate
|
||||
# toolchains, which have a non-numeric suffix on the version
|
||||
# number. Remove the rc qualifier, we just care about the minor
|
||||
|
||||
20
tsd/tsd.go
20
tsd/tsd.go
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -47,6 +48,16 @@ type System struct {
|
||||
StateStore SubSystem[ipn.StateStore]
|
||||
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
|
||||
|
||||
// InitialConfig is initial server config, if any.
|
||||
// It is nil if the node is not in declarative mode.
|
||||
// This value is never updated after startup.
|
||||
// LocalBackend tracks the current config after any reloads.
|
||||
InitialConfig *conffile.Config
|
||||
|
||||
// onlyNetstack is whether the Tun value is a fake TUN device
|
||||
// and we're using netstack for everything.
|
||||
onlyNetstack bool
|
||||
|
||||
controlKnobs controlknobs.Knobs
|
||||
proxyMap proxymap.Mapper
|
||||
}
|
||||
@@ -74,6 +85,12 @@ func (s *System) Set(v any) {
|
||||
case router.Router:
|
||||
s.Router.Set(v)
|
||||
case *tstun.Wrapper:
|
||||
type ft interface {
|
||||
IsFakeTun() bool
|
||||
}
|
||||
if _, ok := v.Unwrap().(ft); ok {
|
||||
s.onlyNetstack = true
|
||||
}
|
||||
s.Tun.Set(v)
|
||||
case *magicsock.Conn:
|
||||
s.MagicSock.Set(v)
|
||||
@@ -97,8 +114,7 @@ func (s *System) IsNetstackRouter() bool {
|
||||
|
||||
// IsNetstack reports whether Tailscale is running as a netstack-based TUN-free engine.
|
||||
func (s *System) IsNetstack() bool {
|
||||
name, _ := s.Tun.Get().Name()
|
||||
return name == tstun.FakeTUNName
|
||||
return s.onlyNetstack
|
||||
}
|
||||
|
||||
// ControlKnobs returns the control knobs for this node.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -21,7 +20,6 @@ var (
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
|
||||
s := new(tsnet.Server)
|
||||
defer s.Close()
|
||||
@@ -32,7 +30,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Serve the Tailscale web client.
|
||||
ws, cleanup := web.NewServer(ctx, web.ServerOpts{
|
||||
ws, cleanup := web.NewServer(web.ServerOpts{
|
||||
DevMode: *devMode,
|
||||
LocalClient: lc,
|
||||
})
|
||||
|
||||
@@ -53,6 +53,7 @@ import (
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
@@ -133,12 +134,26 @@ type Server struct {
|
||||
logtail *logtail.Logger
|
||||
logid logid.PublicID
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
dialer *tsdial.Dialer
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
fallbackTCPHandlers set.HandleSet[FallbackTCPHandler]
|
||||
dialer *tsdial.Dialer
|
||||
closed bool
|
||||
}
|
||||
|
||||
// FallbackTCPHandler describes the callback which
|
||||
// conditionally handles an incoming TCP flow for the
|
||||
// provided (src/port, dst/port) 4-tuple. These are registered
|
||||
// as handlers of last resort, and are called only if no
|
||||
// listener could handle the incoming flow.
|
||||
//
|
||||
// If the callback returns intercept=false, the flow is rejected.
|
||||
//
|
||||
// When intercept=true, the behavior depends on whether the returned handler
|
||||
// is non-nil: if nil, the connection is rejected. If non-nil, handler takes
|
||||
// over the TCP conn.
|
||||
type FallbackTCPHandler func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool)
|
||||
|
||||
// Dial connects to the address on the tailnet.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
@@ -515,6 +530,7 @@ func (s *Server) start() (reterr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
sys.Tun.Get().Start()
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = true
|
||||
ns.GetTCPHandlerForFlow = s.getTCPHandlerForFlow
|
||||
@@ -755,6 +771,14 @@ func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16)
|
||||
func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
|
||||
ln, ok := s.listenerForDstAddr("tcp", dst, false)
|
||||
if !ok {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, handler := range s.fallbackTCPHandlers {
|
||||
connHandler, intercept := handler(src, dst)
|
||||
if intercept {
|
||||
return connHandler, intercept
|
||||
}
|
||||
}
|
||||
return nil, true // don't handle, don't forward to localhost
|
||||
}
|
||||
return ln.handle, true
|
||||
@@ -858,6 +882,24 @@ func (s *Server) ListenTLS(network, addr string) (net.Listener, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
// RegisterFallbackTCPHandler registers a callback which will be called
|
||||
// to handle a TCP flow to this tsnet node, for which no listeners will handle.
|
||||
//
|
||||
// If multiple fallback handlers are registered, they will be called in an
|
||||
// undefined order. See FallbackTCPHandler for details on handling a flow.
|
||||
//
|
||||
// The returned function can be used to deregister this callback.
|
||||
func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
hnd := s.fallbackTCPHandlers.Add(cb)
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.fallbackTCPHandlers, hnd)
|
||||
}
|
||||
}
|
||||
|
||||
// getCert is the GetCertificate function used by ListenTLS.
|
||||
//
|
||||
// It calls GetCertificate on the localClient, passing in the ClientHelloInfo.
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -630,3 +631,45 @@ type bufferedConn struct {
|
||||
func (c *bufferedConn) Read(b []byte) (int, error) {
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func TestFallbackTCPHandler(t *testing.T) {
|
||||
tstest.ResourceCheck(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
controlURL := startControl(t)
|
||||
s1, s1ip := startServer(t, ctx, controlURL, "s1")
|
||||
s2, _ := startServer(t, ctx, controlURL, "s2")
|
||||
|
||||
lc2, err := s2.LocalClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ping to make sure the connection is up.
|
||||
res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("ping success: %#+v", res)
|
||||
|
||||
var s1TcpConnCount atomic.Int32
|
||||
deregister := s1.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
|
||||
s1TcpConnCount.Add(1)
|
||||
return nil, false
|
||||
})
|
||||
|
||||
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
|
||||
t.Fatal("Expected dial error because fallback handler did not intercept")
|
||||
}
|
||||
if got := s1TcpConnCount.Load(); got != 1 {
|
||||
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
|
||||
}
|
||||
deregister()
|
||||
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
|
||||
t.Fatal("Expected dial error because nothing would intercept")
|
||||
}
|
||||
if got := s1TcpConnCount.Load(); got != 1 {
|
||||
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,11 @@ func build(outDir string, targets ...string) error {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
for _, target := range targets {
|
||||
outFile := filepath.Join(outDir, path.Base(target)+exe())
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile)
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
}
|
||||
cmd.Args = append(cmd.Args, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
@@ -138,6 +142,16 @@ func findGo() (string, error) {
|
||||
// 2. Look for a go binary in runtime.GOROOT()/bin if runtime.GOROOT() is non-empty.
|
||||
// 3. Look for a go binary in $PATH.
|
||||
|
||||
// For tests we want to run as root on GitHub actions, we run with -exec=sudo,
|
||||
// but that results in this test running with a different PATH and picking the
|
||||
// wrong Go. So hard code the GitHub Actions case.
|
||||
if os.Getuid() == 0 && os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
const sudoGithubGo = "/home/runner/.cache/tailscale-go/bin/go"
|
||||
if _, err := os.Stat(sudoGithubGo); err == nil {
|
||||
return sudoGithubGo, nil
|
||||
}
|
||||
}
|
||||
|
||||
paths := strings.FieldsFunc(os.Getenv("PATH"), func(r rune) bool { return os.IsPathSeparator(uint8(r)) })
|
||||
if len(paths) > 0 {
|
||||
candidate := filepath.Join(paths[0], "go"+exe())
|
||||
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/rands"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,8 +68,36 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// parallel marks t as a parallel test if not root.
|
||||
// In root mode, the tun devices conflict.
|
||||
func parallel(t *testing.T) {
|
||||
if os.Getuid() != 0 {
|
||||
t.Parallel()
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that tailscaled starts up in TUN mode, and also without data races:
|
||||
// https://github.com/tailscale/tailscale/issues/7894
|
||||
func TestTUNMode(t *testing.T) {
|
||||
if os.Getuid() != 0 {
|
||||
t.Skip("skipping when not root; redundant with other tests")
|
||||
}
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon()
|
||||
|
||||
n1.AwaitResponding()
|
||||
n1.MustUp()
|
||||
|
||||
t.Logf("Got IP: %v", n1.AwaitIP4())
|
||||
n1.AwaitRunning()
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
func TestOneNodeUpNoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
@@ -85,7 +114,7 @@ func TestOneNodeUpNoAuth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOneNodeExpiredKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
@@ -121,7 +150,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestControlKnobs(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
@@ -151,7 +180,7 @@ func TestControlKnobs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n := newTestNode(t, env)
|
||||
|
||||
@@ -181,7 +210,7 @@ func TestCollectPanic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestControlTimeLogLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
env.LogCatcher.StoreRawJSON()
|
||||
n := newTestNode(t, env)
|
||||
@@ -204,7 +233,7 @@ func TestControlTimeLogLine(t *testing.T) {
|
||||
|
||||
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
|
||||
func TestStateSavedOnStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
@@ -240,7 +269,7 @@ func TestStateSavedOnStart(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOneNodeUpAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t, configureControl(func(control *testcontrol.Server) {
|
||||
control.RequireAuth = true
|
||||
}))
|
||||
@@ -284,7 +313,7 @@ func TestOneNodeUpAuth(t *testing.T) {
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/3598")
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
|
||||
// Create two nodes:
|
||||
@@ -333,7 +362,7 @@ func TestTwoNodes(t *testing.T) {
|
||||
// PeersRemoved set) saying that the second node disappeared.
|
||||
func TestIncrementalMapUpdatePeersRemoved(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/3598")
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
|
||||
// Create one node:
|
||||
@@ -417,7 +446,7 @@ func TestIncrementalMapUpdatePeersRemoved(t *testing.T) {
|
||||
|
||||
func TestNodeAddressIPFields(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/7008")
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon()
|
||||
@@ -443,7 +472,7 @@ func TestNodeAddressIPFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.StartDaemon()
|
||||
@@ -495,7 +524,7 @@ func TestAddPingRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestC2NPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.StartDaemon()
|
||||
@@ -565,7 +594,7 @@ func TestC2NPingRequest(t *testing.T) {
|
||||
// Issue 2434: when "down" (WantRunning false), tailscaled shouldn't
|
||||
// be connected to control.
|
||||
func TestNoControlConnWhenDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
@@ -606,7 +635,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
|
||||
// Issue 2137: make sure Windows tailscaled works with the CLI alone,
|
||||
// without the GUI to kick off a Start.
|
||||
func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.upFlagGOOS = "windows"
|
||||
@@ -624,7 +653,7 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
// TestNATPing creates two nodes, n1 and n2, sets up masquerades for both and
|
||||
// tries to do bi-directional pings between them.
|
||||
func TestNATPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
for _, v6 := range []bool{false, true} {
|
||||
env := newTestEnv(t)
|
||||
registerNode := func() (*testNode, key.NodePublic) {
|
||||
@@ -751,7 +780,7 @@ func TestNATPing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogoutRemovesAllPeers(t *testing.T) {
|
||||
t.Parallel()
|
||||
parallel(t)
|
||||
env := newTestEnv(t)
|
||||
// Spin up some nodes.
|
||||
nodes := make([]*testNode, 2)
|
||||
@@ -810,6 +839,7 @@ type testEnv struct {
|
||||
t testing.TB
|
||||
cli string
|
||||
daemon string
|
||||
didTun atomic.Bool // made a TUN node
|
||||
|
||||
LogCatcher *LogCatcher
|
||||
LogCatcherServer *httptest.Server
|
||||
@@ -877,7 +907,8 @@ func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
|
||||
// Currently, the test is simplistic and user==node==machine.
|
||||
// That may grow complexity later to test more.
|
||||
type testNode struct {
|
||||
env *testEnv
|
||||
env *testEnv
|
||||
tunMode bool
|
||||
|
||||
dir string // temp dir for sock & state
|
||||
sockFile string
|
||||
@@ -898,12 +929,29 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
|
||||
sockFile = filepath.Join(os.TempDir(), rands.HexString(8)+".sock")
|
||||
t.Cleanup(func() { os.Remove(sockFile) })
|
||||
}
|
||||
return &testNode{
|
||||
n := &testNode{
|
||||
env: env,
|
||||
dir: dir,
|
||||
sockFile: sockFile,
|
||||
stateFile: filepath.Join(dir, "tailscale.state"),
|
||||
|
||||
// The first node per test gets to be in TUN mode when run
|
||||
// as root.
|
||||
tunMode: os.Getuid() == 0 && env.didTun.CompareAndSwap(false, true),
|
||||
}
|
||||
|
||||
// Look for a data race. Once we see the start marker, start logging the rest.
|
||||
var sawRace bool
|
||||
n.addLogLineHook(func(line []byte) {
|
||||
if mem.Contains(mem.B(line), mem.S("WARNING: DATA RACE")) {
|
||||
sawRace = true
|
||||
}
|
||||
if sawRace {
|
||||
t.Logf("%s", line)
|
||||
}
|
||||
})
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *testNode) diskPrefs() *ipn.Prefs {
|
||||
@@ -962,7 +1010,7 @@ func (n *testNode) socks5AddrChan() <-chan string {
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
addr := string(line)[i+len(sub):]
|
||||
addr := strings.TrimSpace(string(line)[i+len(sub):])
|
||||
select {
|
||||
case ch <- addr:
|
||||
default:
|
||||
@@ -1009,11 +1057,10 @@ func (op *nodeOutputParser) parseLines() {
|
||||
}
|
||||
line := buf[:nl+1]
|
||||
buf = buf[nl+1:]
|
||||
lineTrim := bytes.TrimSpace(line)
|
||||
|
||||
n.mu.Lock()
|
||||
for _, f := range n.onLogLine {
|
||||
f(lineTrim)
|
||||
f(line)
|
||||
}
|
||||
n.mu.Unlock()
|
||||
}
|
||||
@@ -1047,8 +1094,8 @@ func (n *testNode) StartDaemon() *Daemon {
|
||||
|
||||
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
t := n.env.t
|
||||
cmd := exec.Command(n.env.daemon,
|
||||
"--tun=userspace-networking",
|
||||
cmd := exec.Command(n.env.daemon)
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
"--socks5-server=localhost:0",
|
||||
@@ -1056,6 +1103,11 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
if *verboseTailscaled {
|
||||
cmd.Args = append(cmd.Args, "-verbose=2")
|
||||
}
|
||||
if !n.tunMode {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"--tun=userspace-networking",
|
||||
)
|
||||
}
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TS_DEBUG_PERMIT_HTTP_C2N=1",
|
||||
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
|
||||
@@ -1065,6 +1117,9 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
"TS_LOGS_DIR="+t.TempDir(),
|
||||
"TS_NETCHECK_GENERATE_204_URL="+n.env.ControlServer.URL+"/generate_204",
|
||||
)
|
||||
if version.IsRace() {
|
||||
cmd.Env = append(cmd.Env, "GORACE=halt_on_error=1")
|
||||
}
|
||||
cmd.Stderr = &nodeOutputParser{n: n}
|
||||
if *verboseTailscaled {
|
||||
cmd.Stdout = os.Stdout
|
||||
@@ -1136,11 +1191,10 @@ func (n *testNode) AwaitListening() {
|
||||
s := safesocket.DefaultConnectionStrategy(n.sockFile)
|
||||
if err := tstest.WaitFor(20*time.Second, func() (err error) {
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
return err
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
c.Close()
|
||||
return nil
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1234,7 +1288,8 @@ func (n *testNode) AwaitNeedsLogin() {
|
||||
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
|
||||
// It does not start the process.
|
||||
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
|
||||
cmd := exec.Command(n.env.cli)
|
||||
cmd.Args = append(cmd.Args, "--socket="+n.sockFile)
|
||||
cmd.Args = append(cmd.Args, arg...)
|
||||
cmd.Dir = n.dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
_ "tailscale.com/derp/derphttp"
|
||||
_ "tailscale.com/envknob"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/conffile"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/ipnserver"
|
||||
_ "tailscale.com/ipn/store"
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
_ "tailscale.com/derp/derphttp"
|
||||
_ "tailscale.com/envknob"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/conffile"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/ipnserver"
|
||||
_ "tailscale.com/ipn/store"
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
_ "tailscale.com/derp/derphttp"
|
||||
_ "tailscale.com/envknob"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/conffile"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/ipnserver"
|
||||
_ "tailscale.com/ipn/store"
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
_ "tailscale.com/derp/derphttp"
|
||||
_ "tailscale.com/envknob"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/conffile"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/ipnserver"
|
||||
_ "tailscale.com/ipn/store"
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
_ "tailscale.com/derp/derphttp"
|
||||
_ "tailscale.com/envknob"
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/conffile"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/ipnserver"
|
||||
_ "tailscale.com/ipn/store"
|
||||
|
||||
@@ -13,8 +13,19 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// ResourceCheck takes a snapshot of the current goroutines and registers a
|
||||
// cleanup on tb to verify that after the rest, all goroutines created by the
|
||||
// test go away. (well, at least that the count matches. Maybe in the future it
|
||||
// can look at specific routines).
|
||||
//
|
||||
// It panics if called from a parallel test.
|
||||
func ResourceCheck(tb testing.TB) {
|
||||
tb.Helper()
|
||||
|
||||
// Set an environment variable (anything at all) just for the
|
||||
// side effect of tb.Setenv panicking if we're in a parallel test.
|
||||
tb.Setenv("TS_CHECKING_RESOURCES", "1")
|
||||
|
||||
startN, startStacks := goroutines()
|
||||
tb.Cleanup(func() {
|
||||
if tb.Failed() {
|
||||
|
||||
@@ -60,6 +60,46 @@ func Sleep(ctx context.Context, d time.Duration) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultClock is a wrapper around a Clock.
|
||||
// It uses StdClock by default if Clock is nil.
|
||||
type DefaultClock struct{ Clock }
|
||||
|
||||
// TODO: We should make the methods of DefaultClock inlineable
|
||||
// so that we can optimize for the common case where c.Clock == nil.
|
||||
|
||||
func (c DefaultClock) Now() time.Time {
|
||||
if c.Clock == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return c.Clock.Now()
|
||||
}
|
||||
func (c DefaultClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) {
|
||||
if c.Clock == nil {
|
||||
t := time.NewTimer(d)
|
||||
return t, t.C
|
||||
}
|
||||
return c.Clock.NewTimer(d)
|
||||
}
|
||||
func (c DefaultClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) {
|
||||
if c.Clock == nil {
|
||||
t := time.NewTicker(d)
|
||||
return t, t.C
|
||||
}
|
||||
return c.Clock.NewTicker(d)
|
||||
}
|
||||
func (c DefaultClock) AfterFunc(d time.Duration, f func()) TimerController {
|
||||
if c.Clock == nil {
|
||||
return time.AfterFunc(d, f)
|
||||
}
|
||||
return c.Clock.AfterFunc(d, f)
|
||||
}
|
||||
func (c DefaultClock) Since(t time.Time) time.Duration {
|
||||
if c.Clock == nil {
|
||||
return time.Since(t)
|
||||
}
|
||||
return c.Clock.Since(t)
|
||||
}
|
||||
|
||||
// Clock offers a subset of the functionality from the std/time package.
|
||||
// Normally, applications will use the StdClock implementation that calls the
|
||||
// appropriate std/time exported funcs. The advantage of using Clock is that
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user