Compare commits

..

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
c5731e6cd2 tailcfg: redefine Location.CountryCode as upper case
Because other than TLDs, it's traditionally upper case:
https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-06-25 13:11:10 -07:00
234 changed files with 6198 additions and 21388 deletions

View File

@@ -1,37 +0,0 @@
name: govulncheck
on:
schedule:
- cron: "0 12 * * *" # 8am EST / 10am PST / 12pm UTC
workflow_dispatch: # allow manual trigger for testing
pull_request:
paths:
- ".github/workflows/govulncheck.yml"
jobs:
source-scan:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Install govulncheck
run: ./tool/go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Scan source code for known vulnerabilities
run: PATH=$PWD/tool/:$PATH "$(./tool/go env GOPATH)/bin/govulncheck" -test ./...
- uses: ruby/action-slack@v3.2.1
with:
payload: >
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks>
(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|commit>) of ${{ github.repository }}@${{ github.ref_name }} by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'schedule'

View File

@@ -90,11 +90,11 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper
env:
GOARCH: ${{ matrix.goarch }}
- name: bench all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -test.bench=. -test.benchtime=1x -test.run=^$
env:
GOARCH: ${{ matrix.goarch }}
- name: check that no tracked files changed

3
.gitignore vendored
View File

@@ -35,8 +35,5 @@ cmd/tailscaled/tailscaled
# Ignore direnv nix-shell environment cache
.direnv/
.vite/
webui/node_modules
/gocross
/dist

View File

@@ -1 +1 @@
1.47.0
1.45.0

22
api.md
View File

