Compare commits

..

1 Commits

Author SHA1 Message Date
Tom DNetto
ea6c4d4fe1 cmd/derper,derp: implement per-client rate limits
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-16 09:53:36 -07:00
173 changed files with 1097 additions and 4967 deletions

View File

@@ -2,7 +2,7 @@ name: CIFuzz
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:

View File

@@ -1,54 +0,0 @@
name: Android-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Android smoke build
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
# some Android breakages early.
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
env:
GOOS: android
GOARCH: arm64
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -11,7 +11,7 @@ concurrency:
jobs:
ubuntu2004-LTS-cloud-base:
runs-on: ubuntu-22.04
runs-on: [ self-hosted, linux, vm ]
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -32,7 +32,7 @@ jobs:
env:
HOME: "/tmp"
TMPDIR: "/tmp"
XDG_CACHE_HOME: "$HOME/.cache"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -32,7 +32,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.19-alpine AS build-env
FROM golang:1.18-alpine AS build-env
WORKDIR /go/src/tailscale

View File

@@ -9,6 +9,7 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -17,7 +18,7 @@ import (
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}

View File

@@ -24,17 +24,11 @@ func New(socket string) (*BIRDClient, error) {
return newWithTimeout(socket, responseTimeout)
}
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
}
defer func() {
if err != nil {
conn.Close()
}
}()
b := &BIRDClient{
socket: socket,
conn: conn,

View File

@@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
@@ -136,7 +137,7 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
onVersionMismatch(ipn.IPCVersion(), server)
}
if res.StatusCode == 403 {
all, _ := io.ReadAll(res.Body)
all, _ := ioutil.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
@@ -206,7 +207,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
return nil, err
}
defer res.Body.Close()
slurp, err := io.ReadAll(res.Body)
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
@@ -267,47 +268,15 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
}
// BugReportOpts contains options to pass to the Tailscale daemon when
// generating a bug report.
type BugReportOpts struct {
// Note contains an optional user-provided note to add to the logs.
Note string
// Diagnose specifies whether to print additional diagnostic information to
// the logs when generating this bugreport.
Diagnose bool
}
// BugReportWithOpts logs and returns a log marker that can be shared by the
// user with support.
//
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
var qparams url.Values
if opts.Note != "" {
qparams.Set("note", opts.Note)
}
if opts.Diagnose {
qparams.Set("diagnose", "true")
}
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
body, err := lc.send(ctx, "POST", uri, 200, nil)
// BugReport logs and returns a log marker that can be shared by the user with support.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// BugReport logs and returns a log marker that can be shared by the user with support.
//
// This is the same as calling BugReportWithOpts and only specifying the Note
// field.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
@@ -396,7 +365,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}

View File

@@ -17,6 +17,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
)
@@ -130,7 +131,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
// Read response. Limit the response to 10MB.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := io.ReadAll(body)
b, err := ioutil.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}

View File

@@ -51,7 +51,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter

View File

@@ -14,6 +14,7 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
@@ -56,6 +57,11 @@ var (
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
egressInterface = flag.String("egress-interface", "", "the interface to monitor for automatic ratelimit tuning")
egressDataLimit = flag.Int("egress-data-limit", 100*1024*1024/8, "the bandwidth in bytes/s the server will try to stay under, only applies if egress-interface is set")
clientDataMin = flag.Int("client-data-min-limit", 1024*1024/8, "minimum bandwidth in bytes/s for a single client, only applies if egress-interface is set")
clientDataBurst = flag.Int("client-data-burst", 3*1024*1024, "burst limit in bytes for forwarded data from a single client, only applies if egress-interface is set")
)
var (
@@ -98,7 +104,7 @@ func loadConfig() config {
}
log.Printf("no config path specified; using %s", *configPath)
}
b, err := os.ReadFile(*configPath)
b, err := ioutil.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
return writeNewConfig()
@@ -153,8 +159,14 @@ func main() {
s := derp.NewServer(cfg.PrivateKey, log.Printf)
s.SetVerifyClient(*verifyClients)
if *egressInterface != "" && *egressDataLimit > 0 {
if err := s.StartEgressRateLimiter(*egressInterface, *egressDataLimit, *clientDataMin, *clientDataBurst); err != nil {
log.Fatalf("failed to start egress rate limiter: %v", err)
}
}
if *meshPSKFile != "" {
b, err := os.ReadFile(*meshPSKFile)
b, err := ioutil.ReadFile(*meshPSKFile)
if err != nil {
log.Fatal(err)
}

View File

@@ -33,12 +33,6 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because we transmit WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Printf("websocket.Accept: %v", err)

View File

@@ -13,6 +13,7 @@ import (
"errors"
"flag"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
@@ -105,7 +106,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := os.ReadFile("hello.tmpl.html")
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
if os.IsNotExist(err) {
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
return tmpl, nil

View File

@@ -110,12 +110,11 @@ 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.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
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())
}
w.Flush()
return nil

View File

@@ -7,10 +7,8 @@ package cli
import (
"context"
"errors"
"flag"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
@@ -18,15 +16,6 @@ var bugReportCmd = &ffcli.Command{
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
return fs
})(),
}
var bugReportArgs struct {
diagnose bool
}
func runBugReport(ctx context.Context, args []string) error {
@@ -38,10 +27,7 @@ func runBugReport(ctx context.Context, args []string) error {
default:
return errors.New("unknown argumets")
}
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
Note: note,
Diagnose: bugReportArgs.diagnose,
})
logMarker, err := localClient.BugReport(ctx, note)
if err != nil {
return err
}

View File

@@ -789,10 +789,6 @@ func TestUpdatePrefs(t *testing.T) {
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
// It is used to test the --accept-risks flag.
sshOverTailscale bool
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
// updatePrefs might've mutated them (from applyImplicitPrefs).
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
@@ -920,159 +916,15 @@ func TestUpdatePrefs(t *testing.T) {
}
},
},
{
name: "enable_ssh",
flags: []string{"--ssh"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh",
flags: []string{"--ssh=false"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
runSSH: true,
}},
},
{
name: "disable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=false"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
RunSSH: true,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=true"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh",
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh_over_ssh",
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.sshOverTailscale {
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
}
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
flags := CleanUpArgs(tt.flags)
if err := tt.env.flagSet.Parse(flags); err != nil {
t.Fatal(err)
}
tt.env.flagSet.Parse(flags)
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
@@ -1087,8 +939,6 @@ func TestUpdatePrefs(t *testing.T) {
return
}
t.Fatal(err)
} else if tt.wantErrSubtr != "" {
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
}
if tt.checkUpdatePrefsMutations != nil {
tt.checkUpdatePrefsMutations(t, newPrefs)
@@ -1102,18 +952,13 @@ func TestUpdatePrefs(t *testing.T) {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
}
})
}
}
func asJSON(v any) string {
b, _ := json.MarshalIndent(v, "", "\t")
return string(b)
}
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
return a == b
})

View File

@@ -48,11 +48,11 @@ func runConfigureHost(ctx context.Context, args []string) error {
if uid := os.Getuid(); uid != 0 {
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
}
hi := hostinfo.New()
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
osVer := hostinfo.GetOSVersion()
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
if !isDSM6 && !isDSM7 {
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
return fmt.Errorf("unsupported DSM version %q", osVer)
}
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if err := os.MkdirAll("/dev/net", 0755); err != nil {

View File

@@ -489,15 +489,7 @@ func runTS2021(ctx context.Context, args []string) error {
return c, err
}
conn, err := (&controlhttp.Dialer{
Hostname: ts2021Args.host,
HTTPPort: "80",
HTTPSPort: "443",
MachineKey: machinePrivate,
ControlKey: keys.PublicKey,
ProtocolVersion: uint16(ts2021Args.version),
Dialer: dialFunc,
}).Dial(ctx)
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
log.Printf("controlhttp.Dial = %p, %v", conn, err)
if err != nil {
return err

View File

@@ -22,13 +22,9 @@ var downCmd = &ffcli.Command{
FlagSet: newDownFlagSet(),
}
var downArgs struct {
acceptedRisks string
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
registerAcceptRiskFlag(downf)
return downf
}
@@ -38,7 +34,7 @@ func runDown(ctx context.Context, args []string) error {
}
if isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
return err
}
}

View File

@@ -10,6 +10,7 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"sort"
@@ -133,9 +134,6 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
printf("\t* HairPinning: %v\n", report.HairPinning)
printf("\t* PortMapping: %v\n", portMapping(report))
if report.CaptivePortal != "" {
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
}
// When DERP latency checking failed,
// magicsock will try to pick the DERP server that
@@ -204,7 +202,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}

View File

