Compare commits

...

21 Commits

Author SHA1 Message Date
Percy Wegmann
51f7cb0903 ssh/tailssh: remove unused public key authentication logic
In preparation for unforking golang.org/x/crypto/ssh.

Updates #8593

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-06-06 06:28:30 -05:00
Jordan Whited
cf1e6c6e55 cmd/stunstamp: fix remote write retry (#12348)
Evaluation of remote write errors was using errors.Is() where it should
have been using errors.As().

Updates tailscale/corp#20344

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-06-05 06:36:48 -07:00
Brad Fitzpatrick
6d3c10579e gokrazy: update breakglass with now-upstreamed ec2 change
This updates breakglass to use the now-upsteamed
https://github.com/gokrazy/breakglass/pull/18 change
so we're not using our fork now.

It also adds a gok wrapper tool, because doing it by hand
was tedious.

Updates #1866

Change-Id: Ifacbf5fbf0e377b3bd95c5f76c18751c2e1af7d7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-04 15:21:57 -07:00
Andrew Lytvynov
347e3f3d9a go.mod,ipn/ipnlocal: update the ACME fork (#12343)
Update our fork of golang.org/x/crypto to pick up a fix for ACME ARI:
3fde5e568a

Fixes #12278

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-06-04 14:52:54 -07:00
Irbe Krumina
82576190a7 tailcfg,cmd/k8s-operator: moves tailscale.com/cap/kubernetes peer cap to tailcfg (#12235)
This is done in preparation for adding kubectl
session recording rules to this capability grant that will need to
be unmarshalled by control, so will also need to be
in a shared location.

Updates tailscale/corp#19821

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-06-04 18:31:37 +01:00
Andrea Gottardo
d636407f14 net/dns: don't set MatchDomains on Apple platforms when no upstream nameservers available (#12334)
This PR addresses a DNS issue on macOS as discussed this morning.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-06-04 09:41:13 -07:00
Adrian Dewhurst
cf9f507d47 ipn/ipnlocal: only build allowed suggested node list once
Rather than building a new suggested exit node set every time, compute
it once on first use. Currently, syspolicy ensures that values do not
change without a restart anyway.

Since the set is being constructed in a separate func now, the test code
that manipulates syspolicy can live there, and the TestSuggestExitNode
can now run in parallel with other tests because it does not have global
dependencies.

Updates tailscale/corp#19681

Change-Id: Ic4bb40ccc91b671f9e542bd5ba9c96f942081515
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-06-04 12:25:45 -04:00
signed-long
1dc3136a24 cmd/k8s-operator: Support image 'repo' or 'repository' keys in helm values file (#12285)
cmd/k8s-operator/deploy/chart: Support image 'repo' or 'repository' keys in helm values

Fixes #12100

Signed-off-by: Michael Long <michaelongdev@gmail.com>
2024-06-04 17:24:12 +01:00
Andrew Lytvynov
379e2bf189 ipn/ipnlocal: stop offline auto-updates on shutdown (#12342)
Clean up the updater goroutine on shutdown, in addition to doing that on
backend state change. This fixes a goroutine leak on shutdown in tests.

Updates #cleanup
2024-06-04 07:59:59 -07:00
Jordan Whited
ba0dd493c8 cmd/stunstamp: validate STUN tx ID in responses (#12339)
Extremely late arriving responses may leak across probing intervals.

Updates tailscale/corp#20344

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-06-04 07:26:10 -07:00
Andrew Lytvynov
bc4c8b65c7 ipn/ipnlocal: periodically run auto-updates when "offline" (#12118)
When the client is disconnected from control for any reason (typically
just turned off), we should still attempt to update if auto-updates are
enabled. This may help users who turn tailscale on infrequently for
accessing resources.

RELNOTE: Apply auto-updates even if the node is down or disconnected
from the coordination server.

Updates #12117

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-06-03 19:24:53 -07:00
Maisem Ali
2f2f588c80 cmd/natc: use ListenPacket
Now that tsnet supports it, use it.

Updates tailscale/corp#20503

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-06-03 15:36:32 -07:00
Brad Fitzpatrick
e84751217a gokrazy: add prototype Tailscale appliance, build tooling, docs
Updates #1866

Change-Id: I546316cb833bf2919e0d6f55cdc9951f375f165b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-03 15:01:19 -07:00
Maisem Ali
0b1a8586eb cmd/natc: initial implementation of a NAT based connector
This adds a new prototype `cmd/natc` which can be used
to expose a services/domains to the tailnet.

It requires the user to specify a set of IPv4 prefixes
from the CGNAT range. It advertises these as normal subnet
routes. It listens for DNS on the first IP of the first range
provided to it.

When it gets a DNS query it allocates an IP for that domain
from the v4 range. Subsequent connections to the assigned IP
are then tcp proxied to the domain.

It is marked as a WIP prototype and requires the use of the
`TAILSCALE_USE_WIP_CODE` env var.

Updates tailscale/corp#20503

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-06-03 14:37:38 -07:00
Maisem Ali
7b193de6b9 tsnet: return net.Listener from s.listen
A `*listener` implements net.Listener which breaks
a test in another repo.

Regressed in 42cfbf427c.

Updates #12182

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-06-03 14:33:44 -07:00
Adrian Dewhurst
3bf2bddbb5 ipn/ipnlocal: improve testability of random node selection
In order to test the sticky last suggestion code, a test was written for
LocalBackend.SuggestExitNode but it contains a random number generator
which makes writing comprehensive tests very difficult. This doesn't
change how the last suggestion works, but it adds some infrastructure to
make that easier in a later PR.

This adds func parameters for the two randomized parts: breaking ties
between DERP regions and breaking ties between nodes. This way tests can
validate the entire list of tied options, rather than expecting a
particular outcome given a particular random seed.

As a result of this, the global random number generator can be used
rather than seeding a local one each time.

In order to see the tied nodes for the location based (i.e. Mullvad)
case, pickWeighted needed to return a slice instead of a single
arbitrary option, so there is a small change in how that works.

Updates tailscale/corp#19681

Change-Id: I83c48a752abdec0f59c58ccfd8bfb3f3f17d0ea8
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-06-03 16:58:25 -04:00
Jordan Whited
d21c00205d cmd/stunstamp: implement service to measure DERP STUN RTT (#12241)
stunstamp timestamping includes userspace and SO_TIMESTAMPING kernel
timestamping where available. Measurements are written locally to a
sqlite DB, exposed over an HTTP API, and written to prometheus
via remote-write protocol.

Updates tailscale/corp#20344

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-06-03 13:42:06 -07:00
License Updater
1fad06429e licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-06-03 10:42:32 -07:00
Fran Bull
e06862b8d8 appc: log how often routeInfo is stored
So that we have some debugging info if users have trouble with storing
the routeInfo.

Updates #11008

Signed-off-by: Fran Bull <fran@tailscale.com>
2024-06-03 09:03:17 -07:00
Adrian Dewhurst
db6447ce63 ipn/ipnlocal: simplify suggest exit node tests
This mostly removes a lot of repetition by predefining some nodes and
other data structures, plus adds some helpers for creating Peer entries
in the netmap. Several existing test cases were reworked to ensure
better coverage of edge cases, and several new test cases were added to
handle some additional responsibility that is in (or will be shortly
moving in) suggestExitNode().

Updates tailscale/corp#19681

Change-Id: Ie14c2988d7fd482f7d6a877f78525f7788669b85
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-06-03 11:47:21 -04:00
Andrew Dunham
ced9a0d413 net/dns: fix typo in OSConfig logging (#12330)
Updates tailscale/corp#20530

Change-Id: I48834a0a5944ed35509c63bdd2830aa34e1bddeb

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2024-06-03 11:05:38 -04:00
70 changed files with 3930 additions and 1060 deletions

View File

@@ -15,6 +15,7 @@ import (
"slices"
"strings"
"sync"
"time"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
@@ -26,6 +27,46 @@ import (
"tailscale.com/util/slicesx"
)
// rateLogger responds to calls to update by adding a count for the current period and
// calling the callback if any previous period has finished since update was last called
type rateLogger struct {
interval time.Duration
start time.Time
periodStart time.Time
periodCount int64
now func() time.Time
callback func(int64, time.Time)
}
func (rl *rateLogger) currentIntervalStart(now time.Time) time.Time {
millisSince := now.Sub(rl.start).Milliseconds() % rl.interval.Milliseconds()
return now.Add(-(time.Duration(millisSince)) * time.Millisecond)
}
func (rl *rateLogger) update() {
now := rl.now()
periodEnd := rl.periodStart.Add(rl.interval)
if periodEnd.Before(now) {
if rl.periodCount != 0 {
rl.callback(rl.periodCount, rl.periodStart)
}
rl.periodCount = 0
rl.periodStart = rl.currentIntervalStart(now)
}
rl.periodCount++
}
func newRateLogger(now func() time.Time, interval time.Duration, callback func(int64, time.Time)) *rateLogger {
nowTime := now()
return &rateLogger{
callback: callback,
now: now,
interval: interval,
start: nowTime,
periodStart: nowTime,
}
}
// RouteAdvertiser is an interface that allows the AppConnector to advertise
// newly discovered routes that need to be served through the AppConnector.
type RouteAdvertiser interface {
@@ -81,6 +122,9 @@ type AppConnector struct {
// queue provides ordering for update operations
queue execqueue.ExecQueue
writeRateMinute *rateLogger
writeRateDay *rateLogger
}
// NewAppConnector creates a new AppConnector.
@@ -95,6 +139,12 @@ func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, routeInf
ac.wildcards = routeInfo.Wildcards
ac.controlRoutes = routeInfo.Control
}
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time) {
ac.logf("routeInfo write rate: %d in minute starting at %v", c, s)
})
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time) {
ac.logf("routeInfo write rate: %d in 24 hours starting at %v", c, s)
})
return ac
}
@@ -109,6 +159,8 @@ func (e *AppConnector) storeRoutesLocked() error {
if !e.ShouldStoreRoutes() {
return nil
}
e.writeRateMinute.update()
e.writeRateDay.update()
return e.storeRoutesFunc(&RouteInfo{
Control: e.controlRoutes,
Domains: e.domains,

View File

@@ -9,10 +9,12 @@ import (
"reflect"
"slices"
"testing"
"time"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc/appctest"
"tailscale.com/tstest"
"tailscale.com/util/mak"
"tailscale.com/util/must"
)
@@ -520,3 +522,50 @@ func TestRoutesWithout(t *testing.T) {
assert("a has fewer", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32")), []netip.Prefix{})
assert("a has more", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32"), prefixes("1.1.1.1/32", "1.1.1.3/32")), prefixes("1.1.1.2/32", "1.1.1.4/32"))
}
func TestRateLogger(t *testing.T) {
clock := tstest.Clock{}
wasCalled := false
rl := newRateLogger(func() time.Time { return clock.Now() }, 1*time.Second, func(count int64, _ time.Time) {
if count != 3 {
t.Fatalf("count for prev period: got %d, want 3", count)
}
wasCalled = true
})
for i := 0; i < 3; i++ {
clock.Advance(1 * time.Millisecond)
rl.update()
if wasCalled {
t.Fatalf("wasCalled: got true, want false")
}
}
clock.Advance(1 * time.Second)
rl.update()
if !wasCalled {
t.Fatalf("wasCalled: got false, want true")
}
wasCalled = false
rl = newRateLogger(func() time.Time { return clock.Now() }, 1*time.Hour, func(count int64, _ time.Time) {
if count != 3 {
t.Fatalf("count for prev period: got %d, want 3", count)
}
wasCalled = true
})
for i := 0; i < 3; i++ {
clock.Advance(1 * time.Minute)
rl.update()
if wasCalled {
t.Fatalf("wasCalled: got true, want false")
}
}
clock.Advance(1 * time.Hour)
rl.update()
if !wasCalled {
t.Fatalf("wasCalled: got false, want true")
}
}

View File

@@ -49,7 +49,7 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- $operatorTag:= printf ":%s" ( .Values.operatorConfig.image.tag | default .Chart.AppVersion )}}
image: {{ .Values.operatorConfig.image.repo }}{{- if .Values.operatorConfig.image.digest -}}{{ printf "@%s" .Values.operatorConfig.image.digest}}{{- else -}}{{ printf "%s" $operatorTag }}{{- end }}
image: {{ coalesce .Values.operatorConfig.image.repo .Values.operatorConfig.image.repository }}{{- if .Values.operatorConfig.image.digest -}}{{ printf "@%s" .Values.operatorConfig.image.digest}}{{- else -}}{{ printf "%s" $operatorTag }}{{- end }}
imagePullPolicy: {{ .Values.operatorConfig.image.pullPolicy }}
env:
- name: OPERATOR_INITIAL_TAGS
@@ -70,7 +70,7 @@ spec:
value: /oauth/client_secret
{{- $proxyTag := printf ":%s" ( .Values.proxyConfig.image.tag | default .Chart.AppVersion )}}
- name: PROXY_IMAGE
value: {{ .Values.proxyConfig.image.repo }}{{- if .Values.proxyConfig.image.digest -}}{{ printf "@%s" .Values.proxyConfig.image.digest}}{{- else -}}{{ printf "%s" $proxyTag }}{{- end }}
value: {{ coalesce .Values.proxyConfig.image.repo .Values.proxyConfig.image.repository }}{{- if .Values.proxyConfig.image.digest -}}{{ printf "@%s" .Values.proxyConfig.image.digest}}{{- else -}}{{ printf "%s" $proxyTag }}{{- end }}
- name: PROXY_TAGS
value: {{ .Values.proxyConfig.defaultTags }}
- name: APISERVER_PROXY

View File

@@ -23,7 +23,7 @@ operatorConfig:
- "tag:k8s-operator"
image:
repo: tailscale/k8s-operator
repository: tailscale/k8s-operator
# Digest will be prioritized over tag. If neither are set appVersion will be
# used.
tag: ""
@@ -57,7 +57,7 @@ operatorConfig:
# https://tailscale.com/kb/1236/kubernetes-operator#cluster-resource-customization-using-proxyclass-custom-resource
proxyConfig:
image:
repo: tailscale/tailscale
repository: tailscale/tailscale
# Digest will be prioritized over tag. If neither are set appVersion will be
# used.
tag: ""

View File

@@ -207,8 +207,11 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLo
}
const (
capabilityName = "tailscale.com/cap/kubernetes"
oldCapabilityName = "https://" + capabilityName
// oldCapabilityName is a legacy form of
// tailfcg.PeerCapabilityKubernetes capability. The only capability rule
// that is respected for this form is group impersonation - for
// backwards compatibility reasons.
oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes
)
type capRule struct {
@@ -229,7 +232,7 @@ type impersonateRule struct {
func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
log = log.With("remote", r.RemoteAddr)
who := whoIsKey.Value(r.Context())
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
if len(rules) == 0 && err == nil {
// Try the old capability name for backwards compatibility.
rules, err = tailcfg.UnmarshalCapJSON[capRule](who.CapMap, oldCapabilityName)

View File

@@ -49,7 +49,7 @@ func TestImpersonationHeaders(t *testing.T) {
name: "user-with-cap",
emailish: "foo@example.com",
capMap: tailcfg.PeerCapMap{
capabilityName: {
tailcfg.PeerCapabilityKubernetes: {
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`),
@@ -71,7 +71,7 @@ func TestImpersonationHeaders(t *testing.T) {
emailish: "tagged-device",
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
tailcfg.PeerCapabilityKubernetes: {
tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`),
},
},
@@ -85,7 +85,7 @@ func TestImpersonationHeaders(t *testing.T) {
emailish: "tagged-device",
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
tailcfg.PeerCapabilityKubernetes: {
tailcfg.RawMessage(`[]`),
},
},

457
cmd/natc/natc.go Normal file
View File

@@ -0,0 +1,457 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The natc command is a work-in-progress implementation of a NAT based
// connector for Tailscale. It is intended to be used to route traffic to a
// specific domain through a specific node.
package main
import (
"context"
"encoding/binary"
"errors"
"flag"
"fmt"
"log"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"os"
"strings"
"sync"
"time"
"github.com/gaissmai/bart"
"github.com/inetaf/tcpproxy"
"github.com/peterbourgon/ff/v3"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
)
func main() {
hostinfo.SetApp("natc")
if !envknob.UseWIPCode() {
log.Fatal("cmd/natc is a work in progress and has not been security reviewed;\nits use requires TAILSCALE_USE_WIP_CODE=1 be set in the environment for now.")
}
// Parse flags
fs := flag.NewFlagSet("natc", flag.ExitOnError)
var (
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
hostname = fs.String("hostname", "", "Hostname to register the service under")
siteID = fs.Uint("site-id", 1, "an integer site ID to use for the ULA prefix which allows for multiple proxies to act in a HA configuration")
v4PfxStr = fs.String("v4-pfx", "100.64.1.0/24", "comma-separated list of IPv4 prefixes to advertise")
verboseTSNet = fs.Bool("verbose-tsnet", false, "enable verbose logging in tsnet")
printULA = fs.Bool("print-ula", false, "print the ULA prefix and exit")
)
ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_NATC"))
if *printULA {
fmt.Println(ula(uint16(*siteID)))
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if *siteID == 0 {
log.Fatalf("site-id must be set")
} else if *siteID > 0xffff {
log.Fatalf("site-id must be in the range [0, 65535]")
}
var v4Prefixes []netip.Prefix
for _, s := range strings.Split(*v4PfxStr, ",") {
p := netip.MustParsePrefix(strings.TrimSpace(s))
if p.Masked() != p {
log.Fatalf("v4 prefix %v is not a masked prefix", p)
}
v4Prefixes = append(v4Prefixes, p)
}
if len(v4Prefixes) == 0 {
log.Fatalf("no v4 prefixes specified")
}
dnsAddr := v4Prefixes[0].Addr()
ts := &tsnet.Server{
Hostname: *hostname,
}
defer ts.Close()
if *verboseTSNet {
ts.Logf = log.Printf
}
// Start special-purpose listeners: dns, http promotion, debug server
if *debugPort != 0 {
mux := http.NewServeMux()
tsweb.Debugger(mux)
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
if err != nil {
log.Fatalf("failed listening on debug port: %v", err)
}
defer dln.Close()
go func() {
log.Fatalf("debug serve: %v", http.Serve(dln, mux))
}()
}
lc, err := ts.LocalClient()
if err != nil {
log.Fatalf("LocalClient() failed: %v", err)
}
if _, err := ts.Up(ctx); err != nil {
log.Fatalf("ts.Up: %v", err)
}
c := &connector{
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
v6ULA: ula(uint16(*siteID)),
}
c.run(ctx)
}
type connector struct {
// ts is the tsnet.Server used to host the connector.
ts *tsnet.Server
// lc is the LocalClient used to interact with the tsnet.Server hosting this
// connector.
lc *tailscale.LocalClient
// dnsAddr is the IPv4 address to listen on for DNS requests. It is used to
// prevent the app connector from assigning it to a domain.
dnsAddr netip.Addr
// v4Ranges is the list of IPv4 ranges to advertise and assign addresses from.
// These are masked prefixes.
v4Ranges []netip.Prefix
// v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
v6ULA netip.Prefix
perPeerMap syncs.Map[tailcfg.NodeID, *perPeerState]
}
// v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
// The 8th and 9th bytes are used to encode the site ID which allows for
// multiple proxies to act in a HA configuration.
// mnemonic: a99c = appc
var v6ULA = netip.MustParsePrefix("fd7a:115c:a1e0:a99c::/64")
func ula(siteID uint16) netip.Prefix {
as16 := v6ULA.Addr().As16()
as16[8] = byte(siteID >> 8)
as16[9] = byte(siteID)
return netip.PrefixFrom(netip.AddrFrom16(as16), 64+16)
}
// run runs the connector.
//
// The passed in context is only used for the initial setup. The connector runs
// forever.
func (c *connector) run(ctx context.Context) {
if _, err := c.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
Prefs: ipn.Prefs{
AdvertiseRoutes: append(c.v4Ranges, c.v6ULA),
},
}); err != nil {
log.Fatalf("failed to advertise routes: %v", err)
}
c.ts.RegisterFallbackTCPHandler(c.handleTCPFlow)
c.serveDNS()
}
func (c *connector) serveDNS() {
pc, err := c.ts.ListenPacket("udp", net.JoinHostPort(c.dnsAddr.String(), "53"))
if err != nil {
log.Fatalf("failed listening on port 53: %v", err)
}
defer pc.Close()
log.Printf("Listening for DNS on %s", pc.LocalAddr().String())
for {
buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
log.Printf("serveDNS.ReadFrom failed: %v", err)
continue
}
go c.handleDNS(pc, buf[:n], addr.(*net.UDPAddr))
}
}
// handleDNS handles a DNS request to the app connector.
// It generates a response based on the request and the node that sent it.
//
// Each node is assigned a unique pair of IP addresses for each domain it
// queries. This assignment is done lazily and is not persisted across restarts.
// A per-peer assignment allows the connector to reuse a limited number of IP
// addresses across multiple nodes and domains. It also allows for clear
// failover behavior when an app connector is restarted.
//
// This assignment later allows the connector to determine where to forward
// traffic based on the destination IP address.
func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDPAddr) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
who, err := c.lc.WhoIs(ctx, remoteAddr.String())
if err != nil {
log.Printf("HandleDNS: WhoIs failed: %v\n", err)
return
}
var msg dnsmessage.Message
err = msg.Unpack(buf)
if err != nil {
log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
return
}
resp, err := c.generateDNSResponse(&msg, who.Node.ID)
if err != nil {
log.Printf("HandleDNS: connector handling failed: %v\n", err)
return
}
if len(resp) == 0 {
return
}
// This connector handled the DNS request
_, err = pc.WriteTo(resp, remoteAddr)
if err != nil {
log.Printf("HandleDNS: write failed: %v\n", err)
}
}
// tsMBox is the mailbox used in SOA records.
// The convention is to replace the @ symbol with a dot.
// So in this case, the mailbox is support.tailscale.com. with the trailing dot
// to indicate that it is a fully qualified domain name.
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
// generateDNSResponse generates a DNS response for the given request. The from
// argument is the NodeID of the node that sent the request.
func (c *connector) generateDNSResponse(req *dnsmessage.Message, from tailcfg.NodeID) ([]byte, error) {
pm, _ := c.perPeerMap.LoadOrStore(from, &perPeerState{c: c})
b := dnsmessage.NewBuilder(nil,
dnsmessage.Header{
ID: req.Header.ID,
Response: true,
Authoritative: true,
})
b.EnableCompression()
if len(req.Questions) == 0 {
return b.Finish()
}
q := req.Questions[0]
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(q); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
var err error
switch q.Type {
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
var addrs []netip.Addr
addrs, err = pm.ipForDomain(q.Name.String())
if err != nil {
return nil, err
}
want6 := q.Type == dnsmessage.TypeAAAA
found := false
for _, ip := range addrs {
if want6 != ip.Is6() {
continue
}
found = true
if want6 {
err = b.AAAAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AAAAResource{AAAA: ip.As16()},
)
} else {
err = b.AResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AResource{A: ip.As4()},
)
}
break
}
if !found {
err = errors.New("no address found")
}
case dnsmessage.TypeSOA:
err = b.SOAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
)
case dnsmessage.TypeNS:
err = b.NSResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.NSResource{NS: tsMBox},
)
}
if err != nil {
return nil, err
}
return b.Finish()
}
// handleTCPFlow handles a TCP flow from the given source to the given
// destination. It uses the source address to determine the node that sent the
// request and the destination address to determine the domain that the request
// is for based on the IP address assigned to the destination in the DNS
// response.
func (c *connector) handleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
who, err := c.lc.WhoIs(ctx, src.Addr().String())
cancel()
if err != nil {
log.Printf("HandleTCPFlow: WhoIs failed: %v\n", err)
return nil, false
}
from := who.Node.ID
ps, ok := c.perPeerMap.Load(from)
if !ok {
log.Printf("handleTCPFlow: no perPeerState for %v", from)
return nil, false
}
domain, ok := ps.domainForIP(dst.Addr())
if !ok {
log.Printf("handleTCPFlow: no domain for IP %v\n", dst.Addr())
return nil, false
}
return func(conn net.Conn) {
proxyTCPConn(conn, domain)
}, true
}
func proxyTCPConn(c net.Conn, dest string) {
addrPortStr := c.LocalAddr().String()
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("tcpRoundRobinHandler.Handle: bogus addrPort %q", addrPortStr)
c.Close()
return
}
p := &tcpproxy.Proxy{
ListenFunc: func(net, laddr string) (net.Listener, error) {
return netutil.NewOneConnListener(c, nil), nil
},
}
p.AddRoute(addrPortStr, &tcpproxy.DialProxy{
Addr: fmt.Sprintf("%s:%s", dest, port),
})
p.Start()
}
// perPeerState holds the state for a single peer.
type perPeerState struct {
c *connector
mu sync.Mutex
domainToAddr map[string][]netip.Addr
addrToDomain *bart.Table[string]
}
// domainForIP returns the domain name assigned to the given IP address and
// whether it was found.
func (ps *perPeerState) domainForIP(ip netip.Addr) (_ string, ok bool) {
ps.mu.Lock()
defer ps.mu.Unlock()
return ps.addrToDomain.Get(ip)
}
// ipForDomain assigns a pair of unique IP addresses for the given domain and
// returns them. The first address is an IPv4 address and the second is an IPv6
// address. If the domain already has assigned addresses, it returns them.
func (ps *perPeerState) ipForDomain(domain string) ([]netip.Addr, error) {
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return nil, err
}
domain = fqdn.WithoutTrailingDot()
ps.mu.Lock()
defer ps.mu.Unlock()
if addrs, ok := ps.domainToAddr[domain]; ok {
return addrs, nil
}
addrs := ps.assignAddrsLocked(domain)
return addrs, nil
}
// isIPUsedLocked reports whether the given IP address is already assigned to a
// domain.
// ps.mu must be held.
func (ps *perPeerState) isIPUsedLocked(ip netip.Addr) bool {
_, ok := ps.addrToDomain.Get(ip)
return ok
}
// unusedIPv4Locked returns an unused IPv4 address from the available ranges.
func (ps *perPeerState) unusedIPv4Locked() netip.Addr {
// TODO: skip ranges that have been exhausted
for _, r := range ps.c.v4Ranges {
ip := randV4(r)
for r.Contains(ip) {
if !ps.isIPUsedLocked(ip) && ip != ps.c.dnsAddr {
return ip
}
ip = ip.Next()
}
}
return netip.Addr{}
}
// randV4 returns a random IPv4 address within the given prefix.
func randV4(maskedPfx netip.Prefix) netip.Addr {
bits := 32 - maskedPfx.Bits()
randBits := rand.Uint32N(1 << uint(bits))
ip4 := maskedPfx.Addr().As4()
pn := binary.BigEndian.Uint32(ip4[:])
binary.BigEndian.PutUint32(ip4[:], randBits|pn)
return netip.AddrFrom4(ip4)
}
// assignAddrsLocked assigns a pair of unique IP addresses for the given domain
// and returns them. The first address is an IPv4 address and the second is an
// IPv6 address. It does not check if the domain already has assigned addresses.
// ps.mu must be held.
func (ps *perPeerState) assignAddrsLocked(domain string) []netip.Addr {
if ps.addrToDomain == nil {
ps.addrToDomain = &bart.Table[string]{}
}
v4 := ps.unusedIPv4Locked()
as16 := ps.c.v6ULA.Addr().As16()
as4 := v4.As4()
copy(as16[12:], as4[:])
v6 := netip.AddrFrom16(as16)
addrs := []netip.Addr{v4, v6}
mak.Set(&ps.domainToAddr, domain, addrs)
for _, a := range addrs {
ps.addrToDomain.Insert(netip.PrefixFrom(a, a.BitLen()), domain)
}
return addrs
}