@@ -101,8 +101,8 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
``` jsonc
{
// addresses (array of strings) is a list of Tailscale IP
// addresses for the device, including both IPv4 (formatted as 100.x.y.z)
// and IPv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses.
// addresses for the device, including both ipv4 (formatted as 100.x.y.z)
// and ipv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses.
"addresses": [
"100.87.74.78",
"fd7a:115c:a1e0:ac82:4843:ca90:697d:c36e"
@@ -516,8 +516,7 @@ The ID of the device.
#### `authorized` (required in `POST` body)
Specify whether the device is authorized. False to deauthorize an authorized device, and true to authorize a new device or to re-authorize a previously deauthorized device.
Specify whether the device is authorized.
``` jsonc
{
@@ -1115,21 +1114,6 @@ Look at the response body to determine whether there was a problem within your A
}
```
If your tailnet has [user and group provisioning](https://tailscale.com/kb/1180/sso-okta-scim/) turned on, we will also warn you about
any groups that are used in the policy file that are not being synced from SCIM. Explicitly defined groups will not trigger this warning.
```jsonc
{
"message":"warning(s) found",
"data":[
{
"user": "group:unknown@example.com",
"warnings":["group is not syncing from SCIM and will be ignored by rules in the policy file"]
}
]
}
```
<a href="tailnet-devices"></a>
## List tailnet devices

View File

@@ -150,9 +150,8 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
// ACLTestFailureSummary specifies the JSON format sent to the
// JavaScript client to be rendered in the HTML.
type ACLTestFailureSummary struct {
User string `json:"user,omitempty"`
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
User string `json:"user"`
Errors []string `json:"errors"`
}
// ACLTestError is ErrResponse but with an extra field to account for ACLTestFailureSummary.

View File

@@ -10,14 +10,12 @@ import "tailscale.com/tailcfg"
const LocalAPIHost = "local-tailscaled.sock"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
// In successful whois responses, Node and UserProfile are never nil.
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
// CapMap is a map of capabilities to their values.
// See tailcfg.PeerCapMap and tailcfg.PeerCapability for details.
CapMap tailcfg.PeerCapMap
// Caps are extra capabilities that the remote Node has to this node.
Caps []string `json:",omitempty"`
}
// FileTarget is a node to which files can be sent, and the PeerAPI

View File

@@ -946,57 +946,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
return nil
}
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
}
return decodeJSON[*tka.DeeplinkValidationResult](body)
}
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
}{removeKeys, forkFrom.String()}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
r := bytes.NewReader(aum.Serialize())
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
if err != nil {
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
r := bytes.NewReader(aum.Serialize())
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
if err != nil {
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return nil
}
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
@@ -1124,27 +1073,6 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
return err
}
// QueryFeature makes a request for instructions on how to enable a
// feature, such as Funnel, for the node's tailnet.
//
// This request itself does not directly enable the feature on behalf
// of the node, but rather returns information that can be presented
// to the acting user about where/how to enable the feature.
//
// If relevant, this includes a control URL the user can visit to
// explicitly consent to using the feature. LocalClient.WatchIPNBus
// can be used to block on the feature being enabled.
//
// 2023-08-02: Valid feature values are "serve" and "funnel".
func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
v := url.Values{"feature": {feature}}
body, err := lc.send(ctx, "POST", "/localapi/v0/query-feature?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
}
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
v := url.Values{"region": {regionIDOrCode}}
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)

View File

@@ -131,8 +131,6 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
} else {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
}
} else if ft.Elem().String() == "encoding/json.RawMessage" {
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}

View File

@@ -12,16 +12,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
@@ -30,7 +23,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
@@ -42,12 +34,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
@@ -104,7 +93,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@@ -115,7 +103,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/derp+
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb from tailscale.com/cmd/derper
@@ -143,9 +130,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
@@ -169,7 +155,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/types/views
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -195,6 +180,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from internal/profile+
L compress/zlib from debug/elf
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -218,6 +204,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
crypto/tls from golang.org/x/crypto/acme+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -233,6 +221,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
L hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem

View File

@@ -182,9 +182,8 @@ func main() {
}
mux.HandleFunc("/derp/probe", probeHandler)
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
io.WriteString(w, `<html><body>
@@ -204,7 +203,6 @@ func main() {
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
io.WriteString(w, "User-agent: *\nDisallow: /\n")
}))
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent))
@@ -279,6 +277,18 @@ func main() {
defer tlsActiveVersion.Add(label, -1)
}
// Set HTTP headers to appease automated security scanners.
//
// Security automation gets cranky when HTTPS sites don't
// set HSTS, and when they don't specify a content
// security policy for XSS mitigation.
//
// DERP's HTTP interface is only ever used for debug
// access (for which trivial safe policies work just
// fine), and by DERP clients which don't obey any of
// these browser-centric headers anyway.
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
if *httpPort > -1 {

4
cmd/dist/dist.go vendored
View File

@@ -19,10 +19,10 @@ import (
var synologyPackageCenter bool
func getTargets(signers unixpkgs.Signers) ([]dist.Target, error) {
func getTargets() ([]dist.Target, error) {
var ret []dist.Target
ret = append(ret, unixpkgs.Targets(signers)...)
ret = append(ret, unixpkgs.Targets()...)
// Synology packages can be built either for sideloading, or for
// distribution by Synology in their package center. When
// distributed through the package center, apps can request

View File

@@ -23,7 +23,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/tailscale/hujson"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/util/httpm"
)
@@ -271,7 +270,7 @@ func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, poli
got := resp.StatusCode
want := http.StatusOK
if got != want {
var ate ACLGitopsTestError
var ate ACLTestError
err := json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
@@ -307,7 +306,7 @@ func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, poli
}
defer resp.Body.Close()
var ate ACLGitopsTestError
var ate ACLTestError
err = json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
@@ -328,12 +327,12 @@ func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, poli
var lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)
// ACLGitopsTestError is redefined here so we can add a custom .Error() response
type ACLGitopsTestError struct {
tailscale.ACLTestError
type ACLTestError struct {
Message string `json:"message"`
Data []ACLTestErrorDetail `json:"data"`
}
func (ate ACLGitopsTestError) Error() string {
func (ate ACLTestError) Error() string {
var sb strings.Builder
if *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {
@@ -350,28 +349,20 @@ func (ate ACLGitopsTestError) Error() string {
fmt.Fprintln(&sb)
for _, data := range ate.Data {
if data.User != "" {
fmt.Fprintf(&sb, "For user %s:\n", data.User)
}
if len(data.Errors) > 0 {
fmt.Fprint(&sb, "Errors found:\n")
for _, err := range data.Errors {
fmt.Fprintf(&sb, "- %s\n", err)
}
}
if len(data.Warnings) > 0 {
fmt.Fprint(&sb, "Warnings found:\n")
for _, err := range data.Warnings {
fmt.Fprintf(&sb, "- %s\n", err)
}
fmt.Fprintf(&sb, "For user %s:\n", data.User)
for _, err := range data.Errors {
fmt.Fprintf(&sb, "- %s\n", err)
}
}
return sb.String()
}
type ACLTestErrorDetail struct {
User string `json:"user"`
Errors []string `json:"errors"`
}
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
if err != nil {

View File

@@ -1,55 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/json"
"strings"
"testing"
"tailscale.com/client/tailscale"
)
func TestEmbeddedTypeUnmarshal(t *testing.T) {
var gitopsErr ACLGitopsTestError
gitopsErr.Message = "gitops response error"
gitopsErr.Data = []tailscale.ACLTestFailureSummary{
{
User: "GitopsError",
Errors: []string{"this was initially created as a gitops error"},
},
}
var aclTestErr tailscale.ACLTestError
aclTestErr.Message = "native ACL response error"
aclTestErr.Data = []tailscale.ACLTestFailureSummary{
{
User: "ACLError",
Errors: []string{"this was initially created as an ACL error"},
},
}
t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
b, _ := json.Marshal(aclTestErr)
var e ACLGitopsTestError
err := json.Unmarshal(b, &e)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(e.Error(), "For user ACLError") { // the gitops error prints out the user, the acl error doesn't
t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
}
})
t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
b, _ := json.Marshal(gitopsErr)
var e tailscale.ACLTestError
err := json.Unmarshal(b, &e)
if err != nil {
t.Fatal(err)
}
expectedErr := `Status: 0, Message: "gitops response error", Data: [{User:GitopsError Errors:[this was initially created as a gitops error] Warnings:[]}]`
if e.Error() != expectedErr {
t.Fatalf("got %v\n, expected %v", e.Error(), expectedErr)
}
})
}

View File

@@ -11,40 +11,35 @@ import (
"os"
"strings"
"github.com/goreleaser/nfpm/v2"
_ "github.com/goreleaser/nfpm/v2/deb"
"github.com/goreleaser/nfpm/v2/files"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/goreleaser/nfpm"
_ "github.com/goreleaser/nfpm/deb"
_ "github.com/goreleaser/nfpm/rpm"
)
// parseFiles parses a comma-separated list of colon-separated pairs
// into files.Contents format.
func parseFiles(s string, typ string) (files.Contents, error) {
// into a map of filePathOnDisk -> filePathInPackage.
func parseFiles(s string) (map[string]string, error) {
ret := map[string]string{}
if len(s) == 0 {
return nil, nil
return ret, nil
}
var contents files.Contents
for _, f := range strings.Split(s, ",") {
fs := strings.Split(f, ":")
if len(fs) != 2 {
return nil, fmt.Errorf("unparseable file field %q", f)
}
contents = append(contents, &files.Content{Type: files.TypeFile, Source: fs[0], Destination: fs[1]})
ret[fs[0]] = fs[1]
}
return contents, nil
return ret, nil
}
func parseEmptyDirs(s string) files.Contents {
func parseEmptyDirs(s string) []string {
// strings.Split("", ",") would return []string{""}, which is not suitable:
// this would create an empty dir record with path "", breaking the package
if s == "" {
return nil
}
var contents files.Contents
for _, d := range strings.Split(s, ",") {
contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
}
return contents
return strings.Split(s, ",")
}
func main() {
@@ -53,7 +48,7 @@ func main() {
description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
goarch := flag.String("arch", "amd64", "GOARCH this package is for")
pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
regularFiles := flag.String("files", "", "comma-separated list of files in src:dst form")
files := flag.String("files", "", "comma-separated list of files in src:dst form")
configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
version := flag.String("version", "0.0.0", "version of the package")
@@ -65,20 +60,15 @@ func main() {
recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
flag.Parse()
filesList, err := parseFiles(*regularFiles, files.TypeFile)
filesMap, err := parseFiles(*files)
if err != nil {
log.Fatalf("Parsing --files: %v", err)
}
configsList, err := parseFiles(*configFiles, files.TypeConfig)
configsMap, err := parseFiles(*configFiles)
if err != nil {
log.Fatalf("Parsing --configs: %v", err)
}
emptyDirList := parseEmptyDirs(*emptyDirs)
contents := append(filesList, append(configsList, emptyDirList...)...)
contents, err = files.PrepareForPackager(contents, 0, *pkgType, false)
if err != nil {
log.Fatalf("Building package contents: %v", err)
}
info := nfpm.WithDefaults(&nfpm.Info{
Name: *name,
Arch: *goarch,
@@ -89,7 +79,9 @@ func main() {
Homepage: "https://www.tailscale.com",
License: "MIT",
Overridables: nfpm.Overridables{
Contents: contents,
EmptyFolders: emptyDirList,
Files: filesMap,
ConfigFiles: configsMap,
Scripts: nfpm.Scripts{
PostInstall: *postinst,
PreRemove: *prerm,

View File

@@ -45,7 +45,6 @@ import (
"golang.org/x/exp/slices"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/cmpx"
"tailscale.com/util/must"
)
@@ -152,10 +151,10 @@ func printMessage(msg message) {
if len(traffic) == 0 {
return
}
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) int {
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) bool {
nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes
ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes
return cmpx.Compare(ny, nx)
return nx > ny
})
var sum netlogtype.Counts
for _, cc := range traffic {

View File

@@ -22,25 +22,15 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tsnet"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
)
var (
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
wgPort = flag.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
)
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
var (
numSessions = clientmetric.NewCounter("sniproxy_sessions")
numBadAddrPort = clientmetric.NewCounter("sniproxy_bad_addrport")
dnsResponses = clientmetric.NewCounter("sniproxy_dns_responses")
dnsFailures = clientmetric.NewCounter("sniproxy_dns_failed")
httpPromoted = clientmetric.NewCounter("sniproxy_http_promoted")
)
func main() {
flag.Parse()
if *ports == "" {
@@ -50,7 +40,6 @@ func main() {
hostinfo.SetApp("sniproxy")
var s server
s.ts.Port = uint16(*wgPort)
defer s.ts.Close()
lc, err := s.ts.LocalClient()
@@ -118,7 +107,6 @@ func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
n, err := c.Read(buf)
if err != nil {
log.Printf("c.Read failed: %v\n ", err)
dnsFailures.Add(1)
return
}
@@ -126,25 +114,20 @@ func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
err = msg.Unpack(buf[:n])
if err != nil {
log.Printf("dnsmessage unpack failed: %v\n ", err)
dnsFailures.Add(1)
return
}
buf, err = s.dnsResponse(&msg)
if err != nil {
log.Printf("s.dnsResponse failed: %v\n", err)
dnsFailures.Add(1)
return
}
_, err = c.Write(buf)
if err != nil {
log.Printf("c.Write failed: %v\n", err)
dnsFailures.Add(1)
return
}
dnsResponses.Add(1)
}
func (s *server) serveConn(c net.Conn) {
@@ -152,7 +135,6 @@ func (s *server) serveConn(c net.Conn) {
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("bogus addrPort %q", addrPortStr)
numBadAddrPort.Add(1)
c.Close()
return
}
@@ -165,7 +147,6 @@ func (s *server) serveConn(c net.Conn) {
return netutil.NewOneConnListener(c, nil), nil
}
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
numSessions.Add(1)
return &tcpproxy.DialProxy{
Addr: net.JoinHostPort(sniName, port),
DialContext: dialer.DialContext,
@@ -235,7 +216,6 @@ func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
func (s *server) promoteHTTPS(ln net.Listener) {
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpPromoted.Add(1)
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
}))
log.Fatalf("promoteHTTPS http.Serve: %v", err)

View File

@@ -6,15 +6,33 @@
package cli
import (
"tailscale.com/util/winutil/authenticode"
"unsafe"
"golang.org/x/sys/windows"
)
func init() {
verifyAuthenticode = verifyTailscale
verifyAuthenticode = verifyAuthenticodeWindows
}
const certSubjectTailscale = "Tailscale Inc."
func verifyTailscale(path string) error {
return authenticode.Verify(path, certSubjectTailscale)
func verifyAuthenticodeWindows(path string) error {
path16, err := windows.UTF16PtrFromString(path)
if err != nil {
return err
}
data := &windows.WinTrustData{
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
UIChoice: windows.WTD_UI_NONE,
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
UnionChoice: windows.WTD_CHOICE_FILE,
StateAction: windows.WTD_STATEACTION_VERIFY,
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
FilePath: path16,
}),
}
err = windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
data.StateAction = windows.WTD_STATEACTION_CLOSE
windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
return err
}

View File

@@ -129,12 +129,16 @@ change in the future.
certCmd,
netlockCmd,
licensesCmd,
exitNodeCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
}
for _, c := range rootCmd.Subcommands {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands,
idTokenCmd,
@@ -152,12 +156,6 @@ change in the future.
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
for _, c := range rootCmd.Subcommands {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil

View File

@@ -1,248 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"errors"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
)
var exitNodeCmd = &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
Subcommands: []*ffcli.Command{
{
Name: "list",
ShortUsage: "exit-node list [flags]",
ShortHelp: "Show exit nodes",
Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("list")
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
return fs
})(),
},
},
Exec: func(context.Context, []string) error {
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
},
}
var exitNodeArgs struct {
filter string
}
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
// If the exit node has location and priority data, only the highest
// priority node for each city location is shown to the user.
// If the country location has more than one city, an 'Any' city
// is returned for the country, which lists the highest priority
// node in that country.
// For countries without location data, each exit node is displayed.
func runExitNodeList(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected non-flag arguments to 'tailscale exit-node list'")
}
getStatus := localClient.Status
st, err := getStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
var peers []*ipnstate.PeerStatus
for _, ps := range st.Peer {
if !ps.ExitNodeOption {
// We only show location based exit nodes.
continue
}
peers = append(peers, ps)
}
if len(peers) == 0 {
return errors.New("no exit nodes found")
}
filteredPeers := filterFormatAndSortExitNodes(peers, exitNodeArgs.filter)
if len(filteredPeers.Countries) == 0 && exitNodeArgs.filter != "" {
return fmt.Errorf("no exit nodes found for %q", exitNodeArgs.filter)
}
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
defer w.Flush()
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", "IP", "HOSTNAME", "COUNTRY", "CITY", "STATUS")
for _, country := range filteredPeers.Countries {
for _, city := range country.Cities {
for _, peer := range city.Peers {
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer))
}
}
}
fmt.Fprintln(w)
fmt.Fprintln(w)
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP")
return nil
}
// peerStatus returns a string representing the current state of
// a peer. If there is no notable state, a - is returned.
func peerStatus(peer *ipnstate.PeerStatus) string {
if !peer.Active {
if peer.ExitNode {
return "selected but offline"
}
if !peer.Online {
return "offline"
}
}
if peer.ExitNode {
return "selected"
}
return "-"
}
type filteredExitNodes struct {
Countries []*filteredCountry
}
type filteredCountry struct {
Name string
Cities []*filteredCity
}
type filteredCity struct {
Name string
Peers []*ipnstate.PeerStatus
}
const noLocationData = "-"
// filterFormatAndSortExitNodes filters and sorts exit nodes into
// alphabetical order, by country, city and then by priority if
// present.
// If an exit node has location data, and the country has more than
// once city, an `Any` city is added to the country that contains the
// highest priority exit node within that country.
// For exit nodes without location data, their country fields are
// defined as '-' to indicate that the data is not available.
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
countries := make(map[string]*filteredCountry)
cities := make(map[string]*filteredCity)
for _, ps := range peers {
if ps.Location == nil {
ps.Location = &tailcfg.Location{
Country: noLocationData,
CountryCode: noLocationData,
City: noLocationData,
CityCode: noLocationData,
}
}
if filterBy != "" && ps.Location.Country != filterBy {
continue
}
co, coOK := countries[ps.Location.CountryCode]
if !coOK {
co = &filteredCountry{
Name: ps.Location.Country,
}
countries[ps.Location.CountryCode] = co
}
ci, ciOK := cities[ps.Location.CityCode]
if !ciOK {
ci = &filteredCity{
Name: ps.Location.City,
}
cities[ps.Location.CityCode] = ci
co.Cities = append(co.Cities, ci)
}
ci.Peers = append(ci.Peers, ps)
}
filteredExitNodes := filteredExitNodes{
Countries: maps.Values(countries),
}
for _, country := range filteredExitNodes.Countries {
if country.Name == noLocationData {
// Countries without location data should not
// be filtered further.
continue
}
var countryANYPeer []*ipnstate.PeerStatus
for _, city := range country.Cities {
sortPeersByPriority(city.Peers)
countryANYPeer = append(countryANYPeer, city.Peers...)
var reducedCityPeers []*ipnstate.PeerStatus
for i, peer := range city.Peers {
if i == 0 || peer.ExitNode {
// We only return the highest priority peer and any peer that
// is currently the active exit node.
reducedCityPeers = append(reducedCityPeers, peer)
}
}
city.Peers = reducedCityPeers
}
sortByCityName(country.Cities)
sortPeersByPriority(countryANYPeer)
if len(country.Cities) > 1 {
// For countries with more than one city, we want to return the
// option of the best peer for that country.
country.Cities = append([]*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{countryANYPeer[0]},
},
}, country.Cities...)
}
}
sortByCountryName(filteredExitNodes.Countries)
return filteredExitNodes
}
// sortPeersByPriority sorts a slice of PeerStatus
// by location.Priority, in order of highest priority.
func sortPeersByPriority(peers []*ipnstate.PeerStatus) {
slices.SortStableFunc(peers, func(a, b *ipnstate.PeerStatus) int {
return cmpx.Compare(b.Location.Priority, a.Location.Priority)
})
}
// sortByCityName sorts a slice of filteredCity alphabetically
// by name. The '-' used to indicate no location data will always
// be sorted to the front of the slice.
func sortByCityName(cities []*filteredCity) {
slices.SortStableFunc(cities, func(a, b *filteredCity) int { return strings.Compare(a.Name, b.Name) })
}
// sortByCountryName sorts a slice of filteredCountry alphabetically
// by name. The '-' used to indicate no location data will always
// be sorted to the front of the slice.
func sortByCountryName(countries []*filteredCountry) {
slices.SortStableFunc(countries, func(a, b *filteredCountry) int { return strings.Compare(a.Name, b.Name) })
}

View File

@@ -1,308 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func TestFilterFormatAndSortExitNodes(t *testing.T) {
t.Run("without filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "everest-1",
Location: &tailcfg.Location{
Country: "Everest",
CountryCode: "evr",
City: "Hillary",
CityCode: "hil",
Priority: 100,
},
},
{
HostName: "lhotse-1",
Location: &tailcfg.Location{
Country: "Lhotse",
CountryCode: "lho",
City: "Fritz",
CityCode: "fri",
Priority: 200,
},
},
{
HostName: "lhotse-2",
Location: &tailcfg.Location{
Country: "Lhotse",
CountryCode: "lho",
City: "Fritz",
CityCode: "fri",
Priority: 100,
},
},
{
HostName: "nuptse-1",
Location: &tailcfg.Location{
Country: "Nuptse",
CountryCode: "nup",
City: "Walmsley",
CityCode: "wal",
Priority: 200,
},
},
{
HostName: "nuptse-2",
Location: &tailcfg.Location{
Country: "Nuptse",
CountryCode: "nup",
City: "Bonington",
CityCode: "bon",
Priority: 10,
},
},
{
HostName: "Makalu",
},
}
want := filteredExitNodes{
Countries: []*filteredCountry{
{
Name: noLocationData,
Cities: []*filteredCity{
{
Name: noLocationData,
Peers: []*ipnstate.PeerStatus{
ps[5],
},
},
},
},
{
Name: "Everest",
Cities: []*filteredCity{
{
Name: "Hillary",
Peers: []*ipnstate.PeerStatus{
ps[0],
},
},
},
},
{
Name: "Lhotse",
Cities: []*filteredCity{
{
Name: "Fritz",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
},
},
{
Name: "Nuptse",
Cities: []*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{
ps[3],
},
},
{
Name: "Bonington",
Peers: []*ipnstate.PeerStatus{
ps[4],
},
},
{
Name: "Walmsley",
Peers: []*ipnstate.PeerStatus{
ps[3],
},
},
},
},
},
}
result := filterFormatAndSortExitNodes(ps, "")
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
t.Fatalf(res)
}
})
t.Run("with country filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "baker-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Baker",
CityCode: "col",
Priority: 100,
},
},
{
HostName: "hood-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Hood",
CityCode: "hoo",
Priority: 500,
},
},
{
HostName: "rainier-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Rainier",
CityCode: "rai",
Priority: 100,
},
},
{
HostName: "rainier-2",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Rainier",
CityCode: "rai",
Priority: 10,
},
},
{
HostName: "mitchell-1",
Location: &tailcfg.Location{
Country: "Atlantic",
CountryCode: "atl",
City: "Mitchell",
CityCode: "mit",
Priority: 200,
},
},
}
want := filteredExitNodes{
Countries: []*filteredCountry{
{
Name: "Pacific",
Cities: []*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
{
Name: "Baker",
Peers: []*ipnstate.PeerStatus{
ps[0],
},
},
{
Name: "Hood",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
{
Name: "Rainier",
Peers: []*ipnstate.PeerStatus{
ps[2],
},
},
},
},
},
}
result := filterFormatAndSortExitNodes(ps, "Pacific")
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
t.Fatalf(res)
}
})
}
func TestSortPeersByPriority(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
Location: &tailcfg.Location{
Priority: 100,
},
},
{
Location: &tailcfg.Location{
Priority: 200,
},
},
{
Location: &tailcfg.Location{
Priority: 300,
},
},
}
sortPeersByPriority(ps)
if ps[0].Location.Priority != 300 {
t.Fatalf("sortPeersByPriority did not order PeerStatus with highest priority as index 0, got %v, want %v", ps[0].Location.Priority, 300)
}
}
func TestSortByCountryName(t *testing.T) {
fc := []*filteredCountry{
{
Name: "Albania",
},
{
Name: "Sweden",
},
{
Name: "Zimbabwe",
},
{
Name: noLocationData,
},
}
sortByCountryName(fc)
if fc[0].Name != noLocationData {
t.Fatalf("sortByCountryName did not order countries by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
}
}
func TestSortByCityName(t *testing.T) {
fc := []*filteredCity{
{
Name: "Kingston",
},
{
Name: "Goteborg",
},
{
Name: "Squamish",
},
{
Name: noLocationData,
},
}
sortByCityName(fc)
if fc[0].Name != noLocationData {
t.Fatalf("sortByCityName did not order cities by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
}
}

View File

@@ -23,7 +23,6 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
var netlockCmd = &ffcli.Command{
@@ -41,7 +40,6 @@ var netlockCmd = &ffcli.Command{
nlDisablementKDFCmd,
nlLogCmd,
nlLocalDisableCmd,
nlRevokeKeysCmd,
},
Exec: runNetworkLockNoSubcommand,
}
@@ -467,16 +465,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
}
}
err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
// Provide a better help message for when someone clicks through the signing flow
// on the wrong device.
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
fmt.Fprintln(os.Stderr)
}
return err
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
}
var nlDisableCmd = &ffcli.Command{
@@ -713,114 +702,3 @@ func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) er
fmt.Println(wrapped)
return nil
}
var nlRevokeKeysArgs struct {
cosign bool
finish bool
forkFrom string
}
var nlRevokeKeysCmd = &ffcli.Command{
Name: "revoke-keys",
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortHelp: "Revoke compromised tailnet-lock keys",
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).
Revoked keys are prevented from being used in the future. Any nodes previously signed
by revoked keys lose their authorization and must be signed again.
Revocation is a multi-step process that requires several signing nodes to ` + "`--cosign`" + ` the revocation. Use ` + "`tailscale lock remove`" + ` instead if the key has not been compromised.
1. To start, run ` + "`tailscale revoke-keys <tlpub-keys>`" + ` with the tailnet lock keys to revoke.
2. Re-run the ` + "`--cosign`" + ` command output by ` + "`revoke-keys`" + ` on other signing nodes. Use the
most recent command output on the next signing node in sequence.
3. Once the number of ` + "`--cosign`" + `s is greater than the number of keys being revoked,
run the command one final time with ` + "`--finish`" + ` instead of ` + "`--cosign`" + `.`,
Exec: runNetworkLockRevokeKeys,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock revoke-keys")
fs.BoolVar(&nlRevokeKeysArgs.cosign, "cosign", false, "continue generating the recovery using the tailnet lock key on this device and the provided recovery blob")
fs.BoolVar(&nlRevokeKeysArgs.finish, "finish", false, "finish the recovery process by transmitting the revocation")
fs.StringVar(&nlRevokeKeysArgs.forkFrom, "fork-from", "", "parent AUM hash to rewrite from (advanced users only)")
return fs
})(),
}
func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
// First step in the process
if !nlRevokeKeysArgs.cosign && !nlRevokeKeysArgs.finish {
removeKeys, _, err := parseNLArgs(args, true, false)
if err != nil {
return err
}
keyIDs := make([]tkatype.KeyID, len(removeKeys))
for i, k := range removeKeys {
keyIDs[i], err = k.ID()
if err != nil {
return fmt.Errorf("generating keyID: %v", err)
}
}
var forkFrom tka.AUMHash
if nlRevokeKeysArgs.forkFrom != "" {
if len(nlRevokeKeysArgs.forkFrom) == (len(forkFrom) * 2) {
// Hex-encoded: like the output of the lock log command.
b, err := hex.DecodeString(nlRevokeKeysArgs.forkFrom)
if err != nil {
return fmt.Errorf("invalid fork-from hash: %v", err)
}
copy(forkFrom[:], b)
} else {
if err := forkFrom.UnmarshalText([]byte(nlRevokeKeysArgs.forkFrom)); err != nil {
return fmt.Errorf("invalid fork-from hash: %v", err)
}
}
}
aumBytes, err := localClient.NetworkLockGenRecoveryAUM(ctx, keyIDs, forkFrom)
if err != nil {
return fmt.Errorf("generation of recovery AUM failed: %w", err)
}
fmt.Printf(`Run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
`, os.Args[0], aumBytes)
return nil
}
// If we got this far, we need to co-sign the AUM and/or transmit it for distribution.
b, err := hex.DecodeString(args[0])
if err != nil {
return fmt.Errorf("parsing hex: %v", err)
}
var recoveryAUM tka.AUM
if err := recoveryAUM.Unserialize(b); err != nil {
return fmt.Errorf("decoding recovery AUM: %v", err)
}
if nlRevokeKeysArgs.cosign {
aumBytes, err := localClient.NetworkLockCosignRecoveryAUM(ctx, recoveryAUM)
if err != nil {
return fmt.Errorf("co-signing recovery AUM failed: %w", err)
}
fmt.Printf(`Co-signing completed successfully.
To accumulate an additional signature, run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
Alternatively if you are done with co-signing, complete recovery by running the following command:
%s lock recover-compromised-key --finish %X
`, os.Args[0], aumBytes, os.Args[0], aumBytes)
}
if nlRevokeKeysArgs.finish {
if err := localClient.NetworkLockSubmitRecoveryAUM(ctx, recoveryAUM); err != nil {
return fmt.Errorf("submitting recovery AUM failed: %w", err)
}
fmt.Println("Recovery completed.")
}
return nil
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -129,7 +128,6 @@ type localServeClient interface {
Status(context.Context) (*ipnstate.Status, error)
GetServeConfig(context.Context) (*ipn.ServeConfig, error)
SetServeConfig(context.Context, *ipn.ServeConfig) error
QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error)
}
// serveEnv is the environment the serve command runs within. All I/O should be
@@ -701,7 +699,7 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
portPart = ""
}
if scheme == "http" {
hostname, _, _ := strings.Cut(host, ".")
hostname, _, _ := strings.Cut("host", ".")
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
}
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)

View File

@@ -782,10 +782,6 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
return nil
}
func (lc *fakeLocalServeClient) QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error) {
return nil, nil
}
// exactError returns an error checker that wants exactly the provided want error.
// If optName is non-empty, it's used in the error message.
func exactErr(want error, optName ...string) func(error) string {

View File

@@ -200,8 +200,6 @@ func runStatus(ctx context.Context, args []string) error {
if statusArgs.self && st.Self != nil {
printPS(st.Self)
}
locBasedExitNode := false
if statusArgs.peers {
var peers []*ipnstate.PeerStatus
for _, peer := range st.Peers() {
@@ -209,12 +207,6 @@ func runStatus(ctx context.Context, args []string) error {
if ps.ShareeNode {
continue
}
if ps.Location != nil && ps.ExitNodeOption && !ps.ExitNode {
// Location based exit nodes are only shown with the
// `exit-node list` command.
locBasedExitNode = true
continue
}
peers = append(peers, ps)
}
ipnstate.SortPeers(peers)
@@ -226,10 +218,6 @@ func runStatus(ctx context.Context, args []string) error {
}
}
Stdout.Write(buf.Bytes())
if locBasedExitNode {
println()
println("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
}
if len(st.Health) > 0 {
outln()
printHealth()

View File

@@ -30,6 +30,7 @@ import (
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -725,8 +726,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
// the health check, rather than just a string.
func upWorthyWarning(s string) bool {
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
strings.Contains(s, healthmsg.LockedOut)
strings.Contains(s, healthmsg.WarnAcceptRoutesOff)
}
func checkUpWarnings(ctx context.Context) {
@@ -1132,6 +1132,9 @@ func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
if !strings.HasPrefix(v, "tskey-client-") {
return v, nil
}
if !envknob.Bool("TS_EXPERIMENT_OAUTH_AUTHKEY") {
return "", errors.New("oauth authkeys are in experimental status")
}
if tags == "" {
return "", errors.New("oauth authkeys require --advertise-tags")
}

View File

@@ -44,27 +44,17 @@ var updateCmd = &ffcli.Command{
fs := newFlagSet("update")
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts")
fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts")
fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)")
// These flags are not supported on several systems that only provide
// the latest version of Tailscale:
//
// - Arch (and other pacman-based distros)
// - Alpine (and other apk-based distros)
// - FreeBSD (and other pkg-based distros)
if distro.Get() != distro.Arch && distro.Get() != distro.Alpine && runtime.GOOS != "freebsd" {
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
}
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
return fs
})(),
}
var updateArgs struct {
yes bool
dryRun bool
appStore bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
yes bool
dryRun bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
}
// winMSIEnv is the environment variable that, if set, is the MSI file for the
@@ -147,37 +137,16 @@ func newUpdater() (*updater, error) {
up.update = up.updateSynology
case distro.Debian: // includes Ubuntu
up.update = up.updateDebLike
case distro.Arch:
up.update = up.updateArchLike
case distro.Alpine:
up.update = up.updateAlpineLike
}
// TODO(awly): add support for Alpine
switch {
case haveExecutable("pacman"):
up.update = up.updateArchLike
case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
// The distro.Debian switch case above should catch most apt-based
// systems, but add this fallback just in case.
up.update = up.updateDebLike
case haveExecutable("dnf"):
up.update = up.updateFedoraLike("dnf")
case haveExecutable("yum"):
up.update = up.updateFedoraLike("yum")
case haveExecutable("apk"):
up.update = up.updateAlpineLike
}
case "darwin":
switch {
case !updateArgs.appStore && !version.IsSandboxedMacOS():
case !version.IsSandboxedMacOS():
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
case !updateArgs.appStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys
default:
up.update = up.updateMacAppStore
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
}
case "freebsd":
up.update = up.updateFreeBSD
}
if up.update == nil {
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
@@ -202,8 +171,6 @@ func (up *updater) currentOrDryRun(ver string) bool {
return false
}
var errUserAborted = errors.New("aborting update")
func (up *updater) confirm(ver string) error {
if updateArgs.yes {
log.Printf("Updating Tailscale from %v to %v; --yes given, continuing without prompts.\n", version.Short(), ver)
@@ -218,7 +185,7 @@ func (up *updater) confirm(ver string) error {
case "y", "yes", "sure":
return nil
}
return errUserAborted
return errors.New("aborting update")
}
func (up *updater) updateSynology() error {
@@ -230,22 +197,48 @@ func (up *updater) updateSynology() error {
}
func (up *updater) updateDebLike() error {
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
ver := updateArgs.version
if ver == "" {
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
if err != nil {
return err
}
var latest struct {
Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz"
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
f, ok := latest.Tarballs[runtime.GOARCH]
if !ok {
return fmt.Errorf("can't update architecture %q", runtime.GOARCH)
}
ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_")
if !ok {
return fmt.Errorf("can't parse version from %q", f)
}
}
if up.currentOrDryRun(ver) {
return nil
}
if err := requireRoot(); err != nil {
return err
track := "unstable"
if stable, ok := versionIsStable(ver); !ok {
return fmt.Errorf("malformed version %q", ver)
} else if stable {
track = "stable"
}
if updated, err := updateDebianAptSourcesList(up.track); err != nil {
if os.Geteuid() != 0 {
return errors.New("must be root; use sudo")
}
if updated, err := updateDebianAptSourcesList(track); err != nil {
return err
} else if updated {
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, up.track)
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, track)
}
cmd := exec.Command("apt-get", "update",
@@ -331,204 +324,6 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
return buf.Bytes(), nil
}
func (up *updater) updateArchLike() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "pacman --sync --refresh tailscale"`, err)
}
}()
out, err := exec.Command("pacman", "--sync", "--refresh", "--info", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking pacman for latest tailscale version: %w, output: %q", err, out)
}
ver, err := parsePacmanVersion(out)
if err != nil {
return err
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("pacman", "--sync", "--noconfirm", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pacman: %w", err)
}
return nil
}
func parsePacmanVersion(out []byte) (string, error) {
for _, line := range strings.Split(string(out), "\n") {
// The line we're looking for looks like this:
// Version : 1.44.2-1
if !strings.HasPrefix(line, "Version") {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
}
ver := strings.TrimSpace(parts[1])
// Trim the Arch patch version.
ver = strings.Split(ver, "-")[0]
if ver == "" {
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
}
return ver, nil
}
return "", fmt.Errorf("could not find latest version of tailscale via pacman")
}
const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
// updateFedoraLike updates tailscale on any distros in the Fedora family,
// specifically anything that uses "dnf" or "yum" package managers. The actual
// package manager is passed via packageManager.
func (up *updater) updateFedoraLike(packageManager string) func() error {
return func() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "%s upgrade tailscale"`, err, packageManager)
}
}()
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.track); err != nil {
return err
} else if updated {
fmt.Printf("Updated %s to use the %s track\n", yumRepoConfigFile, up.track)
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
}
// updateYUMRepoTrack updates the repoFile file to make sure it has the
// provided track (stable or unstable) in it.
func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
was, err := os.ReadFile(repoFile)
if err != nil {
return false, err
}
urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
s := bufio.NewScanner(bytes.NewReader(was))
newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
for s.Scan() {
line := s.Text()
// Handle repo section name, like "[tailscale-stable]".
if len(line) > 0 && line[0] == '[' {
if !strings.HasPrefix(line, "[tailscale-") {
return false, fmt.Errorf("%q does not look like a tailscale repo file, it contains an unexpected %q section", repoFile, line)
}
fmt.Fprintf(newContent, "[tailscale-%s]\n", dstTrack)
continue
}
// Update the track mentioned in repo name.
if strings.HasPrefix(line, "name=") {
fmt.Fprintf(newContent, "name=Tailscale %s\n", dstTrack)
continue
}
// Update the actual repo URLs.
if strings.HasPrefix(line, "baseurl=") || strings.HasPrefix(line, "gpgkey=") {
fmt.Fprintln(newContent, urlRe.ReplaceAllString(line, urlReplacement))
continue
}
fmt.Fprintln(newContent, line)
}
if bytes.Equal(was, newContent.Bytes()) {
return false, nil
}
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
}
func (up *updater) updateAlpineLike() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "apk upgrade tailscale"`, err)
}
}()
out, err := exec.Command("apk", "update").CombinedOutput()
if err != nil {
return fmt.Errorf("failed refresh apk repository indexes: %w, output: %q", err, out)
}
out, err = exec.Command("apk", "info", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking apk for latest tailscale version: %w, output: %q", err, out)
}
ver, err := parseAlpinePackageVersion(out)
if err != nil {
return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("apk", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using apk: %w", err)
}
return nil
}
func parseAlpinePackageVersion(out []byte) (string, error) {
s := bufio.NewScanner(bytes.NewReader(out))
for s.Scan() {
// The line should look like this:
// tailscale-1.44.2-r0 description:
line := strings.TrimSpace(s.Text())
if !strings.HasPrefix(line, "tailscale-") {
continue
}
parts := strings.SplitN(line, "-", 3)
if len(parts) < 3 {
return "", fmt.Errorf("malformed info line: %q", line)
}
return parts[1], nil
}
return "", errors.New("tailscale version not found in output")
}
func (up *updater) updateMacSys() error {
// use sparkle? do we have permissions from this context? does sudo help?
// We can at least fail with a command they can run to update from the shell.
@@ -538,68 +333,30 @@ func (up *updater) updateMacSys() error {
return errors.New("The 'update' command is not yet implemented on macOS.")
}
func (up *updater) updateMacAppStore() error {
out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput()
if err != nil {
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out))
}
const on = "1\n"
if string(out) != on {
fmt.Fprintln(os.Stderr, "NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for update).")
}
out, err = exec.Command("softwareupdate", "--list").CombinedOutput()
if err != nil {
return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out))
}
newTailscale := parseSoftwareupdateList(out)
if newTailscale == "" {
fmt.Println("no Tailscale update available")
return nil
}
newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-")
if up.currentOrDryRun(newTailscaleVer) {
return nil
}
if err := up.confirm(newTailscaleVer); err != nil {
return err
}
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
return nil
}
var macOSAppStoreListPattern = regexp.MustCompile(`(?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+)`)
// parseSoftwareupdateList searches the output of `softwareupdate --list` on
// Darwin and returns the matching Tailscale package label. If there is none,
// returns the empty string.
//
// See TestParseSoftwareupdateList for example inputs.
func parseSoftwareupdateList(stdout []byte) string {
matches := macOSAppStoreListPattern.FindSubmatch(stdout)
if len(matches) < 2 {
return ""
}
return string(matches[1])
}
var (
verifyAuthenticode func(string) error // or nil on non-Windows
markTempFileFunc func(string) error // or nil on non-Windows
)
func (up *updater) updateWindows() error {
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
ver := updateArgs.version
if ver == "" {
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
if err != nil {
return err
}
var latest struct {
Version string
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
ver = latest.Version
if ver == "" {
return errors.New("no version found")
}
}
arch := runtime.GOARCH
if arch == "386" {
@@ -828,85 +585,3 @@ func (pw *progressWriter) print() {
pw.lastPrint = time.Now()
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
}
func (up *updater) updateFreeBSD() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "pkg upgrade tailscale"`, err)
}
}()
out, err := exec.Command("pkg", "update").CombinedOutput()
if err != nil {
return fmt.Errorf("failed refresh pkg repository indexes: %w, output: %q", err, out)
}
out, err = exec.Command("pkg", "rquery", "%v", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking pkg for latest tailscale version: %w, output: %q", err, out)
}
ver := string(bytes.TrimSpace(out))
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("pkg", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pkg: %w", err)
}
return nil
}
func haveExecutable(name string) bool {
path, err := exec.LookPath(name)
return err == nil && path != ""
}
func requestedTailscaleVersion(ver, track string) (string, error) {
if ver != "" {
return ver, nil
}
return latestTailscaleVersion(track)
}
func latestTailscaleVersion(track string) (string, error) {
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
res, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("fetching latest tailscale version: %w", err)
}
var latest struct {
Version string
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return "", fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
if latest.Version == "" {
return "", fmt.Errorf("no version found at %q", url)
}
return latest.Version, nil
}
func requireRoot() error {
if os.Geteuid() == 0 {
return nil
}
switch runtime.GOOS {
case "linux":
return errors.New("must be root; use sudo")
case "freebsd", "openbsd":
return errors.New("must be root; use doas")
default:
return errors.New("must be root")
}
}

View File

@@ -3,11 +3,7 @@
package cli
import (
"os"
"path/filepath"
"testing"
)
import "testing"
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
tests := []struct {
@@ -77,366 +73,3 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
})
}
}
func TestParseSoftwareupdateList(t *testing.T) {
tests := []struct {
name string
input []byte
want string
}{
{
name: "update-at-end-of-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
* Label: Tailscale-1.23.4
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
`),
want: "Tailscale-1.23.4",
},
{
name: "update-in-middle-of-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: Tailscale-1.23.5000
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
`),
want: "Tailscale-1.23.5000",
},
{
name: "update-not-in-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
`),
want: "",
},
{
name: "decoy-in-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: Malware-1.0
Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
`),
want: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := parseSoftwareupdateList(test.input)
if test.want != got {
t.Fatalf("got %q, want %q", got, test.want)
}
})
}
}
func TestParsePacmanVersion(t *testing.T) {
tests := []struct {
desc string
out string
want string
wantErr bool
}{
{
desc: "valid version",
out: `
:: Synchronizing package databases...
endeavouros is up to date
core is up to date
extra is up to date
multilib is up to date
Repository : extra
Name : tailscale
Version : 1.44.2-1
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
Architecture : x86_64
URL : https://tailscale.com
Licenses : MIT
Groups : None
Provides : None
Depends On : glibc
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 7.98 MiB
Installed Size : 32.47 MiB
Packager : Christian Heusel <gromit@archlinux.org>
Build Date : Tue 18 Jul 2023 12:28:37 PM PDT
Validated By : MD5 Sum SHA-256 Sum Signature
`,
want: "1.44.2",
},
{
desc: "version without Arch patch number",
out: `
... snip ...
Name : tailscale
Version : 1.44.2
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
want: "1.44.2",
},
{
desc: "missing version",
out: `
... snip ...
Name : tailscale
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
wantErr: true,
},
{
desc: "empty version",
out: `
... snip ...
Name : tailscale
Version :
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
wantErr: true,
},
{
desc: "empty input",
out: "",
wantErr: true,
},
{
desc: "sneaky version in description",
out: `
... snip ...
Name : tailscale
Description : A mesh VPN that makes it easy to connect your devices, wherever they are. Version : 1.2.3
Version : 1.44.2
... snip ...
`,
want: "1.44.2",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := parsePacmanVersion([]byte(tt.out))
if err == nil && tt.wantErr {
t.Fatalf("got nil error and version %q, want non-nil error", got)
}
if err != nil && !tt.wantErr {
t.Fatalf("got error: %q, want nil", err)
}
if got != tt.want {
t.Fatalf("got version: %q, want %q", got, tt.want)
}
})
}
}
func TestUpdateYUMRepoTrack(t *testing.T) {
tests := []struct {
desc string
before string
track string
after string
rewrote bool
wantErr bool
}{
{
desc: "same track",
before: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
track: "stable",
after: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
},
{
desc: "change track",
before: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
track: "unstable",
after: `
[tailscale-unstable]
name=Tailscale unstable
baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
`,
rewrote: true,
},
{
desc: "non-tailscale repo file",
before: `
[fedora]
name=Fedora $releasever - $basearch
#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
enabled=1
countme=1
metadata_expire=7d
repo_gpgcheck=0
type=rpm
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=False
`,
track: "stable",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "tailscale.repo")
if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
t.Fatal(err)
}
rewrote, err := updateYUMRepoTrack(path, tt.track)
if err == nil && tt.wantErr {
t.Fatal("got nil error, want non-nil")
}
if err != nil && !tt.wantErr {
t.Fatalf("got error %q, want nil", err)
}
if err != nil {
return
}
if rewrote != tt.rewrote {
t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
}
after, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(after) != tt.after {
t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
}
})
}
}
func TestParseAlpinePackageVersion(t *testing.T) {
tests := []struct {
desc string
out string
want string
wantErr bool
}{
{
desc: "valid version",
out: `
tailscale-1.44.2-r0 description:
The easiest, most secure way to use WireGuard and 2FA
tailscale-1.44.2-r0 webpage:
https://tailscale.com/
tailscale-1.44.2-r0 installed size:
32 MiB
`,
want: "1.44.2",
},
{
desc: "wrong package output",
out: `
busybox-1.36.1-r0 description:
Size optimized toolbox of many common UNIX utilities
busybox-1.36.1-r0 webpage:
https://busybox.net/
busybox-1.36.1-r0 installed size:
924 KiB
`,
wantErr: true,
},
{
desc: "missing version",
out: `
tailscale description:
The easiest, most secure way to use WireGuard and 2FA
tailscale webpage:
https://tailscale.com/
tailscale installed size:
32 MiB
`,
wantErr: true,
},
{
desc: "empty output",
out: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := parseAlpinePackageVersion([]byte(tt.out))
if err == nil && tt.wantErr {
t.Fatalf("got nil error and version %q, want non-nil error", got)
}
if err != nil && !tt.wantErr {
t.Fatalf("got error: %q, want nil", err)
}
if got != tt.want {
t.Fatalf("got version: %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -23,16 +23,14 @@ var versionCmd = &ffcli.Command{
fs := newFlagSet("version")
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format")
fs.BoolVar(&versionArgs.upstream, "upstream", false, "fetch and print the latest upstream release version from pkgs.tailscale.com")
return fs
})(),
Exec: runVersion,
}
var versionArgs struct {
daemon bool // also check local node's daemon version
json bool
upstream bool
daemon bool // also check local node's daemon version
json bool
}
func runVersion(ctx context.Context, args []string) error {
@@ -49,46 +47,21 @@ func runVersion(ctx context.Context, args []string) error {
}
}
var upstreamVer string
if versionArgs.upstream {
track := "stable"
if version.IsUnstableBuild() {
track = "unstable"
}
upstreamVer, err = latestTailscaleVersion(track)
if err != nil {
return err
}
}
if versionArgs.json {
m := version.GetMeta()
if st != nil {
m.DaemonLong = st.Version
}
out := struct {
version.Meta
Upstream string `json:"upstream,omitempty"`
}{
Meta: m,
Upstream: upstreamVer,
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
return e.Encode(out)
return e.Encode(m)
}
if st == nil {
outln(version.String())
if versionArgs.upstream {
printf(" upstream: %s\n", upstreamVer)
}
return nil
}
printf("Client: %s\n", version.String())
printf("Daemon: %s\n", st.Version)
if versionArgs.upstream {
printf("Upstream: %s\n", upstreamVer)
}
return nil
}

View File

@@ -32,7 +32,6 @@ import (
"tailscale.com/util/cmpx"
"tailscale.com/util/groupmember"
"tailscale.com/version/distro"
"tailscale.com/webui"
)
//go:embed web.html
@@ -92,7 +91,6 @@ Tailscale, as opposed to a CLI or a native app.
webf := newFlagSet("web")
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
webf.BoolVar(&webArgs.dev, "dev", false, "run in dev mode")
return webf
})(),
Exec: runWeb,
@@ -101,7 +99,6 @@ Tailscale, as opposed to a CLI or a native app.
var webArgs struct {
listen string
cgi bool
dev bool
}
func tlsConfigFromEnvironment() *tls.Config {
@@ -132,18 +129,8 @@ func runWeb(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
handler := webHandler
if true {
newServer := &webui.Server{
DevMode: webArgs.dev,
}
cleanup := webui.RunJSDevServer()
defer cleanup()
handler = newServer.Handle
}
if webArgs.cgi {
if err := cgi.Serve(http.HandlerFunc(handler)); err != nil {
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
log.Printf("tailscale.cgi: %v", err)
return err
}
@@ -155,14 +142,14 @@ func runWeb(ctx context.Context, args []string) error {
server := &http.Server{
Addr: webArgs.listen,
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(handler),
Handler: http.HandlerFunc(webHandler),
}
log.Printf("web server running on: https://%s", server.Addr)
return server.ListenAndServeTLS("", "")
} else {
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(handler))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
}

View File

@@ -10,17 +10,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil/authenticode
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
@@ -32,9 +23,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
github.com/miekg/dns from tailscale.com/net/dns/recursive
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
@@ -47,14 +36,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/util/linuxfw
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
@@ -68,7 +54,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
💣 tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
@@ -82,7 +68,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
@@ -99,7 +84,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@@ -110,7 +94,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/control/controlhttp+
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/types/dnstype from tailscale.com/tailcfg
@@ -137,7 +120,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/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/net/interfaces+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
@@ -146,7 +128,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/cmd/tailscale/cli
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
@@ -164,8 +145,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/types/views+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -222,12 +202,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+

View File

@@ -75,24 +75,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
@@ -116,10 +109,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
github.com/miekg/dns from tailscale.com/net/dns/recursive
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
L github.com/pierrec/lz4/v4 from github.com/u-root/uio/uio
L github.com/pierrec/lz4/v4/internal/lz4block from github.com/pierrec/lz4/v4+
@@ -130,7 +121,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
@@ -252,7 +242,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
@@ -275,7 +264,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
@@ -329,11 +317,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
tailscale.com/util/racebuild from tailscale.com/logpolicy
@@ -345,7 +331,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
@@ -362,6 +347,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine
tailscale.com/wgengine/wglog from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/acme from tailscale.com/ipn/ipnlocal
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
@@ -379,7 +365,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/wgengine+
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -412,7 +398,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
W compress/zlib from debug/pe
L compress/zlib from debug/elf
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
container/list from crypto/tls+
context from crypto/tls+
@@ -437,12 +423,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from tailscale.com+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+

View File

@@ -342,7 +342,7 @@ func run() error {
}
sys.Set(netMon)
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
pol := logpolicy.New(logtail.CollectionNode, netMon)
pol.SetVerbosityLevel(args.verbose)
logPol = pol
defer func() {

View File

@@ -3,32 +3,10 @@
package main // import "tailscale.com/cmd/tailscaled"
import (
"testing"
"tailscale.com/tstest/deptest"
)
import "testing"
func TestNothing(t *testing.T) {
// This test does nothing on purpose, so we can run
// GODEBUG=memprofilerate=1 go test -v -run=Nothing -memprofile=prof.mem
// without any errors about no matching tests.
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
},
}.Check(t)
deptest.DepChecker{
GOOS: "linux",
GOARCH: "arm64",
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
},
}.Check(t)
}

View File