@@ -16,8 +16,9 @@ import (
)
var (
riskTypes []string
riskLoseSSH = registerRiskType("lose-ssh")
riskTypes []string
acceptedRisks string
riskLoseSSH = registerRiskType("lose-ssh")
)
func registerRiskType(riskType string) string {
@@ -27,13 +28,12 @@ func registerRiskType(riskType string) string {
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
// in presentRiskToUser.
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
func registerAcceptRiskFlag(f *flag.FlagSet) {
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
}
// isRiskAccepted reports whether riskType is in the comma-separated list of
// risks in acceptedRisks.
func isRiskAccepted(riskType, acceptedRisks string) bool {
// riskAccepted reports whether riskType is in acceptedRisks.
func riskAccepted(riskType string) bool {
for _, r := range strings.Split(acceptedRisks, ",") {
if r == riskType {
return true
@@ -49,16 +49,12 @@ var errAborted = errors.New("aborted, no changes made")
// It is used by the presentRiskToUser function below.
const riskAbortTimeSeconds = 5
// presentRiskToUser displays the risk message and waits for the user to cancel.
// It returns errorAborted if the user aborts. In tests it returns errAborted
// immediately unless the risk has been explicitly accepted.
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
if isRiskAccepted(riskType, acceptedRisks) {
// presentRiskToUser displays the risk message and waits for the user to
// cancel. It returns errorAborted if the user aborts.
func presentRiskToUser(riskType, riskMessage string) error {
if riskAccepted(riskType) {
return nil
}
if inTest() {
return errAborted
}
outln(riskMessage)
printf("To skip this warning, use --accept-risk=%s\n", riskType)

View File

@@ -116,7 +116,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
registerAcceptRiskFlag(upf)
return upf
}
@@ -150,7 +150,6 @@ type upArgsT struct {
opUser string
json bool
timeout time.Duration
acceptedRisks string
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -377,20 +376,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
}
// Do this after validations to avoid the 5s delay if we're going to error
// out anyway.
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
if wantSSH != haveSSH && isSSHOverTailscale() {
if wantSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
}
if err != nil {
return false, nil, err
}
}
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
simpleUp = env.flagSet.NFlag() == 0 &&
@@ -490,6 +475,17 @@ func runUp(ctx context.Context, args []string) (retErr error) {
curExitNodeIP: exitNodeIP(curPrefs, st),
}
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
if upArgs.runSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
}
if err != nil {
return err
}
}
defer func() {
if retErr == nil {
checkSSHUpWarnings(ctx)

View File

@@ -15,6 +15,7 @@ import (
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -253,7 +254,7 @@ func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
return "", nil, err
}
defer resp.Body.Close()
out, err := io.ReadAll(resp.Body)
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}

View File

@@ -100,7 +100,6 @@ 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+

View File

@@ -15,6 +15,7 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -172,7 +173,7 @@ func checkDerp(ctx context.Context, derpRegion string) error {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}

View File

@@ -109,7 +109,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
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
@@ -190,8 +190,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/hostinfo from tailscale.com/control/controlclient+
@@ -232,7 +230,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
@@ -275,7 +272,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
LW tailscale.com/util/endian from tailscale.com/net/dns+
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/lineread from tailscale.com/hostinfo+

View File

@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -141,7 +142,7 @@ func installSystemDaemonDarwin(args []string) (err error) {
return err
}
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}

View File

@@ -26,7 +26,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
@@ -98,20 +97,6 @@ func defaultTunName() string {
return "tailscale0"
}
// defaultPort returns the default UDP port to listen on for disco+wireguard.
// By default it returns 0, to pick one randomly from the kernel.
// If the environment variable PORT is set, that's used instead.
// The PORT environment variable is chosen to match what the Linux systemd
// unit uses, to make documentation more consistent.
func defaultPort() uint16 {
if s := envknob.String("PORT"); s != "" {
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
return uint16(p)
}
}
return 0
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
@@ -147,9 +132,6 @@ var subCommands = map[string]*func([]string) error{
var beCLI func() // non-nil if CLI is linked in
func main() {
envknob.PanicIfAnyEnvCheckedInInit()
envknob.ApplyDiskConfig()
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
@@ -157,7 +139,7 @@ func main() {
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
@@ -326,10 +308,6 @@ func run() error {
pol.Shutdown(ctx)
}()
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
@@ -398,7 +376,7 @@ func run() error {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
ns.ProcessSubnets = useNetstack || wrapNetstack
if useNetstack {
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
@@ -499,6 +477,8 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
return nil, false, multierr.New(errs...)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
return v
@@ -569,7 +549,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
}
conf.DNS = d
conf.Router = r
if shouldWrapNetstack() {
if wrapNetstack {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}

View File

@@ -7,7 +7,7 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStartPre=/usr/sbin/tailscaled --cleanup
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup
Restart=on-failure

View File

@@ -197,9 +197,6 @@ func beWindowsSubprocess() bool {
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
go func() {
b := make([]byte, 16)
@@ -277,7 +274,7 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, nil, fmt.Errorf("router: %w", err)
}
if shouldWrapNetstack() {
if wrapNetstack {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
@@ -304,7 +301,7 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = shouldWrapNetstack()
ns.ProcessSubnets = wrapNetstack
if err := ns.Start(); err != nil {
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
}

View File

@@ -7,6 +7,7 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path"
@@ -46,7 +47,7 @@ func runBuild() {
if err != nil {
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
}
if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
log.Fatalf("Cannot write metadata: %v", err)
}

View File

@@ -6,6 +6,7 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
@@ -182,7 +183,7 @@ func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
func buildWasm(dev bool) ([]byte, error) {
start := time.Now()
outputFile, err := os.CreateTemp("", "main.*.wasm")
outputFile, err := ioutil.TempFile("", "main.*.wasm")
if err != nil {
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
}

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
@@ -74,7 +75,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
}
defer esbuildMetadataFile.Close()
esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
if err != nil {
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
}

View File

@@ -46,7 +46,7 @@ function SSHSession({
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
runSSHSession(ref.current, def, ipn, onDone, (err) => console.error(err))
runSSHSession(ref.current, def, ipn, onDone)
}
}, [ref])

View File

@@ -5,8 +5,6 @@ import { WebLinksAddon } from "xterm-addon-web-links"
export type SSHSessionDef = {
username: string
hostname: string
/** Defaults to 5 seconds */
timeoutSeconds?: number
}
export function runSSHSession(
@@ -14,7 +12,6 @@ export function runSSHSession(
def: SSHSessionDef,
ipn: IPN,
onDone: () => void,
onError?: (err: string) => void,
terminalOptions?: ITerminalOptions
) {
const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
@@ -49,7 +46,7 @@ export function runSSHSession(
term.write(input)
},
writeErrorFn(err) {
onError?.(err)
console.error(err)
term.write(err)
},
setReadFn(hook) {
@@ -65,7 +62,6 @@ export function runSSHSession(
}
onDone()
},
timeoutSeconds: def.timeoutSeconds,
})
// Make terminal and SSH session track the size of the containing DOM node.

View File

@@ -23,8 +23,6 @@ declare global {
setReadFn: (readFn: (data: string) => void) => void
rows: number
cols: number
/** Defaults to 5 seconds */
timeoutSeconds?: number
onDone: () => void
}
): IPNSSHSession

View File

@@ -360,10 +360,6 @@ func (s *jsSSHSession) Run() {
setReadFn := s.termConfig.Get("setReadFn")
rows := s.termConfig.Get("rows").Int()
cols := s.termConfig.Get("cols").Int()
timeoutSeconds := 5.0
if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
timeoutSeconds = jsTimeoutSeconds.Float()
}
onDone := s.termConfig.Get("onDone")
defer onDone.Invoke()
@@ -371,7 +367,7 @@ func (s *jsSSHSession) Run() {
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
if err != nil {

View File

@@ -114,11 +114,19 @@ 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(direct.ReportHealthChange)
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
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.

View File

@@ -8,11 +8,12 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"log"
"net/http"
"runtime"
"strconv"
"time"
"tailscale.com/util/goroutines"
)
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
@@ -21,7 +22,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
zbuf := new(bytes.Buffer)
zw := gzip.NewWriter(zbuf)
zw.Write(goroutines.ScrubbedGoroutineDump())
zw.Write(scrubbedGoroutineDump())
zw.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
@@ -39,3 +40,83 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
}
}
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func scrubbedGoroutineDump() []byte {
var buf []byte
// Grab stacks multiple times into increasingly larger buffer sizes
// to minimize the risk that we blow past our iOS memory limit.
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
// It fit.
break
}
}
return scrubHex(buf)
}
func scrubHex(buf []byte) []byte {
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
foreachHexAddress(buf, func(in []byte) {
if string(in) == "0x0" {
return
}
if v, ok := saw[string(in)]; ok {
for i := range in {
in[i] = '_'
}
copy(in, v)
return
}
inStr := string(in)
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
for i := range in {
in[i] = '_'
}
if err != nil {
in[0] = '?'
return
}
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
saw[inStr] = v
copy(in, v)
})
return buf
}
var ohx = []byte("0x")
// foreachHexAddress calls f with each subslice of b that matches
// regexp `0x[0-9a-f]*`.
func foreachHexAddress(b []byte, f func([]byte)) {
for len(b) > 0 {
i := bytes.Index(b, ohx)
if i == -1 {
return
}
b = b[i:]
hx := hexPrefix(b)
f(hx)
b = b[len(hx):]
}
}
func hexPrefix(b []byte) []byte {
for i, c := range b {
if i < 2 {
continue
}
if !isHexByte(c) {
return b[:i]
}
}
return b
}
func isHexByte(b byte) bool {
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}

View File

@@ -2,12 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package goroutines
package controlclient
import "testing"
func TestScrubbedGoroutineDump(t *testing.T) {
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump())
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
}
func TestScrubHex(t *testing.T) {

View File

@@ -14,6 +14,7 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@@ -76,8 +77,6 @@ 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
@@ -108,7 +107,6 @@ type Options struct {
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor
PopBrowserURL func(url string) // optional func to open browser
@@ -135,34 +133,6 @@ 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.
@@ -246,7 +216,6 @@ 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())
@@ -258,12 +227,6 @@ func NewDirect(opts Options) (*Direct, error) {
c.SetNetInfo(ni)
}
}
if opts.NoiseTestClient != nil {
c.noiseClient = &noiseClient{
Client: opts.NoiseTestClient,
}
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
}
return c, nil
}
@@ -527,7 +490,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister() {
if debugRegister {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
@@ -560,7 +523,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
return regen, opt.URL, fmt.Errorf("register request: %w", err)
}
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -570,7 +533,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if debugRegister() {
if debugRegister {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
@@ -752,7 +715,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
vlogf := logger.Discard
if DevKnob.DumpNetMaps() {
if DevKnob.DumpNetMaps {
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
// want to upload it always.
vlogf = c.logf
@@ -841,7 +804,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
}
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -851,7 +814,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
health.NoteMapRequestHeard(request)
if cb == nil {
io.Copy(io.Discard, res.Body)
io.Copy(ioutil.Discard, res.Body)
return nil
}
@@ -946,14 +909,6 @@ 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{}{}:
@@ -1008,12 +963,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
controlTrimWGConfig.Store(d.TrimWGConfig)
}
if DevKnob.StripEndpoints() {
if DevKnob.StripEndpoints {
for _, p := range resp.Peers {
p.Endpoints = nil
}
}
if DevKnob.StripCaps() {
if DevKnob.StripCaps {
nm.SelfNode.Capabilities = nil
}
@@ -1043,7 +998,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
defer res.Body.Close()
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return err
}
@@ -1057,8 +1012,8 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
}
var (
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
debugMap = envknob.Bool("TS_DEBUG_MAP")
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
)
var jsonEscapedZero = []byte(`\u0000`)
@@ -1096,7 +1051,7 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
return err
}
}
if debugMap() {
if debugMap {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
@@ -1133,7 +1088,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
if err != nil {
return nil, err
}
if debugMap() {
if debugMap {
if _, ok := v.(*tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -1155,7 +1110,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
return nil, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 64<<10))
if err != nil {
return nil, fmt.Errorf("fetch control key response: %v", err)
}
@@ -1184,18 +1139,18 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
var DevKnob = initDevKnob()
type devKnobs struct {
DumpNetMaps func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
StripCaps func() bool // strip all local node's control-provided capabilities
DumpNetMaps bool
ForceProxyDNS bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
}
func initDevKnob() devKnobs {
return devKnobs{
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
DumpNetMaps: envknob.Bool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
}
}
@@ -1404,17 +1359,12 @@ 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
}
c.logf("creating new noise client")
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
if err != nil {
return nil, err
}
@@ -1434,17 +1384,21 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) error {
newReq := *req
newReq.Version = tailcfg.CurrentCapabilityVersion
nc, err := c.getNoiseClient()
np, err := c.getNoiseClient()
if err != nil {
return err
}
res, err := nc.post(ctx, "/machine/set-dns", &newReq)
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))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
msg, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse
@@ -1510,7 +1464,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
msg, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse
@@ -1586,38 +1540,6 @@ 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")

View File

@@ -48,7 +48,6 @@ type mapSession struct {
lastHealth []string
lastPopBrowserURL string
stickyDebug tailcfg.Debug // accumulated opt.Bool values
lastTKAInfo *tailcfg.TKAInfo
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.
@@ -116,9 +115,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.Health != nil {
ms.lastHealth = resp.Health
}
if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo
}
debug := resp.Debug
if debug != nil {
@@ -156,17 +152,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
}
ms.netMapBuilding = nm
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
ms.logf("error unmarshalling TKAHead: %v", err)
nm.TKAEnabled = false
}
}
if resp.Node != nil {
ms.lastNode = resp.Node
}
@@ -202,7 +190,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
ms.addUserProfile(peer.User)
}
if DevKnob.ForceProxyDNS() {
if DevKnob.ForceProxyDNS {
nm.DNS.Proxied = true
}
ms.netMapBuilding = nil
@@ -368,13 +356,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
return v2
}
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
switch {
default:
return in
case debugSelfIPv6Only():
case debugSelfIPv6Only:
for _, a := range in {
if a.Addr().Is6() {
ret = append(ret, a)

View File

@@ -5,10 +5,8 @@
package controlclient
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"math"
"net"
"net/http"
@@ -55,11 +53,6 @@ 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
@@ -68,9 +61,7 @@ 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).
//
// dialPlan may be nil
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
@@ -98,7 +89,6 @@ 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
@@ -165,61 +155,17 @@ 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,
HTTPSPort: nc.httpsPort,
MachineKey: nc.privKey,
ControlKey: nc.serverPubKey,
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
DialPlan: dialPlan,
}).Dial(ctx)
conn, err := controlhttp.Dial(ctx, nc.host, nc.httpPort, nc.httpsPort, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
if err != nil {
return nil, err
}
@@ -230,16 +176,3 @@ 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)
}

View File

@@ -28,231 +28,69 @@ 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"
"tailscale.com/types/key"
)
var stdDialer net.Dialer
// Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
// switch to the Tailscale control protocol, and returns an established control
// Dial connects to the HTTP server at host:httpPort, requests to switch to the
// Tailscale control protocol, and returns an established control
// protocol connection.
//
// If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
// Dialer's Host:HTTPSPort as a compatibility fallback.
// If Dial fails to connect using addr, it also tries to tunnel over
// TLS to host:httpsPort as a compatibility fallback.
//
// The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established.
func (a *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if a.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
a := &dialParams{
host: host,
httpPort: httpPort,
httpsPort: httpsPort,
machineKey: machineKey,
controlKey: controlKey,
version: protocolVersion,
proxyFunc: tshttpproxy.ProxyFromEnvironment,
dialer: dialer,
}
return a.dial(ctx)
}
func (a *Dialer) logf(format string, args ...any) {
if a.Logf != nil {
a.Logf(format, args...)
}
type dialParams struct {
host string
httpPort string
httpsPort string
machineKey key.MachinePrivate
controlKey key.MachinePublic
version uint16
proxyFunc func(*http.Request) (*url.URL, error) // or nil
dialer dnscache.DialContextFunc
// For tests only
insecureTLS bool
testFallbackDelay time.Duration
}
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
if a.proxyFunc != nil {
return a.proxyFunc
}
return tshttpproxy.ProxyFromEnvironment
}
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
// starting to try a.HTTPSPort.
func (a *Dialer) httpsFallbackDelay() time.Duration {
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
// starting to try a.httpsPort.
func (a *dialParams) httpsFallbackDelay() time.Duration {
if v := a.testFallbackDelay; v != 0 {
return v
}
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) {
func (a *dialParams) dial(ctx context.Context) (*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.
@@ -264,12 +102,12 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Co
// we'll speak Noise.
u80 := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
Host: net.JoinHostPort(a.host, a.httpPort),
Path: serverUpgradePath,
}
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
Host: net.JoinHostPort(a.host, a.httpsPort),
Path: serverUpgradePath,
}
@@ -280,7 +118,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Co
}
ch := make(chan tryURLRes) // must be unbuffered
try := func(u *url.URL) {
cbConn, err := a.dialURL(ctx, u, addr)
cbConn, err := a.dialURL(ctx, u)
select {
case ch <- tryURLRes{u, cbConn, err}:
case <-ctx.Done():
@@ -331,12 +169,12 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Co
}
// dialURL attempts to connect to the given URL.
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)
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
if err != nil {
return nil, err
}
netConn, err := a.tryURLUpgrade(ctx, u, addr, init)
netConn, err := a.tryURLUpgrade(ctx, u, init)
if err != nil {
return nil, err
}
@@ -348,50 +186,29 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*con
return cbConn, nil
}
// 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.
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
//
// Only the provided ctx is used, not a.ctx.
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,
}
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
dns := &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
}
var dialer dnscache.DialContextFunc
if a.Dialer != nil {
dialer = a.Dialer
} else {
dialer = stdDialer.DialContext
}
tr := http.DefaultTransport.(*http.Transport).Clone()
defer tr.CloseIdleConnections()
tr.Proxy = a.getProxyFunc()
tr.Proxy = a.proxyFunc
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.DialContext = dnscache.Dialer(dialer, dns)
tr.DialContext = dnscache.Dialer(a.dialer, dns)
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
tr.TLSClientConfig = tlsdial.Config(a.Hostname, tr.TLSClientConfig)
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig)
if a.insecureTLS {
tr.TLSClientConfig.InsecureSkipVerify = true
tr.TLSClientConfig.VerifyConnection = nil
}
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the

