This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

86
vendor/tailscale.com/util/backoff/backoff.go generated vendored Normal file
View 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
View 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
}

View File

@@ -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
View 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 }

View File

@@ -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
View 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
}

View 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
}

View File

@@ -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
View 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>

Binary file not shown.

Binary file not shown.

97
vendor/tailscale.com/util/eventbus/assets/main.html generated vendored Normal file
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View File

@@ -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()
}

View File

@@ -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()]
}

View File

@@ -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
}

View File

@@ -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 doesnt support "--version", as long as it
// can determine a version then itll carry on.
hostinfo.SetFirewallMode("ipt-fb")
return FirewallModeIPTables
}
}

View File

@@ -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
}

View File

@@ -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])
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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) {}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
View 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
}

View File

@@ -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
View 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
View 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
}
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
View 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"
)

View File

@@ -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)
}
}

View 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
}

View File

@@ -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'

View File

@@ -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]
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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 = '/'

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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...)}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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) {}

View File

@@ -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")
}
}

View File

@@ -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
View 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

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}