@@ -50,7 +50,6 @@ import (
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/osdiag"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
@@ -128,7 +127,7 @@ var syslogf logger.Logf = logger.Discard
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
go func() {
osdiag.LogSupportInfo(logger.WithPrefix(log.Printf, "Support Info: "), osdiag.LogSupportInfoReasonStartup)
winutil.LogSupportInfo(log.Printf)
}()
if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {

View File

@@ -7,20 +7,16 @@
package flakytest
import (
"fmt"
"os"
"regexp"
"testing"
)
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
// and retry them.
const FlakyTestLogMessage = "flakytest: this is a known flaky test"
// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
// when a flaky test is retried. It contains the attempt number, starting at 1.
const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
// InTestWrapper returns whether or not this binary is running under our test
// wrapper.
func InTestWrapper() bool {
return os.Getenv("TS_IN_TESTWRAPPER") != ""
}
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
@@ -34,6 +30,16 @@ func Mark(t testing.TB, issue string) {
t.Fatalf("bad issue format: %q", issue)
}
fmt.Fprintln(os.Stderr, FlakyTestLogMessage) // sentinel value for testwrapper
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
if !InTestWrapper() {
return
}
t.Cleanup(func() {
if t.Failed() {
t.Logf("flakytest: signaling test wrapper to retry test")
// Signal to test wrapper that we should restart.
os.Exit(123)
}
})
}

View File

@@ -3,10 +3,7 @@
package flakytest
import (
"os"
"testing"
)
import "testing"
func TestIssueFormat(t *testing.T) {
testCases := []struct {
@@ -27,17 +24,3 @@ func TestIssueFormat(t *testing.T) {
}
}
}
// TestFlakeRun is a test that fails when run in the testwrapper
// for the first time, but succeeds on the second run.
// It's used to test whether the testwrapper retries flaky tests.
func TestFlakeRun(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
e := os.Getenv(FlakeAttemptEnv)
if e == "" {
t.Skip("not running in testwrapper")
}
if e == "1" {
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
}
}

View File

@@ -1,288 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// testwrapper is a wrapper for retrying flaky tests. It is an alternative to
// `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It
// takes different arguments than go test and requires the first positional
// argument to be the pattern to test.
// testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
// 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
// themselves as flaky and be retried on failure.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"sort"
"strings"
"time"
"golang.org/x/exp/maps"
"tailscale.com/cmd/testwrapper/flakytest"
)
const maxAttempts = 3
type testAttempt struct {
name testName
outcome string // "pass", "fail", "skip"
logs bytes.Buffer
isMarkedFlaky bool // set if the test is marked as flaky
pkgFinished bool
}
type testName struct {
pkg string // "tailscale.com/types/key"
name string // "TestFoo"
}
type packageTests struct {
// pattern is the package pattern to run.
// Must be a single pattern, not a list of patterns.
pattern string // "./...", "./types/key"
// tests is a list of tests to run. If empty, all tests in the package are
// run.
tests []string // ["TestFoo", "TestBar"]
}
type goTestOutput struct {
Time time.Time
Action string
Package string
Test string
Output string
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// runTests runs the tests in pt and sends the results on ch. It sends a
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
// set to true.
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
defer close(ch)
args := []string{"test", "-json", pt.pattern}
args = append(args, otherArgs...)
if len(pt.tests) > 0 {
runArg := strings.Join(pt.tests, "|")
args = append(args, "-run", runArg)
}
if debug {
fmt.Println("running", strings.Join(args, " "))
}
cmd := exec.CommandContext(ctx, "go", args...)
r, err := cmd.StdoutPipe()
if err != nil {
log.Printf("error creating stdout pipe: %v", err)
}
defer r.Close()
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt))
if err := cmd.Start(); err != nil {
log.Printf("error starting test: %v", err)
os.Exit(1)
}
done := make(chan struct{})
go func() {
defer close(done)
cmd.Wait()
}()
jd := json.NewDecoder(r)
resultMap := make(map[testName]*testAttempt)
for {
var goOutput goTestOutput
if err := jd.Decode(&goOutput); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
// `go test -json` outputs invalid JSON when a build fails.
// In that case, discard the the output and start reading again.
// The build error will be printed to stderr.
// See: https://github.com/golang/go/issues/35169
if _, ok := err.(*json.SyntaxError); ok {
jd = json.NewDecoder(r)
continue
}
panic(err)
}
if goOutput.Test == "" {
switch goOutput.Action {
case "fail", "pass", "skip":
ch <- &testAttempt{
name: testName{
pkg: goOutput.Package,
},
outcome: goOutput.Action,
pkgFinished: true,
}
}
continue
}
name := testName{
pkg: goOutput.Package,
name: goOutput.Test,
}
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
name.name = test
if goOutput.Action == "output" {
resultMap[name].logs.WriteString(goOutput.Output)
}
continue
}
switch goOutput.Action {
case "start":
// ignore
case "run":
resultMap[name] = &testAttempt{
name: name,
}
case "skip", "pass", "fail":
resultMap[name].outcome = goOutput.Action
ch <- resultMap[name]
case "output":
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
resultMap[name].isMarkedFlaky = true
} else {
resultMap[name].logs.WriteString(goOutput.Output)
}
}
}
<-done
}
const (
retryStatus = 123
maxIterations = 3
)
func main() {
ctx := context.Background()
debug := os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// We only need to parse the -v flag to figure out whether to print the logs
// for a test. We don't need to parse any other flags, so we just use the
// flag package to parse the -v flag and then pass the rest of the args
// through to 'go test'.
// We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's
// output.
v := flag.Bool("v", false, "verbose")
flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
fmt.Println()
fmt.Println("testwrapper-flags:")
flag.CommandLine.PrintDefaults()
fmt.Println()
fmt.Println("examples:")
fmt.Println("\ttestwrapper -v ./... -count=1")
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
fmt.Println()
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
}
flag.Parse()
args := flag.Args()
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
fmt.Println("no pattern specified")
flag.Usage()
os.Exit(1)
} else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
fmt.Println("expected single pattern")
flag.Usage()
os.Exit(1)
}
pattern, otherArgs := args[0], args[1:]
type nextRun struct {
tests []*packageTests
attempt int
log.SetPrefix("testwrapper: ")
if !debug {
log.SetFlags(0)
}
toRun := []*nextRun{
{
tests: []*packageTests{{pattern: pattern}},
attempt: 1,
},
}
printPkgOutcome := func(pkg, outcome string, attempt int) {
if outcome == "skip" {
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
for i := 1; i <= maxIterations; i++ {
if i > 1 {
log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
}
cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
err := cmd.Run()
if err == nil {
return
}
if outcome == "pass" {
outcome = "ok"
}
if outcome == "fail" {
outcome = "FAIL"
}
if attempt > 1 {
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
return
}
fmt.Printf("%s\t%s\n", outcome, pkg)
}
for len(toRun) > 0 {
var thisRun *nextRun
thisRun, toRun = toRun[0], toRun[1:]
if thisRun.attempt >= maxAttempts {
fmt.Println("max attempts reached")
os.Exit(1)
}
if thisRun.attempt > 1 {
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
}
failed := false
toRetry := make(map[string][]string) // pkg -> tests to retry
for _, pt := range thisRun.tests {
ch := make(chan *testAttempt)
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
for tr := range ch {
if tr.pkgFinished {
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
continue
}
if *v || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs)
}
if tr.outcome != "fail" {
continue
}
if tr.isMarkedFlaky {
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
} else {
failed = true
}
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
if debug {
log.Printf("error isn't an ExitError")
}
}
if failed {
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
os.Exit(1)
}
if len(toRetry) == 0 {
continue
if code := exitErr.ExitCode(); code != retryStatus {
if debug {
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
}
os.Exit(code)
}
pkgs := maps.Keys(toRetry)
sort.Strings(pkgs)
nextRun := &nextRun{
attempt: thisRun.attempt + 1,
}
for _, pkg := range pkgs {
tests := toRetry[pkg]
sort.Strings(tests)
nextRun.tests = append(nextRun.tests, &packageTests{
pattern: pkg,
tests: tests,
})
}
toRun = append(toRun, nextRun)
}
log.Printf("test did not pass in %d iterations", maxIterations)
os.Exit(1)
}

View File

@@ -15,7 +15,6 @@ import (
"tailscale.com/logtail/backoff"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -49,7 +48,7 @@ var _ Client = (*Auto)(nil)
// It's a concrete implementation of the Client interface.
type Auto struct {
direct *Direct // our interface to the server APIs
clock tstime.Clock
timeNow func() time.Time
logf logger.Logf
expiry *time.Time
closed bool
@@ -108,12 +107,12 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
if opts.Logf == nil {
opts.Logf = func(fmt string, args ...any) {}
}
if opts.Clock == nil {
opts.Clock = tstime.StdClock{}
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
c := &Auto{
direct: direct,
clock: opts.Clock,
timeNow: opts.TimeNow,
logf: opts.Logf,
newMapCh: make(chan struct{}, 1),
quit: make(chan struct{}),
@@ -209,7 +208,7 @@ func (c *Auto) sendNewMapRequest() {
c.liteMapUpdateCancel = cancel
go func() {
defer cancel()
t0 := c.clock.Now()
t0 := time.Now()
err := c.direct.SendLiteMapUpdate(ctx)
d := time.Since(t0).Round(time.Millisecond)
@@ -552,8 +551,6 @@ func (c *Auto) mapRoutine() {
if stillAuthed {
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
}
// Reset the backoff timer if we got a netmap.
bo.BackOff(ctx, nil)
})
health.SetInPollNetMap(false)
@@ -705,14 +702,14 @@ func (c *Auto) Logout(ctx context.Context) error {
c.mu.Unlock()
c.cancelAuth()
timer, timerChannel := c.clock.NewTimer(10 * time.Second)
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case err := <-errc:
return err
case <-ctx.Done():
return ctx.Err()
case <-timerChannel:
case <-timer.C:
return context.DeadlineExceeded
}
}
@@ -773,7 +770,7 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) {
}
func (c *Auto) TestOnlyTimeNow() time.Time {
return c.clock.Now()
return c.timeNow()
}
// SetDNS sends the SetDNSRequest request to the control plane server,

View File

@@ -45,7 +45,6 @@ import (
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -64,7 +63,7 @@ type Direct struct {
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
serverURL string // URL of the tailcontrol server
clock tstime.Clock
timeNow func() time.Time
lastPrintMap time.Time
newDecompressor func() (Decompressor, error)
keepAlive bool
@@ -106,8 +105,8 @@ type Options struct {
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
Clock tstime.Clock
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic
NewDecompressor func() (Decompressor, error)
KeepAlive bool
@@ -192,8 +191,8 @@ func NewDirect(opts Options) (*Direct, error) {
if err != nil {
return nil, err
}
if opts.Clock == nil {
opts.Clock = tstime.StdClock{}
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
if opts.Logf == nil {
// TODO(apenwarr): remove this default and fail instead.
@@ -236,7 +235,7 @@ func NewDirect(opts Options) (*Direct, error) {
httpc: httpc,
getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL,
clock: opts.Clock,
timeNow: opts.TimeNow,
logf: opts.Logf,
newDecompressor: opts.NewDecompressor,
keepAlive: opts.KeepAlive,
@@ -433,7 +432,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
machinePrivKey, err := c.getMachinePrivKey()
@@ -538,7 +537,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
err = errors.New("hostinfo: BackendLogID missing")
return regen, opt.URL, nil, err
}
now := c.clock.Now().Round(time.Second)
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
OldNodeKey: oldNodeKey,
@@ -771,8 +770,6 @@ func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
// It always returns a non-nil error describing the reason for the failure
// or why the request ended.
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
return c.sendMapRequest(ctx, -1, false, cb)
}
@@ -801,12 +798,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
// every minute.
const pollTimeout = 120 * time.Second
// sendMapRequest makes a /map request to download the network map, calling cb with
// each new netmap. If maxPolls is -1, it will poll forever and only returns if
// the context expires or the server returns an error/closes the connection and as
// such always returns a non-nil error.
//
// If cb is nil, OmitPeers will be set to true.
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool, cb func(*netmap.NetworkMap)) error {
metricMapRequests.Add(1)
metricMapRequestsActive.Add(1)
@@ -912,7 +904,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
defer cancel()
machinePubKey := machinePrivKey.Public()
t0 := c.clock.Now()
t0 := time.Now()
// Url and httpc are protocol specific.
var url string
@@ -955,7 +947,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
return nil
}
timeout, timeoutChannel := c.clock.NewTimer(pollTimeout)
timeout := time.NewTimer(pollTimeout)
timeoutReset := make(chan struct{})
pollDone := make(chan struct{})
defer close(pollDone)
@@ -965,14 +957,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
case <-pollDone:
vlogf("netmap: ending timeout goroutine")
return
case <-timeoutChannel:
case <-timeout.C:
c.logf("map response long-poll timed out!")
cancel()
return
case <-timeoutReset:
if !timeout.Stop() {
select {
case <-timeoutChannel:
case <-timeout.C:
case <-pollDone:
vlogf("netmap: ending timeout goroutine")
return
@@ -1097,7 +1089,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
}
if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 {
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep, c.clock); err != nil {
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil {
return err
}
}
@@ -1127,7 +1119,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
now := c.clock.Now()
now := c.timeNow()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
@@ -1305,7 +1297,7 @@ func initDevKnob() devKnobs {
}
}
var clock tstime.Clock = tstime.StdClock{}
var clockNow = time.Now
// opt.Bool configs from control.
var (
@@ -1409,9 +1401,9 @@ func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
if pr.Log {
logf("answerHeadPing: sending HEAD ping to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := clock.Since(t0).Round(time.Millisecond)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
logf("answerHeadPing error: %v to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
@@ -1457,7 +1449,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
if pr.Log {
logf("answerC2NPing: sending POST ping to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
@@ -1467,7 +1459,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
}
}
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration, clock tstime.Clock) error {
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error {
const maxSleep = 5 * time.Minute
if d > maxSleep {
logf("sleeping for %v, capped from server-requested %v ...", maxSleep, d)
@@ -1476,20 +1468,20 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
logf("sleeping for server-requested %v ...", d)
}
ticker, tickerChannel := clock.NewTicker(pollTimeout / 2)
ticker := time.NewTicker(pollTimeout / 2)
defer ticker.Stop()
timer, timerChannel := clock.NewTimer(d)
timer := time.NewTimer(d)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timerChannel:
case <-timer.C:
return nil
case <-tickerChannel:
case <-ticker.C:
select {
case timeoutReset <- struct{}{}:
case <-timerChannel:
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
@@ -1666,7 +1658,7 @@ func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pin
logf("invalid ping request: missing url, ip or pinger")
return
}
start := clock.Now()
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@@ -1704,7 +1696,7 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
if pr.Log {
logf("postPingResult: sending ping results to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {

View File

@@ -90,28 +90,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
ms.lastUserProfile[up.ID] = up
}
if dm := resp.DERPMap; dm != nil {
if resp.DERPMap != nil {
ms.vlogf("netmap: new map contains DERP map")
// Zero-valued fields in a DERPMap mean that we're not changing
// anything and are using the previous value(s).
if ldm := ms.lastDERPMap; ldm != nil {
if dm.Regions == nil {
dm.Regions = ldm.Regions
dm.OmitDefaultRegions = ldm.OmitDefaultRegions
}
if dm.HomeParams == nil {
dm.HomeParams = ldm.HomeParams
} else if oldhh := ldm.HomeParams; oldhh != nil {
// Propagate sub-fields of HomeParams
hh := dm.HomeParams
if hh.RegionScore == nil {
hh.RegionScore = oldhh.RegionScore
}
}
}
ms.lastDERPMap = dm
ms.lastDERPMap = resp.DERPMap
}
if pf := resp.PacketFilter; pf != nil {
@@ -307,7 +288,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
for _, n := range newFull {
peerByID[n.ID] = n
}
now := clock.Now()
now := clockNow()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {

View File

@@ -14,7 +14,6 @@ import (
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
@@ -24,6 +23,9 @@ import (
func TestUndeltaPeers(t *testing.T) {
var curTime time.Time
tstest.Replace(t, &clockNow, func() time.Time {
return curTime
})
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
@@ -296,7 +298,6 @@ func TestUndeltaPeers(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
if !tt.curTime.IsZero() {
curTime = tt.curTime
tstest.Replace(t, &clock, tstime.Clock(tstest.NewClock(tstest.ClockOpts{Start: curTime})))
}
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
@@ -618,108 +619,3 @@ func TestCopyDebugOptBools(t *testing.T) {
}
}
}
func TestDeltaDERPMap(t *testing.T) {
regions1 := map[int]*tailcfg.DERPRegion{
1: {
RegionID: 1,
Nodes: []*tailcfg.DERPNode{{
Name: "derp1a",
RegionID: 1,
HostName: "derp1a" + tailcfg.DotInvalid,
IPv4: "169.254.169.254",
IPv6: "none",
}},
},
}
// As above, but with a changed IPv4 addr
regions2 := map[int]*tailcfg.DERPRegion{1: regions1[1].Clone()}
regions2[1].Nodes[0].IPv4 = "127.0.0.1"
type step struct {
got *tailcfg.DERPMap
want *tailcfg.DERPMap
}
tests := []struct {
name string
steps []step
}{
{
name: "nothing-to-nothing",
steps: []step{
{nil, nil},
{nil, nil},
},
},
{
name: "regions-sticky",
steps: []step{
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
{&tailcfg.DERPMap{}, &tailcfg.DERPMap{Regions: regions1}},
},
},
{
name: "regions-change",
steps: []step{
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
{&tailcfg.DERPMap{Regions: regions2}, &tailcfg.DERPMap{Regions: regions2}},
},
},
{
name: "home-params",
steps: []step{
// Send a DERP map
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
// Send home params, want to still have the same regions
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
},
},
{
name: "home-params-sub-fields",
steps: []step{
// Send a DERP map with home params
{
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
// Sending a struct with a 'HomeParams' field but nil RegionScore doesn't change home params...
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: nil}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
// ... but sending one with a non-nil and empty RegionScore field zeroes that out.
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: map[int]float64{}}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{},
}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ms := newTestMapSession(t)
for stepi, s := range tt.steps {
nm := ms.netmapForResponse(&tailcfg.MapResponse{DERPMap: s.got})
if !reflect.DeepEqual(nm.DERPMap, s.want) {
t.Errorf("unexpected result at step index %v; got: %s", stepi, must.Get(json.Marshal(nm.DERPMap)))
}
}
})
}
}

View File

@@ -23,7 +23,6 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
@@ -288,25 +287,6 @@ func (nc *NoiseClient) GetSingleUseRoundTripper(ctx context.Context) (http.Round
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
}
// contextErr is an error that wraps another error and is used to indicate that
// the error was because a context expired.
type contextErr struct {
err error
}
func (e contextErr) Error() string {
return e.err.Error()
}
func (e contextErr) Unwrap() error {
return e.err
}
// getConn returns a noiseConn that can be used to make requests to the
// coordination server. It may return a cached connection or create a new one.
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
// As such, context values may not be respected as there are no guarantees that
// the context passed to getConn is the same as the context passed to dial.
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
nc.mu.Lock()
if last := nc.last; last != nil && last.canTakeNewRequest() {
@@ -315,35 +295,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
}
nc.mu.Unlock()
for {
// We singeflight the dial to avoid making multiple connections, however
// that means that we can't simply cancel the dial if the context is
// canceled. Instead, we have to additionally check that the context
// which was canceled is our context and retry if our context is still
// valid.
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseConn, error) {
c, err := nc.dial(ctx)
if err != nil {
if ctx.Err() != nil {
return nil, contextErr{ctx.Err()}
}
return nil, err
}
return c, nil
})
var ce contextErr
if err == nil || !errors.As(err, &ce) {
return conn, err
}
if ctx.Err() == nil {
// The dial failed because of a context error, but our context
// is still valid. Retry.
continue
}
// The dial failed because our context was canceled. Return the
// underlying error.
return nil, ce.Unwrap()
conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
if err != nil {
return nil, err
}
return conn, nil
}
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -388,7 +344,7 @@ func (nc *NoiseClient) Close() error {
// dial opens a new connection to tailcontrol, fetching the server noise key
// if not cached.
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
func (nc *NoiseClient) dial() (*noiseConn, error) {
nc.mu.Lock()
connID := nc.nextID
nc.nextID++
@@ -436,7 +392,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
}
timeout := time.Duration(timeoutSec * float64(time.Second))
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
clientConn, err := (&controlhttp.Dialer{
@@ -451,7 +407,6 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
DialPlan: dialPlan,
Logf: nc.logf,
NetMon: nc.netMon,
Clock: tstime.StdClock{},
}).Dial(ctx)
if err != nil {
return nil, err

View File

@@ -127,7 +127,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
return nil, nil, err
}
selected, chain := selectIdentityFromSlice(subject, ids, clock.Now())
selected, chain := selectIdentityFromSlice(subject, ids, time.Now())
for _, id := range ids {
if id != selected {

View File

@@ -45,7 +45,6 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/util/multierr"
)
@@ -148,16 +147,13 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
// 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)
if a.Clock == nil {
a.Clock = tstime.StdClock{}
}
tmr, tmrChannel := a.Clock.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
defer tmr.Stop()
select {
case <-ctx.Done():
err = ctx.Err()
return
case <-tmrChannel:
case <-tmr.C:
}
}
@@ -323,10 +319,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
if a.Clock == nil {
a.Clock = tstime.StdClock{}
}
try443Timer := a.Clock.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
defer try443Timer.Stop()
var err80, err443 error

View File

@@ -11,7 +11,6 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -90,10 +89,6 @@ type Dialer struct {
drainFinished chan struct{}
omitCertErrorLogging bool
testFallbackDelay time.Duration
// tstime.Clock is used instead of time package for methods such as time.Now.
// If not specified, will default to tstime.StdClock{}.
Clock tstime.Clock
}
func strDef(v1, v2 string) string {

View File

@@ -25,7 +25,6 @@ import (
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -205,7 +204,6 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
Logf: t.Logf,
omitCertErrorLogging: true,
testFallbackDelay: 50 * time.Millisecond,
Clock: &tstest.Clock{},
}
if proxy != nil {
@@ -585,20 +583,19 @@ func TestDialPlan(t *testing.T) {
}},
want: goodAddr,
},
// TODO(#8442): fix this test
// {
// 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-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{
@@ -662,7 +659,6 @@ func TestDialPlan(t *testing.T) {
drainFinished: drained,
omitCertErrorLogging: true,
testFallbackDelay: 50 * time.Millisecond,
Clock: &tstest.Clock{},
}
conn, err := a.dial(ctx)

View File

@@ -17,7 +17,6 @@ import (
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -41,8 +40,6 @@ type Client struct {
// Owned by Recv:
peeked int // bytes to discard on next Recv
readErr syncs.AtomicValue[error] // sticky (set by Recv)
clock tstime.Clock
}
// ClientOpt is an option passed to NewClient.
@@ -106,7 +103,6 @@ func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
isProber: opt.IsProber,
clock: tstime.StdClock{},
}
if opt.ServerPub.IsZero() {
if err := c.recvServerKey(); err != nil {
@@ -218,7 +214,7 @@ func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
defer c.wmu.Unlock()
if c.rate != nil {
pktLen := frameHeaderLen + key.NodePublicRawLen + len(pkt)
if !c.rate.AllowN(c.clock.Now(), pktLen) {
if !c.rate.AllowN(time.Now(), pktLen) {
return nil // drop
}
}
@@ -248,7 +244,7 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
c.wmu.Lock()
defer c.wmu.Unlock()
timer := c.clock.AfterFunc(5*time.Second, c.writeTimeoutFired)
timer := time.AfterFunc(5*time.Second, c.writeTimeoutFired)
defer timer.Stop()
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
@@ -461,6 +457,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.readErr.Store(err)
}
}()
for {
c.nc.SetReadDeadline(time.Now().Add(timeout))

View File

@@ -39,7 +39,6 @@ import (
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -165,8 +164,6 @@ type Server struct {
// maps from netip.AddrPort to a client's public key
keyOfAddr map[netip.AddrPort]key.NodePublic
clock tstime.Clock
}
// clientSet represents 1 or more *sclients.
@@ -321,7 +318,6 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
avgQueueDuration: new(uint64),
tcpRtt: metrics.LabelMap{Label: "le"},
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
clock: tstime.StdClock{},
}
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
@@ -471,8 +467,8 @@ func (s *Server) initMetacert() {
CommonName: fmt.Sprintf("derpkey%s", s.publicKey.UntypedHexString()),
},
// Windows requires NotAfter and NotBefore set:
NotAfter: s.clock.Now().Add(30 * 24 * time.Hour),
NotBefore: s.clock.Now().Add(-30 * 24 * time.Hour),
NotAfter: time.Now().Add(30 * 24 * time.Hour),
NotBefore: time.Now().Add(-30 * 24 * time.Hour),
// Per https://github.com/golang/go/issues/51759#issuecomment-1071147836,
// macOS requires BasicConstraints when subject == issuer:
BasicConstraintsValid: true,
@@ -701,7 +697,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
connectedAt: s.clock.Now(),
connectedAt: time.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
@@ -931,7 +927,7 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
return c.sendPkt(dst, pkt{
bs: contents,
enqueuedAt: c.s.clock.Now(),
enqueuedAt: time.Now(),
src: srcKey,
})
}
@@ -998,7 +994,7 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
p := pkt{
bs: contents,
enqueuedAt: c.s.clock.Now(),
enqueuedAt: time.Now(),
src: c.key,
}
return c.sendPkt(dst, p)
@@ -1391,7 +1387,7 @@ func (c *sclient) setPreferred(v bool) {
// graphs, so not important to miss a move. But it shouldn't:
// the netcheck/re-STUNs in magicsock only happen about every
// 30 seconds.
if c.s.clock.Since(c.connectedAt) > 5*time.Second {
if time.Since(c.connectedAt) > 5*time.Second {
homeMove.Add(1)
}
}
@@ -1405,7 +1401,7 @@ func expMovingAverage(prev, newValue, alpha float64) float64 {
// recordQueueTime updates the average queue duration metric after a packet has been sent.
func (c *sclient) recordQueueTime(enqueuedAt time.Time) {
elapsed := float64(c.s.clock.Since(enqueuedAt).Milliseconds())
elapsed := float64(time.Since(enqueuedAt).Milliseconds())
for {
old := atomic.LoadUint64(c.s.avgQueueDuration)
newAvg := expMovingAverage(math.Float64frombits(old), elapsed, 0.1)
@@ -1435,7 +1431,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
}()
jitter := time.Duration(rand.Intn(5000)) * time.Millisecond
keepAliveTick, keepAliveTickChannel := c.s.clock.NewTicker(keepAlive + jitter)
keepAliveTick := time.NewTicker(keepAlive + jitter)
defer keepAliveTick.Stop()
var werr error // last write error
@@ -1465,7 +1461,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTickChannel:
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
continue
default:
@@ -1494,7 +1490,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTickChannel:
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
}
}

View File

@@ -9,37 +9,45 @@ import (
"net"
"time"
"tailscale.com/net/tcpinfo"
"golang.org/x/sys/unix"
)
func (c *sclient) statsLoop(ctx context.Context) error {
// Get the RTT initially to verify it's supported.
conn := c.tcpConn()
if conn == nil {
// If we can't get a TCP socket, then we can't send stats.
tcpConn := c.tcpConn()
if tcpConn == nil {
c.s.tcpRtt.Add("non-tcp", 1)
return nil
}
if _, err := tcpinfo.RTT(conn); err != nil {
c.logf("error fetching initial RTT: %v", err)
rawConn, err := tcpConn.SyscallConn()
if err != nil {
c.logf("error getting SyscallConn: %v", err)
c.s.tcpRtt.Add("error", 1)
return nil
}
const statsInterval = 10 * time.Second
ticker, tickerChannel := c.s.clock.NewTicker(statsInterval)
ticker := time.NewTicker(statsInterval)
defer ticker.Stop()
var (
tcpInfo *unix.TCPInfo
sysErr error
)
statsLoop:
for {
select {
case <-tickerChannel:
rtt, err := tcpinfo.RTT(conn)
if err != nil {
case <-ticker.C:
err = rawConn.Control(func(fd uintptr) {
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
})
if err != nil || sysErr != nil {
continue statsLoop
}
// TODO(andrew): more metrics?
rtt := time.Duration(tcpInfo.Rtt) * time.Microsecond
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
case <-ctx.Done():

View File

@@ -27,7 +27,6 @@ import (
"golang.org/x/time/rate"
"tailscale.com/disco"
"tailscale.com/net/memnet"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -991,10 +990,9 @@ func TestClientRecv(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
clock: &tstest.Clock{},
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
}
got, err := c.Recv()
if err != nil {
@@ -1437,8 +1435,7 @@ func (w *countWriter) ResetStats() {
func TestClientSendRateLimiting(t *testing.T) {
cw := new(countWriter)
c := &Client{
bw: bufio.NewWriter(cw),
clock: &tstest.Clock{},
bw: bufio.NewWriter(cw),
}
c.setSendRateLimiter(ServerInfoMessage{})

View File

@@ -38,7 +38,6 @@ import (
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/cmpx"
@@ -56,11 +55,6 @@ type Client struct {
MeshKey string // optional; for trusted clients
IsProber bool // optional; for probers to optional declare themselves as such
// BaseContext, if non-nil, returns the base context to use for dialing a
// new derp server. If nil, context.Background is used.
// In either case, additional timeouts may be added to the base context.
BaseContext func() context.Context
privateKey key.NodePrivate
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
@@ -89,7 +83,6 @@ type Client struct {
serverPubKey key.NodePublic
tlsState *tls.ConnectionState
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
clock tstime.Clock
}
func (c *Client) String() string {
@@ -108,7 +101,6 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo
getRegion: getRegion,
ctx: ctx,
cancelCtx: cancel,
clock: tstime.StdClock{},
}
return c
}
@@ -116,7 +108,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
// It's used by the netcheck package.
func NewNetcheckClient(logf logger.Logf) *Client {
return &Client{logf: logf, clock: tstime.StdClock{}}
return &Client{logf: logf}
}
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
@@ -137,7 +129,6 @@ func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf) (
url: u,
ctx: ctx,
cancelCtx: cancel,
clock: tstime.StdClock{},
}
return c, nil
}
@@ -149,19 +140,6 @@ func (c *Client) Connect(ctx context.Context) error {
return err
}
// newContext returns a new context for setting up a new DERP connection.
// It uses either c.BaseContext or returns context.Background.
func (c *Client) newContext() context.Context {
if c.BaseContext != nil {
ctx := c.BaseContext()
if ctx == nil {
panic("BaseContext returned nil")
}
return ctx
}
return context.Background()
}
// TLSConnectionState returns the last TLS connection state, if any.
// The client must already be connected.
func (c *Client) TLSConnectionState() (_ *tls.ConnectionState, ok bool) {
@@ -666,14 +644,14 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
nwait++
go func() {
if proto == "tcp4" && c.preferIPv6() {
t, tChannel := c.clock.NewTimer(200 * time.Millisecond)
t := time.NewTimer(200 * time.Millisecond)
select {
case <-ctx.Done():
// Either user canceled original context,
// it timed out, or the v6 dial succeeded.
t.Stop()
return
case <-tChannel:
case <-t.C:
// Start v4 dial
}
}
@@ -794,7 +772,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
}
func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.Send")
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
if err != nil {
return err
}
@@ -894,7 +872,7 @@ func (c *Client) LocalAddr() (netip.AddrPort, error) {
}
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.ForwardPacket")
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
if err != nil {
return err
}
@@ -960,7 +938,7 @@ func (c *Client) NotePreferred(v bool) {
//
// Only trusted connections (using MeshKey) are allowed to use this.
func (c *Client) WatchConnectionChanges() error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.WatchConnectionChanges")
client, _, err := c.connect(context.TODO(), "derphttp.Client.WatchConnectionChanges")
if err != nil {
return err
}
@@ -975,7 +953,7 @@ func (c *Client) WatchConnectionChanges() error {
//
// Only trusted connections (using MeshKey) are allowed to use this.
func (c *Client) ClosePeer(target key.NodePublic) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.ClosePeer")
client, _, err := c.connect(context.TODO(), "derphttp.Client.ClosePeer")
if err != nil {
return err
}
@@ -996,7 +974,7 @@ func (c *Client) Recv() (derp.ReceivedMessage, error) {
// RecvDetail is like Recv, but additional returns the connection generation on each message.
// The connGen value is incremented every time the derphttp.Client reconnects to the server.
func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
client, connGen, err := c.connect(c.newContext(), "derphttp.Client.Recv")
client, connGen, err := c.connect(context.TODO(), "derphttp.Client.Recv")
if err != nil {
return nil, 0, err
}

View File

@@ -51,7 +51,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
present = map[key.NodePublic]bool{}
}
lastConnGen := 0
lastStatus := c.clock.Now()
lastStatus := time.Now()
logConnectedLocked := func() {
if loggedConnected {
return
@@ -61,7 +61,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
const logConnectedDelay = 200 * time.Millisecond
timer := c.clock.AfterFunc(2*time.Second, func() {
timer := time.AfterFunc(2*time.Second, func() {
mu.Lock()
defer mu.Unlock()
logConnectedLocked()
@@ -91,11 +91,11 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
sleep := func(d time.Duration) {
t, tChannel := c.clock.NewTimer(d)
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-tChannel:
case <-t.C:
}
}
@@ -142,7 +142,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
default:
continue
}
if now := c.clock.Now(); now.Sub(lastStatus) > statusInterval {
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
lastStatus = now
infoLogf("%d peers", len(present))
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package disco
import (
"bytes"
"encoding/binary"
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// ToPCAPFrame marshals the bytes for a pcap record that describe a disco frame.
//
// Warning: Alloc garbage. Acceptable while capturing.
func ToPCAPFrame(src netip.AddrPort, derpNodeSrc key.NodePublic, payload []byte) []byte {
var (
b bytes.Buffer
flag uint8
)
b.Grow(128) // Most disco frames will probably be smaller than this.
if src.Addr() == tailcfg.DerpMagicIPAddr {
flag |= 0x01
}
b.WriteByte(flag) // 1b: flag
derpSrc := derpNodeSrc.Raw32()
b.Write(derpSrc[:]) // 32b: derp public key
binary.Write(&b, binary.LittleEndian, uint16(src.Port())) // 2b: port
addr, _ := src.Addr().MarshalBinary()
binary.Write(&b, binary.LittleEndian, uint16(len(addr))) // 2b: len(addr)
b.Write(addr) // Xb: addr
binary.Write(&b, binary.LittleEndian, uint16(len(payload))) // 2b: len(payload)
b.Write(payload) // Xb: payload
return b.Bytes()
}

View File

@@ -6,20 +6,22 @@ SA_NAME ?= tailscale
TS_KUBE_SECRET ?= tailscale
rbac:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml
@echo "---"
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml
@echo "---"
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
userspace-sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
proxy:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g"
kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
subnet-router:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g"
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-

View File

@@ -26,7 +26,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
```bash
export SA_NAME=tailscale
export TS_KUBE_SECRET=tailscale-auth
make rbac | kubectl apply -f-
make rbac
```
### Sample Sidecar
@@ -36,7 +36,7 @@ Running as a sidecar allows you to directly expose a Kubernetes pod over Tailsca
1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash
make sidecar | kubectl apply -f-
make sidecar
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar
```
@@ -60,7 +60,7 @@ You can also run the sidecar in userspace mode. The obvious benefit is reducing
1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash
make userspace-sidecar | kubectl apply -f-
make userspace-sidecar
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar
```
@@ -100,7 +100,7 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
1. Deploy the proxy pod
```bash
make proxy | kubectl apply -f-
make proxy
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs proxy
```
@@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
1. Deploy the subnet-router pod.
```bash
make subnet-router | kubectl apply -f-
make subnet-router
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs subnet-router
```

View File

@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-Fr4VZcKrXnT1PZuEG110KBefjcZzRsQRBSvByELKAy4=
# nix-direnv cache busting line: sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=

60
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/creack/pty v1.1.18
github.com/dave/jennifer v1.6.1
github.com/dblohm7/wingoes v0.0.0-20230803162905-5c6286bb8c6e
github.com/dblohm7/wingoes v0.0.0-20230426155039-111c8c3b57c8
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.14.53
github.com/frankban/quicktest v1.14.5
@@ -33,7 +33,7 @@ require (
github.com/google/go-containerregistry v0.14.0
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c
github.com/google/uuid v1.3.0
github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf
github.com/goreleaser/nfpm v1.10.3
github.com/hdevalence/ed25519consensus v0.1.0
github.com/iancoleman/strcase v0.2.0
github.com/illarion/gonotify v1.0.1
@@ -41,7 +41,7 @@ require (
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/jsimonetti/rtnetlink v1.3.2
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.16.7
github.com/klauspost/compress v1.16.5
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.18
@@ -59,29 +59,28 @@ require (
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e
github.com/tailscale/golang-x-crypto v0.0.0-20221115211329-17a3db2c30d2
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tailscale/wireguard-go v0.0.0-20230710185534-bb2c8f22eccf
github.com/tailscale/wireguard-go v0.0.0-20230410165232-af172621b4dd
github.com/tc-hib/winres v0.2.0
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.11.0
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vishvananda/netns v0.0.4
go.uber.org/zap v1.24.0
go4.org/mem v0.0.0-20220726221520-4f986261bf13
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
golang.org/x/crypto v0.11.0
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
golang.org/x/mod v0.11.0
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
golang.org/x/crypto v0.8.0
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
golang.org/x/mod v0.10.0
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.2.0
golang.org/x/sys v0.10.0
golang.org/x/term v0.10.0
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
golang.org/x/term v0.8.0
golang.org/x/time v0.3.0
golang.org/x/tools v0.9.1
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
@@ -103,10 +102,8 @@ require (
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/Abirdcfly/dupword v0.0.11 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Antonboom/errname v0.1.9 // indirect
github.com/Antonboom/nilnil v0.1.4 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
@@ -115,9 +112,9 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/OpenPeeDeeP/depguard v1.1.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
@@ -157,7 +154,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/docker/cli v23.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
@@ -172,9 +169,9 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/go-critic/go-critic v0.8.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-git/go-git/v5 v5.7.0 // indirect
github.com/go-git/go-git/v5 v5.6.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -204,10 +201,10 @@ require (
github.com/google/gnostic v0.6.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
github.com/google/rpmpack v0.5.0 // indirect
github.com/google/rpmpack v0.0.0-20221120200012-98b63d62fd77 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
github.com/goreleaser/chglog v0.5.0 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/goreleaser/chglog v0.4.2 // indirect
github.com/goreleaser/fileglob v0.3.1 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
@@ -218,7 +215,7 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jgautheron/goconst v1.5.1 // indirect
@@ -233,7 +230,7 @@ require (
github.com/kisielk/errcheck v1.6.3 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.4 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
@@ -271,7 +268,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
@@ -290,27 +287,27 @@ require (
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect
github.com/sassoftware/go-rpmutils v0.2.0 // indirect
github.com/securego/gosec/v2 v2.15.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect
github.com/skeema/knownhosts v1.1.1 // indirect
github.com/skeema/knownhosts v1.1.0 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/spf13/viper v1.15.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect
@@ -325,6 +322,7 @@ require (
github.com/ultraware/whitespace v0.0.5 // indirect
github.com/uudashr/gocognit v1.0.6 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
@@ -335,7 +333,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53 // indirect
golang.org/x/image v0.7.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/text v0.9.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect

View File

@@ -1 +1 @@
sha256-Fr4VZcKrXnT1PZuEG110KBefjcZzRsQRBSvByELKAy4=
sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=

467
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
d149af282305d5365d5a4fb576d9fa81247eb6da
492f6d9d792fa6e4caa388e4d7bab46b48d07ad5

View File

@@ -10,5 +10,4 @@ package healthmsg
const (
WarnAcceptRoutesOff = "Some peers are advertising routes but --accept-routes is false"
TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller
LockedOut = "this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"
)

View File

@@ -283,7 +283,7 @@ func inContainer() opt.Bool {
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
ret.Set(true)
return io.EOF
}

View File

@@ -61,7 +61,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
if secs == 0 {
secs -= 1
}
until := b.clock.Now().Add(time.Duration(secs) * time.Second)
until := time.Now().Add(time.Duration(secs) * time.Second)
err := b.SetComponentDebugLogging(component, until)
var res struct {
Error string `json:",omitempty"`

View File

@@ -22,7 +22,6 @@ import (
"fmt"
"io"
"log"
insecurerand "math/rand"
"net"
"os"
"path/filepath"
@@ -31,7 +30,7 @@ import (
"sync"
"time"
"github.com/tailscale/golang-x-crypto/acme"
"golang.org/x/crypto/acme"
"golang.org/x/exp/slices"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
@@ -53,8 +52,8 @@ var (
// populate the on-disk cache and the rest should use that.
acmeMu sync.Mutex
renewMu sync.Mutex // lock order: acmeMu before renewMu
renewCertAt = map[string]time.Time{}
renewMu sync.Mutex // lock order: don't hold acmeMu and renewMu at the same time
lastRenewCheck = map[string]time.Time{}
)
// certDir returns (creating if needed) the directory in which cached
@@ -80,20 +79,14 @@ func (b *LocalBackend) certDir() (string, error) {
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
// GetCertPEM gets the TLSCertKeyPair for domain, either from cache or via the
// ACME process. ACME process is used for new domain certs, existing expired
// certs or existing certs that should get renewed due to upcoming expiry.
//
// syncRenewal changes renewal behavior for existing certs that are still valid
// but need renewal. When syncRenewal is set, the method blocks until a new
// cert is issued. When syncRenewal is not set, existing cert is returned right
// away and renewal is kicked off in a background goroutine.
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
// getCertPEM gets the KeyPair for domain, either from cache, via the ACME
// process, or from cache and kicking off an async ACME renewal.
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
if !validLookingCertDomain(domain) {
return nil, errors.New("invalid domain")
}
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
now := b.clock.Now()
now := time.Now()
traceACME := func(v any) {
if !acmeDebug() {
return
@@ -108,18 +101,15 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
}
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair)
shouldRenew, err := shouldStartDomainRenewal(domain, now, pair)
if err != nil {
logf("error checking for certificate renewal: %v", err)
} else if !shouldRenew {
return pair, nil
}
if !syncRenewal {
} else if shouldRenew {
logf("starting async renewal")
// Start renewal in the background.
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
}
// Synchronous renewal happens below.
return pair, nil
}
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
@@ -130,46 +120,28 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
return pair, nil
}
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
func shouldStartDomainRenewal(domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
renewMu.Lock()
defer renewMu.Unlock()
if renewAt, ok := renewCertAt[domain]; ok {
return now.After(renewAt), nil
if last, ok := lastRenewCheck[domain]; ok && now.Sub(last) < time.Minute {
// We checked very recently. Don't bother reparsing &
// validating the x509 cert.
return false, nil
}
lastRenewCheck[domain] = now
renewTime, err := b.domainRenewalTimeByARI(cs, pair)
if err != nil {
// Log any ARI failure and fall back to checking for renewal by expiry.
b.logf("acme: ARI check failed: %v; falling back to expiry-based check", err)
renewTime, err = b.domainRenewalTimeByExpiry(pair)
if err != nil {
return false, err
}
}
renewCertAt[domain] = renewTime
return now.After(renewTime), nil
}
func (b *LocalBackend) domainRenewed(domain string) {
renewMu.Lock()
defer renewMu.Unlock()
delete(renewCertAt, domain)
}
func (b *LocalBackend) domainRenewalTimeByExpiry(pair *TLSCertKeyPair) (time.Time, error) {
block, _ := pem.Decode(pair.CertPEM)
if block == nil {
return time.Time{}, fmt.Errorf("parsing certificate PEM")
return false, fmt.Errorf("parsing certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return time.Time{}, fmt.Errorf("parsing certificate: %w", err)
return false, fmt.Errorf("parsing certificate: %w", err)
}
certLifetime := cert.NotAfter.Sub(cert.NotBefore)
if certLifetime < 0 {
return time.Time{}, fmt.Errorf("negative certificate lifetime %v", certLifetime)
return false, fmt.Errorf("negative certificate lifetime %v", certLifetime)
}
// Per https://github.com/tailscale/tailscale/issues/8204, check
@@ -178,43 +150,11 @@ func (b *LocalBackend) domainRenewalTimeByExpiry(pair *TLSCertKeyPair) (time.Tim
// Encrypt.
renewalDuration := certLifetime * 2 / 3
renewAt := cert.NotBefore.Add(renewalDuration)
return renewAt, nil
}
func (b *LocalBackend) domainRenewalTimeByARI(cs certStore, pair *TLSCertKeyPair) (time.Time, error) {
var blocks []*pem.Block
rest := pair.CertPEM
for len(rest) > 0 {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
return time.Time{}, fmt.Errorf("parsing certificate PEM")
}
blocks = append(blocks, block)
if now.After(renewAt) {
return true, nil
}
if len(blocks) < 2 {
return time.Time{}, fmt.Errorf("could not parse certificate chain from certStore, got %d PEM block(s)", len(blocks))
}
ac, err := acmeClient(cs)
if err != nil {
return time.Time{}, err
}
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
ri, err := ac.FetchRenewalInfo(ctx, blocks[0].Bytes, blocks[1].Bytes)
if err != nil {
return time.Time{}, fmt.Errorf("failed to fetch renewal info from ACME server: %w", err)
}
if acmeDebug() {
b.logf("acme: ARI response: %+v", ri)
}
// Select a random time in the suggested window and renew if that time has
// passed. Time is randomized per recommendation in
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
start, end := ri.SuggestedWindow.Start, ri.SuggestedWindow.End
renewTime := start.Add(time.Duration(insecurerand.Int63n(int64(end.Sub(start)))))
return renewTime, nil
return false, nil
}
// certStore provides a way to perist and retrieve TLS certificates.
@@ -339,11 +279,11 @@ func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, err
}
func (s certStateStore) WriteCert(domain string, cert []byte) error {
return ipn.WriteState(s.StateStore, ipn.StateKey(domain+".crt"), cert)
return s.WriteState(ipn.StateKey(domain+".crt"), cert)
}
func (s certStateStore) WriteKey(domain string, key []byte) error {
return ipn.WriteState(s.StateStore, ipn.StateKey(domain+".key"), key)
return s.WriteState(ipn.StateKey(domain+".key"), key)
}
func (s certStateStore) ACMEKey() ([]byte, error) {
@@ -351,7 +291,7 @@ func (s certStateStore) ACMEKey() ([]byte, error) {
}
func (s certStateStore) WriteACMEKey(key []byte) error {
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
return s.WriteState(ipn.StateKey(acmePEMName), key)
}
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
@@ -382,25 +322,19 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
acmeMu.Lock()
defer acmeMu.Unlock()
// In case this method was triggered multiple times in parallel (when
// serving incoming requests), check whether one of the other goroutines
// already renewed the cert before us.
if p, err := getCertPEMCached(cs, domain, now); err == nil {
// shouldStartDomainRenewal caches its result so it's OK to call this
// frequently.
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p)
if err != nil {
logf("error checking for certificate renewal: %v", err)
} else if !shouldRenew {
return p, nil
}
return p, nil
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
return nil, err
}
ac, err := acmeClient(cs)
key, err := acmeKey(cs)
if err != nil {
return nil, err
return nil, fmt.Errorf("acmeKey: %w", err)
}
ac := &acme.Client{
Key: key,
UserAgent: "tailscaled/" + version.Long(),
}
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
@@ -530,7 +464,6 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
return nil, err
}
b.domainRenewed(domain)
return &TLSCertKeyPair{CertPEM: certPEM.Bytes(), KeyPEM: privPEM.Bytes()}, nil
}
@@ -607,20 +540,6 @@ func acmeKey(cs certStore) (crypto.Signer, error) {
return privKey, nil
}
func acmeClient(cs certStore) (*acme.Client, error) {
key, err := acmeKey(cs)
if err != nil {
return nil, fmt.Errorf("acmeKey: %w", err)
}
// Note: if we add support for additional ACME providers (other than
// LetsEncrypt), we should make sure that they support ARI extension (see
// shouldStartDomainRenewalARI).
return &acme.Client{
Key: key,
UserAgent: "tailscaled/" + version.Long(),
}, nil
}
// validCertPEM reports whether the given certificate is valid for domain at now.
//
// If roots != nil, it is used instead of the system root pool. This is meant

View File

@@ -12,6 +12,6 @@ type TLSCertKeyPair struct {
CertPEM, KeyPEM []byte
}
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
return nil, errors.New("not implemented for js/wasm")
}

View File

@@ -112,7 +112,7 @@ func TestShouldStartDomainRenewal(t *testing.T) {
reset := func() {
renewMu.Lock()
defer renewMu.Unlock()
maps.Clear(renewCertAt)
maps.Clear(lastRenewCheck)
}
mustMakePair := func(template *x509.Certificate) *TLSCertKeyPair {
@@ -173,12 +173,11 @@ func TestShouldStartDomainRenewal(t *testing.T) {
want: false,
},
}
b := new(LocalBackend)
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
reset()
ret, err := b.domainRenewalTimeByExpiry(mustMakePair(&x509.Certificate{
ret, err := shouldStartDomainRenewal("example.com", now, mustMakePair(&x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: subject,
NotBefore: tt.notBefore,
@@ -192,9 +191,8 @@ func TestShouldStartDomainRenewal(t *testing.T) {
t.Errorf("got err=%q, want %q", err.Error(), tt.wantErr)
}
} else {
renew := now.After(ret)
if renew != tt.want {
t.Errorf("got renew=%v (ret=%v), want renew %v", renew, ret, tt.want)
if ret != tt.want {
t.Errorf("got ret=%v, want %v", ret, tt.want)
}
}
})

View File

@@ -8,7 +8,6 @@ import (
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -38,22 +37,22 @@ type expiryManager struct {
// time.Now().Add(clockDelta) == MapResponse.ControlTime
clockDelta syncs.AtomicValue[time.Duration]
logf logger.Logf
clock tstime.Clock
logf logger.Logf
timeNow func() time.Time
}
func newExpiryManager(logf logger.Logf) *expiryManager {
return &expiryManager{
previouslyExpired: map[tailcfg.StableNodeID]bool{},
logf: logf,
clock: tstime.StdClock{},
timeNow: time.Now,
}
}
// onControlTime is called whenever we receive a new timestamp from the control
// server to store the delta.
func (em *expiryManager) onControlTime(t time.Time) {
localNow := em.clock.Now()
localNow := em.timeNow()
delta := t.Sub(localNow)
if delta.Abs() > minClockDelta {
em.logf("[v1] netmap: flagExpiredPeers: setting clock delta to %v", delta)

View File

@@ -11,7 +11,6 @@ import (
"time"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
)
@@ -111,7 +110,8 @@ func TestFlagExpiredPeers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
em := newExpiryManager(t.Logf)
em.clock = tstest.NewClock(tstest.ClockOpts{Start: now})
em.timeNow = func() time.Time { return now }
if tt.controlTime != nil {
em.onControlTime(*tt.controlTime)
}
@@ -241,7 +241,7 @@ func TestNextPeerExpiry(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
em := newExpiryManager(t.Logf)
em.clock = tstest.NewClock(tstest.ClockOpts{Start: now})
em.timeNow = func() time.Time { return now }
got := em.nextPeerExpiry(tt.netmap, now)
if !got.Equal(tt.want) {
t.Errorf("got %q, want %q", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
@@ -254,7 +254,7 @@ func TestNextPeerExpiry(t *testing.T) {
t.Run("ClockSkew", func(t *testing.T) {
t.Logf("local time: %q", now.Format(time.RFC3339))
em := newExpiryManager(t.Logf)
em.clock = tstest.NewClock(tstest.ClockOpts{Start: now})
em.timeNow = func() time.Time { return now }
// The local clock is "running fast"; our clock skew is -2h
em.clockDelta.Store(-2 * time.Hour)

View File

@@ -60,7 +60,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tsd"
"tailscale.com/tstime"
"tailscale.com/types/dnstype"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -202,7 +201,7 @@ type LocalBackend struct {
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *netmap.NetworkMap
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil
nmExpiryTimer *time.Timer // for updating netMap on node expiry; can be nil
nodeByAddr map[netip.Addr]*tailcfg.Node
activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus
@@ -260,7 +259,6 @@ type LocalBackend struct {
// tkaSyncLock MUST be taken before mu (or inversely, mu must not be held
// at the moment that tkaSyncLock is taken).
tkaSyncLock sync.Mutex
clock tstime.Clock
}
// clientGen is a func that creates a control plane client.
@@ -295,14 +293,13 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
ctx, cancel := context.WithCancel(context.Background())
portpoll := new(portlist.Poller)
clock := tstime.StdClock{}
b := &LocalBackend{
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
sys: sys,
e: e,
dialer: dialer,
@@ -314,7 +311,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
em: newExpiryManager(logf),
gotPortPollRes: make(chan struct{}),
loginFlags: loginFlags,
clock: clock,
}
netMon := sys.NetMon.Get()
@@ -352,7 +348,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
for _, component := range debuggableComponents {
key := componentStateKey(component)
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
if until := time.Unix(ut, 0); until.After(b.clock.Now()) {
if until := time.Unix(ut, 0); until.After(time.Now()) {
// conditional to avoid log spam at start when off
b.SetComponentDebugLogging(component, until)
}
@@ -364,7 +360,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
type componentLogState struct {
until time.Time
timer tstime.TimerController // if non-nil, the AfterFunc to disable it
timer *time.Timer // if non-nil, the AfterFunc to disable it
}
var debuggableComponents = []string{
@@ -417,7 +413,7 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
return t.Unix()
}
ipn.PutStoreInt(b.store, componentStateKey(component), timeUnixOrZero(until))
now := b.clock.Now()
now := time.Now()
on := now.Before(until)
setEnabled(on)
var onFor time.Duration
@@ -432,7 +428,7 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
}
newSt := componentLogState{until: until}
if on {
newSt.timer = b.clock.AfterFunc(onFor, func() {
newSt.timer = time.AfterFunc(onFor, func() {
// Turn off logging after the timer fires, as long as the state is
// unchanged when the timer actually fires.
b.mu.Lock()
@@ -454,7 +450,7 @@ func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
b.mu.Lock()
defer b.mu.Unlock()
now := b.clock.Now()
now := time.Now()
ls := b.componentLogUntil[component]
if ls.until.IsZero() || ls.until.Before(now) {
return time.Time{}
@@ -746,12 +742,12 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
HostName: p.Hostinfo.Hostname(),
DNSName: p.Name,
OS: p.Hostinfo.OS(),
KeepAlive: p.KeepAlive,
LastSeen: lastSeen,
Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode(),
ExitNode: p.StableID != "" && p.StableID == exitNodeID,
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(),
Location: p.Hostinfo.Location(),
}
peerStatusFromNode(ps, p)
@@ -819,13 +815,13 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
// PeerCaps returns the capabilities that remote src IP has to
// ths current node.
func (b *LocalBackend) PeerCaps(src netip.Addr) tailcfg.PeerCapMap {
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) tailcfg.PeerCapMap {
func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
if b.netMap == nil {
return nil
}
@@ -839,7 +835,7 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
}
dst := a.Addr()
if dst.BitLen() == src.BitLen() { // match on family
return filt.CapsWithValues(src, dst)
return filt.AppendCaps(nil, src, dst)
}
}
return nil
@@ -881,7 +877,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Handle node expiry in the netmap
if st.NetMap != nil {
now := b.clock.Now()
now := time.Now()
b.em.flagExpiredPeers(st.NetMap, now)
// Always stop the existing netmap timer if we have a netmap;
@@ -901,7 +897,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
nextExpiry := b.em.nextPeerExpiry(st.NetMap, now)
if !nextExpiry.IsZero() {
tmrDuration := nextExpiry.Sub(now) + 10*time.Second
b.nmExpiryTimer = b.clock.AfterFunc(tmrDuration, func() {
b.nmExpiryTimer = time.AfterFunc(tmrDuration, func() {
// Skip if the world has moved on past the
// saved call (e.g. if we race stopping this
// timer).
@@ -923,7 +919,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
keyExpiryExtended := false
if st.NetMap != nil {
wasExpired := b.keyExpired
isExpired := !st.NetMap.Expiry.IsZero() && st.NetMap.Expiry.Before(b.clock.Now())
isExpired := !st.NetMap.Expiry.IsZero() && st.NetMap.Expiry.Before(time.Now())
if wasExpired && !isExpired {
keyExpiryExtended = true
}
@@ -1018,7 +1014,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Perform all reconfiguration based on the netmap here.
if st.NetMap != nil {
b.capTailnetLock = hasCapability(st.NetMap, tailcfg.CapabilityTailnetLock)
b.capTailnetLock = hasCapability(st.NetMap, tailcfg.CapabilityTailnetLockAlpha)
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
if err := b.tkaSyncIfNeeded(st.NetMap, prefs.View()); err != nil {
@@ -1384,13 +1380,13 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// prevent it from restarting our map poll
// HTTP request (via doSetHostinfoFilterServices >
// cli.SetHostinfo). In practice this is very quick.
t0 := b.clock.Now()
timer, timerChannel := b.clock.NewTimer(time.Second)
t0 := time.Now()
timer := time.NewTimer(time.Second)
select {
case <-b.gotPortPollRes:
b.logf("[v1] got initial portlist info in %v", b.clock.Since(t0).Round(time.Millisecond))
b.logf("[v1] got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
timer.Stop()
case <-timerChannel:
case <-timer.C:
b.logf("timeout waiting for initial portlist")
}
})
@@ -1813,13 +1809,13 @@ func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
isFirst := true
ticker, tickerChannel := b.clock.NewTicker(portlist.PollInterval())
ticker := time.NewTicker(portlist.PollInterval())
defer ticker.Stop()
initChan := make(chan struct{})
close(initChan)
for {
select {
case <-tickerChannel:
case <-ticker.C:
case <-b.ctx.Done():
return
case <-initChan:
@@ -1988,11 +1984,11 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
// pollRequestEngineStatus calls b.RequestEngineStatus every 2 seconds until ctx
// is done.
func (b *LocalBackend) pollRequestEngineStatus(ctx context.Context) {
ticker, tickerChannel := b.clock.NewTicker(2 * time.Second)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-tickerChannel:
case <-ticker.C:
b.RequestEngineStatus()
case <-ctx.Done():
return
@@ -2209,7 +2205,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
}
keyText, _ = b.machinePrivKey.MarshalText()
if err := ipn.WriteState(b.store, ipn.MachineKeyStateKey, keyText); err != nil {
if err := b.store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil {
b.logf("error writing machine key to store: %v", err)
return err
}
@@ -2224,7 +2220,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
//
// b.mu must be held.
func (b *LocalBackend) clearMachineKeyLocked() error {
if err := ipn.WriteState(b.store, ipn.MachineKeyStateKey, nil); err != nil {
if err := b.store.WriteState(ipn.MachineKeyStateKey, nil); err != nil {
return err
}
b.machinePrivKey = key.MachinePrivate{}
@@ -2402,12 +2398,12 @@ func (b *LocalBackend) StartLoginInteractive() {
func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg.PingType) (*ipnstate.PingResult, error) {
if pingType == tailcfg.PingPeerAPI {
t0 := b.clock.Now()
t0 := time.Now()
node, base, err := b.pingPeerAPI(ctx, ip)
if err != nil && ctx.Err() != nil {
return nil, ctx.Err()
}
d := b.clock.Since(t0)
d := time.Since(t0)
pr := &ipnstate.PingResult{
IP: ip.String(),
NodeIP: ip.String(),
@@ -4333,15 +4329,20 @@ func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool {
return true
}
if len(p.Addresses) > 0 &&
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharingTarget) {
// Explicitly noted in the netmap ACL caps as a target.
return true
}
return false
}
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool {
return b.peerCapsLocked(addr).HasCapability(wantCap)
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
@@ -4463,7 +4464,7 @@ func (b *LocalBackend) CheckIPForwarding() error {
}
// TODO: let the caller pass in the ranges.
warn, err := netutil.CheckIPForwarding(tsaddr.ExitRoutes(), b.sys.NetMon.Get().InterfaceState())
warn, err := netutil.CheckIPForwarding(tsaddr.ExitRoutes(), nil)
if err != nil {
return err
}
@@ -4778,7 +4779,7 @@ func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
// opting-out of rate limits. Limit ourselves to at most one message
// per 20ms and a burst of 60 log lines, which should be fast enough to
// not block for too long but slow enough that we can upload all lines.
logf = logger.SlowLoggerWithClock(ctx, logf, 20*time.Millisecond, 60, b.clock.Now)
logf = logger.SlowLoggerWithClock(ctx, logf, 20*time.Millisecond, 60, time.Now)
var checks []doctor.Check
checks = append(checks,
@@ -4830,7 +4831,7 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error {
if b.store == nil {
return errors.New("no state store")
}
err := ipn.WriteState(b.store, ipn.StateKey(key), []byte(value))
err := b.store.WriteState(ipn.StateKey(key), []byte(value))
b.logf("SetDevStateStore(%q, %q) = %v", key, value, err)
if err != nil {

View File

@@ -20,8 +20,8 @@ import (
"path/filepath"
"time"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
@@ -53,12 +53,20 @@ type tkaState struct {
filtered []ipnstate.TKAFilteredPeer
}
// permitTKAInitLocked returns true if tailnet lock initialization may
// occur.
// b.mu must be held.
func (b *LocalBackend) permitTKAInitLocked() bool {
return envknob.UseWIPCode() || b.capTailnetLock
}
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
// nodes from the netmap whose signature does not verify.
//
// b.mu must be held.
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
if b.tka == nil && !b.capTailnetLock {
// TODO(tom): Remove this guard for 1.35 and later.
if b.tka == nil && !b.permitTKAInitLocked() {
health.SetTKAHealth(nil)
return
}
@@ -116,7 +124,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
// Check that we ourselves are not locked out, report a health issue if so.
if nm.SelfNode != nil && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key, nm.SelfNode.KeySignature) != nil {
health.SetTKAHealth(errors.New(healthmsg.LockedOut))
health.SetTKAHealth(errors.New("this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"))
} else {
health.SetTKAHealth(nil)
}
@@ -145,7 +153,8 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
b.mu.Lock() // take mu to protect access to synchronized fields.
defer b.mu.Unlock()
if b.tka == nil && !b.capTailnetLock {
// TODO(tom): Remove this guard for 1.35 and later.
if b.tka == nil && !b.permitTKAInitLocked() {
return nil
}
@@ -474,9 +483,10 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
var nlPriv key.NLPrivate
b.mu.Lock()
if !b.capTailnetLock {
// TODO(tom): Remove this guard for 1.35 and later.
if !b.permitTKAInitLocked() {
b.mu.Unlock()
return errors.New("not permitted to enable tailnet lock")
return errors.New("this feature is not yet complete, a later release may support this functionality")
}
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
@@ -845,93 +855,6 @@ func (b *LocalBackend) NetworkLockAffectedSigs(keyID tkatype.KeyID) ([]tkatype.M
return resp.Signatures, nil
}
// NetworkLockGenerateRecoveryAUM generates an AUM which retroactively removes trust in the
// specified keys. This AUM is signed by the current node and returned.
//
// If forkFrom is specified, it is used as the parent AUM to fork from. If the zero value,
// the parent AUM is determined automatically.
func (b *LocalBackend) NetworkLockGenerateRecoveryAUM(removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) (*tka.AUM, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return nil, errNetworkLockNotActive
}
var nlPriv key.NLPrivate
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
nlPriv = p.Persist().NetworkLockKey()
}
if nlPriv.IsZero() {
return nil, errMissingNetmap
}
aum, err := b.tka.authority.MakeRetroactiveRevocation(b.tka.storage, removeKeys, nlPriv.KeyID(), forkFrom)
if err != nil {
return nil, err
}
// Sign it ourselves.
aum.Signatures, err = nlPriv.SignAUM(aum.SigHash())
if err != nil {
return nil, fmt.Errorf("signing failed: %w", err)
}
return aum, nil
}
// NetworkLockCosignRecoveryAUM co-signs the provided recovery AUM and returns
// the updated structure.
//
// The recovery AUM provided should be the output from a previous call to
// NetworkLockGenerateRecoveryAUM or NetworkLockCosignRecoveryAUM.
func (b *LocalBackend) NetworkLockCosignRecoveryAUM(aum *tka.AUM) (*tka.AUM, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return nil, errNetworkLockNotActive
}
var nlPriv key.NLPrivate
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
nlPriv = p.Persist().NetworkLockKey()
}
if nlPriv.IsZero() {
return nil, errMissingNetmap
}
for _, sig := range aum.Signatures {
if bytes.Equal(sig.KeyID, nlPriv.KeyID()) {
return nil, errors.New("this node has already signed this recovery AUM")
}
}
// Sign it ourselves.
sigs, err := nlPriv.SignAUM(aum.SigHash())
if err != nil {
return nil, fmt.Errorf("signing failed: %w", err)
}
aum.Signatures = append(aum.Signatures, sigs...)
return aum, nil
}
func (b *LocalBackend) NetworkLockSubmitRecoveryAUM(aum *tka.AUM) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return errNetworkLockNotActive
}
var ourNodeKey key.NodePublic
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
ourNodeKey = p.Persist().PublicNodeKey()
}
if ourNodeKey.IsZero() {
return errors.New("no node-key: is tailscale logged in?")
}
b.mu.Unlock()
_, err := b.tkaDoSyncSend(ourNodeKey, aum.Hash(), []tka.AUM{*aum}, false)
b.mu.Lock()
return err
}
var tkaSuffixEncoder = base64.RawStdEncoding
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to

View File

@@ -17,6 +17,7 @@ import (
"github.com/google/go-cmp/cmp"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
@@ -65,6 +66,8 @@ func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server,
}
func TestTKAEnablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nodePriv := key.NewNode()
// Make a fake TKA authority, getting a usable genesis AUM which
@@ -147,13 +150,12 @@ func TestTKAEnablementFlow(t *testing.T) {
},
}).View()))
b := LocalBackend{
capTailnetLock: true,
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
pm: pm,
store: pm.Store(),
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
pm: pm,
store: pm.Store(),
}
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
@@ -172,6 +174,8 @@ func TestTKAEnablementFlow(t *testing.T) {
}
func TestTKADisablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
@@ -293,6 +297,9 @@ func TestTKADisablementFlow(t *testing.T) {
}
func TestTKASync(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
someKeyPriv := key.NewNLPrivate()
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
@@ -531,6 +538,9 @@ func TestTKASync(t *testing.T) {
}
func TestTKAFilterNetmap(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nlPriv := key.NewNLPrivate()
nlKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
storage := &tka.Mem{}
@@ -587,6 +597,8 @@ func TestTKAFilterNetmap(t *testing.T) {
}
func TestTKADisable(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
@@ -680,6 +692,8 @@ func TestTKADisable(t *testing.T) {
}
func TestTKASign(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nodePriv := key.NewNode()
toSign := key.NewNode()
nlPriv := key.NewNLPrivate()
@@ -766,6 +780,8 @@ func TestTKASign(t *testing.T) {
}
func TestTKAForceDisable(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
@@ -994,129 +1010,3 @@ func TestTKAAffectedSigs(t *testing.T) {
})
}
}
func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
nodePriv := key.NewNode()
nlPriv := key.NewNLPrivate()
cosignPriv := key.NewNLPrivate()
compromisedPriv := key.NewNLPrivate()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
cosignKey := tka.Key{Kind: tka.Key25519, Public: cosignPriv.Public().Verifier(), Votes: 2}
compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1}
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{key, compromisedKey, cosignKey},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, 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/sync/send":
body := new(tailcfg.TKASyncSendRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
t.Logf("got sync send:\n%+v", body)
var remoteHead tka.AUMHash
if err := remoteHead.UnmarshalText([]byte(body.Head)); err != nil {
t.Fatalf("head unmarshal: %v", err)
}
toApply := make([]tka.AUM, len(body.MissingAUMs))
for i, a := range body.MissingAUMs {
if err := toApply[i].Unserialize(a); err != nil {
t.Fatalf("decoding missingAUM[%d]: %v", i, err)
}
}
// Apply the recovery AUM to an authority to make sure it works.
if err := authority.Inform(chonk, toApply); err != nil {
t.Errorf("recovery AUM could not be applied: %v", err)
}
// Make sure the key we removed isn't trusted.
if authority.KeyTrusted(compromisedPriv.KeyID()) {
t.Error("compromised key was not removed from tka")
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASubmitSignatureResponse{}); 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,
},
pm: pm,
store: pm.Store(),
}
aum, err := b.NetworkLockGenerateRecoveryAUM([]tkatype.KeyID{compromisedPriv.KeyID()}, tka.AUMHash{})
if err != nil {
t.Fatalf("NetworkLockGenerateRecoveryAUM() failed: %v", err)
}
// Cosign using the cosigning key.
{
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: cosignPriv,
},
}).View()))
b := LocalBackend{
varRoot: temp,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
pm: pm,
store: pm.Store(),
}
if aum, err = b.NetworkLockCosignRecoveryAUM(aum); err != nil {
t.Fatalf("NetworkLockCosignRecoveryAUM() failed: %v", err)
}
}
// Finally, submit the recovery AUM. Validation is done
// in the fake control handler.
if err := b.NetworkLockSubmitRecoveryAUM(aum); err != nil {
t.Errorf("NetworkLockSubmitRecoveryAUM() failed: %v", err)
}
}

View File

@@ -304,7 +304,7 @@ func (s *peerAPIServer) DeleteFile(baseName string) error {
}
var bo *backoff.Backoff
logf := s.b.logf
t0 := s.b.clock.Now()
t0 := time.Now()
for {
err := os.Remove(path)
if err != nil && !os.IsNotExist(err) {
@@ -323,7 +323,7 @@ func (s *peerAPIServer) DeleteFile(baseName string) error {
if bo == nil {
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
}
if s.b.clock.Since(t0) < 5*time.Second {
if time.Since(t0) < 5*time.Second {
bo.BackOff(context.Background(), err)
continue
}
@@ -902,8 +902,8 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
for label := range stats.Stats {
labels = append(labels, label)
}
slices.SortFunc(labels, func(a, b sockstats.Label) int {
return strings.Compare(a.String(), b.String())
slices.SortFunc(labels, func(a, b sockstats.Label) bool {
return a.String() < b.String()
})
txTotal := uint64(0)
@@ -1000,7 +1000,7 @@ func (f *incomingFile) Write(p []byte) (n int, err error) {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := b.clock.Now()
now := time.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
@@ -1028,7 +1028,7 @@ func (h *peerAPIHandler) canPutFile() bool {
// Unsigned peers can't send files.
return false
}
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityFileSharingSend)
return h.isSelf || h.peerHasCap(tailcfg.CapabilityFileSharingSend)
}
// canDebug reports whether h can debug this node (goroutines, metrics,
@@ -1042,7 +1042,7 @@ func (h *peerAPIHandler) canDebug() bool {
// Unsigned peers can't debug.
return false
}
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer)
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
}
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
@@ -1050,18 +1050,23 @@ func (h *peerAPIHandler) canWakeOnLAN() bool {
if h.peerNode.UnsignedPeerAPIOnly {
return false
}
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
return h.isSelf || h.peerHasCap(tailcfg.CapabilityWakeOnLAN)
}
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
// canIngress reports whether h can send ingress requests to this node.
func (h *peerAPIHandler) canIngress() bool {
return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf)
return h.peerHasCap(tailcfg.CapabilityIngress) || (allowSelfIngress() && h.isSelf)
}
func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
for _, hasCap := range h.ps.b.PeerCaps(h.remoteAddr.Addr()) {
if hasCap == wantCap {
return true
}
}
return false
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
@@ -1113,7 +1118,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad filename", 400)
return
}
t0 := h.ps.b.clock.Now()
t0 := time.Now()
// TODO(bradfitz): prevent same filename being sent by two peers at once
partialFile := dstFile + partialSuffix
f, err := os.Create(partialFile)
@@ -1133,7 +1138,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if r.ContentLength != 0 {
inFile = &incomingFile{
name: baseName,
started: h.ps.b.clock.Now(),
started: time.Now(),
size: r.ContentLength,
w: f,
ph: h,
@@ -1171,7 +1176,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
}
}
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
d := time.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
// TODO: set modtime
@@ -1282,8 +1287,8 @@ func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request)
return
}
var password []byte // TODO(bradfitz): support?
st := h.ps.b.sys.NetMon.Get().InterfaceState()
if st == nil {
st, err := interfaces.GetState()
if err != nil {
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
return
}

View File

@@ -457,7 +457,6 @@ func TestHandlePeerAPI(t *testing.T) {
logf: e.logBuf.Logf,
capFileSharing: tt.capSharing,
netMap: &netmap.NetworkMap{SelfNode: selfNode},
clock: &tstest.Clock{},
}
e.ph = &peerAPIHandler{
isSelf: tt.isSelf,
@@ -507,7 +506,6 @@ func TestFileDeleteRace(t *testing.T) {
b: &LocalBackend{
logf: t.Logf,
capFileSharing: true,
clock: &tstest.Clock{},
},
rootDir: dir,
}

View File

@@ -51,10 +51,6 @@ func (pm *profileManager) dlogf(format string, args ...any) {
pm.logf(format, args...)
}
func (pm *profileManager) WriteState(id ipn.StateKey, val []byte) error {
return ipn.WriteState(pm.store, id, val)
}
// CurrentUserID returns the current user ID. It is only non-empty on
// Windows where we have a multi-user system.
func (pm *profileManager) CurrentUserID() ipn.WindowsUserID {
@@ -186,9 +182,9 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
}
if pm.prefs.ForceDaemon() {
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
return pm.store.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
} else {
return pm.WriteState(ipn.ServerModeStartKey, nil)
return pm.store.WriteState(ipn.ServerModeStartKey, nil)
}
}
@@ -292,7 +288,7 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
if key == "" {
return nil
}
if err := pm.WriteState(key, prefs.ToBytes()); err != nil {
if err := pm.store.WriteState(key, prefs.ToBytes()); err != nil {
pm.logf("WriteState(%q): %v", key, err)
return err
}
@@ -302,8 +298,8 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
// Profiles returns the list of known profiles.
func (pm *profileManager) Profiles() []ipn.LoginProfile {
profiles := pm.matchingProfiles(func(*ipn.LoginProfile) bool { return true })
slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) int {
return strings.Compare(a.Name, b.Name)
slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) bool {
return a.Name < b.Name
})
out := make([]ipn.LoginProfile, 0, len(profiles))
for _, p := range profiles {
@@ -340,7 +336,7 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
func (pm *profileManager) setAsUserSelectedProfileLocked() error {
k := ipn.CurrentProfileKey(string(pm.currentUserID))
return pm.WriteState(k, []byte(pm.currentProfile.Key))
return pm.store.WriteState(k, []byte(pm.currentProfile.Key))
}
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
@@ -398,7 +394,7 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
if kp.ID == pm.currentProfile.ID {
pm.NewProfile()
}
if err := pm.WriteState(kp.Key, nil); err != nil {
if err := pm.store.WriteState(kp.Key, nil); err != nil {
return err
}
delete(pm.knownProfiles, id)
@@ -411,7 +407,7 @@ func (pm *profileManager) DeleteAllProfiles() error {
metricDeleteAllProfile.Add(1)
for _, kp := range pm.knownProfiles {
if err := pm.WriteState(kp.Key, nil); err != nil {
if err := pm.store.WriteState(kp.Key, nil); err != nil {
// Write to remove references to profiles we've already deleted, but
// return the original error.
pm.writeKnownProfiles()
@@ -428,7 +424,7 @@ func (pm *profileManager) writeKnownProfiles() error {
if err != nil {
return err
}
return pm.WriteState(ipn.KnownProfilesStateKey, b)
return pm.store.WriteState(ipn.KnownProfilesStateKey, b)
}
// NewProfile creates and switches to a new unnamed profile. The new profile is

View File

@@ -372,7 +372,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni, false)
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
@@ -415,9 +415,6 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
hostname := r.Host
if r.TLS == nil {
tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix
if host, _, err := net.SplitHostPort(hostname); err == nil {
hostname = host
}
if !strings.HasSuffix(hostname, tcd) {
hostname += tcd
}
@@ -499,7 +496,6 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
// Clear any incoming values squatting in the headers.
r.Out.Header.Del("Tailscale-User-Login")
r.Out.Header.Del("Tailscale-User-Name")
r.Out.Header.Del("Tailscale-User-Profile-Pic")
r.Out.Header.Del("Tailscale-Headers-Info")
c, ok := getServeHTTPContext(r.Out)
@@ -517,7 +513,6 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
}
r.Out.Header.Set("Tailscale-User-Login", user.LoginName)
r.Out.Header.Set("Tailscale-User-Name", user.DisplayName)
r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL)
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
}
@@ -677,7 +672,7 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, hi.ServerName, false)
pair, err := b.GetCertPEM(ctx, hi.ServerName)
if err != nil {
return nil, err
}

View File

@@ -195,9 +195,8 @@ func TestServeHTTPProxy(t *testing.T) {
},
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
tailcfg.UserID(1): {
LoginName: "someone@example.com",
DisplayName: "Some One",
ProfilePicURL: "https://example.com/photo.jpg",
LoginName: "someone@example.com",
DisplayName: "Some One",
},
},
}
@@ -254,7 +253,6 @@ func TestServeHTTPProxy(t *testing.T) {
{"X-Forwarded-For", "100.150.151.152"},
{"Tailscale-User-Login", "someone@example.com"},
{"Tailscale-User-Name", "Some One"},
{"Tailscale-User-Profile-Pic", "https://example.com/photo.jpg"},
{"Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers"},
},
},
@@ -266,7 +264,6 @@ func TestServeHTTPProxy(t *testing.T) {
{"X-Forwarded-For", "100.150.151.153"},
{"Tailscale-User-Login", ""},
{"Tailscale-User-Name", ""},
{"Tailscale-User-Profile-Pic", ""},
{"Tailscale-Headers-Info", ""},
},
},
@@ -278,7 +275,6 @@ func TestServeHTTPProxy(t *testing.T) {
{"X-Forwarded-For", "100.160.161.162"},
{"Tailscale-User-Login", ""},
{"Tailscale-User-Name", ""},
{"Tailscale-User-Profile-Pic", ""},
{"Tailscale-Headers-Info", ""},
},
},

View File

@@ -37,7 +37,7 @@ func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request)
return
}
dialContext := logpolicy.MakeDialFunc(s.netMon, s.logf)
dialContext := logpolicy.MakeDialFunc(s.netMon)
back, err := dialContext(ctx, "tcp", hostPort)
if err != nil {
s.logf("error CONNECT dialing %v: %v", hostPort, err)

View File

@@ -223,8 +223,9 @@ type PeerStatus struct {
LastSeen time.Time // last seen to tailcontrol; only present if offline
LastHandshake time.Time // with local wireguard
Online bool // whether node is connected to the control plane
ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
@@ -273,8 +274,6 @@ type PeerStatus struct {
// KeyExpiry, if present, is the time at which the node key expired or
// will expire.
KeyExpiry *time.Time `json:",omitempty"`
Location *tailcfg.Location `json:",omitempty"`
}
type StatusBuilder struct {
@@ -438,6 +437,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if st.InEngine {
e.InEngine = true
}
if st.KeepAlive {
e.KeepAlive = true
}
if st.ExitNode {
e.ExitNode = true
}
@@ -459,7 +461,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if t := st.KeyExpiry; t != nil {
e.KeyExpiry = ptr.To(*t)
}
e.Location = st.Location
}
type StatusUpdater interface {

View File

@@ -23,7 +23,7 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal handler config wired wrong", 500)
return
}
pair, err := h.b.GetCertPEM(r.Context(), domain, true)
pair, err := h.b.GetCertPEM(r.Context(), domain)
if err != nil {
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
// GetCertPEM (and everywhere) should carry info info to get whether

View File

@@ -39,16 +39,13 @@ import (
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/ptr"
"tailscale.com/types/tkatype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/osdiag"
"tailscale.com/version"
)
@@ -108,13 +105,9 @@ var handler = map[string]localAPIHandler{
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
"tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM,
"tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM,
"tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
"whois": (*Handler).serveWhoIs,
"query-feature": (*Handler).serveQueryFeature,
}
func randHex(n int) string {
@@ -136,7 +129,7 @@ var (
// NewHandler creates a new LocalAPI HTTP handler. All parameters except netMon
// are required (if non-nil it's used to do faster interface lookups).
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, netMon *netmon.Monitor, logID logid.PublicID) *Handler {
return &Handler{b: b, logf: logf, netMon: netMon, backendLogID: logID, clock: tstime.StdClock{}}
return &Handler{b: b, logf: logf, netMon: netMon, backendLogID: logID}
}
type Handler struct {
@@ -162,7 +155,6 @@ type Handler struct {
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
backendLogID logid.PublicID
clock tstime.Clock
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -317,7 +309,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging
logMarker := func() string {
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), randHex(8))
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
}
if envknob.NoLogsNoSupport() {
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
@@ -351,9 +343,6 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
// logs for them.
envknob.LogCurrent(logger.WithPrefix(h.logf, "user bugreport: "))
// OS-specific details
osdiag.LogSupportInfo(logger.WithPrefix(h.logf, "user bugreport OS: "), osdiag.LogSupportInfoReasonBugReport)
if defBool(r.URL.Query().Get("diagnose"), false) {
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
}
@@ -366,7 +355,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
return
}
until := h.clock.Now().Add(12 * time.Hour)
until := time.Now().Add(12 * time.Hour)
var changed map[string]bool
for _, component := range []string{"magicsock"} {
@@ -436,9 +425,9 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
return
}
res := &apitype.WhoIsResponse{
Node: n, // always non-nil per WhoIsResponse contract
UserProfile: &u, // always non-nil per WhoIsResponse contract
CapMap: b.PeerCaps(ipp.Addr()),
Node: n,
UserProfile: &u,
Caps: b.PeerCaps(ipp.Addr()),
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
@@ -777,7 +766,7 @@ func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Requ
}
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second))
err := h.b.SetComponentDebugLogging(component, time.Now().Add(time.Duration(secs)*time.Second))
var res struct {
Error string
}
@@ -1342,7 +1331,7 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
return
}
pingTypeStr := r.FormValue("type")
if pingTypeStr == "" {
if ipStr == "" {
http.Error(w, "missing 'type' parameter", 400)
return
}
@@ -1756,103 +1745,6 @@ func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
type verifyRequest struct {
Keys []tkatype.KeyID
ForkFrom string
}
var req verifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
return
}
var forkFrom tka.AUMHash
if req.ForkFrom != "" {
if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil {
http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest)
return
}
}
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(res.Serialize())
}
func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
body := io.LimitReader(r.Body, 1024*1024)
aumBytes, err := ioutil.ReadAll(body)
if err != nil {
http.Error(w, "reading AUM", http.StatusBadRequest)
return
}
var aum tka.AUM
if err := aum.Unserialize(aumBytes); err != nil {
http.Error(w, "decoding AUM", http.StatusBadRequest)
return
}
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(res.Serialize())
}
func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
body := io.LimitReader(r.Body, 1024*1024)
aumBytes, err := ioutil.ReadAll(body)
if err != nil {
http.Error(w, "reading AUM", http.StatusBadRequest)
return
}
var aum tka.AUM
if err := aum.Unserialize(aumBytes); err != nil {
http.Error(w, "decoding AUM", http.StatusBadRequest)
return
}
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// serveProfiles serves profile switching-related endpoints. Supported methods
// and paths are:
// - GET /profiles/: list all profiles (JSON-encoded array of ipn.LoginProfiles)
@@ -1937,66 +1829,6 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
}
}
// serveQueryFeature makes a request to the "/machine/feature/query"
// Noise endpoint to get instructions on how to enable a feature, such as
// Funnel, for the node's tailnet.
//
// This request itself does not directly enable the feature on behalf of
// the node, but rather returns information that can be presented to the
// acting user about where/how to enable the feature. If relevant, this
// includes a control URL the user can visit to explicitly consent to
// using the feature.
//
// See tailcfg.QueryFeatureResponse for full response structure.
func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
feature := r.FormValue("feature")
switch {
case !h.PermitRead:
http.Error(w, "access denied", http.StatusForbidden)
return
case r.Method != httpm.POST:
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
case feature == "":
http.Error(w, "missing feature", http.StatusInternalServerError)
return
}
nm := h.b.NetMap()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
}
b, err := json.Marshal(&tailcfg.QueryFeatureRequest{
NodeKey: nm.NodeKey,
Feature: feature,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
req, err := http.NewRequestWithContext(r.Context(),
"POST", "https://unused/machine/feature/query", bytes.NewReader(b))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := h.b.DoNoiseRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func defBool(a string, def bool) bool {
if a == "" {
return def
@@ -2055,7 +1887,7 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
// opting-out of rate limits. Limit ourselves to at most one message
// per 20ms and a burst of 60 log lines, which should be fast enough to
// not block for too long but slow enough that we can upload all lines.
logf = logger.SlowLoggerWithClock(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
logf = logger.SlowLoggerWithClock(r.Context(), logf, 20*time.Millisecond, 60, time.Now)
for _, line := range logRequest.Lines {
logf("%s", line)

View File

@@ -4,7 +4,6 @@
package ipn
import (
"bytes"
"context"
"errors"
"fmt"
@@ -72,22 +71,9 @@ type StateStore interface {
// ErrStateNotExist) if the ID doesn't have associated state.
ReadState(id StateKey) ([]byte, error)
// WriteState saves bs as the state associated with ID.
//
// Callers should generally use the ipn.WriteState wrapper func
// instead, which only writes if the value is different from what's
// already in the store.
WriteState(id StateKey, bs []byte) error
}
// WriteState is a wrapper around store.WriteState that only writes if
// the value is different from what's already in the store.
func WriteState(store StateStore, id StateKey, v []byte) error {
if was, err := store.ReadState(id); err == nil && bytes.Equal(was, v) {
return nil
}
return store.WriteState(id, v)
}
// StateStoreDialerSetter is an optional interface that StateStores
// can implement to allow the caller to set a custom dialer.
type StateStoreDialerSetter interface {
@@ -105,5 +91,5 @@ func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
return WriteState(store, id, fmt.Appendf(nil, "%d", val))
return store.WriteState(id, fmt.Appendf(nil, "%d", val))
}

View File

@@ -1,48 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"bytes"
"sync"
"testing"
"tailscale.com/util/mak"
)
type memStore struct {
mu sync.Mutex
writes int
m map[StateKey][]byte
}
func (s *memStore) ReadState(k StateKey) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
return bytes.Clone(s.m[k]), nil
}
func (s *memStore) WriteState(k StateKey, v []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
mak.Set(&s.m, k, bytes.Clone(v))
s.writes++
return nil
}
func TestWriteState(t *testing.T) {
var ss StateStore = new(memStore)
WriteState(ss, "foo", []byte("bar"))
WriteState(ss, "foo", []byte("bar"))
got, err := ss.ReadState("foo")
if err != nil {
t.Fatal(err)
}
if want := []byte("bar"); !bytes.Equal(got, want) {
t.Errorf("got %q; want %q", got, want)
}
if got, want := ss.(*memStore).writes, 1; got != want {
t.Errorf("got %d writes; want %d", got, want)
}
}

View File

@@ -38,30 +38,28 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.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.16.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.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/17a3db2c30d2/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/bb2c8f22eccf/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
@@ -69,17 +67,17 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/intern](https://pkg.go.dev/go4.org/intern) ([BSD-3-Clause](https://github.com/go4org/intern/blob/ae77deb06f29/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.11.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/ee73d164e760/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.10.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.10.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.11.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))

View File

@@ -31,7 +31,6 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
@@ -46,13 +45,12 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/17a3db2c30d2/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/bb2c8f22eccf/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
@@ -60,13 +58,13 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [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/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.11.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.10.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.10.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.11.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))

View File

@@ -35,13 +35,12 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.18/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/5c6286bb8c6e/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/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/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.2.6/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
@@ -50,9 +49,9 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.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.16.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.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/kr/fs](https://pkg.go.dev/github.com/kr/fs) ([BSD-3-Clause](https://github.com/kr/fs/blob/v0.1.0/LICENSE))
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.13/LICENSE))
@@ -61,7 +60,6 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/peterbourgon/ff/v3](https://pkg.go.dev/github.com/peterbourgon/ff/v3) ([Apache-2.0](https://github.com/peterbourgon/ff/blob/v3.3.0/LICENSE))
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
@@ -69,9 +67,9 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.5/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/17a3db2c30d2/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/bb2c8f22eccf/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/v0.11.0/LICENSE))
@@ -80,15 +78,15 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.11.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.10.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.10.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.11.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))

View File

@@ -14,12 +14,10 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [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/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/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/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
@@ -30,34 +28,28 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [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.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/4b0a5c5d37ea/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/84569fd814a9/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f63dace725d8/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/59dfb47dfef1/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.0/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.11.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.10.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.10.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.10.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.11.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [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))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
## Additional Dependencies

View File

@@ -114,7 +114,7 @@ func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *ne
logger := &Logger{
logf: logf,
filch: filch,
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, logf),
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon),
}
logger.logger = logtail.NewLogger(logtail.Config{
BaseURL: logpolicy.LogURL(),

View File

@@ -110,8 +110,6 @@ type Policy struct {
Logtail *logtail.Logger
// PublicID is the logger's instance identifier.
PublicID logid.PublicID
// Logf is where to write informational messages about this Logger.
Logf logger.Logf
}
// NewConfig creates a Config with collection and a newly generated PrivateID.
@@ -312,7 +310,7 @@ func winProgramDataAccessible(dir string) bool {
// log state for that command exists in dir, then the log state is
// moved from wherever it does exist, into dir. Leftover logs state
// in / and $CACHE_DIRECTORY is deleted.
func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
func tryFixLogStateLocation(dir, cmdname string) {
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
// These are the OSes where we might have written stuff into
@@ -322,13 +320,13 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
return
}
if cmdname == "" {
logf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
return
}
if dir == "/" {
// Trying to store things in / still. That's a bug, but don't
// abort hard.
logf("[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale")
log.Printf("[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale")
return
}
if os.Getuid() != 0 {
@@ -385,7 +383,7 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
existsInRoot, err := checkExists("/")
if err != nil {
logf("checking for configs in /: %v", err)
log.Printf("checking for configs in /: %v", err)
return
}
existsInCache := false
@@ -393,12 +391,12 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
if cacheDir != "" {
existsInCache, err = checkExists("/var/cache/tailscale")
if err != nil {
logf("checking for configs in %s: %v", cacheDir, err)
log.Printf("checking for configs in %s: %v", cacheDir, err)
}
}
existsInDest, err := checkExists(dir)
if err != nil {
logf("checking for configs in %s: %v", dir, err)
log.Printf("checking for configs in %s: %v", dir, err)
return
}
@@ -413,13 +411,13 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
// CACHE_DIRECTORY takes precedence over /, move files from
// there.
if err := moveFiles(cacheDir); err != nil {
logf("%v", err)
log.Print(err)
return
}
case existsInRoot:
// Files from root is better than nothing.
if err := moveFiles("/"); err != nil {
logf("%v", err)
log.Print(err)
return
}
}
@@ -441,32 +439,27 @@ func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
if os.IsNotExist(err) {
continue
} else if err != nil {
logf("stat %q: %v", p, err)
log.Printf("stat %q: %v", p, err)
return
}
if err := os.Remove(p); err != nil {
logf("rm %q: %v", p, err)
log.Printf("rm %q: %v", p, err)
}
}
}
}
// New returns a new log policy (a logger and its instance ID) for a given
// collection name.
//
// The netMon parameter is optional; if non-nil it's used to do faster
// interface lookups.
//
// The logf parameter is optional; if non-nil, information logs (e.g. when
// migrating state) are sent to that logger, and global changes to the log
// package are avoided. If nil, logs will be printed using log.Printf.
func New(collection string, netMon *netmon.Monitor, logf logger.Logf) *Policy {
return NewWithConfigPath(collection, "", "", netMon, logf)
// New returns a new log policy (a logger and its instance ID) for a
// given collection name.
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func New(collection string, netMon *netmon.Monitor) *Policy {
return NewWithConfigPath(collection, "", "", netMon)
}
// NewWithConfigPath is identical to New, but uses the specified directory and
// command name. If either is empty, it derives them automatically.
func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor, logf logger.Logf) *Policy {
// NewWithConfigPath is identical to New,
// but uses the specified directory and command name.
// If either is empty, it derives them automatically.
func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor) *Policy {
var lflags int
if term.IsTerminal(2) || runtime.GOOS == "windows" {
lflags = 0
@@ -495,12 +488,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
if cmdName == "" {
cmdName = version.CmdName()
}
useStdLogger := logf == nil
if useStdLogger {
logf = log.Printf
}
tryFixLogStateLocation(dir, cmdName, logf)
tryFixLogStateLocation(dir, cmdName)
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
@@ -568,7 +556,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
}
return w
},
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon, logf)},
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon)},
}
if collection == logtail.CollectionNode {
conf.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
@@ -577,13 +565,13 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
}
if envknob.NoLogsNoSupport() || inTest() {
logf("You have disabled logging. Tailscale will not be able to provide support.")
log.Println("You have disabled logging. Tailscale will not be able to provide support.")
conf.HTTPC = &http.Client{Transport: noopPretendSuccessTransport{}}
} else if val := getLogTarget(); val != "" {
logf("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
conf.BaseURL = val
u, _ := url.Parse(val)
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon, logf)}
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon)}
}
filchOptions := filch.Options{
@@ -600,7 +588,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
filchOptions.MaxFileSize = 1 << 20
} else {
// not a fatal error, we can leave the log files on the spinning disk
logf("Unable to create /tmp directory for log storage: %v\n", err)
log.Printf("Unable to create /tmp directory for log storage: %v\n", err)
}
}
@@ -611,7 +599,7 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
conf.Stderr = filchBuf.OrigStderr
}
}
lw := logtail.NewLogger(conf, logf)
lw := logtail.NewLogger(conf, log.Printf)
var logOutput io.Writer = lw
@@ -624,27 +612,24 @@ func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor,
}
}
if useStdLogger {
log.SetFlags(0) // other log flags are set on console, not here
log.SetOutput(logOutput)
}
log.SetFlags(0) // other log flags are set on console, not here
log.SetOutput(logOutput)
logf("Program starting: v%v, Go %v: %#v",
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long(),
goVersion(),
os.Args)
logf("LogID: %v", newc.PublicID)
log.Printf("LogID: %v", newc.PublicID)
if filchErr != nil {
logf("filch failed: %v", filchErr)
log.Printf("filch failed: %v", filchErr)
}
if earlyErrBuf.Len() != 0 {
logf("%s", earlyErrBuf.Bytes())
log.Printf("%s", earlyErrBuf.Bytes())
}
return &Policy{
Logtail: lw,
PublicID: newc.PublicID,
Logf: logf,
}
}
@@ -681,7 +666,7 @@ func (p *Policy) Close() {
// log upload if it can be done before ctx is canceled.
func (p *Policy) Shutdown(ctx context.Context) error {
if p.Logtail != nil {
p.Logf("flushing log.")
log.Printf("flushing log.")
return p.Logtail.Shutdown(ctx)
}
return nil
@@ -695,14 +680,14 @@ func (p *Policy) Shutdown(ctx context.Context) error {
// for the benefit of older OS platforms which might not include it.
//
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func MakeDialFunc(netMon *netmon.Monitor, logf logger.Logf) func(ctx context.Context, netw, addr string) (net.Conn, error) {
func MakeDialFunc(netMon *netmon.Monitor) func(ctx context.Context, netw, addr string) (net.Conn, error) {
return func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialContext(ctx, netw, addr, netMon, logf)
return dialContext(ctx, netw, addr, netMon)
}
}
func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor, logf logger.Logf) (net.Conn, error) {
nd := netns.FromDialer(logf, netMon, &net.Dialer{
func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor) (net.Conn, error) {
nd := netns.FromDialer(log.Printf, netMon, &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
})
@@ -723,7 +708,7 @@ func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor,
err = errors.New(res.Status)
}
if err != nil {
logf("logtail: CONNECT response error from tailscaled: %v", err)
log.Printf("logtail: CONNECT response error from tailscaled: %v", err)
c.Close()
} else {
dialLog.Printf("connected via tailscaled")
@@ -733,29 +718,25 @@ func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor,
}
// If we failed to dial, try again with bootstrap DNS.
logf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.MakeLookupFunc(logf, netMon),
LookupIPFallback: dnsfallback.MakeLookupFunc(log.Printf, netMon),
NetMon: netMon,
}
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
c, err = dialer(ctx, netw, addr)
if err == nil {
logf("logtail: bootstrap dial succeeded")
log.Printf("logtail: bootstrap dial succeeded")
}
return c, err
}
// NewLogtailTransport returns an HTTP Transport particularly suited to uploading
// logs to the given host name. See DialContext for details on how it works.
//
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
//
// The logf parameter is optional; if non-nil, logs are printed using the
// provided function; if nil, log.Printf will be used instead.
func NewLogtailTransport(host string, netMon *netmon.Monitor, logf logger.Logf) http.RoundTripper {
func NewLogtailTransport(host string, netMon *netmon.Monitor) http.RoundTripper {
if inTest() {
return noopPretendSuccessTransport{}
}
@@ -771,10 +752,7 @@ func NewLogtailTransport(host string, netMon *netmon.Monitor, logf logger.Logf)
tr.DisableCompression = true
// Log whenever we dial:
if logf == nil {
logf = log.Printf
}
tr.DialContext = MakeDialFunc(netMon, logf)
tr.DialContext = MakeDialFunc(netMon)
// We're contacting exactly 1 hostname, so the default's 100
// max idle conns is very high for our needs. Even 2 is

View File

@@ -9,7 +9,6 @@ import (
"math/rand"
"time"
"tailscale.com/tstime"
"tailscale.com/types/logger"
)
@@ -24,8 +23,9 @@ type Backoff struct {
// logf is the function used for log messages when backing off.
logf logger.Logf
// tstime.Clock.NewTimer is used instead time.NewTimer.
Clock tstime.Clock
// NewTimer is the function that acts like time.NewTimer.
// It's for use in unit tests.
NewTimer func(time.Duration) *time.Timer
// LogLongerThan sets the minimum time of a single backoff interval
// before we mention it in the log.
@@ -40,7 +40,7 @@ func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backof
name: name,
logf: logf,
maxBackoff: maxBackoff,
Clock: tstime.StdClock{},
NewTimer: time.NewTimer,
}
}
@@ -72,10 +72,10 @@ func (b *Backoff) BackOff(ctx context.Context, err error) {
if d >= b.LogLongerThan {
b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
}
t, tChannel := b.Clock.NewTimer(d)
t := b.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-tChannel:
case <-t.C:
}
}

View File

@@ -49,18 +49,18 @@ type Encoder interface {
}
type Config struct {
Collection string // collection name, a domain name
PrivateID logid.PrivateID // private ID for the primary log stream
CopyPrivateID logid.PrivateID // private ID for a log stream that is a superset of this log stream
BaseURL string // if empty defaults to "https://log.tailscale.io"
HTTPC *http.Client // if empty defaults to http.DefaultClient
SkipClientTime bool // if true, client_time is not written to logs
LowMemory bool // if true, logtail minimizes memory use
Clock tstime.Clock // if set, Clock.Now substitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
Buffer Buffer // temp storage, if nil a MemoryBuffer
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
Collection string // collection name, a domain name
PrivateID logid.PrivateID // private ID for the primary log stream
CopyPrivateID logid.PrivateID // private ID for a log stream that is a superset of this log stream
BaseURL string // if empty defaults to "https://log.tailscale.io"
HTTPC *http.Client // if empty defaults to http.DefaultClient
SkipClientTime bool // if true, client_time is not written to logs
LowMemory bool // if true, logtail minimizes memory use
TimeNow func() time.Time // if set, substitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
Buffer Buffer // temp storage, if nil a MemoryBuffer
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
// MetricsDelta, if non-nil, is a func that returns an encoding
// delta in clientmetrics to upload alongside existing logs.
@@ -94,8 +94,8 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
if cfg.HTTPC == nil {
cfg.HTTPC = http.DefaultClient
}
if cfg.Clock == nil {
cfg.Clock = tstime.StdClock{}
if cfg.TimeNow == nil {
cfg.TimeNow = time.Now
}
if cfg.Stderr == nil {
cfg.Stderr = os.Stderr
@@ -144,8 +144,9 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
drainWake: make(chan struct{}, 1),
sentinel: make(chan int32, 16),
flushDelayFn: cfg.FlushDelayFn,
clock: cfg.Clock,
timeNow: cfg.TimeNow,
metricsDelta: cfg.MetricsDelta,
sockstatsLabel: sockstats.LabelLogtailLogger,
procID: procID,
includeProcSequence: cfg.IncludeProcSequence,
@@ -153,7 +154,6 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
shutdownStart: make(chan struct{}),
shutdownDone: make(chan struct{}),
}
l.SetSockstatsLabel(sockstats.LabelLogtailLogger)
if cfg.NewZstdEncoder != nil {
l.zstdEncoder = cfg.NewZstdEncoder()
}
@@ -181,32 +181,27 @@ type Logger struct {
flushDelayFn func() time.Duration // negative or zero return value to upload aggressively, or >0 to batch at this delay
flushPending atomic.Bool
sentinel chan int32
clock tstime.Clock
timeNow func() time.Time
zstdEncoder Encoder
uploadCancel func()
explainedRaw bool
metricsDelta func() string // or nil
privateID logid.PrivateID
httpDoCalls atomic.Int32
sockstatsLabel atomicSocktatsLabel
sockstatsLabel sockstats.Label
procID uint32
includeProcSequence bool
writeLock sync.Mutex // guards procSequence, flushTimer, buffer.Write calls
procSequence uint64
flushTimer tstime.TimerController // used when flushDelay is >0
flushTimer *time.Timer // used when flushDelay is >0
shutdownStartMu sync.Mutex // guards the closing of shutdownStart
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closed when shutdown complete
}
type atomicSocktatsLabel struct{ p atomic.Uint32 }
func (p *atomicSocktatsLabel) Load() sockstats.Label { return sockstats.Label(p.p.Load()) }
func (p *atomicSocktatsLabel) Store(label sockstats.Label) { p.p.Store(uint32(label)) }
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
@@ -224,7 +219,7 @@ func (l *Logger) SetNetMon(lm *netmon.Monitor) {
// SetSockstatsLabel sets the label used in sockstat logs to identify network traffic from this logger.
func (l *Logger) SetSockstatsLabel(label sockstats.Label) {
l.sockstatsLabel.Store(label)
l.sockstatsLabel = label
}
// PrivateID returns the logger's private log ID.
@@ -380,7 +375,7 @@ func (l *Logger) uploading(ctx context.Context) {
retryAfter, err := l.upload(ctx, body, origlen)
if err != nil {
numFailures++
firstFailure = l.clock.Now()
firstFailure = time.Now()
if !l.internetUp() {
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
@@ -403,7 +398,7 @@ func (l *Logger) uploading(ctx context.Context) {
} else {
// Only print a success message after recovery.
if numFailures > 0 {
fmt.Fprintf(l.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, l.clock.Since(firstFailure).Round(time.Second))
fmt.Fprintf(l.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, time.Since(firstFailure).Round(time.Second))
}
break
}
@@ -450,7 +445,7 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
// origlen of -1 indicates that the body is not compressed.
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAfter time.Duration, err error) {
const maxUploadTime = 45 * time.Second
ctx = sockstats.WithSockStats(ctx, l.sockstatsLabel.Load(), l.Logf)
ctx = sockstats.WithSockStats(ctx, l.sockstatsLabel, l.Logf)
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
defer cancel()
@@ -545,7 +540,7 @@ func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
if flushDelay > 0 {
if l.flushPending.CompareAndSwap(false, true) {
if l.flushTimer == nil {
l.flushTimer = l.clock.AfterFunc(flushDelay, l.tryDrainWake)
l.flushTimer = time.AfterFunc(flushDelay, l.tryDrainWake)
} else {
l.flushTimer.Reset(flushDelay)
}
@@ -559,7 +554,7 @@ func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
// TODO: instead of allocating, this should probably just append
// directly into the output log buffer.
func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, procSequence uint64, level int) []byte {
now := l.clock.Now()
now := l.timeNow()
// Factor in JSON encoding overhead to try to only do one alloc
// in the make below (so appends don't resize the buffer).
@@ -674,7 +669,7 @@ func (l *Logger) encodeLocked(buf []byte, level int) []byte {
return l.encodeText(buf, l.skipClientTime, l.procID, l.procSequence, level) // text fast-path
}
now := l.clock.Now()
now := l.timeNow()
obj := make(map[string]any)
if err := json.Unmarshal(buf, &obj); err != nil {

View File

@@ -15,7 +15,6 @@ import (
"time"
"tailscale.com/tstest"
"tailscale.com/tstime"
)
func TestFastShutdown(t *testing.T) {
@@ -213,7 +212,7 @@ func TestEncodeSpecialCases(t *testing.T) {
var sink []byte
func TestLoggerEncodeTextAllocs(t *testing.T) {
lg := &Logger{clock: tstime.StdClock{}}
lg := &Logger{timeNow: time.Now}
inBuf := []byte("some text to encode")
procID := uint32(0x24d32ee9)
procSequence := uint64(0x12346)
@@ -227,8 +226,8 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
func TestLoggerWriteLength(t *testing.T) {
lg := &Logger{
clock: tstime.StdClock{},
buffer: NewMemoryBuffer(1024),
timeNow: time.Now,
buffer: NewMemoryBuffer(1024),
}
inBuf := []byte("some text to encode")
n, err := lg.Write(inBuf)
@@ -310,7 +309,7 @@ func unmarshalOne(t *testing.T, body []byte) map[string]any {
}
func TestEncodeTextTruncation(t *testing.T) {
lg := &Logger{clock: tstime.StdClock{}, lowMem: true}
lg := &Logger{timeNow: time.Now, lowMem: true}
in := bytes.Repeat([]byte("a"), 5120)
b := lg.encodeText(in, true, 0, 0, 0)
got := string(b)
@@ -364,7 +363,7 @@ func TestEncode(t *testing.T) {
for _, tt := range tests {
buf := new(simpleMemBuf)
lg := &Logger{
clock: tstest.NewClock(tstest.ClockOpts{Start: time.Unix(123, 456).UTC()}),
timeNow: func() time.Time { return time.Unix(123, 456).UTC() },
buffer: buf,
procID: 7,
procSequence: 1,

View File

@@ -5,14 +5,7 @@
// Tailscale for monitoring.
package metrics
import (
"expvar"
"fmt"
"io"
"strings"
"golang.org/x/exp/slices"
)
import "expvar"
// Set is a string-to-Var map variable that satisfies the expvar.Var
// interface.
@@ -52,14 +45,6 @@ func (m *LabelMap) Get(key string) *expvar.Int {
return m.Map.Get(key).(*expvar.Int)
}
// GetIncrFunc returns a function that increments the expvar.Int named by key.
//
// Most callers should not need this; it exists to satisfy an
// interface elsewhere.
func (m *LabelMap) GetIncrFunc(key string) func(delta int64) {
return m.Get(key).Add
}
// GetFloat returns a direct pointer to the expvar.Float for key, creating it
// if necessary.
func (m *LabelMap) GetFloat(key string) *expvar.Float {
@@ -73,92 +58,3 @@ func (m *LabelMap) GetFloat(key string) *expvar.Float {
func CurrentFDs() int {
return currentFDs()
}
// Histogram is a histogram of values.
// It should be created with NewHistogram.
type Histogram struct {
// buckets is a list of bucket boundaries, in increasing order.
buckets []float64
// bucketStrings is a list of the same buckets, but as strings.
// This are allocated once at creation time by NewHistogram.
bucketStrings []string
bucketVars []expvar.Int
sum expvar.Float
count expvar.Int
}
// NewHistogram returns a new histogram that reports to the given
// expvar map under the given name.
//
// The buckets are the boundaries of the histogram buckets, in
// increasing order. The last bucket is +Inf.
func NewHistogram(buckets []float64) *Histogram {
if !slices.IsSorted(buckets) {
panic("buckets must be sorted")
}
labels := make([]string, len(buckets))
for i, b := range buckets {
labels[i] = fmt.Sprintf("%v", b)
}
h := &Histogram{
buckets: buckets,
bucketStrings: labels,
bucketVars: make([]expvar.Int, len(buckets)),
}
return h
}
// Observe records a new observation in the histogram.
func (h *Histogram) Observe(v float64) {
h.sum.Add(v)
h.count.Add(1)
for i, b := range h.buckets {
if v <= b {
h.bucketVars[i].Add(1)
}
}
}
// String returns a JSON representation of the histogram.
// This is used to satisfy the expvar.Var interface.
func (h *Histogram) String() string {
var b strings.Builder
fmt.Fprintf(&b, "{")
first := true
h.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(&b, ",")
}
fmt.Fprintf(&b, "%q: ", kv.Key)
if kv.Value != nil {
fmt.Fprintf(&b, "%v", kv.Value)
} else {
fmt.Fprint(&b, "null")
}
first = false
})
fmt.Fprintf(&b, "\"sum\": %v,", &h.sum)
fmt.Fprintf(&b, "\"count\": %v", &h.count)
fmt.Fprintf(&b, "}")
return b.String()
}
// Do calls f for each bucket in the histogram.
func (h *Histogram) Do(f func(expvar.KeyValue)) {
for i := range h.bucketVars {
f(expvar.KeyValue{Key: h.bucketStrings[i], Value: &h.bucketVars[i]})
}
f(expvar.KeyValue{Key: "+Inf", Value: &h.count})
}
// PromExport writes the histogram to w in Prometheus exposition format.
func (h *Histogram) PromExport(w io.Writer, name string) {
fmt.Fprintf(w, "# TYPE %s histogram\n", name)
h.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s_bucket{le=%q} %v\n", name, kv.Key, kv.Value)
})
fmt.Fprintf(w, "%s_sum %v\n", name, &h.sum)
fmt.Fprintf(w, "%s_count %v\n", name, &h.count)
}

View File

@@ -11,18 +11,6 @@ import (
"tailscale.com/tstest"
)
func TestLabelMap(t *testing.T) {
var m LabelMap
m.GetIncrFunc("foo")(1)
m.GetIncrFunc("bar")(2)
if g, w := m.Get("foo").Value(), int64(1); g != w {
t.Errorf("foo = %v; want %v", g, w)
}
if g, w := m.Get("bar").Value(), int64(2); g != w {
t.Errorf("bar = %v; want %v", g, w)
}
}
func TestCurrentFileDescriptors(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skipf("skipping on %v", runtime.GOOS)

View File

@@ -16,5 +16,4 @@ func TestMain(m *testing.M) {
// TODO: https://github.com/tailscale/tailscale/issues/7866
os.Exit(0)
}
os.Exit(m.Run())
}

View File

@@ -8,16 +8,10 @@ import (
"fmt"
"io"
"math/bits"
"net/netip"
"strconv"
"strings"
)
const (
debugStrideInsert = false
debugStrideDelete = false
)
// strideEntry is a strideTable entry.
type strideEntry[T any] struct {
// prefixIndex is the prefixIndex(...) value that caused this stride entry's
@@ -38,12 +32,6 @@ type strideEntry[T any] struct {
// The leaves of the binary tree are host routes (/8s). Each parent is a
// successively larger prefix that encompasses its children (/7 through /0).
type strideTable[T any] struct {
// prefix is the prefix represented by the 0/0 route of this
// strideTable. It is used in multi-level tables to support path
// compression. All strideTables must have a valid prefix
// (non-zero value, passes IsValid()) whose length is a multiple
// of 8 (e.g. /8, /16, but not /15).
prefix netip.Prefix
// entries is the nodes of the binary tree, laid out in a flattened array.
//
// The array indices are arranged by the prefixIndex function, such that the
@@ -56,10 +44,10 @@ type strideTable[T any] struct {
// memory trickery to store the refcount, but this is Go, where we don't
// store random bits in pointers lest we confuse the GC)
entries [lastHostIndex + 1]strideEntry[T]
// routeRefs is the number of route entries in this table.
routeRefs uint16
// childRefs is the number of child strideTables referenced by this table.
childRefs uint16
// refs is the number of route entries and child strideTables referenced by
// this table. It is used in the multi-layered logic to determine when this
// table is empty and can be deleted.
refs int
}
const (
@@ -80,56 +68,25 @@ func (t *strideTable[T]) getChild(addr uint8) (child *strideTable[T], idx int) {
// obtained via a call to getChild.
func (t *strideTable[T]) deleteChild(idx int) {
t.entries[idx].child = nil
t.childRefs--
}
// setChild replaces the child strideTable for addr (if any) with child.
func (t *strideTable[T]) setChild(addr uint8, child *strideTable[T]) {
t.setChildByIndex(hostIndex(addr), child)
}
// setChildByIndex replaces the child strideTable at idx (if any) with
// child. idx should be obtained via a call to getChild.
func (t *strideTable[T]) setChildByIndex(idx int, child *strideTable[T]) {
if t.entries[idx].child == nil {
t.childRefs++
}
t.entries[idx].child = child
t.refs--
}
// getOrCreateChild returns the child strideTable for addr, creating it if
// necessary.
func (t *strideTable[T]) getOrCreateChild(addr uint8) (child *strideTable[T], created bool) {
func (t *strideTable[T]) getOrCreateChild(addr uint8) *strideTable[T] {
idx := hostIndex(addr)
if t.entries[idx].child == nil {
t.entries[idx].child = &strideTable[T]{
prefix: childPrefixOf(t.prefix, addr),
}
t.childRefs++
return t.entries[idx].child, true
t.entries[idx].child = new(strideTable[T])
t.refs++
}
return t.entries[idx].child, false
return t.entries[idx].child
}
// getValAndChild returns both the prefix and child strideTable for
// addr. Both returned values can be nil if no entry of that type
// exists for addr.
func (t *strideTable[T]) getValAndChild(addr uint8) (*T, *strideTable[T]) {
idx := hostIndex(addr)
return t.entries[idx].value, t.entries[idx].child
}
// findFirstChild returns the first child strideTable in t, or nil if
// t has no children.
func (t *strideTable[T]) findFirstChild() *strideTable[T] {
for i := firstHostIndex; i <= lastHostIndex; i++ {
if child := t.entries[i].child; child != nil {
return child
}
}
return nil
}
// allot updates entries whose stored prefixIndex matches oldPrefixIndex, in the
// subtree rooted at idx. Matching entries have their stored prefixIndex set to
// newPrefixIndex, and their value set to val.
@@ -170,14 +127,12 @@ func (t *strideTable[T]) insert(addr uint8, prefixLen int, val *T) {
if oldIdx != idx {
// This route entry was freshly created (not just updated), that's a new
// reference.
t.routeRefs++
t.refs++
}
return
}
// delete removes the route addr/prefixLen from t. Returns the value
// that was associated with the deleted prefix, or nil if the prefix
// wasn't in the strideTable.
// delete removes the route addr/prefixLen from t.
func (t *strideTable[T]) delete(addr uint8, prefixLen int) *T {
idx := prefixIndex(addr, prefixLen)
recordedIdx := t.entries[idx].prefixIndex
@@ -189,7 +144,7 @@ func (t *strideTable[T]) delete(addr uint8, prefixLen int) *T {
parentIdx := idx >> 1
t.allot(idx, idx, t.entries[parentIdx].prefixIndex, t.entries[parentIdx].value)
t.routeRefs--
t.refs--
return val
}
@@ -228,7 +183,7 @@ func (t *strideTable[T]) treeDebugString() string {
func (t *strideTable[T]) treeDebugStringRec(w io.Writer, idx, indent int) {
addr, len := inversePrefixIndex(idx)
if t.entries[idx].prefixIndex != 0 && t.entries[idx].prefixIndex == idx {
fmt.Fprintf(w, "%s%d/%d (%02x/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx].value)
fmt.Fprintf(w, "%s%d/%d (%d/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx].value)
indent += 2
}
if idx >= firstHostIndex {
@@ -274,29 +229,3 @@ func formatPrefixTable(addr uint8, len int) string {
}
return fmt.Sprintf("%3d/%d", addr, len)
}
// childPrefixOf returns the child prefix of parent whose final byte
// is stride. The parent prefix must be byte-aligned
// (i.e. parent.Bits() must be a multiple of 8), and be no more
// specific than /24 for IPv4 or /120 for IPv6.
//
// For example, childPrefixOf("192.168.0.0/16", 8) == "192.168.8.0/24".
func childPrefixOf(parent netip.Prefix, stride uint8) netip.Prefix {
l := parent.Bits()
if l%8 != 0 {
panic("parent prefix is not 8-bit aligned")
}
if l >= parent.Addr().BitLen() {
panic("parent prefix cannot be extended further")
}
off := l / 8
if parent.Addr().Is4() {
bs := parent.Addr().As4()
bs[off] = stride
return netip.PrefixFrom(netip.AddrFrom4(bs), l+8)
} else {
bs := parent.Addr().As16()
bs[off] = stride
return netip.PrefixFrom(netip.AddrFrom16(bs), l+8)
}
}

View File

@@ -7,7 +7,6 @@ import (
"bytes"
"fmt"
"math/rand"
"net/netip"
"sort"
"strings"
"testing"
@@ -52,15 +51,11 @@ func TestStrideTableInsert(t *testing.T) {
slow := slowTable[int]{pfxs}
fast := strideTable[int]{}
if debugStrideInsert {
t.Logf("slow table:\n%s", slow.String())
}
t.Logf("slow table:\n%s", slow.String())
for _, pfx := range pfxs {
fast.insert(pfx.addr, pfx.len, pfx.val)
if debugStrideInsert {
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
}
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
}
for i := 0; i < 256; i++ {
@@ -105,7 +100,7 @@ func TestStrideTableInsertShuffled(t *testing.T) {
for _, route := range routes2 {
rt2.insert(route.addr, route.len, route.val)
}
if diff := cmp.Diff(rt, rt2, cmpDiffOpts...); diff != "" {
if diff := cmp.Diff(rt, rt2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
t.Errorf("tables ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
}
@@ -113,7 +108,7 @@ func TestStrideTableInsertShuffled(t *testing.T) {
for _, route := range routes2 {
rtZero2.insert(route.addr, route.len, &zero)
}
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" {
if diff := cmp.Diff(rtZero, rtZero2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
t.Errorf("tables with identical vals ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
}
}
@@ -126,15 +121,11 @@ func TestStrideTableDelete(t *testing.T) {
slow := slowTable[int]{pfxs}
fast := strideTable[int]{}
if debugStrideDelete {
t.Logf("slow table:\n%s", slow.String())
}
t.Logf("slow table:\n%s", slow.String())
for _, pfx := range pfxs {
fast.insert(pfx.addr, pfx.len, pfx.val)
if debugStrideDelete {
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
}
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
}
toDelete := pfxs[:50]
@@ -189,7 +180,7 @@ func TestStrideTableDeleteShuffle(t *testing.T) {
for _, route := range toDelete2 {
rt2.delete(route.addr, route.len)
}
if diff := cmp.Diff(rt, rt2, cmpDiffOpts...); diff != "" {
if diff := cmp.Diff(rt, rt2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
t.Errorf("tables ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
}
@@ -200,7 +191,7 @@ func TestStrideTableDeleteShuffle(t *testing.T) {
for _, route := range toDelete2 {
rtZero2.delete(route.addr, route.len)
}
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" {
if diff := cmp.Diff(rtZero, rtZero2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
t.Errorf("tables with identical vals ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
}
}
@@ -391,8 +382,3 @@ func formatSlowEntriesShort[T any](ents []slowEntry[T]) string {
}
return "[" + strings.Join(ret, " ") + "]"
}
var cmpDiffOpts = []cmp.Option{
cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{}),
cmp.Comparer(func(a, b netip.Prefix) bool { return a == b }),
}

View File

@@ -14,631 +14,149 @@ package art
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math/bits"
"net/netip"
"strings"
"sync"
)
const (
debugInsert = false
debugDelete = false
)
// Table is an IPv4 and IPv6 routing table.
type Table[T any] struct {
v4 strideTable[T]
v6 strideTable[T]
initOnce sync.Once
}
func (t *Table[T]) init() {
t.initOnce.Do(func() {
t.v4.prefix = netip.PrefixFrom(netip.IPv4Unspecified(), 0)
t.v6.prefix = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
})
}
func (t *Table[T]) tableForAddr(addr netip.Addr) *strideTable[T] {
if addr.Is6() {
return &t.v6
}
return &t.v4
v4 strideTable[T]
v6 strideTable[T]
}
// Get does a route lookup for addr and returns the associated value, or nil if
// no route matched.
func (t *Table[T]) Get(addr netip.Addr) *T {
t.init()
// Ideally we would use addr.AsSlice here, but AsSlice is just
// barely complex enough that it can't be inlined, and that in
// turn causes the slice to escape to the heap. Using As16 and
// manual slicing here helps the compiler keep Get alloc-free.
st := t.tableForAddr(addr)
rawAddr := addr.As16()
bs := rawAddr[:]
if addr.Is4() {
bs = bs[12:]
st := &t.v4
if addr.Is6() {
st = &t.v6
}
i := 0
// With path compression, we might skip over some address bits while walking
// to a strideTable leaf. This means the leaf answer we find might not be
// correct, because path compression took us down the wrong subtree. When
// that happens, we have to backtrack and figure out which most specific
// route further up the tree is relevant to addr, and return that.
//
// So, as we walk down the stride tables, each time we find a non-nil route
// result, we have to remember it and the associated strideTable prefix.
//
// We could also deal with this edge case of path compression by checking
// the strideTable prefix on each table as we descend, but that means we
// have to pay N prefix.Contains checks on every route lookup (where N is
// the number of strideTables in the path), rather than only paying M prefix
// comparisons in the edge case (where M is the number of strideTables in
// the path with a non-nil route of their own).
const maxDepth = 16
type prefixAndRoute struct {
prefix netip.Prefix
route *T
}
strideMatch := make([]prefixAndRoute, 0, maxDepth)
findLeaf:
for {
rt, child := st.getValAndChild(bs[i])
var ret *T
for _, stride := range addr.AsSlice() {
rt, child := st.getValAndChild(stride)
if rt != nil {
// This strideTable contains a route that may be relevant to our
// search, remember it.
strideMatch = append(strideMatch, prefixAndRoute{st.prefix, rt})
// Found a more specific route than whatever we found previously,
// keep a note.
ret = rt
}
if child == nil {
// No sub-routes further down, the last thing we recorded
// in strideRoutes is tentatively the result, barring
// misdirection from path compression.
break findLeaf
// No sub-routes further down, whatever we have recorded in ret is
// the result.
return ret
}
st = child
// Path compression means we may be skipping over some intermediate
// tables. We have to skip forward to whatever depth st now references.
i = st.prefix.Bits() / 8
}
// Walk backwards through the hits we recorded in strideRoutes and
// stridePrefixes, returning the first one whose subtree matches addr.
//
// In the common case where path compression did not mislead us, we'll
// return on the first loop iteration because the last route we recorded was
// the correct most-specific route.
for i := len(strideMatch) - 1; i >= 0; i-- {
if m := strideMatch[i]; m.prefix.Contains(addr) {
return m.route
}
}
// We either found no route hits at all (both previous loops terminated
// immediately), or we went on a wild goose chase down a compressed path for
// the wrong prefix, and also found no usable routes on the way back up to
// the root. This is a miss.
return nil
// Unreachable because Insert/Delete won't allow the leaf strideTables to
// have children, so we must return via the nil check in the loop.
panic("unreachable")
}
// Insert adds pfx to the table, with value val.
// If pfx is already present in the table, its value is set to val.
func (t *Table[T]) Insert(pfx netip.Prefix, val *T) {
t.init()
if val == nil {
panic("Table.Insert called with nil value")
}
// The standard library doesn't enforce normalized prefixes (where
// the non-prefix bits are all zero). These algorithms require
// normalized prefixes, so do it upfront.
pfx = pfx.Masked()
if debugInsert {
defer func() {
fmt.Printf("%s", t.debugSummary())
}()
fmt.Printf("\ninsert: start pfx=%s\n", pfx)
st := &t.v4
if pfx.Addr().Is6() {
st = &t.v6
}
st := t.tableForAddr(pfx.Addr())
// This algorithm is full of off-by-one headaches that boil down
// to the fact that pfx.Bits() has (2^n)+1 values, rather than
// just 2^n. For example, an IPv4 prefix length can be 0 through
// 32, which is 33 values.
//
// This extra possible value creates a lot of problems as we do
// bits and bytes math to traverse strideTables below. So, we
// treat the default route 0/0 specially here, that way the rest
// of the logic goes back to having 2^n values to reason about,
// which can be done in a nice and regular fashion with no edge
// cases.
if pfx.Bits() == 0 {
if debugInsert {
fmt.Printf("insert: default route\n")
}
st.insert(0, 0, val)
return
}
// No matter what we do as we traverse strideTables, our final
// action will be to insert the last 1-8 bits of pfx into a
// strideTable somewhere.
//
// We calculate upfront the byte position of the end of the
// prefix; the number of bits within that byte that contain prefix
// data; and the prefix of the strideTable into which we'll
// eventually insert.
//
// We need this in a couple different branches of the code below,
// and because the possible values are 1-indexed (1 through 32 for
// ipv4, 1 through 128 for ipv6), the math is very slightly
// unusual to account for the off-by-one indexing. Do it once up
// here, with this large comment, rather than reproduce the subtle
// math in multiple places further down.
finalByteIdx := (pfx.Bits() - 1) / 8
finalBits := pfx.Bits() - (finalByteIdx * 8)
finalStridePrefix, err := pfx.Addr().Prefix(finalByteIdx * 8)
if err != nil {
panic(fmt.Sprintf("invalid prefix requested: %s/%d", pfx.Addr(), finalByteIdx*8))
}
if debugInsert {
fmt.Printf("insert: finalByteIdx=%d finalBits=%d finalStridePrefix=%s\n", finalByteIdx, finalBits, finalStridePrefix)
}
// The strideTable we want to insert into is potentially at the
// end of a chain of strideTables, each one encoding 8 bits of the
// prefix.
//
// We're expecting to walk down a path of tables, although with
// prefix compression we may end up skipping some links in the
// chain, or taking wrong turns and having to course correct.
//
// As we walk down the tree, byteIdx is the byte of bs we're
// currently examining to choose our next step, and numBits is the
// number of bits that remain in pfx, starting with the byte at
// byteIdx inclusive.
bs := pfx.Addr().AsSlice()
byteIdx := 0
i := 0
numBits := pfx.Bits()
for {
if debugInsert {
fmt.Printf("insert: loop byteIdx=%d numBits=%d st.prefix=%s\n", byteIdx, numBits, st.prefix)
}
if numBits <= 8 {
if debugInsert {
fmt.Printf("insert: existing leaf st.prefix=%s addr=%d/%d\n", st.prefix, bs[finalByteIdx], finalBits)
}
// We've reached the end of the prefix, whichever
// strideTable we're looking at now is the place where we
// need to insert.
st.insert(bs[finalByteIdx], finalBits, val)
return
}
// Otherwise, we need to go down at least one more level of
// strideTables. With prefix compression, each level of
// descent can have one of three outcomes: we find a place
// where prefix compression is possible; a place where prefix
// compression made us take a "wrong turn"; or a point along
// our intended path that we have to keep following.
child, created := st.getOrCreateChild(bs[byteIdx])
switch {
case created:
// The subtree we need for pfx doesn't exist yet. The rest
// of the path, if we were to create it, will consist of a
// bunch of strideTables with a single child each. We can
// use path compression to elide those intermediates, and
// jump straight to the final strideTable that hosts this
// prefix.
child.prefix = finalStridePrefix
child.insert(bs[finalByteIdx], finalBits, val)
if debugInsert {
fmt.Printf("insert: new leaf st.prefix=%s child.prefix=%s addr=%d/%d\n", st.prefix, child.prefix, bs[finalByteIdx], finalBits)
}
return
case !prefixStrictlyContains(child.prefix, pfx):
// child already exists, but its prefix does not contain
// our destination. This means that the path between st
// and child was compressed by a previous insertion, and
// somewhere in the (implicit) compressed path we took a
// wrong turn, into the wrong part of st's subtree.
//
// This is okay, because pfx and child.prefix must have a
// common ancestor node somewhere between st and child. We
// can figure out what node that is, and materialize it.
//
// Once we've done that, we can immediately complete the
// remainder of the insertion in one of two ways, without
// further traversal. See a little further down for what
// those are.
if debugInsert {
fmt.Printf("insert: wrong turn, pfx=%s child.prefix=%s\n", pfx, child.prefix)
}
intermediatePrefix, addrOfExisting, addrOfNew := computePrefixSplit(child.prefix, pfx)
intermediate := &strideTable[T]{prefix: intermediatePrefix} // TODO: make this whole thing be st.AddIntermediate or something?
st.setChild(bs[byteIdx], intermediate)
intermediate.setChild(addrOfExisting, child)
if debugInsert {
fmt.Printf("insert: new intermediate st.prefix=%s intermediate.prefix=%s child.prefix=%s\n", st.prefix, intermediate.prefix, child.prefix)
}
// Now, we have a chain of st -> intermediate -> child.
//
// pfx either lives in a different child of intermediate,
// or in intermediate itself. For example, if we created
// the intermediate 1.2.0.0/16, pfx=1.2.3.4/32 would have
// to go into a new child of intermediate, but
// pfx=1.2.0.0/18 would go into intermediate directly.
if remain := pfx.Bits() - intermediate.prefix.Bits(); remain <= 8 {
// pfx lives in intermediate.
if debugInsert {
fmt.Printf("insert: into intermediate intermediate.prefix=%s addr=%d/%d\n", intermediate.prefix, bs[finalByteIdx], finalBits)
}
intermediate.insert(bs[finalByteIdx], finalBits, val)
} else {
// pfx lives in a different child subtree of
// intermediate. By definition this subtree doesn't
// exist at all, otherwise we'd never have entered
// this entire "wrong turn" codepath in the first
// place.
//
// This means we can apply prefix compression as we
// create this new child, and we're done.
st, created = intermediate.getOrCreateChild(addrOfNew)
if !created {
panic("new child path unexpectedly exists during path decompression")
}
st.prefix = finalStridePrefix
st.insert(bs[finalByteIdx], finalBits, val)
if debugInsert {
fmt.Printf("insert: new child st.prefix=%s addr=%d/%d\n", st.prefix, bs[finalByteIdx], finalBits)
}
}
return
default:
// An expected child table exists along pfx's
// path. Continue traversing downwards.
st = child
byteIdx = child.prefix.Bits() / 8
numBits = pfx.Bits() - child.prefix.Bits()
if debugInsert {
fmt.Printf("insert: descend st.prefix=%s\n", st.prefix)
}
}
// The strideTable we want to insert into is potentially at the end of a
// chain of parent tables, each one encoding successive 8 bits of the
// prefix. Navigate downwards, allocating child tables as needed, until we
// find the one this prefix belongs in.
for numBits > 8 {
st = st.getOrCreateChild(bs[i])
i++
numBits -= 8
}
// Finally, insert the remaining 0-8 bits of the prefix into the child
// table.
st.insert(bs[i], numBits, val)
}
// Delete removes pfx from the table, if it is present.
func (t *Table[T]) Delete(pfx netip.Prefix) {
t.init()
// The standard library doesn't enforce normalized prefixes (where
// the non-prefix bits are all zero). These algorithms require
// normalized prefixes, so do it upfront.
pfx = pfx.Masked()
if debugDelete {
defer func() {
fmt.Printf("%s", t.debugSummary())
}()
fmt.Printf("\ndelete: start pfx=%s table:\n%s", pfx, t.debugSummary())
st := &t.v4
if pfx.Addr().Is6() {
st = &t.v6
}
st := t.tableForAddr(pfx.Addr())
// This algorithm is full of off-by-one headaches, just like
// Insert. See the comment in Insert for more details. Bottom
// line: we handle the default route as a special case, and that
// simplifies the rest of the code slightly.
if pfx.Bits() == 0 {
if debugDelete {
fmt.Printf("delete: default route\n")
}
st.delete(0, 0)
return
}
// Deletion may drive the refcount of some strideTables down to
// zero. We need to clean up these dangling tables, so we have to
// keep track of which tables we touch on the way down, and which
// strideEntry index each child is registered in.
//
// Note that the strideIndex and strideTables entries are off-by-one.
// The child table pointer is recorded at i+1, but it is referenced by a
// particular index in the parent table, at index i.
//
// In other words: entry number strideIndexes[0] in
// strideTables[0] is the same pointer as strideTables[1].
//
// This results in some slightly odd array accesses further down
// in this code, because in a single loop iteration we have to
// write to strideTables[N] and strideIndexes[N-1].
strideIdx := 0
strideTables := [16]*strideTable[T]{st}
strideIndexes := [15]int{}
// Similar to Insert, navigate down the tree of strideTables,
// looking for the one that houses this prefix. This part is
// easier than with insertion, since we can bail if the path ends
// early or takes an unexpected detour. However, unlike
// insertion, there's a whole post-deletion cleanup phase later
// on.
//
// As we walk down the tree, byteIdx is the byte of bs we're
// currently examining to choose our next step, and numBits is the
// number of bits that remain in pfx, starting with the byte at
// byteIdx inclusive.
bs := pfx.Addr().AsSlice()
byteIdx := 0
i := 0
numBits := pfx.Bits()
for numBits > 8 {
if debugDelete {
fmt.Printf("delete: loop byteIdx=%d numBits=%d st.prefix=%s\n", byteIdx, numBits, st.prefix)
}
child, idx := st.getChild(bs[byteIdx])
if child == nil {
// Prefix can't exist in the table, because one of the
// necessary strideTables doesn't exist.
if debugDelete {
fmt.Printf("delete: missing necessary child pfx=%s\n", pfx)
}
return
}
strideIndexes[strideIdx] = idx
strideTables[strideIdx+1] = child
strideIdx++
// Path compression means byteIdx can jump forwards
// unpredictably. Recompute the next byte to look at from the
// child we just found.
byteIdx = child.prefix.Bits() / 8
numBits = pfx.Bits() - child.prefix.Bits()
st = child
// Deletion may drive the refcount of some strideTables down to zero. We
// need to clean up these dangling tables, so we have to keep track of which
// tables we touch on the way down, and which strideEntry index each child
// is registered in.
strideTables := [16]*strideTable[T]{st}
var strideIndexes [16]int
if debugDelete {
fmt.Printf("delete: descend st.prefix=%s\n", st.prefix)
}
}
// We reached a leaf stride table that seems to be in the right
// spot. But path compression might have led us to the wrong
// table.
if !prefixStrictlyContains(st.prefix, pfx) {
// Wrong table, the requested prefix can't exist since its
// path led us to the wrong place.
if debugDelete {
fmt.Printf("delete: wrong leaf table pfx=%s\n", pfx)
}
return
}
if debugDelete {
fmt.Printf("delete: delete from st.prefix=%s addr=%d/%d\n", st.prefix, bs[byteIdx], numBits)
}
if st.delete(bs[byteIdx], numBits) == nil {
// We're in the right strideTable, but pfx wasn't in
// it. Refcounts haven't changed, so we can skip cleanup.
if debugDelete {
fmt.Printf("delete: prefix not present pfx=%s\n", pfx)
}
return
}
// st.delete reduced st's refcount by one. This table may now be
// reclaimable, and depending on how we can reclaim it, the parent
// tables may also need to be reclaimed. This loop ends as soon as
// an iteration takes no action, or takes an action that doesn't
// alter the parent table's refcounts.
// Similar to Insert, navigate down the tree of strideTables, looking for
// the one that houses the last 0-8 bits of the prefix to delete.
//
// We start our walk back at strideTables[strideIdx], which
// contains st.
for strideIdx > 0 {
cur := strideTables[strideIdx]
if debugDelete {
fmt.Printf("delete: GC? strideIdx=%d st.prefix=%s\n", strideIdx, cur.prefix)
}
if cur.routeRefs > 0 {
// the strideTable has other route entries, it cannot be
// deleted or compacted.
if debugDelete {
fmt.Printf("delete: has other routes st.prefix=%s\n", cur.prefix)
}
return
}
switch cur.childRefs {
case 0:
// no routeRefs and no childRefs, this table can be
// deleted. This will alter the parent table's refcount,
// so we'll have to look at it as well (in the next loop
// iteration).
if debugDelete {
fmt.Printf("delete: remove st.prefix=%s\n", cur.prefix)
}
strideTables[strideIdx-1].deleteChild(strideIndexes[strideIdx-1])
strideIdx--
case 1:
// This table has no routes, and a single child. Compact
// this table out of existence by making the parent point
// directly at the one child. This does not affect the
// parent's refcounts, so the parent can't be eligible for
// deletion or compaction, and we can stop.
child := strideTables[strideIdx].findFirstChild() // only 1 child exists, by definition
parent := strideTables[strideIdx-1]
if debugDelete {
fmt.Printf("delete: compact parent.prefix=%s st.prefix=%s child.prefix=%s\n", parent.prefix, cur.prefix, child.prefix)
}
strideTables[strideIdx-1].setChildByIndex(strideIndexes[strideIdx-1], child)
return
default:
// This table has two or more children, so it's acting as a "fork in
// the road" between two prefix subtrees. It cannot be deleted, and
// thus no further cleanups are possible.
if debugDelete {
fmt.Printf("delete: fork table st.prefix=%s\n", cur.prefix)
}
// The only difference is that here, we don't create missing child tables.
// If a child necessary to pfx is missing, then the pfx cannot exist in the
// Table, and we can exit early.
for numBits > 8 {
child, idx := st.getChild(bs[i])
if child == nil {
// Prefix can't exist in the table, one of the necessary
// strideTables doesn't exit.
return
}
// Note that the strideIndex and strideTables entries are off-by-one.
// The child table pointer is recorded at i+1, but it is referenced by a
// particular index in the parent table, at index i.
strideIndexes[i] = idx
i++
strideTables[i] = child
numBits -= 8
st = child
}
if st.delete(bs[i], numBits) == nil {
// Prefix didn't exist in the expected strideTable, refcount hasn't
// changed, no need to run through cleanup.
return
}
// st.delete reduced st's refcount by one, so we may be hanging onto a chain
// of redundant strideTables. Walk back up the path we recorded in the
// descent loop, deleting tables until we encounter one that still has other
// refs (or we hit the root strideTable, which is never deleted).
for i > 0 && strideTables[i].refs == 0 {
strideTables[i-1].deleteChild(strideIndexes[i-1])
i--
}
}
// debugSummary prints the tree of allocated strideTables in t, with each
// strideTable's refcount.
func (t *Table[T]) debugSummary() string {
t.init()
var ret bytes.Buffer
fmt.Fprintf(&ret, "v4: ")
strideSummary(&ret, &t.v4, 4)
strideSummary(&ret, &t.v4, 0)
fmt.Fprintf(&ret, "v6: ")
strideSummary(&ret, &t.v6, 4)
strideSummary(&ret, &t.v6, 0)
return ret.String()
}
func strideSummary[T any](w io.Writer, st *strideTable[T], indent int) {
fmt.Fprintf(w, "%s: %d routes, %d children\n", st.prefix, st.routeRefs, st.childRefs)
indent += 4
st.treeDebugStringRec(w, 1, indent)
fmt.Fprintf(w, "%d refs\n", st.refs)
indent += 2
for i := firstHostIndex; i <= lastHostIndex; i++ {
if child := st.entries[i].child; child != nil {
addr, len := inversePrefixIndex(i)
fmt.Fprintf(w, "%s%d/%d (%02x/%d): ", strings.Repeat(" ", indent), addr, len, addr, len)
fmt.Fprintf(w, "%s%d/%d: ", strings.Repeat(" ", indent), addr, len)
strideSummary(w, child, indent)
}
}
}
// prefixStrictlyContains reports whether child is a prefix within
// parent, but not parent itself.
func prefixStrictlyContains(parent, child netip.Prefix) bool {
return parent.Overlaps(child) && parent.Bits() < child.Bits()
}
// computePrefixSplit returns the smallest common prefix that contains
// both a and b. lastCommon is 8-bit aligned, with aStride and bStride
// indicating the value of the 8-bit stride immediately following
// lastCommon.
//
// computePrefixSplit is used in constructing an intermediate
// strideTable when a new prefix needs to be inserted in a compressed
// table. It can be read as: given that a is already in the table, and
// b is being inserted, what is the prefix of the new intermediate
// strideTable that needs to be created, and at what addresses in that
// new strideTable should a and b's subsequent strideTables be
// attached?
//
// Note as a special case, this can be called with a==b. An example of
// when this happens:
// - We want to insert the prefix 1.2.0.0/16
// - A strideTable exists for 1.2.0.0/16, because another child
// prefix already exists (e.g. 1.2.3.4/32)
// - The 1.0.0.0/8 strideTable does not exist, because path
// compression removed it.
//
// In this scenario, the caller of computePrefixSplit ends up making a
// "wrong turn" while traversing strideTables: it was looking for the
// 1.0.0.0/8 table, but ended up at the 1.2.0.0/16 table. When this
// happens, it will invoke computePrefixSplit(1.2.0.0/16, 1.2.0.0/16),
// and we return 1.0.0.0/8 as the missing intermediate.
func computePrefixSplit(a, b netip.Prefix) (lastCommon netip.Prefix, aStride, bStride uint8) {
a = a.Masked()
b = b.Masked()
if a.Bits() == 0 || b.Bits() == 0 {
panic("computePrefixSplit called with a default route")
}
if a.Addr().Is4() != b.Addr().Is4() {
panic("computePrefixSplit called with mismatched address families")
}
minPrefixLen := a.Bits()
if b.Bits() < minPrefixLen {
minPrefixLen = b.Bits()
}
commonBits := commonBits(a.Addr(), b.Addr(), minPrefixLen)
// We want to know how many 8-bit strides are shared between a and
// b. Naively, this would be commonBits/8, but this introduces an
// off-by-one error. This is due to the way our ART stores
// prefixes whose length falls exactly on a stride boundary.
//
// Consider 192.168.1.0/24 and 192.168.0.0/16. commonBits
// correctly reports that these prefixes have their first 16 bits
// in common. However, in the ART they only share 1 common stride:
// they both use the 192.0.0.0/8 strideTable, but 192.168.0.0/16
// is stored as 168/8 within that table, and not as 0/0 in the
// 192.168.0.0/16 table.
//
// So, when commonBits matches the length of one of the inputs and
// falls on a boundary between strides, the strideTable one
// further up from commonBits/8 is the one we need to create,
// which means we have to adjust the stride count down by one.
if commonBits == minPrefixLen {
commonBits--
}
commonStrides := commonBits / 8
lastCommon, err := a.Addr().Prefix(commonStrides * 8)
if err != nil {
panic(fmt.Sprintf("computePrefixSplit constructing common prefix: %v", err))
}
if a.Addr().Is4() {
aStride = a.Addr().As4()[commonStrides]
bStride = b.Addr().As4()[commonStrides]
} else {
aStride = a.Addr().As16()[commonStrides]
bStride = b.Addr().As16()[commonStrides]
}
return lastCommon, aStride, bStride
}
// commonBits returns the number of common leading bits of a and b.
// If the number of common bits exceeds maxBits, it returns maxBits
// instead.
func commonBits(a, b netip.Addr, maxBits int) int {
if a.Is4() != b.Is4() {
panic("commonStrides called with mismatched address families")
}
var common int
// The following implements an old bit-twiddling trick to compute
// the number of common leading bits: if you XOR two numbers
// together, equal bits become 0 and unequal bits become 1. You
// can then count the number of leading zeros (which is a single
// instruction on modern CPUs) to get the answer.
//
// This code is a little more complex than just XOR + count
// leading zeros, because IPv4 and IPv6 are different sizes, and
// for IPv6 we have to do the math in two 64-bit chunks because Go
// lacks a uint128 type.
if a.Is4() {
aNum, bNum := ipv4AsUint(a), ipv4AsUint(b)
common = bits.LeadingZeros32(aNum ^ bNum)
} else {
aNumHi, aNumLo := ipv6AsUint(a)
bNumHi, bNumLo := ipv6AsUint(b)
common = bits.LeadingZeros64(aNumHi ^ bNumHi)
if common == 64 {
common += bits.LeadingZeros64(aNumLo ^ bNumLo)
}
}
if common > maxBits {
common = maxBits
}
return common
}
// ipv4AsUint returns ip as a uint32.
func ipv4AsUint(ip netip.Addr) uint32 {
bs := ip.As4()
return binary.BigEndian.Uint32(bs[:])
}
// ipv6AsUint returns ip as a pair of uint64s.
func ipv6AsUint(ip netip.Addr) (uint64, uint64) {
bs := ip.As16()
return binary.BigEndian.Uint64(bs[:8]), binary.BigEndian.Uint64(bs[8:])
}

View File

@@ -16,571 +16,7 @@ import (
"tailscale.com/types/ptr"
)
func TestRegression(t *testing.T) {
// These tests are specific triggers for subtle correctness issues
// that came up during initial implementation. Even if they seem
// arbitrary, please do not clean them up. They are checking edge
// cases that are very easy to get wrong, and quite difficult for
// the other statistical tests to trigger promptly.
t.Run("prefixes_aligned_on_stride_boundary", func(t *testing.T) {
// Regression test for computePrefixSplit called with equal
// arguments.
tbl := &Table[int]{}
slow := slowPrefixTable[int]{}
p := netip.MustParsePrefix
v := ptr.To(1)
tbl.Insert(p("226.205.197.0/24"), v)
slow.insert(p("226.205.197.0/24"), v)
v = ptr.To(2)
tbl.Insert(p("226.205.0.0/16"), v)
slow.insert(p("226.205.0.0/16"), v)
probe := netip.MustParseAddr("226.205.121.152")
got, want := tbl.Get(probe), slow.get(probe)
if got != want {
t.Fatalf("got %v, want %v", got, want)
}
})
t.Run("parent_prefix_inserted_in_different_orders", func(t *testing.T) {
// Regression test for the off-by-one correction applied
// within computePrefixSplit.
t1, t2 := &Table[int]{}, &Table[int]{}
p := netip.MustParsePrefix
v1, v2 := ptr.To(1), ptr.To(2)
t1.Insert(p("136.20.0.0/16"), v1)
t1.Insert(p("136.20.201.62/32"), v2)
t2.Insert(p("136.20.201.62/32"), v2)
t2.Insert(p("136.20.0.0/16"), v1)
a := netip.MustParseAddr("136.20.54.139")
got, want := t2.Get(a), t1.Get(a)
if got != want {
t.Errorf("Get(%q) is insertion order dependent (t1=%v, t2=%v)", a, want, got)
}
})
}
func TestComputePrefixSplit(t *testing.T) {
// These tests are partially redundant with other tests. Please
// keep them anyway. computePrefixSplit's behavior is remarkably
// subtle, and all the test cases listed below come from
// hard-earned debugging of malformed route tables.
var tests = []struct {
// prefixA can be a /8, /16 or /24 (v4).
// prefixB can be anything /9 or more specific.
prefixA, prefixB string
lastCommon string
aStride, bStride uint8
}{
{"192.168.1.0/24", "192.168.5.5/32", "192.168.0.0/16", 1, 5},
{"192.168.129.0/24", "192.168.128.0/17", "192.168.0.0/16", 129, 128},
{"192.168.5.0/24", "192.168.0.0/16", "192.0.0.0/8", 168, 168},
{"192.168.0.0/16", "192.168.0.0/16", "192.0.0.0/8", 168, 168},
{"ff:aaaa:aaaa::1/128", "ff:aaaa::/120", "ff:aaaa::/32", 170, 0},
}
for _, test := range tests {
a, b := netip.MustParsePrefix(test.prefixA), netip.MustParsePrefix(test.prefixB)
gotLastCommon, gotAStride, gotBStride := computePrefixSplit(a, b)
if want := netip.MustParsePrefix(test.lastCommon); gotLastCommon != want || gotAStride != test.aStride || gotBStride != test.bStride {
t.Errorf("computePrefixSplit(%q, %q) = %s, %d, %d; want %s, %d, %d", a, b, gotLastCommon, gotAStride, gotBStride, want, test.aStride, test.bStride)
}
}
}
func TestInsert(t *testing.T) {
tbl := &Table[int]{}
p := netip.MustParsePrefix
// Create a new leaf strideTable, with compressed path
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", -1},
{"192.168.0.3", -1},
{"192.168.0.255", -1},
{"192.168.1.1", -1},
{"192.170.1.1", -1},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", -1},
{"10.0.0.15", -1},
})
// Insert into previous leaf, no tree changes
tbl.Insert(p("192.168.0.2/32"), ptr.To(2))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", -1},
{"192.168.0.255", -1},
{"192.168.1.1", -1},
{"192.170.1.1", -1},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", -1},
{"10.0.0.15", -1},
})
// Insert into previous leaf, unaligned prefix covering the /32s
tbl.Insert(p("192.168.0.0/26"), ptr.To(7))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", -1},
{"192.170.1.1", -1},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", -1},
{"10.0.0.15", -1},
})
// Create a different leaf elsewhere
tbl.Insert(p("10.0.0.0/27"), ptr.To(3))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", -1},
{"192.170.1.1", -1},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// Insert that creates a new intermediate table and a new child
tbl.Insert(p("192.168.1.1/32"), ptr.To(4))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", 4},
{"192.170.1.1", -1},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// Insert that creates a new intermediate table but no new child
tbl.Insert(p("192.170.0.0/16"), ptr.To(5))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", 4},
{"192.170.1.1", 5},
{"192.180.0.1", -1},
{"192.180.3.5", -1},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// New leaf in a different subtree, so the next insert can test a
// variant of decompression.
tbl.Insert(p("192.180.0.1/32"), ptr.To(8))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", 4},
{"192.170.1.1", 5},
{"192.180.0.1", 8},
{"192.180.3.5", -1},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// Insert that creates a new intermediate table but no new child,
// with an unaligned intermediate
tbl.Insert(p("192.180.0.0/21"), ptr.To(9))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", -1},
{"192.168.1.1", 4},
{"192.170.1.1", 5},
{"192.180.0.1", 8},
{"192.180.3.5", 9},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// Insert a default route, those have their own codepath.
tbl.Insert(p("0.0.0.0/0"), ptr.To(6))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.168.0.3", 7},
{"192.168.0.255", 6},
{"192.168.1.1", 4},
{"192.170.1.1", 5},
{"192.180.0.1", 8},
{"192.180.3.5", 9},
{"10.0.0.5", 3},
{"10.0.0.15", 3},
})
// Now all of the above again, but for IPv6.
// Create a new leaf strideTable, with compressed path
tbl.Insert(p("ff:aaaa::1/128"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", -1},
{"ff:aaaa::3", -1},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", -1},
{"ff:aaaa:aaaa:bbbb::1", -1},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", -1},
{"ffff:bbbb::15", -1},
})
// Insert into previous leaf, no tree changes
tbl.Insert(p("ff:aaaa::2/128"), ptr.To(2))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", -1},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", -1},
{"ff:aaaa:aaaa:bbbb::1", -1},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", -1},
{"ffff:bbbb::15", -1},
})
// Insert into previous leaf, unaligned prefix covering the /128s
tbl.Insert(p("ff:aaaa::/125"), ptr.To(7))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", -1},
{"ff:aaaa:aaaa:bbbb::1", -1},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", -1},
{"ffff:bbbb::15", -1},
})
// Create a different leaf elsewhere
tbl.Insert(p("ffff:bbbb::/120"), ptr.To(3))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", -1},
{"ff:aaaa:aaaa:bbbb::1", -1},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
// Insert that creates a new intermediate table and a new child
tbl.Insert(p("ff:aaaa:aaaa::1/128"), ptr.To(4))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", 4},
{"ff:aaaa:aaaa:bbbb::1", -1},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
// Insert that creates a new intermediate table but no new child
tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), ptr.To(5))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", 4},
{"ff:aaaa:aaaa:bbbb::1", 5},
{"ff:cccc::1", -1},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
// New leaf in a different subtree, so the next insert can test a
// variant of decompression.
tbl.Insert(p("ff:cccc::1/128"), ptr.To(8))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", 4},
{"ff:aaaa:aaaa:bbbb::1", 5},
{"ff:cccc::1", 8},
{"ff:cccc::ff", -1},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
// Insert that creates a new intermediate table but no new child,
// with an unaligned intermediate
tbl.Insert(p("ff:cccc::/37"), ptr.To(9))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", -1},
{"ff:aaaa:aaaa::1", 4},
{"ff:aaaa:aaaa:bbbb::1", 5},
{"ff:cccc::1", 8},
{"ff:cccc::ff", 9},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
// Insert a default route, those have their own codepath.
tbl.Insert(p("::/0"), ptr.To(6))
checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1},
{"ff:aaaa::2", 2},
{"ff:aaaa::3", 7},
{"ff:aaaa::255", 6},
{"ff:aaaa:aaaa::1", 4},
{"ff:aaaa:aaaa:bbbb::1", 5},
{"ff:cccc::1", 8},
{"ff:cccc::ff", 9},
{"ffff:bbbb::5", 3},
{"ffff:bbbb::15", 3},
})
}
func TestDelete(t *testing.T) {
t.Parallel()
p := netip.MustParsePrefix
t.Run("prefix_in_root", func(t *testing.T) {
// Add/remove prefix from root table.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("10.0.0.0/8"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"10.0.0.1", 1},
{"255.255.255.255", -1},
})
checkSize(t, tbl, 2)
tbl.Delete(p("10.0.0.0/8"))
checkRoutes(t, tbl, []tableTest{
{"10.0.0.1", -1},
{"255.255.255.255", -1},
})
checkSize(t, tbl, 2)
})
t.Run("prefix_in_leaf", func(t *testing.T) {
// Create, then delete a single leaf table.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"255.255.255.255", -1},
})
checkSize(t, tbl, 3)
tbl.Delete(p("192.168.0.1/32"))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", -1},
{"255.255.255.255", -1},
})
checkSize(t, tbl, 2)
})
t.Run("intermediate_no_routes", func(t *testing.T) {
// Create an intermediate with 2 children, then delete one leaf.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
tbl.Insert(p("192.180.0.1/32"), ptr.To(2))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", 2},
{"192.40.0.1", -1},
})
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
tbl.Delete(p("192.180.0.1/32"))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", -1},
{"192.40.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
})
t.Run("intermediate_with_route", func(t *testing.T) {
// Same, but the intermediate carries a route as well.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
tbl.Insert(p("192.180.0.1/32"), ptr.To(2))
tbl.Insert(p("192.0.0.0/10"), ptr.To(3))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", 2},
{"192.40.0.1", 3},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
tbl.Delete(p("192.180.0.1/32"))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", -1},
{"192.40.0.1", 3},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 4) // 2 roots, 1 intermediate w/route, 1 leaf
})
t.Run("intermediate_many_leaves", func(t *testing.T) {
// Intermediate with 3 leaves, then delete one leaf.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
tbl.Insert(p("192.180.0.1/32"), ptr.To(2))
tbl.Insert(p("192.200.0.1/32"), ptr.To(3))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", 2},
{"192.200.0.1", 3},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 6) // 2 roots, 1 intermediate, 3 leaves
tbl.Delete(p("192.180.0.1/32"))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.180.0.1", -1},
{"192.200.0.1", 3},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 5) // 2 roots, 1 intermediate, 2 leaves
})
t.Run("nosuchprefix_missing_child", func(t *testing.T) {
// Delete non-existent prefix, missing strideTable path.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
tbl.Delete(p("200.0.0.0/32")) // lookup miss in root
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
})
t.Run("nosuchprefix_wrong_turn", func(t *testing.T) {
// Delete non-existent prefix, strideTable path exists but
// with a wrong turn.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
tbl.Delete(p("192.40.0.0/32")) // finds wrong child
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
})
t.Run("nosuchprefix_not_in_leaf", func(t *testing.T) {
// Delete non-existent prefix, strideTable path exists but
// leaf doesn't contain route.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
tbl.Delete(p("192.168.0.5/32")) // right leaf, no route
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
})
t.Run("intermediate_with_deleted_route", func(t *testing.T) {
// Intermediate table loses its last route and becomes
// compactable.
tbl := &Table[int]{}
checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
tbl.Insert(p("192.168.0.0/22"), ptr.To(2))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", 2},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 4) // 2 roots, 1 intermediate w/route, 1 leaf
tbl.Delete(p("192.168.0.0/22"))
checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1},
{"192.168.0.2", -1},
{"192.255.0.1", -1},
})
checkSize(t, tbl, 3) // 2 roots, 1 leaf
})
t.Run("default_route", func(t *testing.T) {
// Default routes have a special case in the code.
tbl := &Table[int]{}
tbl.Insert(p("0.0.0.0/0"), ptr.To(1))
tbl.Delete(p("0.0.0.0/0"))
checkRoutes(t, tbl, []tableTest{
{"1.2.3.4", -1},
})
checkSize(t, tbl, 2) // 2 roots
})
}
func TestInsertCompare(t *testing.T) {
// Create large route tables repeatedly, and compare Table's
// behavior to a naive and slow but correct implementation.
t.Parallel()
pfxs := randomPrefixes(10_000)
@@ -591,9 +27,7 @@ func TestInsertCompare(t *testing.T) {
fast.Insert(pfx.pfx, pfx.val)
}
if debugInsert {
t.Logf(fast.debugSummary())
}
t.Logf(fast.debugSummary())
seenVals4 := map[*int]bool{}
seenVals6 := map[*int]bool{}
@@ -610,7 +44,6 @@ func TestInsertCompare(t *testing.T) {
t.Errorf("get(%q) = %p, want %p", a, fastVal, slowVal)
}
}
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
// ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity
// check that we didn't just return a single route for everything should be
@@ -624,65 +57,36 @@ func TestInsertCompare(t *testing.T) {
}
func TestInsertShuffled(t *testing.T) {
// The order in which you insert prefixes into a route table
// should not matter, as long as you're inserting the same set of
// routes. Verify that this is true, because ART does execute
// vastly different code depending on the order of insertion, even
// if the end result is identical.
//
// If you're here because this package's tests are slow and you
// want to make them faster, please do not delete this test (or
// any test, really). It may seem excessive to test this, but
// these shuffle tests found a lot of very nasty edge cases during
// development, and you _really_ don't want to be debugging a
// faulty route table in production.
t.Parallel()
pfxs := randomPrefixes(1000)
var pfxs2 []slowPrefixEntry[int]
pfxs := randomPrefixes(10_000)
defer func() {
if t.Failed() {
t.Logf("pre-shuffle: %#v", pfxs)
t.Logf("post-shuffle: %#v", pfxs2)
}
}()
rt := Table[int]{}
for _, pfx := range pfxs {
rt.Insert(pfx.pfx, pfx.val)
}
for i := 0; i < 10; i++ {
pfxs2 := append([]slowPrefixEntry[int](nil), pfxs...)
rand.Shuffle(len(pfxs2), func(i, j int) { pfxs2[i], pfxs2[j] = pfxs2[j], pfxs2[i] })
addrs := make([]netip.Addr, 0, 10_000)
for i := 0; i < 10_000; i++ {
addrs = append(addrs, randomAddr())
}
rt := Table[int]{}
rt2 := Table[int]{}
for _, pfx := range pfxs {
rt.Insert(pfx.pfx, pfx.val)
}
for _, pfx := range pfxs2 {
rt2.Insert(pfx.pfx, pfx.val)
}
for _, a := range addrs {
// Diffing a deep tree of tables gives cmp.Diff a nervous breakdown, so
// test for equivalence statistically with random probes instead.
for i := 0; i < 10_000; i++ {
a := randomAddr()
val1 := rt.Get(a)
val2 := rt2.Get(a)
if val1 == nil && val2 == nil {
continue
}
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) {
t.Fatalf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
t.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
}
}
}
}
func TestDeleteCompare(t *testing.T) {
// Create large route tables repeatedly, delete half of their
// prefixes, and compare Table's behavior to a naive and slow but
// correct implementation.
func TestDelete(t *testing.T) {
t.Parallel()
const (
@@ -700,19 +104,6 @@ func TestDeleteCompare(t *testing.T) {
toDelete := append([]slowPrefixEntry[int](nil), all4[deleteCut:]...)
toDelete = append(toDelete, all6[deleteCut:]...)
defer func() {
if t.Failed() {
for _, pfx := range pfxs {
fmt.Printf("%q, ", pfx.pfx)
}
fmt.Println("")
for _, pfx := range toDelete {
fmt.Printf("%q, ", pfx.pfx)
}
fmt.Println("")
}
}()
slow := slowPrefixTable[int]{pfxs}
fast := Table[int]{}
@@ -755,18 +146,6 @@ func TestDeleteCompare(t *testing.T) {
}
func TestDeleteShuffled(t *testing.T) {
// The order in which you delete prefixes from a route table
// should not matter, as long as you're deleting the same set of
// routes. Verify that this is true, because ART does execute
// vastly different code depending on the order of deletions, even
// if the end result is identical.
//
// If you're here because this package's tests are slow and you
// want to make them faster, please do not delete this test (or
// any test, really). It may seem excessive to test this, but
// these shuffle tests found a lot of very nasty edge cases during
// development, and you _really_ don't want to be debugging a
// faulty route table in production.
t.Parallel()
const (
@@ -826,58 +205,6 @@ func TestDeleteShuffled(t *testing.T) {
}
}
func TestDeleteIsReverseOfInsert(t *testing.T) {
// Insert N prefixes, then delete those same prefixes in reverse
// order. Each deletion should exactly undo the internal structure
// changes that each insert did.
const N = 100
var tab Table[int]
prefixes := randomPrefixes(N)
defer func() {
if t.Failed() {
fmt.Printf("the prefixes that fail the test: %v\n", prefixes)
}
}()
want := make([]string, 0, len(prefixes))
for _, p := range prefixes {
want = append(want, tab.debugSummary())
tab.Insert(p.pfx, p.val)
}
for i := len(prefixes) - 1; i >= 0; i-- {
tab.Delete(prefixes[i].pfx)
if got := tab.debugSummary(); got != want[i] {
t.Fatalf("after delete %d, mismatch:\n\n got: %s\n\nwant: %s", i, got, want[i])
}
}
}
type tableTest struct {
// addr is an IP address string to look up in a route table.
addr string
// want is the expected >=0 value associated with the route, or -1
// if we expect a lookup miss.
want int
}
// checkRoutes verifies that the route lookups in tt return the
// expected results on tbl.
func checkRoutes(t *testing.T, tbl *Table[int], tt []tableTest) {
t.Helper()
for _, tc := range tt {
v := tbl.Get(netip.MustParseAddr(tc.addr))
if v == nil && tc.want != -1 {
t.Errorf("lookup %q got nil, want %d", tc.addr, tc.want)
}
if v != nil && *v != tc.want {
t.Errorf("lookup %q got %d, want %d", tc.addr, *v, tc.want)
}
}
}
// 100k routes for IPv6, at the current size of strideTable and strideEntry, is
// in the ballpark of 4GiB if you assume worst-case prefix distribution. Future
// optimizations will knock down the memory consumption by over an order of
@@ -1075,32 +402,6 @@ func (t *runningTimer) Elapsed() time.Duration {
return t.cumulative
}
func checkSize(t *testing.T, tbl *Table[int], want int) {
t.Helper()
if got := tbl.numStrides(); got != want {
t.Errorf("wrong table size, got %d strides want %d", got, want)
}
}
func (t *Table[T]) numStrides() int {
seen := map[*strideTable[T]]bool{}
return t.numStridesRec(seen, &t.v4) + t.numStridesRec(seen, &t.v6)
}
func (t *Table[T]) numStridesRec(seen map[*strideTable[T]]bool, st *strideTable[T]) int {
ret := 1
if st.childRefs == 0 {
return ret
}
for i := firstHostIndex; i <= lastHostIndex; i++ {
if c := st.entries[i].child; c != nil && !seen[c] {
seen[c] = true
ret += t.numStridesRec(seen, c)
}
}
return ret
}
// slowPrefixTable is a routing table implemented as a set of prefixes that are
// explicitly scanned in full for every route lookup. It is very slow, but also
// reasonably easy to verify by inspection, and so a good correctness reference
@@ -1115,7 +416,6 @@ type slowPrefixEntry[T any] struct {
}
func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
pfx = pfx.Masked()
ret := make([]slowPrefixEntry[T], 0, len(t.prefixes))
for _, ent := range t.prefixes {
if ent.pfx == pfx {
@@ -1127,10 +427,9 @@ func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
}
func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) {
pfx = pfx.Masked()
for i, ent := range t.prefixes {
for _, ent := range t.prefixes {
if ent.pfx == pfx {
t.prefixes[i].val = val
ent.val = val
return
}
}
@@ -1249,26 +548,3 @@ func roundFloat64(f float64) float64 {
}
return ret
}
func minimize(pfxs []slowPrefixEntry[int], f func(skip map[netip.Prefix]bool) error) (map[netip.Prefix]bool, error) {
if f(nil) == nil {
return nil, nil
}
remove := map[netip.Prefix]bool{}
for lastLen := -1; len(remove) != lastLen; lastLen = len(remove) {
fmt.Println("len is ", len(remove))
for i, pfx := range pfxs {
if remove[pfx.pfx] {
continue
}
remove[pfx.pfx] = true
fmt.Printf("%d %d: trying without %s\n", i, len(remove), pfx.pfx)
if f(remove) == nil {
delete(remove, pfx.pfx)
}
}
}
return remove, f(remove)
}

View File

@@ -27,6 +27,11 @@ import (
"tailscale.com/version/distro"
)
const (
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
resolvConf = "/etc/resolv.conf"
)
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error {
c := &resolvconffile.Config{

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