View File

@@ -7,33 +7,27 @@ package controlhttp
import (
"context"
"encoding/base64"
"errors"
"net"
"net/url"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/types/key"
)
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
// bi-directional communication over an HTTP connection when in JS.
func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if d.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
init, cont, err := controlbase.ClientDeferred(d.MachineKey, d.ControlKey, d.ProtocolVersion)
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
if err != nil {
return nil, err
}
wsScheme := "wss"
host := d.Hostname
// If using a custom control server (on a non-standard port), prefer that.
// This mirrors the port selection in newNoiseClient from noise.go.
if d.HTTPPort != "" && d.HTTPPort != "80" && d.HTTPSPort == "443" {
if host == "localhost" {
wsScheme = "ws"
host = net.JoinHostPort(host, d.HTTPPort)
host = net.JoinHostPort(host, httpPort)
}
wsURL := &url.URL{
Scheme: wsScheme,
@@ -58,4 +52,5 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
return nil, err
}
return cbConn, nil
}

View File

@@ -4,17 +4,6 @@
package controlhttp
import (
"net/http"
"net/url"
"time"
"tailscale.com/net/dnscache"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
@@ -29,64 +18,3 @@ const (
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)
// Dialer contains configuration on how to dial the Tailscale control server.
type Dialer struct {
// Hostname is the hostname to connect to, with no port number.
//
// This field is required.
Hostname string
// MachineKey contains the current machine's private key.
//
// This field is required.
MachineKey key.MachinePrivate
// ControlKey contains the expected public key for the control server.
//
// This field is required.
ControlKey key.MachinePublic
// ProtocolVersion is the expected protocol version to negotiate.
//
// This field is required.
ProtocolVersion uint16
// HTTPPort is the port number to use when making a HTTP connection.
//
// If not specified, this defaults to port 80.
HTTPPort string
// HTTPSPort is the port number to use when making a HTTPS connection.
//
// If not specified, this defaults to port 443.
HTTPSPort string
// Dialer is the dialer used to make outbound connections.
//
// If not specified, this defaults to net.Dialer.DialContext.
Dialer dnscache.DialContextFunc
// Logf, if set, is a logging function to use; if unset, logs are
// 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
}
func strDef(v1, v2 string) string {
if v1 != "" {
return v1
}
return v2
}

View File

@@ -13,21 +13,16 @@ 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 {
@@ -175,16 +170,15 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
defer cancel()
}
a := &Dialer{
Hostname: "localhost",
HTTPPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
HTTPSPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
MachineKey: client,
ControlKey: server.Public(),
ProtocolVersion: testProtocolVersion,
Dialer: new(tsdial.Dialer).SystemDial,
Logf: t.Logf,
a := dialParams{
host: "localhost",
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
machineKey: client,
controlKey: server.Public(),
version: testProtocolVersion,
insecureTLS: true,
dialer: new(tsdial.Dialer).SystemDial,
testFallbackDelay: 50 * time.Millisecond,
}
@@ -449,263 +443,3 @@ 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()
}

View File

@@ -82,12 +82,6 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)

View File

@@ -13,18 +13,20 @@ import (
)
// disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnPControl atomic.Bool
var disableUPnP atomic.Bool
var disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
func init() {
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
}
// DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled.
func DisableUPnP() bool {
return disableUPnPControl.Load() || disableUPnpEnv()
return disableUPnP.Load()
}
// SetDisableUPnP sets whether control says that UPnP should be
// disabled.
func SetDisableUPnP(v bool) {
disableUPnPControl.Store(v)
disableUPnP.Store(v)
}

View File

