Compare commits
28 Commits
bradfitz/g
...
bradfitz/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54a91c1a58 | ||
|
|
d483ed7774 | ||
|
|
282dad1b62 | ||
|
|
d8191a9813 | ||
|
|
f35ff84ee2 | ||
|
|
93a806ba31 | ||
|
|
7dec09d169 | ||
|
|
02b47d123f | ||
|
|
58a4fd43d8 | ||
|
|
b040094b90 | ||
|
|
d4586ca75f | ||
|
|
93cab56277 | ||
|
|
6e57dee7eb | ||
|
|
261cc498d3 | ||
|
|
af2e4909b6 | ||
|
|
86ad1ea60e | ||
|
|
72d2122cad | ||
|
|
121d1d002c | ||
|
|
25663b1307 | ||
|
|
e92adfe5e4 | ||
|
|
bc0eb6b914 | ||
|
|
e8551d6b40 | ||
|
|
e8d140654a | ||
|
|
7e15c78a5a | ||
|
|
239ad57446 | ||
|
|
24509f8b22 | ||
|
|
0913ec023b | ||
|
|
b090d61c0f |
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
/tailcfg/ @tailscale/control-protocol-owners
|
||||
@@ -4,13 +4,13 @@
|
||||
package apitype
|
||||
|
||||
type DNSConfig struct {
|
||||
Resolvers []DNSResolver `json:"resolvers"`
|
||||
FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
|
||||
Routes map[string][]DNSResolver `json:"routes"`
|
||||
Domains []string `json:"domains"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Proxied bool `json:"proxied"`
|
||||
DNSFilterURL string `json:"DNSFilterURL"`
|
||||
Resolvers []DNSResolver `json:"resolvers"`
|
||||
FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
|
||||
Routes map[string][]DNSResolver `json:"routes"`
|
||||
Domains []string `json:"domains"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TempCorpIssue13969 string `json:"TempCorpIssue13969,omitempty"`
|
||||
}
|
||||
|
||||
type DNSResolver struct {
|
||||
|
||||
@@ -28,9 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/winutil"
|
||||
@@ -187,6 +185,8 @@ func (up *updater) confirm(ver string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
const synoinfoConfPath = "/etc/synoinfo.conf"
|
||||
|
||||
func (up *updater) updateSynology() error {
|
||||
if up.Version != "" {
|
||||
return errors.New("installing a specific version on Synology is not supported")
|
||||
@@ -194,7 +194,7 @@ func (up *updater) updateSynology() error {
|
||||
|
||||
// Get the latest version and list of SPKs from pkgs.tailscale.com.
|
||||
osName := fmt.Sprintf("dsm%d", distro.DSMVersion())
|
||||
arch, err := synoArch(hostinfo.New())
|
||||
arch, err := synoArch(runtime.GOARCH, synoinfoConfPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -245,51 +245,62 @@ func (up *updater) updateSynology() error {
|
||||
|
||||
// synoArch returns the Synology CPU architecture matching one of the SPK
|
||||
// architectures served from pkgs.tailscale.com.
|
||||
func synoArch(hinfo *tailcfg.Hostinfo) (string, error) {
|
||||
func synoArch(goArch, synoinfoPath string) (string, error) {
|
||||
// Most Synology boxes just use a different arch name from GOARCH.
|
||||
arch := map[string]string{
|
||||
"amd64": "x86_64",
|
||||
"386": "i686",
|
||||
"arm64": "armv8",
|
||||
}[hinfo.GoArch]
|
||||
// Here's the fun part, some older ARM boxes require you to use SPKs
|
||||
// specifically for their CPU.
|
||||
//
|
||||
// See https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
|
||||
// for a complete list. Here, we override GOARCH for those older boxes that
|
||||
// support at least DSM6.
|
||||
//
|
||||
// This is an artisanal hand-crafted list based on the wiki page. Some
|
||||
// values may be wrong, since we don't have all those devices to actually
|
||||
// test with.
|
||||
switch hinfo.DeviceModel {
|
||||
case "DS213air", "DS213", "DS413j",
|
||||
"DS112", "DS112+", "DS212", "DS212+", "RS212", "RS812", "DS212j", "DS112j",
|
||||
"DS111", "DS211", "DS211+", "DS411slim", "DS411", "RS411", "DS211j", "DS411j":
|
||||
arch = "88f6281"
|
||||
case "NVR1218", "NVR216", "VS960HD", "VS360HD":
|
||||
arch = "hi3535"
|
||||
case "DS1517", "DS1817", "DS416", "DS2015xs", "DS715", "DS1515", "DS215+":
|
||||
arch = "alpine"
|
||||
case "DS216se", "DS115j", "DS114", "DS214se", "DS414slim", "RS214", "DS14", "EDS14", "DS213j":
|
||||
arch = "armada370"
|
||||
case "DS115", "DS215j":
|
||||
arch = "armada375"
|
||||
case "DS419slim", "DS218j", "RS217", "DS116", "DS216j", "DS216", "DS416slim", "RS816", "DS416j":
|
||||
arch = "armada38x"
|
||||
case "RS815", "DS214", "DS214+", "DS414", "RS814":
|
||||
arch = "armadaxp"
|
||||
case "DS414j":
|
||||
arch = "comcerto2k"
|
||||
case "DS216play":
|
||||
arch = "monaco"
|
||||
}
|
||||
}[goArch]
|
||||
|
||||
if arch == "" {
|
||||
return "", fmt.Errorf("cannot determine CPU architecture for Synology model %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose", hinfo.DeviceModel, hinfo.GoArch)
|
||||
// Here's the fun part, some older ARM boxes require you to use SPKs
|
||||
// specifically for their CPU. See
|
||||
// https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
|
||||
// for a complete list.
|
||||
//
|
||||
// Some CPUs will map to neither this list nor the goArch map above, and we
|
||||
// don't have SPKs for them.
|
||||
cpu, err := parseSynoinfo(synoinfoPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get CPU architecture: %w", err)
|
||||
}
|
||||
switch cpu {
|
||||
case "88f6281", "88f6282", "hi3535", "alpine", "armada370",
|
||||
"armada375", "armada38x", "armadaxp", "comcerto2k", "monaco":
|
||||
arch = cpu
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported Synology CPU architecture %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose", cpu, goArch)
|
||||
}
|
||||
}
|
||||
return arch, nil
|
||||
}
|
||||
|
||||
func parseSynoinfo(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Look for a line like:
|
||||
// unique="synology_88f6282_413j"
|
||||
// Extract the CPU in the middle (88f6282 in the above example).
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
l := s.Text()
|
||||
if !strings.HasPrefix(l, "unique=") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(l, "_", 3)
|
||||
if len(parts) != 3 {
|
||||
return "", fmt.Errorf(`malformed %q: found %q, expected format like 'unique="synology_$cpu_$model'`, path, l)
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
return "", fmt.Errorf(`missing "unique=" field in %q`, path)
|
||||
}
|
||||
|
||||
func (up *updater) updateDebLike() error {
|
||||
ver, err := requestedTailscaleVersion(up.Version, up.track)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||
@@ -446,29 +444,151 @@ tailscale installed size:
|
||||
|
||||
func TestSynoArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
goarch string
|
||||
model string
|
||||
want string
|
||||
wantErr bool
|
||||
goarch string
|
||||
synoinfoUnique string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{goarch: "amd64", model: "DS224+", want: "x86_64"},
|
||||
{goarch: "arm64", model: "DS124", want: "armv8"},
|
||||
{goarch: "386", model: "DS415play", want: "i686"},
|
||||
{goarch: "arm", model: "DS213air", want: "88f6281"},
|
||||
{goarch: "arm", model: "NVR1218", want: "hi3535"},
|
||||
{goarch: "arm", model: "DS1517", want: "alpine"},
|
||||
{goarch: "arm", model: "DS216se", want: "armada370"},
|
||||
{goarch: "arm", model: "DS115", want: "armada375"},
|
||||
{goarch: "arm", model: "DS419slim", want: "armada38x"},
|
||||
{goarch: "arm", model: "RS815", want: "armadaxp"},
|
||||
{goarch: "arm", model: "DS414j", want: "comcerto2k"},
|
||||
{goarch: "arm", model: "DS216play", want: "monaco"},
|
||||
{goarch: "riscv64", model: "DS999", wantErr: true},
|
||||
{goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
|
||||
{goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
|
||||
{goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
|
||||
{goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
|
||||
{goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.model), func(t *testing.T) {
|
||||
got, err := synoArch(&tailcfg.Hostinfo{GoArch: tt.goarch, DeviceModel: tt.model})
|
||||
t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
|
||||
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
||||
if err := os.WriteFile(
|
||||
synoinfoConfPath,
|
||||
[]byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
|
||||
0600,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := synoArch(tt.goarch, synoinfoConfPath)
|
||||
if err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Fatalf("got unexpected error %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
t.Fatalf("got %q, expected an error", got)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSynoinfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
content string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "double-quoted",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique="synology_88f6281_213air"
|
||||
`,
|
||||
want: "88f6281",
|
||||
},
|
||||
{
|
||||
desc: "single-quoted",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique='synology_88f6281_213air'
|
||||
`,
|
||||
want: "88f6281",
|
||||
},
|
||||
{
|
||||
desc: "unquoted",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique=synology_88f6281_213air
|
||||
`,
|
||||
want: "88f6281",
|
||||
},
|
||||
{
|
||||
desc: "missing unique",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty unique",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique=
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty unique double-quoted",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique=""
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty unique single-quoted",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique=''
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "malformed unique",
|
||||
content: `
|
||||
company_title="Synology"
|
||||
unique="synology_88f6281"
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty file",
|
||||
content: ``,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty lines and comments",
|
||||
content: `
|
||||
|
||||
# In a file named synoinfo? Shocking!
|
||||
company_title="Synology"
|
||||
|
||||
|
||||
# unique= is_a_field_that_follows
|
||||
unique="synology_88f6281_213air"
|
||||
|
||||
`,
|
||||
want: "88f6281",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
||||
if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := parseSynoinfo(synoinfoConfPath)
|
||||
if err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Fatalf("got unexpected error %v", err)
|
||||
|
||||
@@ -25,6 +25,7 @@ var (
|
||||
dnsCache syncs.AtomicValue[dnsEntryMap]
|
||||
dnsCacheBytes syncs.AtomicValue[[]byte] // of JSON
|
||||
unpublishedDNSCache syncs.AtomicValue[dnsEntryMap]
|
||||
bootstrapLookupMap syncs.Map[string, bool]
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,6 +36,12 @@ var (
|
||||
unpublishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_misses")
|
||||
)
|
||||
|
||||
func init() {
|
||||
expvar.Publish("counter_bootstrap_dns_queried_domains", expvar.Func(func() any {
|
||||
return bootstrapLookupMap.Len()
|
||||
}))
|
||||
}
|
||||
|
||||
func refreshBootstrapDNSLoop() {
|
||||
if *bootstrapDNS == "" && *unpublishedDNS == "" {
|
||||
return
|
||||
@@ -107,6 +114,7 @@ func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Try answering a query from our hidden map first
|
||||
if q := r.URL.Query().Get("q"); q != "" {
|
||||
bootstrapLookupMap.Store(q, true)
|
||||
if ips, ok := unpublishedDNSCache.Load()[q]; ok && len(ips) > 0 {
|
||||
unpublishedDNSHits.Add(1)
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ func resetMetrics() {
|
||||
publishedDNSMisses.Set(0)
|
||||
unpublishedDNSHits.Set(0)
|
||||
unpublishedDNSMisses.Set(0)
|
||||
bootstrapLookupMap.Clear()
|
||||
}
|
||||
|
||||
// Verify that we don't count an empty list in the unpublishedDNSCache as a
|
||||
@@ -148,4 +149,17 @@ func TestUnpublishedDNSEmptyList(t *testing.T) {
|
||||
t.Errorf("got misses=%d; want 0", v)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestLookupMetric(t *testing.T) {
|
||||
d := []string{"a.io", "b.io", "c.io", "d.io", "e.io", "e.io", "e.io", "a.io"}
|
||||
resetMetrics()
|
||||
for _, q := range d {
|
||||
_ = getBootstrapDNS(t, q)
|
||||
}
|
||||
// {"a.io": true, "b.io": true, "c.io": true, "d.io": true, "e.io": true}
|
||||
if bootstrapLookupMap.Len() != 5 {
|
||||
t.Errorf("bootstrapLookupMap.Len() want=5, got %v", bootstrapLookupMap.Len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +168,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
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+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
@@ -193,6 +190,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/time/rate from tailscale.com/cmd/derper+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
cmp from slices
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
container/list from crypto/tls+
|
||||
@@ -242,6 +240,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
maps from tailscale.com/types/views
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
@@ -269,6 +268,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/pprof from net/http/pprof
|
||||
runtime/trace from net/http/pprof
|
||||
slices from tailscale.com/ipn+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
@@ -16,12 +16,26 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
type whoIsKey struct{}
|
||||
|
||||
// whoIsFromRequest returns the WhoIsResponse previously stashed by a call to
|
||||
// addWhoIsToRequest.
|
||||
func whoIsFromRequest(r *http.Request) *apitype.WhoIsResponse {
|
||||
return r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
}
|
||||
|
||||
// addWhoIsToRequest stashes who in r's context, retrievable by a call to
|
||||
// whoIsFromRequest.
|
||||
func addWhoIsToRequest(r *http.Request, who *apitype.WhoIsResponse) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), whoIsKey{}, who))
|
||||
}
|
||||
|
||||
// authProxy is an http.Handler that authenticates requests using the Tailscale
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type authProxy struct {
|
||||
@@ -37,8 +51,7 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), whoIsKey{}, who))
|
||||
h.rp.ServeHTTP(w, r)
|
||||
h.rp.ServeHTTP(w, addWhoIsToRequest(r, who))
|
||||
}
|
||||
|
||||
// runAuthProxy runs an HTTP server that authenticates requests using the
|
||||
@@ -67,6 +80,10 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
|
||||
lc: lc,
|
||||
rp: &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
@@ -85,21 +102,9 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
if who.Node.IsTagged() {
|
||||
// Use the nodes FQDN as the username, and the nodes tags as the groups.
|
||||
// "Impersonate-Group" requires "Impersonate-User" to be set.
|
||||
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
|
||||
for _, tag := range who.Node.Tags {
|
||||
r.Header.Add("Impersonate-Group", tag)
|
||||
}
|
||||
} else {
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
if err := addImpersonationHeaders(r); err != nil {
|
||||
panic("failed to add impersonation headers: " + err.Error())
|
||||
}
|
||||
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
},
|
||||
Transport: rt,
|
||||
},
|
||||
@@ -118,3 +123,58 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
|
||||
log.Fatalf("runAuthProxy: failed to serve %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const capabilityName = "https://tailscale.com/cap/kubernetes"
|
||||
|
||||
type capRule struct {
|
||||
// Impersonate is a list of rules that specify how to impersonate the caller
|
||||
// when proxying to the Kubernetes API.
|
||||
Impersonate *impersonateRule `json:"impersonate,omitempty"`
|
||||
}
|
||||
|
||||
// TODO(maisem): move this to some well-known location so that it can be shared
|
||||
// with control.
|
||||
type impersonateRule struct {
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
|
||||
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
|
||||
// in the context by the authProxy.
|
||||
func addImpersonationHeaders(r *http.Request) error {
|
||||
who := whoIsFromRequest(r)
|
||||
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal capability: %v", err)
|
||||
}
|
||||
|
||||
var groupsAdded set.Slice[string]
|
||||
for _, rule := range rules {
|
||||
if rule.Impersonate == nil {
|
||||
continue
|
||||
}
|
||||
for _, group := range rule.Impersonate.Groups {
|
||||
if groupsAdded.Contains(group) {
|
||||
continue
|
||||
}
|
||||
r.Header.Add("Impersonate-Group", group)
|
||||
groupsAdded.Add(group)
|
||||
}
|
||||
}
|
||||
|
||||
if !who.Node.IsTagged() {
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
return nil
|
||||
}
|
||||
// "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
|
||||
// to the node FQDN for tagged nodes.
|
||||
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
|
||||
|
||||
// For legacy behavior (before caps), set the groups to the nodes tags.
|
||||
if groupsAdded.Slice().Len() == 0 {
|
||||
for _, tag := range who.Node.Tags {
|
||||
r.Header.Add("Impersonate-Group", tag)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
107
cmd/k8s-operator/proxy_test.go
Normal file
107
cmd/k8s-operator/proxy_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestImpersonationHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
emailish string
|
||||
tags []string
|
||||
capMap tailcfg.PeerCapMap
|
||||
|
||||
wantHeaders http.Header
|
||||
}{
|
||||
{
|
||||
name: "user",
|
||||
emailish: "foo@example.com",
|
||||
wantHeaders: http.Header{
|
||||
"Impersonate-User": {"foo@example.com"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tagged",
|
||||
emailish: "tagged-device",
|
||||
tags: []string{"tag:foo", "tag:bar"},
|
||||
wantHeaders: http.Header{
|
||||
"Impersonate-User": {"node.ts.net"},
|
||||
"Impersonate-Group": {"tag:foo", "tag:bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user-with-cap",
|
||||
emailish: "foo@example.com",
|
||||
capMap: tailcfg.PeerCapMap{
|
||||
capabilityName: {
|
||||
[]byte(`{"impersonate":{"groups":["group1","group2"]}}`),
|
||||
[]byte(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
|
||||
[]byte(`{"impersonate":{"groups":["group4"]}}`),
|
||||
[]byte(`{"impersonate":{"groups":["group2"]}}`), // duplicate
|
||||
|
||||
// These should be ignored, but should parse correctly.
|
||||
[]byte(`{}`),
|
||||
[]byte(`{"impersonate":{}}`),
|
||||
[]byte(`{"impersonate":{"groups":[]}}`),
|
||||
},
|
||||
},
|
||||
wantHeaders: http.Header{
|
||||
"Impersonate-Group": {"group1", "group2", "group3", "group4"},
|
||||
"Impersonate-User": {"foo@example.com"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tagged-with-cap",
|
||||
emailish: "tagged-device",
|
||||
tags: []string{"tag:foo", "tag:bar"},
|
||||
capMap: tailcfg.PeerCapMap{
|
||||
capabilityName: {
|
||||
[]byte(`{"impersonate":{"groups":["group1"]}}`),
|
||||
},
|
||||
},
|
||||
wantHeaders: http.Header{
|
||||
"Impersonate-Group": {"group1"},
|
||||
"Impersonate-User": {"node.ts.net"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad-cap",
|
||||
emailish: "tagged-device",
|
||||
tags: []string{"tag:foo", "tag:bar"},
|
||||
capMap: tailcfg.PeerCapMap{
|
||||
capabilityName: {
|
||||
[]byte(`[]`),
|
||||
},
|
||||
},
|
||||
wantHeaders: http.Header{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
r := must.Get(http.NewRequest("GET", "https://op.ts.net/api/foo", nil))
|
||||
r = addWhoIsToRequest(r, &apitype.WhoIsResponse{
|
||||
Node: &tailcfg.Node{
|
||||
Name: "node.ts.net",
|
||||
Tags: tc.tags,
|
||||
},
|
||||
UserProfile: &tailcfg.UserProfile{
|
||||
LoginName: tc.emailish,
|
||||
},
|
||||
CapMap: tc.capMap,
|
||||
})
|
||||
addImpersonationHeaders(r)
|
||||
|
||||
if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" {
|
||||
t.Errorf("unexpected header (-want +got):\n%s", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,14 +35,13 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dsnet/try"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/cmpx"
|
||||
@@ -315,8 +314,8 @@ func mustMakeNamesByAddr() map[netip.Addr]string {
|
||||
namesByAddr := make(map[netip.Addr]string)
|
||||
retry:
|
||||
for i := 0; i < 10; i++ {
|
||||
maps.Clear(seen)
|
||||
maps.Clear(namesByAddr)
|
||||
clear(seen)
|
||||
clear(namesByAddr)
|
||||
for _, d := range m.Devices {
|
||||
name := fieldPrefix(d.Name, i)
|
||||
if seen[name] {
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/paths"
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
"sigs.k8s.io/yaml"
|
||||
"tailscale.com/version"
|
||||
|
||||
@@ -8,14 +8,13 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/cmpx"
|
||||
@@ -182,7 +181,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
}
|
||||
|
||||
filteredExitNodes := filteredExitNodes{
|
||||
Countries: maps.Values(countries),
|
||||
Countries: xmaps.Values(countries),
|
||||
}
|
||||
|
||||
for _, country := range filteredExitNodes.Countries {
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -83,7 +83,7 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status,
|
||||
return nil // already enabled
|
||||
}
|
||||
enableErr := e.enableFeatureInteractive(ctx, "funnel", hasFunnelAttrs)
|
||||
st, statusErr := e.getLocalClientStatus(ctx) // get updated status; interactive flow may block
|
||||
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
|
||||
switch {
|
||||
case statusErr != nil:
|
||||
return fmt.Errorf("getting client status: %w", statusErr)
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -129,7 +129,7 @@ func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.Fla
|
||||
//
|
||||
// The purpose of this interface is to allow tests to provide a mock.
|
||||
type localServeClient interface {
|
||||
Status(context.Context) (*ipnstate.Status, error)
|
||||
StatusWithoutPeers(context.Context) (*ipnstate.Status, error)
|
||||
GetServeConfig(context.Context) (*ipn.ServeConfig, error)
|
||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
||||
@@ -158,19 +158,21 @@ type serveEnv struct {
|
||||
// The trailing dot is removed.
|
||||
// Returns an error if local client status fails.
|
||||
func (e *serveEnv) getSelfDNSName(ctx context.Context) (string, error) {
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
return strings.TrimSuffix(st.Self.DNSName, "."), nil
|
||||
}
|
||||
|
||||
// getLocalClientStatus returns the Status of the local client.
|
||||
// getLocalClientStatusWithoutPeers returns the Status of the local client
|
||||
// without any peers in the response.
|
||||
//
|
||||
// Returns error if unable to reach tailscaled or if self node is nil.
|
||||
//
|
||||
// Exits if status is not running or starting.
|
||||
func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status, error) {
|
||||
st, err := e.lc.Status(ctx)
|
||||
func (e *serveEnv) getLocalClientStatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
st, err := e.lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return nil, fixTailscaledConnectError(err)
|
||||
}
|
||||
@@ -641,7 +643,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
printf("No serve config\n")
|
||||
return nil
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
|
||||
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
|
||||
fakeStatus.Self.Capabilities = tt.caps
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -861,7 +861,7 @@ var fakeStatus = &ipnstate.Status{
|
||||
},
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
func (lc *fakeLocalServeClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return fakeStatus, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
var setCmd = &ffcli.Command{
|
||||
@@ -171,7 +172,7 @@ func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, cu
|
||||
if alreadyAdvertisesExitNode == setArgs.advertiseDefaultRoute {
|
||||
return curPrefs.AdvertiseRoutes, nil
|
||||
}
|
||||
routes = tsaddr.FilterPrefixesCopy(curPrefs.AdvertiseRoutes, func(p netip.Prefix) bool {
|
||||
routes = tsaddr.FilterPrefixesCopy(views.SliceOf(curPrefs.AdvertiseRoutes), func(p netip.Prefix) bool {
|
||||
return p.Bits() != 0
|
||||
})
|
||||
if setArgs.advertiseDefaultRoute {
|
||||
|
||||
@@ -168,9 +168,8 @@ 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/slices from tailscale.com/net/tsaddr+
|
||||
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe
|
||||
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
@@ -199,6 +198,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
cmp from slices
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from image/png+
|
||||
@@ -256,6 +256,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
maps from tailscale.com/types/views
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
@@ -282,6 +283,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from tailscale.com/util/singleflight+
|
||||
slices from tailscale.com/cmd/tailscale/cli+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
|
||||
@@ -242,7 +242,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
|
||||
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
@@ -325,7 +324,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
@@ -380,9 +379,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
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/slices from tailscale.com/ipn/ipnlocal+
|
||||
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
@@ -468,6 +466,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
maps from tailscale.com/types/views
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
@@ -495,9 +494,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/trace from net/http/pprof
|
||||
slices from tailscale.com/wgengine/magicsock
|
||||
slices from tailscale.com/wgengine/magicsock+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
)
|
||||
|
||||
@@ -270,7 +270,7 @@ func main() {
|
||||
if len(toRetry) == 0 {
|
||||
continue
|
||||
}
|
||||
pkgs := maps.Keys(toRetry)
|
||||
pkgs := xmaps.Keys(toRetry)
|
||||
sort.Strings(pkgs)
|
||||
nextRun := &nextRun{
|
||||
attempt: thisRun.attempt + 1,
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -257,21 +257,25 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
},
|
||||
MachineStatus: jsMachineStatus[nm.MachineStatus],
|
||||
},
|
||||
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
|
||||
name := p.Name
|
||||
Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
|
||||
name := p.Name()
|
||||
if name == "" {
|
||||
// In practice this should only happen for Hello.
|
||||
name = p.Hostinfo.Hostname()
|
||||
name = p.Hostinfo().Hostname()
|
||||
}
|
||||
addrs := make([]string, p.Addresses().Len())
|
||||
for i := range p.Addresses().LenIter() {
|
||||
addrs[i] = p.Addresses().At(i).Addr().String()
|
||||
}
|
||||
return jsNetMapPeerNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
Name: name,
|
||||
Addresses: mapSlice(p.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
|
||||
MachineKey: p.Machine.String(),
|
||||
NodeKey: p.Key.String(),
|
||||
Addresses: addrs,
|
||||
MachineKey: p.Machine().String(),
|
||||
NodeKey: p.Key().String(),
|
||||
},
|
||||
Online: p.Online,
|
||||
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
|
||||
Online: p.Online(),
|
||||
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
||||
}
|
||||
}),
|
||||
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
|
||||
|
||||
@@ -309,8 +309,8 @@ func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs,
|
||||
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) }
|
||||
func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.Prefixes)
|
||||
func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] {
|
||||
return views.SliceOf(v.ж.Prefixes)
|
||||
}
|
||||
func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) }
|
||||
|
||||
|
||||
@@ -69,8 +69,6 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
||||
{{end}}
|
||||
{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
|
||||
@@ -176,9 +174,6 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
case "byte":
|
||||
it.Import("go4.org/mem")
|
||||
writeTemplate("byteSliceField")
|
||||
case "inet.af/netip.Prefix", "net/netip.Prefix":
|
||||
it.Import("tailscale.com/types/views")
|
||||
writeTemplate("ipPrefixSliceField")
|
||||
default:
|
||||
it.Import("tailscale.com/types/views")
|
||||
shallow, deep, base := requiresCloning(elem)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d)
|
||||
} else {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -32,7 +33,6 @@ import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
@@ -61,26 +61,25 @@ import (
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // or nil
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
onControlTime func(time.Time) // or nil
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // or nil
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
onControlTime func(time.Time) // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -126,10 +125,6 @@ type Options struct {
|
||||
// Status is called when there's a change in status.
|
||||
Status func(Status)
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
KeepSharerAndUserSplit bool
|
||||
|
||||
// SkipIPForwardingCheck declares that the host's IP
|
||||
// forwarding works and should not be double-checked by the
|
||||
// controlclient package.
|
||||
@@ -244,28 +239,27 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
netMon: opts.NetMon,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
httpc: httpc,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: opts.NetMon,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -449,10 +443,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return false, "", nil, fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
return false, "", "", fmt.Errorf("getMachinePrivKey: %w", err)
|
||||
}
|
||||
if machinePrivKey.IsZero() {
|
||||
return false, "", nil, errors.New("getMachinePrivKey returned zero key")
|
||||
return false, "", "", errors.New("getMachinePrivKey returned zero key")
|
||||
}
|
||||
|
||||
regen := opt.Regen
|
||||
@@ -474,7 +468,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if serverKey.IsZero() {
|
||||
keys, err := loadServerPubKeys(ctx, c.httpc, c.serverURL)
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
return regen, opt.URL, "", err
|
||||
}
|
||||
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
|
||||
|
||||
@@ -517,13 +511,13 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
if tryingNewKey.IsZero() {
|
||||
if opt.Logout {
|
||||
return false, "", nil, errors.New("no nodekey to log out")
|
||||
return false, "", "", errors.New("no nodekey to log out")
|
||||
}
|
||||
log.Fatalf("tryingNewKey is empty, give up")
|
||||
}
|
||||
|
||||
var nodeKeySignature tkatype.MarshaledSignature
|
||||
if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil {
|
||||
if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != "" {
|
||||
if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
|
||||
c.logf("Failed re-signing node-key signature: %v", err)
|
||||
}
|
||||
@@ -533,7 +527,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
// generate a tailnet-lock signature.
|
||||
nk, err := tryingNewKey.Public().MarshalBinary()
|
||||
if err != nil {
|
||||
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
return false, "", "", fmt.Errorf("marshalling node-key: %w", err)
|
||||
}
|
||||
sig := &tka.NodeKeySignature{
|
||||
SigKind: tka.SigRotation,
|
||||
@@ -547,7 +541,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
if backendLogID == "" {
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, opt.URL, nil, err
|
||||
return regen, opt.URL, "", err
|
||||
}
|
||||
now := c.clock.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
@@ -602,33 +596,33 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
request.Version = tailcfg.CurrentCapabilityVersion
|
||||
httpc, err = c.getNoiseClient()
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("getNoiseClient: %w", err)
|
||||
return regen, opt.URL, "", fmt.Errorf("getNoiseClient: %w", err)
|
||||
}
|
||||
url = fmt.Sprintf("%s/machine/register", c.serverURL)
|
||||
url = strings.Replace(url, "http:", "https:", 1)
|
||||
}
|
||||
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
return regen, opt.URL, "", err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
return regen, opt.URL, "", err
|
||||
}
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %w", err)
|
||||
return regen, opt.URL, "", fmt.Errorf("register request: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: http %d: %.200s",
|
||||
return regen, opt.URL, "", fmt.Errorf("register request: http %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, serverKey, serverNoiseKey, machinePrivKey); err != nil {
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, nil, fmt.Errorf("register request: %v", err)
|
||||
return regen, opt.URL, "", fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
if debugRegister() {
|
||||
j, _ := json.MarshalIndent(resp, "", "\t")
|
||||
@@ -640,7 +634,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
|
||||
|
||||
if resp.Error != "" {
|
||||
return false, "", nil, UserVisibleError(resp.Error)
|
||||
return false, "", "", UserVisibleError(resp.Error)
|
||||
}
|
||||
if len(resp.NodeKeySignature) > 0 {
|
||||
return true, "", resp.NodeKeySignature, nil
|
||||
@@ -648,11 +642,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
if resp.NodeKeyExpired {
|
||||
if regen {
|
||||
return true, "", nil, fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
|
||||
return true, "", "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
|
||||
}
|
||||
c.logf("server reports new node key %v has expired",
|
||||
request.NodeKey.ShortString())
|
||||
return true, "", nil, nil
|
||||
return true, "", "", nil
|
||||
}
|
||||
if resp.Login.Provider != "" {
|
||||
persist.Provider = resp.Login.Provider
|
||||
@@ -688,12 +682,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return regen, "", nil, err
|
||||
return regen, "", "", err
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return regen, "", nil, ctx.Err()
|
||||
return regen, "", "", ctx.Err()
|
||||
}
|
||||
return false, resp.AuthURL, nil, nil
|
||||
return false, resp.AuthURL, "", nil
|
||||
}
|
||||
|
||||
// resignNKS re-signs a node-key signature for a new node-key.
|
||||
@@ -709,12 +703,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
func resignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
|
||||
var oldSig tka.NodeKeySignature
|
||||
if err := oldSig.Unserialize(oldNKS); err != nil {
|
||||
return nil, fmt.Errorf("decoding NKS: %w", err)
|
||||
return "", fmt.Errorf("decoding NKS: %w", err)
|
||||
}
|
||||
|
||||
nk, err := nodeKey.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
return "", fmt.Errorf("marshalling node-key: %w", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(nk, oldSig.Pubkey) {
|
||||
@@ -729,7 +723,7 @@ func resignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.Marsha
|
||||
Nested: &oldSig,
|
||||
}
|
||||
if newSig.Signature, err = priv.SignNKS(newSig.SigHash()); err != nil {
|
||||
return nil, fmt.Errorf("signing NKS: %w", err)
|
||||
return "", fmt.Errorf("signing NKS: %w", err)
|
||||
}
|
||||
|
||||
return newSig.Serialize(), nil
|
||||
@@ -994,7 +988,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
sess.logf = c.logf
|
||||
sess.vlogf = vlogf
|
||||
sess.machinePubKey = machinePubKey
|
||||
sess.keepSharerAndUserSplit = c.keepSharerAndUserSplit
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -1085,8 +1078,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
|
||||
hasDebug := resp.Debug != nil
|
||||
// being conservative here, if Debug not present set to False
|
||||
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
|
||||
if hasDebug {
|
||||
if code := resp.Debug.Exit; code != nil {
|
||||
c.logf("exiting process with status %v per controlplane", *code)
|
||||
@@ -1096,12 +1087,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
logtail.Disable()
|
||||
envknob.SetNoLogsNoSupport()
|
||||
}
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
if resp.Debug.GoroutineDumpURL != "" {
|
||||
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 {
|
||||
return err
|
||||
@@ -1109,17 +1094,17 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
}
|
||||
|
||||
// For responses that mutate the self node, check for updated nodeAttrs.
|
||||
if resp.Node != nil {
|
||||
setControlKnobsFromNodeAttrs(resp.Node.Capabilities)
|
||||
}
|
||||
|
||||
nm := sess.netmapForResponse(&resp)
|
||||
if nm.SelfNode == nil {
|
||||
c.logf("MapResponse lacked node")
|
||||
return errors.New("MapResponse lacked node")
|
||||
}
|
||||
|
||||
if d := nm.Debug; d != nil {
|
||||
controlUseDERPRoute.Store(d.DERPRoute)
|
||||
controlTrimWGConfig.Store(d.TrimWGConfig)
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints() {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
@@ -1322,22 +1307,66 @@ func initDevKnob() devKnobs {
|
||||
|
||||
var clock tstime.Clock = tstime.StdClock{}
|
||||
|
||||
// opt.Bool configs from control.
|
||||
// config from control.
|
||||
var (
|
||||
controlUseDERPRoute syncs.AtomicValue[opt.Bool]
|
||||
controlTrimWGConfig syncs.AtomicValue[opt.Bool]
|
||||
controlDisableDRPO atomic.Bool
|
||||
controlKeepFullWGConfig atomic.Bool
|
||||
controlRandomizeClientPort atomic.Bool
|
||||
controlOneCGNAT syncs.AtomicValue[opt.Bool]
|
||||
)
|
||||
|
||||
// DERPRouteFlag reports the last reported value from control for whether
|
||||
// DERP route optimization (Issue 150) should be enabled.
|
||||
func DERPRouteFlag() opt.Bool {
|
||||
return controlUseDERPRoute.Load()
|
||||
// DisableDRPO reports whether control says to disable the
|
||||
// DERP route optimization (Issue 150).
|
||||
func DisableDRPO() bool {
|
||||
return controlDisableDRPO.Load()
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
return controlTrimWGConfig.Load()
|
||||
// KeepFullWGConfig reports whether control says we should disable the lazy
|
||||
// wireguard programming and instead give it the full netmap always.
|
||||
func KeepFullWGConfig() bool {
|
||||
return controlKeepFullWGConfig.Load()
|
||||
}
|
||||
|
||||
// RandomizeClientPort reports whether control says we should randomize
|
||||
// the client port.
|
||||
func RandomizeClientPort() bool {
|
||||
return controlRandomizeClientPort.Load()
|
||||
}
|
||||
|
||||
// ControlOneCGNATSetting returns control's OneCGNAT setting, if any.
|
||||
func ControlOneCGNATSetting() opt.Bool {
|
||||
return controlOneCGNAT.Load()
|
||||
}
|
||||
|
||||
func setControlKnobsFromNodeAttrs(selfNodeAttrs []string) {
|
||||
var (
|
||||
keepFullWG bool
|
||||
disableDRPO bool
|
||||
disableUPnP bool
|
||||
randomizeClientPort bool
|
||||
oneCGNAT opt.Bool
|
||||
)
|
||||
for _, attr := range selfNodeAttrs {
|
||||
switch attr {
|
||||
case tailcfg.NodeAttrDebugDisableWGTrim:
|
||||
keepFullWG = true
|
||||
case tailcfg.NodeAttrDebugDisableDRPO:
|
||||
disableDRPO = true
|
||||
case tailcfg.NodeAttrDisableUPnP:
|
||||
disableUPnP = true
|
||||
case tailcfg.NodeAttrRandomizeClientPort:
|
||||
randomizeClientPort = true
|
||||
case tailcfg.NodeAttrOneCGNATEnable:
|
||||
oneCGNAT.Set(true)
|
||||
case tailcfg.NodeAttrOneCGNATDisable:
|
||||
oneCGNAT.Set(false)
|
||||
}
|
||||
}
|
||||
controlKeepFullWGConfig.Store(keepFullWG)
|
||||
controlDisableDRPO.Store(disableDRPO)
|
||||
controlknobs.SetDisableUPnP(disableUPnP)
|
||||
controlRandomizeClientPort.Store(randomizeClientPort)
|
||||
controlOneCGNAT.Store(oneCGNAT)
|
||||
}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
@@ -1790,7 +1819,7 @@ func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapp
|
||||
}
|
||||
|
||||
sig = new(tka.NodeKeySignature)
|
||||
if err := sig.Unserialize([]byte(rawSig)); err != nil {
|
||||
if err := sig.Unserialize(tkatype.MarshaledSignature(rawSig)); err != nil {
|
||||
logf("decoding wrapped auth-key: signature: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -29,11 +28,10 @@ import (
|
||||
// one MapRequest).
|
||||
type mapSession struct {
|
||||
// Immutable fields.
|
||||
privateNodeKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey key.MachinePublic
|
||||
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
|
||||
privateNodeKey key.NodePrivate
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey key.MachinePublic
|
||||
|
||||
// Fields storing state over the course of multiple MapResponses.
|
||||
lastNode *tailcfg.Node
|
||||
@@ -145,33 +143,18 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
|
||||
debug := resp.Debug
|
||||
if debug != nil {
|
||||
if debug.RandomizeClientPort {
|
||||
debug.SetRandomizeClientPort.Set(true)
|
||||
}
|
||||
if debug.ForceBackgroundSTUN {
|
||||
debug.SetForceBackgroundSTUN.Set(true)
|
||||
}
|
||||
copyDebugOptBools(&ms.stickyDebug, debug)
|
||||
} else if ms.stickyDebug != (tailcfg.Debug{}) {
|
||||
debug = new(tailcfg.Debug)
|
||||
}
|
||||
if debug != nil {
|
||||
copyDebugOptBools(debug, &ms.stickyDebug)
|
||||
if !debug.ForceBackgroundSTUN {
|
||||
debug.ForceBackgroundSTUN, _ = ms.stickyDebug.SetForceBackgroundSTUN.Get()
|
||||
}
|
||||
if !debug.RandomizeClientPort {
|
||||
debug.RandomizeClientPort, _ = ms.stickyDebug.SetRandomizeClientPort.Get()
|
||||
}
|
||||
// TODO(bradfitz): now that this is a view, remove some of the defensive
|
||||
// cloning elsewhere in mapSession.
|
||||
peerViews := make([]tailcfg.NodeView, len(resp.Peers))
|
||||
for i, n := range resp.Peers {
|
||||
peerViews[i] = n.View()
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
Peers: peerViews,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
@@ -181,7 +164,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
@@ -221,11 +203,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if ms.keepSharerAndUserSplit {
|
||||
ms.addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
@@ -352,7 +330,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if v := ec.Capabilities; v != nil {
|
||||
n.Capabilities = *v
|
||||
}
|
||||
if v := ec.KeySignature; v != nil {
|
||||
if v := ec.KeySignature; v != "" {
|
||||
n.KeySignature = v
|
||||
}
|
||||
}
|
||||
@@ -413,18 +391,3 @@ func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
func copyDebugOptBools(dst, src *tailcfg.Debug) {
|
||||
copy := func(v *opt.Bool, s opt.Bool) {
|
||||
if s != "" {
|
||||
*v = s
|
||||
}
|
||||
}
|
||||
copy(&dst.DERPRoute, src.DERPRoute)
|
||||
copy(&dst.DisableSubnetsIfPAC, src.DisableSubnetsIfPAC)
|
||||
copy(&dst.DisableUPnP, src.DisableUPnP)
|
||||
copy(&dst.OneCGNATRoute, src.OneCGNATRoute)
|
||||
copy(&dst.SetForceBackgroundSTUN, src.SetForceBackgroundSTUN)
|
||||
copy(&dst.SetRandomizeClientPort, src.SetRandomizeClientPort)
|
||||
copy(&dst.TrimWGConfig, src.TrimWGConfig)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
@@ -213,12 +212,12 @@ func TestUndeltaPeers(t *testing.T) {
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChangedPatch: []*tailcfg.PeerChange{{
|
||||
NodeID: 1,
|
||||
KeySignature: []byte{3, 4},
|
||||
KeySignature: "ab",
|
||||
}},
|
||||
}, want: peers(&tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "foo",
|
||||
KeySignature: []byte{3, 4},
|
||||
KeySignature: "ab",
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -470,155 +469,6 @@ func TestNetmapForResponse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResponses
|
||||
// entirely or have their opt.Bool values unspecified between MapResponses in a
|
||||
// session and that should mean no change. (as of capver 37). But two Debug
|
||||
// fields existed prior to capver 37 that weren't opt.Bool; we test that we both
|
||||
// still accept the non-opt.Bool form from control for RandomizeClientPort and
|
||||
// ForceBackgroundSTUN and also accept the new form, keeping the old form in
|
||||
// sync.
|
||||
func TestDeltaDebug(t *testing.T) {
|
||||
type step struct {
|
||||
got *tailcfg.Debug
|
||||
want *tailcfg.Debug
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []step
|
||||
}{
|
||||
{
|
||||
name: "nothing-to-nothing",
|
||||
steps: []step{
|
||||
{nil, nil},
|
||||
{nil, nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sticky-with-old-style-randomize-client-port",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{RandomizeClientPort: true},
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
nil, // not sent by server
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sticky-with-new-style-randomize-client-port",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetRandomizeClientPort: "true"},
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
nil, // not sent by server
|
||||
&tailcfg.Debug{
|
||||
RandomizeClientPort: true,
|
||||
SetRandomizeClientPort: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opt-bool-sticky-changing-over-time",
|
||||
steps: []step{
|
||||
{nil, nil},
|
||||
{nil, nil},
|
||||
{
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&tailcfg.Debug{OneCGNATRoute: "true"},
|
||||
},
|
||||
{
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&tailcfg.Debug{OneCGNATRoute: "false"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy-ForceBackgroundSTUN",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opt-bool-SetForceBackgroundSTUN",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "true"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "server-reset-to-default",
|
||||
steps: []step{
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "true"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: true, SetForceBackgroundSTUN: "true"},
|
||||
},
|
||||
{
|
||||
&tailcfg.Debug{SetForceBackgroundSTUN: "unset"},
|
||||
&tailcfg.Debug{ForceBackgroundSTUN: false, SetForceBackgroundSTUN: "unset"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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{Debug: s.got})
|
||||
if !reflect.DeepEqual(nm.Debug, s.want) {
|
||||
t.Errorf("unexpected result at step index %v; got: %s", stepi, must.Get(json.Marshal(nm.Debug)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that copyDebugOptBools doesn't missing any opt.Bools.
|
||||
func TestCopyDebugOptBools(t *testing.T) {
|
||||
rt := reflect.TypeOf(tailcfg.Debug{})
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
sf := rt.Field(i)
|
||||
if sf.Type != reflect.TypeOf(opt.Bool("")) {
|
||||
continue
|
||||
}
|
||||
var src, dst tailcfg.Debug
|
||||
reflect.ValueOf(&src).Elem().Field(i).Set(reflect.ValueOf(opt.Bool("true")))
|
||||
if src == (tailcfg.Debug{}) {
|
||||
t.Fatalf("failed to set field %v", sf.Name)
|
||||
}
|
||||
copyDebugOptBools(&dst, &src)
|
||||
if src != dst {
|
||||
t.Fatalf("copyDebugOptBools didn't copy field %v", sf.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaDERPMap(t *testing.T) {
|
||||
regions1 := map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
package logknob
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -79,8 +79,8 @@ func (v PrefsView) Hostname() string { return v.ж.Hostname }
|
||||
func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs }
|
||||
func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon }
|
||||
func (v PrefsView) Egg() bool { return v.ж.Egg }
|
||||
func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.AdvertiseRoutes)
|
||||
func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
|
||||
return views.SliceOf(v.ж.AdvertiseRoutes)
|
||||
}
|
||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go)
|
||||
|
||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON := func(v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -70,6 +72,13 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
writeJSON(res)
|
||||
case "/debug/logheap":
|
||||
if c2nLogHeap != nil {
|
||||
c2nLogHeap(w, r)
|
||||
} else {
|
||||
http.Error(w, "not implemented", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
case "/ssh/usernames":
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
|
||||
17
ipn/ipnlocal/c2n_pprof.go
Normal file
17
ipn/ipnlocal/c2n_pprof.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !wasm
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
func init() {
|
||||
c2nLogHeap = func(w http.ResponseWriter, r *http.Request) {
|
||||
pprof.WriteHeapProfile(w)
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/acme"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/exp/maps"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
)
|
||||
|
||||
@@ -112,7 +111,7 @@ func TestShouldStartDomainRenewal(t *testing.T) {
|
||||
reset := func() {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
maps.Clear(renewCertAt)
|
||||
clear(renewCertAt)
|
||||
}
|
||||
|
||||
mustMakePair := func(template *x509.Certificate) *TLSCertKeyPair {
|
||||
|
||||
@@ -38,6 +38,14 @@ func ips(ss ...string) (ips []netip.Addr) {
|
||||
return
|
||||
}
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
nv[i] = n.View()
|
||||
}
|
||||
return nv
|
||||
}
|
||||
|
||||
func TestDNSConfigForNetmap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -62,7 +70,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "myname.net",
|
||||
Addresses: ipps("100.101.101.101"),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Name: "peera.net",
|
||||
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
|
||||
@@ -75,7 +83,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
Name: "v6-only.net",
|
||||
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
@@ -96,7 +104,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "myname.net",
|
||||
Addresses: ipps("fe75::1"),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Name: "peera.net",
|
||||
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
|
||||
@@ -109,7 +117,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
Name: "v6-only.net",
|
||||
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
|
||||
@@ -87,24 +87,26 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
|
||||
return
|
||||
}
|
||||
|
||||
for _, peer := range netmap.Peers {
|
||||
for i, peer := range netmap.Peers {
|
||||
// Nodes that don't expire have KeyExpiry set to the zero time;
|
||||
// skip those and peers that are already marked as expired
|
||||
// (e.g. from control).
|
||||
if peer.KeyExpiry.IsZero() || peer.KeyExpiry.After(controlNow) {
|
||||
delete(em.previouslyExpired, peer.StableID)
|
||||
if peer.KeyExpiry().IsZero() || peer.KeyExpiry().After(controlNow) {
|
||||
delete(em.previouslyExpired, peer.StableID())
|
||||
continue
|
||||
} else if peer.Expired {
|
||||
} else if peer.Expired() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !em.previouslyExpired[peer.StableID] {
|
||||
em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID)
|
||||
em.previouslyExpired[peer.StableID] = true
|
||||
if !em.previouslyExpired[peer.StableID()] {
|
||||
em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID())
|
||||
em.previouslyExpired[peer.StableID()] = true
|
||||
}
|
||||
|
||||
mut := peer.AsStruct()
|
||||
|
||||
// Actually mark the node as expired
|
||||
peer.Expired = true
|
||||
mut.Expired = true
|
||||
|
||||
// Control clears the Endpoints and DERP fields of expired
|
||||
// nodes; do so here as well. The Expired bool is the correct
|
||||
@@ -113,12 +115,14 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
|
||||
// NOTE: this is insufficient to actually break connectivity,
|
||||
// since we discover endpoints via DERP, and due to DERP return
|
||||
// path optimization.
|
||||
peer.Endpoints = nil
|
||||
peer.DERP = ""
|
||||
mut.Endpoints = nil
|
||||
mut.DERP = ""
|
||||
|
||||
// Defense-in-depth: break the node's public key as well, in
|
||||
// case something tries to communicate.
|
||||
peer.Key = key.NodePublicWithBadOldPrefix(peer.Key)
|
||||
mut.Key = key.NodePublicWithBadOldPrefix(peer.Key())
|
||||
|
||||
netmap.Peers[i] = mut.View()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,13 +148,13 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim
|
||||
|
||||
var nextExpiry time.Time // zero if none
|
||||
for _, peer := range nm.Peers {
|
||||
if peer.KeyExpiry.IsZero() {
|
||||
if peer.KeyExpiry().IsZero() {
|
||||
continue // tagged node
|
||||
} else if peer.Expired {
|
||||
} else if peer.Expired() {
|
||||
// Peer already expired; Expired is set by the
|
||||
// flagExpiredPeers function, above.
|
||||
continue
|
||||
} else if peer.KeyExpiry.Before(controlNow) {
|
||||
} else if peer.KeyExpiry().Before(controlNow) {
|
||||
// This peer already expired, and peer.Expired
|
||||
// isn't set for some reason. Skip this node.
|
||||
continue
|
||||
@@ -160,8 +164,8 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim
|
||||
// an expiry; otherwise, only update if this node's expiry is
|
||||
// sooner than the currently-stored one (since we want the
|
||||
// soonest-occurring expiry time).
|
||||
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) {
|
||||
nextExpiry = peer.KeyExpiry
|
||||
if nextExpiry.IsZero() || peer.KeyExpiry().Before(nextExpiry) {
|
||||
nextExpiry = peer.KeyExpiry()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,38 +44,38 @@ func TestFlagExpiredPeers(t *testing.T) {
|
||||
name string
|
||||
controlTime *time.Time
|
||||
netmap *netmap.NetworkMap
|
||||
want []*tailcfg.Node
|
||||
want []tailcfg.NodeView
|
||||
}{
|
||||
{
|
||||
name: "no_expiry",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
want: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "expiry",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInPast),
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
want: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInPast, func(n *tailcfg.Node) {
|
||||
n.Expired = true
|
||||
n.Key = expiredKey
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "bad_ControlTime",
|
||||
@@ -83,29 +83,29 @@ func TestFlagExpiredPeers(t *testing.T) {
|
||||
controlTime: &timeBeforeEpoch,
|
||||
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
want: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "tagged_node",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", time.Time{}), // tagged node; zero expiry
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
want: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", time.Time{}), // not expired
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -147,10 +147,10 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "no_expiry",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", noExpiry),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: noExpiry,
|
||||
@@ -158,10 +158,10 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "future_expiry_from_peer",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -169,10 +169,10 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "future_expiry_from_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", noExpiry),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(3, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -180,10 +180,10 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "future_expiry_from_multiple_peers",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInMoreFuture),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -191,9 +191,9 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "future_expiry_from_peer_and_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInMoreFuture),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(2, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -201,7 +201,7 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "only_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{},
|
||||
Peers: nodeViews([]*tailcfg.Node{}),
|
||||
SelfNode: n(1, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -209,9 +209,9 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "peer_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(2, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -219,9 +219,9 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "self_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(2, "self", timeInPast),
|
||||
},
|
||||
want: timeInFuture,
|
||||
@@ -229,9 +229,9 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
{
|
||||
name: "all_nodes_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
}),
|
||||
SelfNode: n(2, "self", timeInPast),
|
||||
},
|
||||
want: noExpiry,
|
||||
@@ -263,9 +263,9 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
// If we don't adjust for the local time, this would return a
|
||||
// time in the past.
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
}),
|
||||
}
|
||||
got := em.nextPeerExpiry(nm, now)
|
||||
want := now.Add(30 * time.Second)
|
||||
@@ -275,24 +275,24 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
func formatNodes(nodes []tailcfg.NodeView) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name)
|
||||
fmt.Fprintf(&sb, "(%d, %q", n.ID(), n.Name())
|
||||
|
||||
if n.Online != nil {
|
||||
fmt.Fprintf(&sb, ", online=%v", *n.Online)
|
||||
if n.Online() != nil {
|
||||
fmt.Fprintf(&sb, ", online=%v", *n.Online())
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix())
|
||||
if n.LastSeen() != nil {
|
||||
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen().Unix())
|
||||
}
|
||||
if n.Key != (key.NodePublic{}) {
|
||||
fmt.Fprintf(&sb, ", key=%v", n.Key.String())
|
||||
if n.Key() != (key.NodePublic{}) {
|
||||
fmt.Fprintf(&sb, ", key=%v", n.Key().String())
|
||||
}
|
||||
if n.Expired {
|
||||
if n.Expired() {
|
||||
fmt.Fprintf(&sb, ", expired=true")
|
||||
}
|
||||
sb.WriteString(")")
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -204,7 +204,7 @@ type LocalBackend struct {
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *netmap.NetworkMap
|
||||
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil
|
||||
nodeByAddr map[netip.Addr]*tailcfg.Node
|
||||
nodeByAddr map[netip.Addr]tailcfg.NodeView
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []tailcfg.Endpoint
|
||||
@@ -684,13 +684,13 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
if !prefs.ExitNodeID().IsZero() {
|
||||
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
|
||||
var online = false
|
||||
if exitPeer.Online != nil {
|
||||
online = *exitPeer.Online
|
||||
if v := exitPeer.Online(); v != nil {
|
||||
online = *v
|
||||
}
|
||||
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
|
||||
ID: prefs.ExitNodeID(),
|
||||
Online: online,
|
||||
TailscaleIPs: exitPeer.Addresses,
|
||||
TailscaleIPs: exitPeer.Addresses().AsSlice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
ss.DNSName = b.netMap.Name
|
||||
ss.UserID = b.netMap.User
|
||||
if sn := b.netMap.SelfNode; sn != nil {
|
||||
peerStatusFromNode(ss, sn)
|
||||
peerStatusFromNode(ss, sn.View())
|
||||
if c := sn.Capabilities; len(c) > 0 {
|
||||
ss.Capabilities = append([]string(nil), c...)
|
||||
}
|
||||
@@ -735,28 +735,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
|
||||
for _, p := range b.netMap.Peers {
|
||||
var lastSeen time.Time
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
if p.LastSeen() != nil {
|
||||
lastSeen = *p.LastSeen()
|
||||
}
|
||||
var tailscaleIPs = make([]netip.Addr, 0, len(p.Addresses))
|
||||
for _, addr := range p.Addresses {
|
||||
var tailscaleIPs = make([]netip.Addr, 0, p.Addresses().Len())
|
||||
for i := range p.Addresses().LenIter() {
|
||||
addr := p.Addresses().At(i)
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
|
||||
tailscaleIPs = append(tailscaleIPs, addr.Addr())
|
||||
}
|
||||
}
|
||||
online := p.Online()
|
||||
ps := &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
UserID: p.User,
|
||||
UserID: p.User(),
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname(),
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS(),
|
||||
HostName: p.Hostinfo().Hostname(),
|
||||
DNSName: p.Name(),
|
||||
OS: p.Hostinfo().OS(),
|
||||
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(),
|
||||
Online: online != nil && *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)
|
||||
|
||||
@@ -767,29 +769,29 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
if u := peerAPIURL(nodeIP(p, netip.Addr.Is6), p6); u != "" {
|
||||
ps.PeerAPIURL = append(ps.PeerAPIURL, u)
|
||||
}
|
||||
sb.AddPeer(p.Key, ps)
|
||||
sb.AddPeer(p.Key(), ps)
|
||||
}
|
||||
}
|
||||
|
||||
// peerStatusFromNode copies fields that exist in the Node struct for
|
||||
// current node and peers into the provided PeerStatus.
|
||||
func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) {
|
||||
ps.ID = n.StableID
|
||||
ps.Created = n.Created
|
||||
ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs)
|
||||
if n.Tags != nil {
|
||||
v := views.SliceOf(n.Tags)
|
||||
func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) {
|
||||
ps.ID = n.StableID()
|
||||
ps.Created = n.Created()
|
||||
ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs())
|
||||
if n.Tags().Len() != 0 {
|
||||
v := n.Tags()
|
||||
ps.Tags = &v
|
||||
}
|
||||
if n.PrimaryRoutes != nil {
|
||||
v := views.IPPrefixSliceOf(n.PrimaryRoutes)
|
||||
if n.PrimaryRoutes().Len() != 0 {
|
||||
v := n.PrimaryRoutes()
|
||||
ps.PrimaryRoutes = &v
|
||||
}
|
||||
|
||||
if n.Expired {
|
||||
if n.Expired() {
|
||||
ps.Expired = true
|
||||
}
|
||||
if t := n.KeyExpiry; !t.IsZero() {
|
||||
if t := n.KeyExpiry(); !t.IsZero() {
|
||||
t = t.Round(time.Second)
|
||||
ps.KeyExpiry = &t
|
||||
}
|
||||
@@ -798,7 +800,8 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) {
|
||||
// WhoIs reports the node and user who owns the node with the given IP:port.
|
||||
// If the IP address is a Tailscale IP, the provided port may be 0.
|
||||
// If ok == true, n and u are valid.
|
||||
func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
var zero tailcfg.NodeView
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
n, ok = b.nodeByAddr[ipp.Addr()]
|
||||
@@ -808,16 +811,16 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
|
||||
ip, ok = b.e.WhoIsIPPort(ipp)
|
||||
}
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
return zero, u, false
|
||||
}
|
||||
n, ok = b.nodeByAddr[ip]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
return zero, u, false
|
||||
}
|
||||
}
|
||||
u, ok = b.netMap.UserProfiles[n.User]
|
||||
u, ok = b.netMap.UserProfiles[n.User()]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
return zero, u, false
|
||||
}
|
||||
return n, u, true
|
||||
}
|
||||
@@ -1114,13 +1117,14 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool)
|
||||
}
|
||||
|
||||
for _, peer := range nm.Peers {
|
||||
for _, addr := range peer.Addresses {
|
||||
for i := range peer.Addresses().LenIter() {
|
||||
addr := peer.Addresses().At(i)
|
||||
if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
|
||||
continue
|
||||
}
|
||||
// Found the node being referenced, upgrade prefs to
|
||||
// reference it directly for next time.
|
||||
prefs.ExitNodeID = peer.StableID
|
||||
prefs.ExitNodeID = peer.StableID()
|
||||
prefs.ExitNodeIP = netip.Addr{}
|
||||
return true
|
||||
}
|
||||
@@ -1597,16 +1601,16 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
//
|
||||
// If this reports true, the packet filter is invalid (the server is either broken
|
||||
// or malicious) and should be ignored for safety.
|
||||
func packetFilterPermitsUnlockedNodes(peers []*tailcfg.Node, packetFilter []filter.Match) bool {
|
||||
func packetFilterPermitsUnlockedNodes(peers []tailcfg.NodeView, packetFilter []filter.Match) bool {
|
||||
var b netipx.IPSetBuilder
|
||||
var numUnlocked int
|
||||
for _, p := range peers {
|
||||
if !p.UnsignedPeerAPIOnly {
|
||||
if !p.UnsignedPeerAPIOnly() {
|
||||
continue
|
||||
}
|
||||
numUnlocked++
|
||||
for _, a := range p.AllowedIPs { // not only addresses!
|
||||
b.AddPrefix(a)
|
||||
for i := range p.AllowedIPs().LenIter() { // not only addresses!
|
||||
b.AddPrefix(p.AllowedIPs().At(i))
|
||||
}
|
||||
}
|
||||
if numUnlocked == 0 {
|
||||
@@ -1764,11 +1768,11 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet,
|
||||
|
||||
// dnsCIDRsEqual determines whether two CIDR lists are equal
|
||||
// for DNS map construction purposes (that is, only the first entry counts).
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []netip.Prefix) bool {
|
||||
if len(newAddr) != len(oldAddr) {
|
||||
func dnsCIDRsEqual(newAddr, oldAddr views.Slice[netip.Prefix]) bool {
|
||||
if newAddr.Len() != oldAddr.Len() {
|
||||
return false
|
||||
}
|
||||
if len(newAddr) == 0 || newAddr[0] == oldAddr[0] {
|
||||
if newAddr.Len() == 0 || newAddr.At(0) == oldAddr.At(0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -1792,16 +1796,16 @@ func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
|
||||
if new.Name != old.Name {
|
||||
return false
|
||||
}
|
||||
if !dnsCIDRsEqual(new.Addresses, old.Addresses) {
|
||||
if !dnsCIDRsEqual(views.SliceOf(new.Addresses), views.SliceOf(old.Addresses)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, newPeer := range new.Peers {
|
||||
oldPeer := old.Peers[i]
|
||||
if newPeer.Name != oldPeer.Name {
|
||||
if newPeer.Name() != oldPeer.Name() {
|
||||
return false
|
||||
}
|
||||
if !dnsCIDRsEqual(newPeer.Addresses, oldPeer.Addresses) {
|
||||
if !dnsCIDRsEqual(newPeer.Addresses(), oldPeer.Addresses()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -2301,7 +2305,8 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
} else {
|
||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix)))
|
||||
filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix)
|
||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered))
|
||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
|
||||
}
|
||||
}
|
||||
@@ -2417,8 +2422,8 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg
|
||||
if err != nil {
|
||||
pr.Err = err.Error()
|
||||
}
|
||||
if node != nil {
|
||||
pr.NodeName = node.Name
|
||||
if node.Valid() {
|
||||
pr.NodeName = node.Name()
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
@@ -2437,36 +2442,37 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer *tailcfg.Node, peerBase string, err error) {
|
||||
func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer tailcfg.NodeView, peerBase string, err error) {
|
||||
var zero tailcfg.NodeView
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
return nil, "", errors.New("no netmap")
|
||||
return zero, "", errors.New("no netmap")
|
||||
}
|
||||
peer, ok := nm.PeerByTailscaleIP(ip)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("no peer found with Tailscale IP %v", ip)
|
||||
return zero, "", fmt.Errorf("no peer found with Tailscale IP %v", ip)
|
||||
}
|
||||
if peer.Expired {
|
||||
return nil, "", errors.New("peer's node key has expired")
|
||||
if peer.Expired() {
|
||||
return zero, "", errors.New("peer's node key has expired")
|
||||
}
|
||||
base := peerAPIBase(nm, peer)
|
||||
if base == "" {
|
||||
return nil, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID, ip)
|
||||
return zero, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID(), ip)
|
||||
}
|
||||
outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return zero, "", err
|
||||
}
|
||||
tr := b.Dialer().PeerAPITransport()
|
||||
res, err := tr.RoundTrip(outReq)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return zero, "", err
|
||||
}
|
||||
defer res.Body.Close() // but unnecessary on HEAD responses
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("HTTP status %v", res.Status)
|
||||
return zero, "", fmt.Errorf("HTTP status %v", res.Status)
|
||||
}
|
||||
return peer, base, nil
|
||||
}
|
||||
@@ -2987,7 +2993,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
||||
disableSubnetsIfPAC := hasCapability(nm, tailcfg.NodeAttrDisableSubnetsIfPAC)
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -3036,7 +3042,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
|
||||
dcfg := dnsConfigForNetmap(nm, prefs, b.logf, version.OS())
|
||||
|
||||
err = b.e.Reconfig(cfg, rcfg, dcfg, nm.Debug)
|
||||
err = b.e.Reconfig(cfg, rcfg, dcfg)
|
||||
if err == wgengine.ErrNoChanges {
|
||||
return
|
||||
}
|
||||
@@ -3052,12 +3058,11 @@ func (b *LocalBackend) authReconfig() {
|
||||
// a runtime.GOOS.
|
||||
func shouldUseOneCGNATRoute(nm *netmap.NetworkMap, logf logger.Logf, versionOS string) bool {
|
||||
// Explicit enabling or disabling always take precedence.
|
||||
if nm.Debug != nil {
|
||||
if v, ok := nm.Debug.OneCGNATRoute.Get(); ok {
|
||||
logf("[v1] shouldUseOneCGNATRoute: explicit=%v", v)
|
||||
return v
|
||||
}
|
||||
if v, ok := controlclient.ControlOneCGNATSetting().Get(); ok {
|
||||
logf("[v1] shouldUseOneCGNATRoute: explicit=%v", v)
|
||||
return v
|
||||
}
|
||||
|
||||
// Also prefer to do this on the Mac, so that we don't need to constantly
|
||||
// update the network extension configuration (which is disruptive to
|
||||
// Chrome, see https://github.com/tailscale/tailscale/issues/3102). Only
|
||||
@@ -3098,17 +3103,24 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
// isn't configured to make MagicDNS resolution truly
|
||||
// magic. Details in
|
||||
// https://github.com/tailscale/tailscale/issues/1886.
|
||||
set := func(name string, addrs []netip.Prefix) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
set := func(name string, addrs views.Slice[netip.Prefix]) {
|
||||
if addrs.Len() == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
have4 := slices.ContainsFunc(addrs, tsaddr.PrefixIs4)
|
||||
var have4 bool
|
||||
for i := range addrs.LenIter() {
|
||||
if addrs.At(i).Addr().Is4() {
|
||||
have4 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var ips []netip.Addr
|
||||
for _, addr := range addrs {
|
||||
for i := range addrs.LenIter() {
|
||||
addr := addrs.At(i)
|
||||
if selfV6Only {
|
||||
if addr.Addr().Is6() {
|
||||
ips = append(ips, addr.Addr())
|
||||
@@ -3130,9 +3142,9 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
set(nm.Name, nm.Addresses)
|
||||
set(nm.Name, views.SliceOf(nm.Addresses))
|
||||
for _, peer := range nm.Peers {
|
||||
set(peer.Name, peer.Addresses)
|
||||
set(peer.Name(), peer.Addresses())
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
switch rec.Type {
|
||||
@@ -3654,7 +3666,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
|
||||
b.blockEngineUpdates(true)
|
||||
fallthrough
|
||||
case ipn.Stopped:
|
||||
err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{}, nil)
|
||||
err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{})
|
||||
if err != nil {
|
||||
b.logf("Reconfig(down): %v", err)
|
||||
}
|
||||
@@ -3796,7 +3808,7 @@ func (b *LocalBackend) stateMachine() {
|
||||
// a status update that predates the "I've shut down" update.
|
||||
func (b *LocalBackend) stopEngineAndWait() {
|
||||
b.logf("stopEngineAndWait...")
|
||||
b.e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{}, nil)
|
||||
b.e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{})
|
||||
b.requestEngineStatusAndWait()
|
||||
b.logf("stopEngineAndWait: done.")
|
||||
}
|
||||
@@ -3995,28 +4007,28 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
|
||||
// Update the nodeByAddr index.
|
||||
if b.nodeByAddr == nil {
|
||||
b.nodeByAddr = map[netip.Addr]*tailcfg.Node{}
|
||||
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{}
|
||||
}
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range b.nodeByAddr {
|
||||
b.nodeByAddr[k] = nil
|
||||
b.nodeByAddr[k] = tailcfg.NodeView{}
|
||||
}
|
||||
addNode := func(n *tailcfg.Node) {
|
||||
for _, ipp := range n.Addresses {
|
||||
if ipp.IsSingleIP() {
|
||||
addNode := func(n tailcfg.NodeView) {
|
||||
for i := range n.Addresses().LenIter() {
|
||||
if ipp := n.Addresses().At(i); ipp.IsSingleIP() {
|
||||
b.nodeByAddr[ipp.Addr()] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
if nm.SelfNode != nil {
|
||||
addNode(nm.SelfNode)
|
||||
addNode(nm.SelfNode.View())
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNode(p)
|
||||
}
|
||||
// Third pass, actually delete the unwanted items.
|
||||
for k, v := range b.nodeByAddr {
|
||||
if v == nil {
|
||||
if !v.Valid() {
|
||||
delete(b.nodeByAddr, k)
|
||||
}
|
||||
}
|
||||
@@ -4293,7 +4305,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, &apitype.FileTarget{
|
||||
Node: p,
|
||||
Node: p.AsStruct(),
|
||||
PeerAPIURL: peerAPI,
|
||||
})
|
||||
}
|
||||
@@ -4306,15 +4318,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
// the netmap.
|
||||
//
|
||||
// b.mu must be locked.
|
||||
func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool {
|
||||
if b.netMap == nil || p == nil {
|
||||
func (b *LocalBackend) peerIsTaildropTargetLocked(p tailcfg.NodeView) bool {
|
||||
if b.netMap == nil || !p.Valid() {
|
||||
return false
|
||||
}
|
||||
if b.netMap.User == p.User {
|
||||
if b.netMap.User == p.User() {
|
||||
return true
|
||||
}
|
||||
if len(p.Addresses) > 0 &&
|
||||
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
if p.Addresses().Len() > 0 &&
|
||||
b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
// Explicitly noted in the netmap ACL caps as a target.
|
||||
return true
|
||||
}
|
||||
@@ -4374,9 +4386,9 @@ func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func peerAPIPorts(peer *tailcfg.Node) (p4, p6 uint16) {
|
||||
svcs := peer.Hostinfo.Services()
|
||||
for i, n := 0, svcs.Len(); i < n; i++ {
|
||||
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
|
||||
svcs := peer.Hostinfo().Services()
|
||||
for i := range svcs.LenIter() {
|
||||
s := svcs.At(i)
|
||||
switch s.Proto {
|
||||
case tailcfg.PeerAPI4:
|
||||
@@ -4402,8 +4414,8 @@ func peerAPIURL(ip netip.Addr, port uint16) string {
|
||||
// peerAPIBase returns the "http://ip:port" URL base to reach peer's peerAPI.
|
||||
// It returns the empty string if the peer doesn't support the peerapi
|
||||
// or there's no matching address family based on the netmap's own addresses.
|
||||
func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
if nm == nil || peer == nil || !peer.Hostinfo.Valid() {
|
||||
func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
|
||||
if nm == nil || !peer.Valid() || !peer.Hostinfo().Valid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4429,8 +4441,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func nodeIP(n *tailcfg.Node, pred func(netip.Addr) bool) netip.Addr {
|
||||
for _, a := range n.Addresses {
|
||||
func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr {
|
||||
for i := range n.Addresses().LenIter() {
|
||||
a := n.Addresses().At(i)
|
||||
if a.IsSingleIP() && pred(a.Addr()) {
|
||||
return a.Addr()
|
||||
}
|
||||
@@ -4540,15 +4553,15 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
|
||||
return "", false
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.StableID == exitNodeID && peerCanProxyDNS(p) {
|
||||
if p.StableID() == exitNodeID && peerCanProxyDNS(p) {
|
||||
return peerAPIBase(nm, p) + "/dns-query", true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func peerCanProxyDNS(p *tailcfg.Node) bool {
|
||||
if p.Cap >= 26 {
|
||||
func peerCanProxyDNS(p tailcfg.NodeView) bool {
|
||||
if p.Cap() >= 26 {
|
||||
// Actually added at 25
|
||||
// (https://github.com/tailscale/tailscale/blob/3ae6f898cfdb58fd0e30937147dd6ce28c6808dd/tailcfg/tailcfg.go#L51)
|
||||
// so anything >= 26 can do it.
|
||||
@@ -4556,10 +4569,9 @@ func peerCanProxyDNS(p *tailcfg.Node) bool {
|
||||
}
|
||||
// If p.Cap is not populated (e.g. older control server), then do the old
|
||||
// thing of searching through services.
|
||||
services := p.Hostinfo.Services()
|
||||
for i, n := 0, services.Len(); i < n; i++ {
|
||||
s := services.At(i)
|
||||
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
services := p.Hostinfo().Services()
|
||||
for i := range services.LenIter() {
|
||||
if s := services.At(i); s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,46 +87,46 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Peers identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Peer list length",
|
||||
// length of Peers list differs
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{}})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "B"}})},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node lists identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node lists differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node2})},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 0}})},
|
||||
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 1}})},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -483,7 +483,7 @@ func TestPeerAPIBase(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerAPIBase(tt.nm, tt.peer)
|
||||
got := peerAPIBase(tt.nm, tt.peer.View())
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := packetFilterPermitsUnlockedNodes(tt.peers, tt.filter); got != tt.want {
|
||||
if got := packetFilterPermitsUnlockedNodes(nodeViews(tt.peers), tt.filter); got != tt.want {
|
||||
t.Errorf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,16 +69,17 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
|
||||
var toDelete map[int]bool // peer index => true
|
||||
for i, p := range nm.Peers {
|
||||
if p.UnsignedPeerAPIOnly {
|
||||
if p.UnsignedPeerAPIOnly() {
|
||||
// Not subject to tailnet lock.
|
||||
continue
|
||||
}
|
||||
if len(p.KeySignature) == 0 {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
|
||||
keySig := p.KeySignature()
|
||||
if keySig == "" {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID(), p.StableID())
|
||||
mak.Set(&toDelete, i, true)
|
||||
} else {
|
||||
if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err)
|
||||
if err := b.tka.authority.NodeKeyAuthorized(p.Key(), keySig); err != nil {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID(), p.StableID(), err)
|
||||
mak.Set(&toDelete, i, true)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +87,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
|
||||
// nm.Peers is ordered, so deletion must be order-preserving.
|
||||
if len(toDelete) > 0 {
|
||||
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
|
||||
peers := make([]tailcfg.NodeView, 0, len(nm.Peers))
|
||||
filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete))
|
||||
for i, p := range nm.Peers {
|
||||
if !toDelete[i] {
|
||||
@@ -94,13 +95,14 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
} else {
|
||||
// Record information about the node we filtered out.
|
||||
fp := ipnstate.TKAFilteredPeer{
|
||||
Name: p.Name,
|
||||
ID: p.ID,
|
||||
StableID: p.StableID,
|
||||
TailscaleIPs: make([]netip.Addr, len(p.Addresses)),
|
||||
NodeKey: p.Key,
|
||||
Name: p.Name(),
|
||||
ID: p.ID(),
|
||||
StableID: p.StableID(),
|
||||
TailscaleIPs: make([]netip.Addr, p.Addresses().Len()),
|
||||
NodeKey: p.Key(),
|
||||
}
|
||||
for i, addr := range p.Addresses {
|
||||
for i := range p.Addresses().LenIter() {
|
||||
addr := p.Addresses().At(i)
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
|
||||
fp.TailscaleIPs[i] = addr.Addr()
|
||||
}
|
||||
@@ -963,7 +965,7 @@ func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.N
|
||||
}
|
||||
|
||||
b.logf("Generated network-lock credential signature using %s", tkaKey.Public().CLIString())
|
||||
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
|
||||
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString([]byte(sig.Serialize())), tkaSuffixEncoder.EncodeToString(priv)), nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink asks the authority to verify the given deeplink
|
||||
|
||||
@@ -558,26 +558,26 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nm := netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
|
||||
{ID: 2, Key: n2.Public(), KeySignature: ""}, // missing sig
|
||||
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
b := &LocalBackend{
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{authority: authority},
|
||||
}
|
||||
b.tkaFilterNetmapLocked(&nm)
|
||||
b.tkaFilterNetmapLocked(nm)
|
||||
|
||||
want := []*tailcfg.Node{
|
||||
want := nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
}
|
||||
})
|
||||
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
||||
return x.Raw32() == y.Raw32()
|
||||
})
|
||||
@@ -987,7 +987,7 @@ func TestTKAAffectedSigs(t *testing.T) {
|
||||
if len(sigs) != 1 {
|
||||
t.Fatalf("len(sigs) = %d, want 1", len(sigs))
|
||||
}
|
||||
if !bytes.Equal(s.Serialize(), sigs[0]) {
|
||||
if s.Serialize() != sigs[0] {
|
||||
t.Errorf("unexpected signature: got %v, want %v", sigs[0], s.Serialize())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -32,7 +33,6 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
@@ -576,7 +576,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: nm.SelfNode.User == peerNode.User,
|
||||
isSelf: nm.SelfNode.User == peerNode.User(),
|
||||
remoteAddr: src,
|
||||
selfNode: nm.SelfNode,
|
||||
peerNode: peerNode,
|
||||
@@ -597,7 +597,7 @@ type peerAPIHandler struct {
|
||||
remoteAddr netip.AddrPort
|
||||
isSelf bool // whether peerNode is owned by same user as this node
|
||||
selfNode *tailcfg.Node // this node; always non-nil
|
||||
peerNode *tailcfg.Node // peerNode is who's making the request
|
||||
peerNode tailcfg.NodeView // peerNode is who's making the request
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
}
|
||||
|
||||
@@ -608,8 +608,8 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
// isAddressValid reports whether addr is a valid destination address for this
|
||||
// node originating from the peer.
|
||||
func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
|
||||
if h.peerNode.SelfNodeV4MasqAddrForThisPeer != nil {
|
||||
return *h.peerNode.SelfNodeV4MasqAddrForThisPeer == addr
|
||||
if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
|
||||
return *v == addr
|
||||
}
|
||||
pfx := netip.PrefixFrom(addr, addr.BitLen())
|
||||
return slices.Contains(h.selfNode.Addresses, pfx)
|
||||
@@ -733,7 +733,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
<body>
|
||||
<h1>Hello, %s (%v)</h1>
|
||||
This is my Tailscale device. Your device is %v.
|
||||
`, html.EscapeString(who), h.remoteAddr.Addr(), html.EscapeString(h.peerNode.ComputedName))
|
||||
`, html.EscapeString(who), h.remoteAddr.Addr(), html.EscapeString(h.peerNode.ComputedName()))
|
||||
|
||||
if h.isSelf {
|
||||
fmt.Fprintf(w, "<p>You are the owner of this node.\n")
|
||||
@@ -1024,7 +1024,7 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
|
||||
// canPutFile reports whether h can put a file ("Taildrop") to this node.
|
||||
func (h *peerAPIHandler) canPutFile() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
// Unsigned peers can't send files.
|
||||
return false
|
||||
}
|
||||
@@ -1038,7 +1038,7 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
// This node does not expose debug info.
|
||||
return false
|
||||
}
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
// Unsigned peers can't debug.
|
||||
return false
|
||||
}
|
||||
@@ -1047,7 +1047,7 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
|
||||
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
|
||||
func (h *peerAPIHandler) canWakeOnLAN() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
if h.peerNode.UnsignedPeerAPIOnly() {
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
|
||||
|
||||
@@ -462,9 +462,9 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
e.ph = &peerAPIHandler{
|
||||
isSelf: tt.isSelf,
|
||||
selfNode: selfNode,
|
||||
peerNode: &tailcfg.Node{
|
||||
peerNode: (&tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
}).View(),
|
||||
ps: &peerAPIServer{
|
||||
b: lb,
|
||||
},
|
||||
@@ -513,9 +513,9 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
}
|
||||
ph := &peerAPIHandler{
|
||||
isSelf: true,
|
||||
peerNode: &tailcfg.Node{
|
||||
peerNode: (&tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
}).View(),
|
||||
selfNode: &tailcfg.Node{
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
|
||||
},
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
|
||||
@@ -17,12 +17,12 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -257,7 +257,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
|
||||
return b.serveConfig
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -201,16 +201,16 @@ func TestServeHTTPProxy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
b.nodeByAddr = map[netip.Addr]*tailcfg.Node{
|
||||
netip.MustParseAddr("100.150.151.152"): {
|
||||
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{
|
||||
netip.MustParseAddr("100.150.151.152"): (&tailcfg.Node{
|
||||
ComputedName: "some-peer",
|
||||
User: tailcfg.UserID(1),
|
||||
},
|
||||
netip.MustParseAddr("100.150.151.153"): {
|
||||
}).View(),
|
||||
netip.MustParseAddr("100.150.151.153"): (&tailcfg.Node{
|
||||
ComputedName: "some-tagged-peer",
|
||||
Tags: []string{"tag:server", "tag:test"},
|
||||
User: tailcfg.UserID(1),
|
||||
},
|
||||
}).View(),
|
||||
}
|
||||
|
||||
// Start test serve endpoint.
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/ssh"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/util/mak"
|
||||
|
||||
@@ -209,7 +209,7 @@ type PeerStatus struct {
|
||||
// PrimaryRoutes are the routes this node is currently the primary
|
||||
// subnet router for, as determined by the control plane. It does
|
||||
// not include the IPs in TailscaleIPs.
|
||||
PrimaryRoutes *views.IPPrefixSlice `json:",omitempty"`
|
||||
PrimaryRoutes *views.Slice[netip.Prefix] `json:",omitempty"`
|
||||
|
||||
// Endpoints:
|
||||
Addrs []string
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
@@ -437,8 +437,8 @@ 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
|
||||
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
|
||||
UserProfile: &u, // always non-nil per WhoIsResponse contract
|
||||
CapMap: b.PeerCaps(ipp.Addr()),
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -506,7 +507,7 @@ func (p *Prefs) AdvertisesExitNode() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
return tsaddr.ContainsExitRoutes(p.AdvertiseRoutes)
|
||||
return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes))
|
||||
}
|
||||
|
||||
// SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js
|
||||
|
||||
// Package logheap logs a heap pprof profile.
|
||||
package logheap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogHeap uploads a JSON logtail record with the base64 heap pprof by means
|
||||
// of an HTTP POST request to the endpoint referred to in postURL.
|
||||
func LogHeap(postURL string) {
|
||||
if postURL == "" {
|
||||
return
|
||||
}
|
||||
runtime.GC()
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pprof.WriteHeapProfile(buf); err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", postURL, buf)
|
||||
if err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("LogHeap: %v", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package logheap
|
||||
|
||||
func LogHeap(postURL string) {
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Set is a string-to-Var map variable that satisfies the expvar.Var
|
||||
|
||||
@@ -18,19 +18,6 @@ const (
|
||||
debugStrideDelete = false
|
||||
)
|
||||
|
||||
// strideEntry is a strideTable entry.
|
||||
type strideEntry[T any] struct {
|
||||
// prefixIndex is the prefixIndex(...) value that caused this stride entry's
|
||||
// value to be populated, or 0 if value is nil.
|
||||
//
|
||||
// We need to keep track of this because allot() uses it to determine
|
||||
// whether an entry was propagated from a parent entry, or if it's a
|
||||
// different independent route.
|
||||
prefixIndex int
|
||||
// value is the value associated with the strideEntry, if any.
|
||||
value *T
|
||||
}
|
||||
|
||||
// strideTable is a binary tree that implements an 8-bit routing table.
|
||||
//
|
||||
// The leaves of the binary tree are host routes (/8s). Each parent is a
|
||||
@@ -54,7 +41,9 @@ type strideTable[T any] struct {
|
||||
// paper, it's hijacked through sneaky C 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]
|
||||
//
|
||||
// A nil value means no route matches the queried route.
|
||||
entries [lastHostIndex + 1]*T
|
||||
// children are the child tables of this table. Each child
|
||||
// represents the address space within one of this table's host
|
||||
// routes (/8).
|
||||
@@ -112,13 +101,6 @@ func (t *strideTable[T]) getOrCreateChild(addr uint8) (child *strideTable[T], cr
|
||||
return ret, false
|
||||
}
|
||||
|
||||
// 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]) {
|
||||
return t.entries[hostIndex(addr)].value, t.children[addr]
|
||||
}
|
||||
|
||||
// findFirstChild returns the first child strideTable in t, or nil if
|
||||
// t has no children.
|
||||
func (t *strideTable[T]) findFirstChild() *strideTable[T] {
|
||||
@@ -130,21 +112,41 @@ func (t *strideTable[T]) findFirstChild() *strideTable[T] {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasPrefixRootedAt reports whether t.entries[idx] is the root node of
|
||||
// a prefix.
|
||||
func (t *strideTable[T]) hasPrefixRootedAt(idx int) bool {
|
||||
val := t.entries[idx]
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
parentIdx := parentIndex(idx)
|
||||
if parentIdx == 0 {
|
||||
// idx is non-nil, and is at the 0/0 route position.
|
||||
return true
|
||||
}
|
||||
if parent := t.entries[parentIdx]; val != parent {
|
||||
// parent node in the tree isn't the same prefix, so idx must
|
||||
// be a root.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// allot is the core of the ART algorithm, enabling efficient insertion/deletion
|
||||
// while preserving very fast lookups.
|
||||
func (t *strideTable[T]) allot(idx int, oldPrefixIndex, newPrefixIndex int, val *T) {
|
||||
if t.entries[idx].prefixIndex != oldPrefixIndex {
|
||||
// current prefixIndex isn't what we expect. This is a recursive call
|
||||
// that found a child subtree that already has a more specific route
|
||||
// installed. Don't touch it.
|
||||
func (t *strideTable[T]) allot(idx int, old, new *T) {
|
||||
if t.entries[idx] != old {
|
||||
// current idx isn't what we expect. This is a recursive call
|
||||
// that found a child subtree that already has a more specific
|
||||
// route installed. Don't touch it.
|
||||
return
|
||||
}
|
||||
t.entries[idx].value = val
|
||||
t.entries[idx].prefixIndex = newPrefixIndex
|
||||
t.entries[idx] = new
|
||||
if idx >= firstHostIndex {
|
||||
// The entry we just updated was a host route, we're at the bottom of
|
||||
// the binary tree.
|
||||
@@ -152,51 +154,73 @@ func (t *strideTable[T]) allot(idx int, oldPrefixIndex, newPrefixIndex int, val
|
||||
}
|
||||
// Propagate the allotment to this node's children.
|
||||
left := idx << 1
|
||||
t.allot(left, oldPrefixIndex, newPrefixIndex, val)
|
||||
t.allot(left, old, new)
|
||||
right := left + 1
|
||||
t.allot(right, oldPrefixIndex, newPrefixIndex, val)
|
||||
t.allot(right, old, new)
|
||||
}
|
||||
|
||||
// insert adds the route addr/prefixLen to t, with value val.
|
||||
func (t *strideTable[T]) insert(addr uint8, prefixLen int, val *T) {
|
||||
func (t *strideTable[T]) insert(addr uint8, prefixLen int, val T) {
|
||||
idx := prefixIndex(addr, prefixLen)
|
||||
old := t.entries[idx].value
|
||||
oldIdx := t.entries[idx].prefixIndex
|
||||
if oldIdx == idx && old == val {
|
||||
// This exact prefix+value is already in the table.
|
||||
return
|
||||
}
|
||||
t.allot(idx, oldIdx, idx, val)
|
||||
if oldIdx != idx {
|
||||
// This route entry was freshly created (not just updated), that's a new
|
||||
// reference.
|
||||
if !t.hasPrefixRootedAt(idx) {
|
||||
// This route entry is being freshly created (not just
|
||||
// updated), that's a new reference.
|
||||
t.routeRefs++
|
||||
}
|
||||
|
||||
old := t.entries[idx]
|
||||
|
||||
// For allot to work correctly, each distinct prefix in the
|
||||
// strideTable must have a different value pointer, even if val is
|
||||
// identical. This new()+assignment guarantees that each inserted
|
||||
// prefix gets a unique address.
|
||||
p := new(T)
|
||||
*p = val
|
||||
|
||||
t.allot(idx, old, p)
|
||||
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.
|
||||
func (t *strideTable[T]) delete(addr uint8, prefixLen int) *T {
|
||||
// delete removes the route addr/prefixLen from t. Reports whether the
|
||||
// prefix existed in the table prior to deletion.
|
||||
func (t *strideTable[T]) delete(addr uint8, prefixLen int) (wasPresent bool) {
|
||||
idx := prefixIndex(addr, prefixLen)
|
||||
recordedIdx := t.entries[idx].prefixIndex
|
||||
if recordedIdx != idx {
|
||||
if !t.hasPrefixRootedAt(idx) {
|
||||
// Route entry doesn't exist
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
val := t.entries[idx].value
|
||||
|
||||
parentIdx := idx >> 1
|
||||
t.allot(idx, idx, t.entries[parentIdx].prefixIndex, t.entries[parentIdx].value)
|
||||
val := t.entries[idx]
|
||||
var parentVal *T
|
||||
if parentIdx := parentIndex(idx); parentIdx != 0 {
|
||||
parentVal = t.entries[parentIdx]
|
||||
}
|
||||
|
||||
t.allot(idx, val, parentVal)
|
||||
t.routeRefs--
|
||||
return val
|
||||
return true
|
||||
}
|
||||
|
||||
// get does a route lookup for addr and returns the associated value, or nil if
|
||||
// no route matched.
|
||||
func (t *strideTable[T]) get(addr uint8) *T {
|
||||
return t.entries[hostIndex(addr)].value
|
||||
// get does a route lookup for addr and (value, true) if a matching
|
||||
// route exists, or (zero, false) otherwise.
|
||||
func (t *strideTable[T]) get(addr uint8) (ret T, ok bool) {
|
||||
if val := t.entries[hostIndex(addr)]; val != nil {
|
||||
return *val, true
|
||||
}
|
||||
return ret, false
|
||||
}
|
||||
|
||||
// getValAndChild returns both the prefix value and child strideTable
|
||||
// for addr. valOK reports whether a prefix value exists for addr, and
|
||||
// child is non-nil if a child exists for addr.
|
||||
func (t *strideTable[T]) getValAndChild(addr uint8) (val T, valOK bool, child *strideTable[T]) {
|
||||
vp := t.entries[hostIndex(addr)]
|
||||
if vp != nil {
|
||||
val = *vp
|
||||
valOK = true
|
||||
}
|
||||
child = t.children[addr]
|
||||
return
|
||||
}
|
||||
|
||||
// TableDebugString returns the contents of t, formatted as a table with one
|
||||
@@ -208,10 +232,10 @@ func (t *strideTable[T]) tableDebugString() string {
|
||||
continue
|
||||
}
|
||||
v := "(nil)"
|
||||
if ent.value != nil {
|
||||
v = fmt.Sprint(*ent.value)
|
||||
if ent != nil {
|
||||
v = fmt.Sprint(*ent)
|
||||
}
|
||||
fmt.Fprintf(&ret, "idx=%3d (%s), parent=%3d (%s), val=%v\n", i, formatPrefixTable(inversePrefixIndex(i)), ent.prefixIndex, formatPrefixTable(inversePrefixIndex((ent.prefixIndex))), v)
|
||||
fmt.Fprintf(&ret, "idx=%3d (%s), val=%v\n", i, formatPrefixTable(inversePrefixIndex(i)), v)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
@@ -227,8 +251,8 @@ 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)
|
||||
if t.hasPrefixRootedAt(idx) {
|
||||
fmt.Fprintf(w, "%s%d/%d (%02x/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx])
|
||||
indent += 2
|
||||
}
|
||||
if idx >= firstHostIndex {
|
||||
@@ -251,6 +275,12 @@ func prefixIndex(addr uint8, prefixLen int) int {
|
||||
return (int(addr) >> (8 - prefixLen)) + (1 << prefixLen)
|
||||
}
|
||||
|
||||
// parentIndex returns the index of idx's parent prefix, or 0 if idx
|
||||
// is the index of 0/0.
|
||||
func parentIndex(idx int) int {
|
||||
return idx >> 1
|
||||
}
|
||||
|
||||
// hostIndex returns the array index of the host route for addr.
|
||||
// It is equivalent to prefixIndex(addr, 8).
|
||||
func hostIndex(addr uint8) int {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestInversePrefix(t *testing.T) {
|
||||
@@ -65,10 +65,10 @@ func TestStrideTableInsert(t *testing.T) {
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
addr := uint8(i)
|
||||
slowVal := slow.get(addr)
|
||||
fastVal := fast.get(addr)
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal)
|
||||
slowVal, slowOK := slow.get(addr)
|
||||
fastVal, fastOK := fast.get(addr)
|
||||
if !getsEqual(fastVal, fastOK, slowVal, slowOK) {
|
||||
t.Fatalf("strideTable.get(%d) = (%v, %v), want (%v, %v)", addr, fastVal, fastOK, slowVal, slowOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,14 @@ func TestStrideTableInsertShuffled(t *testing.T) {
|
||||
|
||||
zero := 0
|
||||
rt := strideTable[int]{}
|
||||
// strideTable has a value interface, but internally has to keep
|
||||
// track of distinct routes even if they all have the same
|
||||
// value. rtZero uses the same value for all routes, and expects
|
||||
// correct behavior.
|
||||
rtZero := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, route.val)
|
||||
rtZero.insert(route.addr, route.len, &zero)
|
||||
rtZero.insert(route.addr, route.len, zero)
|
||||
}
|
||||
|
||||
// Order of insertion should not affect the final shape of the stride table.
|
||||
@@ -105,15 +109,15 @@ 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.tableDebugString(), rt2.tableDebugString()); 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))
|
||||
}
|
||||
|
||||
rtZero2 := strideTable[int]{}
|
||||
for _, route := range routes2 {
|
||||
rtZero2.insert(route.addr, route.len, &zero)
|
||||
rtZero2.insert(route.addr, route.len, zero)
|
||||
}
|
||||
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" {
|
||||
if diff := cmp.Diff(rtZero.tableDebugString(), rtZero2.tableDebugString(), cmpDiffOpts...); 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))
|
||||
}
|
||||
}
|
||||
@@ -150,10 +154,10 @@ func TestStrideTableDelete(t *testing.T) {
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
addr := uint8(i)
|
||||
slowVal := slow.get(addr)
|
||||
fastVal := fast.get(addr)
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal)
|
||||
slowVal, slowOK := slow.get(addr)
|
||||
fastVal, fastOK := fast.get(addr)
|
||||
if !getsEqual(fastVal, fastOK, slowVal, slowOK) {
|
||||
t.Fatalf("strideTable.get(%d) = (%v, %v), want (%v, %v)", addr, fastVal, fastOK, slowVal, slowOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,10 +172,14 @@ func TestStrideTableDeleteShuffle(t *testing.T) {
|
||||
|
||||
zero := 0
|
||||
rt := strideTable[int]{}
|
||||
// strideTable has a value interface, but internally has to keep
|
||||
// track of distinct routes even if they all have the same
|
||||
// value. rtZero uses the same value for all routes, and expects
|
||||
// correct behavior.
|
||||
rtZero := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, route.val)
|
||||
rtZero.insert(route.addr, route.len, &zero)
|
||||
rtZero.insert(route.addr, route.len, zero)
|
||||
}
|
||||
for _, route := range toDelete {
|
||||
rt.delete(route.addr, route.len)
|
||||
@@ -189,18 +197,18 @@ 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.tableDebugString(), rt2.tableDebugString(), cmpDiffOpts...); 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))
|
||||
}
|
||||
|
||||
rtZero2 := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rtZero2.insert(route.addr, route.len, &zero)
|
||||
rtZero2.insert(route.addr, route.len, zero)
|
||||
}
|
||||
for _, route := range toDelete2 {
|
||||
rtZero2.delete(route.addr, route.len)
|
||||
}
|
||||
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" {
|
||||
if diff := cmp.Diff(rtZero.tableDebugString(), rtZero2.tableDebugString(), cmpDiffOpts...); 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))
|
||||
}
|
||||
}
|
||||
@@ -218,31 +226,35 @@ func forStrideCountAndOrdering(b *testing.B, fn func(b *testing.B, routes []slow
|
||||
routes := shufflePrefixes(allPrefixes())
|
||||
for _, nroutes := range strideRouteCount {
|
||||
b.Run(fmt.Sprint(nroutes), func(b *testing.B) {
|
||||
routes := append([]slowEntry[int](nil), routes[:nroutes]...)
|
||||
b.Run("random_order", func(b *testing.B) {
|
||||
runAndRecord := func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var startMem, endMem runtime.MemStats
|
||||
runtime.ReadMemStats(&startMem)
|
||||
fn(b, routes)
|
||||
})
|
||||
runtime.ReadMemStats(&endMem)
|
||||
ops := float64(b.N) * float64(len(routes))
|
||||
allocs := float64(endMem.Mallocs - startMem.Mallocs)
|
||||
bytes := float64(endMem.TotalAlloc - startMem.TotalAlloc)
|
||||
b.ReportMetric(roundFloat64(allocs/ops), "allocs/op")
|
||||
b.ReportMetric(roundFloat64(bytes/ops), "B/op")
|
||||
}
|
||||
|
||||
routes := append([]slowEntry[int](nil), routes[:nroutes]...)
|
||||
b.Run("random_order", runAndRecord)
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].len < routes[j].len {
|
||||
return true
|
||||
}
|
||||
return routes[i].addr < routes[j].addr
|
||||
})
|
||||
b.Run("largest_first", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
fn(b, routes)
|
||||
})
|
||||
b.Run("largest_first", runAndRecord)
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[j].len < routes[i].len {
|
||||
return true
|
||||
}
|
||||
return routes[j].addr < routes[i].addr
|
||||
})
|
||||
b.Run("smallest_first", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
fn(b, routes)
|
||||
})
|
||||
b.Run("smallest_first", runAndRecord)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -253,7 +265,7 @@ func BenchmarkStrideTableInsertion(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var rt strideTable[int]
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, &val)
|
||||
rt.insert(route.addr, route.len, val)
|
||||
}
|
||||
}
|
||||
inserts := float64(b.N) * float64(len(routes))
|
||||
@@ -269,7 +281,7 @@ func BenchmarkStrideTableDeletion(b *testing.B) {
|
||||
val := 0
|
||||
var rt strideTable[int]
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, &val)
|
||||
rt.insert(route.addr, route.len, val)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -287,7 +299,7 @@ func BenchmarkStrideTableDeletion(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
var writeSink *int
|
||||
var writeSink int
|
||||
|
||||
func BenchmarkStrideTableGet(b *testing.B) {
|
||||
// No need to forCountAndOrdering here, route lookup time is independent of
|
||||
@@ -300,7 +312,7 @@ func BenchmarkStrideTableGet(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writeSink = rt.get(uint8(i))
|
||||
writeSink, _ = rt.get(uint8(i))
|
||||
}
|
||||
gets := float64(b.N)
|
||||
elapsedSec := b.Elapsed().Seconds()
|
||||
@@ -318,7 +330,7 @@ type slowTable[T any] struct {
|
||||
type slowEntry[T any] struct {
|
||||
addr uint8
|
||||
len int
|
||||
val *T
|
||||
val T
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) String() string {
|
||||
@@ -331,13 +343,14 @@ func (t *slowTable[T]) String() string {
|
||||
})
|
||||
var ret bytes.Buffer
|
||||
for _, pfx := range pfxs {
|
||||
fmt.Fprintf(&ret, "%3d/%d (%08b/%08b) = %v\n", pfx.addr, pfx.len, pfx.addr, pfxMask(pfx.len), *pfx.val)
|
||||
fmt.Fprintf(&ret, "%3d/%d (%08b/%08b) = %v\n", pfx.addr, pfx.len, pfx.addr, pfxMask(pfx.len), pfx.val)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) insert(addr uint8, prefixLen int, val *T) {
|
||||
func (t *slowTable[T]) insert(addr uint8, prefixLen int, val T) {
|
||||
t.delete(addr, prefixLen) // no-op if prefix doesn't exist
|
||||
|
||||
t.prefixes = append(t.prefixes, slowEntry[T]{addr, prefixLen, val})
|
||||
}
|
||||
|
||||
@@ -352,18 +365,15 @@ func (t *slowTable[T]) delete(addr uint8, prefixLen int) {
|
||||
t.prefixes = pfx
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) get(addr uint8) *T {
|
||||
var (
|
||||
ret *T
|
||||
curLen = -1
|
||||
)
|
||||
func (t *slowTable[T]) get(addr uint8) (ret T, ok bool) {
|
||||
var curLen = -1
|
||||
for _, e := range t.prefixes {
|
||||
if addr&pfxMask(e.len) == e.addr && e.len >= curLen {
|
||||
ret = e.val
|
||||
curLen = e.len
|
||||
}
|
||||
}
|
||||
return ret
|
||||
return ret, curLen != -1
|
||||
}
|
||||
|
||||
func pfxMask(pfxLen int) uint8 {
|
||||
@@ -374,7 +384,7 @@ func allPrefixes() []slowEntry[int] {
|
||||
ret := make([]slowEntry[int], 0, lastHostIndex)
|
||||
for i := 1; i < lastHostIndex+1; i++ {
|
||||
a, l := inversePrefixIndex(i)
|
||||
ret = append(ret, slowEntry[int]{a, l, ptr.To(i)})
|
||||
ret = append(ret, slowEntry[int]{a, l, i})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -393,6 +403,15 @@ func formatSlowEntriesShort[T any](ents []slowEntry[T]) string {
|
||||
}
|
||||
|
||||
var cmpDiffOpts = []cmp.Option{
|
||||
cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{}),
|
||||
cmp.Comparer(func(a, b netip.Prefix) bool { return a == b }),
|
||||
}
|
||||
|
||||
func getsEqual[T comparable](a T, aOK bool, b T, bOK bool) bool {
|
||||
if !aOK && !bOK {
|
||||
return true
|
||||
}
|
||||
if aOK != bOK {
|
||||
return false
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (t *Table[T]) tableForAddr(addr netip.Addr) *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 {
|
||||
func (t *Table[T]) Get(addr netip.Addr) (ret T, ok bool) {
|
||||
t.init()
|
||||
|
||||
// Ideally we would use addr.AsSlice here, but AsSlice is just
|
||||
@@ -84,13 +84,13 @@ func (t *Table[T]) Get(addr netip.Addr) *T {
|
||||
const maxDepth = 16
|
||||
type prefixAndRoute struct {
|
||||
prefix netip.Prefix
|
||||
route *T
|
||||
route T
|
||||
}
|
||||
strideMatch := make([]prefixAndRoute, 0, maxDepth)
|
||||
findLeaf:
|
||||
for {
|
||||
rt, child := st.getValAndChild(bs[i])
|
||||
if rt != nil {
|
||||
rt, rtOK, child := st.getValAndChild(bs[i])
|
||||
if rtOK {
|
||||
// This strideTable contains a route that may be relevant to our
|
||||
// search, remember it.
|
||||
strideMatch = append(strideMatch, prefixAndRoute{st.prefix, rt})
|
||||
@@ -115,7 +115,7 @@ findLeaf:
|
||||
// the correct most-specific route.
|
||||
for i := len(strideMatch) - 1; i >= 0; i-- {
|
||||
if m := strideMatch[i]; m.prefix.Contains(addr) {
|
||||
return m.route
|
||||
return m.route, true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,16 +123,13 @@ findLeaf:
|
||||
// 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
|
||||
return ret, false
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
@@ -423,7 +420,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
|
||||
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 {
|
||||
if routeExisted := st.delete(bs[byteIdx], numBits); !routeExisted {
|
||||
// We're in the right strideTable, but pfx wasn't in
|
||||
// it. Refcounts haven't changed, so we can skip cleanup.
|
||||
if debugDelete {
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestRegression(t *testing.T) {
|
||||
@@ -30,17 +28,16 @@ func TestRegression(t *testing.T) {
|
||||
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)
|
||||
tbl.Insert(p("226.205.197.0/24"), 1)
|
||||
slow.insert(p("226.205.197.0/24"), 1)
|
||||
tbl.Insert(p("226.205.0.0/16"), 2)
|
||||
slow.insert(p("226.205.0.0/16"), 2)
|
||||
|
||||
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)
|
||||
got, gotOK := tbl.Get(probe)
|
||||
want, wantOK := slow.get(probe)
|
||||
if !getsEqual(got, gotOK, want, wantOK) {
|
||||
t.Fatalf("got (%v, %v), want (%v, %v)", got, gotOK, want, wantOK)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,18 +46,18 @@ func TestRegression(t *testing.T) {
|
||||
// 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)
|
||||
t1.Insert(p("136.20.0.0/16"), 1)
|
||||
t1.Insert(p("136.20.201.62/32"), 2)
|
||||
|
||||
t2.Insert(p("136.20.201.62/32"), v2)
|
||||
t2.Insert(p("136.20.0.0/16"), v1)
|
||||
t2.Insert(p("136.20.201.62/32"), 2)
|
||||
t2.Insert(p("136.20.0.0/16"), 1)
|
||||
|
||||
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)
|
||||
got1, ok1 := t1.Get(a)
|
||||
got2, ok2 := t2.Get(a)
|
||||
if !getsEqual(got1, ok1, got2, ok2) {
|
||||
t.Errorf("Get(%q) is insertion order dependent: t1=(%v, %v), t2=(%v, %v)", a, got1, ok1, got2, ok2)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -99,7 +96,7 @@ func TestInsert(t *testing.T) {
|
||||
p := netip.MustParsePrefix
|
||||
|
||||
// Create a new leaf strideTable, with compressed path
|
||||
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", -1},
|
||||
@@ -114,7 +111,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert into previous leaf, no tree changes
|
||||
tbl.Insert(p("192.168.0.2/32"), ptr.To(2))
|
||||
tbl.Insert(p("192.168.0.2/32"), 2)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -129,7 +126,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert into previous leaf, unaligned prefix covering the /32s
|
||||
tbl.Insert(p("192.168.0.0/26"), ptr.To(7))
|
||||
tbl.Insert(p("192.168.0.0/26"), 7)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -144,7 +141,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Create a different leaf elsewhere
|
||||
tbl.Insert(p("10.0.0.0/27"), ptr.To(3))
|
||||
tbl.Insert(p("10.0.0.0/27"), 3)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -159,7 +156,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert that creates a new intermediate table and a new child
|
||||
tbl.Insert(p("192.168.1.1/32"), ptr.To(4))
|
||||
tbl.Insert(p("192.168.1.1/32"), 4)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -174,7 +171,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert that creates a new intermediate table but no new child
|
||||
tbl.Insert(p("192.170.0.0/16"), ptr.To(5))
|
||||
tbl.Insert(p("192.170.0.0/16"), 5)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -190,7 +187,7 @@ func TestInsert(t *testing.T) {
|
||||
|
||||
// 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))
|
||||
tbl.Insert(p("192.180.0.1/32"), 8)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -206,7 +203,7 @@ func TestInsert(t *testing.T) {
|
||||
|
||||
// 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))
|
||||
tbl.Insert(p("192.180.0.0/21"), 9)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -221,7 +218,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert a default route, those have their own codepath.
|
||||
tbl.Insert(p("0.0.0.0/0"), ptr.To(6))
|
||||
tbl.Insert(p("0.0.0.0/0"), 6)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -238,7 +235,7 @@ func TestInsert(t *testing.T) {
|
||||
// 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))
|
||||
tbl.Insert(p("ff:aaaa::1/128"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", -1},
|
||||
@@ -253,7 +250,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert into previous leaf, no tree changes
|
||||
tbl.Insert(p("ff:aaaa::2/128"), ptr.To(2))
|
||||
tbl.Insert(p("ff:aaaa::2/128"), 2)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -268,7 +265,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert into previous leaf, unaligned prefix covering the /128s
|
||||
tbl.Insert(p("ff:aaaa::/125"), ptr.To(7))
|
||||
tbl.Insert(p("ff:aaaa::/125"), 7)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -283,7 +280,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Create a different leaf elsewhere
|
||||
tbl.Insert(p("ffff:bbbb::/120"), ptr.To(3))
|
||||
tbl.Insert(p("ffff:bbbb::/120"), 3)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -298,7 +295,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert that creates a new intermediate table and a new child
|
||||
tbl.Insert(p("ff:aaaa:aaaa::1/128"), ptr.To(4))
|
||||
tbl.Insert(p("ff:aaaa:aaaa::1/128"), 4)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -313,7 +310,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert that creates a new intermediate table but no new child
|
||||
tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), ptr.To(5))
|
||||
tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), 5)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -329,7 +326,7 @@ func TestInsert(t *testing.T) {
|
||||
|
||||
// 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))
|
||||
tbl.Insert(p("ff:cccc::1/128"), 8)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -345,7 +342,7 @@ func TestInsert(t *testing.T) {
|
||||
|
||||
// Insert that creates a new intermediate table but no new child,
|
||||
// with an unaligned intermediate
|
||||
tbl.Insert(p("ff:cccc::/37"), ptr.To(9))
|
||||
tbl.Insert(p("ff:cccc::/37"), 9)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -360,7 +357,7 @@ func TestInsert(t *testing.T) {
|
||||
})
|
||||
|
||||
// Insert a default route, those have their own codepath.
|
||||
tbl.Insert(p("::/0"), ptr.To(6))
|
||||
tbl.Insert(p("::/0"), 6)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"ff:aaaa::1", 1},
|
||||
{"ff:aaaa::2", 2},
|
||||
@@ -384,7 +381,7 @@ func TestDelete(t *testing.T) {
|
||||
tbl := &Table[int]{}
|
||||
checkSize(t, tbl, 2)
|
||||
|
||||
tbl.Insert(p("10.0.0.0/8"), ptr.To(1))
|
||||
tbl.Insert(p("10.0.0.0/8"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"10.0.0.1", 1},
|
||||
{"255.255.255.255", -1},
|
||||
@@ -403,7 +400,7 @@ func TestDelete(t *testing.T) {
|
||||
tbl := &Table[int]{}
|
||||
checkSize(t, tbl, 2)
|
||||
|
||||
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"255.255.255.255", -1},
|
||||
@@ -421,8 +418,8 @@ func TestDelete(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))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
tbl.Insert(p("192.180.0.1/32"), 2)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.180.0.1", 2},
|
||||
@@ -442,9 +439,9 @@ func TestDelete(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))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
tbl.Insert(p("192.180.0.1/32"), 2)
|
||||
tbl.Insert(p("192.0.0.0/10"), 3)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.180.0.1", 2},
|
||||
@@ -466,9 +463,9 @@ func TestDelete(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))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
tbl.Insert(p("192.180.0.1/32"), 2)
|
||||
tbl.Insert(p("192.200.0.1/32"), 3)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.180.0.1", 2},
|
||||
@@ -490,7 +487,7 @@ func TestDelete(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))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.255.0.1", -1},
|
||||
@@ -509,7 +506,7 @@ func TestDelete(t *testing.T) {
|
||||
// with a wrong turn.
|
||||
tbl := &Table[int]{}
|
||||
checkSize(t, tbl, 2)
|
||||
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.255.0.1", -1},
|
||||
@@ -528,7 +525,7 @@ func TestDelete(t *testing.T) {
|
||||
// leaf doesn't contain route.
|
||||
tbl := &Table[int]{}
|
||||
checkSize(t, tbl, 2)
|
||||
tbl.Insert(p("192.168.0.1/32"), ptr.To(1))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.255.0.1", -1},
|
||||
@@ -547,8 +544,8 @@ func TestDelete(t *testing.T) {
|
||||
// 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))
|
||||
tbl.Insert(p("192.168.0.1/32"), 1)
|
||||
tbl.Insert(p("192.168.0.0/22"), 2)
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
{"192.168.0.1", 1},
|
||||
{"192.168.0.2", 2},
|
||||
@@ -568,7 +565,7 @@ func TestDelete(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.Insert(p("0.0.0.0/0"), 1)
|
||||
tbl.Delete(p("0.0.0.0/0"))
|
||||
|
||||
checkRoutes(t, tbl, []tableTest{
|
||||
@@ -595,20 +592,20 @@ func TestInsertCompare(t *testing.T) {
|
||||
t.Logf(fast.debugSummary())
|
||||
}
|
||||
|
||||
seenVals4 := map[*int]bool{}
|
||||
seenVals6 := map[*int]bool{}
|
||||
seenVals4 := map[int]bool{}
|
||||
seenVals6 := map[int]bool{}
|
||||
for i := 0; i < 10_000; i++ {
|
||||
a := randomAddr()
|
||||
slowVal := slow.get(a)
|
||||
fastVal := fast.Get(a)
|
||||
slowVal, slowOK := slow.get(a)
|
||||
fastVal, fastOK := fast.Get(a)
|
||||
if !getsEqual(slowVal, slowOK, fastVal, fastOK) {
|
||||
t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, fastVal, fastOK, slowVal, slowOK)
|
||||
}
|
||||
if a.Is6() {
|
||||
seenVals6[fastVal] = true
|
||||
} else {
|
||||
seenVals4[fastVal] = true
|
||||
}
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("get(%q) = %p, want %p", a, fastVal, slowVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
|
||||
@@ -667,13 +664,10 @@ func TestInsertShuffled(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, a := range addrs {
|
||||
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))
|
||||
val1, ok1 := rt.Get(a)
|
||||
val2, ok2 := rt2.Get(a)
|
||||
if !getsEqual(val1, ok1, val2, ok2) {
|
||||
t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, val2, ok2, val1, ok1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,20 +721,20 @@ func TestDeleteCompare(t *testing.T) {
|
||||
fast.Delete(pfx.pfx)
|
||||
}
|
||||
|
||||
seenVals4 := map[*int]bool{}
|
||||
seenVals6 := map[*int]bool{}
|
||||
seenVals4 := map[int]bool{}
|
||||
seenVals6 := map[int]bool{}
|
||||
for i := 0; i < numProbes; i++ {
|
||||
a := randomAddr()
|
||||
slowVal := slow.get(a)
|
||||
fastVal := fast.Get(a)
|
||||
slowVal, slowOK := slow.get(a)
|
||||
fastVal, fastOK := fast.Get(a)
|
||||
if !getsEqual(slowVal, slowOK, fastVal, fastOK) {
|
||||
t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, fastVal, fastOK, slowVal, slowOK)
|
||||
}
|
||||
if a.Is6() {
|
||||
seenVals6[fastVal] = true
|
||||
} else {
|
||||
seenVals4[fastVal] = true
|
||||
}
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("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
|
||||
@@ -814,13 +808,10 @@ func TestDeleteShuffled(t *testing.T) {
|
||||
// test for equivalence statistically with random probes instead.
|
||||
for i := 0; i < numProbes; 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.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
|
||||
val1, ok1 := rt.Get(a)
|
||||
val2, ok2 := rt2.Get(a)
|
||||
if !getsEqual(val1, ok1, val2, ok2) {
|
||||
t.Errorf("get(%q) = (%v, %v), want (%v, %v)", a, val2, ok2, val1, ok1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -868,12 +859,12 @@ type tableTest struct {
|
||||
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)
|
||||
v, ok := tbl.Get(netip.MustParseAddr(tc.addr))
|
||||
if !ok && tc.want != -1 {
|
||||
t.Errorf("lookup %q got (%v, %v), want (_, false)", tc.addr, v, ok)
|
||||
}
|
||||
if v != nil && *v != tc.want {
|
||||
t.Errorf("lookup %q got %d, want %d", tc.addr, *v, tc.want)
|
||||
if ok && v != tc.want {
|
||||
t.Errorf("lookup %q got (%v, %v), want (%v, true)", tc.addr, v, ok, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1005,7 +996,7 @@ func BenchmarkTableGet(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
addr := genAddr()
|
||||
t.Start()
|
||||
writeSink = rt.Get(addr)
|
||||
writeSink, _ = rt.Get(addr)
|
||||
t.Stop()
|
||||
}
|
||||
})
|
||||
@@ -1112,7 +1103,7 @@ type slowPrefixTable[T any] struct {
|
||||
|
||||
type slowPrefixEntry[T any] struct {
|
||||
pfx netip.Prefix
|
||||
val *T
|
||||
val T
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
|
||||
@@ -1127,7 +1118,7 @@ func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
|
||||
t.prefixes = ret
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) {
|
||||
func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val T) {
|
||||
pfx = pfx.Masked()
|
||||
for i, ent := range t.prefixes {
|
||||
if ent.pfx == pfx {
|
||||
@@ -1138,11 +1129,8 @@ func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) {
|
||||
t.prefixes = append(t.prefixes, slowPrefixEntry[T]{pfx, val})
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) get(addr netip.Addr) *T {
|
||||
var (
|
||||
ret *T
|
||||
bestLen = -1
|
||||
)
|
||||
func (t *slowPrefixTable[T]) get(addr netip.Addr) (ret T, ok bool) {
|
||||
bestLen := -1
|
||||
|
||||
for _, pfx := range t.prefixes {
|
||||
if pfx.pfx.Contains(addr) && pfx.pfx.Bits() > bestLen {
|
||||
@@ -1150,7 +1138,7 @@ func (t *slowPrefixTable[T]) get(addr netip.Addr) *T {
|
||||
bestLen = pfx.pfx.Bits()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
return ret, bestLen != -1
|
||||
}
|
||||
|
||||
// randomPrefixes returns n randomly generated prefixes and associated values,
|
||||
@@ -1176,7 +1164,7 @@ func randomPrefixes4(n int) []slowPrefixEntry[int] {
|
||||
|
||||
ret := make([]slowPrefixEntry[int], 0, len(pfxs))
|
||||
for pfx := range pfxs {
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())})
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, rand.Int()})
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1197,7 +1185,7 @@ func randomPrefixes6(n int) []slowPrefixEntry[int] {
|
||||
|
||||
ret := make([]slowPrefixEntry[int], 0, len(pfxs))
|
||||
for pfx := range pfxs {
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())})
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, rand.Int()})
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1230,14 +1218,6 @@ func randomAddr6() netip.Addr {
|
||||
return netip.AddrFrom16(b)
|
||||
}
|
||||
|
||||
// printIntPtr returns *v as a string, or the literal "<nil>" if v is nil.
|
||||
func printIntPtr(v *int) string {
|
||||
if v == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprint(*v)
|
||||
}
|
||||
|
||||
// roundFloat64 rounds f to 2 decimal places, for display.
|
||||
//
|
||||
// It round-trips through a float->string->float conversion, so should not be
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/netmon"
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
|
||||
@@ -15,8 +15,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -19,11 +19,11 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dns/recursive"
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// ChromeOSVMRange returns the subset of the CGNAT IPv4 range used by
|
||||
@@ -225,9 +226,10 @@ func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() }
|
||||
|
||||
// ContainsExitRoutes reports whether rr contains both the IPv4 and
|
||||
// IPv6 /0 route.
|
||||
func ContainsExitRoutes(rr []netip.Prefix) bool {
|
||||
func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
|
||||
var v4, v6 bool
|
||||
for _, r := range rr {
|
||||
for i := range rr.LenIter() {
|
||||
r := rr.At(i)
|
||||
if r == allIPv4 {
|
||||
v4 = true
|
||||
} else if r == allIPv6 {
|
||||
@@ -237,6 +239,17 @@ func ContainsExitRoutes(rr []netip.Prefix) bool {
|
||||
return v4 && v6
|
||||
}
|
||||
|
||||
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
|
||||
// Routes other than ExitNode Routes.
|
||||
func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool {
|
||||
for i := range rr.LenIter() {
|
||||
if rr.At(i).Bits() != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
allIPv4 = netip.MustParsePrefix("0.0.0.0/0")
|
||||
allIPv6 = netip.MustParsePrefix("::/0")
|
||||
@@ -258,10 +271,10 @@ func SortPrefixes(p []netip.Prefix) {
|
||||
|
||||
// FilterPrefixes returns a new slice, not aliasing in, containing elements of
|
||||
// in that match f.
|
||||
func FilterPrefixesCopy(in []netip.Prefix, f func(netip.Prefix) bool) []netip.Prefix {
|
||||
func FilterPrefixesCopy(in views.Slice[netip.Prefix], f func(netip.Prefix) bool) []netip.Prefix {
|
||||
var out []netip.Prefix
|
||||
for _, v := range in {
|
||||
if f(v) {
|
||||
for i := range in.LenIter() {
|
||||
if v := in.At(i); f(v) {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,17 +48,18 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name == "" {
|
||||
if p.Name() == "" {
|
||||
continue
|
||||
}
|
||||
for _, a := range p.Addresses {
|
||||
for i := range p.Addresses().LenIter() {
|
||||
a := p.Addresses().At(i)
|
||||
ip := a.Addr()
|
||||
if ip.Is4() && !have4 {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(p.Name)] = ip
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(p.Name, suffix))] = ip
|
||||
ret[canonMapKey(p.Name())] = ip
|
||||
if dnsname.HasSuffix(p.Name(), suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
nv[i] = n.View()
|
||||
}
|
||||
return nv
|
||||
}
|
||||
|
||||
func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
ip := netip.MustParseAddr
|
||||
@@ -42,20 +50,20 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
},
|
||||
{
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
want: dnsMap{
|
||||
@@ -74,7 +82,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
@@ -88,7 +96,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: dnsMap{
|
||||
"foo": ip("100::123"),
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/exp/slices"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/net/connstats"
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,7 +32,6 @@ import (
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/u-root/u-root/pkg/termios"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -99,7 +99,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
|
||||
gids := strings.Join(ss.conn.userGroupIDs, ",")
|
||||
remoteUser := ci.uprof.LoginName
|
||||
if ci.node.IsTagged() {
|
||||
remoteUser = strings.Join(ci.node.Tags, ",")
|
||||
remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",")
|
||||
}
|
||||
|
||||
incubatorArgs := []string{
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ type ipnLocalBackend interface {
|
||||
GetSSH_HostKeys() ([]gossh.Signer, error)
|
||||
ShouldRunSSH() bool
|
||||
NetMap() *netmap.NetworkMap
|
||||
WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool)
|
||||
WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
|
||||
DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
Dialer() *tsdial.Dialer
|
||||
TailscaleVarRoot() string
|
||||
@@ -791,7 +791,7 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string {
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"$SRC_NODE_IP", url.QueryEscape(ci.src.Addr().String()),
|
||||
"$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID)),
|
||||
"$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID())),
|
||||
"$DST_NODE_IP", url.QueryEscape(ci.dst.Addr().String()),
|
||||
"$DST_NODE_ID", dstNodeID,
|
||||
"$SSH_USER", url.QueryEscape(ci.sshUser),
|
||||
@@ -1220,7 +1220,7 @@ type sshConnInfo struct {
|
||||
dst netip.AddrPort
|
||||
|
||||
// node is srcIP's node.
|
||||
node *tailcfg.Node
|
||||
node tailcfg.NodeView
|
||||
|
||||
// uprof is node's UserProfile.
|
||||
uprof tailcfg.UserProfile
|
||||
@@ -1334,7 +1334,7 @@ func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
|
||||
if p.Any {
|
||||
return true
|
||||
}
|
||||
if !p.Node.IsZero() && ci.node != nil && p.Node == ci.node.StableID {
|
||||
if !p.Node.IsZero() && ci.node.Valid() && p.Node == ci.node.StableID() {
|
||||
return true
|
||||
}
|
||||
if p.NodeIP != "" {
|
||||
@@ -1702,15 +1702,15 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
|
||||
},
|
||||
SSHUser: ss.conn.info.sshUser,
|
||||
LocalUser: ss.conn.localUser.Username,
|
||||
SrcNode: strings.TrimSuffix(ss.conn.info.node.Name, "."),
|
||||
SrcNodeID: ss.conn.info.node.StableID,
|
||||
SrcNode: strings.TrimSuffix(ss.conn.info.node.Name(), "."),
|
||||
SrcNodeID: ss.conn.info.node.StableID(),
|
||||
ConnectionID: ss.conn.connID,
|
||||
}
|
||||
if !ss.conn.info.node.IsTagged() {
|
||||
ch.SrcNodeUser = ss.conn.info.uprof.LoginName
|
||||
ch.SrcNodeUserID = ss.conn.info.node.User
|
||||
ch.SrcNodeUserID = ss.conn.info.node.User()
|
||||
} else {
|
||||
ch.SrcNodeTags = ss.conn.info.node.Tags
|
||||
ch.SrcNodeTags = ss.conn.info.node.Tags().AsSlice()
|
||||
}
|
||||
j, err := json.Marshal(ch)
|
||||
if err != nil {
|
||||
@@ -1738,7 +1738,7 @@ func (ss *sshSession) notifyControl(ctx context.Context, nodeKey key.NodePublic,
|
||||
ConnectionID: ss.conn.connID,
|
||||
CapVersion: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: nodeKey,
|
||||
SrcNode: ss.conn.info.node.ID,
|
||||
SrcNode: ss.conn.info.node.ID(),
|
||||
SSHUser: ss.conn.info.sshUser,
|
||||
LocalUser: ss.conn.localUser.Username,
|
||||
RecordingAttempts: attempts,
|
||||
|
||||
@@ -177,7 +177,7 @@ func TestMatchRule(t *testing.T) {
|
||||
Principals: []*tailcfg.SSHPrincipal{{Node: "some-node-ID"}},
|
||||
SSHUsers: map[string]string{"*": "ubuntu"},
|
||||
},
|
||||
ci: &sshConnInfo{node: &tailcfg.Node{StableID: "some-node-ID"}},
|
||||
ci: &sshConnInfo{node: (&tailcfg.Node{StableID: "some-node-ID"}).View()},
|
||||
wantUser: "ubuntu",
|
||||
},
|
||||
{
|
||||
@@ -283,11 +283,11 @@ func (ts *localState) NetMap() *netmap.NetworkMap {
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *localState) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
return &tailcfg.Node{
|
||||
func (ts *localState) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
|
||||
return (&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "peer-id",
|
||||
}, tailcfg.UserProfile{
|
||||
}).View(), tailcfg.UserProfile{
|
||||
LoginName: "peer",
|
||||
}, true
|
||||
|
||||
@@ -861,7 +861,7 @@ func TestSSH(t *testing.T) {
|
||||
sshUser: "test",
|
||||
src: netip.MustParseAddrPort("1.2.3.4:32342"),
|
||||
dst: netip.MustParseAddrPort("1.2.3.5:22"),
|
||||
node: &tailcfg.Node{},
|
||||
node: (&tailcfg.Node{}).View(),
|
||||
uprof: tailcfg.UserProfile{},
|
||||
}
|
||||
sc.action0 = &tailcfg.SSHAction{Accept: true}
|
||||
|
||||
@@ -227,6 +227,13 @@ func (m *Map[K, V]) Len() int {
|
||||
return len(m.m)
|
||||
}
|
||||
|
||||
// Clear removes all entries from the map.
|
||||
func (m *Map[K, V]) Clear() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
clear(m.m)
|
||||
}
|
||||
|
||||
// WaitGroup is identical to [sync.WaitGroup],
|
||||
// but provides a Go method to start a goroutine.
|
||||
type WaitGroup struct{ sync.WaitGroup }
|
||||
|
||||
@@ -137,4 +137,22 @@ func TestMap(t *testing.T) {
|
||||
t.Errorf("exactly one LoadOrStore should load")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Clear", func(t *testing.T) {
|
||||
var m Map[string, string]
|
||||
_, _ = m.LoadOrStore("a", "1")
|
||||
_, _ = m.LoadOrStore("b", "2")
|
||||
_, _ = m.LoadOrStore("c", "3")
|
||||
_, _ = m.LoadOrStore("d", "4")
|
||||
_, _ = m.LoadOrStore("e", "5")
|
||||
|
||||
if m.Len() != 5 {
|
||||
t.Errorf("Len after loading want=5 got=%d", m.Len())
|
||||
}
|
||||
|
||||
m.Clear()
|
||||
if m.Len() != 0 {
|
||||
t.Errorf("Len after Clear want=0 got=%d", m.Len())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile --clonefunc
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile --clonefunc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -106,7 +105,10 @@ type CapabilityVersion int
|
||||
// - 66: 2023-07-23: UserProfile.Groups added (available via WhoIs)
|
||||
// - 67: 2023-07-25: Client understands PeerCapMap
|
||||
// - 68: 2023-08-09: Client has dedicated updateRoutine; MapRequest.Stream true means ignore Hostinfo+Endpoints
|
||||
const CurrentCapabilityVersion CapabilityVersion = 68
|
||||
// - 69: 2023-08-16: removed Debug.LogHeap* + GoroutineDumpURL; added c2n /debug/logheap
|
||||
// - 70: 2023-08-16: removed most Debug fields; added NodeAttrDisable*, NodeAttrDebug* instead
|
||||
// - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
|
||||
const CurrentCapabilityVersion CapabilityVersion = 71
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -152,7 +154,6 @@ type User struct {
|
||||
LoginName string `json:"-"` // not stored, filled from Login // TODO REMOVE
|
||||
DisplayName string // if non-empty overrides Login field
|
||||
ProfilePicURL string // if non-empty overrides Login field
|
||||
Domain string
|
||||
Logins []LoginID
|
||||
Created time.Time
|
||||
}
|
||||
@@ -164,7 +165,6 @@ type Login struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// A UserProfile is display-friendly data for a user.
|
||||
@@ -385,6 +385,12 @@ func (n *Node) IsTagged() bool {
|
||||
return len(n.Tags) > 0
|
||||
}
|
||||
|
||||
// IsTagged reports whether the node has any tags.
|
||||
func (n NodeView) IsTagged() bool { return n.ж.IsTagged() }
|
||||
|
||||
// DisplayName wraps Node.DisplayName.
|
||||
func (n NodeView) DisplayName(forOwner bool) string { return n.ж.DisplayName(forOwner) }
|
||||
|
||||
// InitDisplayNames computes and populates n's display name
|
||||
// fields: n.ComputedName, n.computedHostIfDifferent, and
|
||||
// n.ComputedNameWithHost.
|
||||
@@ -734,9 +740,12 @@ type NetInfo struct {
|
||||
// the control plane.
|
||||
DERPLatency map[string]float64 `json:",omitempty"`
|
||||
|
||||
// FirewallMode is the current firewall utility in use by router (iptables, nftables).
|
||||
// FirewallMode ipt means iptables, nft means nftables. When it's empty user is not using
|
||||
// our netfilter runners to manage firewall rules.
|
||||
// FirewallMode encodes both which firewall mode was selected and why.
|
||||
// It is Linux-specific (at least as of 2023-08-19) and is meant to help
|
||||
// debug iptables-vs-nftables issues. The string is of the form
|
||||
// "{nft,ift}-REASON", like "nft-forced" or "ipt-default". Empty means
|
||||
// either not Linux or a configuration in which the host firewall rules
|
||||
// are not managed by tailscaled.
|
||||
FirewallMode string `json:",omitempty"`
|
||||
|
||||
// Update BasicallyEqual when adding fields.
|
||||
@@ -939,6 +948,16 @@ func (st SignatureType) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterResponseAuth is the authentication information returned by the server
|
||||
// in response to a RegisterRequest.
|
||||
type RegisterResponseAuth struct {
|
||||
_ structs.Incomparable
|
||||
// One of Provider/LoginName, Oauth2Token, or AuthKey is set.
|
||||
Provider, LoginName string
|
||||
Oauth2Token *Oauth2Token
|
||||
AuthKey string
|
||||
}
|
||||
|
||||
// RegisterRequest is sent by a client to register the key for a node.
|
||||
// It is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
|
||||
// using the local machine key, and sent to:
|
||||
@@ -957,13 +976,7 @@ type RegisterRequest struct {
|
||||
NodeKey key.NodePublic
|
||||
OldNodeKey key.NodePublic
|
||||
NLKey key.NLPublic
|
||||
Auth struct {
|
||||
_ structs.Incomparable
|
||||
// One of Provider/LoginName, Oauth2Token, or AuthKey is set.
|
||||
Provider, LoginName string
|
||||
Oauth2Token *Oauth2Token
|
||||
AuthKey string
|
||||
}
|
||||
Auth RegisterResponseAuth
|
||||
// Expiry optionally specifies the requested key expiry.
|
||||
// The server policy may override.
|
||||
// As a special case, if Expiry is in the past and NodeKey is
|
||||
@@ -992,29 +1005,6 @@ type RegisterRequest struct {
|
||||
Signature []byte `json:",omitempty"` // as described by SignatureType
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of RegisterRequest.
|
||||
// The result aliases no memory with the original.
|
||||
//
|
||||
// TODO: extend cmd/cloner to generate this method.
|
||||
func (req *RegisterRequest) Clone() *RegisterRequest {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
res := new(RegisterRequest)
|
||||
*res = *req
|
||||
if res.Hostinfo != nil {
|
||||
res.Hostinfo = res.Hostinfo.Clone()
|
||||
}
|
||||
if res.Auth.Oauth2Token != nil {
|
||||
tok := *res.Auth.Oauth2Token
|
||||
res.Auth.Oauth2Token = &tok
|
||||
}
|
||||
res.DeviceCert = append(res.DeviceCert[:0:0], res.DeviceCert...)
|
||||
res.Signature = append(res.Signature[:0:0], res.Signature...)
|
||||
res.NodeKeySignature = append(res.NodeKeySignature[:0:0], res.NodeKeySignature...)
|
||||
return res
|
||||
}
|
||||
|
||||
// RegisterResponse is returned by the server in response to a RegisterRequest.
|
||||
type RegisterResponse struct {
|
||||
User User
|
||||
@@ -1403,8 +1393,11 @@ type DNSConfig struct {
|
||||
//
|
||||
// Matches are case insensitive.
|
||||
ExitNodeFilteredSet []string `json:",omitempty"`
|
||||
// DNSFilterURL contains a user inputed URL that should have a list of domains to be blocked
|
||||
DNSFilterURL string `json:",omitempty"`
|
||||
|
||||
// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype.
|
||||
// It contains a user inputed URL that should have a list of domains to be blocked.
|
||||
// See https://github.com/tailscale/corp/issues/13969.
|
||||
TempCorpIssue13969 string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// DNSRecord is an extra DNS record to add to MagicDNS.
|
||||
@@ -1736,98 +1729,28 @@ type ControlIPCandidate struct {
|
||||
Priority int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Debug are instructions from the control server to the client to adjust debug
|
||||
// settings.
|
||||
//
|
||||
// Deprecated: these should no longer be used. They're a weird mix of declartive
|
||||
// and imperative. The imperative ones should be c2n requests instead, and the
|
||||
// declarative ones (at least the bools) should generally be self
|
||||
// Node.Capabilities.
|
||||
//
|
||||
// TODO(bradfitz): start migrating the imperative ones to c2n requests.
|
||||
// Debug used to be a miscellaneous set of declarative debug config changes and
|
||||
// imperative debug commands. They've since been mostly migrated to node
|
||||
// attributes (MapResponse.Node.Capabilities) for the declarative things and c2n
|
||||
// requests for the imperative things. Not much remains here. Don't add more.
|
||||
type Debug struct {
|
||||
// LogHeapPprof controls whether the client should log
|
||||
// its heap pprof data. Each true value sent from the server
|
||||
// means that client should do one more log.
|
||||
LogHeapPprof bool `json:",omitempty"`
|
||||
|
||||
// LogHeapURL is the URL to POST its heap pprof to.
|
||||
// Empty means to not log.
|
||||
LogHeapURL string `json:",omitempty"`
|
||||
|
||||
// ForceBackgroundSTUN controls whether magicsock should
|
||||
// always do its background STUN queries (see magicsock's
|
||||
// periodicReSTUN), regardless of inactivity.
|
||||
ForceBackgroundSTUN bool `json:",omitempty"`
|
||||
|
||||
// SetForceBackgroundSTUN controls whether magicsock should always do its
|
||||
// background STUN queries (see magicsock's periodicReSTUN), regardless of
|
||||
// inactivity.
|
||||
//
|
||||
// As of capver 37, this field is the preferred field for control to set on
|
||||
// the wire and ForceBackgroundSTUN is only used within the code as the
|
||||
// current map session value. But ForceBackgroundSTUN can still be used too.
|
||||
SetForceBackgroundSTUN opt.Bool `json:",omitempty"`
|
||||
|
||||
// DERPRoute controls whether the DERP reverse path
|
||||
// optimization (see Issue 150) should be enabled or
|
||||
// disabled. The environment variable in magicsock is the
|
||||
// highest priority (if set), then this (if set), then the
|
||||
// binary default value.
|
||||
DERPRoute opt.Bool `json:",omitempty"`
|
||||
|
||||
// TrimWGConfig controls whether Tailscale does lazy, on-demand
|
||||
// wireguard configuration of peers.
|
||||
TrimWGConfig opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableSubnetsIfPAC controls whether subnet routers should be
|
||||
// disabled if WPAD is present on the network.
|
||||
DisableSubnetsIfPAC opt.Bool `json:",omitempty"`
|
||||
|
||||
// GoroutineDumpURL, if non-empty, requests that the client do
|
||||
// a one-time dump of its active goroutines to the given URL.
|
||||
GoroutineDumpURL string `json:",omitempty"`
|
||||
|
||||
// SleepSeconds requests that the client sleep for the
|
||||
// provided number of seconds.
|
||||
// The client can (and should) limit the value (such as 5
|
||||
// minutes).
|
||||
// minutes). This exists as a safety measure to slow down
|
||||
// spinning clients, in case we introduce a bug in the
|
||||
// state machine.
|
||||
SleepSeconds float64 `json:",omitempty"`
|
||||
|
||||
// RandomizeClientPort is whether magicsock should UDP bind to
|
||||
// :0 to get a random local port, ignoring any configured
|
||||
// fixed port.
|
||||
RandomizeClientPort bool `json:",omitempty"`
|
||||
|
||||
// SetRandomizeClientPort is whether magicsock should UDP bind to :0 to get
|
||||
// a random local port, ignoring any configured fixed port.
|
||||
//
|
||||
// As of capver 37, this field is the preferred field for control to set on
|
||||
// the wire and RandomizeClientPort is only used within the code as the
|
||||
// current map session value. But RandomizeClientPort can still be used too.
|
||||
SetRandomizeClientPort opt.Bool `json:",omitempty"`
|
||||
|
||||
// OneCGNATRoute controls whether the client should prefer to make one
|
||||
// big CGNAT /10 route rather than a /32 per peer.
|
||||
OneCGNATRoute opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableUPnP is whether the client will attempt to perform a UPnP portmapping.
|
||||
// By default, we want to enable it to see if it works on more clients.
|
||||
//
|
||||
// If UPnP catastrophically fails for people, this should be set to True to kill
|
||||
// new attempts at UPnP connections.
|
||||
DisableUPnP opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableLogTail disables the logtail package. Once disabled it can't be
|
||||
// re-enabled for the lifetime of the process.
|
||||
//
|
||||
// This is primarily used by Headscale.
|
||||
DisableLogTail bool `json:",omitempty"`
|
||||
|
||||
// EnableSilentDisco disables the use of heartBeatTimer in magicsock and attempts to
|
||||
// handle disco silently. See issue #540 for details.
|
||||
EnableSilentDisco bool `json:",omitempty"`
|
||||
|
||||
// Exit optionally specifies that the client should os.Exit
|
||||
// with this code.
|
||||
// with this code. This is a safety measure in case a client is crash
|
||||
// looping or in an unsafe state and we need to remotely shut it down.
|
||||
Exit *int `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -1862,7 +1785,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
n.UnsignedPeerAPIOnly == n2.UnsignedPeerAPIOnly &&
|
||||
n.Key == n2.Key &&
|
||||
n.KeyExpiry.Equal(n2.KeyExpiry) &&
|
||||
bytes.Equal(n.KeySignature, n2.KeySignature) &&
|
||||
n.KeySignature == n2.KeySignature &&
|
||||
n.Machine == n2.Machine &&
|
||||
n.DiscoKey == n2.DiscoKey &&
|
||||
eqPtr(n.Online, n2.Online) &&
|
||||
@@ -2012,6 +1935,44 @@ const (
|
||||
NodeAttrFunnel = "funnel"
|
||||
// NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions.
|
||||
NodeAttrSSHAggregator = "ssh-aggregator"
|
||||
|
||||
// NodeAttrDebugForceBackgroundSTUN forces a node to always do background
|
||||
// STUN queries regardless of inactivity.
|
||||
NodeAttrDebugForceBackgroundSTUN = "debug-always-stun"
|
||||
|
||||
// NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration,
|
||||
// always giving WireGuard the full netmap, even for idle peers.
|
||||
NodeAttrDebugDisableWGTrim = "debug-no-wg-trim"
|
||||
|
||||
// NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization.
|
||||
// See Issue 150.
|
||||
NodeAttrDebugDisableDRPO = "debug-disable-drpo"
|
||||
|
||||
// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
|
||||
// disabled if WPAD is present on the network.
|
||||
NodeAttrDisableSubnetsIfPAC = "debug-disable-subnets-if-pac"
|
||||
|
||||
// NodeAttrDisableUPnP makes the client not perform a UPnP portmapping.
|
||||
// By default, we want to enable it to see if it works on more clients.
|
||||
//
|
||||
// If UPnP catastrophically fails for people, this should be set kill
|
||||
// new attempts at UPnP connections.
|
||||
NodeAttrDisableUPnP = "debug-disable-upnp"
|
||||
|
||||
// NodeAttrRandomizeClientPort makes magicsock UDP bind to
|
||||
// :0 to get a random local port, ignoring any configured
|
||||
// fixed port.
|
||||
NodeAttrRandomizeClientPort = "randomize-client-port"
|
||||
|
||||
// NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route
|
||||
// rather than a /32 per peer. At most one of this or
|
||||
// NodeAttrOneCGNATDisable may be set; if neither are, it's automatic.
|
||||
NodeAttrOneCGNATEnable = "one-cgnat?v=true"
|
||||
|
||||
// NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer
|
||||
// rather than one big /10 CGNAT route. At most one of this or
|
||||
// NodeAttrOneCGNATEnable may be set; if neither are, it's automatic.
|
||||
NodeAttrOneCGNATDisable = "one-cgnat?v=false"
|
||||
)
|
||||
|
||||
// SetDNSRequest is a request to add a DNS record.
|
||||
|
||||
@@ -34,7 +34,6 @@ var _UserCloneNeedsRegeneration = User(struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
Logins []LoginID
|
||||
Created time.Time
|
||||
}{})
|
||||
@@ -47,7 +46,6 @@ func (src *Node) Clone() *Node {
|
||||
}
|
||||
dst := new(Node)
|
||||
*dst = *src
|
||||
dst.KeySignature = append(src.KeySignature[:0:0], src.KeySignature...)
|
||||
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
|
||||
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
|
||||
dst.Endpoints = append(src.Endpoints[:0:0], src.Endpoints...)
|
||||
@@ -217,7 +215,6 @@ var _LoginCloneNeedsRegeneration = Login(struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of DNSConfig.
|
||||
@@ -261,7 +258,7 @@ var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
|
||||
CertDomains []string
|
||||
ExtraRecords []DNSRecord
|
||||
ExitNodeFilteredSet []string
|
||||
DNSFilterURL string
|
||||
TempCorpIssue13969 string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of RegisterResponse.
|
||||
@@ -273,7 +270,6 @@ func (src *RegisterResponse) Clone() *RegisterResponse {
|
||||
dst := new(RegisterResponse)
|
||||
*dst = *src
|
||||
dst.User = *src.User.Clone()
|
||||
dst.NodeKeySignature = append(src.NodeKeySignature[:0:0], src.NodeKeySignature...)
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -288,6 +284,68 @@ var _RegisterResponseCloneNeedsRegeneration = RegisterResponse(struct {
|
||||
Error string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of RegisterResponseAuth.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *RegisterResponseAuth) Clone() *RegisterResponseAuth {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(RegisterResponseAuth)
|
||||
*dst = *src
|
||||
if dst.Oauth2Token != nil {
|
||||
dst.Oauth2Token = new(Oauth2Token)
|
||||
*dst.Oauth2Token = *src.Oauth2Token
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterResponseAuthCloneNeedsRegeneration = RegisterResponseAuth(struct {
|
||||
_ structs.Incomparable
|
||||
Provider string
|
||||
LoginName string
|
||||
Oauth2Token *Oauth2Token
|
||||
AuthKey string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of RegisterRequest.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *RegisterRequest) Clone() *RegisterRequest {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(RegisterRequest)
|
||||
*dst = *src
|
||||
dst.Auth = *src.Auth.Clone()
|
||||
dst.Hostinfo = src.Hostinfo.Clone()
|
||||
if dst.Timestamp != nil {
|
||||
dst.Timestamp = new(time.Time)
|
||||
*dst.Timestamp = *src.Timestamp
|
||||
}
|
||||
dst.DeviceCert = append(src.DeviceCert[:0:0], src.DeviceCert...)
|
||||
dst.Signature = append(src.Signature[:0:0], src.Signature...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterRequestCloneNeedsRegeneration = RegisterRequest(struct {
|
||||
_ structs.Incomparable
|
||||
Version CapabilityVersion
|
||||
NodeKey key.NodePublic
|
||||
OldNodeKey key.NodePublic
|
||||
NLKey key.NLPublic
|
||||
Auth RegisterResponseAuth
|
||||
Expiry time.Time
|
||||
Followup string
|
||||
Hostinfo *Hostinfo
|
||||
Ephemeral bool
|
||||
NodeKeySignature tkatype.MarshaledSignature
|
||||
SignatureType SignatureType
|
||||
Timestamp *time.Time
|
||||
DeviceCert []byte
|
||||
Signature []byte
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of DERPHomeParams.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *DERPHomeParams) Clone() *DERPHomeParams {
|
||||
@@ -532,7 +590,7 @@ var _UserProfileCloneNeedsRegeneration = UserProfile(struct {
|
||||
|
||||
// Clone duplicates src into dst and reports whether it succeeded.
|
||||
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
|
||||
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile.
|
||||
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile.
|
||||
func Clone(dst, src any) bool {
|
||||
switch src := src.(type) {
|
||||
case *User:
|
||||
@@ -598,6 +656,24 @@ func Clone(dst, src any) bool {
|
||||
*dst = src.Clone()
|
||||
return true
|
||||
}
|
||||
case *RegisterResponseAuth:
|
||||
switch dst := dst.(type) {
|
||||
case *RegisterResponseAuth:
|
||||
*dst = *src.Clone()
|
||||
return true
|
||||
case **RegisterResponseAuth:
|
||||
*dst = src.Clone()
|
||||
return true
|
||||
}
|
||||
case *RegisterRequest:
|
||||
switch dst := dst.(type) {
|
||||
case *RegisterRequest:
|
||||
*dst = *src.Clone()
|
||||
return true
|
||||
case **RegisterRequest:
|
||||
*dst = src.Clone()
|
||||
return true
|
||||
}
|
||||
case *DERPHomeParams:
|
||||
switch dst := dst.(type) {
|
||||
case *DERPHomeParams:
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile
|
||||
|
||||
// View returns a readonly view of User.
|
||||
func (p *User) View() UserView {
|
||||
@@ -71,7 +71,6 @@ func (v UserView) ID() UserID { return v.ж.ID }
|
||||
func (v UserView) LoginName() string { return v.ж.LoginName }
|
||||
func (v UserView) DisplayName() string { return v.ж.DisplayName }
|
||||
func (v UserView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||
func (v UserView) Domain() string { return v.ж.Domain }
|
||||
func (v UserView) Logins() views.Slice[LoginID] { return views.SliceOf(v.ж.Logins) }
|
||||
func (v UserView) Created() time.Time { return v.ж.Created }
|
||||
|
||||
@@ -81,7 +80,6 @@ var _UserViewNeedsRegeneration = User(struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
Logins []LoginID
|
||||
Created time.Time
|
||||
}{})
|
||||
@@ -131,27 +129,25 @@ func (v *NodeView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v NodeView) ID() NodeID { return v.ж.ID }
|
||||
func (v NodeView) StableID() StableNodeID { return v.ж.StableID }
|
||||
func (v NodeView) Name() string { return v.ж.Name }
|
||||
func (v NodeView) User() UserID { return v.ж.User }
|
||||
func (v NodeView) Sharer() UserID { return v.ж.Sharer }
|
||||
func (v NodeView) Key() key.NodePublic { return v.ж.Key }
|
||||
func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry }
|
||||
func (v NodeView) KeySignature() mem.RO { return mem.B(v.ж.KeySignature) }
|
||||
func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine }
|
||||
func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey }
|
||||
func (v NodeView) Addresses() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.Addresses) }
|
||||
func (v NodeView) AllowedIPs() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.AllowedIPs) }
|
||||
func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) }
|
||||
func (v NodeView) DERP() string { return v.ж.DERP }
|
||||
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
|
||||
func (v NodeView) Created() time.Time { return v.ж.Created }
|
||||
func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap }
|
||||
func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v NodeView) PrimaryRoutes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.PrimaryRoutes)
|
||||
}
|
||||
func (v NodeView) ID() NodeID { return v.ж.ID }
|
||||
func (v NodeView) StableID() StableNodeID { return v.ж.StableID }
|
||||
func (v NodeView) Name() string { return v.ж.Name }
|
||||
func (v NodeView) User() UserID { return v.ж.User }
|
||||
func (v NodeView) Sharer() UserID { return v.ж.Sharer }
|
||||
func (v NodeView) Key() key.NodePublic { return v.ж.Key }
|
||||
func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry }
|
||||
func (v NodeView) KeySignature() tkatype.MarshaledSignature { return v.ж.KeySignature }
|
||||
func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine }
|
||||
func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey }
|
||||
func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) }
|
||||
func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) }
|
||||
func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) }
|
||||
func (v NodeView) DERP() string { return v.ж.DERP }
|
||||
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
|
||||
func (v NodeView) Created() time.Time { return v.ж.Created }
|
||||
func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap }
|
||||
func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v NodeView) PrimaryRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.PrimaryRoutes) }
|
||||
func (v NodeView) LastSeen() *time.Time {
|
||||
if v.ж.LastSeen == nil {
|
||||
return nil
|
||||
@@ -266,41 +262,39 @@ func (v *HostinfoView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion }
|
||||
func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
|
||||
func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID }
|
||||
func (v HostinfoView) OS() string { return v.ж.OS }
|
||||
func (v HostinfoView) OSVersion() string { return v.ж.OSVersion }
|
||||
func (v HostinfoView) Container() opt.Bool { return v.ж.Container }
|
||||
func (v HostinfoView) Env() string { return v.ж.Env }
|
||||
func (v HostinfoView) Distro() string { return v.ж.Distro }
|
||||
func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion }
|
||||
func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName }
|
||||
func (v HostinfoView) App() string { return v.ж.App }
|
||||
func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop }
|
||||
func (v HostinfoView) Package() string { return v.ж.Package }
|
||||
func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel }
|
||||
func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken }
|
||||
func (v HostinfoView) Hostname() string { return v.ж.Hostname }
|
||||
func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp }
|
||||
func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode }
|
||||
func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport }
|
||||
func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress }
|
||||
func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate }
|
||||
func (v HostinfoView) Machine() string { return v.ж.Machine }
|
||||
func (v HostinfoView) GoArch() string { return v.ж.GoArch }
|
||||
func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar }
|
||||
func (v HostinfoView) GoVersion() string { return v.ж.GoVersion }
|
||||
func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.RoutableIPs)
|
||||
}
|
||||
func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) }
|
||||
func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) }
|
||||
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
|
||||
func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
|
||||
func (v HostinfoView) Cloud() string { return v.ж.Cloud }
|
||||
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
|
||||
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
|
||||
func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion }
|
||||
func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
|
||||
func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID }
|
||||
func (v HostinfoView) OS() string { return v.ж.OS }
|
||||
func (v HostinfoView) OSVersion() string { return v.ж.OSVersion }
|
||||
func (v HostinfoView) Container() opt.Bool { return v.ж.Container }
|
||||
func (v HostinfoView) Env() string { return v.ж.Env }
|
||||
func (v HostinfoView) Distro() string { return v.ж.Distro }
|
||||
func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion }
|
||||
func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName }
|
||||
func (v HostinfoView) App() string { return v.ж.App }
|
||||
func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop }
|
||||
func (v HostinfoView) Package() string { return v.ж.Package }
|
||||
func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel }
|
||||
func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken }
|
||||
func (v HostinfoView) Hostname() string { return v.ж.Hostname }
|
||||
func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp }
|
||||
func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode }
|
||||
func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport }
|
||||
func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress }
|
||||
func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate }
|
||||
func (v HostinfoView) Machine() string { return v.ж.Machine }
|
||||
func (v HostinfoView) GoArch() string { return v.ж.GoArch }
|
||||
func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar }
|
||||
func (v HostinfoView) GoVersion() string { return v.ж.GoVersion }
|
||||
func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) }
|
||||
func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) }
|
||||
func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) }
|
||||
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
|
||||
func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
|
||||
func (v HostinfoView) Cloud() string { return v.ж.Cloud }
|
||||
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
|
||||
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
|
||||
func (v HostinfoView) Location() *Location {
|
||||
if v.ж.Location == nil {
|
||||
return nil
|
||||
@@ -479,7 +473,6 @@ func (v LoginView) Provider() string { return v.ж.Provider }
|
||||
func (v LoginView) LoginName() string { return v.ж.LoginName }
|
||||
func (v LoginView) DisplayName() string { return v.ж.DisplayName }
|
||||
func (v LoginView) ProfilePicURL() string { return v.ж.ProfilePicURL }
|
||||
func (v LoginView) Domain() string { return v.ж.Domain }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LoginViewNeedsRegeneration = Login(struct {
|
||||
@@ -489,7 +482,6 @@ var _LoginViewNeedsRegeneration = Login(struct {
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Domain string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DNSConfig.
|
||||
@@ -557,7 +549,7 @@ func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.Slic
|
||||
func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] {
|
||||
return views.SliceOf(v.ж.ExitNodeFilteredSet)
|
||||
}
|
||||
func (v DNSConfigView) DNSFilterURL() string { return v.ж.DNSFilterURL }
|
||||
func (v DNSConfigView) TempCorpIssue13969() string { return v.ж.TempCorpIssue13969 }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
|
||||
@@ -570,7 +562,7 @@ var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
|
||||
CertDomains []string
|
||||
ExtraRecords []DNSRecord
|
||||
ExitNodeFilteredSet []string
|
||||
DNSFilterURL string
|
||||
TempCorpIssue13969 string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of RegisterResponse.
|
||||
@@ -618,13 +610,15 @@ func (v *RegisterResponseView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v RegisterResponseView) User() UserView { return v.ж.User.View() }
|
||||
func (v RegisterResponseView) Login() Login { return v.ж.Login }
|
||||
func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired }
|
||||
func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL }
|
||||
func (v RegisterResponseView) NodeKeySignature() mem.RO { return mem.B(v.ж.NodeKeySignature) }
|
||||
func (v RegisterResponseView) Error() string { return v.ж.Error }
|
||||
func (v RegisterResponseView) User() UserView { return v.ж.User.View() }
|
||||
func (v RegisterResponseView) Login() Login { return v.ж.Login }
|
||||
func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired }
|
||||
func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL }
|
||||
func (v RegisterResponseView) NodeKeySignature() tkatype.MarshaledSignature {
|
||||
return v.ж.NodeKeySignature
|
||||
}
|
||||
func (v RegisterResponseView) Error() string { return v.ж.Error }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterResponseViewNeedsRegeneration = RegisterResponse(struct {
|
||||
@@ -637,6 +631,160 @@ var _RegisterResponseViewNeedsRegeneration = RegisterResponse(struct {
|
||||
Error string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of RegisterResponseAuth.
|
||||
func (p *RegisterResponseAuth) View() RegisterResponseAuthView {
|
||||
return RegisterResponseAuthView{ж: p}
|
||||
}
|
||||
|
||||
// RegisterResponseAuthView provides a read-only view over RegisterResponseAuth.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type RegisterResponseAuthView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *RegisterResponseAuth
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v RegisterResponseAuthView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v RegisterResponseAuthView) AsStruct() *RegisterResponseAuth {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v RegisterResponseAuthView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *RegisterResponseAuthView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x RegisterResponseAuth
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v RegisterResponseAuthView) Provider() string { return v.ж.Provider }
|
||||
func (v RegisterResponseAuthView) LoginName() string { return v.ж.LoginName }
|
||||
func (v RegisterResponseAuthView) Oauth2Token() *Oauth2Token {
|
||||
if v.ж.Oauth2Token == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Oauth2Token
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v RegisterResponseAuthView) AuthKey() string { return v.ж.AuthKey }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterResponseAuthViewNeedsRegeneration = RegisterResponseAuth(struct {
|
||||
_ structs.Incomparable
|
||||
Provider string
|
||||
LoginName string
|
||||
Oauth2Token *Oauth2Token
|
||||
AuthKey string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of RegisterRequest.
|
||||
func (p *RegisterRequest) View() RegisterRequestView {
|
||||
return RegisterRequestView{ж: p}
|
||||
}
|
||||
|
||||
// RegisterRequestView provides a read-only view over RegisterRequest.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type RegisterRequestView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *RegisterRequest
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v RegisterRequestView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v RegisterRequestView) AsStruct() *RegisterRequest {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v RegisterRequestView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *RegisterRequestView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x RegisterRequest
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v RegisterRequestView) Version() CapabilityVersion { return v.ж.Version }
|
||||
func (v RegisterRequestView) NodeKey() key.NodePublic { return v.ж.NodeKey }
|
||||
func (v RegisterRequestView) OldNodeKey() key.NodePublic { return v.ж.OldNodeKey }
|
||||
func (v RegisterRequestView) NLKey() key.NLPublic { return v.ж.NLKey }
|
||||
func (v RegisterRequestView) Auth() RegisterResponseAuthView { return v.ж.Auth.View() }
|
||||
func (v RegisterRequestView) Expiry() time.Time { return v.ж.Expiry }
|
||||
func (v RegisterRequestView) Followup() string { return v.ж.Followup }
|
||||
func (v RegisterRequestView) Hostinfo() HostinfoView { return v.ж.Hostinfo.View() }
|
||||
func (v RegisterRequestView) Ephemeral() bool { return v.ж.Ephemeral }
|
||||
func (v RegisterRequestView) NodeKeySignature() tkatype.MarshaledSignature {
|
||||
return v.ж.NodeKeySignature
|
||||
}
|
||||
func (v RegisterRequestView) SignatureType() SignatureType { return v.ж.SignatureType }
|
||||
func (v RegisterRequestView) Timestamp() *time.Time {
|
||||
if v.ж.Timestamp == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Timestamp
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v RegisterRequestView) DeviceCert() mem.RO { return mem.B(v.ж.DeviceCert) }
|
||||
func (v RegisterRequestView) Signature() mem.RO { return mem.B(v.ж.Signature) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
|
||||
_ structs.Incomparable
|
||||
Version CapabilityVersion
|
||||
NodeKey key.NodePublic
|
||||
OldNodeKey key.NodePublic
|
||||
NLKey key.NLPublic
|
||||
Auth RegisterResponseAuth
|
||||
Expiry time.Time
|
||||
Followup string
|
||||
Hostinfo *Hostinfo
|
||||
Ephemeral bool
|
||||
NodeKeySignature tkatype.MarshaledSignature
|
||||
SignatureType SignatureType
|
||||
Timestamp *time.Time
|
||||
DeviceCert []byte
|
||||
Signature []byte
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of DERPHomeParams.
|
||||
func (p *DERPHomeParams) View() DERPHomeParamsView {
|
||||
return DERPHomeParamsView{ж: p}
|
||||
|
||||
@@ -4,6 +4,7 @@ package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -166,7 +166,7 @@ func (s NodeKeySignature) authorizingKeyID() (tkatype.KeyID, error) {
|
||||
func (s NodeKeySignature) SigHash() [blake2s.Size]byte {
|
||||
dupe := s
|
||||
dupe.Signature = nil
|
||||
return blake2s.Sum256(dupe.Serialize())
|
||||
return blake2s.Sum256([]byte(dupe.Serialize()))
|
||||
}
|
||||
|
||||
// Serialize returns the given NKS in a serialized format.
|
||||
@@ -186,7 +186,7 @@ func (s *NodeKeySignature) Serialize() tkatype.MarshaledSignature {
|
||||
// Writing to a bytes.Buffer should never fail.
|
||||
panic(err)
|
||||
}
|
||||
return out.Bytes()
|
||||
return tkatype.MarshaledSignature(out.Bytes())
|
||||
}
|
||||
|
||||
// Unserialize decodes bytes representing a marshaled NKS.
|
||||
@@ -194,9 +194,9 @@ func (s *NodeKeySignature) Serialize() tkatype.MarshaledSignature {
|
||||
// We would implement encoding.BinaryUnmarshaler, except that would
|
||||
// unfortunately get called by the cbor unmarshaller resulting in infinite
|
||||
// recursion.
|
||||
func (s *NodeKeySignature) Unserialize(data []byte) error {
|
||||
func (s *NodeKeySignature) Unserialize(data tkatype.MarshaledSignature) error {
|
||||
dec, _ := cborDecOpts.DecMode()
|
||||
return dec.Unmarshal(data, s)
|
||||
return dec.Unmarshal([]byte(data), s)
|
||||
}
|
||||
|
||||
// verifySignature checks that the NodeKeySignature is authentic & certified
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", "localhost:8060", "address of Tailscale web client")
|
||||
devMode = flag.Bool("dev", false, "run web client in dev mode")
|
||||
)
|
||||
|
||||
@@ -23,12 +24,6 @@ func main() {
|
||||
s := new(tsnet.Server)
|
||||
defer s.Close()
|
||||
|
||||
ln, err := s.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -37,7 +32,8 @@ func main() {
|
||||
// Serve the Tailscale web client.
|
||||
ws, cleanup := web.NewServer(*devMode, lc)
|
||||
defer cleanup()
|
||||
if err := http.Serve(ln, ws); err != nil {
|
||||
log.Printf("Serving Tailscale web client on http://%s", *addr)
|
||||
if err := http.ListenAndServe(*addr, ws); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
package tstest
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -26,7 +27,6 @@ import (
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/smallzstd"
|
||||
@@ -400,6 +400,8 @@ func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
|
||||
return nodes
|
||||
}
|
||||
|
||||
const domain = "fake-control.example.net"
|
||||
|
||||
func (s *Server) getUser(nodeKey key.NodePublic) (*tailcfg.User, *tailcfg.Login) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -413,7 +415,6 @@ func (s *Server) getUser(nodeKey key.NodePublic) (*tailcfg.User, *tailcfg.Login)
|
||||
return u, s.logins[nodeKey]
|
||||
}
|
||||
id := tailcfg.UserID(len(s.users) + 1)
|
||||
domain := "fake-control.example.net"
|
||||
loginName := fmt.Sprintf("user-%d@%s", id, domain)
|
||||
displayName := fmt.Sprintf("User %d", id)
|
||||
login := &tailcfg.Login{
|
||||
@@ -422,13 +423,11 @@ func (s *Server) getUser(nodeKey key.NodePublic) (*tailcfg.User, *tailcfg.Login)
|
||||
LoginName: loginName,
|
||||
DisplayName: displayName,
|
||||
ProfilePicURL: "https://tailscale.com/static/images/marketing/team-carney.jpg",
|
||||
Domain: domain,
|
||||
}
|
||||
user := &tailcfg.User{
|
||||
ID: id,
|
||||
LoginName: loginName,
|
||||
DisplayName: displayName,
|
||||
Domain: domain,
|
||||
Logins: []tailcfg.LoginID{login.ID},
|
||||
}
|
||||
s.users[nodeKey] = user
|
||||
@@ -814,6 +813,8 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
// node key rotated away (once test server supports that)
|
||||
return nil, nil
|
||||
}
|
||||
node.Capabilities = append(node.Capabilities, tailcfg.NodeAttrDisableUPnP)
|
||||
|
||||
user, _ := s.getUser(nk)
|
||||
t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC)
|
||||
dns := s.DNSConfig
|
||||
@@ -827,14 +828,11 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
res = &tailcfg.MapResponse{
|
||||
Node: node,
|
||||
DERPMap: s.DERPMap,
|
||||
Domain: string(user.Domain),
|
||||
Domain: domain,
|
||||
CollectServices: "true",
|
||||
PacketFilter: packetFilterWithIngressCaps(),
|
||||
Debug: &tailcfg.Debug{
|
||||
DisableUPnP: "true",
|
||||
},
|
||||
DNSConfig: dns,
|
||||
ControlTime: &t,
|
||||
DNSConfig: dns,
|
||||
ControlTime: &t,
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
|
||||
@@ -42,7 +42,6 @@ import (
|
||||
_ "tailscale.com/ipn"
|
||||
_ "tailscale.com/ipn/ipnlocal"
|
||||
_ "tailscale.com/ipn/localapi"
|
||||
_ "tailscale.com/log/logheap"
|
||||
_ "tailscale.com/logtail"
|
||||
_ "tailscale.com/logtail/filch"
|
||||
_ "tailscale.com/net/dns"
|
||||
|
||||
@@ -11,9 +11,8 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"slices"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// PrivateID represents a log steam for writing.
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -35,7 +34,7 @@ type NetworkMap struct {
|
||||
Addresses []netip.Prefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly)
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey key.MachinePublic
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
Peers []tailcfg.NodeView // sorted by Node.ID
|
||||
DNS tailcfg.DNSConfig
|
||||
// TODO(maisem) : replace with View.
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
@@ -53,9 +52,6 @@ type NetworkMap struct {
|
||||
// between updates and should not be modified.
|
||||
DERPMap *tailcfg.DERPMap
|
||||
|
||||
// Debug knobs from control server for debug or feature gating.
|
||||
Debug *tailcfg.Debug
|
||||
|
||||
// ControlHealth are the list of health check problems for this
|
||||
// node from the perspective of the control plane.
|
||||
// If empty, there are no known problems from the control plane's
|
||||
@@ -88,7 +84,7 @@ type NetworkMap struct {
|
||||
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
|
||||
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
|
||||
for _, p := range nm.Peers {
|
||||
if len(p.PrimaryRoutes) > 0 {
|
||||
if p.PrimaryRoutes().Len() > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -98,19 +94,21 @@ func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
|
||||
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.
|
||||
//
|
||||
// If nm is nil or no peer is found, ok is false.
|
||||
func (nm *NetworkMap) PeerByTailscaleIP(ip netip.Addr) (peer *tailcfg.Node, ok bool) {
|
||||
func (nm *NetworkMap) PeerByTailscaleIP(ip netip.Addr) (peer tailcfg.NodeView, ok bool) {
|
||||
// TODO(bradfitz):
|
||||
if nm == nil {
|
||||
return nil, false
|
||||
return tailcfg.NodeView{}, false
|
||||
}
|
||||
for _, n := range nm.Peers {
|
||||
for _, a := range n.Addresses {
|
||||
ad := n.Addresses()
|
||||
for i := 0; i < ad.Len(); i++ {
|
||||
a := ad.At(i)
|
||||
if a.Addr() == ip {
|
||||
return n, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return tailcfg.NodeView{}, false
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
|
||||
@@ -157,13 +155,13 @@ func (nm *NetworkMap) VeryConcise() string {
|
||||
}
|
||||
|
||||
// PeerWithStableID finds and returns the peer associated to the inputted StableNodeID.
|
||||
func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ *tailcfg.Node, ok bool) {
|
||||
func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ tailcfg.NodeView, ok bool) {
|
||||
for _, p := range nm.Peers {
|
||||
if p.StableID == pid {
|
||||
if p.StableID() == pid {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return tailcfg.NodeView{}, false
|
||||
}
|
||||
|
||||
// printConciseHeader prints a concise header line representing nm to buf.
|
||||
@@ -182,10 +180,6 @@ func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, " u=%s", login)
|
||||
if nm.Debug != nil {
|
||||
j, _ := json.Marshal(nm.Debug)
|
||||
fmt.Fprintf(buf, " debug=%s", j)
|
||||
}
|
||||
fmt.Fprintf(buf, " %v", nm.Addresses)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
@@ -204,22 +198,24 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (a.Debug == nil && b.Debug == nil) || reflect.DeepEqual(a.Debug, b.Debug)
|
||||
return true
|
||||
}
|
||||
|
||||
// printPeerConcise appends to buf a line representing the peer p.
|
||||
//
|
||||
// If this function is changed to access different fields of p, keep
|
||||
// in nodeConciseEqual in sync.
|
||||
func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
aip := make([]string, len(p.AllowedIPs))
|
||||
for i, a := range p.AllowedIPs {
|
||||
func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
|
||||
aip := make([]string, p.AllowedIPs().Len())
|
||||
for i := range aip {
|
||||
a := p.AllowedIPs().At(i)
|
||||
s := strings.TrimSuffix(fmt.Sprint(a), "/32")
|
||||
aip[i] = s
|
||||
}
|
||||
|
||||
ep := make([]string, len(p.Endpoints))
|
||||
for i, e := range p.Endpoints {
|
||||
ep := make([]string, p.Endpoints().Len())
|
||||
for i := range ep {
|
||||
e := p.Endpoints().At(i)
|
||||
// Align vertically on the ':' between IP and port
|
||||
colon := strings.IndexByte(e, ':')
|
||||
spaces := 0
|
||||
@@ -230,21 +226,21 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
|
||||
}
|
||||
|
||||
derp := p.DERP
|
||||
derp := p.DERP()
|
||||
const derpPrefix = "127.3.3.40:"
|
||||
if strings.HasPrefix(derp, derpPrefix) {
|
||||
derp = "D" + derp[len(derpPrefix):]
|
||||
}
|
||||
var discoShort string
|
||||
if !p.DiscoKey.IsZero() {
|
||||
discoShort = p.DiscoKey.ShortString() + " "
|
||||
if !p.DiscoKey().IsZero() {
|
||||
discoShort = p.DiscoKey().ShortString() + " "
|
||||
}
|
||||
|
||||
// Most of the time, aip is just one element, so format the
|
||||
// table to look good in that case. This will also make multi-
|
||||
// subnet nodes stand out visually.
|
||||
fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n",
|
||||
p.Key.ShortString(),
|
||||
p.Key().ShortString(),
|
||||
discoShort,
|
||||
derp,
|
||||
strings.Join(aip, " "),
|
||||
@@ -252,12 +248,12 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
}
|
||||
|
||||
// nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise.
|
||||
func nodeConciseEqual(a, b *tailcfg.Node) bool {
|
||||
return a.Key == b.Key &&
|
||||
a.DERP == b.DERP &&
|
||||
a.DiscoKey == b.DiscoKey &&
|
||||
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) &&
|
||||
eqStringsIgnoreNil(a.Endpoints, b.Endpoints)
|
||||
func nodeConciseEqual(a, b tailcfg.NodeView) bool {
|
||||
return a.Key() == b.Key() &&
|
||||
a.DERP() == b.DERP() &&
|
||||
a.DiscoKey() == b.DiscoKey() &&
|
||||
eqViewsIgnoreNil(a.AllowedIPs(), b.AllowedIPs()) &&
|
||||
eqViewsIgnoreNil(a.Endpoints(), b.Endpoints())
|
||||
}
|
||||
|
||||
func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
|
||||
@@ -276,7 +272,7 @@ func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
|
||||
for len(aps) > 0 && len(bps) > 0 {
|
||||
pa, pb := aps[0], bps[0]
|
||||
switch {
|
||||
case pa.ID == pb.ID:
|
||||
case pa.ID() == pb.ID():
|
||||
if !nodeConciseEqual(pa, pb) {
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
@@ -284,12 +280,12 @@ func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
|
||||
printPeerConcise(&diff, pb)
|
||||
}
|
||||
aps, bps = aps[1:], bps[1:]
|
||||
case pa.ID > pb.ID:
|
||||
case pa.ID() > pb.ID():
|
||||
// New peer in b.
|
||||
diff.WriteByte('+')
|
||||
printPeerConcise(&diff, pb)
|
||||
bps = bps[1:]
|
||||
case pb.ID > pa.ID:
|
||||
case pb.ID() > pa.ID():
|
||||
// Deleted peer in b.
|
||||
diff.WriteByte('-')
|
||||
printPeerConcise(&diff, pa)
|
||||
@@ -324,28 +320,18 @@ const (
|
||||
AllowSubnetRoutes
|
||||
)
|
||||
|
||||
// eqStringsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqStringsIgnoreNil(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
// eqViewsIgnoreNil reports whether a and b have the same length and comparably
|
||||
// equal values at each index. It's used for comparing views of slices and not
|
||||
// caring about whether the slices are nil or not.
|
||||
func eqViewsIgnoreNil[T comparable](a, b interface {
|
||||
Len() int
|
||||
At(int) T
|
||||
}) bool {
|
||||
if a.Len() != b.Len() {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// eqCIDRsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqCIDRsIgnoreNil(a, b []netip.Prefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
for i, n := 0, a.Len(); i < n; i++ {
|
||||
if a.At(i) != b.At(i) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,14 @@ func testDiscoKey(hexPrefix string) (ret key.DiscoPublic) {
|
||||
return key.DiscoPublicFromRaw32(mem.B(bs[:]))
|
||||
}
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
nv[i] = n.View()
|
||||
}
|
||||
return nv
|
||||
}
|
||||
|
||||
func TestNetworkMapConcise(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
@@ -44,7 +52,7 @@ func TestNetworkMapConcise(t *testing.T) {
|
||||
name: "basic",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
@@ -55,26 +63,10 @@ func TestNetworkMapConcise(t *testing.T) {
|
||||
DERP: "127.3.3.40:4",
|
||||
Endpoints: []string{"10.2.0.100:12", "10.1.0.100:12345"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
|
||||
},
|
||||
{
|
||||
name: "debug_non_nil",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={} []\n",
|
||||
},
|
||||
{
|
||||
name: "debug_values",
|
||||
nm: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{LogHeapPprof: true},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={\"LogHeapPprof\":true} []\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
@@ -99,23 +91,23 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "no_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -123,23 +115,23 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "header_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(2),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n",
|
||||
},
|
||||
@@ -147,18 +139,18 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "peer_add",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
Key: testNodeKey(1),
|
||||
@@ -177,7 +169,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:3",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "+ [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n+ [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
|
||||
},
|
||||
@@ -185,7 +177,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "peer_remove",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 1,
|
||||
Key: testNodeKey(1),
|
||||
@@ -204,18 +196,18 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:3",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "- [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n- [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
|
||||
},
|
||||
@@ -223,25 +215,25 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "peer_port_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "- [AgICA] D2 : 192.168.0.100:12 1.1.1.1:1 \n+ [AgICA] D2 : 192.168.0.100:12 1.1.1.1:2 \n",
|
||||
},
|
||||
@@ -249,7 +241,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
name: "disco_key_only_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
@@ -258,11 +250,11 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
@@ -271,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: "- [AgICA] d:f00f00f00f000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n+ [AgICA] d:ba4ba4ba4b000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n",
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// because this package encodes wire types that should be lightweight to use.
|
||||
package tkatype
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// KeyID references a verification key stored in the key authority. A keyID
|
||||
// uniquely identifies a key. KeyIDs are all 32 bytes.
|
||||
//
|
||||
@@ -19,7 +21,26 @@ package tkatype
|
||||
type KeyID []byte
|
||||
|
||||
// MarshaledSignature represents a marshaled tka.NodeKeySignature.
|
||||
type MarshaledSignature []byte
|
||||
//
|
||||
// While its underlying type is a string, it's just the raw signature bytes, not
|
||||
// hex or base64, etc.
|
||||
//
|
||||
// Think of it as []byte, which it used to be. It's a string only to make it
|
||||
// easier to use with cmd/viewer.
|
||||
type MarshaledSignature string
|
||||
|
||||
func (a MarshaledSignature) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([]byte(a))
|
||||
}
|
||||
|
||||
func (a *MarshaledSignature) UnmarshalJSON(b []byte) error {
|
||||
var bs []byte
|
||||
if err := json.Unmarshal(b, &bs); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = MarshaledSignature(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshaledAUM represents a marshaled tka.AUM.
|
||||
type MarshaledAUM []byte
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package tkatype
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/blake2s"
|
||||
@@ -20,3 +21,23 @@ func TestSigHashSize(t *testing.T) {
|
||||
t.Errorf("NKSSigHash is wrong size: got %d, want %d", len(nksHash), blake2s.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshaledSignatureJSON(t *testing.T) {
|
||||
sig := MarshaledSignature("abcdef")
|
||||
j, err := json.Marshal(sig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const encoded = `"YWJjZGVm"`
|
||||
if string(j) != encoded {
|
||||
t.Errorf("got JSON %q; want %q", j, encoded)
|
||||
}
|
||||
|
||||
var back MarshaledSignature
|
||||
if err := json.Unmarshal([]byte(encoded), &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(back) != string(sig) {
|
||||
t.Errorf("decoded JSON back to %q; want %q", back, sig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,7 @@ package views
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"maps"
|
||||
)
|
||||
|
||||
func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error {
|
||||
@@ -73,6 +69,11 @@ func (v SliceView[T, V]) IsNil() bool { return v.ж == nil }
|
||||
// Len returns the length of the slice.
|
||||
func (v SliceView[T, V]) Len() int { return len(v.ж) }
|
||||
|
||||
// LenIter returns a slice the same length as the v.Len().
|
||||
// The caller can then range over it to get the valid indexes.
|
||||
// It does not allocate.
|
||||
func (v SliceView[T, V]) LenIter() []struct{} { return make([]struct{}, len(v.ж)) }
|
||||
|
||||
// At returns a View of the element at index `i` of the slice.
|
||||
func (v SliceView[T, V]) At(i int) V { return v.ж[i].View() }
|
||||
|
||||
@@ -129,6 +130,11 @@ func (v Slice[T]) IsNil() bool { return v.ж == nil }
|
||||
// Len returns the length of the slice.
|
||||
func (v Slice[T]) Len() int { return len(v.ж) }
|
||||
|
||||
// LenIter returns a slice the same length as the v.Len().
|
||||
// The caller can then range over it to get the valid indexes.
|
||||
// It does not allocate.
|
||||
func (v Slice[T]) LenIter() []struct{} { return make([]struct{}, len(v.ж)) }
|
||||
|
||||
// At returns the element at index `i` of the slice.
|
||||
func (v Slice[T]) At(i int) T { return v.ж[i] }
|
||||
|
||||
@@ -219,79 +225,6 @@ func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IPPrefixSlice is a read-only accessor for a slice of netip.Prefix.
|
||||
type IPPrefixSlice struct {
|
||||
ж Slice[netip.Prefix]
|
||||
}
|
||||
|
||||
// IPPrefixSliceOf returns a IPPrefixSlice for the provided slice.
|
||||
func IPPrefixSliceOf(x []netip.Prefix) IPPrefixSlice { return IPPrefixSlice{SliceOf(x)} }
|
||||
|
||||
// IsNil reports whether the underlying slice is nil.
|
||||
func (v IPPrefixSlice) IsNil() bool { return v.ж.IsNil() }
|
||||
|
||||
// Len returns the length of the slice.
|
||||
func (v IPPrefixSlice) Len() int { return v.ж.Len() }
|
||||
|
||||
// At returns the IPPrefix at index `i` of the slice.
|
||||
func (v IPPrefixSlice) At(i int) netip.Prefix { return v.ж.At(i) }
|
||||
|
||||
// AppendTo appends the underlying slice values to dst.
|
||||
func (v IPPrefixSlice) AppendTo(dst []netip.Prefix) []netip.Prefix {
|
||||
return v.ж.AppendTo(dst)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying Slice[netip.Prefix].
|
||||
func (v IPPrefixSlice) Unwrap() Slice[netip.Prefix] {
|
||||
return v.ж
|
||||
}
|
||||
|
||||
// AsSlice returns a copy of underlying slice.
|
||||
func (v IPPrefixSlice) AsSlice() []netip.Prefix {
|
||||
return v.ж.AsSlice()
|
||||
}
|
||||
|
||||
// Filter returns a new slice, containing elements of v that match f.
|
||||
func (v IPPrefixSlice) Filter(f func(netip.Prefix) bool) []netip.Prefix {
|
||||
return tsaddr.FilterPrefixesCopy(v.ж.ж, f)
|
||||
}
|
||||
|
||||
// PrefixesContainsIP reports whether any IPPrefix contains IP.
|
||||
func (v IPPrefixSlice) ContainsIP(ip netip.Addr) bool {
|
||||
return tsaddr.PrefixesContainsIP(v.ж.ж, ip)
|
||||
}
|
||||
|
||||
// PrefixesContainsFunc reports whether f is true for any IPPrefix in the slice.
|
||||
func (v IPPrefixSlice) ContainsFunc(f func(netip.Prefix) bool) bool {
|
||||
return slices.ContainsFunc(v.ж.ж, f)
|
||||
}
|
||||
|
||||
// ContainsExitRoutes reports whether v contains ExitNode Routes.
|
||||
func (v IPPrefixSlice) ContainsExitRoutes() bool {
|
||||
return tsaddr.ContainsExitRoutes(v.ж.ж)
|
||||
}
|
||||
|
||||
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
|
||||
// Routes other than ExitNode Routes.
|
||||
func (v IPPrefixSlice) ContainsNonExitSubnetRoutes() bool {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if v.At(i).Bits() != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (v IPPrefixSlice) MarshalJSON() ([]byte, error) {
|
||||
return v.ж.MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (v *IPPrefixSlice) UnmarshalJSON(b []byte) error {
|
||||
return v.ж.UnmarshalJSON(b)
|
||||
}
|
||||
|
||||
// MapOf returns a view over m. It is the caller's responsibility to make sure K
|
||||
// and V is immutable, if this is being used to provide a read-only view over m.
|
||||
func MapOf[K comparable, V comparable](m map[K]V) Map[K, V] {
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
|
||||
type viewStruct struct {
|
||||
Int int
|
||||
Addrs IPPrefixSlice
|
||||
Addrs Slice[netip.Prefix]
|
||||
Strings Slice[string]
|
||||
AddrsPtr *IPPrefixSlice `json:",omitempty"`
|
||||
StringsPtr *Slice[string] `json:",omitempty"`
|
||||
AddrsPtr *Slice[netip.Prefix] `json:",omitempty"`
|
||||
StringsPtr *Slice[string] `json:",omitempty"`
|
||||
}
|
||||
|
||||
func BenchmarkSliceIteration(b *testing.B) {
|
||||
@@ -66,7 +66,7 @@ func TestViewsJSON(t *testing.T) {
|
||||
}
|
||||
return
|
||||
}
|
||||
ipp := IPPrefixSliceOf(mustCIDR("192.168.0.0/24"))
|
||||
ipp := SliceOf(mustCIDR("192.168.0.0/24"))
|
||||
ss := SliceOf([]string{"bar"})
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -138,3 +138,15 @@ func TestViewUtils(t *testing.T) {
|
||||
SliceOf([]string{"b", "c"}).SliceTo(1)),
|
||||
qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestLenIter(t *testing.T) {
|
||||
orig := []string{"foo", "bar"}
|
||||
var got []string
|
||||
v := SliceOf(orig)
|
||||
for i := range v.LenIter() {
|
||||
got = append(got, v.At(i))
|
||||
}
|
||||
if !reflect.DeepEqual(orig, got) {
|
||||
t.Errorf("got %q; want %q", got, orig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,8 +581,8 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
{
|
||||
name: "tailcfg.Node",
|
||||
val: &tailcfg.Node{},
|
||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
out32: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
out: "ANY", // magic value; just check it doesn't fail to hash
|
||||
out32: "ANY",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -600,7 +600,7 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
tt.out = tt.out32
|
||||
}
|
||||
h.sum()
|
||||
if got := string(hb.B); got != tt.out {
|
||||
if got := string(hb.B); got != tt.out && tt.out != "ANY" {
|
||||
t.Fatalf("got %q; want %q", got, tt.out)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
@@ -26,12 +27,16 @@ const (
|
||||
chainNamePostrouting = "ts-postrouting"
|
||||
)
|
||||
|
||||
// chainTypeRegular is an nftables chain that does not apply to a hook.
|
||||
const chainTypeRegular = ""
|
||||
|
||||
type chainInfo struct {
|
||||
table *nftables.Table
|
||||
name string
|
||||
chainType nftables.ChainType
|
||||
chainHook *nftables.ChainHook
|
||||
chainPriority *nftables.ChainPriority
|
||||
chainPolicy *nftables.ChainPolicy
|
||||
}
|
||||
|
||||
type nftable struct {
|
||||
@@ -40,6 +45,21 @@ type nftable struct {
|
||||
Nat *nftables.Table
|
||||
}
|
||||
|
||||
// nftablesRunner implements a netfilterRunner using the netlink based nftables
|
||||
// library. As nftables allows for arbitrary tables and chains, there is a need
|
||||
// to follow conventions in order to integrate well with a surrounding
|
||||
// ecosystem. The rules installed by nftablesRunner have the following
|
||||
// properties:
|
||||
// - Install rules that intend to take precedence over rules installed by
|
||||
// other software. Tailscale provides packet filtering for tailnet traffic
|
||||
// inside the daemon based on the tailnet ACL rules.
|
||||
// - As nftables "accept" is not final, rules from high priority tables (low
|
||||
// numbers) will fall through to lower priority tables (high numbers). In
|
||||
// order to effectively be 'final', we install "jump" rules into conventional
|
||||
// tables and chains that will reach an accept verdict inside those tables.
|
||||
// - The table and chain conventions followed here are those used by
|
||||
// `iptables-nft` and `ufw`, so that those tools co-exist and do not
|
||||
// negatively affect Tailscale function.
|
||||
type nftablesRunner struct {
|
||||
conn *nftables.Conn
|
||||
nft4 *nftable
|
||||
@@ -116,6 +136,11 @@ func getChainsFromTable(c *nftables.Conn, table *nftables.Table) ([]*nftables.Ch
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// isTSChain retruns true if the chain name starts with ts
|
||||
func isTSChain(name string) bool {
|
||||
return strings.HasPrefix(name, "ts-")
|
||||
}
|
||||
|
||||
// createChainIfNotExist creates a chain with the given name in the given table
|
||||
// if it does not exist.
|
||||
func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
|
||||
@@ -123,8 +148,11 @@ func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{cinfo.table.Name, cinfo.name}) {
|
||||
return fmt.Errorf("get chain: %w", err)
|
||||
} else if err == nil {
|
||||
// Chain already exists
|
||||
if chain.Type != cinfo.chainType || chain.Hooknum != cinfo.chainHook || chain.Priority != cinfo.chainPriority {
|
||||
// The chain already exists. If it is a TS chain, check the
|
||||
// type/hook/priority, but for "conventional chains" assume they're what
|
||||
// we expect (in case iptables-nft/ufw make minor behavior changes in
|
||||
// the future).
|
||||
if isTSChain(chain.Name) && (chain.Type != cinfo.chainType || chain.Hooknum != cinfo.chainHook || chain.Priority != cinfo.chainPriority) {
|
||||
return fmt.Errorf("chain %s already exists with different type/hook/priority", cinfo.name)
|
||||
}
|
||||
return nil
|
||||
@@ -136,6 +164,7 @@ func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
|
||||
Type: cinfo.chainType,
|
||||
Hooknum: cinfo.chainHook,
|
||||
Priority: cinfo.chainPriority,
|
||||
Policy: cinfo.chainPolicy,
|
||||
})
|
||||
|
||||
if err := c.Flush(); err != nil {
|
||||
@@ -228,6 +257,10 @@ ruleLoop:
|
||||
}
|
||||
|
||||
for i, e := range r.Exprs {
|
||||
// Skip counter expressions, as they will not match.
|
||||
if _, ok := e.(*expr.Counter); ok {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(e, rule.Exprs[i]) {
|
||||
continue ruleLoop
|
||||
}
|
||||
@@ -388,27 +421,49 @@ func (n *nftablesRunner) getNATTables() []*nftable {
|
||||
// AddChains creates custom Tailscale chains in netfilter via nftables
|
||||
// if the ts-chain doesn't already exist.
|
||||
func (n *nftablesRunner) AddChains() error {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
for _, table := range n.getTables() {
|
||||
filter, err := createTableIfNotExist(n.conn, table.Proto, "ts-filter")
|
||||
// Create the filter table if it doesn't exist, this table name is the same
|
||||
// as the name used by iptables-nft and ufw. We install rules into the
|
||||
// same conventional table so that `accept` verdicts from our jump
|
||||
// chains are conclusive.
|
||||
filter, err := createTableIfNotExist(n.conn, table.Proto, "filter")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create table: %w", err)
|
||||
}
|
||||
table.Filter = filter
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, chainNameForward, nftables.ChainTypeFilter, nftables.ChainHookForward, nftables.ChainPriorityRef(-1)}); err != nil {
|
||||
// Adding the "conventional chains" that are used by iptables-nft and ufw.
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, "FORWARD", nftables.ChainTypeFilter, nftables.ChainHookForward, nftables.ChainPriorityFilter, &polAccept}); err != nil {
|
||||
return fmt.Errorf("create forward chain: %w", err)
|
||||
}
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, chainNameInput, nftables.ChainTypeFilter, nftables.ChainHookInput, nftables.ChainPriorityRef(-1)}); err != nil {
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, "INPUT", nftables.ChainTypeFilter, nftables.ChainHookInput, nftables.ChainPriorityFilter, &polAccept}); err != nil {
|
||||
return fmt.Errorf("create input chain: %w", err)
|
||||
}
|
||||
// Adding the tailscale chains that contain our rules.
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, chainNameForward, chainTypeRegular, nil, nil, nil}); err != nil {
|
||||
return fmt.Errorf("create forward chain: %w", err)
|
||||
}
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{filter, chainNameInput, chainTypeRegular, nil, nil, nil}); err != nil {
|
||||
return fmt.Errorf("create input chain: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range n.getNATTables() {
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "ts-nat")
|
||||
// Create the nat table if it doesn't exist, this table name is the same
|
||||
// as the name used by iptables-nft and ufw. We install rules into the
|
||||
// same conventional table so that `accept` verdicts from our jump
|
||||
// chains are conclusive.
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create table: %w", err)
|
||||
}
|
||||
table.Nat = nat
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{nat, chainNamePostrouting, nftables.ChainTypeNAT, nftables.ChainHookPostrouting, nftables.ChainPriorityNATDest}); err != nil {
|
||||
// Adding the "conventional chains" that are used by iptables-nft and ufw.
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{nat, "POSTROUTING", nftables.ChainTypeNAT, nftables.ChainHookPostrouting, nftables.ChainPriorityNATSource, &polAccept}); err != nil {
|
||||
return fmt.Errorf("create postrouting chain: %w", err)
|
||||
}
|
||||
// Adding the tailscale chain that contains our rules.
|
||||
if err = createChainIfNotExist(n.conn, chainInfo{nat, chainNamePostrouting, chainTypeRegular, nil, nil, nil}); err != nil {
|
||||
return fmt.Errorf("create postrouting chain: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -445,19 +500,16 @@ func (n *nftablesRunner) DelChains() error {
|
||||
if err := deleteChainIfExists(n.conn, table.Filter, chainNameInput); err != nil {
|
||||
return fmt.Errorf("delete chain: %w", err)
|
||||
}
|
||||
n.conn.DelTable(table.Filter)
|
||||
}
|
||||
|
||||
if err := deleteChainIfExists(n.conn, n.nft4.Nat, chainNamePostrouting); err != nil {
|
||||
return fmt.Errorf("delete chain: %w", err)
|
||||
}
|
||||
n.conn.DelTable(n.nft4.Nat)
|
||||
|
||||
if n.v6NATAvailable {
|
||||
if err := deleteChainIfExists(n.conn, n.nft6.Nat, chainNamePostrouting); err != nil {
|
||||
return fmt.Errorf("delete chain: %w", err)
|
||||
}
|
||||
n.conn.DelTable(n.nft6.Nat)
|
||||
}
|
||||
|
||||
if err := n.conn.Flush(); err != nil {
|
||||
@@ -467,15 +519,128 @@ func (n *nftablesRunner) DelChains() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddHooks is defined to satisfy the interface. NfTables does not require
|
||||
// AddHooks, since we don't have any default tables or chains in nftables.
|
||||
func (n *nftablesRunner) AddHooks() error {
|
||||
// createHookRule creates a rule to jump from a hooked chain to a regular chain.
|
||||
func createHookRule(table *nftables.Table, fromChain *nftables.Chain, toChainName string) *nftables.Rule {
|
||||
exprs := []expr.Any{
|
||||
&expr.Counter{},
|
||||
&expr.Verdict{
|
||||
Kind: expr.VerdictJump,
|
||||
Chain: toChainName,
|
||||
},
|
||||
}
|
||||
|
||||
rule := &nftables.Rule{
|
||||
Table: table,
|
||||
Chain: fromChain,
|
||||
Exprs: exprs,
|
||||
}
|
||||
|
||||
return rule
|
||||
}
|
||||
|
||||
// addHookRule adds a rule to jump from a hooked chain to a regular chain at top of the hooked chain.
|
||||
func addHookRule(conn *nftables.Conn, table *nftables.Table, fromChain *nftables.Chain, toChainName string) error {
|
||||
rule := createHookRule(table, fromChain, toChainName)
|
||||
_ = conn.InsertRule(rule)
|
||||
|
||||
if err := conn.Flush(); err != nil {
|
||||
return fmt.Errorf("flush add rule: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks is defined to satisfy the interface. NfTables does not require
|
||||
// DelHooks, since we don't have any default tables or chains in nftables.
|
||||
// AddHooks is adding rules to conventional chains like "FORWARD", "INPUT" and "POSTROUTING"
|
||||
// in tables and jump from those chains to tailscale chains.
|
||||
func (n *nftablesRunner) AddHooks() error {
|
||||
conn := n.conn
|
||||
|
||||
for _, table := range n.getTables() {
|
||||
inputChain, err := getChainFromTable(conn, table.Filter, "INPUT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get INPUT chain: %w", err)
|
||||
}
|
||||
err = addHookRule(conn, table.Filter, inputChain, chainNameInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Addhook: %w", err)
|
||||
}
|
||||
forwardChain, err := getChainFromTable(conn, table.Filter, "FORWARD")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get FORWARD chain: %w", err)
|
||||
}
|
||||
err = addHookRule(conn, table.Filter, forwardChain, chainNameForward)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Addhook: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range n.getNATTables() {
|
||||
postroutingChain, err := getChainFromTable(conn, table.Nat, "POSTROUTING")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get INPUT chain: %w", err)
|
||||
}
|
||||
err = addHookRule(conn, table.Nat, postroutingChain, chainNamePostrouting)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Addhook: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delHookRule deletes a rule that jumps from a hooked chain to a regular chain.
|
||||
func delHookRule(conn *nftables.Conn, table *nftables.Table, fromChain *nftables.Chain, toChainName string) error {
|
||||
rule := createHookRule(table, fromChain, toChainName)
|
||||
existingRule, err := findRule(conn, rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to find hook rule: %w", err)
|
||||
}
|
||||
|
||||
if existingRule == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = conn.DelRule(existingRule)
|
||||
|
||||
if err := conn.Flush(); err != nil {
|
||||
return fmt.Errorf("flush del hook rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks is deleting the rules added to conventional chains to jump to tailscale chains.
|
||||
func (n *nftablesRunner) DelHooks(logf logger.Logf) error {
|
||||
conn := n.conn
|
||||
|
||||
for _, table := range n.getTables() {
|
||||
inputChain, err := getChainFromTable(conn, table.Filter, "INPUT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get INPUT chain: %w", err)
|
||||
}
|
||||
err = delHookRule(conn, table.Filter, inputChain, chainNameInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delhook: %w", err)
|
||||
}
|
||||
forwardChain, err := getChainFromTable(conn, table.Filter, "FORWARD")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get FORWARD chain: %w", err)
|
||||
}
|
||||
err = delHookRule(conn, table.Filter, forwardChain, chainNameForward)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delhook: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range n.getNATTables() {
|
||||
postroutingChain, err := getChainFromTable(conn, table.Nat, "POSTROUTING")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get INPUT chain: %w", err)
|
||||
}
|
||||
err = delHookRule(conn, table.Nat, postroutingChain, chainNamePostrouting)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delhook: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -953,25 +1118,62 @@ func (n *nftablesRunner) DelSNATRule() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupChain removes a jump rule from hookChainName to tsChainName, and then
|
||||
// the entire chain tsChainName. Errors are logged, but attempts to remove both
|
||||
// the jump rule and chain continue even if one errors.
|
||||
func cleanupChain(logf logger.Logf, conn *nftables.Conn, table *nftables.Table, hookChainName, tsChainName string) {
|
||||
// remove the jump first, before removing the jump destination.
|
||||
defaultChain, err := getChainFromTable(conn, table, hookChainName)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{table.Name, hookChainName}) {
|
||||
logf("cleanup: did not find default chain: %s", err)
|
||||
}
|
||||
if !errors.Is(err, errorChainNotFound{table.Name, hookChainName}) {
|
||||
// delete hook in convention chain
|
||||
_ = delHookRule(conn, table, defaultChain, tsChainName)
|
||||
}
|
||||
|
||||
tsChain, err := getChainFromTable(conn, table, tsChainName)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{table.Name, tsChainName}) {
|
||||
logf("cleanup: did not find ts-chain: %s", err)
|
||||
}
|
||||
|
||||
if tsChain != nil {
|
||||
// flush and delete ts-chain
|
||||
conn.FlushChain(tsChain)
|
||||
conn.DelChain(tsChain)
|
||||
err = conn.Flush()
|
||||
logf("cleanup: delete and flush chain %s: %s", tsChainName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// NfTablesCleanUp removes all Tailscale added nftables rules.
|
||||
// Any errors that occur are logged to the provided logf.
|
||||
func NfTablesCleanUp(logf logger.Logf) {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
logf("ERROR: nftables connection: %w", err)
|
||||
logf("cleanup: nftables connection: %s", err)
|
||||
}
|
||||
|
||||
tables, err := conn.ListTables() // both v4 and v6
|
||||
if err != nil {
|
||||
logf("ERROR: list tables: %w", err)
|
||||
logf("cleanup: list tables: %s", err)
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
// These table names were used briefly in 1.48.0.
|
||||
if table.Name == "ts-filter" || table.Name == "ts-nat" {
|
||||
conn.DelTable(table)
|
||||
if err := conn.Flush(); err != nil {
|
||||
logf("ERROR: flush table %s: %w", table.Name, err)
|
||||
logf("cleanup: flush delete table %s: %s", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if table.Name == "filter" {
|
||||
cleanupChain(logf, conn, table, "INPUT", chainNameInput)
|
||||
cleanupChain(logf, conn, table, "FORWARD", chainNameForward)
|
||||
}
|
||||
if table.Name == "nat" {
|
||||
cleanupChain(logf, conn, table, "POSTROUTING", chainNamePostrouting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,48 @@ func newTestConn(t *testing.T, want [][]byte) *nftables.Conn {
|
||||
return conn
|
||||
}
|
||||
|
||||
func TestInsertHookRule(t *testing.T) {
|
||||
proto := nftables.TableFamilyIPv4
|
||||
want := [][]byte{
|
||||
// batch begin
|
||||
[]byte("\x00\x00\x00\x0a"),
|
||||
// nft add table ip ts-filter-test
|
||||
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
|
||||
// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0 \; }
|
||||
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
|
||||
// nft add chain ip ts-filter-test ts-jumpto
|
||||
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x0e\x00\x03\x00\x74\x73\x2d\x6a\x75\x6d\x70\x74\x6f\x00\x00\x00"),
|
||||
// nft add rule ip ts-filter-test ts-input-test counter jump ts-jumptp
|
||||
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x70\x00\x04\x80\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x20\x00\x02\x80\x1c\x00\x02\x80\x08\x00\x01\x00\xff\xff\xff\xfd\x0e\x00\x02\x00\x74\x73\x2d\x6a\x75\x6d\x70\x74\x6f\x00\x00\x00"),
|
||||
// batch end
|
||||
[]byte("\x00\x00\x00\x0a"),
|
||||
}
|
||||
testConn := newTestConn(t, want)
|
||||
table := testConn.AddTable(&nftables.Table{
|
||||
Family: proto,
|
||||
Name: "ts-filter-test",
|
||||
})
|
||||
|
||||
fromchain := testConn.AddChain(&nftables.Chain{
|
||||
Name: "ts-input-test",
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookInput,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
})
|
||||
|
||||
tochain := testConn.AddChain(&nftables.Chain{
|
||||
Name: "ts-jumpto",
|
||||
Table: table,
|
||||
})
|
||||
|
||||
err := addHookRule(testConn, table, fromchain, tochain.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInsertLoopbackRule(t *testing.T) {
|
||||
proto := nftables.TableFamilyIPv4
|
||||
want := [][]byte{
|
||||
@@ -461,8 +503,8 @@ func TestAddAndDelNetfilterChains(t *testing.T) {
|
||||
t.Fatalf("list chains failed: %v", err)
|
||||
}
|
||||
|
||||
if len(chainsV4) != 3 {
|
||||
t.Fatalf("len(chainsV4) = %d, want 3", len(chainsV4))
|
||||
if len(chainsV4) != 6 {
|
||||
t.Fatalf("len(chainsV4) = %d, want 6", len(chainsV4))
|
||||
}
|
||||
|
||||
chainsV6, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv6)
|
||||
@@ -470,8 +512,8 @@ func TestAddAndDelNetfilterChains(t *testing.T) {
|
||||
t.Fatalf("list chains failed: %v", err)
|
||||
}
|
||||
|
||||
if len(chainsV6) != 3 {
|
||||
t.Fatalf("len(chainsV6) = %d, want 3", len(chainsV6))
|
||||
if len(chainsV6) != 6 {
|
||||
t.Fatalf("len(chainsV6) = %d, want 6", len(chainsV6))
|
||||
}
|
||||
|
||||
runner.DelChains()
|
||||
@@ -788,3 +830,87 @@ func TestNFTAddAndDelLoopbackRule(t *testing.T) {
|
||||
t.Fatalf("len(inputV4Rules) = %d, want 2", len(inputV4Rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNFTAddAndDelHookRule(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip(t.Name(), " requires privileges to create a namespace in order to run")
|
||||
return
|
||||
}
|
||||
|
||||
conn := newSysConn(t)
|
||||
runner := newFakeNftablesRunner(t, conn)
|
||||
runner.AddChains()
|
||||
defer runner.DelChains()
|
||||
runner.AddHooks()
|
||||
|
||||
forwardChain, err := getChainFromTable(conn, runner.nft4.Filter, "FORWARD")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get forwardChain: %v", err)
|
||||
}
|
||||
|
||||
forwardChainRules, err := conn.GetRules(forwardChain.Table, forwardChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(forwardChainRules) != 1 {
|
||||
t.Fatalf("expected 1 rule in FORWARD chain, got %v", len(forwardChainRules))
|
||||
}
|
||||
|
||||
inputChain, err := getChainFromTable(conn, runner.nft4.Filter, "INPUT")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get inputChain: %v", err)
|
||||
}
|
||||
|
||||
inputChainRules, err := conn.GetRules(inputChain.Table, inputChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(inputChainRules) != 1 {
|
||||
t.Fatalf("expected 1 rule in INPUT chain, got %v", len(inputChainRules))
|
||||
}
|
||||
|
||||
postroutingChain, err := getChainFromTable(conn, runner.nft4.Nat, "POSTROUTING")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get postroutingChain: %v", err)
|
||||
}
|
||||
|
||||
postroutingChainRules, err := conn.GetRules(postroutingChain.Table, postroutingChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(postroutingChainRules) != 1 {
|
||||
t.Fatalf("expected 1 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
|
||||
}
|
||||
|
||||
runner.DelHooks(t.Logf)
|
||||
|
||||
forwardChainRules, err = conn.GetRules(forwardChain.Table, forwardChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(forwardChainRules) != 0 {
|
||||
t.Fatalf("expected 0 rule in FORWARD chain, got %v", len(forwardChainRules))
|
||||
}
|
||||
|
||||
inputChainRules, err = conn.GetRules(inputChain.Table, inputChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(inputChainRules) != 0 {
|
||||
t.Fatalf("expected 0 rule in INPUT chain, got %v", len(inputChainRules))
|
||||
}
|
||||
|
||||
postroutingChainRules, err = conn.GetRules(postroutingChain.Table, postroutingChain)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get rules: %v", err)
|
||||
}
|
||||
|
||||
if len(postroutingChainRules) != 0 {
|
||||
t.Fatalf("expected 0 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ package multierr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// An Error represents multiple errors.
|
||||
|
||||
@@ -6,10 +6,10 @@ package osdiag
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
package set
|
||||
|
||||
import (
|
||||
"golang.org/x/exp/slices"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ package slicesx
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func TestInterleave(t *testing.T) {
|
||||
|
||||
@@ -96,7 +96,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
eps = append(eps, ep.Addr.String())
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
n := &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(0),
|
||||
Name: "n1",
|
||||
Addresses: []netip.Prefix{a1},
|
||||
@@ -106,7 +106,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
e2.SetNetworkMap(&netmap.NetworkMap{
|
||||
NodeKey: k2.Public(),
|
||||
PrivateKey: k2,
|
||||
Peers: []*tailcfg.Node{&n},
|
||||
Peers: []tailcfg.NodeView{n.View()},
|
||||
})
|
||||
|
||||
p := wgcfg.Peer{
|
||||
@@ -114,7 +114,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
AllowedIPs: []netip.Prefix{a1},
|
||||
}
|
||||
c2.Peers = []wgcfg.Peer{p}
|
||||
e2.Reconfig(&c2, &router.Config{}, new(dns.Config), nil)
|
||||
e2.Reconfig(&c2, &router.Config{}, new(dns.Config))
|
||||
e1waitDoneOnce.Do(wait.Done)
|
||||
})
|
||||
|
||||
@@ -133,7 +133,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
eps = append(eps, ep.Addr.String())
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
n := &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(0),
|
||||
Name: "n2",
|
||||
Addresses: []netip.Prefix{a2},
|
||||
@@ -143,7 +143,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
e1.SetNetworkMap(&netmap.NetworkMap{
|
||||
NodeKey: k1.Public(),
|
||||
PrivateKey: k1,
|
||||
Peers: []*tailcfg.Node{&n},
|
||||
Peers: []tailcfg.NodeView{n.View()},
|
||||
})
|
||||
|
||||
p := wgcfg.Peer{
|
||||
@@ -151,7 +151,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
|
||||
AllowedIPs: []netip.Prefix{a2},
|
||||
}
|
||||
c1.Peers = []wgcfg.Peer{p}
|
||||
e1.Reconfig(&c1, &router.Config{}, new(dns.Config), nil)
|
||||
e1.Reconfig(&c1, &router.Config{}, new(dns.Config))
|
||||
e2waitDoneOnce.Do(wait.Done)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ package filter
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/flowtrack"
|
||||
"tailscale.com/net/netaddr"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user