141
cmd/stunstamp/api.go Normal file
View File

@@ -0,0 +1,141 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"compress/gzip"
"encoding/json"
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"time"
sq "github.com/Masterminds/squirrel"
)
type api struct {
db *db
mux *http.ServeMux
}
func newAPI(db *db) *api {
a := &api{
db: db,
}
mux := http.NewServeMux()
mux.HandleFunc("/query", a.query)
a.mux = mux
return a
}
type apiResult struct {
At int `json:"at"` // time.Time.Unix()
RegionID int `json:"regionID"`
Hostname string `json:"hostname"`
Af int `json:"af"` // 4 or 6
Addr string `json:"addr"`
Source int `json:"source"` // timestampSourceUserspace (0) or timestampSourceKernel (1)
StableConn bool `json:"stableConn"`
RttNS *int `json:"rttNS"`
}
func getTimeBounds(vals url.Values) (from time.Time, to time.Time, err error) {
lastForm, ok := vals["last"]
if ok && len(lastForm) > 0 {
dur, err := time.ParseDuration(lastForm[0])
if err != nil {
return time.Time{}, time.Time{}, err
}
now := time.Now()
return now.Add(-dur), now, nil
}
fromForm, ok := vals["from"]
if ok && len(fromForm) > 0 {
fromUnixSec, err := strconv.Atoi(fromForm[0])
if err != nil {
return time.Time{}, time.Time{}, err
}
from = time.Unix(int64(fromUnixSec), 0)
toForm, ok := vals["to"]
if ok && len(toForm) > 0 {
toUnixSec, err := strconv.Atoi(toForm[0])
if err != nil {
return time.Time{}, time.Time{}, err
}
to = time.Unix(int64(toUnixSec), 0)
} else {
return time.Time{}, time.Time{}, errors.New("from specified without to")
}
return from, to, nil
}
// no time bounds specified, default to last 1h
now := time.Now()
return now.Add(-time.Hour), now, nil
}
func (a *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}
func (a *api) query(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
from, to, err := getTimeBounds(r.Form)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
sb := sq.Select("at_unix", "region_id", "hostname", "af", "address", "timestamp_source", "stable_conn", "rtt_ns").From("rtt")
sb = sb.Where(sq.And{
sq.GtOrEq{"at_unix": from.Unix()},
sq.LtOrEq{"at_unix": to.Unix()},
})
query, args, err := sb.ToSql()
if err != nil {
return
}
rows, err := a.db.Query(query, args...)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
results := make([]apiResult, 0)
for rows.Next() {
rtt := 0
result := apiResult{
RttNS: &rtt,
}
err = rows.Scan(&result.At, &result.RegionID, &result.Hostname, &result.Af, &result.Addr, &result.Source, &result.StableConn, &result.RttNS)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
results = append(results, result)
}
if rows.Err() != nil {
http.Error(w, rows.Err().Error(), 500)
return
}
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gz := gzip.NewWriter(w)
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
err = json.NewEncoder(gz).Encode(&results)
} else {
err = json.NewEncoder(w).Encode(&results)
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}

784
cmd/stunstamp/stunstamp.go Normal file
View File