@@ -2,8 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
// protocol.
// Package derp implements DERP, the Detour Encrypted Routing Protocol.
//
// DERP routes packets to clients using curve25519 keys as addresses.
//
@@ -19,6 +18,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"time"
)
@@ -194,7 +194,7 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
}
remain := frameLen - uint32(n)
if remain > 0 {
if _, err := io.CopyN(io.Discard, br, int64(remain)); err != nil {
if _, err := io.CopyN(ioutil.Discard, br, int64(remain)); err != nil {
return 0, 0, err
}
err = io.ErrShortBuffer

View File

@@ -18,6 +18,7 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"math/big"
@@ -46,6 +47,8 @@ import (
"tailscale.com/version"
)
var debug = envknob.Bool("DERP_DEBUG_LOGS")
// verboseDropKeys is the set of destination public keys that should
// verbosely log whenever DERP drops a packet.
var verboseDropKeys = map[key.NodePublic]bool{}
@@ -103,7 +106,9 @@ type Server struct {
limitedLogf logger.Logf
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
debug bool
clientDataLimit *uint64 // limit for how many bytes/s of content a client can send; atomic
clientDataBurst int // burst limit for how many bytes/s of content a client can send
// Counters:
packetsSent, bytesSent expvar.Int
@@ -297,7 +302,6 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
runtime.ReadMemStats(&ms)
s := &Server{
debug: envknob.Bool("DERP_DEBUG_LOGS"),
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
@@ -313,7 +317,10 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
avgQueueDuration: new(uint64),
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
clientDataLimit: new(uint64),
clientDataBurst: 10 * 1024 * 1024, // 10Mb default burst
}
atomic.StoreUint64(s.clientDataLimit, 12*1024*1024) // 12Mb/s default ratelimit
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
@@ -324,12 +331,48 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
s.packetsDroppedReason.Get("queue_head"),
s.packetsDroppedReason.Get("queue_tail"),
s.packetsDroppedReason.Get("write_error"),
s.packetsDroppedReason.Get("rate_limited"),
}
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
return s
}
// StartEgressRateLimiter starts dynamically adjusting the rate limit
// based on the desired limit and the utilization of the specified interface.
//
// It must be called before serving begins. All limits are in bytes/s.
func (s *Server) StartEgressRateLimiter(interfaceName string, egressDataLimit, clientDataMin, clientDataBurst int) error {
limiter, err := newEgressLimiter(interfaceName, uint64(egressDataLimit), uint64(clientDataMin))
if err != nil {
return fmt.Errorf("starting limiter: %v", err)
}
atomic.StoreUint64(s.clientDataLimit, uint64(egressDataLimit))
s.clientDataBurst = clientDataBurst
go func() {
t := time.NewTicker(time.Second)
defer t.Stop()
for {
limit, err := limiter.Limit()
if err != nil {
s.logf("derp: failed to update egress limiter: %v", err)
return
}
atomic.StoreUint64(s.clientDataLimit, uint64(limit))
<-t.C
if s.closed {
return
}
}
}()
return nil
}
// SetMesh sets the pre-shared key that regional DERP servers used to mesh
// amongst themselves.
//
@@ -663,6 +706,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
rateLimiter := rate.NewLimiter(rate.Limit(atomic.LoadUint64(s.clientDataLimit)), s.clientDataBurst)
c := &sclient{
connNum: connNum,
s: s,
@@ -680,6 +724,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan key.NodePublic),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
rateLimiter: rateLimiter,
}
if c.canMesh {
@@ -756,8 +801,20 @@ func (c *sclient) run(ctx context.Context) error {
}
}
func (c *sclient) shouldRatelimitData(dataLen int) bool {
if c.canMesh {
return false // Mesh connections arent regular clients.
}
now := time.Now()
if rateLimit := atomic.LoadUint64(c.s.clientDataLimit); rateLimit != uint64(c.rateLimiter.Limit()) {
c.rateLimiter.SetLimitAt(now, rate.Limit(rateLimit))
}
return !c.rateLimiter.AllowN(now, dataLen)
}
func (c *sclient) handleUnknownFrame(ft frameType, fl uint32) error {
_, err := io.CopyN(io.Discard, c.br, int64(fl))
_, err := io.CopyN(ioutil.Discard, c.br, int64(fl))
return err
}
@@ -800,7 +857,7 @@ func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
return err
}
if extra := int64(fl) - int64(len(m)); extra > 0 {
_, err = io.CopyN(io.Discard, c.br, extra)
_, err = io.CopyN(ioutil.Discard, c.br, extra)
}
select {
case c.sendPongCh <- [8]byte(m):
@@ -857,6 +914,11 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
}
s.packetsForwardedIn.Add(1)
if c.shouldRatelimitData(len(contents)) {
s.recordDrop(contents, c.key, dstKey, dropReasonRateLimited)
return nil
}
var dstLen int
var dst *sclient
@@ -907,6 +969,11 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
return fmt.Errorf("client %x: recvPacket: %v", c.key, err)
}
if c.shouldRatelimitData(len(contents)) {
s.recordDrop(contents, c.key, dstKey, dropReasonRateLimited)
return nil
}
var fwd PacketForwarder
var dstLen int
var dst *sclient
@@ -961,6 +1028,7 @@ const (
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
dropReasonWriteError // OS write() failed
dropReasonDupClient // the public key is connected 2+ times (active/active, fighting)
dropReasonRateLimited // send/forward packet content exceeds rate limit
)
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
@@ -979,7 +1047,7 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
if s.debug {
if debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
}
@@ -1253,6 +1321,7 @@ type sclient struct {
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
rateLimiter *rate.Limiter
// replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by
@@ -1699,6 +1768,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("average_queue_duration_ms", expvar.Func(func() any {
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
}))
m.Set("client_ratelimit_bytes_per_second", expvar.Func(func() any { return atomic.LoadUint64(s.clientDataLimit) }))
var expvarVersion expvar.String
expvarVersion.Set(version.Long)
m.Set("version", &expvarVersion)
@@ -1827,7 +1897,7 @@ func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
var bufioWriterPool = &sync.Pool{
New: func() any {
return bufio.NewWriterSize(io.Discard, 2<<10)
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
},
}
@@ -1860,7 +1930,7 @@ func (w *lazyBufioWriter) Flush() error {
}
err := w.lbw.Flush()
w.lbw.Reset(io.Discard)
w.lbw.Reset(ioutil.Discard)
bufioWriterPool.Put(w.lbw)
w.lbw = nil

View File

@@ -15,9 +15,9 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"reflect"
"sync"
"testing"
@@ -1240,7 +1240,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
}
func BenchmarkWriteUint32(b *testing.B) {
w := bufio.NewWriter(io.Discard)
w := bufio.NewWriter(ioutil.Discard)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -1279,9 +1279,9 @@ func waitConnect(t testing.TB, c *Client) {
}
func TestParseSSOutput(t *testing.T) {
contents, err := os.ReadFile("testdata/example_ss.txt")
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
if err != nil {
t.Errorf("os.ReadFile(example_ss.txt) failed: %v", err)
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
}
seen := parseSSOutput(string(contents))
if len(seen) == 0 {

View File

@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/netip"
@@ -431,7 +432,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
return nil, 0, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
b, _ := io.ReadAll(resp.Body)
b, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
}

View File

@@ -19,11 +19,12 @@ func _() {
_ = x[dropReasonQueueTail-4]
_ = x[dropReasonWriteError-5]
_ = x[dropReasonDupClient-6]
_ = x[dropReasonRateLimited-7]
}
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClient"
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClientRateLimited"
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68}
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68, 79}
func (i dropReason) String() string {
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {

171
derp/limiter.go Normal file
View File

@@ -0,0 +1,171 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package derp
import (
"io/ioutil"
"strconv"
"strings"
"time"
)
func readTxBytes(interfaceName string) (uint64, error) {
v, err := ioutil.ReadFile("/sys/class/net/" + interfaceName + "/statistics/tx_bytes")
if err != nil {
return 0, err
}
tx, err := strconv.Atoi(strings.TrimSpace(string(v)))
if err != nil {
return 0, err
}
return uint64(tx), nil
}
type egressLimiter struct {
interfaceName string
limitBytesSec uint64 // the egress bytes/s we want to stay under.
minBytesSec uint64 // the minimum bytes/s rate limit.
lastTxBytes uint64
controlLoop limiterLoop
}
func newEgressLimiter(interfaceName string, limitBytesSec, minBytesSec uint64) (*egressLimiter, error) {
initial, err := readTxBytes(interfaceName)
if err != nil {
return nil, err
}
return &egressLimiter{
interfaceName: interfaceName,
limitBytesSec: limitBytesSec,
minBytesSec: minBytesSec,
lastTxBytes: initial,
controlLoop: newLimiterLoop(limitBytesSec, time.Now()),
}, err
}
// Limit returns the current rate limit value based on interface utilization.
func (e *egressLimiter) Limit() (uint64, error) {
rx, err := readTxBytes(e.interfaceName)
if err != nil {
return 0, err
}
last := e.lastTxBytes
e.lastTxBytes = rx
limit := e.controlLoop.tick(uint64(rx)-last, time.Now())
if limit < 0 || uint64(limit) < e.minBytesSec {
limit = float64(e.minBytesSec)
}
if uint64(limit) > e.limitBytesSec {
limit = float64(e.limitBytesSec)
}
return uint64(limit), nil
}
// PID loop values for the dynamic ratelimit.
// The wikipedia page on PID is recommended reading if you are not familiar
// with PID loops or open-loop control theory.
//
// Gain values are unitless, but operate on a feedback value in bytes
// and a setpoint value in bytes/s, and a time delta (dt) of seconds.
//
// These values are initial and should be tuned: These are just initial
// values based on first principles and vibin with pretty graphs.
const (
// Proportional gain.
// Given this represents a global ratelimit, the P term doesnt make a lot of
// sense, as each clients contribution to link utilization is entirely
// dependent on the client workload.
//
// For this reason, its set super low: Its expected the I term will do
// most of the heavy lifting.
limiterP float64 = 1.0 / 1024
// Derivative gain.
// This term reacts against 'trends', that is, the first derivative of
// the feedback value. Think of it like a rapid-change damper.
//
// This isnt super important, so again we've set it fairly low.
limiterD float64 = 0.003
// Integral gain.
//
// This is where all the heavy lifting happens. Basically, we increase
// the ratelimit (by limiterIP) when we have room to spare, and
// decrease it once we exceed 4/5ths of the limit (by limiterIN).
// The increase is linear to the error between feedback and the setpoint,
// but clamped proportionate to the limit.
//
// The decrease term is stronger than the increase term, so we 'backoff
// quickly' when we are approaching limits, but test the waters on
// the other end cautiously.
limiterIP float64 = 0.008
limiterIN float64 = 0.3
)
// limiterLoop exposes a dynamic ratelimit, based on the egress rate
// of some interface. The PID loop tries to keep egress at 4/5 of the limit.
type limiterLoop struct {
limitBytesSec uint64 // the egress bytes/s we want to stay under.
integral float64 // the integral sum at lastUpdate instant
lastEgress uint64 // feedback value of previous iteration, bytes/s
lastUpdate time.Time // instant at which last iteration occurred.
}
func newLimiterLoop(limitBytesSec uint64, now time.Time) limiterLoop {
return limiterLoop{
limitBytesSec: limitBytesSec * 4 / 5,
lastUpdate: now,
lastEgress: 0,
integral: float64(limitBytesSec),
}
}
// tick computes & returns the ratelimit value in bytes/s, computing
// the next iteration of the PID loop in the process.
func (l *limiterLoop) tick(egressBytesPerSec uint64, now time.Time) float64 {
var (
dt = now.Sub(l.lastUpdate).Seconds()
err = float64(l.limitBytesSec) - float64(egressBytesPerSec)
)
// Integral term.
var iDelta float64
if err > 0 {
iDelta = err * dt * limiterIP
} else {
iDelta = err * dt * limiterIN
}
// Constrain integral sum change to a 20th of the setpoint per second.
maxDelta := dt * float64(l.limitBytesSec) / 20
if iDelta > maxDelta {
iDelta = maxDelta
} else if iDelta < -maxDelta {
iDelta = -maxDelta
}
l.integral += iDelta
// Constrain integral sum to prevent windup.
if max := float64(l.limitBytesSec); l.integral > max {
l.integral = max
} else if l.integral < -max {
l.integral = -max
}
// Derivative term.
var d float64
if dt > 0 {
d = -(float64(egressBytesPerSec-l.lastEgress) / dt) * limiterD
}
// Proportional term.
p := limiterP * err
l.lastEgress = egressBytesPerSec
l.lastUpdate = now
output := p + l.integral + d
// fmt.Printf("in=%d, out=%0.3f: p=%0.2f d=%0.2f i=%0.2f\n", egressBytesPerSec, output, p, d, l.integral)
return output
}

56
derp/limiter_test.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package derp
import (
"testing"
"time"
)
func mb(mb uint64) uint64 {
return mb * 1024 * 1024
}
func TestLimiterLoopGradual(t *testing.T) {
// Make a limiter that tries to keep under 200Mb/s.
limit := mb(200)
start := time.Now()
l := newLimiterLoop(limit, start)
// Make sure the initial value is sane.
// Lets imagine the egress is only like 1Mb/s.
now := start.Add(time.Second)
if v := uint64(l.tick(1024*1024, now)); v < mb(150) || v > limit {
t.Errorf("initial value = %dMb/s, want 150 < value < limit", v/1024/1024)
}
// Tick through 10 minutes of low usage. Lets make sure the limit stays high.
lowUsage := limit / 10
for i := 0; i < 600; i++ {
now = now.Add(time.Second)
v := uint64(l.tick(lowUsage, now))
if v < mb(150) {
t.Errorf("[t=%0.f] limit too low for low usage: %d (expected >150)", now.Sub(start).Seconds(), v/1024/1024)
}
}
// Lets tick through 60 seconds of steadily-increasing usage.
for i := 0; i < 60; i++ {
now = now.Add(time.Second)
l.tick(uint64(i)*limit/60, now)
}
if v := uint64(l.tick(limit, now)); v > mb(100) || v < mb(1) {
t.Errorf("[t=%0.f] limit = %dMb/s, want 1-100Mb/s", now.Sub(start).Seconds(), v/1024/1024)
}
// Lets imagine we are at limits for 10s. Does the limit drop pretty hard?
for i := 0; i < 10; i++ {
now = now.Add(time.Second)
l.tick(limit, now)
}
if v := uint64(l.tick(limit, now)); v > mb(20) || v < mb(1) {
t.Errorf("[t=%0.f] limit = %dMb/s, want 1-20Mb/s", now.Sub(start).Seconds(), v/1024/1024)
}
}

View File

@@ -37,7 +37,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTH_KEY
key: AUTH_KEY
optional: true
- name: TS_DEST_IP
value: "{{TS_DEST_IP}}"

View File

@@ -17,11 +17,10 @@ TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}"
TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}"
TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}"
TS_SOCKET="${TS_SOCKET:-/tmp/tailscaled.sock}"
set -e
TAILSCALED_ARGS="--socket=${TS_SOCKET}"
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}"
@@ -82,11 +81,11 @@ if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then
fi
echo "Running tailscale up"
tailscale --socket="${TS_SOCKET}" up ${UP_ARGS}
tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
if [[ ! -z "${TS_DEST_IP}" ]]; then
echo "Adding iptables rule for DNAT"
iptables -t nat -I PREROUTING -d "$(tailscale --socket=${TS_SOCKET} ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
fi
echo "Waiting for tailscaled to exit"

