Compare commits
10 Commits
dsnet/tuns
...
buildjet-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1691898a17 | ||
|
|
3d24611e32 | ||
|
|
fb4e23506f | ||
|
|
6d04184325 | ||
|
|
8c72aabbdf | ||
|
|
f7cb535693 | ||
|
|
146f51ce76 | ||
|
|
c66e15772f | ||
|
|
e1bdbfe710 | ||
|
|
acc7baac6d |
8
.github/workflows/cifuzz.yml
vendored
8
.github/workflows/cifuzz.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: CIFuzz
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release-branch/* ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
@@ -7,7 +9,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'tailscale'
|
||||
fuzz-seconds: 300
|
||||
fuzz-seconds: 900
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -27,7 +27,7 @@ concurrency:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/cross-android.yml
vendored
2
.github/workflows/cross-android.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-wasm.yml
vendored
2
.github/workflows/cross-wasm.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
2
.github/workflows/linux-race.yml
vendored
2
.github/workflows/linux-race.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
6
.github/workflows/linux.yml
vendored
6
.github/workflows/linux.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
@@ -38,10 +38,6 @@ jobs:
|
||||
|
||||
- name: Get QEMU
|
||||
run: |
|
||||
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
|
||||
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
|
||||
# use this PPA which brings in a modern qemu.
|
||||
sudo add-apt-repository -y ppa:jacob/virtualisation
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
4
.github/workflows/static-analysis.yml
vendored
4
.github/workflows/static-analysis.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: github-4vcpu-ubuntu-2204
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, windows, darwin]
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-8vcpu
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
|
||||
@@ -110,11 +110,12 @@ func runSpeedtest(ctx context.Context, args []string) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
|
||||
fmt.Println("Results:")
|
||||
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
|
||||
startTime := results[0].IntervalStart
|
||||
for _, r := range results {
|
||||
if r.Total {
|
||||
fmt.Fprintln(w, "-------------------------------------------------------------------------")
|
||||
}
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
|
||||
@@ -100,6 +100,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
|
||||
@@ -114,19 +114,11 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
|
||||
@@ -76,6 +76,8 @@ type Direct struct {
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
@@ -133,6 +135,34 @@ type Options struct {
|
||||
// MapResponse.PingRequest queries from the control plane.
|
||||
// If nil, PingRequest queries are not answered.
|
||||
Pinger Pinger
|
||||
|
||||
// DialPlan contains and stores a previous dial plan that we received
|
||||
// from the control server; if nil, we fall back to using DNS.
|
||||
//
|
||||
// If we receive a new DialPlan from the server, this value will be
|
||||
// updated.
|
||||
DialPlan ControlDialPlanner
|
||||
}
|
||||
|
||||
// ControlDialPlanner is the interface optionally supplied when creating a
|
||||
// control client to control exactly how TCP connections to the control plane
|
||||
// are dialed.
|
||||
//
|
||||
// It is usually implemented by an atomic.Pointer.
|
||||
type ControlDialPlanner interface {
|
||||
// Load returns the current plan for how to connect to control.
|
||||
//
|
||||
// The returned plan can be nil. If so, connections should be made by
|
||||
// resolving the control URL using DNS.
|
||||
Load() *tailcfg.ControlDialPlan
|
||||
|
||||
// Store updates the dial plan with new directions from the control
|
||||
// server.
|
||||
//
|
||||
// The dial plan can span multiple connections to the control server.
|
||||
// That is, a dial plan received when connected over Wi-Fi is still
|
||||
// valid for a subsequent connection over LTE after a network switch.
|
||||
Store(*tailcfg.ControlDialPlan)
|
||||
}
|
||||
|
||||
// Pinger is the LocalBackend.Ping method.
|
||||
@@ -216,6 +246,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -915,6 +946,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
if resp.ControlDialPlan != nil {
|
||||
if c.dialPlan != nil {
|
||||
c.logf("netmap: got new dial plan from control")
|
||||
c.dialPlan.Store(resp.ControlDialPlan)
|
||||
} else {
|
||||
c.logf("netmap: [unexpected] new dial plan; nowhere to store it")
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
@@ -1365,12 +1404,17 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
if nc != nil {
|
||||
return nc, nil
|
||||
}
|
||||
var dp func() *tailcfg.ControlDialPlan
|
||||
if c.dialPlan != nil {
|
||||
dp = c.dialPlan.Load
|
||||
}
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
|
||||
k, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
c.logf("creating new noise client")
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1390,15 +1434,11 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
newReq := *req
|
||||
newReq.Version = tailcfg.CurrentCapabilityVersion
|
||||
np, err := c.getNoiseClient()
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyData, err := json.Marshal(newReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
res, err := nc.post(ctx, "/machine/set-dns", req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1546,6 +1586,38 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportHealthChange reports to the control plane a change to this node's
|
||||
// health.
|
||||
func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
|
||||
if sys == health.SysOverall {
|
||||
// We don't report these. These include things like the network is down
|
||||
// (in which case we can't report anyway) or the user wanted things
|
||||
// stopped, as opposed to the more unexpected failure types in the other
|
||||
// subsystems.
|
||||
return
|
||||
}
|
||||
np, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
// Don't report errors to control if the server doesn't support noise.
|
||||
return
|
||||
}
|
||||
req := &tailcfg.HealthChangeRequest{
|
||||
Subsys: string(sys),
|
||||
}
|
||||
if sysErr != nil {
|
||||
req.Error = sysErr.Error()
|
||||
}
|
||||
|
||||
// Best effort, no logging:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
res, err := np.post(ctx, "/machine/update-health", req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -53,6 +55,11 @@ type noiseClient struct {
|
||||
httpPort string // the default port to call
|
||||
httpsPort string // the fallback Noise-over-https port
|
||||
|
||||
// dialPlan optionally returns a ControlDialPlan previously received
|
||||
// from the control server; either the function or the return value can
|
||||
// be nil.
|
||||
dialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
@@ -61,7 +68,9 @@ type noiseClient struct {
|
||||
|
||||
// newNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
|
||||
//
|
||||
// dialPlan may be nil
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,6 +98,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
dialer: dialer,
|
||||
dialPlan: dialPlan,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
@@ -155,16 +165,51 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
nc.nextID++
|
||||
nc.mu.Unlock()
|
||||
|
||||
// Timeout is a little arbitrary, but plenty long enough for even the
|
||||
// highest latency links.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.dialPlan != nil {
|
||||
dialPlan = nc.dialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := (&controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
@@ -173,6 +218,7 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
ControlKey: nc.serverPubKey,
|
||||
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
|
||||
Dialer: nc.dialer.SystemDial,
|
||||
DialPlan: dialPlan,
|
||||
}).Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -184,3 +230,16 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
mak.Set(&nc.connPool, ncc.id, ncc)
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
func (nc *noiseClient) post(ctx context.Context, path string, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
@@ -28,18 +28,25 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
var stdDialer net.Dialer
|
||||
@@ -82,7 +89,170 @@ func (a *Dialer) httpsFallbackDelay() time.Duration {
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
|
||||
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
|
||||
|
||||
func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// If we don't have a dial plan, just fall back to dialing the single
|
||||
// host we know about.
|
||||
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
|
||||
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
candidates := a.DialPlan.Candidates
|
||||
|
||||
// Otherwise, we try dialing per the plan. Store the highest priority
|
||||
// in the list, so that if we get a connection to one of those
|
||||
// candidates we can return quickly.
|
||||
var highestPriority int = math.MinInt
|
||||
for _, c := range candidates {
|
||||
if c.Priority > highestPriority {
|
||||
highestPriority = c.Priority
|
||||
}
|
||||
}
|
||||
|
||||
// This context allows us to cancel in-flight connections if we get a
|
||||
// highest-priority connection before we're all done.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Now, for each candidate, kick off a dial in parallel.
|
||||
type dialResult struct {
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
addr netip.Addr
|
||||
priority int
|
||||
}
|
||||
resultsCh := make(chan dialResult, len(candidates))
|
||||
|
||||
var pending atomic.Int32
|
||||
pending.Store(int32(len(candidates)))
|
||||
for _, c := range candidates {
|
||||
go func(ctx context.Context, c tailcfg.ControlIPCandidate) {
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
// Always send results back to our channel.
|
||||
defer func() {
|
||||
resultsCh <- dialResult{conn, err, c.IP, c.Priority}
|
||||
if pending.Add(-1) == 0 {
|
||||
close(resultsCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// If non-zero, wait the configured start timeout
|
||||
// before we do anything.
|
||||
if c.DialStartDelaySec > 0 {
|
||||
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
|
||||
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
|
||||
defer tmr.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
case <-tmr.C:
|
||||
}
|
||||
}
|
||||
|
||||
// Now, create a sub-context with the given timeout and
|
||||
// try dialing the provided host.
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.DialTimeoutSec*float64(time.Second)))
|
||||
defer cancel()
|
||||
|
||||
// This will dial, and the defer above sends it back to our parent.
|
||||
a.logf("[v2] controlhttp: trying to dial %q @ %v", a.Hostname, c.IP)
|
||||
conn, err = a.dialHost(ctx, c.IP)
|
||||
}(ctx, c)
|
||||
}
|
||||
|
||||
var results []dialResult
|
||||
for res := range resultsCh {
|
||||
// If we get a response that has the highest priority, we don't
|
||||
// need to wait for any of the other connections to finish; we
|
||||
// can just return this connection.
|
||||
//
|
||||
// TODO(andrew): we could make this better by keeping track of
|
||||
// the highest remaining priority dynamically, instead of just
|
||||
// checking for the highest total
|
||||
if res.priority == highestPriority && res.conn != nil {
|
||||
a.logf("[v1] controlhttp: high-priority success dialing %q @ %v from dial plan", a.Hostname, res.addr)
|
||||
|
||||
// Drain the channel and any existing connections in
|
||||
// the background.
|
||||
go func() {
|
||||
for _, res := range results {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
for res := range resultsCh {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
return res.conn, nil
|
||||
}
|
||||
|
||||
// This isn't a highest-priority result, so just store it until
|
||||
// we're done.
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
// After we finish this function, close any remaining open connections.
|
||||
defer func() {
|
||||
for _, result := range results {
|
||||
// Note: below, we nil out the returned connection (if
|
||||
// any) in the slice so we don't close it.
|
||||
if result.conn != nil {
|
||||
result.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// We don't drain asynchronously after this point, so notify our
|
||||
// channel when we return.
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
|
||||
// Sort by priority, then take the first non-error response.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
// NOTE: intentionally inverted so that the highest priority
|
||||
// item comes first
|
||||
return results[i].priority > results[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
errs []error
|
||||
)
|
||||
for i, result := range results {
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
continue
|
||||
}
|
||||
|
||||
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, result.addr)
|
||||
conn = result.conn
|
||||
results[i].conn = nil // so we don't close it in the defer
|
||||
return conn, nil
|
||||
}
|
||||
merr := multierr.New(errs...)
|
||||
|
||||
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
|
||||
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", merr.Error())
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
|
||||
// dialHost connects to the configured Dialer.Hostname and upgrades the
|
||||
// connection into a controlbase.Conn. If addr is valid, then no DNS is used
|
||||
// and the connection will be made to the provided address.
|
||||
func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
// Create one shared context used by both port 80 and port 443 dials.
|
||||
// If port 80 is still in flight when 443 returns, this deferred cancel
|
||||
// will stop the port 80 dial.
|
||||
@@ -110,7 +280,7 @@ func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
cbConn, err := a.dialURL(ctx, u)
|
||||
cbConn, err := a.dialURL(ctx, u, addr)
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
@@ -161,12 +331,12 @@ func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
}
|
||||
|
||||
// dialURL attempts to connect to the given URL.
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, init)
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, addr, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -178,14 +348,27 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, er
|
||||
return cbConn, nil
|
||||
}
|
||||
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr
|
||||
// is valid, then no DNS is used and the connection will be made to the
|
||||
// provided address.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (net.Conn, error) {
|
||||
var dns *dnscache.Resolver
|
||||
|
||||
// If we were provided an address to dial, then create a resolver that just
|
||||
// returns that value; otherwise, fall back to DNS.
|
||||
if addr.IsValid() {
|
||||
dns = &dnscache.Resolver{
|
||||
SingleHostStaticResult: []netip.Addr{addr},
|
||||
SingleHost: u.Hostname(),
|
||||
}
|
||||
} else {
|
||||
dns = &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
}
|
||||
|
||||
var dialer dnscache.DialContextFunc
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -70,9 +71,15 @@ type Dialer struct {
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// DialPlan, if set, contains instructions from the control server on
|
||||
// how to connect to it. If present, we will try the methods in this
|
||||
// plan before falling back to DNS.
|
||||
DialPlan *tailcfg.ControlDialPlan
|
||||
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// For tests only
|
||||
drainFinished chan struct{}
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
}
|
||||
|
||||
@@ -13,16 +13,21 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type httpTestParam struct {
|
||||
@@ -444,3 +449,263 @@ func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}
|
||||
|
||||
func TestDialPlan(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("only works on Linux due to multiple localhost addresses")
|
||||
}
|
||||
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
const (
|
||||
testProtocolVersion = 1
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort = "40080"
|
||||
httpsPort = "40443"
|
||||
)
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(done)
|
||||
})
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := AcceptHTTP(context.Background(), w, r, server)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
defer conn.Close()
|
||||
}
|
||||
w.Header().Set("X-Handler-Name", name)
|
||||
<-done
|
||||
})
|
||||
if wrap != nil {
|
||||
handler = wrap(handler)
|
||||
}
|
||||
|
||||
httpLn, err := net.Listen("tcp", host.String()+":"+httpPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP listen: %v", err)
|
||||
}
|
||||
httpsLn, err := net.Listen("tcp", host.String()+":"+httpsPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
go httpServer.Serve(httpLn)
|
||||
t.Cleanup(func() {
|
||||
httpServer.Close()
|
||||
})
|
||||
|
||||
httpsServer := &http.Server{
|
||||
Handler: handler,
|
||||
TLSConfig: tlsConfig(t),
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(t.Logf, "http.Server.ErrorLog: ")),
|
||||
}
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
t.Cleanup(func() {
|
||||
httpsServer.Close()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackAddr := netip.MustParseAddr("127.0.0.1")
|
||||
goodAddr := netip.MustParseAddr("127.0.0.2")
|
||||
otherAddr := netip.MustParseAddr("127.0.0.3")
|
||||
other2Addr := netip.MustParseAddr("127.0.0.4")
|
||||
brokenAddr := netip.MustParseAddr("127.0.0.10")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
plan *tailcfg.ControlDialPlan
|
||||
wrap func(http.Handler) http.Handler
|
||||
want netip.Addr
|
||||
|
||||
allowFallback bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "broken-then-good",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials the broken one, which fails, and then
|
||||
// eventually dials the good one and succeeds
|
||||
{IP: brokenAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10, DialStartDelaySec: 1},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-fast-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials some good IPs and our bad one (which
|
||||
// hangs forever), which then hits the fast
|
||||
// path where we bail without waiting.
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-slow-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Our broken address is the highest priority,
|
||||
// so we don't hit our fast path.
|
||||
{IP: brokenAddr, Priority: 10, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 1},
|
||||
}},
|
||||
want: fallbackAddr,
|
||||
allowFallback: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
makeHandler(t, "fallback", fallbackAddr, nil)
|
||||
makeHandler(t, "good", goodAddr, nil)
|
||||
makeHandler(t, "other", otherAddr, nil)
|
||||
makeHandler(t, "other2", other2Addr, nil)
|
||||
makeHandler(t, "broken", brokenAddr, func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(brokenMITMHandler)
|
||||
})
|
||||
|
||||
dialer := closeTrackDialer{
|
||||
t: t,
|
||||
inner: new(tsdial.Dialer).SystemDial,
|
||||
conns: make(map[*closeTrackConn]bool),
|
||||
}
|
||||
defer dialer.Done()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// By default, we intentionally point to something that
|
||||
// we know won't connect, since we want a fallback to
|
||||
// DNS to be an error.
|
||||
host := "example.com"
|
||||
if tt.allowFallback {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
drained := make(chan struct{})
|
||||
a := &Dialer{
|
||||
Hostname: host,
|
||||
HTTPPort: httpPort,
|
||||
HTTPSPort: httpsPort,
|
||||
MachineKey: client,
|
||||
ControlKey: server.Public(),
|
||||
ProtocolVersion: testProtocolVersion,
|
||||
Dialer: dialer.Dial,
|
||||
Logf: t.Logf,
|
||||
DialPlan: tt.plan,
|
||||
proxyFunc: func(*http.Request) (*url.URL, error) { return nil, nil },
|
||||
drainFinished: drained,
|
||||
insecureTLS: true,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
conn, err := a.dial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raddr := conn.RemoteAddr().(*net.TCPAddr)
|
||||
|
||||
got, ok := netip.AddrFromSlice(raddr.IP)
|
||||
if !ok {
|
||||
t.Errorf("invalid remote IP: %v", raddr.IP)
|
||||
} else if got != tt.want {
|
||||
t.Errorf("got connection from %q; want %q", got, tt.want)
|
||||
} else {
|
||||
t.Logf("successfully connected to %q", raddr.String())
|
||||
}
|
||||
|
||||
// Wait until our dialer drains so we can verify that
|
||||
// all connections are closed.
|
||||
<-drained
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type closeTrackDialer struct {
|
||||
t testing.TB
|
||||
inner dnscache.DialContextFunc
|
||||
mu sync.Mutex
|
||||
conns map[*closeTrackConn]bool
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c, err := d.inner(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct := &closeTrackConn{Conn: c, d: d}
|
||||
|
||||
d.mu.Lock()
|
||||
d.conns[ct] = true
|
||||
d.mu.Unlock()
|
||||
return ct, nil
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Done() {
|
||||
// Unfortunately, tsdial.Dialer.SystemDial closes connections
|
||||
// asynchronously in a goroutine, so we can't assume that everything is
|
||||
// closed by the time we get here.
|
||||
//
|
||||
// Sleep/wait a few times on the assumption that things will close
|
||||
// "eventually".
|
||||
const iters = 100
|
||||
for i := 0; i < iters; i++ {
|
||||
d.mu.Lock()
|
||||
if len(d.conns) == 0 {
|
||||
d.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Only error on last iteration
|
||||
if i != iters-1 {
|
||||
d.mu.Unlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
for conn := range d.conns {
|
||||
d.t.Errorf("expected close of conn %p; RemoteAddr=%q", conn, conn.RemoteAddr().String())
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) noteClose(c *closeTrackConn) {
|
||||
d.mu.Lock()
|
||||
delete(d.conns, c) // safe if already deleted
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
type closeTrackConn struct {
|
||||
net.Conn
|
||||
d *closeTrackDialer
|
||||
}
|
||||
|
||||
func (c *closeTrackConn) Close() error {
|
||||
c.d.noteClose(c)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
@@ -189,6 +189,10 @@ type LocalBackend struct {
|
||||
// statusChanged.Broadcast().
|
||||
statusLock sync.Mutex
|
||||
statusChanged *sync.Cond
|
||||
|
||||
// dialPlan is any dial plan that we've received from the control
|
||||
// server during a previous connection; it is cleared on logout.
|
||||
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
|
||||
}
|
||||
|
||||
// clientGen is a func that creates a control plane client.
|
||||
@@ -1087,6 +1091,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
@@ -3112,6 +3117,9 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||
})
|
||||
|
||||
// Clear any previous dial plan(s), if set.
|
||||
b.dialPlan.Store(nil)
|
||||
|
||||
if cc == nil {
|
||||
// Double Logout can happen via repeated IPN
|
||||
// connections to ipnserver making it repeatedly
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Tailscale for Windows dependencies
|
||||
|
||||
The following open source dependencies are used to build the [Tailscale client
|
||||
for windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
[Tailscale client for windows]: https://tailscale.com/kb/1022/install-windows/
|
||||
[Tailscale CLI]: ./tailscale.md
|
||||
|
||||
## Go Packages
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/v1.0.0/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.5/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.5/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.5/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/lxn/walk](https://pkg.go.dev/github.com/lxn/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/ed127cfb919a/LICENSE))
|
||||
- [github.com/lxn/win](https://pkg.go.dev/github.com/lxn/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/c3f813abca9f/LICENSE))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.6.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0: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/+/0de741cf:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.4.10))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/f81723ceac3f/LICENSE))
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
- [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/) ([zlib/libpng](https://nsis.sourceforge.io/License))
|
||||
- [Wintun](https://www.wintun.net/) ([Prebuilt Binaries License](https://git.zx2c4.com/wintun/tree/prebuilt-binaries-license.txt))
|
||||
- [wireguard-windows](https://git.zx2c4.com/wireguard-windows/) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING))
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
const unknown = ipproto.Unknown
|
||||
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
const minFragBlks = (60 + 20) / 8 // max IPv4 header + basic TCP header in fragment blocks (8 bytes each)
|
||||
|
||||
type TCPFlag uint8
|
||||
|
||||
@@ -152,11 +152,12 @@ func (q *Parsed) decode4(b []byte) {
|
||||
// it as Unknown. We can also treat any subsequent fragment that starts
|
||||
// at such a low offset as Unknown.
|
||||
fragFlags := binary.BigEndian.Uint16(b[6:8])
|
||||
moreFrags := (fragFlags & 0x20) != 0
|
||||
moreFrags := (fragFlags & 0x2000) != 0
|
||||
fragOfs := fragFlags & 0x1FFF
|
||||
|
||||
if fragOfs == 0 {
|
||||
// This is the first fragment
|
||||
if moreFrags && len(sub) < minFrag {
|
||||
if moreFrags && len(sub) < minFragBlks {
|
||||
// Suspiciously short first fragment, dump it.
|
||||
q.IPProto = unknown
|
||||
return
|
||||
@@ -216,7 +217,7 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
} else {
|
||||
// This is a fragment other than the first one.
|
||||
if fragOfs < minFrag {
|
||||
if fragOfs < minFragBlks {
|
||||
// First frag was suspiciously short, so we can't
|
||||
// trust the followup either.
|
||||
q.IPProto = unknown
|
||||
|
||||
@@ -37,9 +37,9 @@ func mustIPPort(s string) netip.AddrPort {
|
||||
var icmp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x27, 0xde, 0xad, 0x00, 0x00, 0x40, 0x01, 0x8c, 0x15,
|
||||
// source ip
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// ICMP header
|
||||
0x08, 0x00, 0x7d, 0x22,
|
||||
@@ -61,9 +61,9 @@ var icmp4RequestDecode = Parsed{
|
||||
|
||||
var icmp4ReplyBuffer = []byte{
|
||||
0x45, 0x00, 0x00, 0x25, 0x21, 0x52, 0x00, 0x00, 0x40, 0x01, 0x49, 0x73,
|
||||
// source ip
|
||||
// source IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// ICMP header
|
||||
0x00, 0x00, 0xe6, 0x9e,
|
||||
@@ -119,9 +119,9 @@ var unknownPacketDecode = Parsed{
|
||||
var tcp4PacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x37, 0xde, 0xad, 0x00, 0x00, 0x40, 0x06, 0x49, 0x5f,
|
||||
// source ip
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header with SYN, ACK set
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00,
|
||||
@@ -172,9 +172,9 @@ var tcp6RequestDecode = Parsed{
|
||||
var udp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x2b, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source ip
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// UDP header
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x17, 0x72, 0x1d,
|
||||
@@ -197,9 +197,9 @@ var udp4RequestDecode = Parsed{
|
||||
var invalid4RequestBuffer = []byte{
|
||||
// IP header up to checksum. IHL field points beyond end of packet.
|
||||
0x4a, 0x00, 0x00, 0x14, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source ip
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
}
|
||||
|
||||
@@ -244,9 +244,9 @@ var udp6RequestDecode = Parsed{
|
||||
var udp4ReplyBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
|
||||
// source ip
|
||||
// source IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
// destination IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// UDP header
|
||||
0x02, 0x37, 0x00, 0x7b, 0x00, 0x15, 0xd3, 0x9d,
|
||||
@@ -265,6 +265,59 @@ var udp4ReplyDecode = Parsed{
|
||||
Dst: mustIPPort("5.6.7.8:123"),
|
||||
}
|
||||
|
||||
// First TCP fragment of a packet with leading 24 bytes of 'a's
|
||||
var tcp4MediumFragmentBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x20, 0x00, 0x4c, 0x2c, 0x62, 0x20, 0x00, 0x22, 0x06, 0x3a, 0x0f,
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header
|
||||
0x00, 0x50, 0xf3, 0x8c, 0x58, 0xad, 0x60, 0x94, 0x25, 0xe4, 0x23, 0xa8, 0x80,
|
||||
0x10, 0x01, 0xfd, 0xc6, 0x6e, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0xff, 0x60,
|
||||
0xfb, 0xfe, 0xba, 0x31, 0x78, 0x6a,
|
||||
// data
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
}
|
||||
|
||||
var tcp4MediumFragmentDecode = Parsed{
|
||||
b: tcp4MediumFragmentBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 52,
|
||||
length: len(tcp4MediumFragmentBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: TCP,
|
||||
Src: mustIPPort("1.2.3.4:80"),
|
||||
Dst: mustIPPort("5.6.7.8:62348"),
|
||||
TCPFlags: 0x10,
|
||||
}
|
||||
|
||||
var tcp4ShortFragmentBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x20, 0x00, 0x1e, 0x2c, 0x62, 0x20, 0x00, 0x22, 0x06, 0x3c, 0x4f,
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// partial TCP header
|
||||
0x00, 0x50, 0xf3, 0x8c, 0x58, 0xad, 0x60, 0x94, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var tcp4ShortFragmentDecode = Parsed{
|
||||
b: tcp4ShortFragmentBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 0,
|
||||
length: len(tcp4ShortFragmentBuffer),
|
||||
// short fragments are rejected (marked unknown) to avoid header attacks as described in RFC 1858
|
||||
IPProto: ipproto.Unknown,
|
||||
IPVersion: 4,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var igmpPacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x46, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x02, 0x41, 0x22,
|
||||
@@ -404,6 +457,8 @@ func TestDecode(t *testing.T) {
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
{"ipv4_sctp", sctpBuffer, sctpDecode},
|
||||
{"ipv4_frag", tcp4MediumFragmentBuffer, tcp4MediumFragmentDecode},
|
||||
{"ipv4_fragtooshort", tcp4ShortFragmentBuffer, tcp4ShortFragmentDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = 32000 // size of the block of data to send
|
||||
blockSize = 2 * 1024 * 1024 // size of the block of data to send
|
||||
MinDuration = 5 * time.Second // minimum duration for a test
|
||||
DefaultDuration = MinDuration // default duration for a test
|
||||
MaxDuration = 30 * time.Second // maximum duration for a test
|
||||
version = 1 // value used when comparing client and server versions
|
||||
version = 2 // value used when comparing client and server versions
|
||||
increment = time.Second // increment to display results for, in seconds
|
||||
minInterval = 10 * time.Millisecond // minimum interval length for a result to be included
|
||||
DefaultPort = 20333
|
||||
@@ -37,14 +37,14 @@ type configResponse struct {
|
||||
|
||||
// This represents the Result of a speedtest within a specific interval
|
||||
type Result struct {
|
||||
Bytes int // number of bytes sent/received during the interval
|
||||
IntervalStart time.Duration // duration between the start of the interval and the start of the test
|
||||
IntervalEnd time.Duration // duration between the end of the interval and the start of the test
|
||||
Total bool // if true, this result struct represents the entire test, rather than a segment of the test
|
||||
Bytes int // number of bytes sent/received during the interval
|
||||
IntervalStart time.Time // start of the interval
|
||||
IntervalEnd time.Time // end of the interval
|
||||
Total bool // if true, this result struct represents the entire test, rather than a segment of the test
|
||||
}
|
||||
|
||||
func (r Result) MBitsPerSecond() float64 {
|
||||
return r.MegaBits() / (r.IntervalEnd - r.IntervalStart).Seconds()
|
||||
return r.MegaBits() / r.IntervalEnd.Sub(r.IntervalStart).Seconds()
|
||||
}
|
||||
|
||||
func (r Result) MegaBytes() float64 {
|
||||
@@ -56,7 +56,7 @@ func (r Result) MegaBits() float64 {
|
||||
}
|
||||
|
||||
func (r Result) Interval() time.Duration {
|
||||
return r.IntervalEnd - r.IntervalStart
|
||||
return r.IntervalEnd.Sub(r.IntervalStart)
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
@@ -81,9 +81,6 @@ func doTest(conn net.Conn, conf config) ([]Result, error) {
|
||||
var currentTime time.Time
|
||||
var results []Result
|
||||
|
||||
startTime := time.Now()
|
||||
lastCalculated := startTime
|
||||
|
||||
if conf.Direction == Download {
|
||||
conn.SetReadDeadline(time.Now().Add(conf.TestDuration).Add(5 * time.Second))
|
||||
} else {
|
||||
@@ -94,6 +91,9 @@ func doTest(conn net.Conn, conf config) ([]Result, error) {
|
||||
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
lastCalculated := startTime
|
||||
|
||||
SpeedTestLoop:
|
||||
for {
|
||||
var n int
|
||||
@@ -110,48 +110,37 @@ SpeedTestLoop:
|
||||
return nil, fmt.Errorf("unexpected error has occurred: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Need to change the data a little bit, to avoid any compression.
|
||||
for i := range bufferData {
|
||||
bufferData[i]++
|
||||
}
|
||||
n, err = conn.Write(bufferData)
|
||||
if err != nil {
|
||||
// If the write failed, there is most likely something wrong with the connection.
|
||||
return nil, fmt.Errorf("upload failed: %w", err)
|
||||
}
|
||||
}
|
||||
currentTime = time.Now()
|
||||
intervalBytes += n
|
||||
|
||||
currentTime = time.Now()
|
||||
// checks if the current time is more or equal to the lastCalculated time plus the increment
|
||||
if currentTime.After(lastCalculated.Add(increment)) {
|
||||
intervalStart := lastCalculated.Sub(startTime)
|
||||
intervalEnd := currentTime.Sub(startTime)
|
||||
if (intervalEnd - intervalStart) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: intervalStart, IntervalEnd: intervalEnd, Total: false})
|
||||
}
|
||||
if currentTime.Sub(lastCalculated) >= increment {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: lastCalculated, IntervalEnd: currentTime, Total: false})
|
||||
lastCalculated = currentTime
|
||||
totalBytes += intervalBytes
|
||||
intervalBytes = 0
|
||||
}
|
||||
|
||||
if conf.Direction == Upload && time.Since(startTime) > conf.TestDuration {
|
||||
if conf.Direction == Upload && currentTime.Sub(startTime) > conf.TestDuration {
|
||||
break SpeedTestLoop
|
||||
}
|
||||
}
|
||||
|
||||
// get last segment
|
||||
intervalStart := lastCalculated.Sub(startTime)
|
||||
intervalEnd := currentTime.Sub(startTime)
|
||||
if (intervalEnd - intervalStart) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: intervalStart, IntervalEnd: intervalEnd, Total: false})
|
||||
if currentTime.Sub(lastCalculated) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: lastCalculated, IntervalEnd: currentTime, Total: false})
|
||||
}
|
||||
|
||||
// get total
|
||||
totalBytes += intervalBytes
|
||||
intervalEnd = currentTime.Sub(startTime)
|
||||
if intervalEnd > minInterval {
|
||||
results = append(results, Result{Bytes: totalBytes, IntervalStart: 0, IntervalEnd: intervalEnd, Total: true})
|
||||
if currentTime.Sub(startTime) > minInterval {
|
||||
results = append(results, Result{Bytes: totalBytes, IntervalStart: startTime, IntervalEnd: currentTime, Total: true})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -7,6 +7,7 @@ package speedtest
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
@@ -23,9 +24,9 @@ func TestDownload(t *testing.T) {
|
||||
type state struct {
|
||||
err error
|
||||
}
|
||||
displayResult := func(t *testing.T, r Result) {
|
||||
displayResult := func(t *testing.T, r Result, start time.Time) {
|
||||
t.Helper()
|
||||
t.Logf("{ Megabytes: %.2f, Start: %.1f, End: %.1f, Total: %t }", r.MegaBytes(), r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.Total)
|
||||
t.Logf("{ Megabytes: %.2f, Start: %.1f, End: %.1f, Total: %t }", r.MegaBytes(), r.IntervalStart.Sub(start).Seconds(), r.IntervalEnd.Sub(start).Seconds(), r.Total)
|
||||
}
|
||||
stateChan := make(chan state, 1)
|
||||
|
||||
@@ -49,8 +50,9 @@ func TestDownload(t *testing.T) {
|
||||
t.Fatalf("download results: expected length: %d, actual length: %d", expectedLen, len(results))
|
||||
}
|
||||
|
||||
start := results[0].IntervalStart
|
||||
for _, result := range results {
|
||||
displayResult(t, result)
|
||||
displayResult(t, result, start)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,8 +68,9 @@ func TestDownload(t *testing.T) {
|
||||
t.Fatalf("upload results: expected length: %d, actual length: %d", expectedLen, len(results))
|
||||
}
|
||||
|
||||
start := results[0].IntervalStart
|
||||
for _, result := range results {
|
||||
displayResult(t, result)
|
||||
displayResult(t, result, start)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@ type CapabilityVersion int
|
||||
// - 41: 2022-08-30: uses 100.100.100.100 for route-less ExtraRecords if global nameservers is set
|
||||
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
|
||||
// - 43: 2022-09-21: clients can return usernames for SSH
|
||||
const CurrentCapabilityVersion CapabilityVersion = 43
|
||||
// - 44: 2022-09-22: MapResponse.ControlDialPlan
|
||||
const CurrentCapabilityVersion CapabilityVersion = 44
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -235,6 +236,9 @@ type Node struct {
|
||||
ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
|
||||
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
|
||||
ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
|
||||
|
||||
// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging.
|
||||
DataPlaneAuditLogID string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// DisplayName returns the user-facing name for a node which should
|
||||
@@ -1373,9 +1377,47 @@ type MapResponse struct {
|
||||
// indicates no change from the value sent earlier.
|
||||
TKAInfo *TKAInfo `json:",omitempty"`
|
||||
|
||||
// DomainDataPlaneAuditLogID, if non-empty, is the per-tailnet log ID to be
|
||||
// used when writing data plane audit logs.
|
||||
DomainDataPlaneAuditLogID string `json:",omitempty"`
|
||||
|
||||
// Debug is normally nil, except for when the control server
|
||||
// is setting debug settings on a node.
|
||||
Debug *Debug `json:",omitempty"`
|
||||
|
||||
// ControlDialPlan tells the client how to connect to the control
|
||||
// server. An initial nil is equivalent to new(ControlDialPlan).
|
||||
// A subsequent streamed nil means no change.
|
||||
ControlDialPlan *ControlDialPlan `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ControlDialPlan is instructions from the control server to the client on how
|
||||
// to connect to the control server; this is useful for maintaining connection
|
||||
// if the client's network state changes after the initial connection, or due
|
||||
// to the configuration that the control server pushes.
|
||||
type ControlDialPlan struct {
|
||||
// An empty list means the default: use DNS (unspecified which DNS).
|
||||
Candidates []ControlIPCandidate
|
||||
}
|
||||
|
||||
// ControlIPCandidate represents a single candidate address to use when
|
||||
// connecting to the control server.
|
||||
type ControlIPCandidate struct {
|
||||
// IP is the address to attempt connecting to.
|
||||
IP netip.Addr
|
||||
|
||||
// DialStartSec is the number of seconds after the beginning of the
|
||||
// connection process to wait before trying this candidate.
|
||||
DialStartDelaySec float64 `json:",omitempty"`
|
||||
|
||||
// DialTimeoutSec is the timeout for a connection to this candidate,
|
||||
// starting after DialStartDelaySec.
|
||||
DialTimeoutSec float64 `json:",omitempty"`
|
||||
|
||||
// Priority is the relative priority of this candidate; candidates with
|
||||
// a higher priority are preferred over candidates with a lower
|
||||
// priority.
|
||||
Priority int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Debug are instructions from the control server to the client
|
||||
@@ -1642,6 +1684,13 @@ type SetDNSRequest struct {
|
||||
// SetDNSResponse is the response to a SetDNSRequest.
|
||||
type SetDNSResponse struct{}
|
||||
|
||||
// HealthChangeRequest is the JSON request body type used to report
|
||||
// node health changes to https://<control>/machine/<mkey hex>/update-health.
|
||||
type HealthChangeRequest struct {
|
||||
Subsys string // a health.Subsystem value in string form
|
||||
Error string // or empty if cleared
|
||||
}
|
||||
|
||||
// SSHPolicy is the policy for how to handle incoming SSH connections
|
||||
// over Tailscale.
|
||||
type SSHPolicy struct {
|
||||
|
||||
@@ -95,6 +95,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
DataPlaneAuditLogID string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Hostinfo.
|
||||
|
||||
@@ -332,6 +332,7 @@ func TestNodeEqual(t *testing.T) {
|
||||
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
"DataPlaneAuditLogID",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
||||
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
|
||||
@@ -173,6 +173,7 @@ func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthor
|
||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
|
||||
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
|
||||
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
|
||||
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
@@ -203,6 +204,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
DataPlaneAuditLogID string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Hostinfo.
|
||||
|
||||
@@ -185,9 +185,9 @@ type ReturnHandler interface {
|
||||
}
|
||||
|
||||
type HandlerOptions struct {
|
||||
Quiet200s bool // if set, do not log successfully handled HTTP requests
|
||||
Logf logger.Logf
|
||||
Now func() time.Time // if nil, defaults to time.Now
|
||||
QuietLoggingIfSuccessful bool // if set, do not log successfully handled HTTP requests (200 and 304 status codes)
|
||||
Logf logger.Logf
|
||||
Now func() time.Time // if nil, defaults to time.Now
|
||||
|
||||
// If non-nil, StatusCodeCounters maintains counters
|
||||
// of status codes for handled responses.
|
||||
@@ -317,7 +317,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Code != 200 || !h.opts.Quiet200s {
|
||||
if !h.opts.QuietLoggingIfSuccessful || (msg.Code != http.StatusOK && msg.Code != http.StatusNotModified) {
|
||||
h.opts.Logf("%s", msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ func BenchmarkLogNot200(b *testing.B) {
|
||||
// Implicit 200 OK.
|
||||
return nil
|
||||
})
|
||||
h := StdHandler(rh, HandlerOptions{Quiet200s: true})
|
||||
h := StdHandler(rh, HandlerOptions{QuietLoggingIfSuccessful: true})
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rw := new(httptest.ResponseRecorder)
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -575,7 +575,7 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
{
|
||||
name: "tailcfg.Node",
|
||||
val: &tailcfg.Node{},
|
||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user