@@ -0,0 +1,784 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The stunstamp binary measures STUN round-trip latency with DERPs.
package main
import (
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"math"
"math/rand"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"os/signal"
"slices"
"sync"
"syscall"
"time"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/prompb"
"tailscale.com/logtail/backoff"
"tailscale.com/net/stun"
"tailscale.com/tailcfg"
)
var (
flagDERPMap = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map")
flagOut = flag.String("out", "", "output sqlite filename")
flagInterval = flag.Duration("interval", time.Minute, "interval to probe at in time.ParseDuration() format")
flagAPI = flag.String("api", "", "listen addr for HTTP API")
flagIPv6 = flag.Bool("ipv6", false, "probe IPv6 addresses")
flagRetention = flag.Duration("retention", time.Hour*24*7, "sqlite retention period in time.ParseDuration() format")
flagRemoteWriteURL = flag.String("rw-url", "", "prometheus remote write URL")
flagInstance = flag.String("instance", "", "instance label value; defaults to hostname if unspecified")
)
const (
minInterval = time.Second
maxBufferDuration = time.Hour
)
func getDERPMap(ctx context.Context, url string) (*tailcfg.DERPMap, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dm := tailcfg.DERPMap{}
err = json.NewDecoder(resp.Body).Decode(&dm)
if err != nil {
return nil, nil
}
return &dm, nil
}
type timestampSource int
const (
timestampSourceUserspace timestampSource = iota
timestampSourceKernel
)
func (t timestampSource) String() string {
switch t {
case timestampSourceUserspace:
return "userspace"
case timestampSourceKernel:
return "kernel"
default:
return "unknown"
}
}
type result struct {
at time.Time
meta nodeMeta
timestampSource timestampSource
connStability connStability
rtt *time.Duration // nil signifies failure, e.g. timeout
}
func measureRTT(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error) {
uconn, ok := conn.(*net.UDPConn)
if !ok {
return 0, fmt.Errorf("unexpected conn type: %T", conn)
}
err = uconn.SetReadDeadline(time.Now().Add(time.Second * 2))
if err != nil {
return 0, fmt.Errorf("error setting read deadline: %w", err)
}
txID := stun.NewTxID()
req := stun.Request(txID)
txAt := time.Now()
_, err = uconn.WriteToUDP(req, dst)
if err != nil {
return 0, fmt.Errorf("error writing to udp socket: %w", err)
}
b := make([]byte, 1460)
for {
n, err := uconn.Read(b)
rxAt := time.Now()
if err != nil {
return 0, fmt.Errorf("error reading from udp socket: %w", err)
}
gotTxID, _, err := stun.ParseResponse(b[:n])
if err != nil || gotTxID != txID {
continue
}
return rxAt.Sub(txAt), nil
}
}
func isTemporaryOrTimeoutErr(err error) bool {
if errors.Is(err, os.ErrDeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
return err.Temporary()
}
return false
}
type nodeMeta struct {
regionID int
regionCode string
hostname string
addr netip.Addr
}
type measureFn func(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error)
func probe(meta nodeMeta, conn io.ReadWriteCloser, fn measureFn) (*time.Duration, error) {
ua := &net.UDPAddr{
IP: net.IP(meta.addr.AsSlice()),
Port: 3478,
}
time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // jitter across tx
rtt, err := fn(conn, ua)
if err != nil {
if isTemporaryOrTimeoutErr(err) {
log.Printf("temp error measuring RTT to %s(%s): %v", meta.hostname, meta.addr, err)
return nil, nil
}
}
return &rtt, nil
}
func nodeMetaFromDERPMap(dm *tailcfg.DERPMap, nodeMetaByAddr map[netip.Addr]nodeMeta, ipv6 bool) (stale []nodeMeta, err error) {
// Parse the new derp map before making any state changes in nodeMetaByAddr.
// If parse fails we just stick with the old state.
updated := make(map[netip.Addr]nodeMeta)
for regionID, region := range dm.Regions {
for _, node := range region.Nodes {
v4, err := netip.ParseAddr(node.IPv4)
if err != nil || !v4.Is4() {
return nil, fmt.Errorf("invalid ipv4 addr for node in derp map: %v", node.Name)
}
metas := make([]nodeMeta, 0, 2)
metas = append(metas, nodeMeta{
regionID: regionID,
regionCode: region.RegionCode,
hostname: node.HostName,
addr: v4,
})
if ipv6 {
v6, err := netip.ParseAddr(node.IPv6)
if err != nil || !v6.Is6() {
return nil, fmt.Errorf("invalid ipv6 addr for node in derp map: %v", node.Name)
}
metas = append(metas, metas[0])
metas[1].addr = v6
}
for _, meta := range metas {
updated[meta.addr] = meta
}
}
}
// Find nodeMeta that have changed
for addr, updatedMeta := range updated {
previousMeta, ok := nodeMetaByAddr[addr]
if ok {
if previousMeta == updatedMeta {
continue
}
stale = append(stale, previousMeta)
nodeMetaByAddr[addr] = updatedMeta
} else {
nodeMetaByAddr[addr] = updatedMeta
}
}
// Find nodeMeta that no longer exist
for addr, potentialStale := range nodeMetaByAddr {
_, ok := updated[addr]
if !ok {
stale = append(stale, potentialStale)
}
}
return stale, nil
}
func getStableConns(stableConns map[netip.Addr][2]io.ReadWriteCloser, addr netip.Addr) ([2]io.ReadWriteCloser, error) {
conns, ok := stableConns[addr]
if ok {
return conns, nil
}
if supportsKernelTS() {
kconn, err := getConnKernelTimestamp()
if err != nil {
return conns, err
}
conns[timestampSourceKernel] = kconn
}
uconn, err := net.ListenUDP("udp", &net.UDPAddr{})
if err != nil {
return conns, err
}
conns[timestampSourceUserspace] = uconn
stableConns[addr] = conns
return conns, nil
}
// probeNodes measures the round-trip time for STUN binding requests against the
// DERP nodes described by nodeMetaByAddr while using/updating stableConns for
// UDP sockets that should be recycled across runs. It returns the results or
// an error if one occurs.
func probeNodes(nodeMetaByAddr map[netip.Addr]nodeMeta, stableConns map[netip.Addr][2]io.ReadWriteCloser) ([]result, error) {
wg := sync.WaitGroup{}
results := make([]result, 0)
resultsCh := make(chan result)
errCh := make(chan error)
doneCh := make(chan struct{})
numProbes := 0
at := time.Now()
addrsToProbe := make(map[netip.Addr]bool)
doProbe := func(conn io.ReadWriteCloser, meta nodeMeta, source timestampSource) {
defer wg.Done()
r := result{}
if conn == nil {
var err error
if source == timestampSourceKernel {
conn, err = getConnKernelTimestamp()
} else {
conn, err = net.ListenUDP("udp", &net.UDPAddr{})
}
if err != nil {
select {
case <-doneCh:
return
case errCh <- err:
return
}
}
defer conn.Close()
} else {
r.connStability = stableConn
}
fn := measureRTT
if source == timestampSourceKernel {
fn = measureRTTKernel
}
rtt, err := probe(meta, conn, fn)
if err != nil {
select {
case <-doneCh:
return
case errCh <- err:
return
}
}
r.at = at
r.meta = meta
r.timestampSource = source
r.rtt = rtt
select {
case <-doneCh:
case resultsCh <- r:
}
}
for _, meta := range nodeMetaByAddr {
addrsToProbe[meta.addr] = true
stable, err := getStableConns(stableConns, meta.addr)
if err != nil {
close(doneCh)
wg.Wait()
return nil, err
}
wg.Add(2)
numProbes += 2
go doProbe(stable[timestampSourceUserspace], meta, timestampSourceUserspace)
go doProbe(nil, meta, timestampSourceUserspace)
if supportsKernelTS() {
wg.Add(2)
numProbes += 2
go doProbe(stable[timestampSourceKernel], meta, timestampSourceKernel)
go doProbe(nil, meta, timestampSourceKernel)
}
}
// cleanup conns we no longer need
for k, conns := range stableConns {
if !addrsToProbe[k] {
if conns[timestampSourceKernel] != nil {
conns[timestampSourceKernel].Close()
}
conns[timestampSourceUserspace].Close()
delete(stableConns, k)
}
}
for {
select {
case err := <-errCh:
close(doneCh)
wg.Wait()
return nil, err
case result := <-resultsCh:
results = append(results, result)
if len(results) == numProbes {
return results, nil
}
}
}
}
type connStability bool
const (
unstableConn connStability = false
stableConn connStability = true
)
func timeSeriesLabels(meta nodeMeta, instance string, source timestampSource, stability connStability) []prompb.Label {
addressFamily := "ipv4"
if meta.addr.Is6() {
addressFamily = "ipv6"
}
labels := make([]prompb.Label, 0)
labels = append(labels, prompb.Label{
Name: "job",
Value: "stunstamp-rw",
})
labels = append(labels, prompb.Label{
Name: "instance",
Value: instance,
})
labels = append(labels, prompb.Label{
Name: "region_id",
Value: fmt.Sprintf("%d", meta.regionID),
})
labels = append(labels, prompb.Label{
Name: "region_code",
Value: meta.regionCode,
})
labels = append(labels, prompb.Label{
Name: "address_family",
Value: addressFamily,
})
labels = append(labels, prompb.Label{
Name: "hostname",
Value: meta.hostname,
})
labels = append(labels, prompb.Label{
Name: "__name__",
Value: "stunstamp_derp_stun_rtt_ns",
})
labels = append(labels, prompb.Label{
Name: "timestamp_source",
Value: source.String(),
})
labels = append(labels, prompb.Label{
Name: "stable_conn",
Value: fmt.Sprintf("%v", stability),
})
slices.SortFunc(labels, func(a, b prompb.Label) int {
// prometheus remote-write spec requires lexicographically sorted label names
return cmp.Compare(a.Name, b.Name)
})
return labels
}
const (
// https://prometheus.io/docs/concepts/remote_write_spec/#stale-markers
staleNaN uint64 = 0x7ff0000000000002
)
func staleMarkersFromNodeMeta(stale []nodeMeta, instance string) []prompb.TimeSeries {
staleMarkers := make([]prompb.TimeSeries, 0)
now := time.Now()
for _, s := range stale {
samples := []prompb.Sample{
{
Timestamp: now.UnixMilli(),
Value: math.Float64frombits(staleNaN),
},
}
staleMarkers = append(staleMarkers, prompb.TimeSeries{
Labels: timeSeriesLabels(s, instance, timestampSourceUserspace, unstableConn),
Samples: samples,
})
staleMarkers = append(staleMarkers, prompb.TimeSeries{
Labels: timeSeriesLabels(s, instance, timestampSourceUserspace, stableConn),
Samples: samples,
})
if supportsKernelTS() {
staleMarkers = append(staleMarkers, prompb.TimeSeries{
Labels: timeSeriesLabels(s, instance, timestampSourceKernel, unstableConn),
Samples: samples,
})
staleMarkers = append(staleMarkers, prompb.TimeSeries{
Labels: timeSeriesLabels(s, instance, timestampSourceKernel, stableConn),
Samples: samples,
})
}
}
return staleMarkers
}
func resultToPromTimeSeries(r result, instance string) prompb.TimeSeries {
labels := timeSeriesLabels(r.meta, instance, r.timestampSource, r.connStability)
samples := make([]prompb.Sample, 1)
samples[0].Timestamp = r.at.UnixMilli()
if r.rtt != nil {
samples[0].Value = float64(*r.rtt)
} else {
samples[0].Value = math.NaN()
// TODO: timeout counter
}
ts := prompb.TimeSeries{
Labels: labels,
Samples: samples,
}
slices.SortFunc(ts.Labels, func(a, b prompb.Label) int {
// prometheus remote-write spec requires lexicographically sorted label names
return cmp.Compare(a.Name, b.Name)
})
return ts
}
type remoteWriteClient struct {
c *http.Client
url string
}
type recoverableErr struct {
error
}
func newRemoteWriteClient(url string) *remoteWriteClient {
return &remoteWriteClient{
c: &http.Client{
Timeout: time.Second * 30,
},
url: url,
}
}
func (r *remoteWriteClient) write(ctx context.Context, ts []prompb.TimeSeries) error {
wr := &prompb.WriteRequest{
Timeseries: ts,
}
b, err := wr.Marshal()
if err != nil {
return fmt.Errorf("unable to marshal write request: %w", err)
}
compressed := snappy.Encode(nil, b)
req, err := http.NewRequestWithContext(ctx, "POST", r.url, bytes.NewReader(compressed))
if err != nil {
return fmt.Errorf("unable to create write request: %w", err)
}
req.Header.Add("Content-Encoding", "snappy")
req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("User-Agent", "stunstamp")
req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
resp, err := r.c.Do(req)
if err != nil {
return recoverableErr{fmt.Errorf("error performing write request: %w", err)}
}
if resp.StatusCode/100 != 2 {
err = fmt.Errorf("remote server %s returned HTTP status %d", r.url, resp.StatusCode)
}
if resp.StatusCode/100 == 5 || resp.StatusCode == http.StatusTooManyRequests {
return recoverableErr{err}
}
return err
}
func remoteWriteTimeSeries(client *remoteWriteClient, tsCh chan []prompb.TimeSeries) {
bo := backoff.NewBackoff("remote-write", log.Printf, time.Second*30)
// writeErr may contribute to bo's backoff schedule across tsCh read ops,
// i.e. if an unrecoverable error occurs for client.write(ctx, A), that
// should be accounted against bo prior to attempting to
// client.write(ctx, B).
var writeErr error
for ts := range tsCh {
for {
bo.BackOff(context.Background(), writeErr)
reqCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
writeErr = client.write(reqCtx, ts)
cancel()
var re recoverableErr
recoverable := errors.As(writeErr, &re)
if writeErr != nil {
log.Printf("remote write error(recoverable=%v): %v", recoverable, writeErr)
}
if !recoverable {
// a nil err is not recoverable
break
}
}
}
}
func main() {
flag.Parse()
if len(*flagDERPMap) < 1 {
log.Fatal("derp-map flag is unset")
}
if len(*flagOut) < 1 {
log.Fatal("out flag is unset")
}
if *flagInterval < minInterval || *flagInterval > maxBufferDuration {
log.Fatalf("interval must be >= %s and <= %s", minInterval, maxBufferDuration)
}
if *flagRetention < *flagInterval {
log.Fatalf("retention must be >= interval")
}
if len(*flagRemoteWriteURL) < 1 {
log.Fatalf("rw-url flag is unset")
}
_, err := url.Parse(*flagRemoteWriteURL)
if err != nil {
log.Fatalf("invalid rw-url flag value: %v", err)
}
if len(*flagInstance) < 1 {
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("failed to get hostname: %v", err)
}
*flagInstance = hostname
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
dmCh := make(chan *tailcfg.DERPMap)
go func() {
bo := backoff.NewBackoff("derp-map", log.Printf, time.Second*30)
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
dm, err := getDERPMap(ctx, *flagDERPMap)
cancel()
bo.BackOff(context.Background(), err)
if err != nil {
continue
}
dmCh <- dm
return
}
}()
nodeMetaByAddr := make(map[netip.Addr]nodeMeta)
select {
case <-sigCh:
return
case dm := <-dmCh:
_, err := nodeMetaFromDERPMap(dm, nodeMetaByAddr, *flagIPv6)
if err != nil {
log.Fatalf("error parsing derp map on startup: %v", err)
}
}
db, err := newDB(*flagOut)
if err != nil {
log.Fatalf("error opening output file for writing: %v", err)
}
defer db.Close()
_, err = db.Exec("PRAGMA journal_mode=WAL")
if err != nil {
log.Fatalf("error enabling WAL mode: %v", err)
}
// No indices or primary key. Keep it simple for now. Reads will be full
// scans. We can AUTOINCREMENT rowid in the future and hold an in-memory
// index to at_unix if needed as reads are almost always going to be
// time-bound (e.g. WHERE at_unix >= ?). At the time of authorship we have
// ~300 data points per-interval w/o ipv6 w/kernel timestamping resulting
// in ~2.6m rows in 24h w/a 10s probe interval.
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT, address TEXT, timestamp_source INT, stable_conn INT, rtt_ns INT)
`)
if err != nil {
log.Fatalf("error initializing db: %v", err)
}
wg := sync.WaitGroup{}
httpErrCh := make(chan error, 1)
var httpServer *http.Server
if len(*flagAPI) > 0 {
api := newAPI(db)
httpServer = &http.Server{
Addr: *flagAPI,
Handler: api,
ReadTimeout: time.Second * 60,
WriteTimeout: time.Second * 60,
}
wg.Add(1)
go func() {
err := httpServer.ListenAndServe()
httpErrCh <- err
wg.Done()
}()
}
tsCh := make(chan []prompb.TimeSeries, maxBufferDuration / *flagInterval)
remoteWriteDoneCh := make(chan struct{})
rwc := newRemoteWriteClient(*flagRemoteWriteURL)
go func() {
remoteWriteTimeSeries(rwc, tsCh)
close(remoteWriteDoneCh)
}()
shutdown := func() {
if httpServer != nil {
httpServer.Close()
}
close(tsCh)
select {
case <-time.After(time.Second * 10): // give goroutine some time to flush
case <-remoteWriteDoneCh:
}
// send stale markers on shutdown
staleMeta := make([]nodeMeta, 0, len(nodeMetaByAddr))
for _, v := range nodeMetaByAddr {
staleMeta = append(staleMeta, v)
}
staleMarkers := staleMarkersFromNodeMeta(staleMeta, *flagInstance)
if len(staleMarkers) > 0 {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
rwc.write(ctx, staleMarkers)
cancel()
}
wg.Wait()
return
}
log.Println("stunstamp started")
// Re-using sockets means we get the same 5-tuple across runs. This results
// in a higher probability of the packets traversing the same underlay path.
// Comparison of stable and unstable 5-tuple results can shed light on
// differences between paths where hashing (multipathing/load balancing)
// comes into play.
stableConns := make(map[netip.Addr][2]io.ReadWriteCloser)
derpMapTicker := time.NewTicker(time.Minute * 5)
defer derpMapTicker.Stop()
probeTicker := time.NewTicker(*flagInterval)
defer probeTicker.Stop()
cleanupTicker := time.NewTicker(time.Hour)
defer cleanupTicker.Stop()
for {
select {
case <-cleanupTicker.C:
older := time.Now().Add(-*flagRetention)
log.Printf("cleaning up measurements older than %v", older)
_, err := db.Exec("DELETE FROM rtt WHERE at_unix < ?", older.Unix())
if err != nil {
log.Printf("error cleaning up old data: %v", err)
shutdown()
return
}
case <-probeTicker.C:
results, err := probeNodes(nodeMetaByAddr, stableConns)
if err != nil {
log.Printf("unrecoverable error while probing: %v", err)
shutdown()
return
}
ts := make([]prompb.TimeSeries, 0, len(results))
for _, r := range results {
ts = append(ts, resultToPromTimeSeries(r, *flagInstance))
}
select {
case tsCh <- ts:
default:
select {
case <-tsCh:
log.Println("prometheus remote-write buffer full, dropped measurements")
default:
tsCh <- ts
}
}
tx, err := db.Begin()
if err != nil {
log.Printf("error beginning sqlite tx: %v", err)
shutdown()
return
}
for _, result := range results {
af := 4
if result.meta.addr.Is6() {
af = 6
}
_, err = tx.Exec("INSERT INTO rtt(at_unix, region_id, hostname, af, address, timestamp_source, stable_conn, rtt_ns) VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
result.at.Unix(), result.meta.regionID, result.meta.hostname, af, result.meta.addr.String(), result.timestampSource, result.connStability, result.rtt)
if err != nil {
tx.Rollback()
log.Printf("error adding result to tx: %v", err)
shutdown()
return
}
}
err = tx.Commit()
if err != nil {
log.Printf("error committing tx: %v", err)
shutdown()
return
}
case dm := <-dmCh:
staleMeta, err := nodeMetaFromDERPMap(dm, nodeMetaByAddr, *flagIPv6)
if err != nil {
log.Printf("error parsing DERP map, continuing with stale map: %v", err)
continue
}
staleMarkers := staleMarkersFromNodeMeta(staleMeta, *flagInstance)
if len(staleMarkers) < 1 {
continue
}
select {
case tsCh <- staleMarkers:
default:
select {
case <-tsCh:
log.Println("prometheus remote-write buffer full, dropped measurements")
default:
tsCh <- staleMarkers
}
}
case <-derpMapTicker.C:
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
updatedDM, err := getDERPMap(ctx, *flagDERPMap)
if err != nil {
dmCh <- updatedDM
}
}()
case err := <-httpErrCh:
log.Printf("http server error: %v", err)
shutdown()
return
case <-sigCh:
shutdown()
return
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(windows && 386)
package main
import (
"database/sql"
_ "modernc.org/sqlite"
)
type db struct {
*sql.DB
}
func newDB(path string) (*db, error) {
d, err := sql.Open("sqlite", *flagOut)
if err != nil {
return nil, err
}
return &db{
DB: d,
}, nil
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"database/sql"
"errors"
)
type db struct {
*sql.DB
}
func newDB(path string) (*db, error) {
return nil, errors.New("unsupported platform")
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
package main
import (
"errors"
"io"
"net"
"time"
)
func getConnKernelTimestamp() (io.ReadWriteCloser, error) {
return nil, errors.New("unimplemented")
}
func measureRTTKernel(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error) {
return 0, errors.New("unimplemented")
}
func supportsKernelTS() bool {
return false
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
"github.com/mdlayher/socket"
"golang.org/x/sys/unix"
"tailscale.com/net/stun"
)
const (
flags = unix.SOF_TIMESTAMPING_TX_SOFTWARE | // tx timestamp generation in device driver
unix.SOF_TIMESTAMPING_RX_SOFTWARE | // rx timestamp generation in the kernel
unix.SOF_TIMESTAMPING_SOFTWARE // report software timestamps
)
func getConnKernelTimestamp() (io.ReadWriteCloser, error) {
sconn, err := socket.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP, "udp", nil)
if err != nil {
return nil, err
}
sa := unix.SockaddrInet6{}
err = sconn.Bind(&sa)
if err != nil {
return nil, err
}
err = sconn.SetsockoptInt(unix.SOL_SOCKET, unix.SO_TIMESTAMPING_NEW, flags)
if err != nil {
return nil, err
}
return sconn, nil
}
func parseTimestampFromCmsgs(oob []byte) (time.Time, error) {
msgs, err := unix.ParseSocketControlMessage(oob)
if err != nil {
return time.Time{}, fmt.Errorf("error parsing oob as cmsgs: %w", err)
}
for _, msg := range msgs {
if msg.Header.Level == unix.SOL_SOCKET && msg.Header.Type == unix.SO_TIMESTAMPING_NEW && len(msg.Data) >= 16 {
sec := int64(binary.NativeEndian.Uint64(msg.Data[:8]))
ns := int64(binary.NativeEndian.Uint64(msg.Data[8:16]))
return time.Unix(sec, ns), nil
}
}
return time.Time{}, errors.New("failed to parse timestamp from cmsgs")
}
func measureRTTKernel(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error) {
sconn, ok := conn.(*socket.Conn)
if !ok {
return 0, fmt.Errorf("conn of unexpected type: %T", conn)
}
var to unix.Sockaddr
to4 := dst.IP.To4()
if to4 != nil {
to = &unix.SockaddrInet4{
Port: 3478,
}
copy(to.(*unix.SockaddrInet4).Addr[:], to4)
} else {
to = &unix.SockaddrInet6{
Port: 3478,
}
copy(to.(*unix.SockaddrInet6).Addr[:], dst.IP)
}
txID := stun.NewTxID()
req := stun.Request(txID)
err = sconn.Sendto(context.Background(), req, 0, to)
if err != nil {
return 0, fmt.Errorf("sendto error: %v", err) // don't wrap
}
txCtx, txCancel := context.WithTimeout(context.Background(), time.Second*2)
defer txCancel()
buf := make([]byte, 1024)
oob := make([]byte, 1024)
var txAt time.Time
for {
n, oobn, _, _, err := sconn.Recvmsg(txCtx, buf, oob, unix.MSG_ERRQUEUE)
if err != nil {
return 0, fmt.Errorf("recvmsg (MSG_ERRQUEUE) error: %v", err) // don't wrap
}
buf = buf[:n]
if n < len(req) || !bytes.Equal(req, buf[len(buf)-len(req):]) {
// Spin until we find the message we sent. We get the full packet
// looped including eth header so match against the tail.
continue
}
txAt, err = parseTimestampFromCmsgs(oob[:oobn])
if err != nil {
return 0, fmt.Errorf("failed to get tx timestamp: %v", err) // don't wrap
}
break
}
rxCtx, rxCancel := context.WithTimeout(context.Background(), time.Second*2)
defer rxCancel()
for {
n, oobn, _, _, err := sconn.Recvmsg(rxCtx, buf, oob, 0)
if err != nil {
return 0, fmt.Errorf("recvmsg error: %w", err) // wrap for timeout-related error unwrapping
}
gotTxID, _, err := stun.ParseResponse(buf[:n])
if err != nil || gotTxID != txID {
// Spin until we find the txID we sent. We may end up reading
// extremely late arriving responses from previous intervals. As
// such, we can't be certain if we're parsing the "current"
// response, so spin for parse errors too.
continue
}
rxAt, err := parseTimestampFromCmsgs(oob[:oobn])
if err != nil {
return 0, fmt.Errorf("failed to get rx timestamp: %v", err) // don't wrap
}
return rxAt.Sub(txAt), nil
}
}
func supportsKernelTS() bool {
return true
}

30
go.mod
View File

@@ -5,6 +5,7 @@ go 1.22.0
require (
filippo.io/mkcert v1.4.4
fybrik.io/crdoc v0.6.3
github.com/Masterminds/squirrel v1.5.4
github.com/akutz/memconn v0.1.0
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
github.com/andybalholm/brotli v1.1.0
@@ -34,6 +35,7 @@ require (
github.com/go-ole/go-ole v1.3.0
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/snappy v0.0.4
github.com/golangci/golangci-lint v1.52.2
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.18.0
@@ -64,13 +66,14 @@ require (
github.com/prometheus-community/pro-bing v0.4.0
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/common v0.46.0
github.com/prometheus/prometheus v0.49.2-0.20240125131847-c3b8ef1694ff
github.com/safchain/ethtool v0.3.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/studio-b12/gowebdav v0.9.0
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734
@@ -91,14 +94,14 @@ require (
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/mod v0.14.0
golang.org/x/mod v0.16.0
golang.org/x/net v0.23.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.18.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.18.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.17.0
golang.org/x/tools v0.19.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
gopkg.in/square/go-jose.v2 v2.6.0
@@ -108,6 +111,7 @@ require (
k8s.io/apimachinery v0.29.1
k8s.io/apiserver v0.29.1
k8s.io/client-go v0.29.1
modernc.org/sqlite v1.29.10
nhooyr.io/websocket v1.8.10
sigs.k8s.io/controller-runtime v0.16.2
sigs.k8s.io/controller-tools v0.13.0
@@ -121,9 +125,21 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
github.com/dave/brenda v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
require (
@@ -179,7 +195,7 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.10.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/docker/cli v25.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
@@ -276,7 +292,7 @@ require (
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mdlayher/socket v0.5.0
github.com/mgechev/revive v1.3.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -299,7 +315,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polyfloyd/go-errorlint v1.4.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect

74
go.sum
View File

@@ -75,6 +75,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
@@ -240,8 +242,9 @@ github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs=
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
@@ -260,6 +263,8 @@ github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqY
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU=
@@ -400,6 +405,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
@@ -463,8 +470,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A=
github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
@@ -511,6 +518,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
@@ -610,6 +619,10 @@ github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoa
github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes=
github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ=
github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA=
github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0=
github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo=
@@ -682,6 +695,8 @@ github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAVRfPlSo=
@@ -729,8 +744,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polyfloyd/go-errorlint v1.4.1 h1:r8ru5FhXSn34YU1GJDOuoJv2LdsQkPmK325EOpPMJlM=
github.com/polyfloyd/go-errorlint v1.4.1/go.mod h1:k6fU/+fQe38ednoZS51T7gSIGQW1y94d6TkSr35OzH8=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
@@ -761,6 +777,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prometheus v0.49.2-0.20240125131847-c3b8ef1694ff h1:X1Tly81aZ22DA1fxBdfvR3iw8+yFoUBUHMEd+AX/ZXI=
github.com/prometheus/prometheus v0.49.2-0.20240125131847-c3b8ef1694ff/go.mod h1:FvE8dtQ1Ww63IlyKBn1V4s+zMwF9kHkVNkQBR1pM4CU=
github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc=
github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw=
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
@@ -769,6 +787,8 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -874,8 +894,8 @@ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
@@ -964,8 +984,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
@@ -1038,8 +1058,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1179,8 +1199,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1285,8 +1305,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1453,6 +1473,32 @@ k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 h1:m6dl1pkxz3HuE2mP9MUYPC
k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=

2
gokrazy/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
tsapp.img
go.work

8
gokrazy/Makefile Normal file
View File

@@ -0,0 +1,8 @@
help:
echo "See Makefile"
image:
go run build.go --build
qemu: image
qemu-system-x86_64 -m 1G -drive file=tsapp.img,format=raw -boot d -netdev user,id=user.0 -device virtio-net-pci,netdev=user.0 -serial mon:stdio -audio none

76
gokrazy/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Tailscale Appliance Gokrazy Image
This is (as of 2024-06-02) a **WORK IN PROGRESS** (pre-alpha) experiment to
package Tailscale as a [Gokrazy](https://gokrazy.org/) appliance image
for use on both VMs (AWS, GCP, Azure, Proxmox, ...) and Rasperry Pis.
See https://github.com/tailscale/tailscale/issues/1866
## Overview
It makes a ~70MB image (about the same size as
`tailscale-setup-full-1.66.4.exe` and smaller than the combined
Tailscale Android APK) that combines the Linux kernel and Tailscale
and that's it. Nothing written in C. (except optional busybox for
debugging) So no operating system to maintain. Gokrazy has three
partitions: two read-only ones (one active at a time, the other for
updates for the next boot) and one optional stateful, writable
partition that survives upgrades (`/perm/`)
Initial bootstrap configuration of this appliance will be over either
serial or configuration files (auth keys, subnet routes, etc) baked into
the image (for Raspberry Pis) or in cloud-init/user-data (for AWS, etc).
As of 2024-06-02, AWS user-data config files work.
## Quick start
Install dependencies:
```
$ brew install qemu e2fsprogs
```
Build + launch:
```
$ make qemu
```
That puts serial on stdio. To exit the serial console and escape to
the qemu monitor, type `Ctrl-a c`. Then type `quit` in the monitor to
quit.
## Building
`make image` to build just the image (`tsapp.img`), without uploading it.
## UTM
You can also use UTM, but the qemu path above is easier.
For UTM, see the [UTM instructions](UTM.md).
## AWS
### Build an AMI
`go run build.go --bucket=your-S3-temp-bucket` to build an AMI. Make
sure your "aws" command is in your path and has access.
### Creating an instance
When creating an instance, you need a Nitro machine type to get a
virtual serial console. Notably, that means the `t2.*` instance types
that AWS pushes as a free option are not new enough. Use `t3.*` at least.
As of 2024-06-02 this builder tool only supports x86_64 (arm64 should
be trivial and will come soon), so don't use a Graviton machine type.
To connect to the serial console, you can either use the web console, or
use the CLI like:
```
$ aws ec2-instance-connect send-serial-console-ssh-public-key --instance-id i-0b4a0eabc43629f13 --serial-port 0 --ssh-public-key file:///your/home/.ssh/id_ed25519.pub --region us-west-2
{
"RequestId": "a93b0ea3-9ff9-45d5-b8ed-b1e70ccc0410",
"Success": true
}
$ ssh i-0b4a0eabc43629f13.port0@serial-console.ec2-instance-connect.us-west-2.aws
```

50
gokrazy/UTM.md Normal file
View File

@@ -0,0 +1,50 @@
# tsapp on UTM
qemu from homebrew is recommended for tsapp development.
See the [main README](README.md) for details.
If you don't want to use qemu, this documents a way to use UTM on
macOS for tsapp development. It's not as quick of an edit-run-test
iteration cycle, but this is how:
* Create new VM, choose "Emulate" (for now) and not "Virtualize"
* Pick "Linux" as the operating system
* For "Boot ISO Image", select the built `tsapp.img`
* Architecture: `x86_64` (for now; arm64 later)
* System: `Standard PC (...) (q35)`
* Memory: 1024 MB is fine for testing
* CPUs: Default
* Storage size: 3GB
* Shared Directory: none. Continue.
* Summary: check "Open VM Settings"
* Network: Emulated Network Card: `virtio-net-pci`
* Display: Emulated Display Card: `virtio-vga` (not that there's much to see)
* Drives: delete all disks
* Drives: New... Interface `VirtIO`, Import ... find `tsapp.img` again. Save.
* Devices: New... Serial. Mode: Psuedo-TTY Device, Target: Automatic Serial Device.
Once created & the `img` is imported once, UTM converts it to qcow2 format
under `$HOME/Library/Containers/com.utmapp.UTM/Data/Documents/Tsapp.utm/Data/tsapp.qcow2`.
To update it, stop the VM, then:
```
qemu-img convert -f raw -O qcow2 tsapp.img tsapp.qcow2 && \
mv tsapp.qcow2 $HOME/Library/Containers/com.utmapp.UTM/Data/Documents/Tsapp.utm/Data/tsapp.qcow2
```
To attach to its serial:
```
% /Applications/UTM.app/Contents/MacOS/utmctl list
UUID Status Name
C0DE927B-F426-4ABA-A6E7-E30AA429371F started Tsapp
% % /Applications/UTM.app/Contents/MacOS/utmctl attach C0DE927B-F426-4ABA-A6E7-E30AA429371F
WARNING: attach command is not implemented yet!
PTTY: /dev/ttys017
% screen /dev/ttys017
```
(Then `Ctrl-a K` to kill screen session)

274
gokrazy/build.go Normal file
View File

@@ -0,0 +1,274 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// This program builds the Tailscale Appliance Gokrazy image.
//
// As of 2024-06-02 this is a exploratory work in progress and is
// not intended for serious use.
//
// Tracking issue is https://github.com/tailscale/tailscale/issues/1866
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"time"
)
var (
bucket = flag.String("bucket", "tskrazy-import", "S3 bucket to upload disk image to while making AMI")
build = flag.Bool("build", false, "if true, just build locally and stop, without uploading")
)
func findMkfsExt4() (string, error) {
tries := []string{
"/opt/homebrew/opt/e2fsprogs/sbin/mkfs.ext4",
"/sbin/mkfs.ext4",
}
for _, p := range tries {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
p, err := exec.LookPath("mkfs.ext4")
if err == nil {
return p, nil
}
if runtime.GOOS == "darwin" {
return "", errors.New("no mkfs.ext4 found; run `brew install e2fsprogs`")
}
return "", errors.New("No mkfs.ext4 found on system")
}
func main() {
flag.Parse()
if err := buildImage(); err != nil {
log.Fatalf("build image: %v", err)
}
if *build {
log.Printf("built. stopping.")
return
}
if err := copyToS3(); err != nil {
log.Fatalf("copy to S3: %v", err)
}
importTask, err := startImportSnapshot()
if err != nil {
log.Fatalf("start import snapshot: %v", err)
}
snapID, err := waitForImportSnapshot(importTask)
if err != nil {
log.Fatalf("waitForImportSnapshot(%v): %v", importTask, err)
}
log.Printf("snap ID: %v", snapID)
ami, err := makeAMI(fmt.Sprintf("tsapp-%d", time.Now().Unix()), snapID)
if err != nil {
log.Fatalf("makeAMI: %v", err)
}
log.Printf("made AMI: %v", ami)
}
func buildImage() error {
mkfs, err := findMkfsExt4()
if err != nil {
return err
}
dir, err := os.Getwd()
if err != nil {
return err
}
if fi, err := os.Stat(filepath.Join(dir, "tsapp")); err != nil || !fi.IsDir() {
return fmt.Errorf("in wrong directorg %v; no tsapp subdirectory found", dir)
}
// Build the tsapp.img
var buf bytes.Buffer
cmd := exec.Command("go", "run",
"-exec=env GOOS=linux GOARCH=amd64 ",
"github.com/gokrazy/tools/cmd/gok",
"--parent_dir="+dir,
"--instance=tsapp",
"overwrite",
"--full", "tsapp.img",
"--target_storage_bytes=1258299392")
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
// gok overwrite emits a line of text saying how to run mkfs.ext4
// to create the ext4 /perm filesystem. Parse that and run it.
// The regexp is tight to avoid matching if the command changes,
// to force us to check it's still correct/safe. But it shouldn't
// change on its own because we pin the gok version in our go.mod.
//
// TODO(bradfitz): emit this in a machine-readable way from gok.
rx := regexp.MustCompile(`(?m)/mkfs.ext4 (-F) (-E) (offset=\d+) (\S+) (\d+)\s*?$`)
m := rx.FindStringSubmatch(buf.String())
if m == nil {
return fmt.Errorf("found no ext4 instructions in output")
}
log.Printf("Running %s %q ...", mkfs, m[1:])
out, err := exec.Command(mkfs, m[1:]...).CombinedOutput()
if err != nil {
return fmt.Errorf("error running %v: %v, %s", mkfs, err, out)
}
log.Printf("Success.")
return nil
}
func copyToS3() error {
cmd := exec.Command("aws", "s3", "cp", "tsapp.img", "s3://"+*bucket+"/")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func startImportSnapshot() (importTaskID string, err error) {
out, err := exec.Command("aws", "ec2", "import-snapshot", "--disk-container", "Url=s3://"+*bucket+"/tsappp.img").CombinedOutput()
if err != nil {
return "", fmt.Errorf("import snapshot: %v: %s", err, out)
}
var resp struct {
ImportTaskID string `json:"ImportTaskId"`
}
/*
{
"ImportTaskId": "import-snap-0d2d72622b4359567",
"SnapshotTaskDetail": {
"DiskImageSize": 0.0,
"Progress": "0",
"Status": "active",
"StatusMessage": "pending",
"Url": "s3://tskrazy-import/tskrazy.img"
},
"Tags": []
}
*/
if err := json.Unmarshal(out, &resp); err != nil {
return "", fmt.Errorf("unmarshal response: %v: %s", err, out)
}
return resp.ImportTaskID, nil
}
/*
% aws ec2 describe-import-snapshot-tasks --import-task-ids import-snap-0d2d72622b4359567
{
"ImportSnapshotTasks": [
{
"ImportTaskId": "import-snap-0d2d72622b4359567",
"SnapshotTaskDetail": {
"DiskImageSize": 1258299392.0,
"Format": "RAW",
"SnapshotId": "snap-053efd3539d787927",
"Status": "completed",
"Url": "s3://tskrazy-import/tskrazy.img",
"UserBucket": {
"S3Bucket": "tskrazy-import",
"S3Key": "tskrazy.img"
}
},
"Tags": []
}
]
}
*/
func waitForImportSnapshot(importTaskID string) (snapID string, err error) {
for {
out, err := exec.Command("aws", "ec2", "describe-import-snapshot-tasks", "--import-task-ids", importTaskID).CombinedOutput()
if err != nil {
return "", fmt.Errorf("describe import snapshot tasks: %v: %s", err, out)
}
var resp struct {
ImportSnapshotTasks []struct {
SnapshotTaskDetail struct {
SnapshotID string `json:"SnapshotId"`
Status string `json:"Status"`
} `json:"SnapshotTaskDetail"`
} `json:"ImportSnapshotTasks"`
}
if err := json.Unmarshal(out, &resp); err != nil {
return "", fmt.Errorf("unmarshal response: %v: %s", err, out)
}
if len(resp.ImportSnapshotTasks) > 0 {
first := &resp.ImportSnapshotTasks[0]
if first.SnapshotTaskDetail.Status == "completed" {
return first.SnapshotTaskDetail.SnapshotID, nil
}
}
log.Printf("Still waiting; got: %s", out)
time.Sleep(5 * time.Second)
// TODO(bradfitz): percentage bar?
// Looks like:
/* 2024/05/14 13:03:21 Still waiting; got: {
"ImportSnapshotTasks": [
{
"ImportTaskId": "import-snap-0232251d0fbcb33fd",
"SnapshotTaskDetail": {
"DiskImageSize": 1258299392.0,
"Format": "RAW",
"Progress": "32",
"Status": "active",
"StatusMessage": "validated",
"Url": "s3://tskrazy-import/tskrazy.img",
"UserBucket": {
"S3Bucket": "tskrazy-import",
"S3Key": "tskrazy.img"
}
},
"Tags": []
}
]
}*/
}
}
func makeAMI(name, ebsSnapID string) (ami string, err error) {
out, err := exec.Command("aws", "ec2", "register-image",
"--name", name,
"--architecture", "x86_64",
"--root-device-name", "/dev/sda",
"--ena-support",
"--imds-support", "v2.0",
"--boot-mode", "uefi-preferred",
"--block-device-mappings", "DeviceName=/dev/sda,Ebs={SnapshotId="+ebsSnapID+"}").CombinedOutput()
if err != nil {
return "", fmt.Errorf("register image: %v: %s", err, out)
}
/*
On success:
{
"ImageId": "ami-052e1538166886ad2"
}
*/
var resp struct {
ImageID string `json:"ImageId"`
}
if err := json.Unmarshal(out, &resp); err != nil {
return "", fmt.Errorf("unmarshal response: %v: %s", err, out)
}
if resp.ImageID == "" {
return "", fmt.Errorf("empty image ID in response: %s", out)
}
return resp.ImageID, nil
}

25
gokrazy/go.mod Normal file
View File

@@ -0,0 +1,25 @@
module tailscale.com/gokrazy
go 1.22
require github.com/gokrazy/tools v0.0.0-20240510170341-34b02e215bc2
require (
github.com/breml/rootcerts v0.2.10 // indirect
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 // indirect
github.com/gokrazy/internal v0.0.0-20240510165500-68dd68393b7a // indirect
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)
replace github.com/gokrazy/gokrazy => github.com/tailscale/gokrazy v0.0.0-20240602215456-7b9b6bbf726a
replace github.com/gokrazy/tools => github.com/tailscale/gokrazy-tools v0.0.0-20240602210012-933640538dcf
replace github.com/gokrazy/internal => github.com/tailscale/gokrazy-internal v0.0.0-20240602195241-04c5eda9f6cd

33
gokrazy/go.sum Normal file
View File

@@ -0,0 +1,33 @@
github.com/breml/rootcerts v0.2.10 h1:UGVZ193UTSUASpGtg6pbDwzOd7XQP+at0Ssg1/2E4h8=
github.com/breml/rootcerts v0.2.10/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tailscale/gokrazy-internal v0.0.0-20240602195241-04c5eda9f6cd h1:ZJplHHhYSzxYmrXuDPCNChGRZbLkPqRkYqRBM7KNyng=
github.com/tailscale/gokrazy-internal v0.0.0-20240602195241-04c5eda9f6cd/go.mod h1:t3ZirVhcs9bH+fPAJuGh51rzT7sVCZ9yfXvszf0ZjF0=
github.com/tailscale/gokrazy-tools v0.0.0-20240602210012-933640538dcf h1:lmAGqLbIVoMK1TYWqJvxKFsu+Tb1OecgvXTmypZGAZY=
github.com/tailscale/gokrazy-tools v0.0.0-20240602210012-933640538dcf/go.mod h1:+PSix9a8BHqAz6RV/9+tiE3C1ou0GA1ViR8pqAZVfwI=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

8
gokrazy/gok Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# This is a wrapper around gok that sets --parent_dir.
dir=$(dirname "${BASH_SOURCE[0]}")
cd $dir
$dir/../tool/go run github.com/gokrazy/tools/cmd/gok --parent_dir="$dir" "$@"

10
gokrazy/tidy-deps.go Normal file
View File

@@ -0,0 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build for_go_mod_tidy
package gokrazy
import (
_ "github.com/gokrazy/tools/cmd/gok"
)

12
gokrazy/tsapp/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Tailscale Appliance
This is the definition of the Gokrazy Tailscale Appliance (tsapp) image.
See the parent directory for context.
## File contents
The `config.json` is a Gokrazy config.
The `usr-dir.tar` is a single symlink named `bin` pointing to `/user`. We write it to `/usr/bin => /user` so the Busybox `ash` shell's default `$PATH` includes `/user`, where the `tailscale` CLI is.
The `builddir` is the Gokrazy build environment, defining each program's `go.mod`.

View File

@@ -0,0 +1,19 @@
module gokrazy/build/tsapp
go 1.22.2
require (
github.com/creack/pty v1.1.18 // indirect
github.com/gokrazy/breakglass v0.0.0-20240604170121-09eeab3321d6 // indirect
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83 // indirect
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/kenshaw/evdev v0.1.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pty v1.1.8 // indirect
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 // indirect
github.com/pkg/sftp v1.13.5 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)

View File

@@ -0,0 +1,46 @@
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gokrazy/breakglass v0.0.0-20240529175905-44b3fe64f19c h1:cWzgXJIluB6jAQ0HcnvA1yExLawmtDSssk9H4fLv3yM=
github.com/gokrazy/breakglass v0.0.0-20240529175905-44b3fe64f19c/go.mod h1:4Yffo2Z5w3q2eDvo3HDR8eDnmkDpMAkX0Tn7b/9upgs=
github.com/gokrazy/breakglass v0.0.0-20240604170121-09eeab3321d6 h1:38JB1lVPx+ihCzlWZdbH1LoNmu0KR+jRSmNFR7aMVTg=
github.com/gokrazy/breakglass v0.0.0-20240604170121-09eeab3321d6/go.mod h1:4Yffo2Z5w3q2eDvo3HDR8eDnmkDpMAkX0Tn7b/9upgs=
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83 h1:Y4sADvUYd/c0eqnqebipHHl0GMpAxOQeTzPnwI4ievM=
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83/go.mod h1:9q5Tg+q+YvRjC3VG0gfMFut46dhbhtAnvUEp4lPjc6c=
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0 h1:QTi0skQ/OM7he/5jEWA9k/DYgdwGAhw3hrUoiPGGZHM=
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0/go.mod h1:ddHcxXZ/VVQOSAWcRBbkYY58+QOw4L145ye6phyDmRA=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/kenshaw/evdev v0.1.0 h1:wmtceEOFfilChgdNT+c/djPJ2JineVsQ0N14kGzFRUo=
github.com/kenshaw/evdev v0.1.0/go.mod h1:B/fErKCihUyEobz0mjn2qQbHgyJKFQAxkXSvkeeA/Wo=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 h1:80FAK3TW5lVymfHu3kvB1QvTZvy9Kmx1lx6sT5Ep16s=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5/go.mod h1:z0QjVpjpK4jksEkffQwS3+abQ3XFTm1bnimyDzWyUk0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tailscale/breakglass v0.0.0-20240529174846-0d8ebfc2c652 h1:36TB+ZuYaA8OTdMoPnygC9CJuQmTWxMEmn+a+9XTOgk=
github.com/tailscale/breakglass v0.0.0-20240529174846-0d8ebfc2c652/go.mod h1:4Yffo2Z5w3q2eDvo3HDR8eDnmkDpMAkX0Tn7b/9upgs=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,18 @@
module gokrazy/build/tsapp
go 1.22.2
require (
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/mdlayher/packet v1.0.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.20.0 // indirect
)

View File

@@ -0,0 +1,39 @@
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 h1:gdGRW/wXHPJuZgZD931Lh75mdJfzEEXrL+Dvi97Ck3A=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8=
github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46 h1:3psQveH4RUiv5yc3p7kRySilf1nSXLQhAvJFwg4fgnE=
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46/go.mod h1:Ng1F/s+z0zCMsbEFEneh+30LJa9DrTfmA+REbEqcTPk=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 // indirect

View File

@@ -0,0 +1,14 @@
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 h1:gdGRW/wXHPJuZgZD931Lh75mdJfzEEXrL+Dvi97Ck3A=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
github.com/gokrazy/internal v0.0.0-20240510165500-68dd68393b7a h1:FKeN678rNpKTpWRdFbAhYL9mWzPu57R5XPXCR3WmXdI=
github.com/gokrazy/internal v0.0.0-20240510165500-68dd68393b7a/go.mod h1:t3ZirVhcs9bH+fPAJuGh51rzT7sVCZ9yfXvszf0ZjF0=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/kenshaw/evdev v0.1.0 h1:wmtceEOFfilChgdNT+c/djPJ2JineVsQ0N14kGzFRUo=
github.com/kenshaw/evdev v0.1.0/go.mod h1:B/fErKCihUyEobz0mjn2qQbHgyJKFQAxkXSvkeeA/Wo=
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b h1:7tUBfsEEBWfFeHOB7CUfoOamak+Gx/BlirfXyPk1WjI=
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b/go.mod h1:bmoJUS6qOA3uKFvF3KVuhf7mU1KQirzQMeHXtPyKEqg=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 // indirect

View File

@@ -0,0 +1,8 @@
github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 h1:gdGRW/wXHPJuZgZD931Lh75mdJfzEEXrL+Dvi97Ck3A=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 // indirect

View File

@@ -0,0 +1,4 @@
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 h1:gdGRW/wXHPJuZgZD931Lh75mdJfzEEXrL+Dvi97Ck3A=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,7 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 // indirect
replace github.com/gokrazy/gokrazy => github.com/tailscale/gokrazy v0.0.0-20240602215456-7b9b6bbf726a

View File

@@ -0,0 +1,16 @@
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803 h1:gdGRW/wXHPJuZgZD931Lh75mdJfzEEXrL+Dvi97Ck3A=
github.com/gokrazy/gokrazy v0.0.0-20240525065858-dedadaf38803/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
github.com/gokrazy/internal v0.0.0-20240510165500-68dd68393b7a h1:FKeN678rNpKTpWRdFbAhYL9mWzPu57R5XPXCR3WmXdI=
github.com/gokrazy/internal v0.0.0-20240510165500-68dd68393b7a/go.mod h1:t3ZirVhcs9bH+fPAJuGh51rzT7sVCZ9yfXvszf0ZjF0=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/kenshaw/evdev v0.1.0 h1:wmtceEOFfilChgdNT+c/djPJ2JineVsQ0N14kGzFRUo=
github.com/kenshaw/evdev v0.1.0/go.mod h1:B/fErKCihUyEobz0mjn2qQbHgyJKFQAxkXSvkeeA/Wo=
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b h1:7tUBfsEEBWfFeHOB7CUfoOamak+Gx/BlirfXyPk1WjI=
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b/go.mod h1:bmoJUS6qOA3uKFvF3KVuhf7mU1KQirzQMeHXtPyKEqg=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tailscale/gokrazy v0.0.0-20240602215456-7b9b6bbf726a h1:7dnA8x14JihQmKbPr++Y5CCN/XSyDmOB6cXUxcIj6VQ=
github.com/tailscale/gokrazy v0.0.0-20240602215456-7b9b6bbf726a/go.mod h1:NHROeDlzn0icUl3f+tEYvGGpcyBDMsr3AvKLHOWRe5M=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/mkfs v0.0.0-20230114091253-b6755f9e9632 // indirect

View File

@@ -0,0 +1,4 @@
github.com/gokrazy/internal v0.0.0-20210621162516-1b3b5687a06d h1:qk95CKJfxvU5oi3lbrVkEgID5ak1pOjTyPTdaXs6Q9E=
github.com/gokrazy/internal v0.0.0-20210621162516-1b3b5687a06d/go.mod h1:Gqv1x1DNrObmBvVvblpZbvZizZ0dU5PwiwYHipmtY9Y=
github.com/gokrazy/mkfs v0.0.0-20230114091253-b6755f9e9632 h1:Vt3rJdB4p56yjK4CKhb/awHT6Qj7LoC3CwPoOaiNS6k=
github.com/gokrazy/mkfs v0.0.0-20230114091253-b6755f9e9632/go.mod h1:O2w1ipGvg78u3F61FnLp36Db3MsUbdy8E/ciG64jbGY=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/rpi-eeprom v0.0.0-20240518032756-37da22ee9608 // indirect

View File

@@ -0,0 +1,3 @@
github.com/gokrazy/rpi-eeprom v0.0.0-20240518032756-37da22ee9608 h1:8uderKR+8eXR0nRcyBugql1YPoJQjpjoltHqX9yl2DI=
github.com/gokrazy/rpi-eeprom v0.0.0-20240518032756-37da22ee9608/go.mod h1:vabxV1M+i6S3rGuWoFieHxCJW3jlob3rqe0KV82j+0o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/gokrazy/serial-busybox v0.0.0-20220918193710-d728912733ca // indirect

View File

@@ -0,0 +1,26 @@
github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904 h1:eqfH4A/LLgxv5RvqEXwVoFvfmpRa8+TokRjB5g6xBkk=
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904/go.mod h1:pq6rGHqxMRPSaTXaCMzIZy0wLDusAJyoVNyNo05RLs0=
github.com/gokrazy/internal v0.0.0-20200407075822-660ad467b7c9 h1:x5jR/nNo4/kMSoNo/nwa2xbL7PN1an8S3oIn4OZJdec=
github.com/gokrazy/internal v0.0.0-20200407075822-660ad467b7c9/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
github.com/gokrazy/serial-busybox v0.0.0-20220918193710-d728912733ca h1:x0eSjuFy8qsRctVHeWm3EC474q3xm4h3OOOrYpcqyyA=
github.com/gokrazy/serial-busybox v0.0.0-20220918193710-d728912733ca/go.mod h1:OYcG5tSb+QrelmUOO4EZVUFcIHyyZb0QDbEbZFUp1TA=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5/go.mod h1:FwstIpm6vX98QgtR8KEwZcVjiRn2WP76LjXAHj84fK0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 h1:c1Sgqkh8v6ZxafNGG64r8C8UisIW2TKMJN8P86tKjr0=
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require github.com/tailscale/gokrazy-kernel v0.0.0-20240530042707-3f95c886bcf2 // indirect

View File

@@ -0,0 +1,2 @@
github.com/tailscale/gokrazy-kernel v0.0.0-20240530042707-3f95c886bcf2 h1:xzf+cMvBJBcA/Av7OTWBa0Tjrbfcy00TeatJeJt6zrY=
github.com/tailscale/gokrazy-kernel v0.0.0-20240530042707-3f95c886bcf2/go.mod h1:7Mth+m9bq2IHusSsexMNyupHWPL8RxwOuSvBlSGtgDY=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require tailscale.com v1.66.4 // indirect

View File

@@ -0,0 +1,86 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A=
k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.66.4 h1:V0vTQah3xi2/zbsxJeOfl5QbO1WJPeD9TMlfL0daXqc=
tailscale.com v1.66.4/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM=

View File

@@ -0,0 +1,5 @@
module gokrazy/build/tsapp
go 1.22.2
require tailscale.com v1.66.4 // indirect

View File

@@ -0,0 +1,154 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls=
github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI=
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
tailscale.com v1.66.4 h1:V0vTQah3xi2/zbsxJeOfl5QbO1WJPeD9TMlfL0daXqc=
tailscale.com v1.66.4/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM=

24
gokrazy/tsapp/config.json Normal file
View File

@@ -0,0 +1,24 @@
{
"Hostname": "tsapp",
"Update": { "NoPassword": true },
"SerialConsole": "ttyS0,115200",
"Packages": [
"github.com/gokrazy/serial-busybox",
"github.com/gokrazy/breakglass",
"tailscale.com/cmd/tailscale",
"tailscale.com/cmd/tailscaled"
],
"PackageConfig": {
"github.com/gokrazy/breakglass": {
"CommandLineFlags": [ "-authorized_keys=ec2" ]
},
"tailscale.com/cmd/tailscale": {
"ExtraFilePaths": {
"/usr": "usr-dir"
}
}
},
"KernelPackage": "github.com/tailscale/gokrazy-kernel",
"FirmwarePackage": "github.com/tailscale/gokrazy-kernel",
"InternalCompatibilityFlags": {}
}

BIN
gokrazy/tsapp/usr-dir.tar Normal file

Binary file not shown.

View File

@@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || windows
package ipnlocal
import (
"context"
"time"
"tailscale.com/clientupdate"
"tailscale.com/ipn"
)
func (b *LocalBackend) stopOfflineAutoUpdate() {
if b.offlineAutoUpdateCancel != nil {
b.logf("offline auto-update: stopping update checks")
b.offlineAutoUpdateCancel()
b.offlineAutoUpdateCancel = nil
}
}
func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
if !prefs.AutoUpdate().Apply.EqualBool(true) {
return
}
// AutoUpdate.Apply field in prefs can only be true for platforms that
// support auto-updates. But check it here again, just in case.
if !clientupdate.CanAutoUpdate() {
return
}
if b.offlineAutoUpdateCancel != nil {
// Already running.
return
}
ctx, cancel := context.WithCancel(context.Background())
b.offlineAutoUpdateCancel = cancel
b.logf("offline auto-update: starting update checks")
go b.offlineAutoUpdate(ctx)
}
const offlineAutoUpdateCheckPeriod = time.Hour
func (b *LocalBackend) offlineAutoUpdate(ctx context.Context) {
t := time.NewTicker(offlineAutoUpdateCheckPeriod)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
if err := b.startAutoUpdate("offline auto-update"); err != nil {
b.logf("offline auto-update: failed: %v", err)
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(linux || windows)
package ipnlocal
import (
"tailscale.com/ipn"
)
func (b *LocalBackend) stopOfflineAutoUpdate() {
// Not supported on this platform.
}
func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
// Not supported on this platform.
}

View File

@@ -4,7 +4,6 @@
package ipnlocal
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
@@ -307,60 +306,11 @@ func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request
return
}
// Check if update was already started, and mark as started.
if !b.trySetC2NUpdateStarted() {
res.Err = "update already started"
return
}
defer func() {
// Clear the started flag if something failed.
if res.Err != "" {
b.setC2NUpdateStarted(false)
}
}()
cmdTS, err := findCmdTailscale()
if err != nil {
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
return
}
var ver struct {
Long string `json:"long"`
}
out, err := exec.Command(cmdTS, "version", "--json").Output()
if err != nil {
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
return
}
if err := json.Unmarshal(out, &ver); err != nil {
res.Err = "invalid JSON from cmd/tailscale version --json"
return
}
if ver.Long != version.Long() {
res.Err = "cmd/tailscale version mismatch"
return
}
cmd := tailscaleUpdateCmd(cmdTS)
buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
b.logf("c2n: running %q", strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
res.Err = fmt.Sprintf("failed to start cmd/tailscale update: %v", err)
if err := b.startAutoUpdate("c2n"); err != nil {
res.Err = err.Error()
return
}
res.Started = true
// Run update asynchronously and respond that it started.
go func() {
if err := cmd.Wait(); err != nil {
b.logf("c2n: update command failed: %v, output: %s", err, buf)
} else {
b.logf("c2n: update complete")
}
b.setC2NUpdateStarted(false)
}()
}
func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {

View File

@@ -191,7 +191,7 @@ func (b *LocalBackend) domainRenewalTimeByARI(cs certStore, pair *TLSCertKeyPair
}
blocks = append(blocks, block)
}
if len(blocks) < 2 {
if len(blocks) < 1 {
return time.Time{}, fmt.Errorf("could not parse certificate chain from certStore, got %d PEM block(s)", len(blocks))
}
ac, err := acmeClient(cs)
@@ -200,7 +200,7 @@ func (b *LocalBackend) domainRenewalTimeByARI(cs certStore, pair *TLSCertKeyPair
}
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
defer cancel()
ri, err := ac.FetchRenewalInfo(ctx, blocks[0].Bytes, blocks[1].Bytes)
ri, err := ac.FetchRenewalInfo(ctx, blocks[0].Bytes)
if err != nil {
return time.Time{}, fmt.Errorf("failed to fetch renewal info from ACME server: %w", err)
}

View File

@@ -4,6 +4,7 @@
package ipnlocal
import (
"bytes"
"cmp"
"context"
"encoding/base64"
@@ -20,6 +21,7 @@ import (
"net/netip"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
@@ -78,6 +80,7 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
@@ -291,6 +294,13 @@ type LocalBackend struct {
// capForcedNetfilter is the netfilter that control instructs Linux clients
// to use, unless overridden locally.
capForcedNetfilter string
// offlineAutoUpdateCancel stops offline auto-updates when called. It
// should be used via stopOfflineAutoUpdate and
// maybeStartOfflineAutoUpdate. It is nil when offline auto-updates are
// note running.
//
//lint:ignore U1000 only used in Linux and Windows builds in autoupdate.go
offlineAutoUpdateCancel func()
// ServeConfig fields. (also guarded by mu)
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
@@ -721,6 +731,7 @@ func (b *LocalBackend) Shutdown() {
if b.peerAPIServer != nil {
b.peerAPIServer.taildrop.Shutdown()
}
b.stopOfflineAutoUpdate()
b.unregisterNetMon()
b.unregisterHealthWatch()
@@ -3327,6 +3338,14 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
b.logf("failed to save new controlclient state: %v", err)
}
if newp.AutoUpdate.Apply.EqualBool(true) {
if b.state != ipn.Running {
b.maybeStartOfflineAutoUpdate(newp.View())
}
} else {
b.stopOfflineAutoUpdate()
}
unlock.UnlockEarly()
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
@@ -4347,6 +4366,12 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
}
b.pauseOrResumeControlClientLocked()
if newState == ipn.Running {
b.stopOfflineAutoUpdate()
} else {
b.maybeStartOfflineAutoUpdate(prefs)
}
unlock.UnlockEarly()
// prefs may change irrespective of state; WantRunning should be explicitly
@@ -6420,9 +6445,8 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
}
return last, err
}
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
res, err := suggestExitNode(lastReport, netMap, r)
res, err := suggestExitNode(lastReport, netMap, randomRegion, randomNode, getAllowedSuggestions())
if err != nil {
last, err := lastSuggestedExitNode.asAPIType()
if err != nil {
@@ -6437,6 +6461,13 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
return res, err
}
// selectRegionFunc returns a DERP region from the slice of candidate regions.
// The value is returned, not the slice index.
type selectRegionFunc func(views.Slice[int]) int
// selectNodeFunc returns a node from the slice of candidate nodes.
type selectNodeFunc func(nodes views.Slice[tailcfg.NodeView]) tailcfg.NodeView
// asAPIType formats a response with the last suggested exit node's ID and name.
// Returns error if there is no id or name.
// Used as a fallback before returning a nil response and error.
@@ -6449,19 +6480,34 @@ func (n lastSuggestedExitNode) asAPIType() (res apitype.ExitNodeSuggestionRespon
return res, ErrUnableToSuggestLastExitNode
}
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand.Rand) (res apitype.ExitNodeSuggestionResponse, err error) {
if report.PreferredDERP == 0 {
return res, ErrNoPreferredDERP
var getAllowedSuggestions = lazy.SyncFunc(fillAllowedSuggestions)
func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
nodes, err := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil)
if err != nil {
log.Printf("fillAllowedSuggestions: unable to look up %q policy: %v", syspolicy.AllowedSuggestedExitNodes, err)
return nil
}
var allowedCandidates set.Set[string]
if allowed, err := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil); err != nil {
return res, fmt.Errorf("unable to read %s policy: %w", syspolicy.AllowedSuggestedExitNodes, err)
} else if allowed != nil {
allowedCandidates = set.SetOf(allowed)
if nodes == nil {
return nil
}
s := make(set.Set[tailcfg.StableNodeID], len(nodes))
for _, n := range nodes {
s.Add(tailcfg.StableNodeID(n))
}
return s
}
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
if report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
return res, ErrNoPreferredDERP
}
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
for _, peer := range netMap.Peers {
if allowedCandidates != nil && !allowedCandidates.Contains(string(peer.StableID())) {
if !peer.Valid() {
continue
}
if allowList != nil && !allowList.Contains(peer.StableID()) {
continue
}
if peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
@@ -6484,7 +6530,10 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
}
candidatesByRegion := make(map[int][]tailcfg.NodeView, len(netMap.DERPMap.Regions))
var preferredDERP *tailcfg.DERPRegion = netMap.DERPMap.Regions[report.PreferredDERP]
preferredDERP, ok := netMap.DERPMap.Regions[report.PreferredDERP]
if !ok {
return res, ErrNoPreferredDERP
}
var minDistance float64 = math.MaxFloat64
type nodeDistance struct {
nv tailcfg.NodeView
@@ -6492,9 +6541,6 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
}
distances := make([]nodeDistance, 0, len(candidates))
for _, c := range candidates {
if !c.Valid() {
continue
}
if c.DERP() != "" {
ipp, err := netip.ParseAddrPort(c.DERP())
if err != nil {
@@ -6533,13 +6579,13 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
if len(candidatesByRegion) > 0 {
minRegion := minLatencyDERPRegion(xmaps.Keys(candidatesByRegion), report)
if minRegion == 0 {
minRegion = randomRegion(xmaps.Keys(candidatesByRegion), r)
minRegion = selectRegion(views.SliceOf(xmaps.Keys(candidatesByRegion)))
}
regionCandidates, ok := candidatesByRegion[minRegion]
if !ok {
return res, errors.New("no candidates in expected region: this is a bug")
}
chosen := randomNode(regionCandidates, r)
chosen := selectNode(views.SliceOf(regionCandidates))
res.ID = chosen.StableID()
res.Name = chosen.Name()
if hi := chosen.Hostinfo(); hi.Valid() {
@@ -6565,7 +6611,8 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
pickFrom = append(pickFrom, candidate.nv)
}
}
chosen := pickWeighted(pickFrom)
bestCandidates := pickWeighted(pickFrom)
chosen := selectNode(views.SliceOf(bestCandidates))
if !chosen.Valid() {
return res, errors.New("chosen candidate invalid: this is a bug")
}
@@ -6580,36 +6627,35 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
}
// pickWeighted chooses the node with highest priority given a list of mullvad nodes.
func pickWeighted(candidates []tailcfg.NodeView) tailcfg.NodeView {
func pickWeighted(candidates []tailcfg.NodeView) []tailcfg.NodeView {
maxWeight := 0
var best tailcfg.NodeView
best := make([]tailcfg.NodeView, 0, 1)
for _, c := range candidates {
hi := c.Hostinfo()
if !hi.Valid() {
continue
}
loc := hi.Location()
if loc == nil || loc.Priority <= maxWeight {
if loc == nil || loc.Priority < maxWeight {
continue
}
if maxWeight != loc.Priority {
best = best[:0]
}
maxWeight = loc.Priority
best = c
best = append(best, c)
}
return best
}
// randomNode chooses a node randomly given a list of nodes and a *rand.Rand.
func randomNode(nodes []tailcfg.NodeView, r *rand.Rand) tailcfg.NodeView {
return nodes[r.Intn(len(nodes))]
// randomRegion is a selectRegionFunc that selects a uniformly random region.
func randomRegion(regions views.Slice[int]) int {
return regions.At(rand.Intn(regions.Len()))
}
// randomRegion chooses a region randomly given a list of ints and a *rand.Rand
func randomRegion(regions []int, r *rand.Rand) int {
if testenv.InTest() {
regions = slices.Clone(regions)
slices.Sort(regions)
}
return regions[r.Intn(len(regions))]
// randomNode is a selectNodeFunc that returns a uniformly random node.
func randomNode(nodes views.Slice[tailcfg.NodeView]) tailcfg.NodeView {
return nodes.At(rand.Intn(nodes.Len()))
}
// minLatencyDERPRegion returns the region with the lowest latency value given the last netcheck report.
@@ -6652,3 +6698,55 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusMeters * c
}
// startAutoUpdate triggers an auto-update attempt. The actual update happens
// asynchronously. If another update is in progress, an error is returned.
func (b *LocalBackend) startAutoUpdate(logPrefix string) (retErr error) {
// Check if update was already started, and mark as started.
if !b.trySetC2NUpdateStarted() {
return errors.New("update already started")
}
defer func() {
// Clear the started flag if something failed.
if retErr != nil {
b.setC2NUpdateStarted(false)
}
}()
cmdTS, err := findCmdTailscale()
if err != nil {
return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
}
var ver struct {
Long string `json:"long"`
}
out, err := exec.Command(cmdTS, "version", "--json").Output()
if err != nil {
return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
}
if err := json.Unmarshal(out, &ver); err != nil {
return fmt.Errorf("invalid JSON from cmd/tailscale version --json: %w", err)
}
if ver.Long != version.Long() {
return fmt.Errorf("cmd/tailscale version %q does not match tailscaled version %q", ver.Long, version.Long())
}
cmd := tailscaleUpdateCmd(cmdTS)
buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
b.logf("%s: running %q", logPrefix, strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start cmd/tailscale update: %w", err)
}
go func() {
if err := cmd.Wait(); err != nil {
b.logf("%s: update command failed: %v, output: %s", logPrefix, err, buf)
} else {
b.logf("%s: update attempt complete", logPrefix)
}
b.setC2NUpdateStarted(false)
}()
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -72,15 +72,15 @@ See also the dependencies in the [Tailscale CLI][].
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.22.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.23.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.24.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.19.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.19.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.20.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.20.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.15.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))

View File

@@ -64,17 +64,17 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.22.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.23.0:LICENSE))
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.15.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.17.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.24.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.19.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.19.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.20.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.20.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.15.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))

View File

@@ -52,10 +52,11 @@ type Manager struct {
resolver *resolver.Resolver
os OSConfigurator
goos string // if empty, gets set to runtime.GOOS
}
// NewManagers created a new manager from the given config.
func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs) *Manager {
func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs, goos string) *Manager {
if dialer == nil {
panic("nil Dialer")
}
@@ -63,11 +64,15 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker,
panic("Dialer has nil NetMon")
}
logf = logger.WithPrefix(logf, "dns: ")
if goos == "" {
goos = runtime.GOOS
}
m := &Manager{
logf: logf,
resolver: resolver.New(logf, linkSel, dialer, knobs),
os: oscfg,
health: health,
goos: goos,
}
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
m.logf("using %T", m.os)
@@ -166,7 +171,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// Similarly, the OS always gets search paths.
ocfg.SearchDomains = cfg.SearchDomains
if runtime.GOOS == "windows" {
if m.goos == "windows" {
ocfg.Hosts = compileHostEntries(cfg)
}
@@ -219,8 +224,9 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
//
// This bool is used in a couple of places below to implement this
// workaround.
isWindows := runtime.GOOS == "windows"
if len(cfg.singleResolverSet()) > 0 && m.os.SupportsSplitDNS() && !isWindows {
isWindows := m.goos == "windows"
isApple := (m.goos == "darwin" || m.goos == "ios")
if len(cfg.singleResolverSet()) > 0 && m.os.SupportsSplitDNS() && !isWindows && !isApple {
// Split DNS configuration requested, where all split domains
// go to the same resolvers. We can let the OS do it.
ocfg.Nameservers = toIPsOnly(cfg.singleResolverSet())
@@ -240,8 +246,6 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// selectively answer ExtraRecords, and ignore other DNS traffic. As a
// workaround, we read the existing default resolver configuration and use
// that as the forwarder for all DNS traffic that quad-100 doesn't handle.
const isApple = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
if isApple || !m.os.SupportsSplitDNS() {
// If the OS can't do native split-dns, read out the underlying
// resolver config and blend it into our config.
@@ -257,9 +261,8 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
}
}
if baseCfg == nil || isApple && len(baseCfg.Nameservers) == 0 {
// If there was no base config, or if we're on Apple and the base
// config is empty, then we need to fallback to SplitDNS mode.
if baseCfg == nil {
// If there was no base config, then we need to fallback to SplitDNS mode.
ocfg.MatchDomains = cfg.matchDomains()
} else {
// On iOS only (for now), check if all route names point to resources inside the tailnet.
@@ -269,7 +272,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// we have any Routes outside the tailnet. Otherwise when app connectors are enabled,
// a query for 'work-laptop' might lead to search domain expansion, resolving
// as 'work-laptop.aws.com' for example.
if runtime.GOOS == "ios" && rcfg.RoutesRequireNoCustomResolvers() {
if m.goos == "ios" && rcfg.RoutesRequireNoCustomResolvers() {
for r := range rcfg.Routes {
ocfg.MatchDomains = append(ocfg.MatchDomains, r)
}
@@ -476,7 +479,7 @@ func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
}
d := &tsdial.Dialer{Logf: logf}
d.SetNetMon(netMon)
dns := NewManager(logf, oscfg, nil, d, nil, nil)
dns := NewManager(logf, oscfg, nil, d, nil, nil, runtime.GOOS)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -88,7 +88,7 @@ func TestDNSOverTCP(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
}
m := NewManager(t.Logf, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil)
m := NewManager(t.Logf, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil, "")
m.resolver.TestOnlySetHook(f.SetResolver)
m.Set(Config{
Hosts: hosts(
@@ -173,7 +173,7 @@ func TestDNSOverTCP_TooLarge(t *testing.T) {
SearchDomains: fqdns("coffee.shop"),
},
}
m := NewManager(log, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil)
m := NewManager(log, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil, "")
m.resolver.TestOnlySetHook(f.SetResolver)
m.Set(Config{
Hosts: hosts("andrew.ts.com.", "1.2.3.4"),

View File

@@ -172,6 +172,7 @@ func TestManager(t *testing.T) {
bs OSConfig
os OSConfig
rs resolver.Config
goos string // empty means "linux"
}{
{
name: "empty",
@@ -424,7 +425,7 @@ func TestManager(t *testing.T) {
},
},
{
name: "routes-multi-split",
name: "routes-multi-split-linux",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
@@ -442,6 +443,57 @@ func TestManager(t *testing.T) {
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
goos: "linux",
},
{
// The `routes-multi-split-linux` test case above on Darwin should NOT result in a split
// DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "routes-multi-does-not-split-on-darwin",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
goos: "darwin",
},
{
// The `routes-multi-split-linux` test case above on iOS should NOT result in a split
// DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "routes-multi-does-not-split-on-ios",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
goos: "ios",
},
{
name: "magic",
@@ -489,6 +541,59 @@ func TestManager(t *testing.T) {
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "linux",
},
{
// The `magic-split` test case above on Darwin should NOT result in a split DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "magic-split-does-not-split-on-darwin",
in: Config{
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
Routes: upstreams("ts.com", ""),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "darwin",
},
{
// The `magic-split` test case above on iOS should NOT result in a split DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "magic-split-does-not-split-on-ios",
in: Config{
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
Routes: upstreams("ts.com", ""),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: false,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "ios",
},
{
name: "routes-magic",
@@ -518,7 +623,7 @@ func TestManager(t *testing.T) {
},
},
{
name: "routes-magic-split",
name: "routes-magic-split-linux",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
@@ -541,6 +646,71 @@ func TestManager(t *testing.T) {
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "linux",
},
{
// The `routes-magic-split-linux` test case above on Darwin should NOT result in a
// split DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "routes-magic-does-not-split-on-darwin",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
"ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"corp.com.", "2.2.2.2",
),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "darwin",
},
{
// The `routes-magic-split-linux` test case above on Darwin should NOT result in a
// split DNS configuration.
// Check that MatchDomains is empty. Due to Apple limitations, we cannot set MatchDomains
// without those domains also being SearchDomains.
name: "routes-magic-does-not-split-on-ios",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2",
"ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"corp.com.", "2.2.2.2",
),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
goos: "ios",
},
{
name: "exit-node-forward",
@@ -598,6 +768,76 @@ func TestManager(t *testing.T) {
Routes: upstreams(".", "https://dns.nextdns.io/c3a884"),
},
},
{
// on iOS exclusively, tests the split DNS behavior for battery life optimization added in
// https://github.com/tailscale/tailscale/pull/10576
name: "ios-use-split-dns-when-no-custom-resolvers",
in: Config{
Routes: upstreams("ts.net", "199.247.155.52", "optimistic-display.ts.net", ""),
SearchDomains: fqdns("optimistic-display.ts.net"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("optimistic-display.ts.net"),
MatchDomains: fqdns("ts.net"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"ts.net", "199.247.155.52",
),
LocalDomains: fqdns("optimistic-display.ts.net."),
},
goos: "ios",
},
{
// if using app connectors, the battery life optimization above should not be applied
name: "ios-dont-use-split-dns-when-app-connector-resolver-needed",
in: Config{
Routes: upstreams(
"ts.net", "199.247.155.52",
"optimistic-display.ts.net", "",
"github.com", "https://dnsresolver.bigcorp.com/2f143"),
SearchDomains: fqdns("optimistic-display.ts.net"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("optimistic-display.ts.net"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"github.com", "https://dnsresolver.bigcorp.com/2f143",
"ts.net", "199.247.155.52",
),
LocalDomains: fqdns("optimistic-display.ts.net."),
},
goos: "ios",
},
{
// on darwin, verify that with the same config as in ios-use-split-dns-when-no-custom-resolvers,
// MatchDomains are NOT set.
name: "darwin-dont-use-split-dns-when-no-custom-resolvers",
in: Config{
Routes: upstreams("ts.net", "199.247.155.52", "optimistic-display.ts.net", ""),
SearchDomains: fqdns("optimistic-display.ts.net"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("optimistic-display.ts.net"),
},
rs: resolver.Config{
Routes: upstreams(
".", "",
"ts.net", "199.247.155.52",
),
LocalDomains: fqdns("optimistic-display.ts.net."),
},
goos: "darwin",
},
}
trIP := cmp.Transformer("ipStr", func(ip netip.Addr) string { return ip.String() })
@@ -614,7 +854,11 @@ func TestManager(t *testing.T) {
SplitDNS: test.split,
BaseConfig: test.bs,
}
m := NewManager(t.Logf, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil)
goos := test.goos
if goos == "" {
goos = "linux"
}
m := NewManager(t.Logf, &f, nil, tsdial.NewDialer(netmon.NewStatic()), nil, nil, goos)
m.resolver.TestOnlySetHook(f.SetResolver)
if err := m.Set(test.in); err != nil {

View File

@@ -82,7 +82,7 @@ func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) {
fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains)
}
if len(o.MatchDomains) > 0 {
w.WriteString("SearchDomains:[")
w.WriteString("MatchDomains:[")
sp := ""
var numARPA int
for _, s := range o.MatchDomains {

View File

@@ -10,7 +10,6 @@ import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -80,16 +79,14 @@ type server struct {
logf logger.Logf
tailscaledPath string
pubKeyHTTPClient *http.Client // or nil for http.DefaultClient
timeNow func() time.Time // or nil for time.Now
timeNow func() time.Time // or nil for time.Now
sessionWaitGroup sync.WaitGroup
// mu protects the following
mu sync.Mutex
activeConns map[*conn]bool // set; value is always true
fetchPublicKeysCache map[string]pubKeyCacheEntry // by https URL
shutdownCalled bool
mu sync.Mutex
activeConns map[*conn]bool // set; value is always true
shutdownCalled bool
}
func (srv *server) now() time.Time {
@@ -204,7 +201,6 @@ func (srv *server) OnPolicyChange() {
//
// Do the user auth
// - NoClientAuthHandler
// - PublicKeyHandler (only if NoClientAuthHandler returns errPubKeyRequired)
//
// Once auth is done, the conn can be multiplexed with multiple sessions and
// channels concurrently. At which point any of the following can be called
@@ -234,10 +230,9 @@ type conn struct {
finalAction *tailcfg.SSHAction // set by doPolicyAuth or resolveNextAction
finalActionErr error // set by doPolicyAuth or resolveNextAction
info *sshConnInfo // set by setInfo
localUser *userMeta // set by doPolicyAuth
userGroupIDs []string // set by doPolicyAuth
pubKey gossh.PublicKey // set by doPolicyAuth
info *sshConnInfo // set by setInfo
localUser *userMeta // set by doPolicyAuth
userGroupIDs []string // set by doPolicyAuth
// mu protects the following fields.
//
@@ -267,9 +262,6 @@ func (c *conn) isAuthorized(ctx ssh.Context) error {
action := c.currentAction
for {
if action.Accept {
if c.pubKey != nil {
metricPublicKeyAccepts.Add(1)
}
return nil
}
if action.Reject || action.HoldAndDelegate == "" {
@@ -292,10 +284,6 @@ func (c *conn) isAuthorized(ctx ssh.Context) error {
// policy.
var errDenied = errors.New("ssh: access denied")
// errPubKeyRequired is returned by NoClientAuthCallback to make the client
// resort to public-key auth; not user visible.
var errPubKeyRequired = errors.New("ssh publickey required")
// NoClientAuthCallback implements gossh.NoClientAuthCallback and is called by
// the ssh.Server when the client first connects with the "none"
// authentication method.
@@ -304,13 +292,13 @@ var errPubKeyRequired = errors.New("ssh publickey required")
// starting it afresh). It returns an error if the policy evaluation fails, or
// if the decision is "reject"
//
// It either returns nil (accept) or errPubKeyRequired or errDenied
// (reject). The errors may be wrapped.
// It either returns nil (accept) or errDenied (reject). The errors may be
// wrapped.
func (c *conn) NoClientAuthCallback(ctx ssh.Context) error {
if c.insecureSkipTailscaleAuth {
return nil
}
if err := c.doPolicyAuth(ctx, nil /* no pub key */); err != nil {
if err := c.doPolicyAuth(ctx); err != nil {
return err
}
if err := c.isAuthorized(ctx); err != nil {
@@ -328,11 +316,8 @@ func (c *conn) NoClientAuthCallback(ctx ssh.Context) error {
}
func (c *conn) nextAuthMethodCallback(cm gossh.ConnMetadata, prevErrors []error) (nextMethod []string) {
switch {
case c.anyPasswordIsOkay:
if c.anyPasswordIsOkay {
nextMethod = append(nextMethod, "password")
case len(prevErrors) > 0 && prevErrors[len(prevErrors)-1] == errPubKeyRequired:
nextMethod = append(nextMethod, "publickey")
}
// The fake "tailscale" method is always appended to next so OpenSSH renders
@@ -352,41 +337,19 @@ func (c *conn) fakePasswordHandler(ctx ssh.Context, password string) bool {
return c.anyPasswordIsOkay
}
// PublicKeyHandler implements ssh.PublicKeyHandler is called by the
// ssh.Server when the client presents a public key.
func (c *conn) PublicKeyHandler(ctx ssh.Context, pubKey ssh.PublicKey) error {
if err := c.doPolicyAuth(ctx, pubKey); err != nil {
// TODO(maisem/bradfitz): surface the error here.
c.logf("rejecting SSH public key %s: %v", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)), err)
return err
}
if err := c.isAuthorized(ctx); err != nil {
return err
}
c.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)))
return nil
}
// doPolicyAuth verifies that conn can proceed with the specified (optional)
// pubKey. It returns nil if the matching policy action is Accept or
// HoldAndDelegate. If pubKey is nil, there was no policy match but there is a
// policy that might match a public key it returns errPubKeyRequired. Otherwise,
// it returns errDenied.
func (c *conn) doPolicyAuth(ctx ssh.Context, pubKey ssh.PublicKey) error {
// doPolicyAuth verifies that conn can proceed. It returns nil if the matching
// policy action is Accept or HoldAndDelegate. Otherwise, it returns errDenied.
func (c *conn) doPolicyAuth(ctx ssh.Context) error {
if err := c.setInfo(ctx); err != nil {
c.logf("failed to get conninfo: %v", err)
return errDenied
}
a, localUser, err := c.evaluatePolicy(pubKey)
a, localUser, err := c.evaluatePolicy()
if err != nil {
if pubKey == nil && c.havePubKeyPolicy() {
return errPubKeyRequired
}
return fmt.Errorf("%w: %v", errDenied, err)
}
c.action0 = a
c.currentAction = a
c.pubKey = pubKey
if a.Message != "" {
if err := ctx.SendAuthBanner(a.Message); err != nil {
return fmt.Errorf("SendBanner: %w", err)
@@ -446,7 +409,6 @@ func (srv *server) newConn() (*conn, error) {
ServerConfigCallback: c.ServerConfig,
NoClientAuthHandler: c.NoClientAuthCallback,
PublicKeyHandler: c.PublicKeyHandler,
PasswordHandler: c.fakePasswordHandler,
Handler: c.handleSessionPostSSHAuth,
@@ -514,34 +476,6 @@ func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, de
return false
}
// havePubKeyPolicy reports whether any policy rule may provide access by means
// of a ssh.PublicKey.
func (c *conn) havePubKeyPolicy() bool {
if c.info == nil {
panic("havePubKeyPolicy called before setInfo")
}
// Is there any rule that looks like it'd require a public key for this
// sshUser?
pol, ok := c.sshPolicy()
if !ok {
return false
}
for _, r := range pol.Rules {
if c.ruleExpired(r) {
continue
}
if mapLocalUser(r.SSHUsers, c.info.sshUser) == "" {
continue
}
for _, p := range r.Principals {
if len(p.PubKeys) > 0 && c.principalMatchesTailscaleIdentity(p) {
return true
}
}
}
return false
}
// sshPolicy returns the SSHPolicy for current node.
// If there is no SSHPolicy in the netmap, it returns a debugPolicy
// if one is defined.
@@ -618,117 +552,19 @@ func (c *conn) setInfo(ctx ssh.Context) error {
}
// evaluatePolicy returns the SSHAction and localUser after evaluating
// the SSHPolicy for this conn. The pubKey may be nil for "none" auth.
func (c *conn) evaluatePolicy(pubKey gossh.PublicKey) (_ *tailcfg.SSHAction, localUser string, _ error) {
// the SSHPolicy for this conn.
func (c *conn) evaluatePolicy() (_ *tailcfg.SSHAction, localUser string, _ error) {
pol, ok := c.sshPolicy()
if !ok {
return nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
}
a, localUser, ok := c.evalSSHPolicy(pol, pubKey)
a, localUser, ok := c.evalSSHPolicy(pol)
if !ok {
return nil, "", fmt.Errorf("tailssh: rejecting connection; no matching policy")
}
return a, localUser, nil
}
// pubKeyCacheEntry is the cache value for an HTTPS URL of public keys (like
// "https://github.com/foo.keys")
type pubKeyCacheEntry struct {
lines []string
etag string // if sent by server
at time.Time
}
const (
pubKeyCacheDuration = time.Minute // how long to cache non-empty public keys
pubKeyCacheEmptyDuration = 15 * time.Second // how long to cache empty responses
)
func (srv *server) fetchPublicKeysURLCached(url string) (ce pubKeyCacheEntry, ok bool) {
srv.mu.Lock()
defer srv.mu.Unlock()
// Mostly don't care about the size of this cache. Clean rarely.
if m := srv.fetchPublicKeysCache; len(m) > 50 {
tooOld := srv.now().Add(pubKeyCacheDuration * 10)
for k, ce := range m {
if ce.at.Before(tooOld) {
delete(m, k)
}
}
}
ce, ok = srv.fetchPublicKeysCache[url]
if !ok {
return ce, false
}
maxAge := pubKeyCacheDuration
if len(ce.lines) == 0 {
maxAge = pubKeyCacheEmptyDuration
}
return ce, srv.now().Sub(ce.at) < maxAge
}
func (srv *server) pubKeyClient() *http.Client {
if srv.pubKeyHTTPClient != nil {
return srv.pubKeyHTTPClient
}
return http.DefaultClient
}
// fetchPublicKeysURL fetches the public keys from a URL. The strings are in the
// the typical public key "type base64-string [comment]" format seen at e.g.
// https://github.com/USER.keys
func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
if !strings.HasPrefix(url, "https://") {
return nil, errors.New("invalid URL scheme")
}
ce, ok := srv.fetchPublicKeysURLCached(url)
if ok {
return ce.lines, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if ce.etag != "" {
req.Header.Add("If-None-Match", ce.etag)
}
res, err := srv.pubKeyClient().Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var lines []string
var etag string
switch res.StatusCode {
default:
err = fmt.Errorf("unexpected status %v", res.Status)
srv.logf("fetching public keys from %s: %v", url, err)
case http.StatusNotModified:
lines = ce.lines
etag = ce.etag
case http.StatusOK:
var all []byte
all, err = io.ReadAll(io.LimitReader(res.Body, 4<<10))
if s := strings.TrimSpace(string(all)); s != "" {
lines = strings.Split(s, "\n")
}
etag = res.Header.Get("Etag")
}
srv.mu.Lock()
defer srv.mu.Unlock()
mak.Set(&srv.fetchPublicKeysCache, url, pubKeyCacheEntry{
at: srv.now(),
lines: lines,
etag: etag,
})
return lines, err
}
// handleSessionPostSSHAuth runs an SSH session after the SSH-level authentication,
// but not necessarily before all the Tailscale-level extra verification has
// completed. It also handles SFTP requests.
@@ -830,18 +666,6 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string {
).Replace(actionURL)
}
func (c *conn) expandPublicKeyURL(pubKeyURL string) string {
if !strings.Contains(pubKeyURL, "$") {
return pubKeyURL
}
loginName := c.info.uprof.LoginName
localPart, _, _ := strings.Cut(loginName, "@")
return strings.NewReplacer(
"$LOGINNAME_EMAIL", loginName,
"$LOGINNAME_LOCALPART", localPart,
).Replace(pubKeyURL)
}
// sshSession is an accepted Tailscale SSH session.
type sshSession struct {
ssh.Session
@@ -892,7 +716,7 @@ func (c *conn) newSSHSession(s ssh.Session) *sshSession {
// isStillValid reports whether the conn is still valid.
func (c *conn) isStillValid() bool {
a, localUser, err := c.evaluatePolicy(c.pubKey)
a, localUser, err := c.evaluatePolicy()
c.vlogf("stillValid: %+v %v %v", a, localUser, err)
if err != nil {
return false
@@ -1275,9 +1099,9 @@ func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
return r.RuleExpires.Before(c.srv.now())
}
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy) (a *tailcfg.SSHAction, localUser string, ok bool) {
for _, r := range pol.Rules {
if a, localUser, err := c.matchRule(r, pubKey); err == nil {
if a, localUser, err := c.matchRule(r); err == nil {
return a, localUser, true
}
}
@@ -1294,7 +1118,7 @@ var (
errInvalidConn = errors.New("invalid connection state")
)
func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, err error) {
func (c *conn) matchRule(r *tailcfg.SSHRule) (a *tailcfg.SSHAction, localUser string, err error) {
defer func() {
c.vlogf("matchRule(%+v): %v", r, err)
}()
@@ -1324,7 +1148,7 @@ func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg
return nil, "", errUserMatch
}
}
if ok, err := c.anyPrincipalMatches(r.Principals, pubKey); err != nil {
if ok, err := c.anyPrincipalMatches(r.Principals); err != nil {
return nil, "", err
} else if !ok {
return nil, "", errPrincipalMatch
@@ -1343,30 +1167,20 @@ func mapLocalUser(ruleSSHUsers map[string]string, reqSSHUser string) (localUser
return v
}
func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal) (bool, error) {
for _, p := range ps {
if p == nil {
continue
}
if ok, err := c.principalMatches(p, pubKey); err != nil {
return false, err
} else if ok {
if c.principalMatchesTailscaleIdentity(p) {
return true, nil
}
}
return false, nil
}
func (c *conn) principalMatches(p *tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
if !c.principalMatchesTailscaleIdentity(p) {
return false, nil
}
return c.principalMatchesPubKey(p, pubKey)
}
// principalMatchesTailscaleIdentity reports whether one of p's four fields
// that match the Tailscale identity match (Node, NodeIP, UserLogin, Any).
// This function does not consider PubKeys.
func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
ci := c.info
if p.Any {
@@ -1386,42 +1200,6 @@ func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
return false
}
func (c *conn) principalMatchesPubKey(p *tailcfg.SSHPrincipal, clientPubKey gossh.PublicKey) (bool, error) {
if len(p.PubKeys) == 0 {
return true, nil
}
if clientPubKey == nil {
return false, nil
}
knownKeys := p.PubKeys
if len(knownKeys) == 1 && strings.HasPrefix(knownKeys[0], "https://") {
var err error
knownKeys, err = c.srv.fetchPublicKeysURL(c.expandPublicKeyURL(knownKeys[0]))
if err != nil {
return false, err
}
}
for _, knownKey := range knownKeys {
if pubKeyMatchesAuthorizedKey(clientPubKey, knownKey) {
return true, nil
}
}
return false, nil
}
func pubKeyMatchesAuthorizedKey(pubKey ssh.PublicKey, wantKey string) bool {
wantKeyType, rest, ok := strings.Cut(wantKey, " ")
if !ok {
return false
}
if pubKey.Type() != wantKeyType {
return false
}
wantKeyB64, _, _ := strings.Cut(rest, " ")
wantKeyData, _ := base64.StdEncoding.DecodeString(wantKeyB64)
return len(wantKeyData) > 0 && bytes.Equal(pubKey.Marshal(), wantKeyData)
}
func randBytes(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
@@ -1918,7 +1696,6 @@ func envEq(a, b string) bool {
var (
metricActiveSessions = clientmetric.NewGauge("ssh_active_sessions")
metricIncomingConnections = clientmetric.NewCounter("ssh_incoming_connections")
metricPublicKeyAccepts = clientmetric.NewCounter("ssh_publickey_accepts") // accepted subset of ssh_publickey_connections
metricTerminalAccept = clientmetric.NewCounter("ssh_terminalaction_accept")
metricTerminalReject = clientmetric.NewCounter("ssh_terminalaction_reject")
metricTerminalMalformed = clientmetric.NewCounter("ssh_terminalaction_malformed")

View File

@@ -10,7 +10,6 @@ import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -209,7 +208,7 @@ func TestMatchRule(t *testing.T) {
info: tt.ci,
srv: &server{logf: t.Logf},
}
got, gotUser, err := c.matchRule(tt.rule, nil)
got, gotUser, err := c.matchRule(tt.rule)
if err != tt.wantErr {
t.Errorf("err = %v; want %v", err, tt.wantErr)
}
@@ -990,89 +989,6 @@ func parseEnv(out []byte) map[string]string {
return e
}
func TestPublicKeyFetching(t *testing.T) {
var reqsTotal, reqsIfNoneMatchHit, reqsIfNoneMatchMiss int32
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32((&reqsTotal), 1)
etag := fmt.Sprintf("W/%q", sha256.Sum256([]byte(r.URL.Path)))
w.Header().Set("Etag", etag)
if v := r.Header.Get("If-None-Match"); v != "" {
if v == etag {
atomic.AddInt32(&reqsIfNoneMatchHit, 1)
w.WriteHeader(304)
return
}
atomic.AddInt32(&reqsIfNoneMatchMiss, 1)
}
io.WriteString(w, "foo\nbar\n"+string(r.URL.Path)+"\n")
}))
ts.StartTLS()
defer ts.Close()
keys := ts.URL
clock := &tstest.Clock{}
srv := &server{
pubKeyHTTPClient: ts.Client(),
timeNow: clock.Now,
}
for range 2 {
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
if err != nil {
t.Fatal(err)
}
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}
if got, want := atomic.LoadInt32(&reqsTotal), int32(1); got != want {
t.Errorf("got %d requests; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(0); got != want {
t.Errorf("got %d etag hits; want %d", got, want)
}
clock.Advance(5 * time.Minute)
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
if err != nil {
t.Fatal(err)
}
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q; want %q", got, want)
}
if got, want := atomic.LoadInt32(&reqsTotal), int32(2); got != want {
t.Errorf("got %d requests; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(1); got != want {
t.Errorf("got %d etag hits; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchMiss), int32(0); got != want {
t.Errorf("got %d etag misses; want %d", got, want)
}
}
func TestExpandPublicKeyURL(t *testing.T) {
c := &conn{
info: &sshConnInfo{
uprof: tailcfg.UserProfile{
LoginName: "bar@baz.tld",
},
},
}
if got, want := c.expandPublicKeyURL("foo"), "foo"; got != want {
t.Errorf("basic: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/$LOGINNAME_LOCALPART.keys"), "https://example.com/bar.keys"; got != want {
t.Errorf("localpart: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email=bar@baz.tld"; got != want {
t.Errorf("email: got %q; want %q", got, want)
}
c.info = new(sshConnInfo)
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email="; got != want {
t.Errorf("on empty: got %q; want %q", got, want)
}
}
func TestAcceptEnvPair(t *testing.T) {
tests := []struct {
in string

View File

@@ -1375,6 +1375,12 @@ const (
// PeerCapabilityTaildriveSharer indicates that a peer has the ability to
// share folders with us.
PeerCapabilityTaildriveSharer PeerCapability = "tailscale.com/cap/drive-sharer"
// PeerCapabilityKubernetes grants a peer Kubernetes-specific
// capabilities, such as the ability to impersonate specific Tailscale
// user groups as Kubernetes user groups. This capability is read by
// peers that are Tailscale Kubernetes operator instances.
PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes"
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for
@@ -2397,17 +2403,6 @@ type SSHPrincipal struct {
UserLogin string `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
Any bool `json:"any,omitempty"` // if true, match any connection
// TODO(bradfitz): add StableUserID, once that exists
// PubKeys, if non-empty, means that this SSHPrincipal only
// matches if one of these public keys is presented by the user.
//
// As a special case, if len(PubKeys) == 1 and PubKeys[0] starts
// with "https://", then it's fetched (like https://github.com/username.keys).
// In that case, the following variable expansions are also supported
// in the URL:
// * $LOGINNAME_EMAIL ("foo@bar.com" or "foo@github")
// * $LOGINNAME_LOCALPART (the "foo" from either of the above)
PubKeys []string `json:"pubKeys,omitempty"`
}
// SSHAction is how to handle an incoming connection.