View File

@@ -23,7 +23,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTH_KEY
key: AUTH_KEY
optional: true
securityContext:
capabilities:

View File

@@ -23,7 +23,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTH_KEY
key: AUTH_KEY
optional: true
- name: TS_ROUTES
value: "{{TS_ROUTES}}"

View File

@@ -26,5 +26,5 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: TS_AUTH_KEY
key: AUTH_KEY
optional: true

View File

@@ -1,80 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package doctor contains more in-depth healthchecks that can be run to aid in
// diagnosing Tailscale issues.
package doctor
import (
"context"
"sync"
"tailscale.com/types/logger"
)
// Check is the interface defining a singular check.
//
// A check should log information that it gathers using the provided log
// function, and should attempt to make as much progress as possible in error
// conditions.
type Check interface {
// Name should return a name describing this check, in lower-kebab-case
// (i.e. "my-check", not "MyCheck" or "my_check").
Name() string
// Run executes the check, logging diagnostic information to the
// provided logger function.
Run(context.Context, logger.Logf) error
}
// RunChecks runs a list of checks in parallel, and logs any returned errors
// after all checks have returned.
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
if len(checks) == 0 {
return
}
type namedErr struct {
name string
err error
}
errs := make(chan namedErr, len(checks))
var wg sync.WaitGroup
wg.Add(len(checks))
for _, check := range checks {
go func(c Check) {
defer wg.Done()
plog := logger.WithPrefix(log, c.Name()+": ")
errs <- namedErr{
name: c.Name(),
err: c.Run(ctx, plog),
}
}(check)
}
wg.Wait()
close(errs)
for n := range errs {
if n.err == nil {
continue
}
log("check %s: %v", n.name, n.err)
}
}
// CheckFunc creates a Check from a name and a function.
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
return checkFunc{name, run}
}
type checkFunc struct {
name string
run func(context.Context, logger.Logf) error
}
func (c checkFunc) Name() string { return c.name }
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }

View File

@@ -1,50 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package doctor
import (
"context"
"fmt"
"sync"
"testing"
qt "github.com/frankban/quicktest"
"tailscale.com/types/logger"
)
func TestRunChecks(t *testing.T) {
c := qt.New(t)
var (
mu sync.Mutex
lines []string
)
logf := func(format string, args ...any) {
mu.Lock()
defer mu.Unlock()
lines = append(lines, fmt.Sprintf(format, args...))
}
ctx := context.Background()
RunChecks(ctx, logf,
testCheck1{},
CheckFunc("testcheck2", func(_ context.Context, log logger.Logf) error {
log("check 2")
return nil
}),
)
mu.Lock()
defer mu.Unlock()
c.Assert(lines, qt.Contains, "testcheck1: check 1")
c.Assert(lines, qt.Contains, "testcheck2: check 2")
}
type testCheck1 struct{}
func (t testCheck1) Name() string { return "testcheck1" }
func (t testCheck1) Run(_ context.Context, log logger.Logf) error {
log("check 1")
return nil
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package routetable provides a doctor.Check that dumps the current system's
// route table to the log.
package routetable
import (
"context"
"tailscale.com/net/routetable"
"tailscale.com/types/logger"
)
// MaxRoutes is the maximum number of routes that will be displayed.
const MaxRoutes = 1000
// Check implements the doctor.Check interface.
type Check struct{}
func (Check) Name() string {
return "routetable"
}
func (Check) Run(_ context.Context, logf logger.Logf) error {
rs, err := routetable.Get(MaxRoutes)
if err != nil {
return err
}
for _, r := range rs {
logf("%s", r)
}
return nil
}

View File

@@ -17,43 +17,30 @@
package envknob
import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"tailscale.com/types/opt"
"tailscale.com/version/distro"
)
var (
mu sync.Mutex
set = map[string]string{}
regStr = map[string]*string{}
regBool = map[string]*bool{}
regOptBool = map[string]*opt.Bool{}
mu sync.Mutex
set = map[string]string{}
list []string
)
func noteEnv(k, v string) {
if v == "" {
return
}
mu.Lock()
defer mu.Unlock()
noteEnvLocked(k, v)
}
func noteEnvLocked(k, v string) {
if v != "" {
set[k] = v
} else {
delete(set, k)
if _, ok := set[k]; !ok {
list = append(list, k)
}
set[k] = v
}
// logf is logger.Logf, but logger depends on envknob, so for circular
@@ -65,39 +52,11 @@ type logf = func(format string, args ...any)
func LogCurrent(logf logf) {
mu.Lock()
defer mu.Unlock()
list := make([]string, 0, len(set))
for k := range set {
list = append(list, k)
}
sort.Strings(list)
for _, k := range list {
logf("envknob: %s=%q", k, set[k])
}
}
// Setenv changes an environment variable.
//
// It is not safe for concurrent reading of environment variables via the
// Register functions. All Setenv calls are meant to happen early in main before
// any goroutines are started.
func Setenv(envVar, val string) {
mu.Lock()
defer mu.Unlock()
os.Setenv(envVar, val)
noteEnvLocked(envVar, val)
if p := regStr[envVar]; p != nil {
*p = val
}
if p := regBool[envVar]; p != nil {
setBoolLocked(p, envVar, val)
}
if p := regOptBool[envVar]; p != nil {
setOptBoolLocked(p, envVar, val)
}
}
// String returns the named environment variable, using os.Getenv.
//
// If the variable is non-empty, it's also tracked & logged as being
@@ -108,82 +67,6 @@ func String(envVar string) string {
return v
}
// RegisterString returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterString(envVar string) func() string {
mu.Lock()
defer mu.Unlock()
p, ok := regStr[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = &val
regStr[envVar] = p
}
return func() string { return *p }
}
// RegisterBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterBool(envVar string) func() bool {
mu.Lock()
defer mu.Unlock()
p, ok := regBool[envVar]
if !ok {
var b bool
p = &b
setBoolLocked(p, envVar, os.Getenv(envVar))
regBool[envVar] = p
}
return func() bool { return *p }
}
// RegisterOptBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterOptBool(envVar string) func() opt.Bool {
mu.Lock()
defer mu.Unlock()
p, ok := regOptBool[envVar]
if !ok {
var b opt.Bool
p = &b
setOptBoolLocked(p, envVar, os.Getenv(envVar))
regOptBool[envVar] = p
}
return func() opt.Bool { return *p }
}
func setBoolLocked(p *bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = false
return
}
var err error
*p, err = strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
}
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = ""
return
}
b, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
p.Set(b)
}
// Bool returns the boolean value of the named environment variable.
// If the variable is not set, it returns false.
// An invalid value exits the binary with a failure.
@@ -198,7 +81,6 @@ func BoolDefaultTrue(envVar string) bool {
}
func boolOr(envVar string, implicitValue bool) bool {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return implicitValue
@@ -216,7 +98,6 @@ func boolOr(envVar string, implicitValue bool) bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupBool(envVar string) (v bool, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return false, false
@@ -232,7 +113,6 @@ func LookupBool(envVar string) (v bool, ok bool) {
// OptBool is like Bool, but returns an opt.Bool, so the caller can
// distinguish between implicitly and explicitly false.
func OptBool(envVar string) opt.Bool {
assertNotInInit()
b, ok := LookupBool(envVar)
if !ok {
return ""
@@ -246,7 +126,6 @@ func OptBool(envVar string) opt.Bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupInt(envVar string) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
@@ -285,142 +164,5 @@ func NoLogsNoSupport() bool {
// SetNoLogsNoSupport enables no-logs-no-support mode.
func SetNoLogsNoSupport() {
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
}
// notInInit is set true the first time we've seen a non-init stack trace.
var notInInit atomic.Bool
func assertNotInInit() {
if notInInit.Load() {
return
}
skip := 0
for {
pc, _, _, ok := runtime.Caller(skip)
if !ok {
notInInit.Store(true)
return
}
fu := runtime.FuncForPC(pc)
if fu == nil {
return
}
name := fu.Name()
name = strings.TrimRightFunc(name, func(r rune) bool { return r >= '0' && r <= '9' })
if strings.HasSuffix(name, ".init") || strings.HasSuffix(name, ".init.") {
stack := make([]byte, 1<<10)
stack = stack[:runtime.Stack(stack, false)]
envCheckedInInitStack = stack
}
skip++
}
}
var envCheckedInInitStack []byte
// PanicIfAnyEnvCheckedInInit panics if environment variables were read during
// init.
func PanicIfAnyEnvCheckedInInit() {
if envCheckedInInitStack != nil {
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
}
}
var applyDiskConfigErr error
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
func ApplyDiskConfigError() error { return applyDiskConfigErr }
// ApplyDiskConfig returns a platform-specific config file of environment keys/values and
// applies them. On Linux and Unix operating systems, it's a no-op and always returns nil.
// If no platform-specific config file is found, it also returns nil.
//
// It exists primarily for Windows to make it easy to apply environment variables to
// a running service in a way similar to modifying /etc/default/tailscaled on Linux.
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
func ApplyDiskConfig() (err error) {
var f *os.File
defer func() {
if err != nil {
// Stash away our return error for the healthcheck package to use.
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
}
}()
// First try the explicitly-provided value for development testing. Not
// useful for users to use on their own. (if they can set this, they can set
// any environment variable anyway)
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
f, err = os.Open(name)
if err != nil {
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
}
defer f.Close()
return applyKeyValueEnv(f)
}
name := getPlatformEnvFile()
if name == "" {
return nil
}
f, err = os.Open(name)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
defer f.Close()
return applyKeyValueEnv(f)
}
// getPlatformEnvFile returns the current platform's path to an optional
// tailscaled-env.txt file. It returns an empty string if none is defined
// for the platform.
func getPlatformEnvFile() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
case "linux":
if distro.Get() == distro.Synology {
return "/etc/tailscale/tailscaled-env.txt"
}
case "darwin":
// TODO(bradfitz): figure this out. There are three ways to run
// Tailscale on macOS (tailscaled, GUI App Store, GUI System Extension)
// and we should deal with all three.
}
return ""
}
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
//
// Empty lines and lines beginning with '#' are skipped.
//
// Values can be double quoted, in which case they're unquoted using
// strconv.Unquote.
func applyKeyValueEnv(r io.Reader) error {
bs := bufio.NewScanner(r)
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
k = strings.TrimSpace(k)
if !ok || k == "" {
continue
}
v = strings.TrimSpace(v)
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return fmt.Errorf("invalid value in line %q: %v", line, err)
}
}
Setenv(k, v)
}
return bs.Err()
os.Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
}

