Update
This commit is contained in:
86
vendor/tailscale.com/util/backoff/backoff.go
generated
vendored
Normal file
86
vendor/tailscale.com/util/backoff/backoff.go
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package backoff provides a back-off timer type.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Backoff tracks state the history of consecutive failures and sleeps
|
||||
// an increasing amount of time, up to a provided limit.
|
||||
type Backoff struct {
|
||||
n int // number of consecutive failures
|
||||
maxBackoff time.Duration
|
||||
|
||||
// Name is the name of this backoff timer, for logging purposes.
|
||||
name string
|
||||
// logf is the function used for log messages when backing off.
|
||||
logf logger.Logf
|
||||
|
||||
// tstime.Clock.NewTimer is used instead time.NewTimer.
|
||||
Clock tstime.Clock
|
||||
|
||||
// LogLongerThan sets the minimum time of a single backoff interval
|
||||
// before we mention it in the log.
|
||||
LogLongerThan time.Duration
|
||||
}
|
||||
|
||||
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
|
||||
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
|
||||
// are logged unless the returned Backoff.LogLongerThan is adjusted.
|
||||
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
|
||||
return &Backoff{
|
||||
name: name,
|
||||
logf: logf,
|
||||
maxBackoff: maxBackoff,
|
||||
Clock: tstime.StdClock{},
|
||||
}
|
||||
}
|
||||
|
||||
// BackOff sleeps an increasing amount of time if err is non-nil while the
|
||||
// context is active. It resets the backoff schedule once err is nil.
|
||||
func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
if err == nil {
|
||||
// No error. Reset number of consecutive failures.
|
||||
b.n = 0
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// Fast path.
|
||||
return
|
||||
}
|
||||
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
|
||||
if d > b.maxBackoff {
|
||||
d = b.maxBackoff
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
if d >= b.LogLongerThan {
|
||||
b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
|
||||
}
|
||||
t, tChannel := b.Clock.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-tChannel:
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the backoff schedule, equivalent to calling BackOff with a nil
|
||||
// error.
|
||||
func (b *Backoff) Reset() {
|
||||
b.n = 0
|
||||
}
|
||||
25
vendor/tailscale.com/util/checkchange/checkchange.go
generated
vendored
Normal file
25
vendor/tailscale.com/util/checkchange/checkchange.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package checkchange defines a utility for determining whether a value
|
||||
// has changed since the last time it was checked.
|
||||
package checkchange
|
||||
|
||||
// EqualCloner is an interface for types that can be compared for equality
|
||||
// and can be cloned.
|
||||
type EqualCloner[T any] interface {
|
||||
Equal(T) bool
|
||||
Clone() T
|
||||
}
|
||||
|
||||
// Update sets *old to a clone of new if they are not equal, returning whether
|
||||
// they were different.
|
||||
//
|
||||
// It only modifies *old if they are different. old must be non-nil.
|
||||
func Update[T EqualCloner[T]](old *T, new T) (changed bool) {
|
||||
if (*old).Equal(new) {
|
||||
return false
|
||||
}
|
||||
*old = new.Clone()
|
||||
return true
|
||||
}
|
||||
33
vendor/tailscale.com/util/clientmetric/clientmetric.go
generated
vendored
33
vendor/tailscale.com/util/clientmetric/clientmetric.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_clientmetrics
|
||||
|
||||
// Package clientmetric provides client-side metrics whose values
|
||||
// get occasionally logged.
|
||||
package clientmetric
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@@ -55,6 +58,20 @@ const (
|
||||
TypeCounter
|
||||
)
|
||||
|
||||
// MetricUpdate requests that a client metric value be updated.
|
||||
//
|
||||
// This is the request body sent to /localapi/v0/upload-client-metrics.
|
||||
type MetricUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // one of "counter" or "gauge"
|
||||
Value int `json:"value"` // amount to increment by or set
|
||||
|
||||
// Op indicates if Value is added to the existing metric value,
|
||||
// or if the metric is set to Value.
|
||||
// One of "add" or "set". If empty, defaults to "add".
|
||||
Op string `json:"op"`
|
||||
}
|
||||
|
||||
// Metric is an integer metric value that's tracked over time.
|
||||
//
|
||||
// It's safe for concurrent use.
|
||||
@@ -130,15 +147,20 @@ func (m *Metric) Publish() {
|
||||
metrics[m.name] = m
|
||||
sortedDirty = true
|
||||
|
||||
if m.f != nil {
|
||||
lastLogVal = append(lastLogVal, scanEntry{f: m.f})
|
||||
} else {
|
||||
if m.f == nil {
|
||||
if len(valFreeList) == 0 {
|
||||
valFreeList = make([]int64, 256)
|
||||
}
|
||||
m.v = &valFreeList[0]
|
||||
valFreeList = valFreeList[1:]
|
||||
lastLogVal = append(lastLogVal, scanEntry{v: m.v})
|
||||
}
|
||||
|
||||
if buildfeatures.HasLogTail {
|
||||
if m.f != nil {
|
||||
lastLogVal = append(lastLogVal, scanEntry{f: m.f})
|
||||
} else {
|
||||
lastLogVal = append(lastLogVal, scanEntry{v: m.v})
|
||||
}
|
||||
}
|
||||
|
||||
m.regIdx = len(unsorted)
|
||||
@@ -319,6 +341,9 @@ const (
|
||||
// - increment a metric: (decrements if negative)
|
||||
// 'I' + hex(varint(wireid)) + hex(varint(value))
|
||||
func EncodeLogTailMetricsDelta() string {
|
||||
if !buildfeatures.HasLogTail {
|
||||
return ""
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
|
||||
31
vendor/tailscale.com/util/clientmetric/omit.go
generated
vendored
Normal file
31
vendor/tailscale.com/util/clientmetric/omit.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_clientmetrics
|
||||
|
||||
package clientmetric
|
||||
|
||||
type Metric struct{}
|
||||
|
||||
func (*Metric) Add(int64) {}
|
||||
func (*Metric) Set(int64) {}
|
||||
func (*Metric) Value() int64 { return 0 }
|
||||
func (*Metric) Register(expvarInt any) {}
|
||||
func (*Metric) UnregisterAll() {}
|
||||
|
||||
type MetricUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value"`
|
||||
Op string `json:"op"`
|
||||
}
|
||||
|
||||
func HasPublished(string) bool { panic("unreachable") }
|
||||
func EncodeLogTailMetricsDelta() string { return "" }
|
||||
func WritePrometheusExpositionFormat(any) {}
|
||||
|
||||
var zeroMetric Metric
|
||||
|
||||
func NewCounter(string) *Metric { return &zeroMetric }
|
||||
func NewGauge(string) *Metric { return &zeroMetric }
|
||||
func NewAggregateCounter(string) *Metric { return &zeroMetric }
|
||||
7
vendor/tailscale.com/util/cloudenv/cloudenv.go
generated
vendored
7
vendor/tailscale.com/util/cloudenv/cloudenv.go
generated
vendored
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/lazy"
|
||||
)
|
||||
@@ -51,6 +52,9 @@ const (
|
||||
// ResolverIP returns the cloud host's recursive DNS server or the
|
||||
// empty string if not available.
|
||||
func (c Cloud) ResolverIP() string {
|
||||
if !buildfeatures.HasCloud {
|
||||
return ""
|
||||
}
|
||||
switch c {
|
||||
case GCP:
|
||||
return GoogleMetadataAndDNSIP
|
||||
@@ -92,6 +96,9 @@ var cloudAtomic syncs.AtomicValue[Cloud]
|
||||
|
||||
// Get returns the current cloud, or the empty string if unknown.
|
||||
func Get() Cloud {
|
||||
if !buildfeatures.HasCloud {
|
||||
return ""
|
||||
}
|
||||
if c, ok := cloudAtomic.LoadOk(); ok {
|
||||
return c
|
||||
}
|
||||
|
||||
194
vendor/tailscale.com/util/cloudinfo/cloudinfo.go
generated
vendored
Normal file
194
vendor/tailscale.com/util/cloudinfo/cloudinfo.go
generated
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(ios || android || js)
|
||||
|
||||
// Package cloudinfo provides cloud metadata utilities.
|
||||
package cloudinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cloudenv"
|
||||
)
|
||||
|
||||
const maxCloudInfoWait = 2 * time.Second
|
||||
|
||||
// CloudInfo holds state used in querying instance metadata (IMDS) endpoints.
|
||||
type CloudInfo struct {
|
||||
client http.Client
|
||||
logf logger.Logf
|
||||
|
||||
// The following parameters are fixed for the lifetime of the cloudInfo
|
||||
// object, but are used for testing.
|
||||
cloud cloudenv.Cloud
|
||||
endpoint string
|
||||
}
|
||||
|
||||
// New constructs a new [*CloudInfo] that will log to the provided logger instance.
|
||||
func New(logf logger.Logf) *CloudInfo {
|
||||
if !buildfeatures.HasCloud {
|
||||
return nil
|
||||
}
|
||||
tr := &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: maxCloudInfoWait,
|
||||
}).Dial,
|
||||
}
|
||||
|
||||
return &CloudInfo{
|
||||
client: http.Client{Transport: tr},
|
||||
logf: logf,
|
||||
cloud: cloudenv.Get(),
|
||||
endpoint: "http://" + cloudenv.CommonNonRoutableMetadataIP,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPublicIPs returns any public IPs attached to the current cloud instance,
|
||||
// if the tailscaled process is running in a known cloud and there are any such
|
||||
// IPs present.
|
||||
//
|
||||
// Currently supports only AWS.
|
||||
func (ci *CloudInfo) GetPublicIPs(ctx context.Context) ([]netip.Addr, error) {
|
||||
if !buildfeatures.HasCloud {
|
||||
return nil, nil
|
||||
}
|
||||
switch ci.cloud {
|
||||
case cloudenv.AWS:
|
||||
ret, err := ci.getAWS(ctx)
|
||||
ci.logf("[v1] cloudinfo.GetPublicIPs: AWS: %v, %v", ret, err)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getAWSMetadata makes a request to the AWS metadata service at the given
|
||||
// path, authenticating with the provided IMDSv2 token. The returned metadata
|
||||
// is split by newline and returned as a slice.
|
||||
func (ci *CloudInfo) getAWSMetadata(ctx context.Context, token, path string) ([]string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ci.endpoint+path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request to %q: %w", path, err)
|
||||
}
|
||||
req.Header.Set("X-aws-ec2-metadata-token", token)
|
||||
|
||||
resp, err := ci.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making request to metadata service %q: %w", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
// Good
|
||||
case http.StatusNotFound:
|
||||
// Nothing found, but this isn't an error; just return
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response body for %q: %w", path, err)
|
||||
}
|
||||
|
||||
return strings.Split(strings.TrimSpace(string(body)), "\n"), nil
|
||||
}
|
||||
|
||||
// getAWS returns all public IPv4 and IPv6 addresses present in the AWS instance metadata.
|
||||
func (ci *CloudInfo) getAWS(ctx context.Context) ([]netip.Addr, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, maxCloudInfoWait)
|
||||
defer cancel()
|
||||
|
||||
// Get a token so we can query the metadata service.
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", ci.endpoint+"/latest/api/token", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating token request: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "10")
|
||||
|
||||
resp, err := ci.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making token request to metadata service: %w", err)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading token response body: %w", err)
|
||||
}
|
||||
token := string(body)
|
||||
|
||||
server := resp.Header.Get("Server")
|
||||
if server != "EC2ws" {
|
||||
return nil, fmt.Errorf("unexpected server header: %q", server)
|
||||
}
|
||||
|
||||
// Iterate over all interfaces and get their public IP addresses, both IPv4 and IPv6.
|
||||
macAddrs, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting interface MAC addresses: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
addrs []netip.Addr
|
||||
errs []error
|
||||
)
|
||||
|
||||
addAddr := func(addr string) {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("parsing IP address %q: %w", addr, err))
|
||||
return
|
||||
}
|
||||
addrs = append(addrs, ip)
|
||||
}
|
||||
for _, mac := range macAddrs {
|
||||
ips, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/public-ipv4s")
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting IPv4 addresses for %q: %w", mac, err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
addAddr(ip)
|
||||
}
|
||||
|
||||
// Try querying for IPv6 addresses.
|
||||
ips, err = ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/ipv6s")
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting IPv6 addresses for %q: %w", mac, err))
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
addAddr(ip)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the returned addresses for determinism.
|
||||
slices.SortFunc(addrs, func(a, b netip.Addr) int {
|
||||
return a.Compare(b)
|
||||
})
|
||||
|
||||
// Preferentially return any addresses we found, even if there were errors.
|
||||
if len(addrs) > 0 {
|
||||
return addrs, nil
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("getting IP addresses: %w", errors.Join(errs...))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
26
vendor/tailscale.com/util/cloudinfo/cloudinfo_nocloud.go
generated
vendored
Normal file
26
vendor/tailscale.com/util/cloudinfo/cloudinfo_nocloud.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || android || js
|
||||
|
||||
package cloudinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// CloudInfo is not available in mobile and JS targets.
|
||||
type CloudInfo struct{}
|
||||
|
||||
// New construct a no-op CloudInfo stub.
|
||||
func New(_ logger.Logf) *CloudInfo {
|
||||
return &CloudInfo{}
|
||||
}
|
||||
|
||||
// GetPublicIPs always returns nil slice and error.
|
||||
func (ci *CloudInfo) GetPublicIPs(_ context.Context) ([]netip.Addr, error) {
|
||||
return nil, nil
|
||||
}
|
||||
2
vendor/tailscale.com/util/dnsname/dnsname.go
generated
vendored
2
vendor/tailscale.com/util/dnsname/dnsname.go
generated
vendored
@@ -14,7 +14,7 @@ const (
|
||||
// maxLabelLength is the maximum length of a label permitted by RFC 1035.
|
||||
maxLabelLength = 63
|
||||
// maxNameLength is the maximum length of a DNS name.
|
||||
maxNameLength = 253
|
||||
maxNameLength = 254
|
||||
)
|
||||
|
||||
// A FQDN is a fully-qualified DNS name or name suffix.
|
||||
|
||||
6
vendor/tailscale.com/util/eventbus/assets/event.html
generated
vendored
Normal file
6
vendor/tailscale.com/util/eventbus/assets/event.html
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<li id="monitor" hx-swap-oob="afterbegin">
|
||||
<details>
|
||||
<summary>{{.Count}}: {{.Type}} from {{.Event.From.Name}}, {{len .Event.To}} recipients</summary>
|
||||
{{.Event.Event}}
|
||||
</details>
|
||||
</li>
|
||||
BIN
vendor/tailscale.com/util/eventbus/assets/htmx-websocket.min.js.gz
generated
vendored
Normal file
BIN
vendor/tailscale.com/util/eventbus/assets/htmx-websocket.min.js.gz
generated
vendored
Normal file
Binary file not shown.
BIN
vendor/tailscale.com/util/eventbus/assets/htmx.min.js.gz
generated
vendored
Normal file
BIN
vendor/tailscale.com/util/eventbus/assets/htmx.min.js.gz
generated
vendored
Normal file
Binary file not shown.
97
vendor/tailscale.com/util/eventbus/assets/main.html
generated
vendored
Normal file
97
vendor/tailscale.com/util/eventbus/assets/main.html
generated
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="bus/htmx.min.js"></script>
|
||||
<script src="bus/htmx-websocket.min.js"></script>
|
||||
<link rel="stylesheet" href="bus/style.css">
|
||||
</head>
|
||||
<body hx-ext="ws">
|
||||
<h1>Event bus</h1>
|
||||
|
||||
<section>
|
||||
<h2>General</h2>
|
||||
{{with $.PublishQueue}}
|
||||
{{len .}} pending
|
||||
{{end}}
|
||||
|
||||
<button hx-post="bus/monitor" hx-swap="outerHTML">Monitor all events</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Clients</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Publishing</th>
|
||||
<th>Subscribing</th>
|
||||
<th>Pending</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{range .Clients}}
|
||||
<tr id="{{.Name}}">
|
||||
<td>{{.Name}}</td>
|
||||
<td class="list">
|
||||
<ul>
|
||||
{{range .Publish}}
|
||||
<li><a href="#{{.}}">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</td>
|
||||
<td class="list">
|
||||
<ul>
|
||||
{{range .Subscribe}}
|
||||
<li><a href="#{{.}}">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
{{len ($.SubscribeQueue .Client)}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Types</h2>
|
||||
|
||||
{{range .Types}}
|
||||
|
||||
<section id="{{.}}">
|
||||
<h3>{{.Name}}</h3>
|
||||
<h4>Definition</h4>
|
||||
<code>{{prettyPrintStruct .}}</code>
|
||||
|
||||
<h4>Published by:</h4>
|
||||
{{if len (.Publish)}}
|
||||
<ul>
|
||||
{{range .Publish}}
|
||||
<li><a href="#{{.Name}}">{{.Name}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>No publishers.</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
<h4>Received by:</h4>
|
||||
{{if len (.Subscribe)}}
|
||||
<ul>
|
||||
{{range .Subscribe}}
|
||||
<li><a href="#{{.Name}}">{{.Name}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>No subscribers.</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
5
vendor/tailscale.com/util/eventbus/assets/monitor.html
generated
vendored
Normal file
5
vendor/tailscale.com/util/eventbus/assets/monitor.html
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<ul id="monitor" ws-connect="bus/monitor">
|
||||
</ul>
|
||||
<button hx-get="bus" hx-target="body">Stop monitoring</button>
|
||||
</div>
|
||||
90
vendor/tailscale.com/util/eventbus/assets/style.css
generated
vendored
Normal file
90
vendor/tailscale.com/util/eventbus/assets/style.css
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/* CSS reset, thanks Josh Comeau: https://www.joshwcomeau.com/css/custom-css-reset/ */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { margin: 0; }
|
||||
input, button, textarea, select { font: inherit; }
|
||||
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
|
||||
p { text-wrap: pretty; }
|
||||
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
|
||||
#root, #__next { isolation: isolate; }
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
img, picture, video, canvas, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Local styling begins */
|
||||
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-gap: 6px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
section > * {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
section > h2, section > h3 {
|
||||
margin-left: 0;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
details {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: calc(100% - 48px);
|
||||
border-collapse: collapse;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td.list {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
td ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 12px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#monitor {
|
||||
width: calc(100% - 48px);
|
||||
resize: vertical;
|
||||
padding: 12px;
|
||||
overflow: scroll;
|
||||
height: 15lh;
|
||||
border: 1px inset;
|
||||
min-height: 1em;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
345
vendor/tailscale.com/util/eventbus/bus.go
generated
vendored
Normal file
345
vendor/tailscale.com/util/eventbus/bus.go
generated
vendored
Normal file
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
type PublishedEvent struct {
|
||||
Event any
|
||||
From *Client
|
||||
}
|
||||
|
||||
type RoutedEvent struct {
|
||||
Event any
|
||||
From *Client
|
||||
To []*Client
|
||||
}
|
||||
|
||||
// Bus is an event bus that distributes published events to interested
|
||||
// subscribers.
|
||||
type Bus struct {
|
||||
router *worker
|
||||
write chan PublishedEvent
|
||||
snapshot chan chan []PublishedEvent
|
||||
routeDebug hook[RoutedEvent]
|
||||
logf logger.Logf
|
||||
|
||||
topicsMu syncs.Mutex
|
||||
topics map[reflect.Type][]*subscribeState
|
||||
|
||||
// Used for introspection/debugging only, not in the normal event
|
||||
// publishing path.
|
||||
clientsMu syncs.Mutex
|
||||
clients set.Set[*Client]
|
||||
}
|
||||
|
||||
// New returns a new bus with default options. It is equivalent to
|
||||
// calling [NewWithOptions] with zero [BusOptions].
|
||||
func New() *Bus { return NewWithOptions(BusOptions{}) }
|
||||
|
||||
// NewWithOptions returns a new [Bus] with the specified [BusOptions].
|
||||
// Use [Bus.Client] to construct clients on the bus.
|
||||
// Use [Publish] to make event publishers.
|
||||
// Use [Subscribe] and [SubscribeFunc] to make event subscribers.
|
||||
func NewWithOptions(opts BusOptions) *Bus {
|
||||
ret := &Bus{
|
||||
write: make(chan PublishedEvent),
|
||||
snapshot: make(chan chan []PublishedEvent),
|
||||
topics: map[reflect.Type][]*subscribeState{},
|
||||
clients: set.Set[*Client]{},
|
||||
logf: opts.logger(),
|
||||
}
|
||||
ret.router = runWorker(ret.pump)
|
||||
return ret
|
||||
}
|
||||
|
||||
// BusOptions are optional parameters for a [Bus]. A zero value is ready for
|
||||
// use and provides defaults as described.
|
||||
type BusOptions struct {
|
||||
// Logf, if non-nil, is used for debug logs emitted by the bus and clients,
|
||||
// publishers, and subscribers under its care. If it is nil, logs are sent
|
||||
// to [log.Printf].
|
||||
Logf logger.Logf
|
||||
}
|
||||
|
||||
func (o BusOptions) logger() logger.Logf {
|
||||
if o.Logf == nil {
|
||||
return log.Printf
|
||||
}
|
||||
return o.Logf
|
||||
}
|
||||
|
||||
// Client returns a new client with no subscriptions. Use [Subscribe]
|
||||
// to receive events, and [Publish] to emit events.
|
||||
//
|
||||
// The client's name is used only for debugging, to tell humans what
|
||||
// piece of code a publisher/subscriber belongs to. Aim for something
|
||||
// short but unique, for example "kernel-route-monitor" or "taildrop",
|
||||
// not "watcher".
|
||||
func (b *Bus) Client(name string) *Client {
|
||||
ret := &Client{
|
||||
name: name,
|
||||
bus: b,
|
||||
pub: set.Set[publisher]{},
|
||||
}
|
||||
b.clientsMu.Lock()
|
||||
defer b.clientsMu.Unlock()
|
||||
b.clients.Add(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
// Debugger returns the debugging facility for the bus.
|
||||
func (b *Bus) Debugger() *Debugger {
|
||||
return &Debugger{b}
|
||||
}
|
||||
|
||||
// Close closes the bus. It implicitly closes all clients, publishers and
|
||||
// subscribers attached to the bus.
|
||||
//
|
||||
// Close blocks until the bus is fully shut down. The bus is
|
||||
// permanently unusable after closing.
|
||||
func (b *Bus) Close() {
|
||||
b.router.StopAndWait()
|
||||
|
||||
b.clientsMu.Lock()
|
||||
defer b.clientsMu.Unlock()
|
||||
for c := range b.clients {
|
||||
c.Close()
|
||||
}
|
||||
b.clients = nil
|
||||
}
|
||||
|
||||
func (b *Bus) pump(ctx context.Context) {
|
||||
// Limit how many published events we can buffer in the PublishedEvent queue.
|
||||
//
|
||||
// Subscribers have unbounded DeliveredEvent queues (see tailscale/tailscale#18020),
|
||||
// so this queue doesn't need to be unbounded. Keeping it bounded may also help
|
||||
// catch cases where subscribers stop pumping events completely, such as due to a bug
|
||||
// in [subscribeState.pump], [Subscriber.dispatch], or [SubscriberFunc.dispatch]).
|
||||
const maxPublishedEvents = 16
|
||||
vals := queue[PublishedEvent]{capacity: maxPublishedEvents}
|
||||
acceptCh := func() chan PublishedEvent {
|
||||
if vals.Full() {
|
||||
return nil
|
||||
}
|
||||
return b.write
|
||||
}
|
||||
for {
|
||||
// Drain all pending events. Note that while we're draining
|
||||
// events into subscriber queues, we continue to
|
||||
// opportunistically accept more incoming events, if we have
|
||||
// queue space for it.
|
||||
for !vals.Empty() {
|
||||
val := vals.Peek()
|
||||
dests := b.dest(reflect.TypeOf(val.Event))
|
||||
|
||||
if b.routeDebug.active() {
|
||||
clients := make([]*Client, len(dests))
|
||||
for i := range len(dests) {
|
||||
clients[i] = dests[i].client
|
||||
}
|
||||
b.routeDebug.run(RoutedEvent{
|
||||
Event: val.Event,
|
||||
From: val.From,
|
||||
To: clients,
|
||||
})
|
||||
}
|
||||
|
||||
for _, d := range dests {
|
||||
evt := DeliveredEvent{
|
||||
Event: val.Event,
|
||||
From: val.From,
|
||||
To: d.client,
|
||||
}
|
||||
deliverOne:
|
||||
for {
|
||||
select {
|
||||
case d.write <- evt:
|
||||
break deliverOne
|
||||
case <-d.closed():
|
||||
// Queue closed, don't block but continue
|
||||
// delivering to others.
|
||||
break deliverOne
|
||||
case in := <-acceptCh():
|
||||
vals.Add(in)
|
||||
in.From.publishDebug.run(in)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case ch := <-b.snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
vals.Drop()
|
||||
}
|
||||
|
||||
// Inbound queue empty, wait for at least 1 work item before
|
||||
// resuming.
|
||||
for vals.Empty() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case in := <-b.write:
|
||||
vals.Add(in)
|
||||
in.From.publishDebug.run(in)
|
||||
case ch := <-b.snapshot:
|
||||
ch <- nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logger returns a [logger.Logf] to which logs related to bus activity should be written.
|
||||
func (b *Bus) logger() logger.Logf { return b.logf }
|
||||
|
||||
func (b *Bus) dest(t reflect.Type) []*subscribeState {
|
||||
b.topicsMu.Lock()
|
||||
defer b.topicsMu.Unlock()
|
||||
return b.topics[t]
|
||||
}
|
||||
|
||||
func (b *Bus) shouldPublish(t reflect.Type) bool {
|
||||
if b.routeDebug.active() {
|
||||
return true
|
||||
}
|
||||
|
||||
b.topicsMu.Lock()
|
||||
defer b.topicsMu.Unlock()
|
||||
return len(b.topics[t]) > 0
|
||||
}
|
||||
|
||||
func (b *Bus) listClients() []*Client {
|
||||
b.clientsMu.Lock()
|
||||
defer b.clientsMu.Unlock()
|
||||
return b.clients.Slice()
|
||||
}
|
||||
|
||||
func (b *Bus) snapshotPublishQueue() []PublishedEvent {
|
||||
resp := make(chan []PublishedEvent)
|
||||
select {
|
||||
case b.snapshot <- resp:
|
||||
return <-resp
|
||||
case <-b.router.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) subscribe(t reflect.Type, q *subscribeState) (cancel func()) {
|
||||
b.topicsMu.Lock()
|
||||
defer b.topicsMu.Unlock()
|
||||
b.topics[t] = append(b.topics[t], q)
|
||||
return func() {
|
||||
b.unsubscribe(t, q)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) unsubscribe(t reflect.Type, q *subscribeState) {
|
||||
b.topicsMu.Lock()
|
||||
defer b.topicsMu.Unlock()
|
||||
// Topic slices are accessed by pump without holding a lock, so we
|
||||
// have to replace the entire slice when unsubscribing.
|
||||
// Unsubscribing should be infrequent enough that this won't
|
||||
// matter.
|
||||
i := slices.Index(b.topics[t], q)
|
||||
if i < 0 {
|
||||
return
|
||||
}
|
||||
b.topics[t] = slices.Delete(slices.Clone(b.topics[t]), i, i+1)
|
||||
}
|
||||
|
||||
// A worker runs a worker goroutine and helps coordinate its shutdown.
|
||||
type worker struct {
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
stopped chan struct{}
|
||||
}
|
||||
|
||||
// runWorker creates a worker goroutine running fn. The context passed
|
||||
// to fn is canceled by [worker.Stop].
|
||||
func runWorker(fn func(context.Context)) *worker {
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
ret := &worker{
|
||||
ctx: ctx,
|
||||
stop: stop,
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
go ret.run(fn)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (w *worker) run(fn func(context.Context)) {
|
||||
defer close(w.stopped)
|
||||
fn(w.ctx)
|
||||
}
|
||||
|
||||
// Stop signals the worker goroutine to shut down.
|
||||
func (w *worker) Stop() { w.stop() }
|
||||
|
||||
// Done returns a channel that is closed when the worker goroutine
|
||||
// exits.
|
||||
func (w *worker) Done() <-chan struct{} { return w.stopped }
|
||||
|
||||
// Wait waits until the worker goroutine has exited.
|
||||
func (w *worker) Wait() { <-w.stopped }
|
||||
|
||||
// StopAndWait signals the worker goroutine to shut down, then waits
|
||||
// for it to exit.
|
||||
func (w *worker) StopAndWait() {
|
||||
w.stop()
|
||||
<-w.stopped
|
||||
}
|
||||
|
||||
// stopFlag is a value that can be watched for a notification. The
|
||||
// zero value is ready for use.
|
||||
//
|
||||
// The flag is notified by running [stopFlag.Stop]. Stop can be called
|
||||
// multiple times. Upon the first call to Stop, [stopFlag.Done] is
|
||||
// closed, all pending [stopFlag.Wait] calls return, and future Wait
|
||||
// calls return immediately.
|
||||
//
|
||||
// A stopFlag can only notify once, and is intended for use as a
|
||||
// one-way shutdown signal that's lighter than a cancellable
|
||||
// context.Context.
|
||||
type stopFlag struct {
|
||||
// guards the lazy construction of stopped, and the value of
|
||||
// alreadyStopped.
|
||||
mu syncs.Mutex
|
||||
stopped chan struct{}
|
||||
alreadyStopped bool
|
||||
}
|
||||
|
||||
func (s *stopFlag) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.alreadyStopped {
|
||||
return
|
||||
}
|
||||
s.alreadyStopped = true
|
||||
if s.stopped == nil {
|
||||
s.stopped = make(chan struct{})
|
||||
}
|
||||
close(s.stopped)
|
||||
}
|
||||
|
||||
func (s *stopFlag) Done() <-chan struct{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.stopped == nil {
|
||||
s.stopped = make(chan struct{})
|
||||
}
|
||||
return s.stopped
|
||||
}
|
||||
|
||||
func (s *stopFlag) Wait() {
|
||||
<-s.Done()
|
||||
}
|
||||
182
vendor/tailscale.com/util/eventbus/client.go
generated
vendored
Normal file
182
vendor/tailscale.com/util/eventbus/client.go
generated
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// A Client can publish and subscribe to events on its attached
|
||||
// bus. See [Publish] to publish events, and [Subscribe] to receive
|
||||
// events.
|
||||
//
|
||||
// Subscribers that share the same client receive events one at a
|
||||
// time, in the order they were published.
|
||||
type Client struct {
|
||||
name string
|
||||
bus *Bus
|
||||
publishDebug hook[PublishedEvent]
|
||||
|
||||
mu syncs.Mutex
|
||||
pub set.Set[publisher]
|
||||
sub *subscribeState // Lazily created on first subscribe
|
||||
stop stopFlag // signaled on Close
|
||||
}
|
||||
|
||||
func (c *Client) Name() string { return c.name }
|
||||
|
||||
func (c *Client) logger() logger.Logf { return c.bus.logger() }
|
||||
|
||||
// Close closes the client. It implicitly closes all publishers and
|
||||
// subscribers obtained from this client.
|
||||
func (c *Client) Close() {
|
||||
var (
|
||||
pub set.Set[publisher]
|
||||
sub *subscribeState
|
||||
)
|
||||
|
||||
c.mu.Lock()
|
||||
pub, c.pub = c.pub, nil
|
||||
sub, c.sub = c.sub, nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if sub != nil {
|
||||
sub.close()
|
||||
}
|
||||
for p := range pub {
|
||||
p.Close()
|
||||
}
|
||||
c.stop.Stop()
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool { return c.pub == nil && c.sub == nil }
|
||||
|
||||
// Done returns a channel that is closed when [Client.Close] is called.
|
||||
// The channel is closed after all the publishers and subscribers governed by
|
||||
// the client have been closed.
|
||||
func (c *Client) Done() <-chan struct{} { return c.stop.Done() }
|
||||
|
||||
func (c *Client) snapshotSubscribeQueue() []DeliveredEvent {
|
||||
return c.peekSubscribeState().snapshotQueue()
|
||||
}
|
||||
|
||||
func (c *Client) peekSubscribeState() *subscribeState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.sub
|
||||
}
|
||||
|
||||
func (c *Client) publishTypes() []reflect.Type {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
ret := make([]reflect.Type, 0, len(c.pub))
|
||||
for pub := range c.pub {
|
||||
ret = append(ret, pub.publishType())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Client) subscribeTypes() []reflect.Type {
|
||||
return c.peekSubscribeState().subscribeTypes()
|
||||
}
|
||||
|
||||
func (c *Client) subscribeState() *subscribeState {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.subscribeStateLocked()
|
||||
}
|
||||
|
||||
func (c *Client) subscribeStateLocked() *subscribeState {
|
||||
if c.sub == nil {
|
||||
c.sub = newSubscribeState(c)
|
||||
}
|
||||
return c.sub
|
||||
}
|
||||
|
||||
func (c *Client) addPublisher(pub publisher) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.isClosed() {
|
||||
panic("cannot Publish on a closed client")
|
||||
}
|
||||
c.pub.Add(pub)
|
||||
}
|
||||
|
||||
func (c *Client) deletePublisher(pub publisher) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.pub.Delete(pub)
|
||||
}
|
||||
|
||||
func (c *Client) addSubscriber(t reflect.Type, s *subscribeState) {
|
||||
c.bus.subscribe(t, s)
|
||||
}
|
||||
|
||||
func (c *Client) deleteSubscriber(t reflect.Type, s *subscribeState) {
|
||||
c.bus.unsubscribe(t, s)
|
||||
}
|
||||
|
||||
func (c *Client) publish() chan<- PublishedEvent {
|
||||
return c.bus.write
|
||||
}
|
||||
|
||||
func (c *Client) shouldPublish(t reflect.Type) bool {
|
||||
return c.publishDebug.active() || c.bus.shouldPublish(t)
|
||||
}
|
||||
|
||||
// Subscribe requests delivery of events of type T through the given client.
|
||||
// It panics if c already has a subscriber for type T, or if c is closed.
|
||||
func Subscribe[T any](c *Client) *Subscriber[T] {
|
||||
// Hold the client lock throughout the subscription process so that a caller
|
||||
// attempting to subscribe on a closed client will get a useful diagnostic
|
||||
// instead of a random panic from inside the subscriber plumbing.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// The caller should not race subscriptions with close, give them a useful
|
||||
// diagnostic at the call site.
|
||||
if c.isClosed() {
|
||||
panic("cannot Subscribe on a closed client")
|
||||
}
|
||||
|
||||
r := c.subscribeStateLocked()
|
||||
s := newSubscriber[T](r, logfForCaller(c.logger()))
|
||||
r.addSubscriber(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// SubscribeFunc is like [Subscribe], but calls the provided func for each
|
||||
// event of type T.
|
||||
//
|
||||
// A SubscriberFunc calls f synchronously from the client's goroutine.
|
||||
// This means the callback must not block for an extended period of time,
|
||||
// as this will block the subscriber and slow event processing for all
|
||||
// subscriptions on c.
|
||||
func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// The caller should not race subscriptions with close, give them a useful
|
||||
// diagnostic at the call site.
|
||||
if c.isClosed() {
|
||||
panic("cannot SubscribeFunc on a closed client")
|
||||
}
|
||||
|
||||
r := c.subscribeStateLocked()
|
||||
s := newSubscriberFunc[T](r, f, logfForCaller(c.logger()))
|
||||
r.addSubscriber(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// Publish returns a publisher for event type T using the given client.
|
||||
// It panics if c is closed.
|
||||
func Publish[T any](c *Client) *Publisher[T] {
|
||||
p := newPublisher[T](c)
|
||||
c.addPublisher(p)
|
||||
return p
|
||||
}
|
||||
242
vendor/tailscale.com/util/eventbus/debug.go
generated
vendored
Normal file
242
vendor/tailscale.com/util/eventbus/debug.go
generated
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// slowSubscriberTimeout is a timeout after which a subscriber that does not
|
||||
// accept a pending event will be flagged as being slow.
|
||||
const slowSubscriberTimeout = 5 * time.Second
|
||||
|
||||
// A Debugger offers access to a bus's privileged introspection and
|
||||
// debugging facilities.
|
||||
//
|
||||
// The debugger's functionality is intended for humans and their tools
|
||||
// to examine and troubleshoot bus clients, and should not be used in
|
||||
// normal codepaths.
|
||||
//
|
||||
// In particular, the debugger provides access to information that is
|
||||
// deliberately withheld from bus clients to encourage more robust and
|
||||
// maintainable code - for example, the sender of an event, or the
|
||||
// event streams of other clients. Please don't use the debugger to
|
||||
// circumvent these restrictions for purposes other than debugging.
|
||||
type Debugger struct {
|
||||
bus *Bus
|
||||
}
|
||||
|
||||
// Clients returns a list of all clients attached to the bus.
|
||||
func (d *Debugger) Clients() []*Client {
|
||||
ret := d.bus.listClients()
|
||||
slices.SortFunc(ret, func(a, b *Client) int {
|
||||
return cmp.Compare(a.Name(), b.Name())
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// PublishQueue returns the contents of the publish queue.
|
||||
//
|
||||
// The publish queue contains events that have been accepted by the
|
||||
// bus from Publish() calls, but have not yet been routed to relevant
|
||||
// subscribers.
|
||||
//
|
||||
// This queue is expected to be almost empty in normal operation. A
|
||||
// full publish queue indicates that a slow subscriber downstream is
|
||||
// causing backpressure and stalling the bus.
|
||||
func (d *Debugger) PublishQueue() []PublishedEvent {
|
||||
return d.bus.snapshotPublishQueue()
|
||||
}
|
||||
|
||||
// checkClient verifies that client is attached to the same bus as the
|
||||
// Debugger, and panics if not.
|
||||
func (d *Debugger) checkClient(client *Client) {
|
||||
if client.bus != d.bus {
|
||||
panic(fmt.Errorf("SubscribeQueue given client belonging to wrong bus"))
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeQueue returns the contents of the given client's subscribe
|
||||
// queue.
|
||||
//
|
||||
// The subscribe queue contains events that are to be delivered to the
|
||||
// client, but haven't yet been handed off to the relevant
|
||||
// [Subscriber].
|
||||
//
|
||||
// This queue is expected to be almost empty in normal operation. A
|
||||
// full subscribe queue indicates that the client is accepting events
|
||||
// too slowly, and may be causing the rest of the bus to stall.
|
||||
func (d *Debugger) SubscribeQueue(client *Client) []DeliveredEvent {
|
||||
d.checkClient(client)
|
||||
return client.snapshotSubscribeQueue()
|
||||
}
|
||||
|
||||
// WatchBus streams information about all events passing through the
|
||||
// bus.
|
||||
//
|
||||
// Monitored events are delivered in the bus's global publication
|
||||
// order (see "Concurrency properties" in the package docs).
|
||||
//
|
||||
// The caller must consume monitoring events promptly to avoid
|
||||
// stalling the bus (see "Expected subscriber behavior" in the package
|
||||
// docs).
|
||||
func (d *Debugger) WatchBus() *Subscriber[RoutedEvent] {
|
||||
return newMonitor(d.bus.routeDebug.add)
|
||||
}
|
||||
|
||||
// WatchPublish streams information about all events published by the
|
||||
// given client.
|
||||
//
|
||||
// Monitored events are delivered in the bus's global publication
|
||||
// order (see "Concurrency properties" in the package docs).
|
||||
//
|
||||
// The caller must consume monitoring events promptly to avoid
|
||||
// stalling the bus (see "Expected subscriber behavior" in the package
|
||||
// docs).
|
||||
func (d *Debugger) WatchPublish(client *Client) *Subscriber[PublishedEvent] {
|
||||
d.checkClient(client)
|
||||
return newMonitor(client.publishDebug.add)
|
||||
}
|
||||
|
||||
// WatchSubscribe streams information about all events received by the
|
||||
// given client.
|
||||
//
|
||||
// Monitored events are delivered in the bus's global publication
|
||||
// order (see "Concurrency properties" in the package docs).
|
||||
//
|
||||
// The caller must consume monitoring events promptly to avoid
|
||||
// stalling the bus (see "Expected subscriber behavior" in the package
|
||||
// docs).
|
||||
func (d *Debugger) WatchSubscribe(client *Client) *Subscriber[DeliveredEvent] {
|
||||
d.checkClient(client)
|
||||
return newMonitor(client.subscribeState().debug.add)
|
||||
}
|
||||
|
||||
// PublishTypes returns the list of types being published by client.
|
||||
//
|
||||
// The returned types are those for which the client has obtained a
|
||||
// [Publisher]. The client may not have ever sent the type in
|
||||
// question.
|
||||
func (d *Debugger) PublishTypes(client *Client) []reflect.Type {
|
||||
d.checkClient(client)
|
||||
return client.publishTypes()
|
||||
}
|
||||
|
||||
// SubscribeTypes returns the list of types being subscribed to by
|
||||
// client.
|
||||
//
|
||||
// The returned types are those for which the client has obtained a
|
||||
// [Subscriber]. The client may not have ever received the type in
|
||||
// question, and here may not be any publishers of the type.
|
||||
func (d *Debugger) SubscribeTypes(client *Client) []reflect.Type {
|
||||
d.checkClient(client)
|
||||
return client.subscribeTypes()
|
||||
}
|
||||
|
||||
// A hook collects hook functions that can be run as a group.
|
||||
type hook[T any] struct {
|
||||
syncs.Mutex
|
||||
fns []hookFn[T]
|
||||
}
|
||||
|
||||
var hookID atomic.Uint64
|
||||
|
||||
// add registers fn to be called when the hook is run. Returns an
|
||||
// unregistration function that removes fn from the hook when called.
|
||||
func (h *hook[T]) add(fn func(T)) (remove func()) {
|
||||
id := hookID.Add(1)
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.fns = append(h.fns, hookFn[T]{id, fn})
|
||||
return func() { h.remove(id) }
|
||||
}
|
||||
|
||||
// remove removes the function with the given ID from the hook.
|
||||
func (h *hook[T]) remove(id uint64) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
h.fns = slices.DeleteFunc(h.fns, func(f hookFn[T]) bool { return f.ID == id })
|
||||
}
|
||||
|
||||
// active reports whether any functions are registered with the
|
||||
// hook. This can be used to skip expensive work when the hook is
|
||||
// inactive.
|
||||
func (h *hook[T]) active() bool {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
return len(h.fns) > 0
|
||||
}
|
||||
|
||||
// run calls all registered functions with the value v.
|
||||
func (h *hook[T]) run(v T) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
for _, fn := range h.fns {
|
||||
fn.Fn(v)
|
||||
}
|
||||
}
|
||||
|
||||
type hookFn[T any] struct {
|
||||
ID uint64
|
||||
Fn func(T)
|
||||
}
|
||||
|
||||
// DebugEvent is a representation of an event used for debug clients.
|
||||
type DebugEvent struct {
|
||||
Count int
|
||||
Type string
|
||||
From string
|
||||
To []string
|
||||
Event any
|
||||
}
|
||||
|
||||
// DebugTopics provides the JSON encoding as a wrapper for a collection of [DebugTopic].
|
||||
type DebugTopics struct {
|
||||
Topics []DebugTopic
|
||||
}
|
||||
|
||||
// DebugTopic provides the JSON encoding of publishers and subscribers for a
|
||||
// given topic.
|
||||
type DebugTopic struct {
|
||||
Name string
|
||||
Publisher string
|
||||
Subscribers []string
|
||||
}
|
||||
|
||||
// logfForCaller returns a [logger.Logf] that prefixes its output with the
|
||||
// package, filename, and line number of the caller's caller.
|
||||
// If logf == nil, it returns [logger.Discard].
|
||||
// If the caller location could not be determined, it returns logf unmodified.
|
||||
func logfForCaller(logf logger.Logf) logger.Logf {
|
||||
if logf == nil {
|
||||
return logger.Discard
|
||||
}
|
||||
pc, fpath, line, _ := runtime.Caller(2) // +1 for my caller, +1 for theirs
|
||||
if f := runtime.FuncForPC(pc); f != nil {
|
||||
return logger.WithPrefix(logf, fmt.Sprintf("%s %s:%d: ", funcPackageName(f.Name()), filepath.Base(fpath), line))
|
||||
}
|
||||
return logf
|
||||
}
|
||||
|
||||
func funcPackageName(funcName string) string {
|
||||
ls := max(strings.LastIndex(funcName, "/"), 0)
|
||||
for {
|
||||
i := strings.LastIndex(funcName, ".")
|
||||
if i <= ls {
|
||||
return funcName
|
||||
}
|
||||
funcName = funcName[:i]
|
||||
}
|
||||
}
|
||||
240
vendor/tailscale.com/util/eventbus/debughttp.go
generated
vendored
Normal file
240
vendor/tailscale.com/util/eventbus/debughttp.go
generated
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !ts_omit_debugeventbus
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
type httpDebugger struct {
|
||||
*Debugger
|
||||
}
|
||||
|
||||
func (d *Debugger) RegisterHTTP(td *tsweb.DebugHandler) {
|
||||
dh := httpDebugger{d}
|
||||
td.Handle("bus", "Event bus", dh)
|
||||
td.HandleSilent("bus/monitor", http.HandlerFunc(dh.serveMonitor))
|
||||
td.HandleSilent("bus/style.css", serveStatic("style.css"))
|
||||
td.HandleSilent("bus/htmx.min.js", serveStatic("htmx.min.js.gz"))
|
||||
td.HandleSilent("bus/htmx-websocket.min.js", serveStatic("htmx-websocket.min.js.gz"))
|
||||
}
|
||||
|
||||
//go:embed assets/*.html
|
||||
var templatesSrc embed.FS
|
||||
|
||||
var templates = sync.OnceValue(func() *template.Template {
|
||||
d, err := fs.Sub(templatesSrc, "assets")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("getting eventbus debughttp templates subdir: %w", err))
|
||||
}
|
||||
ret := template.New("").Funcs(map[string]any{
|
||||
"prettyPrintStruct": prettyPrintStruct,
|
||||
})
|
||||
return template.Must(ret.ParseFS(d, "*"))
|
||||
})
|
||||
|
||||
//go:generate go run fetch-htmx.go
|
||||
|
||||
//go:embed assets/*.css assets/*.min.js.gz
|
||||
var static embed.FS
|
||||
|
||||
func serveStatic(name string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".css"):
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
case strings.HasSuffix(name, ".min.js.gz"):
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
case strings.HasSuffix(name, ".js"):
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
default:
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := static.Open(filepath.Join("assets", name))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("opening asset: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
http.Error(w, fmt.Sprintf("serving asset: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func render(w http.ResponseWriter, name string, data any) {
|
||||
err := templates().ExecuteTemplate(w, name+".html", data)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("rendering template: %v", err)
|
||||
log.Print(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h httpDebugger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
type clientInfo struct {
|
||||
*Client
|
||||
Publish []reflect.Type
|
||||
Subscribe []reflect.Type
|
||||
}
|
||||
type typeInfo struct {
|
||||
reflect.Type
|
||||
Publish []*Client
|
||||
Subscribe []*Client
|
||||
}
|
||||
type info struct {
|
||||
*Debugger
|
||||
Clients map[string]*clientInfo
|
||||
Types map[string]*typeInfo
|
||||
}
|
||||
|
||||
data := info{
|
||||
Debugger: h.Debugger,
|
||||
Clients: map[string]*clientInfo{},
|
||||
Types: map[string]*typeInfo{},
|
||||
}
|
||||
|
||||
getTypeInfo := func(t reflect.Type) *typeInfo {
|
||||
if data.Types[t.Name()] == nil {
|
||||
data.Types[t.Name()] = &typeInfo{
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
return data.Types[t.Name()]
|
||||
}
|
||||
|
||||
for _, c := range h.Clients() {
|
||||
ci := &clientInfo{
|
||||
Client: c,
|
||||
Publish: h.PublishTypes(c),
|
||||
Subscribe: h.SubscribeTypes(c),
|
||||
}
|
||||
slices.SortFunc(ci.Publish, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) })
|
||||
slices.SortFunc(ci.Subscribe, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) })
|
||||
data.Clients[c.Name()] = ci
|
||||
|
||||
for _, t := range ci.Publish {
|
||||
ti := getTypeInfo(t)
|
||||
ti.Publish = append(ti.Publish, c)
|
||||
}
|
||||
for _, t := range ci.Subscribe {
|
||||
ti := getTypeInfo(t)
|
||||
ti.Subscribe = append(ti.Subscribe, c)
|
||||
}
|
||||
}
|
||||
|
||||
render(w, "main", data)
|
||||
}
|
||||
|
||||
func (h httpDebugger) serveMonitor(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
h.serveMonitorStream(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
render(w, "monitor", nil)
|
||||
}
|
||||
|
||||
func (h httpDebugger) serveMonitorStream(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
wsCtx := conn.CloseRead(r.Context())
|
||||
|
||||
mon := h.WatchBus()
|
||||
defer mon.Close()
|
||||
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-wsCtx.Done():
|
||||
return
|
||||
case <-mon.Done():
|
||||
return
|
||||
case event := <-mon.Events():
|
||||
msg, err := conn.Writer(r.Context(), websocket.MessageText)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data := map[string]any{
|
||||
"Count": i,
|
||||
"Type": reflect.TypeOf(event.Event),
|
||||
"Event": event,
|
||||
}
|
||||
i++
|
||||
if err := templates().ExecuteTemplate(msg, "event.html", data); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if err := msg.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prettyPrintStruct(t reflect.Type) string {
|
||||
if t.Kind() != reflect.Struct {
|
||||
return t.String()
|
||||
}
|
||||
var rec func(io.Writer, int, reflect.Type)
|
||||
rec = func(out io.Writer, indent int, t reflect.Type) {
|
||||
ind := strings.Repeat(" ", indent)
|
||||
fmt.Fprintf(out, "%s", t.String())
|
||||
fs := collectFields(t)
|
||||
if len(fs) > 0 {
|
||||
io.WriteString(out, " {\n")
|
||||
for _, f := range fs {
|
||||
fmt.Fprintf(out, "%s %s ", ind, f.Name)
|
||||
if f.Type.Kind() == reflect.Struct {
|
||||
rec(out, indent+1, f.Type)
|
||||
} else {
|
||||
fmt.Fprint(out, f.Type)
|
||||
}
|
||||
io.WriteString(out, "\n")
|
||||
}
|
||||
fmt.Fprintf(out, "%s}", ind)
|
||||
}
|
||||
}
|
||||
|
||||
var ret bytes.Buffer
|
||||
rec(&ret, 0, t)
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func collectFields(t reflect.Type) (ret []reflect.StructField) {
|
||||
for _, f := range reflect.VisibleFields(t) {
|
||||
if !f.IsExported() {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, f)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
10
vendor/tailscale.com/util/eventbus/debughttp_off.go
generated
vendored
Normal file
10
vendor/tailscale.com/util/eventbus/debughttp_off.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || android || ts_omit_debugeventbus
|
||||
|
||||
package eventbus
|
||||
|
||||
type tswebDebugHandler = any // actually *tsweb.DebugHandler; any to avoid import tsweb with ts_omit_debugeventbus
|
||||
|
||||
func (*Debugger) RegisterHTTP(td tswebDebugHandler) {}
|
||||
102
vendor/tailscale.com/util/eventbus/doc.go
generated
vendored
Normal file
102
vendor/tailscale.com/util/eventbus/doc.go
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package eventbus provides an in-process event bus.
|
||||
//
|
||||
// An event bus connects publishers of typed events with subscribers
|
||||
// interested in those events. Typically, there is one global event
|
||||
// bus per process.
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// To send or receive events, first use [Bus.Client] to register with
|
||||
// the bus. Clients should register with a human-readable name that
|
||||
// identifies the code using the client, to aid in debugging.
|
||||
//
|
||||
// To publish events, use [Publish] on a Client to get a typed
|
||||
// publisher for your event type, then call [Publisher.Publish] as
|
||||
// needed. If your event is expensive to construct, you can optionally
|
||||
// use [Publisher.ShouldPublish] to skip the work if nobody is
|
||||
// listening for the event.
|
||||
//
|
||||
// To receive events, use [Subscribe] to get a typed subscriber for
|
||||
// each event type you're interested in. Receive the events themselves
|
||||
// by selecting over all your [Subscriber.Events] channels, as well as
|
||||
// [Subscriber.Done] for shutdown notifications.
|
||||
//
|
||||
// # Concurrency properties
|
||||
//
|
||||
// The bus serializes all published events across all publishers, and
|
||||
// preserves that ordering when delivering to subscribers that are
|
||||
// attached to the same Client. In more detail:
|
||||
//
|
||||
// - An event is published to the bus at some instant between the
|
||||
// start and end of the call to [Publisher.Publish].
|
||||
// - Two events cannot be published at the same instant, and so are
|
||||
// totally ordered by their publication time. Given two events E1
|
||||
// and E2, either E1 happens before E2, or E2 happens before E1.
|
||||
// - Clients dispatch events to their Subscribers in publication
|
||||
// order: if E1 happens before E2, the client always delivers E1
|
||||
// before E2.
|
||||
// - Clients do not synchronize subscriptions with each other: given
|
||||
// clients C1 and C2, both subscribed to events E1 and E2, C1 may
|
||||
// deliver both E1 and E2 before C2 delivers E1.
|
||||
//
|
||||
// Less formally: there is one true timeline of all published events.
|
||||
// If you make a Client and subscribe to events, you will receive
|
||||
// events one at a time, in the same order as the one true
|
||||
// timeline. You will "skip over" events you didn't subscribe to, but
|
||||
// your view of the world always moves forward in time, never
|
||||
// backwards, and you will observe events in the same order as
|
||||
// everyone else.
|
||||
//
|
||||
// However, you cannot assume that what your client see as "now" is
|
||||
// the same as what other clients. They may be further behind you in
|
||||
// working through the timeline, or running ahead of you. This means
|
||||
// you should be careful about reaching out to another component
|
||||
// directly after receiving an event, as its view of the world may not
|
||||
// yet (or ever) be exactly consistent with yours.
|
||||
//
|
||||
// To make your code more testable and understandable, you should try
|
||||
// to structure it following the actor model: you have some local
|
||||
// state over which you have authority, but your only way to interact
|
||||
// with state elsewhere in the program is to receive and process
|
||||
// events coming from elsewhere, or to emit events of your own.
|
||||
//
|
||||
// # Expected subscriber behavior
|
||||
//
|
||||
// Subscribers are expected to promptly receive their events on
|
||||
// [Subscriber.Events]. The bus has a small, fixed amount of internal
|
||||
// buffering, meaning that a slow subscriber will eventually cause
|
||||
// backpressure and block publication of all further events.
|
||||
//
|
||||
// In general, you should receive from your subscriber(s) in a loop,
|
||||
// and only do fast state updates within that loop. Any heavier work
|
||||
// should be offloaded to another goroutine.
|
||||
//
|
||||
// Causing publishers to block from backpressure is considered a bug
|
||||
// in the slow subscriber causing the backpressure, and should be
|
||||
// addressed there. Publishers should assume that Publish will not
|
||||
// block for extended periods of time, and should not make exceptional
|
||||
// effort to behave gracefully if they do get blocked.
|
||||
//
|
||||
// These blocking semantics are provisional and subject to
|
||||
// change. Please speak up if this causes development pain, so that we
|
||||
// can adapt the semantics to better suit our needs.
|
||||
//
|
||||
// # Debugging facilities
|
||||
//
|
||||
// The [Debugger], obtained through [Bus.Debugger], provides
|
||||
// introspection facilities to monitor events flowing through the bus,
|
||||
// and inspect publisher and subscriber state.
|
||||
//
|
||||
// Additionally, a debug command exists for monitoring the eventbus:
|
||||
//
|
||||
// tailscale debug daemon-bus-events
|
||||
//
|
||||
// # Testing facilities
|
||||
//
|
||||
// Helpers for testing code with the eventbus can be found in:
|
||||
//
|
||||
// eventbus/eventbustest
|
||||
package eventbus
|
||||
54
vendor/tailscale.com/util/eventbus/monitor.go
generated
vendored
Normal file
54
vendor/tailscale.com/util/eventbus/monitor.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import "tailscale.com/syncs"
|
||||
|
||||
// A Monitor monitors the execution of a goroutine processing events from a
|
||||
// [Client], allowing the caller to block until it is complete. The zero value
|
||||
// of m is valid; its Close and Wait methods return immediately, and its Done
|
||||
// method returns an already-closed channel.
|
||||
type Monitor struct {
|
||||
// These fields are immutable after initialization
|
||||
cli *Client
|
||||
done <-chan struct{}
|
||||
}
|
||||
|
||||
// Close closes the client associated with m and blocks until the processing
|
||||
// goroutine is complete.
|
||||
func (m Monitor) Close() {
|
||||
if m.cli == nil {
|
||||
return
|
||||
}
|
||||
m.cli.Close()
|
||||
<-m.done
|
||||
}
|
||||
|
||||
// Wait blocks until the goroutine monitored by m has finished executing, but
|
||||
// does not close the associated client. It is safe to call Wait repeatedly,
|
||||
// and from multiple concurrent goroutines.
|
||||
func (m Monitor) Wait() {
|
||||
if m.done == nil {
|
||||
return
|
||||
}
|
||||
<-m.done
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the monitored goroutine has
|
||||
// finished executing.
|
||||
func (m Monitor) Done() <-chan struct{} {
|
||||
if m.done == nil {
|
||||
return syncs.ClosedChan()
|
||||
}
|
||||
return m.done
|
||||
}
|
||||
|
||||
// Monitor executes f in a new goroutine attended by a [Monitor]. The caller
|
||||
// is responsible for waiting for the goroutine to complete, by calling either
|
||||
// [Monitor.Close] or [Monitor.Wait].
|
||||
func (c *Client) Monitor(f func(*Client)) Monitor {
|
||||
done := make(chan struct{})
|
||||
go func() { defer close(done); f(c) }()
|
||||
return Monitor{cli: c, done: done}
|
||||
}
|
||||
74
vendor/tailscale.com/util/eventbus/publish.go
generated
vendored
Normal file
74
vendor/tailscale.com/util/eventbus/publish.go
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// publisher is a uniformly typed wrapper around Publisher[T], so that
|
||||
// debugging facilities can look at active publishers.
|
||||
type publisher interface {
|
||||
publishType() reflect.Type
|
||||
Close()
|
||||
}
|
||||
|
||||
// A Publisher publishes typed events on a bus.
|
||||
type Publisher[T any] struct {
|
||||
client *Client
|
||||
stop stopFlag
|
||||
}
|
||||
|
||||
func newPublisher[T any](c *Client) *Publisher[T] {
|
||||
return &Publisher[T]{client: c}
|
||||
}
|
||||
|
||||
// Close closes the publisher.
|
||||
//
|
||||
// Calls to Publish after Close silently do nothing.
|
||||
//
|
||||
// If the Bus or Client from which the Publisher was created is closed,
|
||||
// the Publisher is implicitly closed and does not need to be closed
|
||||
// separately.
|
||||
func (p *Publisher[T]) Close() {
|
||||
// Just unblocks any active calls to Publish, no other
|
||||
// synchronization needed.
|
||||
p.stop.Stop()
|
||||
p.client.deletePublisher(p)
|
||||
}
|
||||
|
||||
func (p *Publisher[T]) publishType() reflect.Type {
|
||||
return reflect.TypeFor[T]()
|
||||
}
|
||||
|
||||
// Publish publishes event v on the bus.
|
||||
func (p *Publisher[T]) Publish(v T) {
|
||||
// Check for just a stopped publisher or bus before trying to
|
||||
// write, so that once closed Publish consistently does nothing.
|
||||
select {
|
||||
case <-p.stop.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
evt := PublishedEvent{
|
||||
Event: v,
|
||||
From: p.client,
|
||||
}
|
||||
|
||||
select {
|
||||
case p.client.publish() <- evt:
|
||||
case <-p.stop.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldPublish reports whether anyone is subscribed to the events
|
||||
// that this publisher emits.
|
||||
//
|
||||
// ShouldPublish can be used to skip expensive event construction if
|
||||
// nobody seems to care. Publishers must not assume that someone will
|
||||
// definitely receive an event if ShouldPublish returns true.
|
||||
func (p *Publisher[T]) ShouldPublish() bool {
|
||||
return p.client.shouldPublish(reflect.TypeFor[T]())
|
||||
}
|
||||
85
vendor/tailscale.com/util/eventbus/queue.go
generated
vendored
Normal file
85
vendor/tailscale.com/util/eventbus/queue.go
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// queue is an ordered queue of length up to capacity,
|
||||
// if capacity is non-zero. Otherwise it is unbounded.
|
||||
type queue[T any] struct {
|
||||
vals []T
|
||||
start int
|
||||
capacity int // zero means unbounded
|
||||
}
|
||||
|
||||
// canAppend reports whether a value can be appended to q.vals without
|
||||
// shifting values around.
|
||||
func (q *queue[T]) canAppend() bool {
|
||||
return q.capacity == 0 || cap(q.vals) < q.capacity || len(q.vals) < cap(q.vals)
|
||||
}
|
||||
|
||||
func (q *queue[T]) Full() bool {
|
||||
return q.start == 0 && !q.canAppend()
|
||||
}
|
||||
|
||||
func (q *queue[T]) Empty() bool {
|
||||
return q.start == len(q.vals)
|
||||
}
|
||||
|
||||
func (q *queue[T]) Len() int {
|
||||
return len(q.vals) - q.start
|
||||
}
|
||||
|
||||
// Add adds v to the end of the queue. Blocks until append can be
|
||||
// done.
|
||||
func (q *queue[T]) Add(v T) {
|
||||
if !q.canAppend() {
|
||||
if q.start == 0 {
|
||||
panic("Add on a full queue")
|
||||
}
|
||||
|
||||
// Slide remaining values back to the start of the array.
|
||||
n := copy(q.vals, q.vals[q.start:])
|
||||
toClear := len(q.vals) - n
|
||||
clear(q.vals[len(q.vals)-toClear:])
|
||||
q.vals = q.vals[:n]
|
||||
q.start = 0
|
||||
}
|
||||
|
||||
q.vals = append(q.vals, v)
|
||||
}
|
||||
|
||||
// Peek returns the first value in the queue, without removing it from
|
||||
// the queue, or nil if the queue is empty.
|
||||
func (q *queue[T]) Peek() T {
|
||||
if q.Empty() {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
return q.vals[q.start]
|
||||
}
|
||||
|
||||
// Drop discards the first value in the queue, if any.
|
||||
func (q *queue[T]) Drop() {
|
||||
if q.Empty() {
|
||||
return
|
||||
}
|
||||
|
||||
var zero T
|
||||
q.vals[q.start] = zero
|
||||
q.start++
|
||||
if q.Empty() {
|
||||
// Reset cursor to start of array, it's free to do.
|
||||
q.start = 0
|
||||
q.vals = q.vals[:0]
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot returns a copy of the queue's contents.
|
||||
func (q *queue[T]) Snapshot() []T {
|
||||
return slices.Clone(q.vals[q.start:])
|
||||
}
|
||||
356
vendor/tailscale.com/util/eventbus/subscribe.go
generated
vendored
Normal file
356
vendor/tailscale.com/util/eventbus/subscribe.go
generated
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cibuild"
|
||||
)
|
||||
|
||||
type DeliveredEvent struct {
|
||||
Event any
|
||||
From *Client
|
||||
To *Client
|
||||
}
|
||||
|
||||
// subscriber is a uniformly typed wrapper around Subscriber[T], so
|
||||
// that debugging facilities can look at active subscribers.
|
||||
type subscriber interface {
|
||||
subscribeType() reflect.Type
|
||||
// dispatch is a function that dispatches the head value in vals to
|
||||
// a subscriber, while also handling stop and incoming queue write
|
||||
// events.
|
||||
//
|
||||
// dispatch exists because of the strongly typed Subscriber[T]
|
||||
// wrapper around subscriptions: within the bus events are boxed in an
|
||||
// 'any', and need to be unpacked to their full type before delivery
|
||||
// to the subscriber. This involves writing to a strongly-typed
|
||||
// channel, so subscribeState cannot handle that dispatch by itself -
|
||||
// but if that strongly typed send blocks, we also need to keep
|
||||
// processing other potential sources of wakeups, which is how we end
|
||||
// up at this awkward type signature and sharing of internal state
|
||||
// through dispatch.
|
||||
dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool
|
||||
Close()
|
||||
}
|
||||
|
||||
// subscribeState handles dispatching of events received from a Bus.
|
||||
type subscribeState struct {
|
||||
client *Client
|
||||
|
||||
dispatcher *worker
|
||||
write chan DeliveredEvent
|
||||
snapshot chan chan []DeliveredEvent
|
||||
debug hook[DeliveredEvent]
|
||||
|
||||
outputsMu syncs.Mutex
|
||||
outputs map[reflect.Type]subscriber
|
||||
}
|
||||
|
||||
func newSubscribeState(c *Client) *subscribeState {
|
||||
ret := &subscribeState{
|
||||
client: c,
|
||||
write: make(chan DeliveredEvent),
|
||||
snapshot: make(chan chan []DeliveredEvent),
|
||||
outputs: map[reflect.Type]subscriber{},
|
||||
}
|
||||
ret.dispatcher = runWorker(ret.pump)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *subscribeState) pump(ctx context.Context) {
|
||||
var vals queue[DeliveredEvent]
|
||||
acceptCh := func() chan DeliveredEvent {
|
||||
if vals.Full() {
|
||||
return nil
|
||||
}
|
||||
return s.write
|
||||
}
|
||||
for {
|
||||
if !vals.Empty() {
|
||||
val := vals.Peek()
|
||||
sub := s.subscriberFor(val.Event)
|
||||
if sub == nil {
|
||||
// Raced with unsubscribe.
|
||||
vals.Drop()
|
||||
continue
|
||||
}
|
||||
if !sub.dispatch(ctx, &vals, acceptCh, s.snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.debug.active() {
|
||||
s.debug.run(DeliveredEvent{
|
||||
Event: val.Event,
|
||||
From: val.From,
|
||||
To: s.client,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Keep the cases in this select in sync with
|
||||
// Subscriber.dispatch and SubscriberFunc.dispatch below.
|
||||
// The only difference should be that this select doesn't deliver
|
||||
// queued values to anyone, and unconditionally accepts new values.
|
||||
select {
|
||||
case val := <-s.write:
|
||||
vals.Add(val)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case ch := <-s.snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscribeState) snapshotQueue() []DeliveredEvent {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp := make(chan []DeliveredEvent)
|
||||
select {
|
||||
case s.snapshot <- resp:
|
||||
return <-resp
|
||||
case <-s.dispatcher.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscribeState) subscribeTypes() []reflect.Type {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
ret := make([]reflect.Type, 0, len(s.outputs))
|
||||
for t := range s.outputs {
|
||||
ret = append(ret, t)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *subscribeState) addSubscriber(sub subscriber) {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
t := sub.subscribeType()
|
||||
if s.outputs[t] != nil {
|
||||
panic(fmt.Errorf("double subscription for event %s", t))
|
||||
}
|
||||
s.outputs[t] = sub
|
||||
s.client.addSubscriber(t, s)
|
||||
}
|
||||
|
||||
func (s *subscribeState) deleteSubscriber(t reflect.Type) {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
delete(s.outputs, t)
|
||||
s.client.deleteSubscriber(t, s)
|
||||
}
|
||||
|
||||
func (s *subscribeState) subscriberFor(val any) subscriber {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
return s.outputs[reflect.TypeOf(val)]
|
||||
}
|
||||
|
||||
// Close closes the subscribeState. It implicitly closes all Subscribers
|
||||
// linked to this state, and any pending events are discarded.
|
||||
func (s *subscribeState) close() {
|
||||
s.dispatcher.StopAndWait()
|
||||
|
||||
var subs map[reflect.Type]subscriber
|
||||
s.outputsMu.Lock()
|
||||
subs, s.outputs = s.outputs, nil
|
||||
s.outputsMu.Unlock()
|
||||
for _, sub := range subs {
|
||||
sub.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscribeState) closed() <-chan struct{} {
|
||||
return s.dispatcher.Done()
|
||||
}
|
||||
|
||||
// A Subscriber delivers one type of event from a [Client].
|
||||
// Events are sent to the [Subscriber.Events] channel.
|
||||
type Subscriber[T any] struct {
|
||||
stop stopFlag
|
||||
read chan T
|
||||
unregister func()
|
||||
logf logger.Logf
|
||||
slow *time.Timer // used to detect slow subscriber service
|
||||
}
|
||||
|
||||
func newSubscriber[T any](r *subscribeState, logf logger.Logf) *Subscriber[T] {
|
||||
slow := time.NewTimer(0)
|
||||
slow.Stop() // reset in dispatch
|
||||
return &Subscriber[T]{
|
||||
read: make(chan T),
|
||||
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
|
||||
logf: logf,
|
||||
slow: slow,
|
||||
}
|
||||
}
|
||||
|
||||
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
|
||||
ret := &Subscriber[T]{
|
||||
read: make(chan T, 100), // arbitrary, large
|
||||
}
|
||||
ret.unregister = attach(ret.monitor)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) subscribeType() reflect.Type {
|
||||
return reflect.TypeFor[T]()
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) monitor(debugEvent T) {
|
||||
select {
|
||||
case s.read <- debugEvent:
|
||||
case <-s.stop.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
|
||||
t := vals.Peek().Event.(T)
|
||||
|
||||
start := time.Now()
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
defer s.slow.Stop()
|
||||
|
||||
for {
|
||||
// Keep the cases in this select in sync with subscribeState.pump
|
||||
// above. The only difference should be that this select
|
||||
// delivers a value on s.read.
|
||||
select {
|
||||
case s.read <- t:
|
||||
vals.Drop()
|
||||
return true
|
||||
case val := <-acceptCh():
|
||||
vals.Add(val)
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case ch := <-snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
case <-s.slow.C:
|
||||
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Events returns a channel on which the subscriber's events are
|
||||
// delivered.
|
||||
func (s *Subscriber[T]) Events() <-chan T {
|
||||
return s.read
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the subscriber is
|
||||
// closed.
|
||||
func (s *Subscriber[T]) Done() <-chan struct{} {
|
||||
return s.stop.Done()
|
||||
}
|
||||
|
||||
// Close closes the Subscriber, indicating the caller no longer wishes
|
||||
// to receive this event type. After Close, receives on
|
||||
// [Subscriber.Events] block for ever.
|
||||
//
|
||||
// If the Bus from which the Subscriber was created is closed,
|
||||
// the Subscriber is implicitly closed and does not need to be closed
|
||||
// separately.
|
||||
func (s *Subscriber[T]) Close() {
|
||||
s.stop.Stop() // unblock receivers
|
||||
s.unregister()
|
||||
}
|
||||
|
||||
// A SubscriberFunc delivers one type of event from a [Client].
|
||||
// Events are forwarded synchronously to a function provided at construction.
|
||||
type SubscriberFunc[T any] struct {
|
||||
stop stopFlag
|
||||
read func(T)
|
||||
unregister func()
|
||||
logf logger.Logf
|
||||
slow *time.Timer // used to detect slow subscriber service
|
||||
}
|
||||
|
||||
func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *SubscriberFunc[T] {
|
||||
slow := time.NewTimer(0)
|
||||
slow.Stop() // reset in dispatch
|
||||
return &SubscriberFunc[T]{
|
||||
read: f,
|
||||
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
|
||||
logf: logf,
|
||||
slow: slow,
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the SubscriberFunc, indicating the caller no longer wishes to
|
||||
// receive this event type. After Close, no further events will be passed to
|
||||
// the callback.
|
||||
//
|
||||
// If the [Bus] from which s was created is closed, s is implicitly closed and
|
||||
// does not need to be closed separately.
|
||||
func (s *SubscriberFunc[T]) Close() { s.stop.Stop(); s.unregister() }
|
||||
|
||||
// subscribeType implements part of the subscriber interface.
|
||||
func (s *SubscriberFunc[T]) subscribeType() reflect.Type { return reflect.TypeFor[T]() }
|
||||
|
||||
// dispatch implements part of the subscriber interface.
|
||||
func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
|
||||
t := vals.Peek().Event.(T)
|
||||
callDone := make(chan struct{})
|
||||
go s.runCallback(t, callDone)
|
||||
|
||||
start := time.Now()
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
defer s.slow.Stop()
|
||||
|
||||
// Keep the cases in this select in sync with subscribeState.pump
|
||||
// above. The only difference should be that this select
|
||||
// delivers a value by calling s.read.
|
||||
for {
|
||||
select {
|
||||
case <-callDone:
|
||||
vals.Drop()
|
||||
return true
|
||||
case val := <-acceptCh():
|
||||
vals.Add(val)
|
||||
case <-ctx.Done():
|
||||
// Wait for the callback to be complete, but not forever.
|
||||
s.slow.Reset(5 * slowSubscriberTimeout)
|
||||
select {
|
||||
case <-s.slow.C:
|
||||
s.logf("giving up on subscriber for %T after %v at close", t, time.Since(start))
|
||||
if cibuild.On() {
|
||||
all := make([]byte, 2<<20)
|
||||
n := runtime.Stack(all, true)
|
||||
s.logf("goroutine stacks:\n%s", all[:n])
|
||||
}
|
||||
case <-callDone:
|
||||
}
|
||||
return false
|
||||
case ch := <-snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
case <-s.slow.C:
|
||||
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCallback invokes the callback on v and closes ch when it returns.
|
||||
// This should be run in a goroutine.
|
||||
func (s *SubscriberFunc[T]) runCallback(v T, ch chan struct{}) {
|
||||
defer close(ch)
|
||||
s.read(v)
|
||||
}
|
||||
57
vendor/tailscale.com/util/execqueue/execqueue.go
generated
vendored
57
vendor/tailscale.com/util/execqueue/execqueue.go
generated
vendored
@@ -7,11 +7,14 @@ package execqueue
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
type ExecQueue struct {
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
ctx context.Context // context.Background + closed on Shutdown
|
||||
cancel context.CancelFunc // closes ctx
|
||||
closed bool
|
||||
inFlight bool // whether a goroutine is running q.run
|
||||
doneWaiter chan struct{} // non-nil if waiter is waiting, then closed
|
||||
@@ -24,6 +27,7 @@ func (q *ExecQueue) Add(f func()) {
|
||||
if q.closed {
|
||||
return
|
||||
}
|
||||
q.initCtxLocked()
|
||||
if q.inFlight {
|
||||
q.queue = append(q.queue, f)
|
||||
} else {
|
||||
@@ -35,21 +39,21 @@ func (q *ExecQueue) Add(f func()) {
|
||||
// RunSync waits for the queue to be drained and then synchronously runs f.
|
||||
// It returns an error if the queue is closed before f is run or ctx expires.
|
||||
func (q *ExecQueue) RunSync(ctx context.Context, f func()) error {
|
||||
for {
|
||||
if err := q.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
q.mu.Lock()
|
||||
if q.inFlight {
|
||||
q.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
defer q.mu.Unlock()
|
||||
if q.closed {
|
||||
return errors.New("closed")
|
||||
}
|
||||
f()
|
||||
q.mu.Lock()
|
||||
q.initCtxLocked()
|
||||
shutdownCtx := q.ctx
|
||||
q.mu.Unlock()
|
||||
|
||||
ch := make(chan struct{})
|
||||
q.Add(f)
|
||||
q.Add(func() { close(ch) })
|
||||
select {
|
||||
case <-ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-shutdownCtx.Done():
|
||||
return errExecQueueShutdown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +83,35 @@ func (q *ExecQueue) Shutdown() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.closed = true
|
||||
if q.cancel != nil {
|
||||
q.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Wait waits for the queue to be empty.
|
||||
func (q *ExecQueue) initCtxLocked() {
|
||||
if q.ctx == nil {
|
||||
q.ctx, q.cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
var errExecQueueShutdown = errors.New("execqueue shut down")
|
||||
|
||||
// Wait waits for the queue to be empty or shut down.
|
||||
func (q *ExecQueue) Wait(ctx context.Context) error {
|
||||
q.mu.Lock()
|
||||
q.initCtxLocked()
|
||||
waitCh := q.doneWaiter
|
||||
if q.inFlight && waitCh == nil {
|
||||
waitCh = make(chan struct{})
|
||||
q.doneWaiter = waitCh
|
||||
}
|
||||
closed := q.closed
|
||||
shutdownCtx := q.ctx
|
||||
q.mu.Unlock()
|
||||
|
||||
if closed {
|
||||
return errExecQueueShutdown
|
||||
}
|
||||
if waitCh == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -98,6 +119,8 @@ func (q *ExecQueue) Wait(ctx context.Context) error {
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-shutdownCtx.Done():
|
||||
return errExecQueueShutdown
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
4
vendor/tailscale.com/util/goroutines/tracker.go
generated
vendored
4
vendor/tailscale.com/util/goroutines/tracker.go
generated
vendored
@@ -4,9 +4,9 @@
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ type Tracker struct {
|
||||
started atomic.Int64 // counter
|
||||
running atomic.Int64 // gauge
|
||||
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
onDone set.HandleSet[func()]
|
||||
}
|
||||
|
||||
|
||||
197
vendor/tailscale.com/util/httphdr/httphdr.go
generated
vendored
197
vendor/tailscale.com/util/httphdr/httphdr.go
generated
vendored
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package httphdr implements functionality for parsing and formatting
|
||||
// standard HTTP headers.
|
||||
package httphdr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Range is a range of bytes within some content.
|
||||
type Range struct {
|
||||
// Start is the starting offset.
|
||||
// It is zero if Length is negative; it must not be negative.
|
||||
Start int64
|
||||
// Length is the length of the content.
|
||||
// It is zero if the length extends to the end of the content.
|
||||
// It is negative if the length is relative to the end (e.g., last 5 bytes).
|
||||
Length int64
|
||||
}
|
||||
|
||||
// ows is optional whitespace.
|
||||
const ows = " \t" // per RFC 7230, section 3.2.3
|
||||
|
||||
// ParseRange parses a "Range" header per RFC 7233, section 3.
|
||||
// It only handles "Range" headers where the units is "bytes".
|
||||
// The "Range" header is usually only specified in GET requests.
|
||||
func ParseRange(hdr string) (ranges []Range, ok bool) {
|
||||
// Grammar per RFC 7233, appendix D:
|
||||
// Range = byte-ranges-specifier | other-ranges-specifier
|
||||
// byte-ranges-specifier = bytes-unit "=" byte-range-set
|
||||
// bytes-unit = "bytes"
|
||||
// byte-range-set =
|
||||
// *("," OWS)
|
||||
// (byte-range-spec | suffix-byte-range-spec)
|
||||
// *(OWS "," [OWS ( byte-range-spec | suffix-byte-range-spec )])
|
||||
// byte-range-spec = first-byte-pos "-" [last-byte-pos]
|
||||
// suffix-byte-range-spec = "-" suffix-length
|
||||
// We do not support other-ranges-specifier.
|
||||
// All other identifiers are 1*DIGIT.
|
||||
hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
|
||||
units, elems, hasUnits := strings.Cut(hdr, "=")
|
||||
elems = strings.TrimLeft(elems, ","+ows)
|
||||
for _, elem := range strings.Split(elems, ",") {
|
||||
elem = strings.Trim(elem, ows) // per RFC 7230, section 7
|
||||
switch {
|
||||
case strings.HasPrefix(elem, "-"): // i.e., "-" suffix-length
|
||||
n, ok := parseNumber(strings.TrimPrefix(elem, "-"))
|
||||
if !ok {
|
||||
return ranges, false
|
||||
}
|
||||
ranges = append(ranges, Range{0, -n})
|
||||
case strings.HasSuffix(elem, "-"): // i.e., first-byte-pos "-"
|
||||
n, ok := parseNumber(strings.TrimSuffix(elem, "-"))
|
||||
if !ok {
|
||||
return ranges, false
|
||||
}
|
||||
ranges = append(ranges, Range{n, 0})
|
||||
default: // i.e., first-byte-pos "-" last-byte-pos
|
||||
prefix, suffix, hasDash := strings.Cut(elem, "-")
|
||||
n, ok2 := parseNumber(prefix)
|
||||
m, ok3 := parseNumber(suffix)
|
||||
if !hasDash || !ok2 || !ok3 || m < n {
|
||||
return ranges, false
|
||||
}
|
||||
ranges = append(ranges, Range{n, m - n + 1})
|
||||
}
|
||||
}
|
||||
return ranges, units == "bytes" && hasUnits && len(ranges) > 0 // must see at least one element per RFC 7233, section 2.1
|
||||
}
|
||||
|
||||
// FormatRange formats a "Range" header per RFC 7233, section 3.
|
||||
// It only handles "Range" headers where the units is "bytes".
|
||||
// The "Range" header is usually only specified in GET requests.
|
||||
func FormatRange(ranges []Range) (hdr string, ok bool) {
|
||||
b := []byte("bytes=")
|
||||
for _, r := range ranges {
|
||||
switch {
|
||||
case r.Length > 0: // i.e., first-byte-pos "-" last-byte-pos
|
||||
if r.Start < 0 {
|
||||
return string(b), false
|
||||
}
|
||||
b = strconv.AppendUint(b, uint64(r.Start), 10)
|
||||
b = append(b, '-')
|
||||
b = strconv.AppendUint(b, uint64(r.Start+r.Length-1), 10)
|
||||
b = append(b, ',')
|
||||
case r.Length == 0: // i.e., first-byte-pos "-"
|
||||
if r.Start < 0 {
|
||||
return string(b), false
|
||||
}
|
||||
b = strconv.AppendUint(b, uint64(r.Start), 10)
|
||||
b = append(b, '-')
|
||||
b = append(b, ',')
|
||||
case r.Length < 0: // i.e., "-" suffix-length
|
||||
if r.Start != 0 {
|
||||
return string(b), false
|
||||
}
|
||||
b = append(b, '-')
|
||||
b = strconv.AppendUint(b, uint64(-r.Length), 10)
|
||||
b = append(b, ',')
|
||||
default:
|
||||
return string(b), false
|
||||
}
|
||||
}
|
||||
return string(bytes.TrimRight(b, ",")), len(ranges) > 0
|
||||
}
|
||||
|
||||
// ParseContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
|
||||
// It only handles "Content-Range" headers where the units is "bytes".
|
||||
// The "Content-Range" header is usually only specified in HTTP responses.
|
||||
//
|
||||
// If only the completeLength is specified, then start and length are both zero.
|
||||
//
|
||||
// Otherwise, the parses the start and length and the optional completeLength,
|
||||
// which is -1 if unspecified. The start is non-negative and the length is positive.
|
||||
func ParseContentRange(hdr string) (start, length, completeLength int64, ok bool) {
|
||||
// Grammar per RFC 7233, appendix D:
|
||||
// Content-Range = byte-content-range | other-content-range
|
||||
// byte-content-range = bytes-unit SP (byte-range-resp | unsatisfied-range)
|
||||
// bytes-unit = "bytes"
|
||||
// byte-range-resp = byte-range "/" (complete-length | "*")
|
||||
// unsatisfied-range = "*/" complete-length
|
||||
// byte-range = first-byte-pos "-" last-byte-pos
|
||||
// We do not support other-content-range.
|
||||
// All other identifiers are 1*DIGIT.
|
||||
hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
|
||||
suffix, hasUnits := strings.CutPrefix(hdr, "bytes ")
|
||||
suffix, unsatisfied := strings.CutPrefix(suffix, "*/")
|
||||
if unsatisfied { // i.e., unsatisfied-range
|
||||
n, ok := parseNumber(suffix)
|
||||
if !ok {
|
||||
return start, length, completeLength, false
|
||||
}
|
||||
completeLength = n
|
||||
} else { // i.e., byte-range "/" (complete-length | "*")
|
||||
prefix, suffix, hasDash := strings.Cut(suffix, "-")
|
||||
middle, suffix, hasSlash := strings.Cut(suffix, "/")
|
||||
n, ok0 := parseNumber(prefix)
|
||||
m, ok1 := parseNumber(middle)
|
||||
o, ok2 := parseNumber(suffix)
|
||||
if suffix == "*" {
|
||||
o, ok2 = -1, true
|
||||
}
|
||||
if !hasDash || !hasSlash || !ok0 || !ok1 || !ok2 || m < n || (o >= 0 && o <= m) {
|
||||
return start, length, completeLength, false
|
||||
}
|
||||
start = n
|
||||
length = m - n + 1
|
||||
completeLength = o
|
||||
}
|
||||
return start, length, completeLength, hasUnits
|
||||
}
|
||||
|
||||
// FormatContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
|
||||
// It only handles "Content-Range" headers where the units is "bytes".
|
||||
// The "Content-Range" header is usually only specified in HTTP responses.
|
||||
//
|
||||
// If start and length are non-positive, then it encodes just the completeLength,
|
||||
// which must be a non-negative value.
|
||||
//
|
||||
// Otherwise, it encodes the start and length as a byte-range,
|
||||
// and optionally emits the complete length if it is non-negative.
|
||||
// The length must be positive (as RFC 7233 uses inclusive end offsets).
|
||||
func FormatContentRange(start, length, completeLength int64) (hdr string, ok bool) {
|
||||
b := []byte("bytes ")
|
||||
switch {
|
||||
case start <= 0 && length <= 0 && completeLength >= 0: // i.e., unsatisfied-range
|
||||
b = append(b, "*/"...)
|
||||
b = strconv.AppendUint(b, uint64(completeLength), 10)
|
||||
ok = true
|
||||
case start >= 0 && length > 0: // i.e., byte-range "/" (complete-length | "*")
|
||||
b = strconv.AppendUint(b, uint64(start), 10)
|
||||
b = append(b, '-')
|
||||
b = strconv.AppendUint(b, uint64(start+length-1), 10)
|
||||
b = append(b, '/')
|
||||
if completeLength >= 0 {
|
||||
b = strconv.AppendUint(b, uint64(completeLength), 10)
|
||||
ok = completeLength >= start+length && start+length > 0
|
||||
} else {
|
||||
b = append(b, '*')
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return string(b), ok
|
||||
}
|
||||
|
||||
// parseNumber parses s as an unsigned decimal integer.
|
||||
// It parses according to the 1*DIGIT grammar, which allows leading zeros.
|
||||
func parseNumber(s string) (int64, bool) {
|
||||
suffix := strings.TrimLeft(s, "0123456789")
|
||||
prefix := s[:len(s)-len(suffix)]
|
||||
n, err := strconv.ParseInt(prefix, 10, 64)
|
||||
return n, suffix == "" && err == nil
|
||||
}
|
||||
130
vendor/tailscale.com/util/linuxfw/detector.go
generated
vendored
130
vendor/tailscale.com/util/linuxfw/detector.go
generated
vendored
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func detectFirewallMode(logf logger.Logf, prefHint string) FirewallMode {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
logf("GoKrazy should use nftables.")
|
||||
hostinfo.SetFirewallMode("nft-gokrazy")
|
||||
return FirewallModeNfTables
|
||||
}
|
||||
|
||||
mode := envknob.String("TS_DEBUG_FIREWALL_MODE")
|
||||
// If the envknob isn't set, fall back to the pref suggested by c2n or
|
||||
// nodeattrs.
|
||||
if mode == "" {
|
||||
mode = prefHint
|
||||
logf("using firewall mode pref %s", prefHint)
|
||||
} else if prefHint != "" {
|
||||
logf("TS_DEBUG_FIREWALL_MODE set, overriding firewall mode from %s to %s", prefHint, mode)
|
||||
}
|
||||
|
||||
var det linuxFWDetector
|
||||
if mode == "" {
|
||||
// We have no preference, so check if `iptables` is even available.
|
||||
_, err := det.iptDetect()
|
||||
if err != nil && errors.Is(err, exec.ErrNotFound) {
|
||||
logf("iptables not found: %v; falling back to nftables", err)
|
||||
mode = "nftables"
|
||||
}
|
||||
}
|
||||
|
||||
// We now use iptables as default and have "auto" and "nftables" as
|
||||
// options for people to test further.
|
||||
switch mode {
|
||||
case "auto":
|
||||
return pickFirewallModeFromInstalledRules(logf, det)
|
||||
case "nftables":
|
||||
hostinfo.SetFirewallMode("nft-forced")
|
||||
return FirewallModeNfTables
|
||||
case "iptables":
|
||||
hostinfo.SetFirewallMode("ipt-forced")
|
||||
default:
|
||||
logf("default choosing iptables")
|
||||
hostinfo.SetFirewallMode("ipt-default")
|
||||
}
|
||||
return FirewallModeIPTables
|
||||
}
|
||||
|
||||
// tableDetector abstracts helpers to detect the firewall mode.
|
||||
// It is implemented for testing purposes.
|
||||
type tableDetector interface {
|
||||
iptDetect() (int, error)
|
||||
nftDetect() (int, error)
|
||||
}
|
||||
|
||||
type linuxFWDetector struct{}
|
||||
|
||||
// iptDetect returns the number of iptables rules in the current namespace.
|
||||
func (l linuxFWDetector) iptDetect() (int, error) {
|
||||
return detectIptables()
|
||||
}
|
||||
|
||||
// nftDetect returns the number of nftables rules in the current namespace.
|
||||
func (l linuxFWDetector) nftDetect() (int, error) {
|
||||
return detectNetfilter()
|
||||
}
|
||||
|
||||
// pickFirewallModeFromInstalledRules returns the firewall mode to use based on
|
||||
// the environment and the system's capabilities.
|
||||
func pickFirewallModeFromInstalledRules(logf logger.Logf, det tableDetector) FirewallMode {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Reduce startup logging on gokrazy. There's no way to do iptables on
|
||||
// gokrazy anyway.
|
||||
return FirewallModeNfTables
|
||||
}
|
||||
iptAva, nftAva := true, true
|
||||
iptRuleCount, err := det.iptDetect()
|
||||
if err != nil {
|
||||
logf("detect iptables rule: %v", err)
|
||||
iptAva = false
|
||||
}
|
||||
nftRuleCount, err := det.nftDetect()
|
||||
if err != nil {
|
||||
logf("detect nftables rule: %v", err)
|
||||
nftAva = false
|
||||
}
|
||||
logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
|
||||
switch {
|
||||
case nftRuleCount > 0 && iptRuleCount == 0:
|
||||
logf("nftables is currently in use")
|
||||
hostinfo.SetFirewallMode("nft-inuse")
|
||||
return FirewallModeNfTables
|
||||
case iptRuleCount > 0 && nftRuleCount == 0:
|
||||
logf("iptables is currently in use")
|
||||
hostinfo.SetFirewallMode("ipt-inuse")
|
||||
return FirewallModeIPTables
|
||||
case nftAva:
|
||||
// if both iptables and nftables are available but
|
||||
// neither/both are currently used, use nftables.
|
||||
logf("nftables is available")
|
||||
hostinfo.SetFirewallMode("nft")
|
||||
return FirewallModeNfTables
|
||||
case iptAva:
|
||||
logf("iptables is available")
|
||||
hostinfo.SetFirewallMode("ipt")
|
||||
return FirewallModeIPTables
|
||||
default:
|
||||
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
|
||||
// runner which exists but won't do anything. Creating iptablesRunner errors only
|
||||
// if the iptables command is missing or doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll carry on.
|
||||
hostinfo.SetFirewallMode("ipt-fb")
|
||||
return FirewallModeIPTables
|
||||
}
|
||||
}
|
||||
142
vendor/tailscale.com/util/linuxfw/fake.go
generated
vendored
142
vendor/tailscale.com/util/linuxfw/fake.go
generated
vendored
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type fakeIPTables struct {
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
type fakeRule struct {
|
||||
table, chain string
|
||||
args []string
|
||||
}
|
||||
|
||||
func newFakeIPTables() *fakeIPTables {
|
||||
return &fakeIPTables{
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
"nat/PREROUTING": nil,
|
||||
"nat/OUTPUT": nil,
|
||||
"nat/POSTROUTING": nil,
|
||||
"mangle/FORWARD": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Insert(table, chain string, pos int, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if pos > len(rules)+1 {
|
||||
return fmt.Errorf("bad position %d in %s", pos, k)
|
||||
}
|
||||
rules = append(rules, "")
|
||||
copy(rules[pos:], rules[pos-1:])
|
||||
rules[pos-1] = strings.Join(args, " ")
|
||||
n.n[k] = rules
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Append(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
return n.Insert(table, chain, len(n.n[k])+1, args...)
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Exists(table, chain string, args ...string) (bool, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for _, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else {
|
||||
return false, fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
for i, rule := range rules {
|
||||
if rule == strings.Join(args, " ") {
|
||||
rules = append(rules[:i], rules[i+1:]...)
|
||||
n.n[k] = rules
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) List(table, chain string) ([]string, error) {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
return rules, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) ClearChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("exitcode:1")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) NewChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if _, ok := n.n[k]; ok {
|
||||
return fmt.Errorf("table/chain %s already exists", k)
|
||||
}
|
||||
n.n[k] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
||||
k := table + "/" + chain
|
||||
if rules, ok := n.n[k]; ok {
|
||||
if len(rules) != 0 {
|
||||
return fmt.Errorf("table/chain %s is not empty", k)
|
||||
}
|
||||
delete(n.n, k)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("unknown table/chain %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
func NewFakeIPTablesRunner() *iptablesRunner {
|
||||
ipt4 := newFakeIPTables()
|
||||
v6Available := false
|
||||
var ipt6 iptablesInterface
|
||||
if use6, err := strconv.ParseBool(os.Getenv("TS_TEST_FAKE_NETFILTER_6")); use6 || err != nil {
|
||||
ipt6 = newFakeIPTables()
|
||||
v6Available = true
|
||||
}
|
||||
|
||||
iptr := &iptablesRunner{ipt4, ipt6, v6Available, v6Available, v6Available}
|
||||
return iptr
|
||||
}
|
||||
39
vendor/tailscale.com/util/linuxfw/helpers.go
generated
vendored
39
vendor/tailscale.com/util/linuxfw/helpers.go
generated
vendored
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
func formatMaybePrintable(b []byte) string {
|
||||
// Remove a single trailing null, if any.
|
||||
if slicesx.LastEqual(b, 0) {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
|
||||
nonprintable := strings.IndexFunc(string(b), func(r rune) bool {
|
||||
return r > unicode.MaxASCII || !unicode.IsPrint(r)
|
||||
})
|
||||
if nonprintable >= 0 {
|
||||
return "<hex>" + hex.EncodeToString(b)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func formatPortRange(r [2]uint16) string {
|
||||
if r == [2]uint16{0, 65535} {
|
||||
return fmt.Sprintf(`any`)
|
||||
} else if r[0] == r[1] {
|
||||
return fmt.Sprintf(`%d`, r[0])
|
||||
}
|
||||
return fmt.Sprintf(`%d-%d`, r[0], r[1])
|
||||
}
|
||||
73
vendor/tailscale.com/util/linuxfw/iptables.go
generated
vendored
73
vendor/tailscale.com/util/linuxfw/iptables.go
generated
vendored
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// DebugNetfilter prints debug information about iptables rules to the
|
||||
// provided log function.
|
||||
func DebugIptables(logf logger.Logf) error {
|
||||
// unused.
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectIptables returns the number of iptables rules that are present in the
|
||||
// system, ignoring the default "ACCEPT" rule present in the standard iptables
|
||||
// chains.
|
||||
//
|
||||
// It only returns an error when there is no iptables binary, or when iptables -S
|
||||
// fails. In all other cases, it returns the number of non-default rules.
|
||||
//
|
||||
// If the iptables binary is not found, it returns an underlying exec.ErrNotFound
|
||||
// error.
|
||||
func detectIptables() (int, error) {
|
||||
// run "iptables -S" to get the list of rules using iptables
|
||||
// exec.Command returns an error if the binary is not found
|
||||
cmd := exec.Command("iptables", "-S")
|
||||
output, err := cmd.Output()
|
||||
ip6cmd := exec.Command("ip6tables", "-S")
|
||||
ip6output, ip6err := ip6cmd.Output()
|
||||
var allLines []string
|
||||
outputStr := string(output)
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
ip6outputStr := string(ip6output)
|
||||
ip6lines := strings.Split(ip6outputStr, "\n")
|
||||
switch {
|
||||
case err == nil && ip6err == nil:
|
||||
allLines = append(lines, ip6lines...)
|
||||
case err == nil && ip6err != nil:
|
||||
allLines = lines
|
||||
case err != nil && ip6err == nil:
|
||||
allLines = ip6lines
|
||||
default:
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeIPTables,
|
||||
Err: fmt.Errorf("iptables command run fail: %w", multierr.New(err, ip6err)),
|
||||
}
|
||||
}
|
||||
|
||||
// count the number of non-default rules
|
||||
count := 0
|
||||
for _, line := range allLines {
|
||||
trimmedLine := strings.TrimLeftFunc(line, unicode.IsSpace)
|
||||
if line != "" && strings.HasPrefix(trimmedLine, "-A") {
|
||||
// if the line is not empty and starts with "-A", it is a rule appended not default
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// return the count of non-default rules
|
||||
return count, nil
|
||||
}
|
||||
79
vendor/tailscale.com/util/linuxfw/iptables_for_svcs.go
generated
vendored
79
vendor/tailscale.com/util/linuxfw/iptables_for_svcs.go
generated
vendored
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// This file contains functionality to insert portmapping rules for a 'service'.
|
||||
// These are currently only used by the Kubernetes operator proxies.
|
||||
// An iptables rule for such a service contains a comment with the service name.
|
||||
|
||||
// EnsurePortMapRuleForSvc adds a prerouting rule that forwards traffic received
|
||||
// on match port and NOT on the provided interface to target IP and target port.
|
||||
// Rule will only be added if it does not already exists.
|
||||
func (i *iptablesRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
table := i.getIPTByAddr(targetIP)
|
||||
args := argsForPortMapRule(svc, tun, targetIP, pm)
|
||||
exists, err := table.Exists("nat", "PREROUTING", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return table.Append("nat", "PREROUTING", args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMapRuleForSvc constructs a prerouting rule as would be created by
|
||||
// EnsurePortMapRuleForSvc with the provided args and, if such a rule exists,
|
||||
// deletes it.
|
||||
func (i *iptablesRunner) DeletePortMapRuleForSvc(svc, excludeI string, targetIP netip.Addr, pm PortMap) error {
|
||||
table := i.getIPTByAddr(targetIP)
|
||||
args := argsForPortMapRule(svc, excludeI, targetIP, pm)
|
||||
exists, err := table.Exists("nat", "PREROUTING", args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return table.Delete("nat", "PREROUTING", args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSvc constructs all possible rules that would have been created by
|
||||
// EnsurePortMapRuleForSvc from the provided args and ensures that each one that
|
||||
// exists is deleted.
|
||||
func (i *iptablesRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr, pms []PortMap) error {
|
||||
for _, tip := range targetIPs {
|
||||
for _, pm := range pms {
|
||||
if err := i.DeletePortMapRuleForSvc(svc, tun, tip, pm); err != nil {
|
||||
return fmt.Errorf("error deleting rule: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func argsForPortMapRule(svc, excludeI string, targetIP netip.Addr, pm PortMap) []string {
|
||||
c := commentForSvc(svc, pm)
|
||||
return []string{
|
||||
"!", "-i", excludeI,
|
||||
"-p", pm.Protocol,
|
||||
"--dport", fmt.Sprintf("%d", pm.MatchPort),
|
||||
"-m", "comment", "--comment", c,
|
||||
"-j", "DNAT",
|
||||
"--to-destination", fmt.Sprintf("%v:%v", targetIP, pm.TargetPort),
|
||||
}
|
||||
}
|
||||
|
||||
// commentForSvc generates a comment to be added to an iptables DNAT rule for a
|
||||
// service. This is for iptables debugging/readability purposes only.
|
||||
func commentForSvc(svc string, pm PortMap) string {
|
||||
return fmt.Sprintf("%s:%s:%d -> %s:%d", svc, pm.Protocol, pm.MatchPort, pm.Protocol, pm.TargetPort)
|
||||
}
|
||||
774
vendor/tailscale.com/util/linuxfw/iptables_runner.go
generated
vendored
774
vendor/tailscale.com/util/linuxfw/iptables_runner.go
generated
vendored
@@ -1,774 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// isNotExistError needs to be overridden in tests that rely on distinguishing
|
||||
// this error, because we don't have a good way how to create a new
|
||||
// iptables.Error of that type.
|
||||
var isNotExistError = func(err error) bool {
|
||||
var e *iptables.Error
|
||||
return errors.As(err, &e) && e.IsNotExist()
|
||||
}
|
||||
|
||||
type iptablesInterface interface {
|
||||
// Adding this interface for testing purposes so we can mock out
|
||||
// the iptables library, in reality this is a wrapper to *iptables.IPTables.
|
||||
Insert(table, chain string, pos int, args ...string) error
|
||||
Append(table, chain string, args ...string) error
|
||||
Exists(table, chain string, args ...string) (bool, error)
|
||||
Delete(table, chain string, args ...string) error
|
||||
List(table, chain string) ([]string, error)
|
||||
ClearChain(table, chain string) error
|
||||
NewChain(table, chain string) error
|
||||
DeleteChain(table, chain string) error
|
||||
}
|
||||
|
||||
type iptablesRunner struct {
|
||||
ipt4 iptablesInterface
|
||||
ipt6 iptablesInterface
|
||||
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
v6FilterAvailable bool
|
||||
}
|
||||
|
||||
func checkIP6TablesExists() error {
|
||||
// Some distros ship ip6tables separately from iptables.
|
||||
if _, err := exec.LookPath("ip6tables"); err != nil {
|
||||
return fmt.Errorf("path not found: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
|
||||
// If the underlying iptables library fails to initialize, that error is
|
||||
// returned. The runner probes for IPv6 support once at initialization time and
|
||||
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
|
||||
func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportsV6, supportsV6NAT, supportsV6Filter := false, false, false
|
||||
v6err := CheckIPv6(logf)
|
||||
ip6terr := checkIP6TablesExists()
|
||||
var ipt6 *iptables.IPTables
|
||||
switch {
|
||||
case v6err != nil:
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
case ip6terr != nil:
|
||||
logf("disabling tunneled IPv6 due to missing ip6tables: %v", ip6terr)
|
||||
default:
|
||||
supportsV6 = true
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
supportsV6Filter = checkSupportsV6Filter(ipt6, logf)
|
||||
supportsV6NAT = checkSupportsV6NAT(ipt6, logf)
|
||||
logf("netfilter running in iptables mode v6 = %v, v6filter = %v, v6nat = %v", supportsV6, supportsV6Filter, supportsV6NAT)
|
||||
}
|
||||
return &iptablesRunner{
|
||||
ipt4: ipt4,
|
||||
ipt6: ipt6,
|
||||
v6Available: supportsV6,
|
||||
v6NATAvailable: supportsV6NAT,
|
||||
v6FilterAvailable: supportsV6Filter}, nil
|
||||
}
|
||||
|
||||
// checkSupportsV6Filter returns whether the system has a "filter" table in the
|
||||
// IPv6 tables. Some container environments such as GitHub codespaces have
|
||||
// limited local IPv6 support, and containers containing ip6tables, but do not
|
||||
// have kernel support for IPv6 filtering.
|
||||
// We will not set ip6tables rules in these instances.
|
||||
func checkSupportsV6Filter(ipt *iptables.IPTables, logf logger.Logf) bool {
|
||||
if ipt == nil {
|
||||
return false
|
||||
}
|
||||
_, filterListErr := ipt.ListChains("filter")
|
||||
if filterListErr == nil {
|
||||
return true
|
||||
}
|
||||
logf("ip6tables filtering is not supported on this host: %v", filterListErr)
|
||||
return false
|
||||
}
|
||||
|
||||
// checkSupportsV6NAT returns whether the system has a "nat" table in the
|
||||
// IPv6 netfilter stack.
|
||||
//
|
||||
// The nat table was added after the initial release of ipv6
|
||||
// netfilter, so some older distros ship a kernel that can't NAT IPv6
|
||||
// traffic.
|
||||
// ipt must be initialized for IPv6.
|
||||
func checkSupportsV6NAT(ipt *iptables.IPTables, logf logger.Logf) bool {
|
||||
if ipt == nil || ipt.Proto() != iptables.ProtocolIPv6 {
|
||||
return false
|
||||
}
|
||||
_, natListErr := ipt.ListChains("nat")
|
||||
if natListErr == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO (irbekrm): the following two checks were added before the check
|
||||
// above that verifies that nat chains can be listed. It is a
|
||||
// container-friendly check (see
|
||||
// https://github.com/tailscale/tailscale/issues/11344), but also should
|
||||
// be good enough on its own in other environments. If we never observe
|
||||
// it falsely succeed, let's remove the other two checks.
|
||||
|
||||
bs, err := os.ReadFile("/proc/net/ip6_tables_names")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if bytes.Contains(bs, []byte("nat\n")) {
|
||||
logf("[unexpected] listing nat chains failed, but /proc/net/ip6_tables_name reports a nat table existing")
|
||||
return true
|
||||
}
|
||||
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
|
||||
logf("[unexpected] listing nat chains failed, but modprobe ip6table_nat succeeded")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasIPV6 reports true if the system supports IPv6.
|
||||
func (i *iptablesRunner) HasIPV6() bool {
|
||||
return i.v6Available
|
||||
}
|
||||
|
||||
// HasIPV6Filter reports true if the system supports ip6tables filter table.
|
||||
func (i *iptablesRunner) HasIPV6Filter() bool {
|
||||
return i.v6FilterAvailable
|
||||
}
|
||||
|
||||
// HasIPV6NAT reports true if the system supports IPv6 NAT.
|
||||
func (i *iptablesRunner) HasIPV6NAT() bool {
|
||||
return i.v6NATAvailable
|
||||
}
|
||||
|
||||
// getIPTByAddr returns the iptablesInterface with correct IP family
|
||||
// that we will be using for the given address.
|
||||
func (i *iptablesRunner) getIPTByAddr(addr netip.Addr) iptablesInterface {
|
||||
nf := i.ipt4
|
||||
if addr.Is6() {
|
||||
nf = i.ipt6
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
// AddLoopbackRule adds an iptables rule to permit loopback traffic to
|
||||
// a local Tailscale IP.
|
||||
func (i *iptablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsChain returns the name of the tailscale sub-chain corresponding
|
||||
// to the given "parent" chain (e.g. INPUT, FORWARD, ...).
|
||||
func tsChain(chain string) string {
|
||||
return "ts-" + strings.ToLower(chain)
|
||||
}
|
||||
|
||||
// DelLoopbackRule removes the iptables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
func (i *iptablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
if err := i.getIPTByAddr(addr).Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTables gets the available iptablesInterface in iptables runner.
|
||||
func (i *iptablesRunner) getTables() []iptablesInterface {
|
||||
if i.HasIPV6Filter() {
|
||||
return []iptablesInterface{i.ipt4, i.ipt6}
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// getNATTables gets the available iptablesInterface in iptables runner.
|
||||
// If the system does not support IPv6 NAT, only the IPv4 iptablesInterface
|
||||
// is returned.
|
||||
func (i *iptablesRunner) getNATTables() []iptablesInterface {
|
||||
if i.HasIPV6NAT() {
|
||||
return i.getTables()
|
||||
}
|
||||
return []iptablesInterface{i.ipt4}
|
||||
}
|
||||
|
||||
// AddHooks inserts calls to tailscale's netfilter chains in
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist. If they do not, an error is returned.
|
||||
func (i *iptablesRunner) AddHooks() error {
|
||||
// divert inserts a jump to the tailscale chain in the given table/chain.
|
||||
// If the jump already exists, it is a no-op.
|
||||
divert := func(ipt iptablesInterface, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := ipt.Exists(table, chain, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := divert(ipt, "filter", "INPUT"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert(ipt, "filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := divert(ipt, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddChains creates custom Tailscale chains in netfilter via iptables
|
||||
// if the ts-chain doesn't already exist.
|
||||
func (i *iptablesRunner) AddChains() error {
|
||||
// create creates a chain in the given table if it doesn't already exist.
|
||||
// If the chain already exists, it is a no-op.
|
||||
create := func(ipt iptablesInterface, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. let's create it!
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := create(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := create(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBase adds some basic processing rules to be supplemented by
|
||||
// later calls to other helpers.
|
||||
func (i *iptablesRunner) AddBase(tunname string) error {
|
||||
if err := i.addBase4(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
if i.HasIPV6Filter() {
|
||||
if err := i.addBase6(tunname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addBase4 adds some basic IPv4 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase4(tunname string) error {
|
||||
// Only allow CGNAT range traffic to come from tailscale0. There
|
||||
// is an exception carved out for ranges used by ChromeOS, for
|
||||
// which we fall out of the Tailscale chain.
|
||||
//
|
||||
// Note, this will definitely break nodes that end up using the
|
||||
// CGNAT range for other purposes :(.
|
||||
args := []string{"!", "-i", tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
args = []string{"!", "-i", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Explicitly allow all other inbound traffic to the tun interface
|
||||
args = []string{"-i", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
// traffic to the tailscale interface by default. We use packet
|
||||
// marks here so both filter/FORWARD and nat/POSTROUTING can match
|
||||
// on these packets of interest.
|
||||
//
|
||||
// In particular, we only want to apply SNAT rules in
|
||||
// nat/POSTROUTING to packets that originated from the Tailscale
|
||||
// interface, but we can't match on the inbound interface in
|
||||
// POSTROUTING. So instead, we match on the inbound interface in
|
||||
// filter/FORWARD, and set a packet mark that nat/POSTROUTING can
|
||||
// use to effectively run that same test again.
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt4.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) AddDNATRule(origDst, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
return table.Insert("nat", "PREROUTING", 1, "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dst.String())
|
||||
}
|
||||
|
||||
// EnsureSNATForDst sets up firewall to ensure that all traffic aimed for dst, has its source ip set to src:
|
||||
// - creates a SNAT rule if not already present
|
||||
// - ensures that any no longer valid SNAT rules for the same dst are removed
|
||||
func (i *iptablesRunner) EnsureSNATForDst(src, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
rules, err := table.List("nat", "POSTROUTING")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing rules: %v", err)
|
||||
}
|
||||
// iptables accept either address or a CIDR value for the --destination flag, but converts an address to /32
|
||||
// CIDR. Explicitly passing a /32 CIDR made it possible to test this rule.
|
||||
dstPrefix, err := dst.Prefix(32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calculating prefix of dst %v: %v", dst, err)
|
||||
}
|
||||
|
||||
// wantsArgsPrefix is the prefix of the SNAT rule for the provided destination.
|
||||
// We should only have one POSTROUTING rule with this prefix.
|
||||
wantsArgsPrefix := fmt.Sprintf("-d %s -j SNAT --to-source", dstPrefix.String())
|
||||
// wantsArgs is the actual SNAT rule that we want.
|
||||
wantsArgs := fmt.Sprintf("%s %s", wantsArgsPrefix, src.String())
|
||||
for _, r := range rules {
|
||||
args := argsFromPostRoutingRule(r)
|
||||
if strings.HasPrefix(args, wantsArgsPrefix) {
|
||||
if strings.HasPrefix(args, wantsArgs) {
|
||||
return nil
|
||||
}
|
||||
// SNAT rule matching the destination, but for a different source - delete.
|
||||
if err := table.Delete("nat", "POSTROUTING", strings.Split(args, " ")...); err != nil {
|
||||
// If we failed to delete don't crash the node- the proxy should still be functioning.
|
||||
log.Printf("[unexpected] error deleting rule %s: %v, please report it.", r, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return table.Insert("nat", "POSTROUTING", 1, "-d", dstPrefix.String(), "-j", "SNAT", "--to-source", src.String())
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) DNATNonTailscaleTraffic(tun string, dst netip.Addr) error {
|
||||
table := i.getIPTByAddr(dst)
|
||||
return table.Insert("nat", "PREROUTING", 1, "!", "-i", tun, "-j", "DNAT", "--to-destination", dst.String())
|
||||
}
|
||||
|
||||
// DNATWithLoadBalancer adds iptables rules to forward all traffic received for
|
||||
// originDst to the backend dsts. Traffic will be load balanced using round robin.
|
||||
func (i *iptablesRunner) DNATWithLoadBalancer(origDst netip.Addr, dsts []netip.Addr) error {
|
||||
table := i.getIPTByAddr(dsts[0])
|
||||
if err := table.ClearChain("nat", "PREROUTING"); err != nil && !isNotExistError(err) {
|
||||
// If clearing the PREROUTING chain fails, fail the whole operation. This
|
||||
// rule is currently only used in Kubernetes containers where a
|
||||
// failed container gets restarted which should hopefully fix things.
|
||||
return fmt.Errorf("error clearing nat PREROUTING chain: %w", err)
|
||||
}
|
||||
// If dsts contain more than one address, for n := n in range(len(dsts)..2) route packets for every nth connection to dsts[n].
|
||||
for i := len(dsts); i >= 2; i-- {
|
||||
dst := dsts[i-1] // the order in which rules for addrs are installed does not matter
|
||||
if err := table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-m", "statistic", "--mode", "nth", "--every", fmt.Sprint(i), "--packet", "0", "-j", "DNAT", "--to-destination", dst.String()); err != nil {
|
||||
return fmt.Errorf("error adding DNAT rule for %s: %w", dst.String(), err)
|
||||
}
|
||||
}
|
||||
// If the packet falls through to this rule, we route to the first destination in the list unconditionally.
|
||||
return table.Append("nat", "PREROUTING", "--destination", origDst.String(), "-j", "DNAT", "--to-destination", dsts[0].String())
|
||||
}
|
||||
|
||||
func (i *iptablesRunner) ClampMSSToPMTU(tun string, addr netip.Addr) error {
|
||||
table := i.getIPTByAddr(addr)
|
||||
return table.Append("mangle", "FORWARD", "-o", tun, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
|
||||
}
|
||||
|
||||
// addBase6 adds some basic IPv6 processing rules to be
|
||||
// supplemented by later calls to other helpers.
|
||||
func (i *iptablesRunner) addBase6(tunname string) error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
// Explicitly allow all other inbound traffic to the tun interface
|
||||
args := []string{"-i", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
args = []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
|
||||
// (see corresponding IPv4 CGNAT rule).
|
||||
args = []string{"-o", tunname, "-j", "ACCEPT"}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelChains removes the custom Tailscale chains from netfilter via iptables.
|
||||
func (i *iptablesRunner) DelChains() error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter via iptables.
|
||||
func (i *iptablesRunner) DelBase() error {
|
||||
del := func(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := del(ipt, "filter", "ts-input"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del(ipt, "filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := del(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHooks deletes the calls to tailscale's netfilter chains
|
||||
// in the relevant main netfilter chains.
|
||||
func (i *iptablesRunner) DelHooks(logf logger.Logf) error {
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSNATRule adds a netfilter rule to SNAT traffic destined for
|
||||
// local subnets.
|
||||
func (i *iptablesRunner) AddSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Append("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSNATRule removes the netfilter rule to SNAT traffic destined for
|
||||
// local subnets. An error is returned if the rule does not exist.
|
||||
func (i *iptablesRunner) DelSNATRule() error {
|
||||
args := []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "MASQUERADE"}
|
||||
for _, ipt := range i.getNATTables() {
|
||||
if err := ipt.Delete("nat", "ts-postrouting", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func statefulRuleArgs(tunname string) []string {
|
||||
return []string{"-o", tunname, "-m", "conntrack", "!", "--ctstate", "ESTABLISHED,RELATED", "-j", "DROP"}
|
||||
}
|
||||
|
||||
// AddStatefulRule adds a netfilter rule for stateful packet filtering using
|
||||
// conntrack.
|
||||
func (i *iptablesRunner) AddStatefulRule(tunname string) error {
|
||||
// Drop packets that are destined for the tailscale interface if
|
||||
// they're a new connection, per conntrack, to prevent hosts on the
|
||||
// same subnet from being able to use this device as a way to forward
|
||||
// packets on to the Tailscale network.
|
||||
//
|
||||
// The conntrack states are:
|
||||
// NEW A packet which creates a new connection.
|
||||
// ESTABLISHED A packet which belongs to an existing connection
|
||||
// (i.e., a reply packet, or outgoing packet on a
|
||||
// connection which has seen replies).
|
||||
// RELATED A packet which is related to, but not part of, an
|
||||
// existing connection, such as an ICMP error.
|
||||
// INVALID A packet which could not be identified for some
|
||||
// reason: this includes running out of memory and ICMP
|
||||
// errors which don't correspond to any known
|
||||
// connection. Generally these packets should be
|
||||
// dropped.
|
||||
//
|
||||
// We drop NEW packets to prevent connections from coming "into"
|
||||
// Tailscale from other hosts on the same network segment; we drop
|
||||
// INVALID packets as well.
|
||||
args := statefulRuleArgs(tunname)
|
||||
for _, ipt := range i.getTables() {
|
||||
// First, find the final "accept" rule.
|
||||
rules, err := ipt.List("filter", "ts-forward")
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing rules in filter/ts-forward: %w", err)
|
||||
}
|
||||
want := fmt.Sprintf("-A %s -o %s -j ACCEPT", "ts-forward", tunname)
|
||||
|
||||
pos := slices.Index(rules, want)
|
||||
if pos < 0 {
|
||||
return fmt.Errorf("couldn't find final ACCEPT rule in filter/ts-forward")
|
||||
}
|
||||
|
||||
if err := ipt.Insert("filter", "ts-forward", pos, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelStatefulRule removes the netfilter rule for stateful packet filtering
|
||||
// using conntrack.
|
||||
func (i *iptablesRunner) DelStatefulRule(tunname string) error {
|
||||
args := statefulRuleArgs(tunname)
|
||||
for _, ipt := range i.getTables() {
|
||||
if err := ipt.Delete("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("deleting %v in filter/ts-forward: %w", args, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMagicsockPortRule generates the string slice containing the arguments
|
||||
// to describe a rule accepting traffic on a particular port to iptables. It is
|
||||
// separated out here to avoid repetition in AddMagicsockPortRule and
|
||||
// RemoveMagicsockPortRule, since it is important that the same rule is passed
|
||||
// to Append() and Delete().
|
||||
func buildMagicsockPortRule(port uint16) []string {
|
||||
return []string{"-p", "udp", "--dport", strconv.FormatUint(uint64(port), 10), "-j", "ACCEPT"}
|
||||
}
|
||||
|
||||
// AddMagicsockPortRule adds a rule to iptables to allow incoming traffic on
|
||||
// the specified UDP port, so magicsock can accept incoming connections.
|
||||
// network must be either "udp4" or "udp6" - this determines whether the rule
|
||||
// is added for IPv4 or IPv6.
|
||||
func (i *iptablesRunner) AddMagicsockPortRule(port uint16, network string) error {
|
||||
var ipt iptablesInterface
|
||||
switch network {
|
||||
case "udp4":
|
||||
ipt = i.ipt4
|
||||
case "udp6":
|
||||
ipt = i.ipt6
|
||||
default:
|
||||
return fmt.Errorf("unsupported network %s", network)
|
||||
}
|
||||
|
||||
args := buildMagicsockPortRule(port)
|
||||
|
||||
if err := ipt.Append("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelMagicsockPortRule removes a rule added by AddMagicsockPortRule to accept
|
||||
// incoming traffic on a particular UDP port.
|
||||
// network must be either "udp4" or "udp6" - this determines whether the rule
|
||||
// is removed for IPv4 or IPv6.
|
||||
func (i *iptablesRunner) DelMagicsockPortRule(port uint16, network string) error {
|
||||
var ipt iptablesInterface
|
||||
switch network {
|
||||
case "udp4":
|
||||
ipt = i.ipt4
|
||||
case "udp6":
|
||||
ipt = i.ipt6
|
||||
default:
|
||||
return fmt.Errorf("unsupported network %s", network)
|
||||
}
|
||||
|
||||
args := buildMagicsockPortRule(port)
|
||||
|
||||
if err := ipt.Delete("filter", "ts-input", args...); err != nil {
|
||||
return fmt.Errorf("removing %v in filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPTablesCleanUp removes all Tailscale added iptables rules.
|
||||
// Any errors that occur are logged to the provided logf.
|
||||
func IPTablesCleanUp(logf logger.Logf) {
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
// Gokrazy uses nftables and doesn't have the "iptables" command.
|
||||
// Avoid log spam on cleanup. (#12277)
|
||||
return
|
||||
}
|
||||
err := clearRules(iptables.ProtocolIPv4, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear iptables: %v", err)
|
||||
}
|
||||
|
||||
err = clearRules(iptables.ProtocolIPv6, logf)
|
||||
if err != nil {
|
||||
logf("linuxfw: clear ip6tables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delTSHook deletes hook in a chain that jumps to a ts-chain. If the hook does not
|
||||
// exist, it's a no-op since the desired state is already achieved but we log the
|
||||
// error because error code from the iptables module resists unwrapping.
|
||||
func delTSHook(ipt iptablesInterface, table, chain string, logf logger.Logf) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := ipt.Delete(table, chain, args...); err != nil && !isNotExistError(err) {
|
||||
return fmt.Errorf("deleting %v in %s/%s: %v", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// delChain flushes and deletes a chain. If the chain does not exist, it's a no-op
|
||||
// since the desired state is already achieved. otherwise, it returns an error.
|
||||
func delChain(ipt iptablesInterface, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if isNotExistError(err) {
|
||||
// nonexistent chain. nothing to do.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := ipt.DeleteChain(table, chain); err != nil {
|
||||
return fmt.Errorf("deleting %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearRules clears all the iptables rules created by Tailscale
|
||||
// for the given protocol. If error occurs, it's logged but not returned.
|
||||
func clearRules(proto iptables.Protocol, logf logger.Logf) error {
|
||||
ipt, err := iptables.NewWithProtocol(proto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if err := delTSHook(ipt, "filter", "INPUT", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "filter", "FORWARD", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delTSHook(ipt, "nat", "POSTROUTING", logf); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "filter", "ts-input"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := delChain(ipt, "filter", "ts-forward"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := delChain(ipt, "nat", "ts-postrouting"); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
// argsFromPostRoutingRule accepts a rule as returned by iptables.List and, if it is a rule from POSTROUTING chain,
|
||||
// returns the args part, else returns the original rule.
|
||||
func argsFromPostRoutingRule(r string) string {
|
||||
args, _ := strings.CutPrefix(r, "-A POSTROUTING ")
|
||||
return args
|
||||
}
|
||||
182
vendor/tailscale.com/util/linuxfw/linuxfw.go
generated
vendored
182
vendor/tailscale.com/util/linuxfw/linuxfw.go
generated
vendored
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Package linuxfw returns the kind of firewall being used by the kernel.
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/netlink"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MatchDecision is the decision made by the firewall for a packet matched by a rule.
|
||||
// It is used to decide whether to accept or masquerade a packet in addMatchSubnetRouteMarkRule.
|
||||
type MatchDecision int
|
||||
|
||||
const (
|
||||
Accept MatchDecision = iota
|
||||
Masq
|
||||
)
|
||||
|
||||
type FWModeNotSupportedError struct {
|
||||
Mode FirewallMode
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Error() string {
|
||||
return fmt.Sprintf("firewall mode %q not supported: %v", e.Mode, e.Err)
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Is(target error) bool {
|
||||
_, ok := target.(FWModeNotSupportedError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e FWModeNotSupportedError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type FirewallMode string
|
||||
|
||||
const (
|
||||
FirewallModeIPTables FirewallMode = "iptables"
|
||||
FirewallModeNfTables FirewallMode = "nftables"
|
||||
)
|
||||
|
||||
// The following bits are added to packet marks for Tailscale use.
|
||||
//
|
||||
// We tried to pick bits sufficiently out of the way that it's
|
||||
// unlikely to collide with existing uses. We have 4 bytes of mark
|
||||
// bits to play with. We leave the lower byte alone on the assumption
|
||||
// that sysadmins would use those. Kubernetes uses a few bits in the
|
||||
// second byte, so we steer clear of that too.
|
||||
//
|
||||
// Empirically, most of the documentation on packet marks on the
|
||||
// internet gives the impression that the marks are 16 bits
|
||||
// wide. Based on this, we theorize that the upper two bytes are
|
||||
// relatively unused in the wild, and so we consume bits 16:23 (the
|
||||
// third byte).
|
||||
//
|
||||
// The constants are in the iptables/iproute2 string format for
|
||||
// matching and setting the bits, so they can be directly embedded in
|
||||
// commands.
|
||||
const (
|
||||
// The mask for reading/writing the 'firewall mask' bits on a packet.
|
||||
// See the comment on the const block on why we only use the third byte.
|
||||
//
|
||||
// We claim bits 16:23 entirely. For now we only use the lower four
|
||||
// bits, leaving the higher 4 bits for future use.
|
||||
TailscaleFwmarkMask = "0xff0000"
|
||||
TailscaleFwmarkMaskNum = 0xff0000
|
||||
|
||||
// Packet is from Tailscale and to a subnet route destination, so
|
||||
// is allowed to be routed through this machine.
|
||||
TailscaleSubnetRouteMark = "0x40000"
|
||||
TailscaleSubnetRouteMarkNum = 0x40000
|
||||
|
||||
// Packet was originated by tailscaled itself, and must not be
|
||||
// routed over the Tailscale network.
|
||||
TailscaleBypassMark = "0x80000"
|
||||
TailscaleBypassMarkNum = 0x80000
|
||||
)
|
||||
|
||||
// getTailscaleFwmarkMaskNeg returns the negation of TailscaleFwmarkMask in bytes.
|
||||
func getTailscaleFwmarkMaskNeg() []byte {
|
||||
return []byte{0xff, 0x00, 0xff, 0xff}
|
||||
}
|
||||
|
||||
// getTailscaleFwmarkMask returns the TailscaleFwmarkMask in bytes.
|
||||
func getTailscaleFwmarkMask() []byte {
|
||||
return []byte{0x00, 0xff, 0x00, 0x00}
|
||||
}
|
||||
|
||||
// getTailscaleSubnetRouteMark returns the TailscaleSubnetRouteMark in bytes.
|
||||
func getTailscaleSubnetRouteMark() []byte {
|
||||
return []byte{0x00, 0x04, 0x00, 0x00}
|
||||
}
|
||||
|
||||
// checkIPv6ForTest can be set in tests.
|
||||
var checkIPv6ForTest func(logger.Logf) error
|
||||
|
||||
// checkIPv6 checks whether the system appears to have a working IPv6
|
||||
// network stack. It returns an error explaining what looks wrong or
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func CheckIPv6(logf logger.Logf) error {
|
||||
if f := checkIPv6ForTest; f != nil {
|
||||
return f(logf)
|
||||
}
|
||||
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
bs, err := os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
if err != nil {
|
||||
// Be conservative if we can't find the IPv6 configuration knob.
|
||||
return err
|
||||
}
|
||||
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_ipv6 has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_ipv6 is set")
|
||||
}
|
||||
|
||||
// Older kernels don't support IPv6 policy routing. Some kernels
|
||||
// support policy routing but don't have this knob, so absence of
|
||||
// the knob is not fatal.
|
||||
bs, err = os.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
|
||||
if err == nil {
|
||||
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
|
||||
if err != nil {
|
||||
return errors.New("disable_policy has invalid bool")
|
||||
}
|
||||
if disabled {
|
||||
return errors.New("disable_policy is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := CheckIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = TailscaleBypassMarkNum
|
||||
rule.Table = 52
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
netlink.RuleDel(rule)
|
||||
// And clean up on exit.
|
||||
defer netlink.RuleDel(rule)
|
||||
return netlink.RuleAdd(rule)
|
||||
}
|
||||
40
vendor/tailscale.com/util/linuxfw/linuxfw_unsupported.go
generated
vendored
40
vendor/tailscale.com/util/linuxfw/linuxfw_unsupported.go
generated
vendored
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// NOTE: linux_{arm64, amd64} are the only two currently supported archs due to missing
|
||||
// support in upstream dependencies.
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && !(arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// ErrUnsupported is the error returned from all functions on non-Linux
|
||||
// platforms.
|
||||
var ErrUnsupported = errors.New("linuxfw:unsupported")
|
||||
|
||||
// DebugNetfilter is not supported on non-Linux platforms.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
}
|
||||
|
||||
// DetectNetfilter is not supported on non-Linux platforms.
|
||||
func detectNetfilter() (int, error) {
|
||||
return 0, ErrUnsupported
|
||||
}
|
||||
|
||||
// DebugIptables is not supported on non-Linux platforms.
|
||||
func debugIptables(logf logger.Logf) error {
|
||||
return ErrUnsupported
|
||||
}
|
||||
|
||||
// DetectIptables is not supported on non-Linux platforms.
|
||||
func detectIptables() (int, error) {
|
||||
return 0, ErrUnsupported
|
||||
}
|
||||
292
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
292
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
@@ -1,292 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/google/nftables/xt"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// DebugNetfilter prints debug information about netfilter rules to the
|
||||
// provided log function.
|
||||
func DebugNetfilter(logf logger.Logf) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chains, err := conn.ListChains()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot list chains: %w", err)
|
||||
}
|
||||
|
||||
if len(chains) == 0 {
|
||||
logf("netfilter: no chains")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, chain := range chains {
|
||||
logf("netfilter: table=%s chain=%s", chain.Table.Name, chain.Name)
|
||||
|
||||
rules, err := conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sort.Slice(rules, func(i, j int) bool {
|
||||
return rules[i].Position < rules[j].Position
|
||||
})
|
||||
|
||||
for i, rule := range rules {
|
||||
logf("netfilter: rule[%d]: pos=%d flags=%d", i, rule.Position, rule.Flags)
|
||||
for _, ex := range rule.Exprs {
|
||||
switch v := ex.(type) {
|
||||
case *expr.Meta:
|
||||
key := cmp.Or(metaKeyNames[v.Key], "UNKNOWN")
|
||||
logf("netfilter: Meta: key=%s source_register=%v register=%d", key, v.SourceRegister, v.Register)
|
||||
|
||||
case *expr.Cmp:
|
||||
op := cmp.Or(cmpOpNames[v.Op], "UNKNOWN")
|
||||
logf("netfilter: Cmp: op=%s register=%d data=%s", op, v.Register, formatMaybePrintable(v.Data))
|
||||
|
||||
case *expr.Counter:
|
||||
// don't print
|
||||
|
||||
case *expr.Verdict:
|
||||
kind := cmp.Or(verdictNames[v.Kind], "UNKNOWN")
|
||||
logf("netfilter: Verdict: kind=%s data=%s", kind, v.Chain)
|
||||
|
||||
case *expr.Target:
|
||||
logf("netfilter: Target: name=%s info=%s", v.Name, printTargetInfo(v.Name, v.Info))
|
||||
|
||||
case *expr.Match:
|
||||
logf("netfilter: Match: name=%s info=%+v", v.Name, printMatchInfo(v.Name, v.Info))
|
||||
|
||||
case *expr.Payload:
|
||||
logf("netfilter: Payload: op=%s src=%d dst=%d base=%s offset=%d len=%d",
|
||||
payloadOperationTypeNames[v.OperationType],
|
||||
v.SourceRegister, v.DestRegister,
|
||||
payloadBaseNames[v.Base],
|
||||
v.Offset, v.Len)
|
||||
// TODO(andrew): csum
|
||||
|
||||
case *expr.Bitwise:
|
||||
var xor string
|
||||
for _, b := range v.Xor {
|
||||
if b != 0 {
|
||||
xor = fmt.Sprintf(" xor=%v", v.Xor)
|
||||
break
|
||||
}
|
||||
}
|
||||
logf("netfilter: Bitwise: src=%d dst=%d len=%d mask=%v%s",
|
||||
v.SourceRegister, v.DestRegister, v.Len, v.Mask, xor)
|
||||
|
||||
default:
|
||||
logf("netfilter: unknown %T: %+v", v, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectNetfilter returns the number of nftables rules present in the system.
|
||||
func detectNetfilter() (int, error) {
|
||||
// Frist try creating a dummy postrouting chain. Emperically, we have
|
||||
// noticed that on some devices there is partial nftables support and the
|
||||
// kernel rejects some chains that are valid on other devices. This is a
|
||||
// workaround to detect that case.
|
||||
//
|
||||
// This specifically allows us to run in on GKE nodes using COS images which
|
||||
// have partial nftables support (as of 2023-10-18). When we try to create a
|
||||
// dummy postrouting chain, we get an error like:
|
||||
// add chain: conn.Receive: netlink receive: no such file or directory
|
||||
nft, err := newNfTablesRunner(logger.Discard)
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: fmt.Errorf("cannot create nftables runner: %w", err),
|
||||
}
|
||||
}
|
||||
if err := nft.createDummyPostroutingChains(); err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
chains, err := conn.ListChains()
|
||||
if err != nil {
|
||||
return 0, FWModeNotSupportedError{
|
||||
Mode: FirewallModeNfTables,
|
||||
Err: fmt.Errorf("cannot list chains: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
var validRules int
|
||||
for _, chain := range chains {
|
||||
rules, err := conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
validRules += len(rules)
|
||||
}
|
||||
|
||||
return validRules, nil
|
||||
}
|
||||
|
||||
func printMatchInfo(name string, info xt.InfoAny) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{`)
|
||||
|
||||
var handled bool = true
|
||||
switch v := info.(type) {
|
||||
// TODO(andrew): we should support these common types
|
||||
//case *xt.ConntrackMtinfo3:
|
||||
//case *xt.ConntrackMtinfo2:
|
||||
case *xt.Tcp:
|
||||
fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
|
||||
if v.Option != 0 {
|
||||
fmt.Fprintf(&sb, " Option:%d", v.Option)
|
||||
}
|
||||
if v.FlagsMask != 0 {
|
||||
fmt.Fprintf(&sb, " FlagsMask:%d", v.FlagsMask)
|
||||
}
|
||||
if v.FlagsCmp != 0 {
|
||||
fmt.Fprintf(&sb, " FlagsCmp:%d", v.FlagsCmp)
|
||||
}
|
||||
if v.InvFlags != 0 {
|
||||
fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
|
||||
}
|
||||
|
||||
case *xt.Udp:
|
||||
fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
|
||||
if v.InvFlags != 0 {
|
||||
fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
|
||||
}
|
||||
|
||||
case *xt.AddrType:
|
||||
var sprefix, dprefix string
|
||||
if v.InvertSource {
|
||||
sprefix = "!"
|
||||
}
|
||||
if v.InvertDest {
|
||||
dprefix = "!"
|
||||
}
|
||||
// TODO(andrew): translate source/dest
|
||||
fmt.Fprintf(&sb, "Source:%s%d Dest:%s%d", sprefix, v.Source, dprefix, v.Dest)
|
||||
|
||||
case *xt.AddrTypeV1:
|
||||
// TODO(andrew): translate source/dest
|
||||
fmt.Fprintf(&sb, "Source:%d Dest:%d", v.Source, v.Dest)
|
||||
|
||||
var flags []string
|
||||
for flag, name := range addrTypeFlagNames {
|
||||
if v.Flags&flag != 0 {
|
||||
flags = append(flags, name)
|
||||
}
|
||||
}
|
||||
if len(flags) > 0 {
|
||||
sort.Strings(flags)
|
||||
fmt.Fprintf(&sb, "Flags:%s", strings.Join(flags, ","))
|
||||
}
|
||||
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
if handled {
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
unknown, ok := info.(*xt.Unknown)
|
||||
if !ok {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
data := []byte(*unknown)
|
||||
|
||||
// Things where upstream has no type
|
||||
handled = true
|
||||
switch name {
|
||||
case "pkttype":
|
||||
if len(data) != 8 {
|
||||
handled = false
|
||||
break
|
||||
}
|
||||
|
||||
pkttype := int(binary.NativeEndian.Uint32(data[0:4]))
|
||||
invert := int(binary.NativeEndian.Uint32(data[4:8]))
|
||||
var invertPrefix string
|
||||
if invert != 0 {
|
||||
invertPrefix = "!"
|
||||
}
|
||||
|
||||
pkttypeName := packetTypeNames[pkttype]
|
||||
if pkttypeName != "" {
|
||||
fmt.Fprintf(&sb, "PktType:%s%s", invertPrefix, pkttypeName)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "PktType:%s%d", invertPrefix, pkttype)
|
||||
}
|
||||
|
||||
default:
|
||||
handled = true
|
||||
}
|
||||
|
||||
if !handled {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func printTargetInfo(name string, info xt.InfoAny) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{`)
|
||||
|
||||
unknown, ok := info.(*xt.Unknown)
|
||||
if !ok {
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
data := []byte(*unknown)
|
||||
|
||||
// Things where upstream has no type
|
||||
switch name {
|
||||
case "LOG":
|
||||
if len(data) != 32 {
|
||||
fmt.Fprintf(&sb, `Error:"bad size; want 32, got %d"`, len(data))
|
||||
break
|
||||
}
|
||||
|
||||
level := data[0]
|
||||
logflags := data[1]
|
||||
prefix := unix.ByteSliceToString(data[2:])
|
||||
fmt.Fprintf(&sb, "Level:%d LogFlags:%d Prefix:%q", level, logflags, prefix)
|
||||
default:
|
||||
return fmt.Sprintf("(%T)%+v", info, info)
|
||||
}
|
||||
|
||||
sb.WriteString(`}`)
|
||||
return sb.String()
|
||||
}
|
||||
245
vendor/tailscale.com/util/linuxfw/nftables_for_svcs.go
generated
vendored
245
vendor/tailscale.com/util/linuxfw/nftables_for_svcs.go
generated
vendored
@@ -1,245 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/binaryutil"
|
||||
"github.com/google/nftables/expr"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// This file contains functionality that is currently (09/2024) used to set up
|
||||
// routing for the Tailscale Kubernetes operator egress proxies. A tailnet
|
||||
// service (identified by tailnet IP or FQDN) that gets exposed to cluster
|
||||
// workloads gets a separate prerouting chain created for it for each IP family
|
||||
// of the chain's target addresses. Each service's prerouting chain contains one
|
||||
// or more portmapping rules. A portmapping rule DNATs traffic received on a
|
||||
// particular port to a port of the tailnet service. Creating a chain per
|
||||
// service makes it easier to delete a service when no longer needed and helps
|
||||
// with readability.
|
||||
|
||||
// EnsurePortMapRuleForSvc:
|
||||
// - ensures that nat table exists
|
||||
// - ensures that there is a prerouting chain for the given service and IP family of the target address in the nat table
|
||||
// - ensures that there is a portmapping rule mathcing the given portmap (only creates the rule if it does not already exist)
|
||||
func (n *nftablesRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
t, ch, err := n.ensureChainForSvc(svc, targetIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ensuring chain for %s: %w", svc, err)
|
||||
}
|
||||
meta := svcPortMapRuleMeta(svc, targetIP, pm)
|
||||
rule, err := n.findRuleByMetadata(t, ch, meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up rule: %w", err)
|
||||
}
|
||||
if rule != nil {
|
||||
return nil
|
||||
}
|
||||
p, err := protoFromString(pm.Protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting protocol %s: %w", pm.Protocol, err)
|
||||
}
|
||||
|
||||
rule = portMapRule(t, ch, tun, targetIP, pm.MatchPort, pm.TargetPort, p, meta)
|
||||
n.conn.InsertRule(rule)
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
// DeletePortMapRuleForSvc deletes a portmapping rule in the given service/IP family chain.
|
||||
// It finds the matching rule using metadata attached to the rule.
|
||||
// The caller is expected to call DeleteSvc if the whole service (the chain)
|
||||
// needs to be deleted, so we don't deal with the case where this is the only
|
||||
// rule in the chain here.
|
||||
func (n *nftablesRunner) DeletePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
|
||||
table, err := n.getNFTByAddr(targetIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %s: %w", targetIP, err)
|
||||
}
|
||||
t, err := getTableIfExists(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if nat table exists: %w", err)
|
||||
}
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
ch, err := getChainFromTable(n.conn, t, svc)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return fmt.Errorf("error checking if chain %s exists: %w", svc, err)
|
||||
}
|
||||
if errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return nil // service chain does not exist, so neither does the portmapping rule
|
||||
}
|
||||
meta := svcPortMapRuleMeta(svc, targetIP, pm)
|
||||
rule, err := n.findRuleByMetadata(t, ch, meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if rule exists: %w", err)
|
||||
}
|
||||
if rule == nil {
|
||||
return nil
|
||||
}
|
||||
if err := n.conn.DelRule(rule); err != nil {
|
||||
return fmt.Errorf("error deleting rule: %w", err)
|
||||
}
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
// DeleteSvc deletes the chains for the given service if any exist.
|
||||
func (n *nftablesRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr, pm []PortMap) error {
|
||||
for _, tip := range targetIPs {
|
||||
table, err := n.getNFTByAddr(tip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %s: %w", tip, err)
|
||||
}
|
||||
t, err := getTableIfExists(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if nat table exists: %w", err)
|
||||
}
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
ch, err := getChainFromTable(n.conn, t, svc)
|
||||
if err != nil && !errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return fmt.Errorf("error checking if chain %s exists: %w", svc, err)
|
||||
}
|
||||
if errors.Is(err, errorChainNotFound{t.Name, svc}) {
|
||||
return nil
|
||||
}
|
||||
n.conn.DelChain(ch)
|
||||
}
|
||||
return n.conn.Flush()
|
||||
}
|
||||
|
||||
func portMapRule(t *nftables.Table, ch *nftables.Chain, tun string, targetIP netip.Addr, matchPort, targetPort uint16, proto uint8, meta []byte) *nftables.Rule {
|
||||
var fam uint32
|
||||
if targetIP.Is4() {
|
||||
fam = unix.NFPROTO_IPV4
|
||||
} else {
|
||||
fam = unix.NFPROTO_IPV6
|
||||
}
|
||||
rule := &nftables.Rule{
|
||||
Table: t,
|
||||
Chain: ch,
|
||||
UserData: meta,
|
||||
Exprs: []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpNeq,
|
||||
Register: 1,
|
||||
Data: []byte(tun),
|
||||
},
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{proto},
|
||||
},
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: binaryutil.BigEndian.PutUint16(matchPort),
|
||||
},
|
||||
&expr.Immediate{
|
||||
Register: 1,
|
||||
Data: targetIP.AsSlice(),
|
||||
},
|
||||
&expr.Immediate{
|
||||
Register: 2,
|
||||
Data: binaryutil.BigEndian.PutUint16(targetPort),
|
||||
},
|
||||
&expr.NAT{
|
||||
Type: expr.NATTypeDestNAT,
|
||||
Family: fam,
|
||||
RegAddrMin: 1,
|
||||
RegAddrMax: 1,
|
||||
RegProtoMin: 2,
|
||||
RegProtoMax: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
// svcPortMapRuleMeta generates metadata for a rule.
|
||||
// This metadata can then be used to find the rule.
|
||||
// https://github.com/google/nftables/issues/48
|
||||
func svcPortMapRuleMeta(svcName string, targetIP netip.Addr, pm PortMap) []byte {
|
||||
return []byte(fmt.Sprintf("svc:%s,targetIP:%s:matchPort:%v,targetPort:%v,proto:%v", svcName, targetIP.String(), pm.MatchPort, pm.TargetPort, pm.Protocol))
|
||||
}
|
||||
|
||||
func (n *nftablesRunner) findRuleByMetadata(t *nftables.Table, ch *nftables.Chain, meta []byte) (*nftables.Rule, error) {
|
||||
if n.conn == nil || t == nil || ch == nil || len(meta) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rules, err := n.conn.GetRules(t, ch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing rules: %w", err)
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if reflect.DeepEqual(rule.UserData, meta) {
|
||||
return rule, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *nftablesRunner) ensureChainForSvc(svc string, targetIP netip.Addr) (*nftables.Table, *nftables.Chain, error) {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
table, err := n.getNFTByAddr(targetIP)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error setting up nftables for IP family of %v: %w", targetIP, err)
|
||||
}
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error ensuring nat table: %w", err)
|
||||
}
|
||||
svcCh, err := getOrCreateChain(n.conn, chainInfo{
|
||||
table: nat,
|
||||
name: svc,
|
||||
chainType: nftables.ChainTypeNAT,
|
||||
chainHook: nftables.ChainHookPrerouting,
|
||||
chainPriority: nftables.ChainPriorityNATDest,
|
||||
chainPolicy: &polAccept,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error ensuring prerouting chain: %w", err)
|
||||
}
|
||||
return nat, svcCh, nil
|
||||
}
|
||||
|
||||
// // PortMap is the port mapping for a service rule.
|
||||
type PortMap struct {
|
||||
// MatchPort is the local port to which the rule should apply.
|
||||
MatchPort uint16
|
||||
// TargetPort is the port to which the traffic should be forwarded.
|
||||
TargetPort uint16
|
||||
// Protocol is the protocol to match packets on. Only TCP and UDP are
|
||||
// supported.
|
||||
Protocol string
|
||||
}
|
||||
|
||||
func protoFromString(s string) (uint8, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "tcp":
|
||||
return unix.IPPROTO_TCP, nil
|
||||
case "udp":
|
||||
return unix.IPPROTO_UDP, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unrecognized protocol: %q", s)
|
||||
}
|
||||
}
|
||||
2055
vendor/tailscale.com/util/linuxfw/nftables_runner.go
generated
vendored
2055
vendor/tailscale.com/util/linuxfw/nftables_runner.go
generated
vendored
File diff suppressed because it is too large
Load Diff
95
vendor/tailscale.com/util/linuxfw/nftables_types.go
generated
vendored
95
vendor/tailscale.com/util/linuxfw/nftables_types.go
generated
vendored
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// TODO(#8502): add support for more architectures
|
||||
//go:build linux && (arm64 || amd64)
|
||||
|
||||
package linuxfw
|
||||
|
||||
import (
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/google/nftables/xt"
|
||||
)
|
||||
|
||||
var metaKeyNames = map[expr.MetaKey]string{
|
||||
expr.MetaKeyLEN: "LEN",
|
||||
expr.MetaKeyPROTOCOL: "PROTOCOL",
|
||||
expr.MetaKeyPRIORITY: "PRIORITY",
|
||||
expr.MetaKeyMARK: "MARK",
|
||||
expr.MetaKeyIIF: "IIF",
|
||||
expr.MetaKeyOIF: "OIF",
|
||||
expr.MetaKeyIIFNAME: "IIFNAME",
|
||||
expr.MetaKeyOIFNAME: "OIFNAME",
|
||||
expr.MetaKeyIIFTYPE: "IIFTYPE",
|
||||
expr.MetaKeyOIFTYPE: "OIFTYPE",
|
||||
expr.MetaKeySKUID: "SKUID",
|
||||
expr.MetaKeySKGID: "SKGID",
|
||||
expr.MetaKeyNFTRACE: "NFTRACE",
|
||||
expr.MetaKeyRTCLASSID: "RTCLASSID",
|
||||
expr.MetaKeySECMARK: "SECMARK",
|
||||
expr.MetaKeyNFPROTO: "NFPROTO",
|
||||
expr.MetaKeyL4PROTO: "L4PROTO",
|
||||
expr.MetaKeyBRIIIFNAME: "BRIIIFNAME",
|
||||
expr.MetaKeyBRIOIFNAME: "BRIOIFNAME",
|
||||
expr.MetaKeyPKTTYPE: "PKTTYPE",
|
||||
expr.MetaKeyCPU: "CPU",
|
||||
expr.MetaKeyIIFGROUP: "IIFGROUP",
|
||||
expr.MetaKeyOIFGROUP: "OIFGROUP",
|
||||
expr.MetaKeyCGROUP: "CGROUP",
|
||||
expr.MetaKeyPRANDOM: "PRANDOM",
|
||||
}
|
||||
|
||||
var cmpOpNames = map[expr.CmpOp]string{
|
||||
expr.CmpOpEq: "EQ",
|
||||
expr.CmpOpNeq: "NEQ",
|
||||
expr.CmpOpLt: "LT",
|
||||
expr.CmpOpLte: "LTE",
|
||||
expr.CmpOpGt: "GT",
|
||||
expr.CmpOpGte: "GTE",
|
||||
}
|
||||
|
||||
var verdictNames = map[expr.VerdictKind]string{
|
||||
expr.VerdictReturn: "RETURN",
|
||||
expr.VerdictGoto: "GOTO",
|
||||
expr.VerdictJump: "JUMP",
|
||||
expr.VerdictBreak: "BREAK",
|
||||
expr.VerdictContinue: "CONTINUE",
|
||||
expr.VerdictDrop: "DROP",
|
||||
expr.VerdictAccept: "ACCEPT",
|
||||
expr.VerdictStolen: "STOLEN",
|
||||
expr.VerdictQueue: "QUEUE",
|
||||
expr.VerdictRepeat: "REPEAT",
|
||||
expr.VerdictStop: "STOP",
|
||||
}
|
||||
|
||||
var payloadOperationTypeNames = map[expr.PayloadOperationType]string{
|
||||
expr.PayloadLoad: "LOAD",
|
||||
expr.PayloadWrite: "WRITE",
|
||||
}
|
||||
|
||||
var payloadBaseNames = map[expr.PayloadBase]string{
|
||||
expr.PayloadBaseLLHeader: "ll-header",
|
||||
expr.PayloadBaseNetworkHeader: "network-header",
|
||||
expr.PayloadBaseTransportHeader: "transport-header",
|
||||
}
|
||||
|
||||
var packetTypeNames = map[int]string{
|
||||
0 /* PACKET_HOST */ : "unicast",
|
||||
1 /* PACKET_BROADCAST */ : "broadcast",
|
||||
2 /* PACKET_MULTICAST */ : "multicast",
|
||||
}
|
||||
|
||||
var addrTypeFlagNames = map[xt.AddrTypeFlags]string{
|
||||
xt.AddrTypeUnspec: "unspec",
|
||||
xt.AddrTypeUnicast: "unicast",
|
||||
xt.AddrTypeLocal: "local",
|
||||
xt.AddrTypeBroadcast: "broadcast",
|
||||
xt.AddrTypeAnycast: "anycast",
|
||||
xt.AddrTypeMulticast: "multicast",
|
||||
xt.AddrTypeBlackhole: "blackhole",
|
||||
xt.AddrTypeUnreachable: "unreachable",
|
||||
xt.AddrTypeProhibit: "prohibit",
|
||||
xt.AddrTypeThrow: "throw",
|
||||
xt.AddrTypeNat: "nat",
|
||||
xt.AddrTypeXresolve: "xresolve",
|
||||
}
|
||||
34
vendor/tailscale.com/util/mak/mak.go
generated
vendored
34
vendor/tailscale.com/util/mak/mak.go
generated
vendored
@@ -5,11 +5,6 @@
|
||||
// things, notably to maps, but also slices.
|
||||
package mak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Set populates an entry in a map, making the map if necessary.
|
||||
//
|
||||
// That is, it assigns (*m)[k] = v, making *m if it was nil.
|
||||
@@ -20,35 +15,6 @@ func Set[K comparable, V any, T ~map[K]V](m *T, k K, v V) {
|
||||
(*m)[k] = v
|
||||
}
|
||||
|
||||
// NonNil takes a pointer to a Go data structure
|
||||
// (currently only a slice or a map) and makes sure it's non-nil for
|
||||
// JSON serialization. (In particular, JavaScript clients usually want
|
||||
// the field to be defined after they decode the JSON.)
|
||||
//
|
||||
// Deprecated: use NonNilSliceForJSON or NonNilMapForJSON instead.
|
||||
func NonNil(ptr any) {
|
||||
if ptr == nil {
|
||||
panic("nil interface")
|
||||
}
|
||||
rv := reflect.ValueOf(ptr)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
|
||||
}
|
||||
if rv.Pointer() == 0 {
|
||||
panic("nil pointer")
|
||||
}
|
||||
rv = rv.Elem()
|
||||
if rv.Pointer() != 0 {
|
||||
return
|
||||
}
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Slice:
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
|
||||
case reflect.Map:
|
||||
rv.Set(reflect.MakeMap(rv.Type()))
|
||||
}
|
||||
}
|
||||
|
||||
// NonNilSliceForJSON makes sure that *slicePtr is non-nil so it will
|
||||
// won't be omitted from JSON serialization and possibly confuse JavaScript
|
||||
// clients expecting it to be present.
|
||||
|
||||
136
vendor/tailscale.com/util/multierr/multierr.go
generated
vendored
136
vendor/tailscale.com/util/multierr/multierr.go
generated
vendored
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package multierr provides a simple multiple-error type.
|
||||
// It was inspired by github.com/go-multierror/multierror.
|
||||
package multierr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An Error represents multiple errors.
|
||||
type Error struct {
|
||||
errs []error
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e Error) Error() string {
|
||||
s := new(strings.Builder)
|
||||
s.WriteString("multiple errors:")
|
||||
for _, err := range e.errs {
|
||||
s.WriteString("\n\t")
|
||||
s.WriteString(err.Error())
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// Errors returns a slice containing all errors in e.
|
||||
func (e Error) Errors() []error {
|
||||
return slices.Clone(e.errs)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying errors as-is.
|
||||
func (e Error) Unwrap() []error {
|
||||
// Do not clone since Unwrap requires callers to not mutate the slice.
|
||||
// See the documentation in the Go "errors" package.
|
||||
return e.errs
|
||||
}
|
||||
|
||||
// New returns an error composed from errs.
|
||||
// Some errors in errs get special treatment:
|
||||
// - nil errors are discarded
|
||||
// - errors of type Error are expanded into the top level
|
||||
//
|
||||
// If the resulting slice has length 0, New returns nil.
|
||||
// If the resulting slice has length 1, New returns that error.
|
||||
// If the resulting slice has length > 1, New returns that slice as an Error.
|
||||
func New(errs ...error) error {
|
||||
// First count the number of errors to avoid allocating.
|
||||
var n int
|
||||
var errFirst error
|
||||
for _, e := range errs {
|
||||
switch e := e.(type) {
|
||||
case nil:
|
||||
continue
|
||||
case Error:
|
||||
n += len(e.errs)
|
||||
if errFirst == nil && len(e.errs) > 0 {
|
||||
errFirst = e.errs[0]
|
||||
}
|
||||
default:
|
||||
n++
|
||||
if errFirst == nil {
|
||||
errFirst = e
|
||||
}
|
||||
}
|
||||
}
|
||||
if n <= 1 {
|
||||
return errFirst // nil if n == 0
|
||||
}
|
||||
|
||||
// More than one error, allocate slice and construct the multi-error.
|
||||
dst := make([]error, 0, n)
|
||||
for _, e := range errs {
|
||||
switch e := e.(type) {
|
||||
case nil:
|
||||
continue
|
||||
case Error:
|
||||
dst = append(dst, e.errs...)
|
||||
default:
|
||||
dst = append(dst, e)
|
||||
}
|
||||
}
|
||||
return Error{errs: dst}
|
||||
}
|
||||
|
||||
// Is reports whether any error in e matches target.
|
||||
func (e Error) Is(target error) bool {
|
||||
for _, err := range e.errs {
|
||||
if errors.Is(err, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// As finds the first error in e that matches target, and if any is found,
|
||||
// sets target to that error value and returns true. Otherwise, it returns false.
|
||||
func (e Error) As(target any) bool {
|
||||
for _, err := range e.errs {
|
||||
if ok := errors.As(err, target); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Range performs a pre-order, depth-first iteration of the error tree
|
||||
// by successively unwrapping all error values.
|
||||
// For each iteration it calls fn with the current error value and
|
||||
// stops iteration if it ever reports false.
|
||||
func Range(err error, fn func(error) bool) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if !fn(err) {
|
||||
return false
|
||||
}
|
||||
switch err := err.(type) {
|
||||
case interface{ Unwrap() error }:
|
||||
if err := err.Unwrap(); err != nil {
|
||||
if !Range(err, fn) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case interface{ Unwrap() []error }:
|
||||
for _, err := range err.Unwrap() {
|
||||
if !Range(err, fn) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
8
vendor/tailscale.com/util/must/must.go
generated
vendored
8
vendor/tailscale.com/util/must/must.go
generated
vendored
@@ -23,3 +23,11 @@ func Get[T any](v T, err error) T {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Get2 returns v1 and v2 as is. It panics if err is non-nil.
|
||||
func Get2[T any, U any](v1 T, v2 U, err error) (T, U) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v1, v2
|
||||
}
|
||||
|
||||
10
vendor/tailscale.com/util/osdiag/zsyscall_windows.go
generated
vendored
10
vendor/tailscale.com/util/osdiag/zsyscall_windows.go
generated
vendored
@@ -51,7 +51,7 @@ var (
|
||||
)
|
||||
|
||||
func regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) {
|
||||
r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(pData)), uintptr(unsafe.Pointer(cbData)), 0)
|
||||
r0, _, _ := syscall.SyscallN(procRegEnumValueW.Addr(), uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(pData)), uintptr(unsafe.Pointer(cbData)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLe
|
||||
}
|
||||
|
||||
func globalMemoryStatusEx(memStatus *_MEMORYSTATUSEX) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGlobalMemoryStatusEx.Addr(), 1, uintptr(unsafe.Pointer(memStatus)), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procGlobalMemoryStatusEx.Addr(), uintptr(unsafe.Pointer(memStatus)))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -67,19 +67,19 @@ func globalMemoryStatusEx(memStatus *_MEMORYSTATUSEX) (err error) {
|
||||
}
|
||||
|
||||
func wscEnumProtocols(iProtocols *int32, protocolBuffer *wsaProtocolInfo, bufLen *uint32, errno *int32) (ret int32) {
|
||||
r0, _, _ := syscall.Syscall6(procWSCEnumProtocols.Addr(), 4, uintptr(unsafe.Pointer(iProtocols)), uintptr(unsafe.Pointer(protocolBuffer)), uintptr(unsafe.Pointer(bufLen)), uintptr(unsafe.Pointer(errno)), 0, 0)
|
||||
r0, _, _ := syscall.SyscallN(procWSCEnumProtocols.Addr(), uintptr(unsafe.Pointer(iProtocols)), uintptr(unsafe.Pointer(protocolBuffer)), uintptr(unsafe.Pointer(bufLen)), uintptr(unsafe.Pointer(errno)))
|
||||
ret = int32(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func wscGetProviderInfo(providerId *windows.GUID, infoType _WSC_PROVIDER_INFO_TYPE, info unsafe.Pointer, infoSize *uintptr, flags uint32, errno *int32) (ret int32) {
|
||||
r0, _, _ := syscall.Syscall6(procWSCGetProviderInfo.Addr(), 6, uintptr(unsafe.Pointer(providerId)), uintptr(infoType), uintptr(info), uintptr(unsafe.Pointer(infoSize)), uintptr(flags), uintptr(unsafe.Pointer(errno)))
|
||||
r0, _, _ := syscall.SyscallN(procWSCGetProviderInfo.Addr(), uintptr(unsafe.Pointer(providerId)), uintptr(infoType), uintptr(info), uintptr(unsafe.Pointer(infoSize)), uintptr(flags), uintptr(unsafe.Pointer(errno)))
|
||||
ret = int32(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func wscGetProviderPath(providerId *windows.GUID, providerDllPath *uint16, providerDllPathLen *int32, errno *int32) (ret int32) {
|
||||
r0, _, _ := syscall.Syscall6(procWSCGetProviderPath.Addr(), 4, uintptr(unsafe.Pointer(providerId)), uintptr(unsafe.Pointer(providerDllPath)), uintptr(unsafe.Pointer(providerDllPathLen)), uintptr(unsafe.Pointer(errno)), 0, 0)
|
||||
r0, _, _ := syscall.SyscallN(procWSCGetProviderPath.Addr(), uintptr(unsafe.Pointer(providerId)), uintptr(unsafe.Pointer(providerDllPath)), uintptr(unsafe.Pointer(providerDllPathLen)), uintptr(unsafe.Pointer(errno)))
|
||||
ret = int32(r0)
|
||||
return
|
||||
}
|
||||
|
||||
12
vendor/tailscale.com/util/osshare/filesharingstatus_noop.go
generated
vendored
12
vendor/tailscale.com/util/osshare/filesharingstatus_noop.go
generated
vendored
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package osshare
|
||||
|
||||
import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func SetFileSharingEnabled(enabled bool, logf logger.Logf) {}
|
||||
106
vendor/tailscale.com/util/osshare/filesharingstatus_windows.go
generated
vendored
106
vendor/tailscale.com/util/osshare/filesharingstatus_windows.go
generated
vendored
@@ -1,106 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package osshare provides utilities for enabling/disabling Taildrop file
|
||||
// sharing on Windows.
|
||||
package osshare
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
sendFileShellKey = `*\shell\tailscale`
|
||||
)
|
||||
|
||||
var ipnExePath struct {
|
||||
sync.Mutex
|
||||
cache string // absolute path of tailscale-ipn.exe, populated lazily on first use
|
||||
}
|
||||
|
||||
func getIpnExePath(logf logger.Logf) string {
|
||||
ipnExePath.Lock()
|
||||
defer ipnExePath.Unlock()
|
||||
|
||||
if ipnExePath.cache != "" {
|
||||
return ipnExePath.cache
|
||||
}
|
||||
|
||||
// Find the absolute path of tailscale-ipn.exe assuming that it's in the same
|
||||
// directory as this executable (tailscaled.exe).
|
||||
p, err := os.Executable()
|
||||
if err != nil {
|
||||
logf("os.Executable error: %v", err)
|
||||
return ""
|
||||
}
|
||||
if p, err = filepath.EvalSymlinks(p); err != nil {
|
||||
logf("filepath.EvalSymlinks error: %v", err)
|
||||
return ""
|
||||
}
|
||||
p = filepath.Join(filepath.Dir(p), "tailscale-ipn.exe")
|
||||
if p, err = filepath.Abs(p); err != nil {
|
||||
logf("filepath.Abs error: %v", err)
|
||||
return ""
|
||||
}
|
||||
ipnExePath.cache = p
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// SetFileSharingEnabled adds/removes "Send with Tailscale" from the Windows shell menu.
|
||||
func SetFileSharingEnabled(enabled bool, logf logger.Logf) {
|
||||
logf = logger.WithPrefix(logf, fmt.Sprintf("SetFileSharingEnabled(%v) error: ", enabled))
|
||||
if enabled {
|
||||
enableFileSharing(logf)
|
||||
} else {
|
||||
disableFileSharing(logf)
|
||||
}
|
||||
}
|
||||
|
||||
func enableFileSharing(logf logger.Logf) {
|
||||
path := getIpnExePath(logf)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
|
||||
k, _, err := registry.CreateKey(registry.CLASSES_ROOT, sendFileShellKey, registry.WRITE)
|
||||
if err != nil {
|
||||
logf("failed to create HKEY_CLASSES_ROOT\\%s reg key: %v", sendFileShellKey, err)
|
||||
return
|
||||
}
|
||||
defer k.Close()
|
||||
if err := k.SetStringValue("", "Send with Tailscale..."); err != nil {
|
||||
logf("k.SetStringValue error: %v", err)
|
||||
return
|
||||
}
|
||||
if err := k.SetStringValue("Icon", path+",0"); err != nil {
|
||||
logf("k.SetStringValue error: %v", err)
|
||||
return
|
||||
}
|
||||
c, _, err := registry.CreateKey(k, "command", registry.WRITE)
|
||||
if err != nil {
|
||||
logf("failed to create HKEY_CLASSES_ROOT\\%s\\command reg key: %v", sendFileShellKey, err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
if err := c.SetStringValue("", "\""+path+"\" /push \"%1\""); err != nil {
|
||||
logf("c.SetStringValue error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func disableFileSharing(logf logger.Logf) {
|
||||
if err := registry.DeleteKey(registry.CLASSES_ROOT, sendFileShellKey+"\\command"); err != nil &&
|
||||
err != registry.ErrNotExist {
|
||||
logf("registry.DeleteKey error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if err := registry.DeleteKey(registry.CLASSES_ROOT, sendFileShellKey); err != nil && err != registry.ErrNotExist {
|
||||
logf("registry.DeleteKey error: %v\n", err)
|
||||
}
|
||||
}
|
||||
4
vendor/tailscale.com/util/osuser/group_ids.go
generated
vendored
4
vendor/tailscale.com/util/osuser/group_ids.go
generated
vendored
@@ -19,6 +19,10 @@ import (
|
||||
// an error. It will first try to use the 'id' command to get the group IDs,
|
||||
// and if that fails, it will fall back to the user.GroupIds method.
|
||||
func GetGroupIds(user *user.User) ([]string, error) {
|
||||
if runtime.GOOS == "plan9" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
return user.GroupIds()
|
||||
}
|
||||
|
||||
23
vendor/tailscale.com/util/osuser/user.go
generated
vendored
23
vendor/tailscale.com/util/osuser/user.go
generated
vendored
@@ -54,9 +54,18 @@ func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, st
|
||||
// Skip getent entirely on Non-Unix platforms that won't ever have it.
|
||||
// (Using HasPrefix for "wasip1", anticipating that WASI support will
|
||||
// move beyond "preview 1" some day.)
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" {
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" || runtime.GOOS == "plan9" {
|
||||
var shell string
|
||||
if wantShell && runtime.GOOS == "plan9" {
|
||||
shell = "/bin/rc"
|
||||
}
|
||||
if runtime.GOOS == "plan9" {
|
||||
if u, err := user.Current(); err == nil {
|
||||
return u, shell, nil
|
||||
}
|
||||
}
|
||||
u, err := std(usernameOrUID)
|
||||
return u, "", err
|
||||
return u, shell, err
|
||||
}
|
||||
|
||||
// No getent on Gokrazy. So hard-code the login shell.
|
||||
@@ -78,6 +87,16 @@ func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, st
|
||||
return u, shell, nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "plan9" {
|
||||
return &user.User{
|
||||
Uid: "0",
|
||||
Gid: "0",
|
||||
Username: "glenda",
|
||||
Name: "Glenda",
|
||||
HomeDir: "/",
|
||||
}, "/bin/rc", nil
|
||||
}
|
||||
|
||||
// Start with getent if caller wants to get the user shell.
|
||||
if wantShell {
|
||||
return userLookupGetent(usernameOrUID, std)
|
||||
|
||||
39
vendor/tailscale.com/util/progresstracking/progresstracking.go
generated
vendored
39
vendor/tailscale.com/util/progresstracking/progresstracking.go
generated
vendored
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package progresstracking provides wrappers around io.Reader and io.Writer
|
||||
// that track progress.
|
||||
package progresstracking
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewReader wraps the given Reader with a progress tracking Reader that
|
||||
// reports progress at the following points:
|
||||
//
|
||||
// - First read
|
||||
// - Every read spaced at least interval since the prior read
|
||||
// - Last read
|
||||
func NewReader(r io.Reader, interval time.Duration, onProgress func(totalRead int, err error)) io.Reader {
|
||||
return &reader{Reader: r, interval: interval, onProgress: onProgress}
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
io.Reader
|
||||
interval time.Duration
|
||||
onProgress func(int, error)
|
||||
lastTracked time.Time
|
||||
totalRead int
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (int, error) {
|
||||
n, err := r.Reader.Read(p)
|
||||
r.totalRead += n
|
||||
if time.Since(r.lastTracked) > r.interval || err != nil {
|
||||
r.onProgress(r.totalRead, err)
|
||||
r.lastTracked = time.Now()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
79
vendor/tailscale.com/util/ringbuffer/ringbuffer.go
generated
vendored
79
vendor/tailscale.com/util/ringbuffer/ringbuffer.go
generated
vendored
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ringbuffer contains a fixed-size concurrency-safe generic ring
|
||||
// buffer.
|
||||
package ringbuffer
|
||||
|
||||
import "sync"
|
||||
|
||||
// New creates a new RingBuffer containing at most max items.
|
||||
func New[T any](max int) *RingBuffer[T] {
|
||||
return &RingBuffer[T]{
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
// RingBuffer is a concurrency-safe ring buffer.
|
||||
type RingBuffer[T any] struct {
|
||||
mu sync.Mutex
|
||||
pos int
|
||||
buf []T
|
||||
max int
|
||||
}
|
||||
|
||||
// Add appends a new item to the RingBuffer, possibly overwriting the oldest
|
||||
// item in the buffer if it is already full.
|
||||
//
|
||||
// It does nothing if rb is nil.
|
||||
func (rb *RingBuffer[T]) Add(t T) {
|
||||
if rb == nil {
|
||||
return
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
if len(rb.buf) < rb.max {
|
||||
rb.buf = append(rb.buf, t)
|
||||
} else {
|
||||
rb.buf[rb.pos] = t
|
||||
rb.pos = (rb.pos + 1) % rb.max
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns a copy of all the entries in the ring buffer in the order they
|
||||
// were added.
|
||||
//
|
||||
// It returns nil if rb is nil.
|
||||
func (rb *RingBuffer[T]) GetAll() []T {
|
||||
if rb == nil {
|
||||
return nil
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
out := make([]T, len(rb.buf))
|
||||
for i := range len(rb.buf) {
|
||||
x := (rb.pos + i) % rb.max
|
||||
out[i] = rb.buf[x]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the ring buffer. Note that this value
|
||||
// could change immediately after being returned if a concurrent caller
|
||||
// modifies the buffer.
|
||||
func (rb *RingBuffer[T]) Len() int {
|
||||
if rb == nil {
|
||||
return 0
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
return len(rb.buf)
|
||||
}
|
||||
|
||||
// Clear will empty the ring buffer.
|
||||
func (rb *RingBuffer[T]) Clear() {
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
rb.pos = 0
|
||||
rb.buf = nil
|
||||
}
|
||||
78
vendor/tailscale.com/util/ringlog/ringlog.go
generated
vendored
Normal file
78
vendor/tailscale.com/util/ringlog/ringlog.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ringlog contains a limited-size concurrency-safe generic ring log.
|
||||
package ringlog
|
||||
|
||||
import "tailscale.com/syncs"
|
||||
|
||||
// New creates a new [RingLog] containing at most max items.
|
||||
func New[T any](max int) *RingLog[T] {
|
||||
return &RingLog[T]{
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
// RingLog is a concurrency-safe fixed size log window containing entries of [T].
|
||||
type RingLog[T any] struct {
|
||||
mu syncs.Mutex
|
||||
pos int
|
||||
buf []T
|
||||
max int
|
||||
}
|
||||
|
||||
// Add appends a new item to the [RingLog], possibly overwriting the oldest
|
||||
// item in the log if it is already full.
|
||||
//
|
||||
// It does nothing if rb is nil.
|
||||
func (rb *RingLog[T]) Add(t T) {
|
||||
if rb == nil {
|
||||
return
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
if len(rb.buf) < rb.max {
|
||||
rb.buf = append(rb.buf, t)
|
||||
} else {
|
||||
rb.buf[rb.pos] = t
|
||||
rb.pos = (rb.pos + 1) % rb.max
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns a copy of all the entries in the ring log in the order they
|
||||
// were added.
|
||||
//
|
||||
// It returns nil if rb is nil.
|
||||
func (rb *RingLog[T]) GetAll() []T {
|
||||
if rb == nil {
|
||||
return nil
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
out := make([]T, len(rb.buf))
|
||||
for i := range len(rb.buf) {
|
||||
x := (rb.pos + i) % rb.max
|
||||
out[i] = rb.buf[x]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the ring log. Note that this value
|
||||
// could change immediately after being returned if a concurrent caller
|
||||
// modifies the log.
|
||||
func (rb *RingLog[T]) Len() int {
|
||||
if rb == nil {
|
||||
return 0
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
return len(rb.buf)
|
||||
}
|
||||
|
||||
// Clear will empty the ring log.
|
||||
func (rb *RingLog[T]) Clear() {
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
rb.pos = 0
|
||||
rb.buf = nil
|
||||
}
|
||||
16
vendor/tailscale.com/util/set/handle.go
generated
vendored
16
vendor/tailscale.com/util/set/handle.go
generated
vendored
@@ -9,20 +9,28 @@ package set
|
||||
type HandleSet[T any] map[Handle]T
|
||||
|
||||
// Handle is an opaque comparable value that's used as the map key in a
|
||||
// HandleSet. The only way to get one is to call HandleSet.Add.
|
||||
// HandleSet.
|
||||
type Handle struct {
|
||||
v *byte
|
||||
}
|
||||
|
||||
// NewHandle returns a new handle value.
|
||||
func NewHandle() Handle {
|
||||
return Handle{new(byte)}
|
||||
}
|
||||
|
||||
// Add adds the element (map value) e to the set.
|
||||
//
|
||||
// It returns the handle (map key) with which e can be removed, using a map
|
||||
// delete.
|
||||
// It returns a new handle (map key) with which e can be removed, using a map
|
||||
// delete or the [HandleSet.Delete] method.
|
||||
func (s *HandleSet[T]) Add(e T) Handle {
|
||||
h := Handle{new(byte)}
|
||||
h := NewHandle()
|
||||
if *s == nil {
|
||||
*s = make(HandleSet[T])
|
||||
}
|
||||
(*s)[h] = e
|
||||
return h
|
||||
}
|
||||
|
||||
// Delete removes the element with handle h from the set.
|
||||
func (s HandleSet[T]) Delete(h Handle) { delete(s, h) }
|
||||
|
||||
198
vendor/tailscale.com/util/set/intset.go
generated
vendored
Normal file
198
vendor/tailscale.com/util/set/intset.go
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
"math/bits"
|
||||
"math/rand/v2"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// IntSet is a set optimized for integer values close to zero
|
||||
// or set of integers that are close in value.
|
||||
type IntSet[T constraints.Integer] struct {
|
||||
// bits is a [bitSet] for numbers less than [bits.UintSize].
|
||||
bits bitSet
|
||||
|
||||
// extra is a mapping of [bitSet] for numbers not in bits,
|
||||
// where the key is a number modulo [bits.UintSize].
|
||||
extra map[uint64]bitSet
|
||||
|
||||
// extraLen is the count of numbers in extra since len(extra)
|
||||
// does not reflect that each bitSet may have multiple numbers.
|
||||
extraLen int
|
||||
}
|
||||
|
||||
// IntsOf constructs an [IntSet] with the provided elements.
|
||||
func IntsOf[T constraints.Integer](slice ...T) IntSet[T] {
|
||||
var s IntSet[T]
|
||||
for _, e := range slice {
|
||||
s.Add(e)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Values returns an iterator over the elements of the set.
|
||||
// The iterator will yield the elements in no particular order.
|
||||
func (s IntSet[T]) Values() iter.Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
if s.bits != 0 {
|
||||
for i := range s.bits.values() {
|
||||
if !yield(decodeZigZag[T](i)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.extra != nil {
|
||||
for hi, bs := range s.extra {
|
||||
for lo := range bs.values() {
|
||||
if !yield(decodeZigZag[T](hi*bits.UintSize + lo)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contains reports whether e is in the set.
|
||||
func (s IntSet[T]) Contains(e T) bool {
|
||||
if v := encodeZigZag(e); v < bits.UintSize {
|
||||
return s.bits.contains(v)
|
||||
} else {
|
||||
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
|
||||
return s.extra[hi].contains(lo)
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds e to the set.
|
||||
//
|
||||
// When storing a IntSet in a map as a value type,
|
||||
// it is important to re-assign the map entry after calling Add or Delete,
|
||||
// as the IntSet's representation may change.
|
||||
func (s *IntSet[T]) Add(e T) {
|
||||
if v := encodeZigZag(e); v < bits.UintSize {
|
||||
s.bits.add(v)
|
||||
} else {
|
||||
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
|
||||
if bs := s.extra[hi]; !bs.contains(lo) {
|
||||
bs.add(lo)
|
||||
mak.Set(&s.extra, hi, bs)
|
||||
s.extra[hi] = bs
|
||||
s.extraLen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddSeq adds the values from seq to the set.
|
||||
func (s *IntSet[T]) AddSeq(seq iter.Seq[T]) {
|
||||
for e := range seq {
|
||||
s.Add(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Len reports the number of elements in the set.
|
||||
func (s IntSet[T]) Len() int {
|
||||
return s.bits.len() + s.extraLen
|
||||
}
|
||||
|
||||
// Delete removes e from the set.
|
||||
//
|
||||
// When storing a IntSet in a map as a value type,
|
||||
// it is important to re-assign the map entry after calling Add or Delete,
|
||||
// as the IntSet's representation may change.
|
||||
func (s *IntSet[T]) Delete(e T) {
|
||||
if v := encodeZigZag(e); v < bits.UintSize {
|
||||
s.bits.delete(v)
|
||||
} else {
|
||||
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
|
||||
if bs := s.extra[hi]; bs.contains(lo) {
|
||||
bs.delete(lo)
|
||||
mak.Set(&s.extra, hi, bs)
|
||||
s.extra[hi] = bs
|
||||
s.extraLen--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteSeq deletes the values in seq from the set.
|
||||
func (s *IntSet[T]) DeleteSeq(seq iter.Seq[T]) {
|
||||
for e := range seq {
|
||||
s.Delete(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Equal reports whether s is equal to other.
|
||||
func (s IntSet[T]) Equal(other IntSet[T]) bool {
|
||||
for hi, bits := range s.extra {
|
||||
if other.extra[hi] != bits {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s.extraLen == other.extraLen && s.bits == other.bits
|
||||
}
|
||||
|
||||
// Clone returns a copy of s that doesn't alias the original.
|
||||
func (s IntSet[T]) Clone() IntSet[T] {
|
||||
return IntSet[T]{
|
||||
bits: s.bits,
|
||||
extra: maps.Clone(s.extra),
|
||||
extraLen: s.extraLen,
|
||||
}
|
||||
}
|
||||
|
||||
type bitSet uint
|
||||
|
||||
func (s bitSet) values() iter.Seq[uint64] {
|
||||
return func(yield func(uint64) bool) {
|
||||
// Hyrum-proofing: randomly iterate in forwards or reverse.
|
||||
if rand.Uint64()%2 == 0 {
|
||||
for i := 0; i < bits.UintSize; i++ {
|
||||
if s.contains(uint64(i)) && !yield(uint64(i)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := bits.UintSize; i >= 0; i-- {
|
||||
if s.contains(uint64(i)) && !yield(uint64(i)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s bitSet) len() int { return bits.OnesCount(uint(s)) }
|
||||
func (s bitSet) contains(i uint64) bool { return s&(1<<i) > 0 }
|
||||
func (s *bitSet) add(i uint64) { *s |= 1 << i }
|
||||
func (s *bitSet) delete(i uint64) { *s &= ^(1 << i) }
|
||||
|
||||
// encodeZigZag encodes an integer as an unsigned integer ensuring that
|
||||
// negative integers near zero still have a near zero positive value.
|
||||
// For unsigned integers, it returns the value verbatim.
|
||||
func encodeZigZag[T constraints.Integer](v T) uint64 {
|
||||
var zero T
|
||||
if ^zero >= 0 { // must be constraints.Unsigned
|
||||
return uint64(v)
|
||||
} else { // must be constraints.Signed
|
||||
// See [google.golang.org/protobuf/encoding/protowire.EncodeZigZag]
|
||||
return uint64(int64(v)<<1) ^ uint64(int64(v)>>63)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeZigZag decodes an unsigned integer as an integer ensuring that
|
||||
// negative integers near zero still have a near zero positive value.
|
||||
// For unsigned integers, it returns the value verbatim.
|
||||
func decodeZigZag[T constraints.Integer](v uint64) T {
|
||||
var zero T
|
||||
if ^zero >= 0 { // must be constraints.Unsigned
|
||||
return T(v)
|
||||
} else { // must be constraints.Signed
|
||||
// See [google.golang.org/protobuf/encoding/protowire.DecodeZigZag]
|
||||
return T(int64(v>>1) ^ int64(v)<<63>>63)
|
||||
}
|
||||
}
|
||||
148
vendor/tailscale.com/util/set/smallset.go
generated
vendored
Normal file
148
vendor/tailscale.com/util/set/smallset.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// SmallSet is a set that is optimized for reducing memory overhead when the
|
||||
// expected size of the set is 0 or 1 elements.
|
||||
//
|
||||
// The zero value of SmallSet is a usable empty set.
|
||||
//
|
||||
// When storing a SmallSet in a map as a value type, it is important to re-assign
|
||||
// the map entry after calling Add or Delete, as the SmallSet's representation
|
||||
// may change.
|
||||
//
|
||||
// Copying a SmallSet by value may alias the previous value. Use the Clone method
|
||||
// to create a new SmallSet with the same contents.
|
||||
type SmallSet[T comparable] struct {
|
||||
_ structs.Incomparable // to prevent == mistakes
|
||||
one T // if non-zero, then single item in set
|
||||
m Set[T] // if non-nil, the set of items, which might be size 1 if it's the zero value of T
|
||||
}
|
||||
|
||||
// Values returns an iterator over the elements of the set.
|
||||
// The iterator will yield the elements in no particular order.
|
||||
func (s SmallSet[T]) Values() iter.Seq[T] {
|
||||
if s.m != nil {
|
||||
return maps.Keys(s.m)
|
||||
}
|
||||
var zero T
|
||||
return func(yield func(T) bool) {
|
||||
if s.one != zero {
|
||||
yield(s.one)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contains reports whether e is in the set.
|
||||
func (s SmallSet[T]) Contains(e T) bool {
|
||||
if s.m != nil {
|
||||
return s.m.Contains(e)
|
||||
}
|
||||
var zero T
|
||||
return e != zero && s.one == e
|
||||
}
|
||||
|
||||
// SoleElement returns the single value in the set, if the set has exactly one
|
||||
// element.
|
||||
//
|
||||
// If the set is empty or has more than one element, ok will be false and e will
|
||||
// be the zero value of T.
|
||||
func (s SmallSet[T]) SoleElement() (e T, ok bool) {
|
||||
return s.one, s.Len() == 1
|
||||
}
|
||||
|
||||
// Add adds e to the set.
|
||||
//
|
||||
// When storing a SmallSet in a map as a value type, it is important to
|
||||
// re-assign the map entry after calling Add or Delete, as the SmallSet's
|
||||
// representation may change.
|
||||
func (s *SmallSet[T]) Add(e T) {
|
||||
var zero T
|
||||
if s.m != nil {
|
||||
s.m.Add(e)
|
||||
return
|
||||
}
|
||||
// Non-zero elements can go into s.one.
|
||||
if e != zero {
|
||||
if s.one == zero {
|
||||
s.one = e // Len 0 to Len 1
|
||||
return
|
||||
}
|
||||
if s.one == e {
|
||||
return // dup
|
||||
}
|
||||
}
|
||||
// Need to make a multi map, either
|
||||
// because we now have two items, or
|
||||
// because e is the zero value.
|
||||
s.m = Set[T]{}
|
||||
if s.one != zero {
|
||||
s.m.Add(s.one) // move single item to multi
|
||||
}
|
||||
s.m.Add(e) // add new item, possibly zero
|
||||
s.one = zero
|
||||
}
|
||||
|
||||
// Len reports the number of elements in the set.
|
||||
func (s SmallSet[T]) Len() int {
|
||||
var zero T
|
||||
if s.m != nil {
|
||||
return s.m.Len()
|
||||
}
|
||||
if s.one != zero {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Delete removes e from the set.
|
||||
//
|
||||
// When storing a SmallSet in a map as a value type, it is important to
|
||||
// re-assign the map entry after calling Add or Delete, as the SmallSet's
|
||||
// representation may change.
|
||||
func (s *SmallSet[T]) Delete(e T) {
|
||||
var zero T
|
||||
if s.m == nil {
|
||||
if s.one == e {
|
||||
s.one = zero
|
||||
}
|
||||
return
|
||||
}
|
||||
s.m.Delete(e)
|
||||
|
||||
// If the map size drops to zero, that means
|
||||
// it only contained the zero value of T.
|
||||
if s.m.Len() == 0 {
|
||||
s.m = nil
|
||||
return
|
||||
}
|
||||
|
||||
// If the map size drops to one element and doesn't
|
||||
// contain the zero value, we can switch back to the
|
||||
// single-item representation.
|
||||
if s.m.Len() == 1 {
|
||||
for v := range s.m {
|
||||
if v != zero {
|
||||
s.one = v
|
||||
s.m = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clone returns a copy of s that doesn't alias the original.
|
||||
func (s SmallSet[T]) Clone() SmallSet[T] {
|
||||
return SmallSet[T]{
|
||||
one: s.one,
|
||||
m: maps.Clone(s.m), // preserves nilness
|
||||
}
|
||||
}
|
||||
117
vendor/tailscale.com/util/syspolicy/handler.go
generated
vendored
117
vendor/tailscale.com/util/syspolicy/handler.go
generated
vendored
@@ -1,117 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
// TODO(nickkhyl): delete this file once other repos are updated.
|
||||
|
||||
// Handler reads system policies from OS-specific storage.
|
||||
//
|
||||
// Deprecated: implementing a [source.Store] should be preferred.
|
||||
type Handler interface {
|
||||
// ReadString reads the policy setting's string value for the given key.
|
||||
// It should return ErrNoSuchKey if the key does not have a value set.
|
||||
ReadString(key string) (string, error)
|
||||
// ReadUInt64 reads the policy setting's uint64 value for the given key.
|
||||
// It should return ErrNoSuchKey if the key does not have a value set.
|
||||
ReadUInt64(key string) (uint64, error)
|
||||
// ReadBool reads the policy setting's boolean value for the given key.
|
||||
// It should return ErrNoSuchKey if the key does not have a value set.
|
||||
ReadBoolean(key string) (bool, error)
|
||||
// ReadStringArray reads the policy setting's string array value for the given key.
|
||||
// It should return ErrNoSuchKey if the key does not have a value set.
|
||||
ReadStringArray(key string) ([]string, error)
|
||||
}
|
||||
|
||||
// RegisterHandler wraps and registers the specified handler as the device's
|
||||
// policy [source.Store] for the program's lifetime.
|
||||
//
|
||||
// Deprecated: using [RegisterStore] should be preferred.
|
||||
func RegisterHandler(h Handler) {
|
||||
rsop.RegisterStore("DeviceHandler", setting.DeviceScope, WrapHandler(h))
|
||||
}
|
||||
|
||||
// TB is a subset of testing.TB that we use to set up test helpers.
|
||||
// It's defined here to avoid pulling in the testing package.
|
||||
type TB = internal.TB
|
||||
|
||||
// SetHandlerForTest wraps and sets the specified handler as the device's policy
|
||||
// [source.Store] for the duration of tb.
|
||||
//
|
||||
// Deprecated: using [MustRegisterStoreForTest] should be preferred.
|
||||
func SetHandlerForTest(tb TB, h Handler) {
|
||||
RegisterWellKnownSettingsForTest(tb)
|
||||
MustRegisterStoreForTest(tb, "DeviceHandler-TestOnly", setting.DefaultScope(), WrapHandler(h))
|
||||
}
|
||||
|
||||
var _ source.Store = (*handlerStore)(nil)
|
||||
|
||||
// handlerStore is a [source.Store] that calls the underlying [Handler].
|
||||
//
|
||||
// TODO(nickkhyl): remove it when the corp and android repos are updated.
|
||||
type handlerStore struct {
|
||||
h Handler
|
||||
}
|
||||
|
||||
// WrapHandler returns a [source.Store] that wraps the specified [Handler].
|
||||
func WrapHandler(h Handler) source.Store {
|
||||
return handlerStore{h}
|
||||
}
|
||||
|
||||
// Lock implements [source.Lockable].
|
||||
func (s handlerStore) Lock() error {
|
||||
if lockable, ok := s.h.(source.Lockable); ok {
|
||||
return lockable.Lock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock implements [source.Lockable].
|
||||
func (s handlerStore) Unlock() {
|
||||
if lockable, ok := s.h.(source.Lockable); ok {
|
||||
lockable.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChangeCallback implements [source.Changeable].
|
||||
func (s handlerStore) RegisterChangeCallback(callback func()) (unregister func(), err error) {
|
||||
if changeable, ok := s.h.(source.Changeable); ok {
|
||||
return changeable.RegisterChangeCallback(callback)
|
||||
}
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
// ReadString implements [source.Store].
|
||||
func (s handlerStore) ReadString(key setting.Key) (string, error) {
|
||||
return s.h.ReadString(string(key))
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [source.Store].
|
||||
func (s handlerStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
return s.h.ReadUInt64(string(key))
|
||||
}
|
||||
|
||||
// ReadBoolean implements [source.Store].
|
||||
func (s handlerStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
return s.h.ReadBoolean(string(key))
|
||||
}
|
||||
|
||||
// ReadStringArray implements [source.Store].
|
||||
func (s handlerStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
return s.h.ReadStringArray(string(key))
|
||||
}
|
||||
|
||||
// Done implements [source.Expirable].
|
||||
func (s handlerStore) Done() <-chan struct{} {
|
||||
if expirable, ok := s.h.(source.Expirable); ok {
|
||||
return expirable.Done()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
15
vendor/tailscale.com/util/syspolicy/internal/internal.go
generated
vendored
15
vendor/tailscale.com/util/syspolicy/internal/internal.go
generated
vendored
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -25,22 +26,10 @@ func OS() string {
|
||||
return OSForTesting.Get(version.OS)
|
||||
}
|
||||
|
||||
// TB is a subset of testing.TB that we use to set up test helpers.
|
||||
// It's defined here to avoid pulling in the testing package.
|
||||
type TB interface {
|
||||
Helper()
|
||||
Cleanup(func())
|
||||
Logf(format string, args ...any)
|
||||
Error(args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
Fatal(args ...any)
|
||||
Fatalf(format string, args ...any)
|
||||
}
|
||||
|
||||
// EqualJSONForTest compares the JSON in j1 and j2 for semantic equality.
|
||||
// It returns "", "", true if j1 and j2 are equal. Otherwise, it returns
|
||||
// indented versions of j1 and j2 and false.
|
||||
func EqualJSONForTest(tb TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool) {
|
||||
func EqualJSONForTest(tb testenv.TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool) {
|
||||
tb.Helper()
|
||||
j1 = j1.Clone()
|
||||
j2 = j2.Clone()
|
||||
|
||||
4
vendor/tailscale.com/util/syspolicy/internal/loggerx/logger.go
generated
vendored
4
vendor/tailscale.com/util/syspolicy/internal/loggerx/logger.go
generated
vendored
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,7 +58,7 @@ func verbosef(format string, args ...any) {
|
||||
|
||||
// SetForTest sets the specified printf and verbosef functions for the duration
|
||||
// of tb and its subtests.
|
||||
func SetForTest(tb internal.TB, printf, verbosef logger.Logf) {
|
||||
func SetForTest(tb testenv.TB, printf, verbosef logger.Logf) {
|
||||
lazyPrintf.SetForTest(tb, printf, nil)
|
||||
lazyVerbosef.SetForTest(tb, verbosef, nil)
|
||||
}
|
||||
|
||||
9
vendor/tailscale.com/util/syspolicy/internal/metrics/metrics.go
generated
vendored
9
vendor/tailscale.com/util/syspolicy/internal/metrics/metrics.go
generated
vendored
@@ -17,6 +17,7 @@ import (
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
@@ -209,7 +210,7 @@ func scopeMetrics(origin *setting.Origin) *policyScopeMetrics {
|
||||
|
||||
var (
|
||||
settingMetricsMu sync.RWMutex
|
||||
settingMetricsMap map[setting.Key]*settingMetrics
|
||||
settingMetricsMap map[pkey.Key]*settingMetrics
|
||||
)
|
||||
|
||||
func settingMetricsFor(setting *setting.Definition) *settingMetrics {
|
||||
@@ -259,7 +260,7 @@ var addMetricTestHook, setMetricTestHook syncs.AtomicValue[metricFn]
|
||||
|
||||
// SetHooksForTest sets the specified addMetric and setMetric functions
|
||||
// as the metric functions for the duration of tb and all its subtests.
|
||||
func SetHooksForTest(tb internal.TB, addMetric, setMetric metricFn) {
|
||||
func SetHooksForTest(tb testenv.TB, addMetric, setMetric metricFn) {
|
||||
oldAddMetric := addMetricTestHook.Swap(addMetric)
|
||||
oldSetMetric := setMetricTestHook.Swap(setMetric)
|
||||
tb.Cleanup(func() {
|
||||
@@ -283,8 +284,8 @@ func SetHooksForTest(tb internal.TB, addMetric, setMetric metricFn) {
|
||||
lazyUserMetrics.SetForTest(tb, newScopeMetrics(setting.UserSetting), nil)
|
||||
}
|
||||
|
||||
func newSettingMetric(key setting.Key, scope setting.Scope, suffix string, typ clientmetric.Type) metric {
|
||||
name := strings.ReplaceAll(string(key), string(setting.KeyPathSeparator), "_")
|
||||
func newSettingMetric(key pkey.Key, scope setting.Scope, suffix string, typ clientmetric.Type) metric {
|
||||
name := strings.ReplaceAll(string(key), string(pkey.KeyPathSeparator), "_")
|
||||
name = strings.ReplaceAll(name, ".", "_") // dots are not allowed in metric names
|
||||
return newMetric([]string{name, metricScopeName(scope), suffix}, typ)
|
||||
}
|
||||
|
||||
5
vendor/tailscale.com/util/syspolicy/internal/metrics/test_handler.go
generated
vendored
5
vendor/tailscale.com/util/syspolicy/internal/metrics/test_handler.go
generated
vendored
@@ -9,6 +9,7 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// TestState represents a metric name and its expected value.
|
||||
@@ -19,13 +20,13 @@ type TestState struct {
|
||||
|
||||
// TestHandler facilitates testing of the code that uses metrics.
|
||||
type TestHandler struct {
|
||||
t internal.TB
|
||||
t testenv.TB
|
||||
|
||||
m map[string]int64
|
||||
}
|
||||
|
||||
// NewTestHandler returns a new TestHandler.
|
||||
func NewTestHandler(t internal.TB) *TestHandler {
|
||||
func NewTestHandler(t testenv.TB) *TestHandler {
|
||||
return &TestHandler{t, make(map[string]int64)}
|
||||
}
|
||||
|
||||
|
||||
190
vendor/tailscale.com/util/syspolicy/pkey/pkey.go
generated
vendored
Normal file
190
vendor/tailscale.com/util/syspolicy/pkey/pkey.go
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package pkey defines the keys used to store system policies in the registry.
|
||||
//
|
||||
// This is a leaf package meant to only contain string constants, not code.
|
||||
package pkey
|
||||
|
||||
// Key is a string that uniquely identifies a policy and must remain unchanged
|
||||
// once established and documented for a given policy setting. It may contain
|
||||
// alphanumeric characters and zero or more [KeyPathSeparator]s to group
|
||||
// individual policy settings into categories.
|
||||
type Key string
|
||||
|
||||
// KeyPathSeparator allows logical grouping of policy settings into categories.
|
||||
const KeyPathSeparator = '/'
|
||||
|
||||
// The const block below lists known policy keys.
|
||||
// When adding a key to this list, remember to add a corresponding
|
||||
// [setting.Definition] to [implicitDefinitions] in util/syspolicy/policy_keys.go.
|
||||
// Otherwise, the [TestKnownKeysRegistered] test will fail as a reminder.
|
||||
|
||||
const (
|
||||
// Keys with a string value
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
Tailnet Key = "Tailnet" // default ""; if blank, no tailnet name is sent to the server.
|
||||
|
||||
// AlwaysOn is a boolean key that controls whether Tailscale
|
||||
// should always remain in a connected state, and the user should
|
||||
// not be able to disconnect at their discretion.
|
||||
//
|
||||
// Warning: This policy setting is experimental and may change or be removed in the future.
|
||||
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
|
||||
// See tailscale/corp#26247, tailscale/corp#26248 and tailscale/corp#26249 for more information.
|
||||
AlwaysOn Key = "AlwaysOn.Enabled"
|
||||
|
||||
// AlwaysOnOverrideWithReason is a boolean key that alters the behavior
|
||||
// of [AlwaysOn]. When true, the user is allowed to disconnect Tailscale
|
||||
// by providing a reason. The reason is logged and sent to the control
|
||||
// for auditing purposes. It has no effect when [AlwaysOn] is false.
|
||||
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
|
||||
|
||||
// ReconnectAfter is a string value formatted for use with time.ParseDuration()
|
||||
// that defines the duration after which the client should automatically reconnect
|
||||
// to the Tailscale network following a user-initiated disconnect.
|
||||
// An empty string or a zero duration disables automatic reconnection.
|
||||
ReconnectAfter Key = "ReconnectAfter"
|
||||
|
||||
// AllowTailscaledRestart is a boolean key that controls whether users with write access
|
||||
// to the LocalAPI are allowed to shutdown tailscaled with the intention of restarting it.
|
||||
// On Windows, tailscaled will be restarted automatically by the service process
|
||||
// (see babysitProc in cmd/tailscaled/tailscaled_windows.go).
|
||||
// On other platforms, it is the client's responsibility to restart tailscaled.
|
||||
AllowTailscaledRestart Key = "AllowTailscaledRestart"
|
||||
|
||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
||||
// Exit node ID takes precedence over exit node IP.
|
||||
// To find the node ID, go to /api.md#device.
|
||||
ExitNodeID Key = "ExitNodeID"
|
||||
ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
|
||||
|
||||
// AllowExitNodeOverride is a boolean key that allows the user to override exit node policy settings
|
||||
// and manually select an exit node. It does not allow disabling exit node usage entirely.
|
||||
// It is typically used in conjunction with [ExitNodeID] set to "auto:any".
|
||||
//
|
||||
// Warning: This policy setting is experimental and may change, be renamed or removed in the future.
|
||||
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
|
||||
// See tailscale/corp#29969.
|
||||
AllowExitNodeOverride Key = "ExitNode.AllowOverride"
|
||||
|
||||
// Keys with a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated. Enforcement of
|
||||
// these policies is typically performed in ipnlocal.applySysPolicy(). GUIs
|
||||
// typically hide menu items related to policies that are enforced.
|
||||
EnableIncomingConnections Key = "AllowIncomingConnections"
|
||||
EnableServerMode Key = "UnattendedMode"
|
||||
ExitNodeAllowLANAccess Key = "ExitNodeAllowLANAccess"
|
||||
EnableTailscaleDNS Key = "UseTailscaleDNSSettings"
|
||||
EnableTailscaleSubnets Key = "UseTailscaleSubnets"
|
||||
|
||||
// EnableDNSRegistration is a string value that can be set to "always", "never"
|
||||
// or "user-decides". It controls whether DNS registration and dynamic DNS
|
||||
// updates are enabled for the Tailscale interface. For historical reasons
|
||||
// and to maintain compatibility with existing setups, the default is "never".
|
||||
// It is only used on Windows.
|
||||
EnableDNSRegistration Key = "EnableDNSRegistration"
|
||||
|
||||
// CheckUpdates is the key to signal if the updater should periodically
|
||||
// check for updates.
|
||||
CheckUpdates Key = "CheckUpdates"
|
||||
// ApplyUpdates is the key to signal if updates should be automatically
|
||||
// installed. Its value is "InstallUpdates" because of an awkwardly-named
|
||||
// visibility option "ApplyUpdates" on MacOS.
|
||||
ApplyUpdates Key = "InstallUpdates"
|
||||
// EnableRunExitNode controls if the device acts as an exit node. Even when
|
||||
// running as an exit node, the device must be approved by a tailnet
|
||||
// administrator. Its name is slightly awkward because RunExitNodeVisibility
|
||||
// predates this option but is preserved for backwards compatibility.
|
||||
EnableRunExitNode Key = "AdvertiseExitNode"
|
||||
|
||||
// Keys with a string value that controls visibility: "show", "hide".
|
||||
// The default is "show" unless otherwise stated. Enforcement of these
|
||||
// policies is typically performed by the UI code for the relevant operating
|
||||
// system.
|
||||
AdminConsoleVisibility Key = "AdminConsole"
|
||||
NetworkDevicesVisibility Key = "NetworkDevices"
|
||||
TestMenuVisibility Key = "TestMenu"
|
||||
UpdateMenuVisibility Key = "UpdateMenu"
|
||||
ResetToDefaultsVisibility Key = "ResetToDefaults"
|
||||
// RunExitNodeVisibility controls if the "run as exit node" menu item is
|
||||
// visible, without controlling the setting itself. This is preserved for
|
||||
// backwards compatibility but prefer EnableRunExitNode in new deployments.
|
||||
RunExitNodeVisibility Key = "RunExitNode"
|
||||
PreferencesMenuVisibility Key = "PreferencesMenu"
|
||||
ExitNodeMenuVisibility Key = "ExitNodesPicker"
|
||||
// AutoUpdateVisibility is the key to signal if the menu item for automatic
|
||||
// installation of updates should be visible. It is only used by macsys
|
||||
// installations and uses the Sparkle naming convention, even though it does
|
||||
// not actually control updates, merely the UI for that setting.
|
||||
AutoUpdateVisibility Key = "ApplyUpdates"
|
||||
// SuggestedExitNodeVisibility controls the visibility of suggested exit nodes in the client GUI.
|
||||
// When this system policy is set to 'hide', an exit node suggestion won't be presented to the user as part of the exit nodes picker.
|
||||
SuggestedExitNodeVisibility Key = "SuggestedExitNode"
|
||||
// OnboardingFlowVisibility controls the visibility of the onboarding flow in the client GUI.
|
||||
// When this system policy is set to 'hide', the onboarding flow is never shown to the user.
|
||||
OnboardingFlowVisibility Key = "OnboardingFlow"
|
||||
|
||||
// Keys with a string value formatted for use with time.ParseDuration().
|
||||
KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
|
||||
|
||||
// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
|
||||
// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
|
||||
// The default is 0 unless otherwise stated.
|
||||
LogSCMInteractions Key = "LogSCMInteractions"
|
||||
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
|
||||
|
||||
// EncryptState is a boolean setting that specifies whether to encrypt the
|
||||
// tailscaled state file.
|
||||
// Windows and Linux use a TPM device, Apple uses the Keychain.
|
||||
// It's a noop on other platforms.
|
||||
EncryptState Key = "EncryptState"
|
||||
|
||||
// HardwareAttestation is a boolean key that controls whether to use a
|
||||
// hardware-backed key to bind the node identity to this device.
|
||||
HardwareAttestation Key = "HardwareAttestation"
|
||||
|
||||
// PostureChecking indicates if posture checking is enabled and the client shall gather
|
||||
// posture data.
|
||||
// Key is a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated.
|
||||
PostureChecking Key = "PostureChecking"
|
||||
// DeviceSerialNumber is the serial number of the device that is running Tailscale.
|
||||
// This is used on Android, iOS and tvOS to allow IT administrators to manually give us a serial number via MDM.
|
||||
// We are unable to programmatically get the serial number on mobile due to sandboxing restrictions.
|
||||
DeviceSerialNumber Key = "DeviceSerialNumber"
|
||||
|
||||
// ManagedByOrganizationName indicates the name of the organization managing the Tailscale
|
||||
// install. It is displayed inside the client UI in a prominent location.
|
||||
ManagedByOrganizationName Key = "ManagedByOrganizationName"
|
||||
// ManagedByCaption is an info message displayed inside the client UI as a caption when
|
||||
// ManagedByOrganizationName is set. It can be used to provide a pointer to support resources
|
||||
// for Tailscale within the organization.
|
||||
ManagedByCaption Key = "ManagedByCaption"
|
||||
// ManagedByURL is a valid URL pointing to a support help desk for Tailscale within the
|
||||
// organization. A button in the client UI provides easy access to this URL.
|
||||
ManagedByURL Key = "ManagedByURL"
|
||||
|
||||
// AuthKey is an auth key that will be used to login whenever the backend starts. This can be used to
|
||||
// automatically authenticate managed devices, without requiring user interaction.
|
||||
AuthKey Key = "AuthKey"
|
||||
|
||||
// MachineCertificateSubject is the exact name of a Subject that needs
|
||||
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
||||
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
||||
// itself, an intermediate CA or the root CA.
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
MachineCertificateSubject Key = "MachineCertificateSubject"
|
||||
|
||||
// Hostname is the hostname of the device that is running Tailscale.
|
||||
// When this policy is set, it overrides the hostname that the client
|
||||
// would otherwise obtain from the OS, e.g. by calling os.Hostname().
|
||||
Hostname Key = "Hostname"
|
||||
|
||||
// Keys with a string array value.
|
||||
|
||||
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
|
||||
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
|
||||
)
|
||||
258
vendor/tailscale.com/util/syspolicy/policy_keys.go
generated
vendored
258
vendor/tailscale.com/util/syspolicy/policy_keys.go
generated
vendored
@@ -4,203 +4,63 @@
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// Key is a string that uniquely identifies a policy and must remain unchanged
|
||||
// once established and documented for a given policy setting. It may contain
|
||||
// alphanumeric characters and zero or more [KeyPathSeparator]s to group
|
||||
// individual policy settings into categories.
|
||||
type Key = setting.Key
|
||||
|
||||
// The const block below lists known policy keys.
|
||||
// When adding a key to this list, remember to add a corresponding
|
||||
// [setting.Definition] to [implicitDefinitions] below.
|
||||
// Otherwise, the [TestKnownKeysRegistered] test will fail as a reminder.
|
||||
|
||||
const (
|
||||
// Keys with a string value
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
Tailnet Key = "Tailnet" // default ""; if blank, no tailnet name is sent to the server.
|
||||
|
||||
// AlwaysOn is a boolean key that controls whether Tailscale
|
||||
// should always remain in a connected state, and the user should
|
||||
// not be able to disconnect at their discretion.
|
||||
//
|
||||
// Warning: This policy setting is experimental and may change or be removed in the future.
|
||||
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
|
||||
// See tailscale/corp#26247, tailscale/corp#26248 and tailscale/corp#26249 for more information.
|
||||
AlwaysOn Key = "AlwaysOn.Enabled"
|
||||
|
||||
// AlwaysOnOverrideWithReason is a boolean key that alters the behavior
|
||||
// of [AlwaysOn]. When true, the user is allowed to disconnect Tailscale
|
||||
// by providing a reason. The reason is logged and sent to the control
|
||||
// for auditing purposes. It has no effect when [AlwaysOn] is false.
|
||||
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
|
||||
|
||||
// ReconnectAfter is a string value formatted for use with time.ParseDuration()
|
||||
// that defines the duration after which the client should automatically reconnect
|
||||
// to the Tailscale network following a user-initiated disconnect.
|
||||
// An empty string or a zero duration disables automatic reconnection.
|
||||
ReconnectAfter Key = "ReconnectAfter"
|
||||
|
||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
||||
// Exit node ID takes precedence over exit node IP.
|
||||
// To find the node ID, go to /api.md#device.
|
||||
ExitNodeID Key = "ExitNodeID"
|
||||
ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
|
||||
|
||||
// Keys with a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated. Enforcement of
|
||||
// these policies is typically performed in ipnlocal.applySysPolicy(). GUIs
|
||||
// typically hide menu items related to policies that are enforced.
|
||||
EnableIncomingConnections Key = "AllowIncomingConnections"
|
||||
EnableServerMode Key = "UnattendedMode"
|
||||
ExitNodeAllowLANAccess Key = "ExitNodeAllowLANAccess"
|
||||
EnableTailscaleDNS Key = "UseTailscaleDNSSettings"
|
||||
EnableTailscaleSubnets Key = "UseTailscaleSubnets"
|
||||
// CheckUpdates is the key to signal if the updater should periodically
|
||||
// check for updates.
|
||||
CheckUpdates Key = "CheckUpdates"
|
||||
// ApplyUpdates is the key to signal if updates should be automatically
|
||||
// installed. Its value is "InstallUpdates" because of an awkwardly-named
|
||||
// visibility option "ApplyUpdates" on MacOS.
|
||||
ApplyUpdates Key = "InstallUpdates"
|
||||
// EnableRunExitNode controls if the device acts as an exit node. Even when
|
||||
// running as an exit node, the device must be approved by a tailnet
|
||||
// administrator. Its name is slightly awkward because RunExitNodeVisibility
|
||||
// predates this option but is preserved for backwards compatibility.
|
||||
EnableRunExitNode Key = "AdvertiseExitNode"
|
||||
|
||||
// Keys with a string value that controls visibility: "show", "hide".
|
||||
// The default is "show" unless otherwise stated. Enforcement of these
|
||||
// policies is typically performed by the UI code for the relevant operating
|
||||
// system.
|
||||
AdminConsoleVisibility Key = "AdminConsole"
|
||||
NetworkDevicesVisibility Key = "NetworkDevices"
|
||||
TestMenuVisibility Key = "TestMenu"
|
||||
UpdateMenuVisibility Key = "UpdateMenu"
|
||||
ResetToDefaultsVisibility Key = "ResetToDefaults"
|
||||
// RunExitNodeVisibility controls if the "run as exit node" menu item is
|
||||
// visible, without controlling the setting itself. This is preserved for
|
||||
// backwards compatibility but prefer EnableRunExitNode in new deployments.
|
||||
RunExitNodeVisibility Key = "RunExitNode"
|
||||
PreferencesMenuVisibility Key = "PreferencesMenu"
|
||||
ExitNodeMenuVisibility Key = "ExitNodesPicker"
|
||||
// AutoUpdateVisibility is the key to signal if the menu item for automatic
|
||||
// installation of updates should be visible. It is only used by macsys
|
||||
// installations and uses the Sparkle naming convention, even though it does
|
||||
// not actually control updates, merely the UI for that setting.
|
||||
AutoUpdateVisibility Key = "ApplyUpdates"
|
||||
// SuggestedExitNodeVisibility controls the visibility of suggested exit nodes in the client GUI.
|
||||
// When this system policy is set to 'hide', an exit node suggestion won't be presented to the user as part of the exit nodes picker.
|
||||
SuggestedExitNodeVisibility Key = "SuggestedExitNode"
|
||||
// OnboardingFlowVisibility controls the visibility of the onboarding flow in the client GUI.
|
||||
// When this system policy is set to 'hide', the onboarding flow is never shown to the user.
|
||||
OnboardingFlowVisibility Key = "OnboardingFlow"
|
||||
|
||||
// Keys with a string value formatted for use with time.ParseDuration().
|
||||
KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
|
||||
|
||||
// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
|
||||
// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
|
||||
// The default is 0 unless otherwise stated.
|
||||
LogSCMInteractions Key = "LogSCMInteractions"
|
||||
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
|
||||
|
||||
// PostureChecking indicates if posture checking is enabled and the client shall gather
|
||||
// posture data.
|
||||
// Key is a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated.
|
||||
PostureChecking Key = "PostureChecking"
|
||||
// DeviceSerialNumber is the serial number of the device that is running Tailscale.
|
||||
// This is used on iOS/tvOS to allow IT administrators to manually give us a serial number via MDM.
|
||||
// We are unable to programmatically get the serial number from IOKit due to sandboxing restrictions.
|
||||
DeviceSerialNumber Key = "DeviceSerialNumber"
|
||||
|
||||
// ManagedByOrganizationName indicates the name of the organization managing the Tailscale
|
||||
// install. It is displayed inside the client UI in a prominent location.
|
||||
ManagedByOrganizationName Key = "ManagedByOrganizationName"
|
||||
// ManagedByCaption is an info message displayed inside the client UI as a caption when
|
||||
// ManagedByOrganizationName is set. It can be used to provide a pointer to support resources
|
||||
// for Tailscale within the organization.
|
||||
ManagedByCaption Key = "ManagedByCaption"
|
||||
// ManagedByURL is a valid URL pointing to a support help desk for Tailscale within the
|
||||
// organization. A button in the client UI provides easy access to this URL.
|
||||
ManagedByURL Key = "ManagedByURL"
|
||||
|
||||
// AuthKey is an auth key that will be used to login whenever the backend starts. This can be used to
|
||||
// automatically authenticate managed devices, without requiring user interaction.
|
||||
AuthKey Key = "AuthKey"
|
||||
|
||||
// MachineCertificateSubject is the exact name of a Subject that needs
|
||||
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
||||
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
||||
// itself, an intermediate CA or the root CA.
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
MachineCertificateSubject Key = "MachineCertificateSubject"
|
||||
|
||||
// Hostname is the hostname of the device that is running Tailscale.
|
||||
// When this policy is set, it overrides the hostname that the client
|
||||
// would otherwise obtain from the OS, e.g. by calling os.Hostname().
|
||||
Hostname Key = "Hostname"
|
||||
|
||||
// Keys with a string array value.
|
||||
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
|
||||
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
|
||||
)
|
||||
|
||||
// implicitDefinitions is a list of [setting.Definition] that will be registered
|
||||
// automatically when the policy setting definitions are first used by the syspolicy package hierarchy.
|
||||
// This includes the first time a policy needs to be read from any source.
|
||||
var implicitDefinitions = []*setting.Definition{
|
||||
// Device policy settings (can only be configured on a per-device basis):
|
||||
setting.NewDefinition(AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
|
||||
setting.NewDefinition(AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(AuthKey, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ControlURL, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ExitNodeID, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(ExitNodeIP, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(Hostname, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
|
||||
setting.NewDefinition(pkey.AllowExitNodeOverride, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AllowTailscaledRestart, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.AuthKey, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ControlURL, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.EnableDNSRegistration, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ExitNodeID, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ExitNodeIP, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.EncryptState, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.Hostname, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.LogTarget, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||
setting.NewDefinition(pkey.Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.HardwareAttestation, setting.DeviceSetting, setting.BooleanValue),
|
||||
|
||||
// User policy settings (can be configured on a user- or device-basis):
|
||||
setting.NewDefinition(AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
|
||||
setting.NewDefinition(ManagedByCaption, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(ManagedByURL, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
|
||||
setting.NewDefinition(pkey.ManagedByCaption, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ManagedByURL, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -218,31 +78,3 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var implicitDefinitionMap lazy.SyncValue[setting.DefinitionMap]
|
||||
|
||||
// WellKnownSettingDefinition returns a well-known, implicit setting definition by its key,
|
||||
// or an [ErrNoSuchKey] if a policy setting with the specified key does not exist
|
||||
// among implicit policy definitions.
|
||||
func WellKnownSettingDefinition(k Key) (*setting.Definition, error) {
|
||||
m, err := implicitDefinitionMap.GetErr(func() (setting.DefinitionMap, error) {
|
||||
return setting.DefinitionMapOf(implicitDefinitions)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d, ok := m[k]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, ErrNoSuchKey
|
||||
}
|
||||
|
||||
// RegisterWellKnownSettingsForTest registers all implicit setting definitions
|
||||
// for the duration of the test.
|
||||
func RegisterWellKnownSettingsForTest(tb TB) {
|
||||
tb.Helper()
|
||||
err := setting.SetDefinitionsForTest(tb, implicitDefinitions...)
|
||||
if err != nil {
|
||||
tb.Fatalf("Failed to register well-known settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
145
vendor/tailscale.com/util/syspolicy/policyclient/policyclient.go
generated
vendored
Normal file
145
vendor/tailscale.com/util/syspolicy/policyclient/policyclient.go
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package policyclient contains the minimal syspolicy interface as needed by
|
||||
// client code using syspolicy. It's the part that's always linked in, even if the rest
|
||||
// of syspolicy is omitted from the build.
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// Client is the interface between code making questions about the system policy
|
||||
// and the actual implementation.
|
||||
type Client interface {
|
||||
// GetString returns a string policy setting with the specified key,
|
||||
// or defaultValue (and a nil error) if it does not exist.
|
||||
GetString(key pkey.Key, defaultValue string) (string, error)
|
||||
|
||||
// GetStringArray returns a string array policy setting with the specified key,
|
||||
// or defaultValue (and a nil error) if it does not exist.
|
||||
GetStringArray(key pkey.Key, defaultValue []string) ([]string, error)
|
||||
|
||||
// GetBoolean returns a boolean policy setting with the specified key,
|
||||
// or defaultValue (and a nil error) if it does not exist.
|
||||
GetBoolean(key pkey.Key, defaultValue bool) (bool, error)
|
||||
|
||||
// GetUint64 returns a numeric policy setting with the specified key,
|
||||
// or defaultValue (and a nil error) if it does not exist.
|
||||
GetUint64(key pkey.Key, defaultValue uint64) (uint64, error)
|
||||
|
||||
// GetDuration loads a policy from the registry that can be managed by an
|
||||
// enterprise policy management system and describes a duration for some
|
||||
// action. The registry value should be a string that time.ParseDuration
|
||||
// understands. If the registry value is "" or can not be processed,
|
||||
// defaultValue (and a nil error) is returned instead.
|
||||
GetDuration(key pkey.Key, defaultValue time.Duration) (time.Duration, error)
|
||||
|
||||
// GetPreferenceOption loads a policy from the registry that can be
|
||||
// managed by an enterprise policy management system and allows administrative
|
||||
// overrides of users' choices in a way that we do not want tailcontrol to have
|
||||
// the authority to set. It describes user-decides/always/never options, where
|
||||
// "always" and "never" remove the user's ability to make a selection. If not
|
||||
// present or set to a different value, defaultValue (and a nil error) is returned.
|
||||
GetPreferenceOption(key pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error)
|
||||
|
||||
// GetVisibility returns whether a UI element should be visible based on
|
||||
// the system's configuration.
|
||||
// If unconfigured, implementations should return [ptype.VisibleByPolicy]
|
||||
// and a nil error.
|
||||
GetVisibility(key pkey.Key) (ptype.Visibility, error)
|
||||
|
||||
// SetDebugLoggingEnabled enables or disables debug logging for the policy client.
|
||||
SetDebugLoggingEnabled(enabled bool)
|
||||
|
||||
// HasAnyOf returns whether at least one of the specified policy settings is
|
||||
// configured, or an error if no keys are provided or the check fails.
|
||||
HasAnyOf(keys ...pkey.Key) (bool, error)
|
||||
|
||||
// RegisterChangeCallback registers a callback function that will be called
|
||||
// whenever a policy change is detected. It returns a function to unregister
|
||||
// the callback and an error if the registration fails.
|
||||
RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error)
|
||||
}
|
||||
|
||||
// Get returns a non-nil [Client] implementation as a function of the
|
||||
// build tags. It returns a no-op implementation if the full syspolicy
|
||||
// package is omitted from the build, or in tests.
|
||||
func Get() Client {
|
||||
if testenv.InTest() {
|
||||
// This is a little redundant (the Windows implementation at least
|
||||
// already does this) but it's here for redundancy and clarity, that we
|
||||
// don't want to accidentally use the real system policy when running
|
||||
// tests.
|
||||
return NoPolicyClient{}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// RegisterClientImpl registers a [Client] implementation to be returned by
|
||||
// [Get].
|
||||
func RegisterClientImpl(c Client) {
|
||||
client = c
|
||||
}
|
||||
|
||||
var client Client = NoPolicyClient{}
|
||||
|
||||
// PolicyChange is the interface representing a change in policy settings.
|
||||
type PolicyChange interface {
|
||||
// HasChanged reports whether the policy setting identified by the given key
|
||||
// has changed.
|
||||
HasChanged(pkey.Key) bool
|
||||
|
||||
// HasChangedAnyOf reports whether any of the provided policy settings
|
||||
// changed in this change.
|
||||
HasChangedAnyOf(keys ...pkey.Key) bool
|
||||
}
|
||||
|
||||
// NoPolicyClient is a no-op implementation of [Client] that only
|
||||
// returns default values.
|
||||
type NoPolicyClient struct{}
|
||||
|
||||
var _ Client = NoPolicyClient{}
|
||||
|
||||
func (NoPolicyClient) GetBoolean(key pkey.Key, defaultValue bool) (bool, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetString(key pkey.Key, defaultValue string) (string, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetVisibility(name pkey.Key) (ptype.Visibility, error) {
|
||||
return ptype.VisibleByPolicy, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) SetDebugLoggingEnabled(enabled bool) {}
|
||||
|
||||
func (NoPolicyClient) RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error) {
|
||||
return func() {}, nil
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
)
|
||||
// Package ptype contains types used by syspolicy.
|
||||
//
|
||||
// It's a leaf package for dependency reasons and should not contain much if any
|
||||
// code, and should not import much (or anything).
|
||||
package ptype
|
||||
|
||||
// PreferenceOption is a policy that governs whether a boolean variable
|
||||
// is forcibly assigned an administrator-defined value, or allowed to receive
|
||||
@@ -18,9 +18,10 @@ const (
|
||||
AlwaysByPolicy
|
||||
)
|
||||
|
||||
// Show returns if the UI option that controls the choice administered by this
|
||||
// policy should be shown. Currently this is true if and only if the policy is
|
||||
// [ShowChoiceByPolicy].
|
||||
// Show reports whether the UI option that controls the choice administered by
|
||||
// this policy should be shown (that is, available for users to change).
|
||||
//
|
||||
// Currently this is true if and only if the policy is [ShowChoiceByPolicy].
|
||||
func (p PreferenceOption) Show() bool {
|
||||
return p == ShowChoiceByPolicy
|
||||
}
|
||||
@@ -91,11 +92,6 @@ func (p *PreferenceOption) UnmarshalText(text []byte) error {
|
||||
// component of a user interface is to be shown.
|
||||
type Visibility byte
|
||||
|
||||
var (
|
||||
_ encoding.TextMarshaler = (*Visibility)(nil)
|
||||
_ encoding.TextUnmarshaler = (*Visibility)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
VisibleByPolicy Visibility = 'v'
|
||||
HiddenByPolicy Visibility = 'h'
|
||||
19
vendor/tailscale.com/util/syspolicy/rsop/change_callbacks.go
generated
vendored
19
vendor/tailscale.com/util/syspolicy/rsop/change_callbacks.go
generated
vendored
@@ -9,8 +9,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
@@ -20,7 +24,7 @@ type Change[T any] struct {
|
||||
}
|
||||
|
||||
// PolicyChangeCallback is a function called whenever a policy changes.
|
||||
type PolicyChangeCallback func(*PolicyChange)
|
||||
type PolicyChangeCallback func(policyclient.PolicyChange)
|
||||
|
||||
// PolicyChange describes a policy change.
|
||||
type PolicyChange struct {
|
||||
@@ -37,8 +41,8 @@ func (c PolicyChange) Old() *setting.Snapshot {
|
||||
return c.snapshots.Old
|
||||
}
|
||||
|
||||
// HasChanged reports whether a policy setting with the specified [setting.Key], has changed.
|
||||
func (c PolicyChange) HasChanged(key setting.Key) bool {
|
||||
// HasChanged reports whether a policy setting with the specified [pkey.Key], has changed.
|
||||
func (c PolicyChange) HasChanged(key pkey.Key) bool {
|
||||
new, newErr := c.snapshots.New.GetErr(key)
|
||||
old, oldErr := c.snapshots.Old.GetErr(key)
|
||||
if newErr != nil && oldErr != nil {
|
||||
@@ -48,7 +52,7 @@ func (c PolicyChange) HasChanged(key setting.Key) bool {
|
||||
return true
|
||||
}
|
||||
switch newVal := new.(type) {
|
||||
case bool, uint64, string, setting.Visibility, setting.PreferenceOption, time.Duration:
|
||||
case bool, uint64, string, ptype.Visibility, ptype.PreferenceOption, time.Duration:
|
||||
return newVal != old
|
||||
case []string:
|
||||
oldVal, ok := old.([]string)
|
||||
@@ -59,10 +63,15 @@ func (c PolicyChange) HasChanged(key setting.Key) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// HasChangedAnyOf reports whether any of the specified policy settings has changed.
|
||||
func (c PolicyChange) HasChangedAnyOf(keys ...pkey.Key) bool {
|
||||
return slices.ContainsFunc(keys, c.HasChanged)
|
||||
}
|
||||
|
||||
// policyChangeCallbacks are the callbacks to invoke when the effective policy changes.
|
||||
// It is safe for concurrent use.
|
||||
type policyChangeCallbacks struct {
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
cbs set.HandleSet[PolicyChangeCallback]
|
||||
}
|
||||
|
||||
|
||||
8
vendor/tailscale.com/util/syspolicy/rsop/resultant_policy.go
generated
vendored
8
vendor/tailscale.com/util/syspolicy/rsop/resultant_policy.go
generated
vendored
@@ -7,13 +7,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
@@ -58,7 +58,7 @@ type Policy struct {
|
||||
|
||||
changeCallbacks policyChangeCallbacks
|
||||
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
watcherStarted bool // whether [Policy.watchReload] was started
|
||||
sources source.ReadableSources
|
||||
closing bool // whether [Policy.Close] was called (even if we're still closing)
|
||||
@@ -449,7 +449,7 @@ func (p *Policy) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func setForTest[T any](tb internal.TB, target *T, newValue T) {
|
||||
func setForTest[T any](tb testenv.TB, target *T, newValue T) {
|
||||
oldValue := *target
|
||||
tb.Cleanup(func() { *target = oldValue })
|
||||
*target = newValue
|
||||
|
||||
3
vendor/tailscale.com/util/syspolicy/rsop/rsop.go
generated
vendored
3
vendor/tailscale.com/util/syspolicy/rsop/rsop.go
generated
vendored
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/slicesx"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
policyMu sync.Mutex // protects [policySources] and [effectivePolicies]
|
||||
policyMu syncs.Mutex // protects [policySources] and [effectivePolicies]
|
||||
policySources []*source.Source // all registered policy sources
|
||||
effectivePolicies []*Policy // all active (non-closed) effective policies returned by [PolicyFor]
|
||||
|
||||
|
||||
4
vendor/tailscale.com/util/syspolicy/rsop/store_registration.go
generated
vendored
4
vendor/tailscale.com/util/syspolicy/rsop/store_registration.go
generated
vendored
@@ -9,9 +9,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// ErrAlreadyConsumed is the error returned when [StoreRegistration.ReplaceStore]
|
||||
@@ -33,7 +33,7 @@ func RegisterStore(name string, scope setting.PolicyScope, store source.Store) (
|
||||
|
||||
// RegisterStoreForTest is like [RegisterStore], but unregisters the store when
|
||||
// tb and all its subtests complete.
|
||||
func RegisterStoreForTest(tb internal.TB, name string, scope setting.PolicyScope, store source.Store) (*StoreRegistration, error) {
|
||||
func RegisterStoreForTest(tb testenv.TB, name string, scope setting.PolicyScope, store source.Store) (*StoreRegistration, error) {
|
||||
setForTest(tb, &policyReloadMinDelay, 10*time.Millisecond)
|
||||
setForTest(tb, &policyReloadMaxDelay, 500*time.Millisecond)
|
||||
|
||||
|
||||
13
vendor/tailscale.com/util/syspolicy/setting/key.go
generated
vendored
13
vendor/tailscale.com/util/syspolicy/setting/key.go
generated
vendored
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
// Key is a string that uniquely identifies a policy and must remain unchanged
|
||||
// once established and documented for a given policy setting. It may contain
|
||||
// alphanumeric characters and zero or more [KeyPathSeparator]s to group
|
||||
// individual policy settings into categories.
|
||||
type Key string
|
||||
|
||||
// KeyPathSeparator allows logical grouping of policy settings into categories.
|
||||
const KeyPathSeparator = '/'
|
||||
3
vendor/tailscale.com/util/syspolicy/setting/raw_item.go
generated
vendored
3
vendor/tailscale.com/util/syspolicy/setting/raw_item.go
generated
vendored
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
)
|
||||
|
||||
// RawItem contains a raw policy setting value as read from a policy store, or an
|
||||
@@ -169,4 +170,4 @@ func (v *RawValue) UnmarshalJSON(b []byte) error {
|
||||
}
|
||||
|
||||
// RawValues is a map of keyed setting values that can be read from a JSON.
|
||||
type RawValues map[Key]RawValue
|
||||
type RawValues map[pkey.Key]RawValue
|
||||
|
||||
45
vendor/tailscale.com/util/syspolicy/setting/setting.go
generated
vendored
45
vendor/tailscale.com/util/syspolicy/setting/setting.go
generated
vendored
@@ -11,11 +11,14 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// Scope indicates the broadest scope at which a policy setting may apply,
|
||||
@@ -128,12 +131,12 @@ func (t Type) String() string {
|
||||
|
||||
// ValueType is a constraint that allows Go types corresponding to [Type].
|
||||
type ValueType interface {
|
||||
bool | uint64 | string | []string | Visibility | PreferenceOption | time.Duration
|
||||
bool | uint64 | string | []string | ptype.Visibility | ptype.PreferenceOption | time.Duration
|
||||
}
|
||||
|
||||
// Definition defines policy key, scope and value type.
|
||||
type Definition struct {
|
||||
key Key
|
||||
key pkey.Key
|
||||
scope Scope
|
||||
typ Type
|
||||
platforms PlatformList
|
||||
@@ -141,12 +144,12 @@ type Definition struct {
|
||||
|
||||
// NewDefinition returns a new [Definition] with the specified
|
||||
// key, scope, type and supported platforms (see [PlatformList]).
|
||||
func NewDefinition(k Key, s Scope, t Type, platforms ...string) *Definition {
|
||||
func NewDefinition(k pkey.Key, s Scope, t Type, platforms ...string) *Definition {
|
||||
return &Definition{key: k, scope: s, typ: t, platforms: platforms}
|
||||
}
|
||||
|
||||
// Key returns a policy setting's identifier.
|
||||
func (d *Definition) Key() Key {
|
||||
func (d *Definition) Key() pkey.Key {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -207,12 +210,12 @@ func (d *Definition) Equal(d2 *Definition) bool {
|
||||
}
|
||||
|
||||
// DefinitionMap is a map of setting [Definition] by [Key].
|
||||
type DefinitionMap map[Key]*Definition
|
||||
type DefinitionMap map[pkey.Key]*Definition
|
||||
|
||||
var (
|
||||
definitions lazy.SyncValue[DefinitionMap]
|
||||
|
||||
definitionsMu sync.Mutex
|
||||
definitionsMu syncs.Mutex
|
||||
definitionsList []*Definition
|
||||
definitionsUsed bool
|
||||
)
|
||||
@@ -223,7 +226,7 @@ var (
|
||||
// invoking any functions that use the registered policy definitions. This
|
||||
// includes calling [Definitions] or [DefinitionOf] directly, or reading any
|
||||
// policy settings via syspolicy.
|
||||
func Register(k Key, s Scope, t Type, platforms ...string) {
|
||||
func Register(k pkey.Key, s Scope, t Type, platforms ...string) {
|
||||
RegisterDefinition(NewDefinition(k, s, t, platforms...))
|
||||
}
|
||||
|
||||
@@ -277,7 +280,7 @@ func DefinitionMapOf(settings []*Definition) (DefinitionMap, error) {
|
||||
// for the test duration. It is not concurrency-safe, but unlike [Register],
|
||||
// it does not panic and can be called anytime.
|
||||
// It returns an error if ds contains two different settings with the same [Key].
|
||||
func SetDefinitionsForTest(tb lazy.TB, ds ...*Definition) error {
|
||||
func SetDefinitionsForTest(tb testenv.TB, ds ...*Definition) error {
|
||||
m, err := DefinitionMapOf(ds)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -289,7 +292,7 @@ func SetDefinitionsForTest(tb lazy.TB, ds ...*Definition) error {
|
||||
// DefinitionOf returns a setting definition by key,
|
||||
// or [ErrNoSuchKey] if the specified key does not exist,
|
||||
// or an error if there are conflicting policy definitions.
|
||||
func DefinitionOf(k Key) (*Definition, error) {
|
||||
func DefinitionOf(k pkey.Key) (*Definition, error) {
|
||||
ds, err := settingDefinitions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -319,33 +322,33 @@ func Definitions() ([]*Definition, error) {
|
||||
type PlatformList []string
|
||||
|
||||
// Has reports whether l contains the target platform.
|
||||
func (l PlatformList) Has(target string) bool {
|
||||
if len(l) == 0 {
|
||||
func (ls PlatformList) Has(target string) bool {
|
||||
if len(ls) == 0 {
|
||||
return true
|
||||
}
|
||||
return slices.ContainsFunc(l, func(os string) bool {
|
||||
return slices.ContainsFunc(ls, func(os string) bool {
|
||||
return strings.EqualFold(os, target)
|
||||
})
|
||||
}
|
||||
|
||||
// HasCurrent is like Has, but for the current platform.
|
||||
func (l PlatformList) HasCurrent() bool {
|
||||
return l.Has(internal.OS())
|
||||
func (ls PlatformList) HasCurrent() bool {
|
||||
return ls.Has(internal.OS())
|
||||
}
|
||||
|
||||
// mergeFrom merges l2 into l. Since an empty list indicates no platform restrictions,
|
||||
// if either l or l2 is empty, the merged result in l will also be empty.
|
||||
func (l *PlatformList) mergeFrom(l2 PlatformList) {
|
||||
func (ls *PlatformList) mergeFrom(l2 PlatformList) {
|
||||
switch {
|
||||
case len(*l) == 0:
|
||||
case len(*ls) == 0:
|
||||
// No-op. An empty list indicates no platform restrictions.
|
||||
case len(l2) == 0:
|
||||
// Merging with an empty list results in an empty list.
|
||||
*l = l2
|
||||
*ls = l2
|
||||
default:
|
||||
// Append, sort and dedup.
|
||||
*l = append(*l, l2...)
|
||||
slices.Sort(*l)
|
||||
*l = slices.Compact(*l)
|
||||
*ls = append(*ls, l2...)
|
||||
slices.Sort(*ls)
|
||||
*ls = slices.Compact(*ls)
|
||||
}
|
||||
}
|
||||
|
||||
46
vendor/tailscale.com/util/syspolicy/setting/snapshot.go
generated
vendored
46
vendor/tailscale.com/util/syspolicy/setting/snapshot.go
generated
vendored
@@ -9,39 +9,41 @@ import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
)
|
||||
|
||||
// Snapshot is an immutable collection of ([Key], [RawItem]) pairs, representing
|
||||
// a set of policy settings applied at a specific moment in time.
|
||||
// A nil pointer to [Snapshot] is valid.
|
||||
type Snapshot struct {
|
||||
m map[Key]RawItem
|
||||
m map[pkey.Key]RawItem
|
||||
sig deephash.Sum // of m
|
||||
summary Summary
|
||||
}
|
||||
|
||||
// NewSnapshot returns a new [Snapshot] with the specified items and options.
|
||||
func NewSnapshot(items map[Key]RawItem, opts ...SummaryOption) *Snapshot {
|
||||
func NewSnapshot(items map[pkey.Key]RawItem, opts ...SummaryOption) *Snapshot {
|
||||
return &Snapshot{m: xmaps.Clone(items), sig: deephash.Hash(&items), summary: SummaryWith(opts...)}
|
||||
}
|
||||
|
||||
// All returns an iterator over policy settings in s. The iteration order is not
|
||||
// specified and is not guaranteed to be the same from one call to the next.
|
||||
func (s *Snapshot) All() iter.Seq2[Key, RawItem] {
|
||||
func (s *Snapshot) All() iter.Seq2[pkey.Key, RawItem] {
|
||||
if s == nil {
|
||||
return func(yield func(Key, RawItem) bool) {}
|
||||
return func(yield func(pkey.Key, RawItem) bool) {}
|
||||
}
|
||||
return maps.All(s.m)
|
||||
}
|
||||
|
||||
// Get returns the value of the policy setting with the specified key
|
||||
// or nil if it is not configured or has an error.
|
||||
func (s *Snapshot) Get(k Key) any {
|
||||
func (s *Snapshot) Get(k pkey.Key) any {
|
||||
v, _ := s.GetErr(k)
|
||||
return v
|
||||
}
|
||||
@@ -49,7 +51,7 @@ func (s *Snapshot) Get(k Key) any {
|
||||
// GetErr returns the value of the policy setting with the specified key,
|
||||
// [ErrNotConfigured] if it is not configured, or an error returned by
|
||||
// the policy Store if the policy setting could not be read.
|
||||
func (s *Snapshot) GetErr(k Key) (any, error) {
|
||||
func (s *Snapshot) GetErr(k pkey.Key) (any, error) {
|
||||
if s != nil {
|
||||
if s, ok := s.m[k]; ok {
|
||||
return s.Value(), s.Error()
|
||||
@@ -61,7 +63,7 @@ func (s *Snapshot) GetErr(k Key) (any, error) {
|
||||
// GetSetting returns the untyped policy setting with the specified key and true
|
||||
// if a policy setting with such key has been configured;
|
||||
// otherwise, it returns zero, false.
|
||||
func (s *Snapshot) GetSetting(k Key) (setting RawItem, ok bool) {
|
||||
func (s *Snapshot) GetSetting(k pkey.Key) (setting RawItem, ok bool) {
|
||||
setting, ok = s.m[k]
|
||||
return setting, ok
|
||||
}
|
||||
@@ -93,9 +95,9 @@ func (s *Snapshot) EqualItems(s2 *Snapshot) bool {
|
||||
|
||||
// Keys return an iterator over keys in s. The iteration order is not specified
|
||||
// and is not guaranteed to be the same from one call to the next.
|
||||
func (s *Snapshot) Keys() iter.Seq[Key] {
|
||||
func (s *Snapshot) Keys() iter.Seq[pkey.Key] {
|
||||
if s.m == nil {
|
||||
return func(yield func(Key) bool) {}
|
||||
return func(yield func(pkey.Key) bool) {}
|
||||
}
|
||||
return maps.Keys(s.m)
|
||||
}
|
||||
@@ -143,8 +145,8 @@ func (s *Snapshot) String() string {
|
||||
|
||||
// snapshotJSON holds JSON-marshallable data for [Snapshot].
|
||||
type snapshotJSON struct {
|
||||
Summary Summary `json:",omitzero"`
|
||||
Settings map[Key]RawItem `json:",omitempty"`
|
||||
Summary Summary `json:",omitzero"`
|
||||
Settings map[pkey.Key]RawItem `json:",omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -152,6 +154,24 @@ var (
|
||||
_ jsonv2.UnmarshalerFrom = (*Snapshot)(nil)
|
||||
)
|
||||
|
||||
// As of 2025-07-28, jsonv2 no longer has a default representation for [time.Duration],
|
||||
// so we need to provide a custom marshaler.
|
||||
//
|
||||
// This is temporary until the decision on the default representation is made
|
||||
// (see https://github.com/golang/go/issues/71631#issuecomment-2981670799).
|
||||
//
|
||||
// In the future, we might either use the default representation (if compatible with
|
||||
// [time.Duration.String]) or specify something like json.WithFormat[time.Duration]("units")
|
||||
// when golang/go#71664 is implemented.
|
||||
//
|
||||
// TODO(nickkhyl): revisit this when the decision on the default [time.Duration]
|
||||
// representation is made in golang/go#71631 and/or golang/go#71664 is implemented.
|
||||
var formatDurationAsUnits = jsonv2.JoinOptions(
|
||||
jsonv2.WithMarshalers(jsonv2.MarshalToFunc(func(e *jsontext.Encoder, t time.Duration) error {
|
||||
return e.WriteToken(jsontext.String(t.String()))
|
||||
})),
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
data := &snapshotJSON{}
|
||||
@@ -159,7 +179,7 @@ func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
data.Summary = s.summary
|
||||
data.Settings = s.m
|
||||
}
|
||||
return jsonv2.MarshalEncode(out, data)
|
||||
return jsonv2.MarshalEncode(out, data, formatDurationAsUnits)
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
@@ -213,7 +233,7 @@ func MergeSnapshots(snapshot1, snapshot2 *Snapshot) *Snapshot {
|
||||
}
|
||||
return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
|
||||
}
|
||||
m := make(map[Key]RawItem, snapshot1.Len()+snapshot2.Len())
|
||||
m := make(map[pkey.Key]RawItem, snapshot1.Len()+snapshot2.Len())
|
||||
xmaps.Copy(m, snapshot1.m)
|
||||
xmaps.Copy(m, snapshot2.m) // snapshot2 has higher precedence
|
||||
return &Snapshot{m, deephash.Hash(&m), SummaryWith(summaryOpts...)}
|
||||
|
||||
15
vendor/tailscale.com/util/syspolicy/source/env_policy_store.go
generated
vendored
15
vendor/tailscale.com/util/syspolicy/source/env_policy_store.go
generated
vendored
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ var _ Store = (*EnvPolicyStore)(nil)
|
||||
type EnvPolicyStore struct{}
|
||||
|
||||
// ReadString implements [Store].
|
||||
func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
|
||||
func (s *EnvPolicyStore) ReadString(key pkey.Key) (string, error) {
|
||||
_, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -31,7 +32,7 @@ func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [Store].
|
||||
func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
func (s *EnvPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
|
||||
name, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -47,7 +48,7 @@ func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
}
|
||||
|
||||
// ReadBoolean implements [Store].
|
||||
func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
func (s *EnvPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
|
||||
name, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -63,7 +64,7 @@ func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
}
|
||||
|
||||
// ReadStringArray implements [Store].
|
||||
func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
func (s *EnvPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
|
||||
_, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil || str == "" {
|
||||
return nil, err
|
||||
@@ -79,7 +80,7 @@ func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
return res[0:dst], nil
|
||||
}
|
||||
|
||||
func (s *EnvPolicyStore) lookupSettingVariable(key setting.Key) (name, value string, err error) {
|
||||
func (s *EnvPolicyStore) lookupSettingVariable(key pkey.Key) (name, value string, err error) {
|
||||
name, err = keyToEnvVarName(key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -103,7 +104,7 @@ var (
|
||||
//
|
||||
// It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
|
||||
// [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
|
||||
func keyToEnvVarName(key setting.Key) (string, error) {
|
||||
func keyToEnvVarName(key pkey.Key) (string, error) {
|
||||
if len(key) == 0 {
|
||||
return "", errEmptyKey
|
||||
}
|
||||
@@ -135,7 +136,7 @@ func keyToEnvVarName(key setting.Key) (string, error) {
|
||||
}
|
||||
case isDigit(c):
|
||||
split = currentWord.Len() > 0 && !isDigit(key[i-1])
|
||||
case c == setting.KeyPathSeparator:
|
||||
case c == pkey.KeyPathSeparator:
|
||||
words = append(words, currentWord.String())
|
||||
currentWord.Reset()
|
||||
continue
|
||||
|
||||
14
vendor/tailscale.com/util/syspolicy/source/policy_reader.go
generated
vendored
14
vendor/tailscale.com/util/syspolicy/source/policy_reader.go
generated
vendored
@@ -16,6 +16,8 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/internal/metrics"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
@@ -138,9 +140,9 @@ func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
|
||||
|
||||
metrics.Reset(r.origin)
|
||||
|
||||
var m map[setting.Key]setting.RawItem
|
||||
var m map[pkey.Key]setting.RawItem
|
||||
if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
|
||||
m = make(map[setting.Key]setting.RawItem, lastPolicyCount)
|
||||
m = make(map[pkey.Key]setting.RawItem, lastPolicyCount)
|
||||
}
|
||||
for _, s := range r.settings {
|
||||
if !r.origin.Scope().IsConfigurableSetting(s) {
|
||||
@@ -364,21 +366,21 @@ func readPolicySettingValue(store Store, s *setting.Definition) (value any, err
|
||||
case setting.PreferenceOptionValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
var value setting.PreferenceOption
|
||||
var value ptype.PreferenceOption
|
||||
if err = value.UnmarshalText([]byte(s)); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return setting.ShowChoiceByPolicy, err
|
||||
return ptype.ShowChoiceByPolicy, err
|
||||
case setting.VisibilityValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
var value setting.Visibility
|
||||
var value ptype.Visibility
|
||||
if err = value.UnmarshalText([]byte(s)); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return setting.VisibleByPolicy, err
|
||||
return ptype.VisibleByPolicy, err
|
||||
case setting.DurationValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
|
||||
9
vendor/tailscale.com/util/syspolicy/source/policy_source.go
generated
vendored
9
vendor/tailscale.com/util/syspolicy/source/policy_source.go
generated
vendored
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
@@ -31,19 +32,19 @@ type Store interface {
|
||||
// ReadString returns the value of a [setting.StringValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadString(key setting.Key) (string, error)
|
||||
ReadString(key pkey.Key) (string, error)
|
||||
// ReadUInt64 returns the value of a [setting.IntegerValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadUInt64(key setting.Key) (uint64, error)
|
||||
ReadUInt64(key pkey.Key) (uint64, error)
|
||||
// ReadBoolean returns the value of a [setting.BooleanValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadBoolean(key setting.Key) (bool, error)
|
||||
ReadBoolean(key pkey.Key) (bool, error)
|
||||
// ReadStringArray returns the value of a [setting.StringListValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadStringArray(key setting.Key) ([]string, error)
|
||||
ReadStringArray(key pkey.Key) ([]string, error)
|
||||
}
|
||||
|
||||
// Lockable is an optional interface that [Store] implementations may support.
|
||||
|
||||
29
vendor/tailscale.com/util/syspolicy/source/policy_store_windows.go
generated
vendored
29
vendor/tailscale.com/util/syspolicy/source/policy_store_windows.go
generated
vendored
@@ -13,6 +13,7 @@ import (
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/winutil/gp"
|
||||
)
|
||||
@@ -251,7 +252,7 @@ func (ps *PlatformPolicyStore) onChange() {
|
||||
|
||||
// ReadString retrieves a string policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadString(key setting.Key) (val string, err error) {
|
||||
func (ps *PlatformPolicyStore) ReadString(key pkey.Key) (val string, err error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (string, error) {
|
||||
val, _, err := key.GetStringValue(valueName)
|
||||
@@ -261,7 +262,7 @@ func (ps *PlatformPolicyStore) ReadString(key setting.Key) (val string, err erro
|
||||
|
||||
// ReadUInt64 retrieves an integer policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
func (ps *PlatformPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (uint64, error) {
|
||||
val, _, err := key.GetIntegerValue(valueName)
|
||||
@@ -271,7 +272,7 @@ func (ps *PlatformPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
|
||||
// ReadBoolean retrieves a boolean policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
func (ps *PlatformPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (bool, error) {
|
||||
val, _, err := key.GetIntegerValue(valueName)
|
||||
@@ -283,8 +284,8 @@ func (ps *PlatformPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
}
|
||||
|
||||
// ReadString retrieves a multi-string policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
// It returns [pkey.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) ([]string, error) {
|
||||
val, _, err := key.GetStringsValue(valueName)
|
||||
@@ -322,25 +323,25 @@ func (ps *PlatformPolicyStore) ReadStringArray(key setting.Key) ([]string, error
|
||||
})
|
||||
}
|
||||
|
||||
// splitSettingKey extracts the registry key name and value name from a [setting.Key].
|
||||
// The [setting.Key] format allows grouping settings into nested categories using one
|
||||
// or more [setting.KeyPathSeparator]s in the path. How individual policy settings are
|
||||
// splitSettingKey extracts the registry key name and value name from a [pkey.Key].
|
||||
// The [pkey.Key] format allows grouping settings into nested categories using one
|
||||
// or more [pkey.KeyPathSeparator]s in the path. How individual policy settings are
|
||||
// stored is an implementation detail of each [Store]. In the [PlatformPolicyStore]
|
||||
// for Windows, we map nested policy categories onto the Registry key hierarchy.
|
||||
// The last component after a [setting.KeyPathSeparator] is treated as the value name,
|
||||
// The last component after a [pkey.KeyPathSeparator] is treated as the value name,
|
||||
// while everything preceding it is considered a subpath (relative to the {HKLM,HKCU}\Software\Policies\Tailscale key).
|
||||
// If there are no [setting.KeyPathSeparator]s in the key, the policy setting value
|
||||
// If there are no [pkey.KeyPathSeparator]s in the key, the policy setting value
|
||||
// is meant to be stored directly under {HKLM,HKCU}\Software\Policies\Tailscale.
|
||||
func splitSettingKey(key setting.Key) (path, valueName string) {
|
||||
if idx := strings.LastIndexByte(string(key), setting.KeyPathSeparator); idx != -1 {
|
||||
path = strings.ReplaceAll(string(key[:idx]), string(setting.KeyPathSeparator), `\`)
|
||||
func splitSettingKey(key pkey.Key) (path, valueName string) {
|
||||
if idx := strings.LastIndexByte(string(key), pkey.KeyPathSeparator); idx != -1 {
|
||||
path = strings.ReplaceAll(string(key[:idx]), string(pkey.KeyPathSeparator), `\`)
|
||||
valueName = string(key[idx+1:])
|
||||
return path, valueName
|
||||
}
|
||||
return "", string(key)
|
||||
}
|
||||
|
||||
func getPolicyValue[T any](ps *PlatformPolicyStore, key setting.Key, getter registryValueGetter[T]) (T, error) {
|
||||
func getPolicyValue[T any](ps *PlatformPolicyStore, key pkey.Key, getter registryValueGetter[T]) (T, error) {
|
||||
var zero T
|
||||
|
||||
ps.mu.Lock()
|
||||
|
||||
44
vendor/tailscale.com/util/syspolicy/source/test_store.go
generated
vendored
44
vendor/tailscale.com/util/syspolicy/source/test_store.go
generated
vendored
@@ -12,8 +12,9 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,7 +32,7 @@ type TestValueType interface {
|
||||
// TestSetting is a policy setting in a [TestStore].
|
||||
type TestSetting[T TestValueType] struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
Key pkey.Key
|
||||
// Error is the error to be returned by the [TestStore] when reading
|
||||
// a policy setting with the specified key.
|
||||
Error error
|
||||
@@ -43,20 +44,20 @@ type TestSetting[T TestValueType] struct {
|
||||
|
||||
// TestSettingOf returns a [TestSetting] representing a policy setting
|
||||
// configured with the specified key and value.
|
||||
func TestSettingOf[T TestValueType](key setting.Key, value T) TestSetting[T] {
|
||||
func TestSettingOf[T TestValueType](key pkey.Key, value T) TestSetting[T] {
|
||||
return TestSetting[T]{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// TestSettingWithError returns a [TestSetting] representing a policy setting
|
||||
// with the specified key and error.
|
||||
func TestSettingWithError[T TestValueType](key setting.Key, err error) TestSetting[T] {
|
||||
func TestSettingWithError[T TestValueType](key pkey.Key, err error) TestSetting[T] {
|
||||
return TestSetting[T]{Key: key, Error: err}
|
||||
}
|
||||
|
||||
// testReadOperation describes a single policy setting read operation.
|
||||
type testReadOperation struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
Key pkey.Key
|
||||
// Type is a value type of a read operation.
|
||||
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
|
||||
Type setting.Type
|
||||
@@ -65,7 +66,7 @@ type testReadOperation struct {
|
||||
// TestExpectedReads is the number of read operations with the specified details.
|
||||
type TestExpectedReads struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
Key pkey.Key
|
||||
// Type is a value type of a read operation.
|
||||
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
|
||||
Type setting.Type
|
||||
@@ -79,7 +80,7 @@ func (r TestExpectedReads) operation() testReadOperation {
|
||||
|
||||
// TestStore is a [Store] that can be used in tests.
|
||||
type TestStore struct {
|
||||
tb internal.TB
|
||||
tb testenv.TB
|
||||
|
||||
done chan struct{}
|
||||
|
||||
@@ -87,8 +88,8 @@ type TestStore struct {
|
||||
storeLockCount atomic.Int32
|
||||
|
||||
mu sync.RWMutex
|
||||
suspendCount int // change callback are suspended if > 0
|
||||
mr, mw map[setting.Key]any // maps for reading and writing; they're the same unless the store is suspended.
|
||||
suspendCount int // change callback are suspended if > 0
|
||||
mr, mw map[pkey.Key]any // maps for reading and writing; they're the same unless the store is suspended.
|
||||
cbs set.HandleSet[func()]
|
||||
closed bool
|
||||
|
||||
@@ -98,8 +99,8 @@ type TestStore struct {
|
||||
|
||||
// NewTestStore returns a new [TestStore].
|
||||
// The tb will be used to report coding errors detected by the [TestStore].
|
||||
func NewTestStore(tb internal.TB) *TestStore {
|
||||
m := make(map[setting.Key]any)
|
||||
func NewTestStore(tb testenv.TB) *TestStore {
|
||||
m := make(map[pkey.Key]any)
|
||||
store := &TestStore{
|
||||
tb: tb,
|
||||
done: make(chan struct{}),
|
||||
@@ -112,7 +113,7 @@ func NewTestStore(tb internal.TB) *TestStore {
|
||||
|
||||
// NewTestStoreOf is a shorthand for [NewTestStore] followed by [TestStore.SetBooleans],
|
||||
// [TestStore.SetUInt64s], [TestStore.SetStrings] or [TestStore.SetStringLists].
|
||||
func NewTestStoreOf[T TestValueType](tb internal.TB, settings ...TestSetting[T]) *TestStore {
|
||||
func NewTestStoreOf[T TestValueType](tb testenv.TB, settings ...TestSetting[T]) *TestStore {
|
||||
store := NewTestStore(tb)
|
||||
switch settings := any(settings).(type) {
|
||||
case []TestSetting[bool]:
|
||||
@@ -154,8 +155,15 @@ func (s *TestStore) RegisterChangeCallback(callback func()) (unregister func(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsEmpty reports whether the store does not contain any settings.
|
||||
func (s *TestStore) IsEmpty() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.mr) == 0
|
||||
}
|
||||
|
||||
// ReadString implements [Store].
|
||||
func (s *TestStore) ReadString(key setting.Key) (string, error) {
|
||||
func (s *TestStore) ReadString(key pkey.Key) (string, error) {
|
||||
defer s.recordRead(key, setting.StringValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -174,7 +182,7 @@ func (s *TestStore) ReadString(key setting.Key) (string, error) {
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [Store].
|
||||
func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
func (s *TestStore) ReadUInt64(key pkey.Key) (uint64, error) {
|
||||
defer s.recordRead(key, setting.IntegerValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -193,7 +201,7 @@ func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
}
|
||||
|
||||
// ReadBoolean implements [Store].
|
||||
func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
func (s *TestStore) ReadBoolean(key pkey.Key) (bool, error) {
|
||||
defer s.recordRead(key, setting.BooleanValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -212,7 +220,7 @@ func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
}
|
||||
|
||||
// ReadStringArray implements [Store].
|
||||
func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
func (s *TestStore) ReadStringArray(key pkey.Key) ([]string, error) {
|
||||
defer s.recordRead(key, setting.StringListValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -230,7 +238,7 @@ func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func (s *TestStore) recordRead(key setting.Key, typ setting.Type) {
|
||||
func (s *TestStore) recordRead(key pkey.Key, typ setting.Type) {
|
||||
s.readsMu.Lock()
|
||||
op := testReadOperation{key, typ}
|
||||
num := s.reads[op]
|
||||
@@ -392,7 +400,7 @@ func (s *TestStore) SetStringLists(settings ...TestSetting[[]string]) {
|
||||
}
|
||||
|
||||
// Delete deletes the specified settings from s.
|
||||
func (s *TestStore) Delete(keys ...setting.Key) {
|
||||
func (s *TestStore) Delete(keys ...pkey.Key) {
|
||||
s.storeLock.Lock()
|
||||
for _, key := range keys {
|
||||
s.mu.Lock()
|
||||
|
||||
133
vendor/tailscale.com/util/syspolicy/syspolicy.go
generated
vendored
133
vendor/tailscale.com/util/syspolicy/syspolicy.go
generated
vendored
@@ -1,13 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package syspolicy facilitates retrieval of the current policy settings
|
||||
// applied to the device or user and receiving notifications when the policy
|
||||
// changes.
|
||||
//
|
||||
// It provides functions that return specific policy settings by their unique
|
||||
// [setting.Key]s, such as [GetBoolean], [GetUint64], [GetString],
|
||||
// [GetStringArray], [GetPreferenceOption], [GetVisibility] and [GetDuration].
|
||||
// Package syspolicy contains the implementation of system policy management.
|
||||
// Calling code should use the client interface in
|
||||
// tailscale.com/util/syspolicy/policyclient.
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
@@ -17,6 +13,9 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
@@ -45,65 +44,79 @@ func RegisterStore(name string, scope setting.PolicyScope, store source.Store) (
|
||||
return rsop.RegisterStore(name, scope, store)
|
||||
}
|
||||
|
||||
// MustRegisterStoreForTest is like [rsop.RegisterStoreForTest], but it fails the test if the store could not be registered.
|
||||
func MustRegisterStoreForTest(tb TB, name string, scope setting.PolicyScope, store source.Store) *rsop.StoreRegistration {
|
||||
tb.Helper()
|
||||
reg, err := rsop.RegisterStoreForTest(tb, name, scope, store)
|
||||
if err != nil {
|
||||
tb.Fatalf("Failed to register policy store %q as a %v policy source: %v", name, scope, err)
|
||||
// hasAnyOf returns whether at least one of the specified policy settings is configured,
|
||||
// or an error if no keys are provided or the check fails.
|
||||
func hasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
if len(keys) == 0 {
|
||||
return false, errors.New("at least one key must be specified")
|
||||
}
|
||||
return reg
|
||||
policy, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
effective := policy.Get()
|
||||
for _, k := range keys {
|
||||
_, err := effective.GetErr(k)
|
||||
if errors.Is(err, setting.ErrNotConfigured) || errors.Is(err, setting.ErrNoSuchKey) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetString returns a string policy setting with the specified key,
|
||||
// getString returns a string policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetString(key Key, defaultValue string) (string, error) {
|
||||
func getString(key pkey.Key, defaultValue string) (string, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetUint64 returns a numeric policy setting with the specified key,
|
||||
// getUint64 returns a numeric policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetUint64(key Key, defaultValue uint64) (uint64, error) {
|
||||
func getUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetBoolean returns a boolean policy setting with the specified key,
|
||||
// getBoolean returns a boolean policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetBoolean(key Key, defaultValue bool) (bool, error) {
|
||||
func getBoolean(key pkey.Key, defaultValue bool) (bool, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetStringArray returns a multi-string policy setting with the specified key,
|
||||
// getStringArray returns a multi-string policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetStringArray(key Key, defaultValue []string) ([]string, error) {
|
||||
func getStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetPreferenceOption loads a policy from the registry that can be
|
||||
// getPreferenceOption loads a policy from the registry that can be
|
||||
// managed by an enterprise policy management system and allows administrative
|
||||
// overrides of users' choices in a way that we do not want tailcontrol to have
|
||||
// the authority to set. It describes user-decides/always/never options, where
|
||||
// "always" and "never" remove the user's ability to make a selection. If not
|
||||
// present or set to a different value, "user-decides" is the default.
|
||||
func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
|
||||
return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
|
||||
// present or set to a different value, defaultValue (and a nil error) is returned.
|
||||
func getPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
|
||||
return getCurrentPolicySettingValue(name, defaultValue)
|
||||
}
|
||||
|
||||
// GetVisibility loads a policy from the registry that can be managed
|
||||
// getVisibility loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes show/hide decisions
|
||||
// for UI elements. The registry value should be a string set to "show" (return
|
||||
// true) or "hide" (return true). If not present or set to a different value,
|
||||
// "show" (return false) is the default.
|
||||
func GetVisibility(name Key) (setting.Visibility, error) {
|
||||
return getCurrentPolicySettingValue(name, setting.VisibleByPolicy)
|
||||
func getVisibility(name pkey.Key) (ptype.Visibility, error) {
|
||||
return getCurrentPolicySettingValue(name, ptype.VisibleByPolicy)
|
||||
}
|
||||
|
||||
// GetDuration loads a policy from the registry that can be managed
|
||||
// getDuration loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes a duration for some
|
||||
// action. The registry value should be a string that time.ParseDuration
|
||||
// understands. If the registry value is "" or can not be processed,
|
||||
// defaultValue is returned instead.
|
||||
func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
func getDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
d, err := getCurrentPolicySettingValue(name, defaultValue)
|
||||
if err != nil {
|
||||
return d, err
|
||||
@@ -114,9 +127,9 @@ func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// RegisterChangeCallback adds a function that will be called whenever the effective policy
|
||||
// registerChangeCallback adds a function that will be called whenever the effective policy
|
||||
// for the default scope changes. The returned function can be used to unregister the callback.
|
||||
func RegisterChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), err error) {
|
||||
func registerChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), err error) {
|
||||
effective, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,7 +141,7 @@ func RegisterChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), er
|
||||
// specified by its key from the [rsop.Policy] of the [setting.DefaultScope]. It
|
||||
// returns def if the policy setting is not configured, or an error if it has
|
||||
// an error or could not be converted to the specified type T.
|
||||
func getCurrentPolicySettingValue[T setting.ValueType](key Key, def T) (T, error) {
|
||||
func getCurrentPolicySettingValue[T setting.ValueType](key pkey.Key, def T) (T, error) {
|
||||
effective, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return def, err
|
||||
@@ -199,7 +212,53 @@ func SelectControlURL(reg, disk string) string {
|
||||
return def
|
||||
}
|
||||
|
||||
// SetDebugLoggingEnabled controls whether spammy debug logging is enabled.
|
||||
func SetDebugLoggingEnabled(v bool) {
|
||||
loggerx.SetDebugLoggingEnabled(v)
|
||||
func init() {
|
||||
policyclient.RegisterClientImpl(globalSyspolicy{})
|
||||
}
|
||||
|
||||
// globalSyspolicy implements [policyclient.Client] using the syspolicy global
|
||||
// functions and global registrations.
|
||||
//
|
||||
// TODO: de-global-ify. This implementation using the old global functions
|
||||
// is an intermediate stage while changing policyclient to be modular.
|
||||
type globalSyspolicy struct{}
|
||||
|
||||
func (globalSyspolicy) GetBoolean(key pkey.Key, defaultValue bool) (bool, error) {
|
||||
return getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetString(key pkey.Key, defaultValue string) (string, error) {
|
||||
return getString(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
|
||||
return getStringArray(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) SetDebugLoggingEnabled(enabled bool) {
|
||||
loggerx.SetDebugLoggingEnabled(enabled)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
|
||||
return getUint64(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
return getDuration(name, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
|
||||
return getPreferenceOption(name, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetVisibility(name pkey.Key) (ptype.Visibility, error) {
|
||||
return getVisibility(name)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
return hasAnyOf(keys...)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) RegisterChangeCallback(cb func(policyclient.PolicyChange)) (unregister func(), err error) {
|
||||
return registerChangeCallback(cb)
|
||||
}
|
||||
|
||||
2
vendor/tailscale.com/util/syspolicy/syspolicy_windows.go
generated
vendored
2
vendor/tailscale.com/util/syspolicy/syspolicy_windows.go
generated
vendored
@@ -43,7 +43,7 @@ func init() {
|
||||
|
||||
// configureSyspolicy configures syspolicy for use on Windows,
|
||||
// either in test or regular builds depending on whether tb has a non-nil value.
|
||||
func configureSyspolicy(tb internal.TB) error {
|
||||
func configureSyspolicy(tb testenv.TB) error {
|
||||
const localSystemSID = "S-1-5-18"
|
||||
// Always create and register a machine policy store that reads
|
||||
// policy settings from the HKEY_LOCAL_MACHINE registry hive.
|
||||
|
||||
10
vendor/tailscale.com/util/sysresources/memory.go
generated
vendored
10
vendor/tailscale.com/util/sysresources/memory.go
generated
vendored
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package sysresources
|
||||
|
||||
// TotalMemory returns the total accessible system memory, in bytes. If the
|
||||
// value cannot be determined, then 0 will be returned.
|
||||
func TotalMemory() uint64 {
|
||||
return totalMemoryImpl()
|
||||
}
|
||||
16
vendor/tailscale.com/util/sysresources/memory_bsd.go
generated
vendored
16
vendor/tailscale.com/util/sysresources/memory_bsd.go
generated
vendored
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd || openbsd || dragonfly || netbsd
|
||||
|
||||
package sysresources
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func totalMemoryImpl() uint64 {
|
||||
val, err := unix.SysctlUint64("hw.physmem")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
16
vendor/tailscale.com/util/sysresources/memory_darwin.go
generated
vendored
16
vendor/tailscale.com/util/sysresources/memory_darwin.go
generated
vendored
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package sysresources
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func totalMemoryImpl() uint64 {
|
||||
val, err := unix.SysctlUint64("hw.memsize")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
19
vendor/tailscale.com/util/sysresources/memory_linux.go
generated
vendored
19
vendor/tailscale.com/util/sysresources/memory_linux.go
generated
vendored
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package sysresources
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func totalMemoryImpl() uint64 {
|
||||
var info unix.Sysinfo_t
|
||||
|
||||
if err := unix.Sysinfo(&info); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// uint64 casts are required since these might be uint32s
|
||||
return uint64(info.Totalram) * uint64(info.Unit)
|
||||
}
|
||||
8
vendor/tailscale.com/util/sysresources/memory_unsupported.go
generated
vendored
8
vendor/tailscale.com/util/sysresources/memory_unsupported.go
generated
vendored
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(linux || darwin || freebsd || openbsd || dragonfly || netbsd)
|
||||
|
||||
package sysresources
|
||||
|
||||
func totalMemoryImpl() uint64 { return 0 }
|
||||
6
vendor/tailscale.com/util/sysresources/sysresources.go
generated
vendored
6
vendor/tailscale.com/util/sysresources/sysresources.go
generated
vendored
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package sysresources provides OS-independent methods of determining the
|
||||
// resources available to the current system.
|
||||
package sysresources
|
||||
13
vendor/tailscale.com/util/systemd/doc.go
generated
vendored
13
vendor/tailscale.com/util/systemd/doc.go
generated
vendored
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
/*
|
||||
Package systemd contains a minimal wrapper around systemd-notify to enable
|
||||
applications to signal readiness and status to systemd.
|
||||
|
||||
This package will only have effect on Linux systems running Tailscale in a
|
||||
systemd unit with the Type=notify flag set. On other operating systems (or
|
||||
when running in a Linux distro without being run from inside systemd) this
|
||||
package will become a no-op.
|
||||
*/
|
||||
package systemd
|
||||
77
vendor/tailscale.com/util/systemd/systemd_linux.go
generated
vendored
77
vendor/tailscale.com/util/systemd/systemd_linux.go
generated
vendored
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package systemd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/mdlayher/sdnotify"
|
||||
)
|
||||
|
||||
var getNotifyOnce struct {
|
||||
sync.Once
|
||||
v *sdnotify.Notifier
|
||||
}
|
||||
|
||||
type logOnce struct {
|
||||
sync.Once
|
||||
}
|
||||
|
||||
func (l *logOnce) logf(format string, args ...any) {
|
||||
l.Once.Do(func() {
|
||||
log.Printf(format, args...)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
readyOnce = &logOnce{}
|
||||
statusOnce = &logOnce{}
|
||||
)
|
||||
|
||||
func notifier() *sdnotify.Notifier {
|
||||
getNotifyOnce.Do(func() {
|
||||
var err error
|
||||
getNotifyOnce.v, err = sdnotify.New()
|
||||
// Not exist means probably not running under systemd, so don't log.
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("systemd: systemd-notifier error: %v", err)
|
||||
}
|
||||
})
|
||||
return getNotifyOnce.v
|
||||
}
|
||||
|
||||
// Ready signals readiness to systemd. This will unblock service dependents from starting.
|
||||
func Ready() {
|
||||
err := notifier().Notify(sdnotify.Ready)
|
||||
if err != nil {
|
||||
readyOnce.logf("systemd: error notifying: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Status sends a single line status update to systemd so that information shows up
|
||||
// in systemctl output. For example:
|
||||
//
|
||||
// $ systemctl status tailscale
|
||||
// ● tailscale.service - Tailscale client daemon
|
||||
// Loaded: loaded (/nix/store/qc312qcy907wz80fqrgbbm8a9djafmlg-unit-tailscale.service/tailscale.service; enabled; vendor preset: enabled)
|
||||
// Active: active (running) since Tue 2020-11-24 17:54:07 EST; 13h ago
|
||||
// Main PID: 26741 (.tailscaled-wra)
|
||||
// Status: "Connected; user@host.domain.tld; 100.101.102.103"
|
||||
// IP: 0B in, 0B out
|
||||
// Tasks: 22 (limit: 4915)
|
||||
// Memory: 30.9M
|
||||
// CPU: 2min 38.469s
|
||||
// CGroup: /system.slice/tailscale.service
|
||||
// └─26741 /nix/store/sv6cj4mw2jajm9xkbwj07k29dj30lh0n-tailscale-date.20200727/bin/tailscaled --port 41641
|
||||
func Status(format string, args ...any) {
|
||||
err := notifier().Notify(sdnotify.Statusf(format, args...))
|
||||
if err != nil {
|
||||
statusOnce.logf("systemd: error notifying: %v", err)
|
||||
}
|
||||
}
|
||||
9
vendor/tailscale.com/util/systemd/systemd_nonlinux.go
generated
vendored
9
vendor/tailscale.com/util/systemd/systemd_nonlinux.go
generated
vendored
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package systemd
|
||||
|
||||
func Ready() {}
|
||||
func Status(string, ...any) {}
|
||||
46
vendor/tailscale.com/util/testenv/testenv.go
generated
vendored
46
vendor/tailscale.com/util/testenv/testenv.go
generated
vendored
@@ -6,6 +6,7 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
@@ -19,3 +20,48 @@ func InTest() bool {
|
||||
return flag.Lookup("test.v") != nil
|
||||
})
|
||||
}
|
||||
|
||||
// TB is testing.TB, to avoid importing "testing" in non-test code.
|
||||
type TB interface {
|
||||
Cleanup(func())
|
||||
Error(args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
Fail()
|
||||
FailNow()
|
||||
Failed() bool
|
||||
Fatal(args ...any)
|
||||
Fatalf(format string, args ...any)
|
||||
Helper()
|
||||
Log(args ...any)
|
||||
Logf(format string, args ...any)
|
||||
Name() string
|
||||
Setenv(key, value string)
|
||||
Chdir(dir string)
|
||||
Skip(args ...any)
|
||||
SkipNow()
|
||||
Skipf(format string, args ...any)
|
||||
Skipped() bool
|
||||
TempDir() string
|
||||
Context() context.Context
|
||||
}
|
||||
|
||||
// InParallelTest reports whether t is running as a parallel test.
|
||||
//
|
||||
// Use of this function taints t such that its Parallel method (assuming t is an
|
||||
// actual *testing.T) will panic if called after this function.
|
||||
func InParallelTest(t TB) (isParallel bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
isParallel = true
|
||||
}
|
||||
}()
|
||||
t.Chdir(".") // panics in a t.Parallel test
|
||||
return false
|
||||
}
|
||||
|
||||
// AssertInTest panics if called outside of a test binary.
|
||||
func AssertInTest() {
|
||||
if !InTest() {
|
||||
panic("func called outside of test binary")
|
||||
}
|
||||
}
|
||||
|
||||
13
vendor/tailscale.com/util/usermetric/metrics.go
generated
vendored
13
vendor/tailscale.com/util/usermetric/metrics.go
generated
vendored
@@ -10,15 +10,15 @@ package usermetric
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
)
|
||||
|
||||
// Metrics contains user-facing metrics that are used by multiple packages.
|
||||
type Metrics struct {
|
||||
initOnce sync.Once
|
||||
|
||||
droppedPacketsInbound *metrics.MultiLabelMap[DropLabels]
|
||||
droppedPacketsOutbound *metrics.MultiLabelMap[DropLabels]
|
||||
droppedPacketsInbound *MultiLabelMap[DropLabels]
|
||||
droppedPacketsOutbound *MultiLabelMap[DropLabels]
|
||||
}
|
||||
|
||||
// DropReason is the reason why a packet was dropped.
|
||||
@@ -55,6 +55,9 @@ type DropLabels struct {
|
||||
|
||||
// initOnce initializes the common metrics.
|
||||
func (r *Registry) initOnce() {
|
||||
if !buildfeatures.HasUserMetrics {
|
||||
return
|
||||
}
|
||||
r.m.initOnce.Do(func() {
|
||||
r.m.droppedPacketsInbound = NewMultiLabelMapWithRegistry[DropLabels](
|
||||
r,
|
||||
@@ -73,13 +76,13 @@ func (r *Registry) initOnce() {
|
||||
|
||||
// DroppedPacketsOutbound returns the outbound dropped packet metric, creating it
|
||||
// if necessary.
|
||||
func (r *Registry) DroppedPacketsOutbound() *metrics.MultiLabelMap[DropLabels] {
|
||||
func (r *Registry) DroppedPacketsOutbound() *MultiLabelMap[DropLabels] {
|
||||
r.initOnce()
|
||||
return r.m.droppedPacketsOutbound
|
||||
}
|
||||
|
||||
// DroppedPacketsInbound returns the inbound dropped packet metric.
|
||||
func (r *Registry) DroppedPacketsInbound() *metrics.MultiLabelMap[DropLabels] {
|
||||
func (r *Registry) DroppedPacketsInbound() *MultiLabelMap[DropLabels] {
|
||||
r.initOnce()
|
||||
return r.m.droppedPacketsInbound
|
||||
}
|
||||
|
||||
29
vendor/tailscale.com/util/usermetric/omit.go
generated
vendored
Normal file
29
vendor/tailscale.com/util/usermetric/omit.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_usermetrics
|
||||
|
||||
package usermetric
|
||||
|
||||
type Registry struct {
|
||||
m Metrics
|
||||
}
|
||||
|
||||
func (*Registry) NewGauge(name, help string) *Gauge { return nil }
|
||||
|
||||
type MultiLabelMap[T comparable] = noopMap[T]
|
||||
|
||||
type noopMap[T comparable] struct{}
|
||||
|
||||
type Gauge struct{}
|
||||
|
||||
func (*Gauge) Set(float64) {}
|
||||
|
||||
func NewMultiLabelMapWithRegistry[T comparable](m *Registry, name string, promType, helpText string) *MultiLabelMap[T] {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*noopMap[T]) Add(T, int64) {}
|
||||
func (*noopMap[T]) Set(T, any) {}
|
||||
|
||||
func (r *Registry) Handler(any, any) {} // no-op HTTP handler
|
||||
6
vendor/tailscale.com/util/usermetric/usermetric.go
generated
vendored
6
vendor/tailscale.com/util/usermetric/usermetric.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_usermetrics
|
||||
|
||||
// Package usermetric provides a container and handler
|
||||
// for user-facing metrics.
|
||||
package usermetric
|
||||
@@ -25,6 +27,10 @@ type Registry struct {
|
||||
m Metrics
|
||||
}
|
||||
|
||||
// MultiLabelMap is an alias for metrics.MultiLabelMap in the common case,
|
||||
// or an alias to a lighter type when usermetrics are omitted from the build.
|
||||
type MultiLabelMap[T comparable] = metrics.MultiLabelMap[T]
|
||||
|
||||
// NewMultiLabelMapWithRegistry creates and register a new
|
||||
// MultiLabelMap[T] variable with the given name and returns it.
|
||||
// The variable is registered with the userfacing metrics package.
|
||||
|
||||
20
vendor/tailscale.com/util/winutil/authenticode/zsyscall_windows.go
generated
vendored
20
vendor/tailscale.com/util/winutil/authenticode/zsyscall_windows.go
generated
vendored
@@ -56,7 +56,7 @@ var (
|
||||
)
|
||||
|
||||
func cryptMsgClose(cryptMsg windows.Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCryptMsgClose.Addr(), 1, uintptr(cryptMsg), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptMsgClose.Addr(), uintptr(cryptMsg))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func cryptMsgClose(cryptMsg windows.Handle) (err error) {
|
||||
}
|
||||
|
||||
func cryptMsgGetParam(cryptMsg windows.Handle, paramType uint32, index uint32, data unsafe.Pointer, dataLen *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procCryptMsgGetParam.Addr(), 5, uintptr(cryptMsg), uintptr(paramType), uintptr(index), uintptr(data), uintptr(unsafe.Pointer(dataLen)), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptMsgGetParam.Addr(), uintptr(cryptMsg), uintptr(paramType), uintptr(index), uintptr(data), uintptr(unsafe.Pointer(dataLen)))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func cryptMsgGetParam(cryptMsg windows.Handle, paramType uint32, index uint32, d
|
||||
}
|
||||
|
||||
func cryptVerifyMessageSignature(pVerifyPara *_CRYPT_VERIFY_MESSAGE_PARA, signerIndex uint32, pbSignedBlob *byte, cbSignedBlob uint32, pbDecoded *byte, pdbDecoded *uint32, ppSignerCert **windows.CertContext) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procCryptVerifyMessageSignature.Addr(), 7, uintptr(unsafe.Pointer(pVerifyPara)), uintptr(signerIndex), uintptr(unsafe.Pointer(pbSignedBlob)), uintptr(cbSignedBlob), uintptr(unsafe.Pointer(pbDecoded)), uintptr(unsafe.Pointer(pdbDecoded)), uintptr(unsafe.Pointer(ppSignerCert)), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptVerifyMessageSignature.Addr(), uintptr(unsafe.Pointer(pVerifyPara)), uintptr(signerIndex), uintptr(unsafe.Pointer(pbSignedBlob)), uintptr(cbSignedBlob), uintptr(unsafe.Pointer(pbDecoded)), uintptr(unsafe.Pointer(pdbDecoded)), uintptr(unsafe.Pointer(ppSignerCert)))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -80,13 +80,13 @@ func cryptVerifyMessageSignature(pVerifyPara *_CRYPT_VERIFY_MESSAGE_PARA, signer
|
||||
}
|
||||
|
||||
func msiGetFileSignatureInformation(signedObjectPath *uint16, flags uint32, certCtx **windows.CertContext, pbHashData *byte, cbHashData *uint32) (ret wingoes.HRESULT) {
|
||||
r0, _, _ := syscall.Syscall6(procMsiGetFileSignatureInformationW.Addr(), 5, uintptr(unsafe.Pointer(signedObjectPath)), uintptr(flags), uintptr(unsafe.Pointer(certCtx)), uintptr(unsafe.Pointer(pbHashData)), uintptr(unsafe.Pointer(cbHashData)), 0)
|
||||
r0, _, _ := syscall.SyscallN(procMsiGetFileSignatureInformationW.Addr(), uintptr(unsafe.Pointer(signedObjectPath)), uintptr(flags), uintptr(unsafe.Pointer(certCtx)), uintptr(unsafe.Pointer(pbHashData)), uintptr(unsafe.Pointer(cbHashData)))
|
||||
ret = wingoes.HRESULT(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func cryptCATAdminAcquireContext2(hCatAdmin *_HCATADMIN, pgSubsystem *windows.GUID, hashAlgorithm *uint16, strongHashPolicy *windows.CertStrongSignPara, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procCryptCATAdminAcquireContext2.Addr(), 5, uintptr(unsafe.Pointer(hCatAdmin)), uintptr(unsafe.Pointer(pgSubsystem)), uintptr(unsafe.Pointer(hashAlgorithm)), uintptr(unsafe.Pointer(strongHashPolicy)), uintptr(flags), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptCATAdminAcquireContext2.Addr(), uintptr(unsafe.Pointer(hCatAdmin)), uintptr(unsafe.Pointer(pgSubsystem)), uintptr(unsafe.Pointer(hashAlgorithm)), uintptr(unsafe.Pointer(strongHashPolicy)), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func cryptCATAdminAcquireContext2(hCatAdmin *_HCATADMIN, pgSubsystem *windows.GU
|
||||
}
|
||||
|
||||
func cryptCATAdminCalcHashFromFileHandle2(hCatAdmin _HCATADMIN, file windows.Handle, pcbHash *uint32, pbHash *byte, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procCryptCATAdminCalcHashFromFileHandle2.Addr(), 5, uintptr(hCatAdmin), uintptr(file), uintptr(unsafe.Pointer(pcbHash)), uintptr(unsafe.Pointer(pbHash)), uintptr(flags), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptCATAdminCalcHashFromFileHandle2.Addr(), uintptr(hCatAdmin), uintptr(file), uintptr(unsafe.Pointer(pcbHash)), uintptr(unsafe.Pointer(pbHash)), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func cryptCATAdminCalcHashFromFileHandle2(hCatAdmin _HCATADMIN, file windows.Han
|
||||
}
|
||||
|
||||
func cryptCATAdminEnumCatalogFromHash(hCatAdmin _HCATADMIN, pbHash *byte, cbHash uint32, flags uint32, prevCatInfo *_HCATINFO) (ret _HCATINFO, err error) {
|
||||
r0, _, e1 := syscall.Syscall6(procCryptCATAdminEnumCatalogFromHash.Addr(), 5, uintptr(hCatAdmin), uintptr(unsafe.Pointer(pbHash)), uintptr(cbHash), uintptr(flags), uintptr(unsafe.Pointer(prevCatInfo)), 0)
|
||||
r0, _, e1 := syscall.SyscallN(procCryptCATAdminEnumCatalogFromHash.Addr(), uintptr(hCatAdmin), uintptr(unsafe.Pointer(pbHash)), uintptr(cbHash), uintptr(flags), uintptr(unsafe.Pointer(prevCatInfo)))
|
||||
ret = _HCATINFO(r0)
|
||||
if ret == 0 {
|
||||
err = errnoErr(e1)
|
||||
@@ -111,7 +111,7 @@ func cryptCATAdminEnumCatalogFromHash(hCatAdmin _HCATADMIN, pbHash *byte, cbHash
|
||||
}
|
||||
|
||||
func cryptCATAdminReleaseCatalogContext(hCatAdmin _HCATADMIN, hCatInfo _HCATINFO, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCryptCATAdminReleaseCatalogContext.Addr(), 3, uintptr(hCatAdmin), uintptr(hCatInfo), uintptr(flags))
|
||||
r1, _, e1 := syscall.SyscallN(procCryptCATAdminReleaseCatalogContext.Addr(), uintptr(hCatAdmin), uintptr(hCatInfo), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func cryptCATAdminReleaseCatalogContext(hCatAdmin _HCATADMIN, hCatInfo _HCATINFO
|
||||
}
|
||||
|
||||
func cryptCATAdminReleaseContext(hCatAdmin _HCATADMIN, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCryptCATAdminReleaseContext.Addr(), 2, uintptr(hCatAdmin), uintptr(flags), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procCryptCATAdminReleaseContext.Addr(), uintptr(hCatAdmin), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func cryptCATAdminReleaseContext(hCatAdmin _HCATADMIN, flags uint32) (err error)
|
||||
}
|
||||
|
||||
func cryptCATAdminCatalogInfoFromContext(hCatInfo _HCATINFO, catInfo *_CATALOG_INFO, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCryptCATCatalogInfoFromContext.Addr(), 3, uintptr(hCatInfo), uintptr(unsafe.Pointer(catInfo)), uintptr(flags))
|
||||
r1, _, e1 := syscall.SyscallN(procCryptCATCatalogInfoFromContext.Addr(), uintptr(hCatInfo), uintptr(unsafe.Pointer(catInfo)), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
|
||||
80
vendor/tailscale.com/util/winutil/gp/policylock_windows.go
generated
vendored
80
vendor/tailscale.com/util/winutil/gp/policylock_windows.go
generated
vendored
@@ -127,32 +127,32 @@ func NewUserPolicyLock(token windows.Token) (*PolicyLock, error) {
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
// Lock locks l.
|
||||
// It returns [ErrInvalidLockState] if l has a zero value or has already been closed,
|
||||
// Lock locks lk.
|
||||
// It returns [ErrInvalidLockState] if lk has a zero value or has already been closed,
|
||||
// [ErrLockRestricted] if the lock cannot be acquired due to a restriction in place,
|
||||
// or a [syscall.Errno] if the underlying Group Policy lock cannot be acquired.
|
||||
//
|
||||
// As a special case, it fails with [windows.ERROR_ACCESS_DENIED]
|
||||
// if l is a user policy lock, and the corresponding user is not logged in
|
||||
// if lk is a user policy lock, and the corresponding user is not logged in
|
||||
// interactively at the time of the call.
|
||||
func (l *PolicyLock) Lock() error {
|
||||
func (lk *PolicyLock) Lock() error {
|
||||
if policyLockRestricted.Load() > 0 {
|
||||
return ErrLockRestricted
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.lockCnt.Add(2)&1 == 0 {
|
||||
lk.mu.Lock()
|
||||
defer lk.mu.Unlock()
|
||||
if lk.lockCnt.Add(2)&1 == 0 {
|
||||
// The lock cannot be acquired because it has either never been properly
|
||||
// created or its Close method has already been called. However, we need
|
||||
// to call Unlock to both decrement lockCnt and leave the underlying
|
||||
// CriticalPolicySection if we won the race with another goroutine and
|
||||
// now own the lock.
|
||||
l.Unlock()
|
||||
lk.Unlock()
|
||||
return ErrInvalidLockState
|
||||
}
|
||||
|
||||
if l.handle != 0 {
|
||||
if lk.handle != 0 {
|
||||
// The underlying CriticalPolicySection is already acquired.
|
||||
// It is an R-Lock (with the W-counterpart owned by the Group Policy service),
|
||||
// meaning that it can be acquired by multiple readers simultaneously.
|
||||
@@ -160,20 +160,20 @@ func (l *PolicyLock) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return l.lockSlow()
|
||||
return lk.lockSlow()
|
||||
}
|
||||
|
||||
// lockSlow calls enterCriticalPolicySection to acquire the underlying GP read lock.
|
||||
// It waits for either the lock to be acquired, or for the Close method to be called.
|
||||
//
|
||||
// l.mu must be held.
|
||||
func (l *PolicyLock) lockSlow() (err error) {
|
||||
func (lk *PolicyLock) lockSlow() (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Decrement the counter if the lock cannot be acquired,
|
||||
// and complete the pending close request if we're the last owner.
|
||||
if l.lockCnt.Add(-2) == 0 {
|
||||
l.closeInternal()
|
||||
if lk.lockCnt.Add(-2) == 0 {
|
||||
lk.closeInternal()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -190,12 +190,12 @@ func (l *PolicyLock) lockSlow() (err error) {
|
||||
resultCh := make(chan policyLockResult)
|
||||
|
||||
go func() {
|
||||
closing := l.closing
|
||||
if l.scope == UserPolicy && l.token != 0 {
|
||||
closing := lk.closing
|
||||
if lk.scope == UserPolicy && lk.token != 0 {
|
||||
// Impersonate the user whose critical policy section we want to acquire.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := impersonateLoggedOnUser(l.token); err != nil {
|
||||
if err := impersonateLoggedOnUser(lk.token); err != nil {
|
||||
initCh <- err
|
||||
return
|
||||
}
|
||||
@@ -209,10 +209,10 @@ func (l *PolicyLock) lockSlow() (err error) {
|
||||
close(initCh)
|
||||
|
||||
var machine bool
|
||||
if l.scope == MachinePolicy {
|
||||
if lk.scope == MachinePolicy {
|
||||
machine = true
|
||||
}
|
||||
handle, err := l.enterFn(machine)
|
||||
handle, err := lk.enterFn(machine)
|
||||
|
||||
send_result:
|
||||
for {
|
||||
@@ -226,7 +226,7 @@ func (l *PolicyLock) lockSlow() (err error) {
|
||||
// The lock is being closed, and we lost the race to l.closing
|
||||
// it the calling goroutine.
|
||||
if err == nil {
|
||||
l.leaveFn(handle)
|
||||
lk.leaveFn(handle)
|
||||
}
|
||||
break send_result
|
||||
default:
|
||||
@@ -247,21 +247,21 @@ func (l *PolicyLock) lockSlow() (err error) {
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if result.err == nil {
|
||||
l.handle = result.handle
|
||||
lk.handle = result.handle
|
||||
}
|
||||
return result.err
|
||||
case <-l.closing:
|
||||
case <-lk.closing:
|
||||
return ErrInvalidLockState
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock unlocks l.
|
||||
// It panics if l is not locked on entry to Unlock.
|
||||
func (l *PolicyLock) Unlock() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
func (lk *PolicyLock) Unlock() {
|
||||
lk.mu.Lock()
|
||||
defer lk.mu.Unlock()
|
||||
|
||||
lockCnt := l.lockCnt.Add(-2)
|
||||
lockCnt := lk.lockCnt.Add(-2)
|
||||
if lockCnt < 0 {
|
||||
panic("negative lockCnt")
|
||||
}
|
||||
@@ -273,33 +273,33 @@ func (l *PolicyLock) Unlock() {
|
||||
return
|
||||
}
|
||||
|
||||
if l.handle != 0 {
|
||||
if lk.handle != 0 {
|
||||
// Impersonation is not required to unlock a critical policy section.
|
||||
// The handle we pass determines which mutex will be unlocked.
|
||||
leaveCriticalPolicySection(l.handle)
|
||||
l.handle = 0
|
||||
leaveCriticalPolicySection(lk.handle)
|
||||
lk.handle = 0
|
||||
}
|
||||
|
||||
if lockCnt == 0 {
|
||||
// Complete the pending close request if there's no more readers.
|
||||
l.closeInternal()
|
||||
lk.closeInternal()
|
||||
}
|
||||
}
|
||||
|
||||
// Close releases resources associated with l.
|
||||
// It is a no-op for the machine policy lock.
|
||||
func (l *PolicyLock) Close() error {
|
||||
lockCnt := l.lockCnt.Load()
|
||||
func (lk *PolicyLock) Close() error {
|
||||
lockCnt := lk.lockCnt.Load()
|
||||
if lockCnt&1 == 0 {
|
||||
// The lock has never been initialized, or close has already been called.
|
||||
return nil
|
||||
}
|
||||
|
||||
close(l.closing)
|
||||
close(lk.closing)
|
||||
|
||||
// Unset the LSB to indicate a pending close request.
|
||||
for !l.lockCnt.CompareAndSwap(lockCnt, lockCnt&^int32(1)) {
|
||||
lockCnt = l.lockCnt.Load()
|
||||
for !lk.lockCnt.CompareAndSwap(lockCnt, lockCnt&^int32(1)) {
|
||||
lockCnt = lk.lockCnt.Load()
|
||||
}
|
||||
|
||||
if lockCnt != 0 {
|
||||
@@ -307,16 +307,16 @@ func (l *PolicyLock) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return l.closeInternal()
|
||||
return lk.closeInternal()
|
||||
}
|
||||
|
||||
func (l *PolicyLock) closeInternal() error {
|
||||
if l.token != 0 {
|
||||
if err := l.token.Close(); err != nil {
|
||||
func (lk *PolicyLock) closeInternal() error {
|
||||
if lk.token != 0 {
|
||||
if err := lk.token.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
l.token = 0
|
||||
lk.token = 0
|
||||
}
|
||||
l.closing = nil
|
||||
lk.closing = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
12
vendor/tailscale.com/util/winutil/gp/zsyscall_windows.go
generated
vendored
12
vendor/tailscale.com/util/winutil/gp/zsyscall_windows.go
generated
vendored
@@ -50,7 +50,7 @@ var (
|
||||
)
|
||||
|
||||
func impersonateLoggedOnUser(token windows.Token) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procImpersonateLoggedOnUser.Addr(), 1, uintptr(token), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procImpersonateLoggedOnUser.Addr(), uintptr(token))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func enterCriticalPolicySection(machine bool) (handle policyLockHandle, err erro
|
||||
if machine {
|
||||
_p0 = 1
|
||||
}
|
||||
r0, _, e1 := syscall.Syscall(procEnterCriticalPolicySection.Addr(), 1, uintptr(_p0), 0, 0)
|
||||
r0, _, e1 := syscall.SyscallN(procEnterCriticalPolicySection.Addr(), uintptr(_p0))
|
||||
handle = policyLockHandle(r0)
|
||||
if int32(handle) == 0 {
|
||||
err = errnoErr(e1)
|
||||
@@ -71,7 +71,7 @@ func enterCriticalPolicySection(machine bool) (handle policyLockHandle, err erro
|
||||
}
|
||||
|
||||
func leaveCriticalPolicySection(handle policyLockHandle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procLeaveCriticalPolicySection.Addr(), 1, uintptr(handle), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procLeaveCriticalPolicySection.Addr(), uintptr(handle))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func refreshPolicyEx(machine bool, flags uint32) (err error) {
|
||||
if machine {
|
||||
_p0 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.Syscall(procRefreshPolicyEx.Addr(), 2, uintptr(_p0), uintptr(flags), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procRefreshPolicyEx.Addr(), uintptr(_p0), uintptr(flags))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func registerGPNotification(event windows.Handle, machine bool) (err error) {
|
||||
if machine {
|
||||
_p0 = 1
|
||||
}
|
||||
r1, _, e1 := syscall.Syscall(procRegisterGPNotification.Addr(), 2, uintptr(event), uintptr(_p0), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procRegisterGPNotification.Addr(), uintptr(event), uintptr(_p0))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func registerGPNotification(event windows.Handle, machine bool) (err error) {
|
||||
}
|
||||
|
||||
func unregisterGPNotification(event windows.Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procUnregisterGPNotification.Addr(), 1, uintptr(event), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procUnregisterGPNotification.Addr(), uintptr(event))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
|
||||
3
vendor/tailscale.com/util/winutil/restartmgr_windows.go
generated
vendored
3
vendor/tailscale.com/util/winutil/restartmgr_windows.go
generated
vendored
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/dblohm7/wingoes"
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -538,7 +537,7 @@ func (rps RestartableProcesses) Terminate(logf logger.Logf, exitCode uint32, tim
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return multierr.New(errs...)
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
4
vendor/tailscale.com/util/winutil/startupinfo_windows.go
generated
vendored
4
vendor/tailscale.com/util/winutil/startupinfo_windows.go
generated
vendored
@@ -83,8 +83,8 @@ func (sib *StartupInfoBuilder) Resolve() (startupInfo *windows.StartupInfo, inhe
|
||||
// Always create a Unicode environment.
|
||||
createProcessFlags = windows.CREATE_UNICODE_ENVIRONMENT
|
||||
|
||||
if l := uint32(len(sib.attrs)); l > 0 {
|
||||
attrCont, err := windows.NewProcThreadAttributeList(l)
|
||||
if ln := uint32(len(sib.attrs)); ln > 0 {
|
||||
attrCont, err := windows.NewProcThreadAttributeList(ln)
|
||||
if err != nil {
|
||||
return nil, false, 0, err
|
||||
}
|
||||
|
||||
6
vendor/tailscale.com/util/winutil/winenv/zsyscall_windows.go
generated
vendored
6
vendor/tailscale.com/util/winutil/winenv/zsyscall_windows.go
generated
vendored
@@ -55,7 +55,7 @@ func isDeviceRegisteredWithManagement(isMDMRegistered *bool, upnBufLen uint32, u
|
||||
if *isMDMRegistered {
|
||||
_p0 = 1
|
||||
}
|
||||
r0, _, e1 := syscall.Syscall(procIsDeviceRegisteredWithManagement.Addr(), 3, uintptr(unsafe.Pointer(&_p0)), uintptr(upnBufLen), uintptr(unsafe.Pointer(upnBuf)))
|
||||
r0, _, e1 := syscall.SyscallN(procIsDeviceRegisteredWithManagement.Addr(), uintptr(unsafe.Pointer(&_p0)), uintptr(upnBufLen), uintptr(unsafe.Pointer(upnBuf)))
|
||||
*isMDMRegistered = _p0 != 0
|
||||
hr = int32(r0)
|
||||
if hr == 0 {
|
||||
@@ -65,13 +65,13 @@ func isDeviceRegisteredWithManagement(isMDMRegistered *bool, upnBufLen uint32, u
|
||||
}
|
||||
|
||||
func verSetConditionMask(condMask verCondMask, typ verTypeMask, cond verCond) (res verCondMask) {
|
||||
r0, _, _ := syscall.Syscall(procVerSetConditionMask.Addr(), 3, uintptr(condMask), uintptr(typ), uintptr(cond))
|
||||
r0, _, _ := syscall.SyscallN(procVerSetConditionMask.Addr(), uintptr(condMask), uintptr(typ), uintptr(cond))
|
||||
res = verCondMask(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func verifyVersionInfo(verInfo *osVersionInfoEx, typ verTypeMask, cond verCondMask) (res bool) {
|
||||
r0, _, _ := syscall.Syscall(procVerifyVersionInfoW.Addr(), 3, uintptr(unsafe.Pointer(verInfo)), uintptr(typ), uintptr(cond))
|
||||
r0, _, _ := syscall.SyscallN(procVerifyVersionInfoW.Addr(), uintptr(unsafe.Pointer(verInfo)), uintptr(typ), uintptr(cond))
|
||||
res = r0 != 0
|
||||
return
|
||||
}
|
||||
|
||||
25
vendor/tailscale.com/util/winutil/winutil_windows.go
generated
vendored
25
vendor/tailscale.com/util/winutil/winutil_windows.go
generated
vendored
@@ -8,8 +8,10 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -33,6 +35,10 @@ var ErrNoShell = errors.New("no Shell process is present")
|
||||
// ErrNoValue is returned when the value doesn't exist in the registry.
|
||||
var ErrNoValue = registry.ErrNotExist
|
||||
|
||||
// ErrBadRegValueFormat is returned when a string value does not match the
|
||||
// expected format.
|
||||
var ErrBadRegValueFormat = errors.New("registry value formatted incorrectly")
|
||||
|
||||
// GetDesktopPID searches the PID of the process that's running the
|
||||
// currently active desktop. Returns ErrNoShell if the shell is not present.
|
||||
// Usually the PID will be for explorer.exe.
|
||||
@@ -947,3 +953,22 @@ func IsDomainName(name string) (bool, error) {
|
||||
|
||||
return isDomainName(name16)
|
||||
}
|
||||
|
||||
// GUIPathFromReg obtains the path to the client GUI executable from the
|
||||
// registry value that was written during installation.
|
||||
func GUIPathFromReg() (string, error) {
|
||||
regPath, err := GetRegString("GUIPath")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(regPath) {
|
||||
return "", ErrBadRegValueFormat
|
||||
}
|
||||
|
||||
if _, err := os.Stat(regPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return regPath, nil
|
||||
}
|
||||
|
||||
26
vendor/tailscale.com/util/winutil/zsyscall_windows.go
generated
vendored
26
vendor/tailscale.com/util/winutil/zsyscall_windows.go
generated
vendored
@@ -62,7 +62,7 @@ var (
|
||||
)
|
||||
|
||||
func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procQueryServiceConfig2W.Addr(), 5, uintptr(hService), uintptr(infoLevel), uintptr(unsafe.Pointer(buf)), uintptr(bufLen), uintptr(unsafe.Pointer(bytesNeeded)), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procQueryServiceConfig2W.Addr(), uintptr(hService), uintptr(infoLevel), uintptr(unsafe.Pointer(buf)), uintptr(bufLen), uintptr(unsafe.Pointer(bytesNeeded)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -70,19 +70,19 @@ func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, b
|
||||
}
|
||||
|
||||
func getApplicationRestartSettings(process windows.Handle, commandLine *uint16, commandLineLen *uint32, flags *uint32) (ret wingoes.HRESULT) {
|
||||
r0, _, _ := syscall.Syscall6(procGetApplicationRestartSettings.Addr(), 4, uintptr(process), uintptr(unsafe.Pointer(commandLine)), uintptr(unsafe.Pointer(commandLineLen)), uintptr(unsafe.Pointer(flags)), 0, 0)
|
||||
r0, _, _ := syscall.SyscallN(procGetApplicationRestartSettings.Addr(), uintptr(process), uintptr(unsafe.Pointer(commandLine)), uintptr(unsafe.Pointer(commandLineLen)), uintptr(unsafe.Pointer(flags)))
|
||||
ret = wingoes.HRESULT(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret wingoes.HRESULT) {
|
||||
r0, _, _ := syscall.Syscall(procRegisterApplicationRestart.Addr(), 2, uintptr(unsafe.Pointer(cmdLineExclExeName)), uintptr(flags), 0)
|
||||
r0, _, _ := syscall.SyscallN(procRegisterApplicationRestart.Addr(), uintptr(unsafe.Pointer(cmdLineExclExeName)), uintptr(flags))
|
||||
ret = wingoes.HRESULT(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func dsGetDcName(computerName *uint16, domainName *uint16, domainGuid *windows.GUID, siteName *uint16, flags dsGetDcNameFlag, dcInfo **_DOMAIN_CONTROLLER_INFO) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procDsGetDcNameW.Addr(), 6, uintptr(unsafe.Pointer(computerName)), uintptr(unsafe.Pointer(domainName)), uintptr(unsafe.Pointer(domainGuid)), uintptr(unsafe.Pointer(siteName)), uintptr(flags), uintptr(unsafe.Pointer(dcInfo)))
|
||||
r0, _, _ := syscall.SyscallN(procDsGetDcNameW.Addr(), uintptr(unsafe.Pointer(computerName)), uintptr(unsafe.Pointer(domainName)), uintptr(unsafe.Pointer(domainGuid)), uintptr(unsafe.Pointer(siteName)), uintptr(flags), uintptr(unsafe.Pointer(dcInfo)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func dsGetDcName(computerName *uint16, domainName *uint16, domainGuid *windows.G
|
||||
}
|
||||
|
||||
func netValidateName(server *uint16, name *uint16, account *uint16, password *uint16, nameType _NETSETUP_NAME_TYPE) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procNetValidateName.Addr(), 5, uintptr(unsafe.Pointer(server)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(account)), uintptr(unsafe.Pointer(password)), uintptr(nameType), 0)
|
||||
r0, _, _ := syscall.SyscallN(procNetValidateName.Addr(), uintptr(unsafe.Pointer(server)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(account)), uintptr(unsafe.Pointer(password)), uintptr(nameType))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func netValidateName(server *uint16, name *uint16, account *uint16, password *ui
|
||||
}
|
||||
|
||||
func rmEndSession(session _RMHANDLE) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procRmEndSession.Addr(), 1, uintptr(session), 0, 0)
|
||||
r0, _, _ := syscall.SyscallN(procRmEndSession.Addr(), uintptr(session))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func rmEndSession(session _RMHANDLE) (ret error) {
|
||||
}
|
||||
|
||||
func rmGetList(session _RMHANDLE, nProcInfoNeeded *uint32, nProcInfo *uint32, rgAffectedApps *_RM_PROCESS_INFO, pRebootReasons *uint32) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procRmGetList.Addr(), 5, uintptr(session), uintptr(unsafe.Pointer(nProcInfoNeeded)), uintptr(unsafe.Pointer(nProcInfo)), uintptr(unsafe.Pointer(rgAffectedApps)), uintptr(unsafe.Pointer(pRebootReasons)), 0)
|
||||
r0, _, _ := syscall.SyscallN(procRmGetList.Addr(), uintptr(session), uintptr(unsafe.Pointer(nProcInfoNeeded)), uintptr(unsafe.Pointer(nProcInfo)), uintptr(unsafe.Pointer(rgAffectedApps)), uintptr(unsafe.Pointer(pRebootReasons)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func rmGetList(session _RMHANDLE, nProcInfoNeeded *uint32, nProcInfo *uint32, rg
|
||||
}
|
||||
|
||||
func rmJoinSession(pSession *_RMHANDLE, sessionKey *uint16) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procRmJoinSession.Addr(), 2, uintptr(unsafe.Pointer(pSession)), uintptr(unsafe.Pointer(sessionKey)), 0)
|
||||
r0, _, _ := syscall.SyscallN(procRmJoinSession.Addr(), uintptr(unsafe.Pointer(pSession)), uintptr(unsafe.Pointer(sessionKey)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func rmJoinSession(pSession *_RMHANDLE, sessionKey *uint16) (ret error) {
|
||||
}
|
||||
|
||||
func rmRegisterResources(session _RMHANDLE, nFiles uint32, rgsFileNames **uint16, nApplications uint32, rgApplications *_RM_UNIQUE_PROCESS, nServices uint32, rgsServiceNames **uint16) (ret error) {
|
||||
r0, _, _ := syscall.Syscall9(procRmRegisterResources.Addr(), 7, uintptr(session), uintptr(nFiles), uintptr(unsafe.Pointer(rgsFileNames)), uintptr(nApplications), uintptr(unsafe.Pointer(rgApplications)), uintptr(nServices), uintptr(unsafe.Pointer(rgsServiceNames)), 0, 0)
|
||||
r0, _, _ := syscall.SyscallN(procRmRegisterResources.Addr(), uintptr(session), uintptr(nFiles), uintptr(unsafe.Pointer(rgsFileNames)), uintptr(nApplications), uintptr(unsafe.Pointer(rgApplications)), uintptr(nServices), uintptr(unsafe.Pointer(rgsServiceNames)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func rmRegisterResources(session _RMHANDLE, nFiles uint32, rgsFileNames **uint16
|
||||
}
|
||||
|
||||
func rmStartSession(pSession *_RMHANDLE, flags uint32, sessionKey *uint16) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procRmStartSession.Addr(), 3, uintptr(unsafe.Pointer(pSession)), uintptr(flags), uintptr(unsafe.Pointer(sessionKey)))
|
||||
r0, _, _ := syscall.SyscallN(procRmStartSession.Addr(), uintptr(unsafe.Pointer(pSession)), uintptr(flags), uintptr(unsafe.Pointer(sessionKey)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func rmStartSession(pSession *_RMHANDLE, flags uint32, sessionKey *uint16) (ret
|
||||
}
|
||||
|
||||
func expandEnvironmentStringsForUser(token windows.Token, src *uint16, dst *uint16, dstLen uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procExpandEnvironmentStringsForUserW.Addr(), 4, uintptr(token), uintptr(unsafe.Pointer(src)), uintptr(unsafe.Pointer(dst)), uintptr(dstLen), 0, 0)
|
||||
r1, _, e1 := syscall.SyscallN(procExpandEnvironmentStringsForUserW.Addr(), uintptr(token), uintptr(unsafe.Pointer(src)), uintptr(unsafe.Pointer(dst)), uintptr(dstLen))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func expandEnvironmentStringsForUser(token windows.Token, src *uint16, dst *uint
|
||||
}
|
||||
|
||||
func loadUserProfile(token windows.Token, profileInfo *_PROFILEINFO) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procLoadUserProfileW.Addr(), 2, uintptr(token), uintptr(unsafe.Pointer(profileInfo)), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procLoadUserProfileW.Addr(), uintptr(token), uintptr(unsafe.Pointer(profileInfo)))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func loadUserProfile(token windows.Token, profileInfo *_PROFILEINFO) (err error)
|
||||
}
|
||||
|
||||
func unloadUserProfile(token windows.Token, profile registry.Key) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procUnloadUserProfile.Addr(), 2, uintptr(token), uintptr(profile), 0)
|
||||
r1, _, e1 := syscall.SyscallN(procUnloadUserProfile.Addr(), uintptr(token), uintptr(profile))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user