View File

@@ -529,7 +529,6 @@ func (src *SSHPrincipal) Clone() *SSHPrincipal {
}
dst := new(SSHPrincipal)
*dst = *src
dst.PubKeys = append(src.PubKeys[:0:0], src.PubKeys...)
return dst
}
@@ -539,7 +538,6 @@ var _SSHPrincipalCloneNeedsRegeneration = SSHPrincipal(struct {
NodeIP string
UserLogin string
Any bool
PubKeys []string
}{})
// Clone makes a deep copy of ControlDialPlan.

View File

@@ -1258,11 +1258,10 @@ func (v *SSHPrincipalView) UnmarshalJSON(b []byte) error {
return nil
}
func (v SSHPrincipalView) Node() StableNodeID { return v.ж.Node }
func (v SSHPrincipalView) NodeIP() string { return v.ж.NodeIP }
func (v SSHPrincipalView) UserLogin() string { return v.ж.UserLogin }
func (v SSHPrincipalView) Any() bool { return v.ж.Any }
func (v SSHPrincipalView) PubKeys() views.Slice[string] { return views.SliceOf(v.ж.PubKeys) }
func (v SSHPrincipalView) Node() StableNodeID { return v.ж.Node }
func (v SSHPrincipalView) NodeIP() string { return v.ж.NodeIP }
func (v SSHPrincipalView) UserLogin() string { return v.ж.UserLogin }
func (v SSHPrincipalView) Any() bool { return v.ж.Any }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHPrincipalViewNeedsRegeneration = SSHPrincipal(struct {
@@ -1270,7 +1269,6 @@ var _SSHPrincipalViewNeedsRegeneration = SSHPrincipal(struct {
NodeIP string
UserLogin string
Any bool
PubKeys []string
}{})
// View returns a readonly view of ControlDialPlan.

View File

@@ -1146,7 +1146,7 @@ func resolveListenAddr(network, addr string) (netip.AddrPort, error) {
return netip.AddrPortFrom(bindHostOrZero, uint16(port)), nil
}
func (s *Server) listen(network, addr string, lnOn listenOn) (*listener, error) {
func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, error) {
switch network {
case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
default:

View File

@@ -348,7 +348,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
tunName, _ := conf.Tun.Name()
conf.Dialer.SetTUNName(tunName)
conf.Dialer.SetNetMon(e.netMon)
e.dns = dns.NewManager(logf, conf.DNS, e.health, conf.Dialer, fwdDNSLinkSelector{e, tunName}, conf.ControlKnobs)
e.dns = dns.NewManager(logf, conf.DNS, e.health, conf.Dialer, fwdDNSLinkSelector{e, tunName}, conf.ControlKnobs, runtime.GOOS)
// TODO: there's probably a better place for this
sockstats.SetNetMon(e.netMon)