View File

@@ -325,7 +325,7 @@ func OverallError() error {
return overallErrorLocked()
}
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
func overallErrorLocked() error {
if !anyInterfaceUp {
@@ -383,10 +383,7 @@ func overallErrorLocked() error {
for _, s := range controlHealth {
errs = append(errs, errors.New(s))
}
if err := envknob.ApplyDiskConfigError(); err != nil {
errs = append(errs, err)
}
if e := fakeErrForTesting(); len(errs) == 0 && e != "" {
if e := fakeErrForTesting; len(errs) == 0 && e != "" {
return errors.New(e)
}
sort.Slice(errs, func(i, j int) bool {

View File

@@ -9,6 +9,7 @@ package hostinfo
import (
"bytes"
"io/ioutil"
"os"
"strings"
@@ -98,11 +99,11 @@ func linuxVersionMeta() (meta versionMeta) {
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
case distro.WDMyCloud:
slurp, _ := os.ReadFile("/etc/version")
slurp, _ := ioutil.ReadFile("/etc/version")
meta.DistroVersion = string(bytes.TrimSpace(slurp))
return
case distro.QNAP:
slurp, _ := os.ReadFile("/etc/version_info")
slurp, _ := ioutil.ReadFile("/etc/version_info")
meta.DistroVersion = getQnapQtsVersion(string(slurp))
return
}
@@ -132,7 +133,7 @@ func linuxVersionMeta() (meta versionMeta) {
case "debian":
// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
// Or "bookworm/sid" on sid/testing.
slurp, _ := os.ReadFile("/etc/debian_version")
slurp, _ := ioutil.ReadFile("/etc/debian_version")
if v := string(bytes.TrimSpace(slurp)); v != "" {
if '0' <= v[0] && v[0] <= '9' {
meta.DistroVersion = v
@@ -142,7 +143,7 @@ func linuxVersionMeta() (meta versionMeta) {
}
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
if meta.DistroVersion == "" {
if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
meta.DistroVersion = string(bytes.TrimSpace(cr))
}
}

View File

@@ -5,47 +5,16 @@
package ipnlocal
import (
"encoding/json"
"io"
"net/http"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
)
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
writeJSON := func(v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
switch r.URL.Path {
case "/echo":
// Test handler.
body, _ := io.ReadAll(r.Body)
w.Write(body)
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump())
case "/debug/prefs":
writeJSON(b.Prefs())
case "/debug/metrics":
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
res, err := b.getSSHUsernames(&req)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
writeJSON(res)
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}

View File

@@ -24,10 +24,9 @@ import (
"time"
"go4.org/netipx"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
"tailscale.com/doctor/routetable"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
@@ -69,6 +68,7 @@ import (
)
var controlDebugFlags = getControlDebugFlags()
var canSSH = envknob.CanSSHD()
func getControlDebugFlags() []string {
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
@@ -191,10 +191,6 @@ 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.
@@ -579,10 +575,6 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
b.mu.Lock()
defer b.mu.Unlock()
return b.peerCapsLocked(src)
}
func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
if b.netMap == nil {
return nil
}
@@ -594,9 +586,9 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
if !a.IsSingleIP() {
continue
}
dst := a.Addr()
if dst.BitLen() == src.BitLen() { // match on family
return filt.AppendCaps(nil, src, dst)
dstIP := a.Addr()
if dstIP.BitLen() == src.BitLen() {
return filt.AppendCaps(nil, src, a.Addr())
}
}
return nil
@@ -690,9 +682,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
}
if st.NetMap != nil {
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
b.logf("[v1] TKA sync error: %v", err)
}
if b.findExitNodeIDLocked(st.NetMap) {
prefsChanged = true
}
@@ -1093,7 +1082,6 @@ 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.
@@ -1522,12 +1510,12 @@ func (b *LocalBackend) tellClientToBrowseToURL(url string) {
}
// For testing lazy machine key generation.
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
var cache syncs.AtomicValue[key.MachinePrivate]
return func() (key.MachinePrivate, error) {
if panicOnMachineKeyGeneration() {
if panicOnMachineKeyGeneration {
panic("machine key generated")
}
if v, ok := cache.LoadOk(); ok {
@@ -1764,7 +1752,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
// from the prefs p, which may be nil.
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
b.sshAtomicBool.Store(p != nil && p.RunSSH && envknob.CanSSHD())
b.sshAtomicBool.Store(p != nil && p.RunSSH && canSSH)
if p == nil {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
@@ -1979,7 +1967,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !envknob.CanSSHD() {
if !canSSH {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
@@ -2044,7 +2032,7 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
b.logf("EditPrefs check error: %v", err)
return nil, err
}
if p1.RunSSH && !envknob.CanSSHD() {
if p1.RunSSH && !canSSH {
b.mu.Unlock()
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
return nil, errors.New("Tailscale SSH server administratively disabled.")
@@ -2866,7 +2854,7 @@ func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Pre
hi.ShieldsUp = prefs.ShieldsUp
var sshHostKeys []string
if prefs.RunSSH && envknob.CanSSHD() {
if prefs.RunSSH && canSSH {
// TODO(bradfitz): this is called with b.mu held. Not ideal.
// If the filesystem gets wedged or something we could block for
// a long time. But probably fine.
@@ -3085,7 +3073,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.setAtomicValuesFromPrefs(nil)
}
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && canSSH }
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
@@ -3119,9 +3107,6 @@ 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
@@ -3238,17 +3223,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
}
// operatorUserName returns the current pref's OperatorUser's name, or the
// empty string if none.
func (b *LocalBackend) operatorUserName() string {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return ""
}
return b.prefs.OperatorUser
}
// OperatorUserID returns the current pref's OperatorUser's ID (in
// os/user.User.Uid string form), or the empty string if none.
func (b *LocalBackend) OperatorUserID() string {
@@ -3331,15 +3305,13 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
if len(p.Addresses) == 0 {
continue
}
if p.User != nm.User && b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharing) {
if p.User != nm.User && !slices.Contains(p.Capabilities, tailcfg.CapabilityFileSharingTarget) {
continue
}
peerAPI := peerAPIBase(b.netMap, p)
if peerAPI == "" {
continue
}
ret = append(ret, &apitype.FileTarget{
Node: p,
@@ -3350,15 +3322,6 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return ret, nil
}
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap string) bool {
for _, hasCap := range b.peerCapsLocked(addr) {
if hasCap == wantCap {
return true
}
}
return false
}
// SetDNS adds a DNS record for the given domain name & TXT record
// value.
//
@@ -3617,17 +3580,6 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
return cc.DoNoiseRequest(req)
}
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
// on prefs. It returns false if there are no prefs set.
func (b *LocalBackend) tailscaleSSHEnabled() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
return b.prefs.RunSSH
}
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -3686,19 +3638,3 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
}
io.WriteString(w, "</ul>\n")
}
func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
var checks []doctor.Check
checks = append(checks, routetable.Check{})
// TODO(andrew): more
numChecks := len(checks)
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
log("%d checks", numChecks)
return nil
}))
doctor.RunChecks(ctx, logf, checks...)
}

View File

@@ -478,8 +478,8 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
defer func(old func() bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = func() bool { return true }
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = true
var logf logger.Logf = logger.Discard
store := new(mem.Store)

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
@@ -12,8 +12,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"tailscale.com/envknob"
@@ -26,123 +24,13 @@ import (
"tailscale.com/types/tkatype"
)
var networkLockAvailable = envknob.RegisterBool("TS_EXPERIMENTAL_NETWORK_LOCK")
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
type tkaState struct {
authority *tka.Authority
storage *tka.FS
}
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
// performing the steps necessary to synchronize local tka state.
//
// There are 4 scenarios handled here:
// - Enablement: nm.TKAEnabled but b.tka == nil
// ∴ reach out to /machine/tka/boostrap to get the genesis AUM, then
// initialize TKA.
// - Disablement: !nm.TKAEnabled but b.tka != nil
// ∴ reach out to /machine/tka/boostrap to read the disablement secret,
// then verify and clear tka local state.
// - Sync needed: b.tka.Head != nm.TKAHead
// ∴ complete multi-step synchronization flow.
// - Everything up to date: All other cases.
// ∴ no action necessary.
//
// b.mu must be held. b.mu will be stepped out of (and back in) during network
// RPCs.
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
if !networkLockAvailable() {
// If the feature flag is not enabled, pretend we don't exist.
return nil
}
isEnabled := b.tka != nil
wantEnabled := nm.TKAEnabled
if isEnabled != wantEnabled {
var ourHead tka.AUMHash
if b.tka != nil {
ourHead = b.tka.authority.Head()
}
// Regardless of whether we are moving to disabled or enabled, we
// need information from the tka bootstrap endpoint.
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
b.mu.Unlock()
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
b.mu.Lock()
if err != nil {
return fmt.Errorf("fetching bootstrap: %v", err)
}
if wantEnabled && !isEnabled {
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
return fmt.Errorf("bootstrap: %v", err)
}
isEnabled = true
} else if !wantEnabled && isEnabled {
if b.tka.authority.ValidDisablement(bs.DisablementSecret) {
b.tka = nil
isEnabled = false
if err := os.RemoveAll(b.chonkPath()); err != nil {
return fmt.Errorf("os.RemoveAll: %v", err)
}
} else {
b.logf("Disablement secret did not verify, leaving TKA enabled.")
}
} else {
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
}
}
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
// TODO(tom): Implement sync
}
return nil
}
// chonkPath returns the absolute path to the directory in which TKA
// state (the 'tailchonk') is stored.
func (b *LocalBackend) chonkPath() string {
return filepath.Join(b.TailscaleVarRoot(), "tka")
}
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
// tailnet key authority, based on the given genesis AUM.
//
// b.mu must be held.
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
if !b.CanSupportNetworkLock() {
return errors.New("network lock not supported in this configuration")
}
var genesis tka.AUM
if err := genesis.Unserialize(g); err != nil {
return fmt.Errorf("reading genesis: %v", err)
}
chonkDir := b.chonkPath()
if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("mkdir: %v", err)
}
chonk, err := tka.ChonkDir(chonkDir)
if err != nil {
return fmt.Errorf("chonk: %v", err)
}
authority, err := tka.Bootstrap(chonk, genesis)
if err != nil {
return fmt.Errorf("tka bootstrap: %v", err)
}
b.tka = &tkaState{
authority: authority,
storage: chonk,
}
return nil
}
// CanSupportNetworkLock returns true if tailscaled is able to operate
// a local tailnet key authority (and hence enforce network lock).
func (b *LocalBackend) CanSupportNetworkLock() bool {
@@ -194,21 +82,15 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
if b.tka != nil {
return errors.New("network-lock is already initialized")
}
if !networkLockAvailable() {
if !networkLockAvailable {
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
}
if !b.CanSupportNetworkLock() {
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
}
var ourNodeKey key.NodePublic
b.mu.Lock()
if b.prefs != nil {
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
}
b.mu.Unlock()
if ourNodeKey.IsZero() {
return errors.New("no node-key: is tailscale logged in?")
nm := b.NetMap()
if nm == nil {
return errors.New("no netmap: are you logged into tailscale?")
}
// Generates a genesis AUM representing trust in the provided keys.
@@ -230,7 +112,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
}
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
initResp, err := b.tkaInitBegin(nm, genesisAUM)
if err != nil {
return fmt.Errorf("tka init-begin RPC: %w", err)
}
@@ -251,7 +133,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
}
// Finalize enablement by transmitting signature for all nodes to Control.
_, err = b.tkaInitFinish(ourNodeKey, sigs)
_, err = b.tkaInitFinish(nm, sigs)
return err
}
@@ -274,11 +156,10 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
return &sig, nil
}
func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
NodeID: nm.SelfNode.ID,
GenesisAUM: aum.Serialize(),
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
@@ -316,11 +197,10 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
}
}
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
NodeID: nm.SelfNode.ID,
Signatures: nks,
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
@@ -357,51 +237,3 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
return a, nil
}
}
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
// over noise. This is used to get values necessary to enable or disable TKA.
func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
bootstrapReq := tailcfg.TKABootstrapRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
}
if !head.IsZero() {
head, err := head.MarshalText()
if err != nil {
return nil, fmt.Errorf("head.MarshalText failed: %v", err)
}
bootstrapReq.Head = string(head)
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(bootstrapReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/bootstrap", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKABootstrapResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}

View File

@@ -1,258 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipnlocal
import (
"bytes"
"context"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"tailscale.com/control/controlclient"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
)
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
hi := hostinfo.New()
ni := tailcfg.NetInfo{LinkType: "wired"}
hi.NetInfo = &ni
k := key.NewMachine()
opts := controlclient.Options{
ServerURL: "https://example.com",
Hostinfo: hi,
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
},
HTTPTestClient: c,
NoiseTestClient: c,
Status: func(controlclient.Status) {},
}
cc, err := controlclient.NewNoStart(opts)
if err != nil {
t.Fatal(err)
}
return cc
}
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
// httptest plumbing, despite the domain being unused in the actual noise request transport.
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
ts := httptest.NewUnstartedServer(handler)
ts.StartTLS()
client := ts.Client()
client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, ts.Listener.Addr().String())
}
return ts, client
}
func TestTKAEnablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
nodePriv := key.NewNode()
// Make a fake TKA authority, getting a usable genesis AUM which
// our mock server can communicate.
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
a1, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/bootstrap":
body := new(tailcfg.TKABootstrapRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("bootstrap nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
}
if body.Head != "" {
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
}
w.WriteHeader(200)
out := tailcfg.TKABootstrapResponse{
GenesisAUM: genesisAUM.Serialize(),
}
if err := json.NewEncoder(w).Encode(out); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
temp := t.TempDir()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: tka.AUMHash{},
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka == nil {
t.Fatal("tka was not initialized")
}
if b.tka.authority.Head() != a1.Head() {
t.Errorf("authority.Head() = %x, want %x", b.tka.authority.Head(), a1.Head())
}
}
func TestTKADisablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
returnWrongSecret := false
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/bootstrap":
body := new(tailcfg.TKABootstrapRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
}
var head tka.AUMHash
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
t.Fatalf("failed unmarshal of body.Head: %v", err)
}
if head != authority.Head() {
t.Errorf("reported head = %x, want %x", head, authority.Head())
}
var disablement []byte
if returnWrongSecret {
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
} else {
disablement = disablementSecret
}
w.WriteHeader(200)
out := tailcfg.TKABootstrapResponse{
DisablementSecret: disablement,
}
if err := json.NewEncoder(w).Encode(out); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}
// Test that the wrong disablement secret does not shut down the authority.
returnWrongSecret = true
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka == nil {
t.Error("TKA was disabled despite incorrect disablement secret")
}
// Test the correct disablement secret shuts down the authority.
returnWrongSecret = false
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
if b.tka != nil {
t.Fatal("tka was not shut down")
}
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
}
}

View File

@@ -44,7 +44,6 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/strs"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
@@ -721,8 +720,8 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
return
}
rawPath := r.URL.EscapedPath()
suffix, ok := strs.CutPrefix(rawPath, "/v0/put/")
if !ok {
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
if suffix == rawPath {
http.Error(w, "misconfigured internals", 500)
return
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
@@ -86,7 +87,7 @@ func fileHasContents(name string, want string) check {
return
}
path := filepath.Join(root, name)
got, err := os.ReadFile(path)
got, err := ioutil.ReadFile(path)
if err != nil {
t.Errorf("fileHasContents: %v", err)
return
@@ -516,7 +517,7 @@ func TestDeletedMarkers(t *testing.T) {
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := os.ReadDir(dir); err != nil {
if fis, err := ioutil.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {

View File

@@ -18,98 +18,24 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/envknob"
"tailscale.com/util/mak"
)
var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS")
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys or try to generate for ourselves when not
// running as root.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
// getSSHUsernames discovers and returns the list of usernames that are
// potential Tailscale SSH user targets.
//
// Invariant: must not be called with b.mu held.
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
res := new(tailcfg.C2NSSHUsernamesResponse)
if !b.tailscaleSSHEnabled() {
return res, nil
}
max := 10
if req != nil && req.Max != 0 {
max = req.Max
}
add := func(u string) {
if req != nil && req.Exclude[u] {
return
}
switch u {
case "nobody", "daemon", "sync":
return
}
if slices.Contains(res.Usernames, u) {
return
}
if len(res.Usernames) > max {
// Enough for a hint.
return
}
res.Usernames = append(res.Usernames, u)
}
if opUser := b.operatorUserName(); opUser != "" {
add(opUser)
}
// Check popular usernames and see if they exist with a real shell.
switch runtime.GOOS {
case "darwin":
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
}
add(string(line))
return nil
})
default:
lineread.File("/etc/passwd", func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
return res, nil
}
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
var existing map[string]ssh.Signer
if os.Geteuid() == 0 {
@@ -157,7 +83,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
defer keyGenMu.Unlock()
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := os.ReadFile(path)
v, err := ioutil.ReadFile(path)
if err == nil {
return v, nil
}
@@ -198,7 +124,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
for _, typ := range keyTypes {
filename := "/etc/ssh/ssh_host_" + typ + "_key"
hostKey, err := os.ReadFile(filename)
hostKey, err := ioutil.ReadFile(filename)
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
continue
}

View File

@@ -6,16 +6,6 @@
package ipnlocal
import (
"errors"
"tailscale.com/tailcfg"
)
func (b *LocalBackend) getSSHHostKeyPublicStrings() []string {
return nil
}
func (b *LocalBackend) getSSHUsernames(*tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
return nil, errors.New("not implemented")
}

View File

@@ -2,18 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || (darwin && !ios)
// +build linux darwin,!ios
//go:build linux
// +build linux
package ipnlocal
import (
"encoding/json"
"reflect"
"testing"
"tailscale.com/tailcfg"
"tailscale.com/util/must"
)
func TestSSHKeyGen(t *testing.T) {
@@ -44,17 +40,3 @@ func TestSSHKeyGen(t *testing.T) {
t.Errorf("got different keys on second call")
}
}
type fakeSSHServer struct {
SSHServer
}
func TestGetSSHUsernames(t *testing.T) {
b := new(LocalBackend)
b.sshServer = fakeSSHServer{}
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", must.Get(json.Marshal(res)))
}

View File

@@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -214,7 +215,7 @@ func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
s.logf("blocking client while server in use; connIdentity=%v", ci)
connDone := make(chan struct{})
go func() {
io.Copy(io.Discard, conn)
io.Copy(ioutil.Discard, conn)
close(connDone)
}()
ch := make(chan struct{}, 1)
@@ -772,7 +773,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
})
if root := b.TailscaleVarRoot(); root != "" {
chonkDir := filepath.Join(root, "tka")
chonkDir := filepath.Join(root, "chonk")
if _, err := os.Stat(chonkDir); err == nil {
// The directory exists, which means network-lock has been initialized.
storage, err := tka.ChonkDir(chonkDir)
@@ -932,6 +933,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
startTime := time.Now()
log.Printf("exec: %#v %v", executable, args)
cmd := exec.Command(executable, args...)
if runtime.GOOS == "windows" {
extraEnv, err := loadExtraEnv()
if err != nil {
logf("errors loading extra env file; ignoring: %v", err)
} else {
cmd.Env = append(os.Environ(), extraEnv...)
}
}
// Create a pipe object to use as the subproc's stdin.
// When the writer goes away, the reader gets EOF.
@@ -1166,7 +1175,7 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := os.ReadDir("/mnt")
fis, err := ioutil.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
@@ -1200,3 +1209,38 @@ func findQnapTaildropDir(name string) (string, error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
func loadExtraEnv() (env []string, err error) {
if runtime.GOOS != "windows" {
return nil, nil
}
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
contents, err := os.ReadFile(name)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
for _, line := range strings.Split(string(contents), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok || k == "" {
continue
}
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
}
env = append(env, k+"="+v)
} else {
env = append(env, line)
}
}
return env, nil
}

View File

@@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -37,7 +38,6 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/logger"
"tailscale.com/util/strs"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -73,7 +73,7 @@ func (h *Handler) certDir() (string, error) {
return full, nil
}
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite && !h.PermitCert {
@@ -87,19 +87,16 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
return
}
domain, ok := strs.CutPrefix(r.URL.Path, "/localapi/v0/cert/")
if !ok {
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
if domain == r.URL.Path {
http.Error(w, "internal handler config wired wrong", 500)
return
}
if !validLookingCertDomain(domain) {
http.Error(w, "invalid domain", 400)
return
}
now := time.Now()
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
traceACME := func(v any) {
if !acmeDebug() {
if !acmeDebug {
return
}
j, _ := json.MarshalIndent(v, "", "\t")
@@ -168,11 +165,6 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
// keypair for domain exists on disk in dir that is valid at the
// provided now time.
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
if !validLookingCertDomain(domain) {
// Before we read files from disk using it, validate it's halfway
// reasonable looking.
return nil, false
}
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
certPEM, _ := os.ReadFile(certFile(dir, domain))
if validCertPEM(domain, keyPEM, certPEM, now) {
@@ -301,7 +293,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
if err := os.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
return nil, err
}
@@ -324,7 +316,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
return nil, err
}
}
if err := os.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
return nil, err
}
@@ -380,7 +372,7 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := os.ReadFile(pemName); err == nil {
if v, err := ioutil.ReadFile(pemName); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
@@ -396,7 +388,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
return nil, err
}
return privKey, nil
@@ -434,21 +426,6 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
return err == nil
}
// validLookingCertDomain reports whether name looks like a valid domain name that
// we might be able to get a cert for.
//
// It's a light check primarily for double checking before it's used
// as part of a filesystem path. The actual validation happens in checkCertDomain.
func validLookingCertDomain(name string) bool {
if name == "" ||
strings.Contains(name, "..") ||
strings.ContainsAny(name, ":/\\\x00") ||
!strings.Contains(name, ".") {
return false
}
return true
}
func checkCertDomain(st *ipnstate.Status, domain string) error {
if domain == "" {
return errors.New("missing domain name")

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios && !android && !js
// +build !ios,!android,!js
package localapi
import "testing"
func TestValidLookingCertDomain(t *testing.T) {
tests := []struct {
in string
want bool
}{
{"foo.com", true},
{"foo..com", false},
{"foo/com.com", false},
{"NUL", false},
{"", false},
{"foo\\bar.com", false},
{"foo\x00bar.com", false},
}
for _, tt := range tests {
if got := validLookingCertDomain(tt.in); got != tt.want {
t.Errorf("validLookingCertDomain(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}

View File

@@ -221,9 +221,6 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
if note := r.FormValue("note"); len(note) > 0 {
h.logf("user bugreport note: %s", note)
}
if defBool(r.FormValue("diagnose"), false) {
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, logMarker)
}

View File

@@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/netip"
"os"
@@ -617,7 +618,7 @@ func PrefsFromBytes(b []byte) (*Prefs, error) {
// LoadPrefs loads a legacy relaynode config file into Prefs
// with sensible migration defaults set.
func LoadPrefs(filename string) (*Prefs, error) {
data, err := os.ReadFile(filename)
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
}

View File

@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/netip"
"os"
"reflect"
@@ -473,7 +474,7 @@ func TestLoadPrefsNotExist(t *testing.T) {
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
if err != nil {
t.Fatal(err)
}

View File

@@ -9,6 +9,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -127,7 +128,7 @@ func NewFileStore(logf logger.Logf, path string) (ipn.StateStore, error) {
return nil, fmt.Errorf("creating state directory: %w", err)
}
bs, err := os.ReadFile(path)
bs, err := ioutil.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)

47
licenses/win.md Normal file
View File

@@ -0,0 +1,47 @@
# 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))

View File

@@ -9,6 +9,7 @@ package filelogger
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -185,18 +186,12 @@ func (w *logFileWriter) startNewFileLocked() {
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
entries, _ := os.ReadDir(w.dir)
fis, _ := ioutil.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
w.wrappedLogf("error getting log file info: %v", err)
continue
}
for _, fi := range fis {
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue

View File

@@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -247,7 +248,7 @@ func logsDir(logf logger.Logf) string {
// No idea where to put stuff. Try to create a temp dir. It'll
// mean we might lose some logs and rotate through log IDs, but
// it's something.
tmp, err := os.MkdirTemp("", "tailscaled-log-*")
tmp, err := ioutil.TempDir("", "tailscaled-log-*")
if err != nil {
panic("no safe place found to store log state")
}
@@ -258,7 +259,7 @@ func logsDir(logf logger.Logf) string {
// runningUnderSystemd reports whether we're running under systemd.
func runningUnderSystemd() bool {
if runtime.GOOS == "linux" && os.Getppid() == 1 {
slurp, _ := os.ReadFile("/proc/1/stat")
slurp, _ := ioutil.ReadFile("/proc/1/stat")
return bytes.HasPrefix(slurp, []byte("1 (systemd) "))
}
return false

View File

@@ -6,7 +6,7 @@ package main
import (
"flag"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
@@ -39,7 +39,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("logadopt: response read failed %d: %v", resp.StatusCode, err)

View File

@@ -9,7 +9,7 @@ import (
"bufio"
"encoding/json"
"flag"
"io"
"io/ioutil"
"log"
"net/http"
"os"
@@ -50,7 +50,7 @@ func main() {
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, err := io.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
}

View File

@@ -6,7 +6,7 @@ package filch
import (
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
@@ -195,7 +195,7 @@ func TestFilchStderr(t *testing.T) {
f.close(t)
pipeW.Close()
b, err := io.ReadAll(pipeR)
b, err := ioutil.ReadAll(pipeR)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,6 +13,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
@@ -429,7 +430,7 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
if resp.StatusCode != 200 {
uploaded = resp.StatusCode == 400 // the server saved the logs anyway
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
return uploaded, fmt.Errorf("log upload of %d bytes %s failed %d: %q", len(body), compressedNote, resp.StatusCode, b)
}
@@ -653,7 +654,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
return 0, nil
}
level, buf := parseAndRemoveLogLevel(buf)
if l.stderr != nil && l.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if l.stderr != nil && l.stderr != ioutil.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {

View File

@@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
@@ -51,7 +52,7 @@ func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
ts.srv = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error("failed to read HTTP request")
}

View File

@@ -12,6 +12,7 @@ import (
"bytes"
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -157,7 +158,7 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
if sc.Text() == resolvconfConfigName {
continue
}
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
bs, err := ioutil.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
if err != nil {
if os.IsNotExist(err) {
// Probably raced with a deletion, that's okay.

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"net/netip"
"os"
"os/exec"
@@ -451,7 +452,7 @@ func (fs directFS) Rename(oldName, newName string) error {
func (fs directFS) Remove(name string) error { return os.Remove(fs.path(name)) }
func (fs directFS) ReadFile(name string) ([]byte, error) {
return os.ReadFile(fs.path(name))
return ioutil.ReadFile(fs.path(name))
}
func (fs directFS) Truncate(name string) error {
@@ -459,7 +460,7 @@ func (fs directFS) Truncate(name string) error {
}
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
return os.WriteFile(fs.path(name), contents, perm)
return ioutil.WriteFile(fs.path(name), contents, perm)
}
// runningAsGUIDesktopUser reports whether it seems that this code is

View File

@@ -6,13 +6,14 @@ package dns
import (
"fmt"
"io/ioutil"
"os"
"tailscale.com/types/logger"
)
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
bs, err := os.ReadFile("/etc/resolv.conf")
bs, err := ioutil.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
return newDirectManager(logf), nil
}

View File

@@ -32,7 +32,7 @@ const (
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
)
var configureWSL = envknob.RegisterBool("TS_DEBUG_CONFIGURE_WSL")
var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
type windowsManager struct {
logf logger.Logf
@@ -359,7 +359,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
// On initial setup of WSL, the restart caused by --shutdown is slow,
// so we do it out-of-line.
if configureWSL() {
if configureWSL {
go func() {
if err := m.wslManager.SetDNS(cfg); err != nil {
m.logf("WSL SetDNS: %v", err) // continue

View File

@@ -5,12 +5,9 @@
package dns
import (
"bufio"
"errors"
"fmt"
"net/netip"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -100,44 +97,6 @@ func (a OSConfig) Equal(b OSConfig) bool {
return true
}
// Format implements the fmt.Formatter interface to ensure that Hosts is
// printed correctly (i.e. not as a bunch of pointers).
//
// Fixes https://github.com/tailscale/tailscale/issues/5669
func (a OSConfig) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
w.WriteString(`{Nameservers:[`)
for i, ns := range a.Nameservers {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", ns)
}
w.WriteString(`] SearchDomains:[`)
for i, domain := range a.SearchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] MatchDomains:[`)
for i, domain := range a.MatchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] Hosts:[`)
for i, host := range a.Hosts {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", host)
}
w.WriteString(`]}`)
}).Format(f, verb)
}
// ErrGetBaseConfigNotSupported is the error
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.

View File

@@ -1,44 +0,0 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dns
import (
"fmt"
"net/netip"
"testing"
"tailscale.com/util/dnsname"
)
func TestOSConfigPrintable(t *testing.T) {
ocfg := OSConfig{
Hosts: []*HostEntry{
{
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
Hosts: []string{"server", "client"},
},
{
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 4}),
Hosts: []string{"otherhost"},
},
},
Nameservers: []netip.Addr{
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
},
SearchDomains: []dnsname.FQDN{
dnsname.FQDN("foo.beta.tailscale.net."),
dnsname.FQDN("bar.beta.tailscale.net."),
},
MatchDomains: []dnsname.FQDN{
dnsname.FQDN("ts.com."),
},
}
s := fmt.Sprintf("%+v", ocfg)
const expected = `{Nameservers:[8.8.8.8] SearchDomains:[foo.beta.tailscale.net. bar.beta.tailscale.net.] MatchDomains:[ts.com.] Hosts:[&{Addr:100.1.2.3 Hosts:[server client]} &{Addr:100.1.2.4 Hosts:[otherhost]}]}`
if s != expected {
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
}
}

View File

@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
@@ -473,7 +474,7 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
res, err := io.ReadAll(hres.Body)
res, err := ioutil.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
@@ -483,13 +484,13 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
return res, err
}
var verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if verboseDNSForward() {
if verboseDNSForward {
f.logf("forwarder.send(%q) ...", rr.name.Addr)
defer func() {
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)

View File

@@ -141,7 +141,7 @@ func (r *Resolver) ttl() time.Duration {
return 10 * time.Minute
}
var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE")
var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
// LookupIP returns the host's primary IP address (either IPv4 or
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
@@ -167,14 +167,14 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
if debug() {
if debug {
log.Printf("dnscache: %q is an IP", host)
}
return ip, zaddr, []netip.Addr{ip}, nil
}
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug() {
if debug {
log.Printf("dnscache: %q = %v (cached)", host, ip)
}
return ip, ip6, allIPs, nil
@@ -192,13 +192,13 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
if res.Err != nil {
if r.UseLastGood {
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
if debug() {
if debug {
log.Printf("dnscache: %q using %v after error", host, ip)
}
return ip, ip6, allIPs, nil
}
}
if debug() {
if debug {
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
}
return zaddr, zaddr, nil, res.Err
@@ -206,7 +206,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
r := res.Val
return r.ip, r.ip6, r.allIPs, nil
case <-ctx.Done():
if debug() {
if debug {
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
}
return zaddr, zaddr, nil, ctx.Err()
@@ -250,7 +250,7 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) {
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug() {
if debug {
log.Printf("dnscache: %q found in cache as %v", host, ip)
}
return ip, ip6, allIPs, nil
@@ -300,13 +300,13 @@ func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Ad
if ip.IsPrivate() {
// Don't cache obviously wrong entries from captive portals.
// TODO: use DoH or DoT for the forwarding resolver?
if debug() {
if debug {
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
}
return
}
if debug() {
if debug {
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
}
@@ -382,7 +382,7 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC
}
i4s := v4addrs(allIPs)
if len(i4s) < 2 {
if debug() {
if debug {
log.Printf("dnscache: dialing %s, %s for %s", network, ip, address)
}
c, err := dc.dialOne(ctx, ip.Unmap())
@@ -406,7 +406,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// Can't try bootstrap DNS if we don't have a fallback function
if d.dnsCache.LookupIPFallback == nil {
if debug() {
if debug {
log.Printf("dnscache: not using bootstrap DNS: no fallback")
}
return false
@@ -415,7 +415,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// We can't retry if the context is canceled, since any further
// operations with this context will fail.
if ctxErr := ctx.Err(); ctxErr != nil {
if debug() {
if debug {
log.Printf("dnscache: not using bootstrap DNS: context error: %v", ctxErr)
}
return false
@@ -423,7 +423,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
wasTrustworthy := dc.dnsWasTrustworthy()
if wasTrustworthy {
if debug() {
if debug {
log.Printf("dnscache: not using bootstrap DNS: DNS was trustworthy")
}
return false

View File

@@ -167,8 +167,10 @@ func TestInterleaveSlices(t *testing.T) {
func TestShouldTryBootstrap(t *testing.T) {
oldDebug := debug
t.Cleanup(func() { debug = oldDebug })
debug = func() bool { return true }
t.Cleanup(func() {
debug = oldDebug
})
debug = true
type step struct {
ip netip.Addr // IP we pretended to dial

Some files were not shown because too many files have changed in this diff Show More