Compare commits
62 Commits
andrew/wor
...
bradfitz/j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f845922bf | ||
|
|
d2fef01206 | ||
|
|
9df107f4f0 | ||
|
|
e181f12a7b | ||
|
|
c4b20c5411 | ||
|
|
01a7726cf7 | ||
|
|
309afa53cf | ||
|
|
42f01afe26 | ||
|
|
59936e6d4a | ||
|
|
732af2f6e0 | ||
|
|
458decdeb0 | ||
|
|
4e5ef5b628 | ||
|
|
012933635b | ||
|
|
da32468988 | ||
|
|
ddf94a7b39 | ||
|
|
b56058d7e3 | ||
|
|
d780755340 | ||
|
|
489b990240 | ||
|
|
d15250aae9 | ||
|
|
8965e87fa8 | ||
|
|
114d1caf55 | ||
|
|
b565a9faa7 | ||
|
|
781f79408d | ||
|
|
4651827f20 | ||
|
|
8f7588900a | ||
|
|
0bb82561ba | ||
|
|
2064dc20d4 | ||
|
|
23c5870bd3 | ||
|
|
18939df0a7 | ||
|
|
1d6ab9f9db | ||
|
|
210264f942 | ||
|
|
6b801a8e9e | ||
|
|
b3f91845dc | ||
|
|
46fda6bf4c | ||
|
|
9766f0e110 | ||
|
|
94defc4056 | ||
|
|
b292f7f9ac | ||
|
|
5f177090e3 | ||
|
|
0323dd01b2 | ||
|
|
8487fd2ec2 | ||
|
|
a6b13e6972 | ||
|
|
75254178a0 | ||
|
|
787ead835f | ||
|
|
6e55d8f6a1 | ||
|
|
30f8d8199a | ||
|
|
da078b4c09 | ||
|
|
53a5d00fff | ||
|
|
8161024176 | ||
|
|
a475c435ec | ||
|
|
27033c6277 | ||
|
|
d5e692f7e7 | ||
|
|
94415e8029 | ||
|
|
3485e4bf5a | ||
|
|
7eb8a77ac8 | ||
|
|
24a40f54d9 | ||
|
|
d91e5c25ce | ||
|
|
ded7734c36 | ||
|
|
200d92121f | ||
|
|
7dd76c3411 | ||
|
|
591979b95f | ||
|
|
91786ff958 | ||
|
|
5ffb2668ef |
10
.github/workflows/checklocks.yml
vendored
10
.github/workflows/checklocks.yml
vendored
@@ -24,5 +24,11 @@ jobs:
|
||||
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks
|
||||
|
||||
- name: Run checklocks vet
|
||||
# TODO: remove || true once we have applied checklocks annotations everywhere.
|
||||
run: ./tool/go vet -vettool=/tmp/checklocks ./... || true
|
||||
# TODO(#12625): add more packages as we add annotations
|
||||
run: |-
|
||||
./tool/go vet -vettool=/tmp/checklocks \
|
||||
./envknob \
|
||||
./ipn/store/mem \
|
||||
./net/stun/stuntest \
|
||||
./net/wsconn \
|
||||
./proxymap
|
||||
|
||||
5
.github/workflows/installer.yml
vendored
5
.github/workflows/installer.yml
vendored
@@ -67,6 +67,11 @@ jobs:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
steps:
|
||||
- name: install dependencies (pacman)
|
||||
# Refresh the package databases to ensure that the tailscale package is
|
||||
# defined.
|
||||
run: pacman -Sy
|
||||
if: contains(matrix.image, 'archlinux')
|
||||
- name: install dependencies (yum)
|
||||
# tar and gzip are needed by the actions/checkout below.
|
||||
run: yum install -y --allowerasing tar gzip ${{ matrix.deps }}
|
||||
|
||||
@@ -442,8 +442,10 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
if len(toAdvertise) > 0 {
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -476,18 +476,20 @@ runLoop:
|
||||
newCurentEgressIPs = deephash.Hash(&egressAddrs)
|
||||
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
|
||||
if egressIPsHaveChanged && len(egressAddrs) != 0 {
|
||||
var rulesInstalled bool
|
||||
for _, egressAddr := range egressAddrs {
|
||||
ea := egressAddr.Addr()
|
||||
// TODO (irbekrm): make it work for IPv6 too.
|
||||
if ea.Is6() {
|
||||
log.Println("Not installing egress forwarding rules for IPv6 as this is currently not supported")
|
||||
continue
|
||||
}
|
||||
log.Printf("Installing forwarding rules for destination %v", ea.String())
|
||||
if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil {
|
||||
log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err)
|
||||
if ea.Is4() || (ea.Is6() && nfr.HasIPV6NAT()) {
|
||||
rulesInstalled = true
|
||||
log.Printf("Installing forwarding rules for destination %v", ea.String())
|
||||
if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil {
|
||||
log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !rulesInstalled {
|
||||
log.Fatalf("no forwarding rules for egress addresses %v, host supports IPv6: %v", egressAddrs, nfr.HasIPV6NAT())
|
||||
}
|
||||
}
|
||||
currentEgressIPs = newCurentEgressIPs
|
||||
}
|
||||
@@ -941,7 +943,7 @@ func enableIPForwarding(v4Forwarding, v6Forwarding bool, root string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
func installEgressForwardingRule(_ context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
}
|
||||
defer kube.Close()
|
||||
|
||||
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: func(s string) *string { return &s }("foo"), Version: "alpha0"}
|
||||
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: ptr.To("foo"), Version: "alpha0"}
|
||||
tailscaledConfBytes, err := json.Marshal(tailscaledConf)
|
||||
if err != nil {
|
||||
t.Fatalf("error unmarshaling tailscaled config: %v", err)
|
||||
@@ -116,6 +116,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
// WantFiles files that should exist in the container and their
|
||||
// contents.
|
||||
WantFiles map[string]string
|
||||
// WantFatalLog is the fatal log message we expect from containerboot.
|
||||
// If set for a phase, the test will finish on that phase.
|
||||
WantFatalLog string
|
||||
}
|
||||
runningNotify := &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
@@ -349,12 +352,57 @@ func TestContainerBoot(t *testing.T) {
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "egress_proxy_fqdn_ipv6_target_on_ipv4_host",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net", // resolves to IPv6 address
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_TEST_FAKE_NETFILTER_6": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
NetMap: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("myID"),
|
||||
Name: "test-node.test.ts.net",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("ipv6ID"),
|
||||
Name: "ipv6-node.test.ts.net",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("::1/128")},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
WantFatalLog: "no forwarding rules for egress addresses [::1/128], host supports IPv6: false",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authkey_once",
|
||||
Env: map[string]string{
|
||||
@@ -697,6 +745,25 @@ func TestContainerBoot(t *testing.T) {
|
||||
var wantCmds []string
|
||||
for i, p := range test.Phases {
|
||||
lapi.Notify(p.Notify)
|
||||
if p.WantFatalLog != "" {
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
state, err := cmd.Process.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.ExitCode() != 1 {
|
||||
return fmt.Errorf("process exited with code %d but wanted %d", state.ExitCode(), 1)
|
||||
}
|
||||
waitLogLine(t, time.Second, cbOut, p.WantFatalLog)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Early test return, we don't expect the successful startup log message.
|
||||
return
|
||||
}
|
||||
wantCmds = append(wantCmds, p.WantCmds...)
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
This is the code for the [Tailscale DERP server](https://tailscale.com/kb/1232/derp-servers).
|
||||
|
||||
In general, you should not need to nor want to run this code. The overwhelming majority of Tailscale users (both individuals and companies) do not.
|
||||
In general, you should not need to or want to run this code. The overwhelming
|
||||
majority of Tailscale users (both individuals and companies) do not.
|
||||
|
||||
In the happy path, Tailscale establishes direct connections between peers and
|
||||
data plane traffic flows directly between them, without using DERP for more than
|
||||
@@ -11,7 +12,7 @@ find yourself wanting DERP for more bandwidth, the real problem is usually the
|
||||
network configuration of your Tailscale node(s), making sure that Tailscale can
|
||||
get direction connections via some mechanism.
|
||||
|
||||
But if you've decided or been advised to run your own `derper`, then read on.
|
||||
If you've decided or been advised to run your own `derper`, then read on.
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -28,7 +29,10 @@ But if you've decided or been advised to run your own `derper`, then read on.
|
||||
|
||||
* You must build and update the `cmd/derper` binary yourself. There are no
|
||||
packages. Use `go install tailscale.com/cmd/derper@latest` with the latest
|
||||
version of Go.
|
||||
version of Go. You should update this binary approximately as regularly as
|
||||
you update Tailscale nodes. If using `--verify-clients`, the `derper` binary
|
||||
and `tailscaled` binary on the machine must be built from the same git revision.
|
||||
(It might work otherwise, but they're developed and only tested together.)
|
||||
|
||||
* The DERP protocol does a protocol switch inside TLS from HTTP to a custom
|
||||
bidirectional binary protocol. It is thus incompatible with many HTTP proxies.
|
||||
@@ -55,7 +59,7 @@ rely on its DNS which might be broken and dependent on DERP to get back up.
|
||||
* Monitor your DERP servers with [`cmd/derpprobe`](../derpprobe/).
|
||||
|
||||
* If using `--verify-clients`, a `tailscaled` must be running alongside the
|
||||
`derper`.
|
||||
`derper`, and all clients must be visible to the derper tailscaled in the ACL.
|
||||
|
||||
* If using `--verify-clients`, a `tailscaled` must also be running alongside
|
||||
your `derpprobe`, and `derpprobe` needs to use `--derp-map=local`.
|
||||
@@ -72,3 +76,34 @@ rely on its DNS which might be broken and dependent on DERP to get back up.
|
||||
* Don't rate-limit UDP STUN packets.
|
||||
|
||||
* Don't rate-limit outbound TCP traffic (only inbound).
|
||||
|
||||
## Diagnostics
|
||||
|
||||
This is not a complete guide on DERP diagnostics.
|
||||
|
||||
Running your own DERP services requires exeprtise in multi-layer network and
|
||||
application diagnostics. As the DERP runs multiple protocols at multiple layers
|
||||
and is not a regular HTTP(s) server you will need expertise in correlative
|
||||
analysis to diagnose the most tricky problems. There is no "plain text" or
|
||||
"open" mode of operation for DERP.
|
||||
|
||||
* The debug handler is accessible at URL path `/debug/`. It is only accessible
|
||||
over localhost or from a Tailscale IP address.
|
||||
|
||||
* Go pprof can be accessed via the debug handler at `/debug/pprof/`
|
||||
|
||||
* Prometheus compatible metrics can be gathered from the debug handler at
|
||||
`/debug/varz`.
|
||||
|
||||
* `cmd/stunc` in the Tailscale repository provides a basic tool for diagnosing
|
||||
issues with STUN.
|
||||
|
||||
* `cmd/derpprobe` provides a service for monitoring DERP cluster health.
|
||||
|
||||
* `tailscale debug derp` and `tailscale netcheck` provide additional client
|
||||
driven diagnostic information for DERP communications.
|
||||
|
||||
* Tailscale logs may provide insight for certain problems, such as if DERPs are
|
||||
unreachable or peers are regularly not reachable in their DERP home regions.
|
||||
There are many possible misconfiguration causes for these problems, but
|
||||
regular log entries are a good first indicator that there is a problem.
|
||||
|
||||
@@ -10,6 +10,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
@@ -99,7 +105,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/net/stunserver
|
||||
@@ -114,7 +120,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/derp
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The derper binary is a simple DERP server.
|
||||
//
|
||||
// For more information, see:
|
||||
//
|
||||
// - About: https://tailscale.com/kb/1232/derp-servers
|
||||
// - Protocol & Go docs: https://pkg.go.dev/tailscale.com/derp
|
||||
// - Running a DERP server: https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp
|
||||
package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
@@ -22,6 +28,9 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
runtimemetrics "runtime/metrics"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -206,11 +215,16 @@ func main() {
|
||||
io.WriteString(w, `<html><body>
|
||||
<h1>DERP</h1>
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://tailscale.com/">Tailscale</a>
|
||||
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
|
||||
server.
|
||||
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
|
||||
</p>
|
||||
<p>
|
||||
Documentation:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
|
||||
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
|
||||
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
|
||||
</ul>
|
||||
`)
|
||||
if !*runDERP {
|
||||
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
|
||||
@@ -236,6 +250,20 @@ func main() {
|
||||
}
|
||||
}))
|
||||
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
|
||||
debug.Handle("set-mutex-profile-fraction", "SetMutexProfileFraction", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s := r.FormValue("rate")
|
||||
if s == "" || r.Header.Get("Sec-Debug") != "derp" {
|
||||
http.Error(w, "To set, use: curl -HSec-Debug:derp 'http://derp/debug/set-mutex-profile-fraction?rate=100'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
http.Error(w, "bad rate value", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
old := runtime.SetMutexProfileFraction(v)
|
||||
fmt.Fprintf(w, "mutex changed from %v to %v\n", old, v)
|
||||
}))
|
||||
|
||||
// Longer lived DERP connections send an application layer keepalive. Note
|
||||
// if the keepalive is hit, the user timeout will take precedence over the
|
||||
@@ -452,3 +480,16 @@ func (l *rateLimitedListener) Accept() (net.Conn, error) {
|
||||
l.numAccepts.Add(1)
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
expvar.Publish("go_sync_mutex_wait_seconds", expvar.Func(func() any {
|
||||
const name = "/sync/mutex/wait/total:seconds" // Go 1.20+
|
||||
var s [1]runtimemetrics.Sample
|
||||
s[0].Name = name
|
||||
runtimemetrics.Read(s[:])
|
||||
if v := s[0].Value; v.Kind() == runtimemetrics.KindFloat64 {
|
||||
return v.Float64()
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -71,8 +69,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
|
||||
add := func(m derp.PeerPresentMessage) { s.AddPacketForwarder(m.Key, c) }
|
||||
remove := func(m derp.PeerGoneMessage) { s.RemovePacketForwarder(m.Peer, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,6 +294,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
|
||||
Selector: map[string]string{
|
||||
"app": sts.ParentResourceUID,
|
||||
},
|
||||
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||
},
|
||||
}
|
||||
logger.Debugf("reconciling headless service for StatefulSet")
|
||||
|
||||
@@ -319,7 +319,8 @@ func expectedHeadlessService(name string, parentType string) *corev1.Service {
|
||||
Selector: map[string]string{
|
||||
"app": "1234-UID",
|
||||
},
|
||||
ClusterIP: "None",
|
||||
ClusterIP: "None",
|
||||
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/google/uuid from tailscale.com/util/fastuuid
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
|
||||
@@ -59,7 +65,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/tsweb
|
||||
tailscale.com/types/opt from tailscale.com/envknob+
|
||||
tailscale.com/types/ptr from tailscale.com/tailcfg
|
||||
tailscale.com/types/ptr from tailscale.com/tailcfg+
|
||||
tailscale.com/types/structs from tailscale.com/tailcfg+
|
||||
tailscale.com/types/tkatype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/views from tailscale.com/net/tsaddr+
|
||||
@@ -128,6 +134,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from github.com/go-json-experiment/json
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// 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"`
|
||||
DstPort int `json:"dstPort"`
|
||||
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", "dst_port", "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.DstPort, &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
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,8 @@ import (
|
||||
|
||||
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")
|
||||
flagDstPorts = flag.String("dst-ports", "", "comma-separated list of destination ports to monitor")
|
||||
@@ -63,10 +60,13 @@ func getDERPMap(ctx context.Context, url string) (*tailcfg.DERPMap, error) {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("non-200 derp map resp: %d", resp.StatusCode)
|
||||
}
|
||||
dm := tailcfg.DERPMap{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&dm)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("failed to decode derp map resp: %v", err)
|
||||
}
|
||||
return &dm, nil
|
||||
}
|
||||
@@ -639,15 +639,9 @@ func main() {
|
||||
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.Fatal("retention must be >= interval")
|
||||
}
|
||||
if len(*flagRemoteWriteURL) < 1 {
|
||||
log.Fatal("rw-url flag is unset")
|
||||
}
|
||||
@@ -693,49 +687,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
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, dst_port 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)
|
||||
@@ -745,9 +696,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
}()
|
||||
|
||||
shutdown := func() {
|
||||
if httpServer != nil {
|
||||
httpServer.Close()
|
||||
}
|
||||
close(tsCh)
|
||||
select {
|
||||
case <-time.After(time.Second * 10): // give goroutine some time to flush
|
||||
@@ -766,7 +714,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
cancel()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -787,20 +734,9 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
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, dstPorts)
|
||||
if err != nil {
|
||||
@@ -819,32 +755,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
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.key.meta.addr.Is6() {
|
||||
af = 6
|
||||
}
|
||||
_, err = tx.Exec("INSERT INTO rtt(at_unix, region_id, hostname, af, address, timestamp_source, stable_conn, dst_port, rtt_ns) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
result.at.Unix(), result.key.meta.regionID, result.key.meta.hostname, af, result.key.meta.addr.String(), result.key.timestampSource, result.key.connStability, result.key.dstPort, 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 {
|
||||
@@ -874,10 +784,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
dmCh <- updatedDM
|
||||
}
|
||||
}()
|
||||
case err := <-httpErrCh:
|
||||
log.Printf("http server error: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
case <-sigCh:
|
||||
shutdown()
|
||||
return
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/envknob"
|
||||
@@ -136,6 +137,7 @@ func runExitNodeList(ctx context.Context, args []string) error {
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "# To view the complete list of exit nodes for a country, use `tailscale exit-node list --filter=` followed by the country name.")
|
||||
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP.")
|
||||
if hasAnyExitNodeSuggestions(peers) {
|
||||
fmt.Fprintln(w, "# To have Tailscale suggest an exit node, use `tailscale exit-node suggest`.")
|
||||
@@ -154,7 +156,8 @@ func runExitNodeSuggest(ctx context.Context, args []string) error {
|
||||
fmt.Println("No exit node suggestion is available.")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", res.Name, res.ID)
|
||||
hostname := strings.TrimSuffix(res.Name, ".")
|
||||
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", hostname, shellquote.Join(hostname))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -229,7 +232,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
for _, ps := range peers {
|
||||
loc := cmp.Or(ps.Location, noLocation)
|
||||
|
||||
if filterBy != "" && loc.Country != filterBy {
|
||||
if filterBy != "" && !strings.EqualFold(loc.Country, filterBy) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -269,9 +272,14 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
countryAnyPeer = append(countryAnyPeer, city.Peers...)
|
||||
var reducedCityPeers []*ipnstate.PeerStatus
|
||||
for i, peer := range city.Peers {
|
||||
if filterBy != "" {
|
||||
// If the peers are being filtered, we return all peers to the user.
|
||||
reducedCityPeers = append(reducedCityPeers, city.Peers...)
|
||||
break
|
||||
}
|
||||
// If the peers are not being filtered, we only return the highest priority peer and any peer that
|
||||
// is currently the active exit node.
|
||||
if i == 0 || peer.ExitNode {
|
||||
// We only return the highest priority peer and any peer that
|
||||
// is currently the active exit node.
|
||||
reducedCityPeers = append(reducedCityPeers, peer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestFilterFormatAndSortExitNodes(t *testing.T) {
|
||||
{
|
||||
Name: "Rainier",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[2],
|
||||
ps[2], ps[3],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
@@ -89,7 +89,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -117,7 +117,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -131,7 +131,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -157,10 +157,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/abc": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -171,7 +171,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -182,7 +182,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
@@ -236,7 +236,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -247,10 +247,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/abc": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -261,7 +261,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -272,7 +272,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
@@ -361,7 +361,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -372,10 +372,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -439,7 +439,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TCPForward: "localhost:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -466,7 +466,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:123",
|
||||
TCPForward: "localhost:123",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -560,7 +560,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -572,7 +572,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -584,10 +584,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -599,10 +599,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -614,10 +614,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -628,7 +628,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -636,10 +636,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
{ // start a tcp forwarder on 8443
|
||||
command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "localhost:5432"}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -647,7 +647,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
{ // remove primary port http handler
|
||||
command: cmd("serve off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "localhost:5432"}},
|
||||
},
|
||||
},
|
||||
{ // remove tcp forwarder
|
||||
@@ -717,7 +717,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TCPForward: "localhost:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -738,7 +738,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -758,7 +758,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -769,8 +769,8 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/bar": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
"/bar": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -800,7 +800,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{3000: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:3000": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,6 +9,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
@@ -103,7 +109,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/capture
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
@@ -121,7 +127,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
|
||||
|
||||
@@ -90,11 +90,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun+
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
@@ -303,7 +304,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
@@ -335,7 +336,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/control/controlclient+
|
||||
tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"tailscale.com/derp/xdp"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
flagDevice = flag.String("device", "", "target device name")
|
||||
flagDevice = flag.String("device", "", "target device name (default: autodetect)")
|
||||
flagPort = flag.Int("dst-port", 0, "destination UDP port to serve")
|
||||
flagVerbose = flag.Bool("verbose", false, "verbose output including verifier errors")
|
||||
flagMode = flag.String("mode", "xdp", "XDP mode; valid modes: [xdp, xdpgeneric, xdpdrv, xdpoffload]")
|
||||
@@ -41,8 +42,18 @@ func main() {
|
||||
default:
|
||||
log.Fatal("invalid mode")
|
||||
}
|
||||
deviceName := *flagDevice
|
||||
if deviceName == "" {
|
||||
var err error
|
||||
deviceName, _, err = netutil.DefaultInterfacePortable()
|
||||
if err != nil || deviceName == "" {
|
||||
log.Fatalf("failed to detect default route interface: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("binding to device: %s", deviceName)
|
||||
|
||||
server, err := xdp.NewSTUNServer(&xdp.STUNServerConfig{
|
||||
DeviceName: *flagDevice,
|
||||
DeviceName: deviceName,
|
||||
DstPort: *flagPort,
|
||||
AttachFlags: attachFlags,
|
||||
FullVerifierErr: *flagVerbose,
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -491,7 +489,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverLegacyKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf)
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
|
||||
@@ -588,18 +586,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
// We were given a wrapped pre-auth key, which means that in addition
|
||||
// to being a regular pre-auth key there was a suffix with information to
|
||||
// generate a tailnet-lock signature.
|
||||
nk, err := tryingNewKey.Public().MarshalBinary()
|
||||
nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())
|
||||
if err != nil {
|
||||
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
return false, "", nil, err
|
||||
}
|
||||
sig := &tka.NodeKeySignature{
|
||||
SigKind: tka.SigRotation,
|
||||
Pubkey: nk,
|
||||
Nested: wrappedSig,
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
|
||||
nodeKeySignature = sig.Serialize()
|
||||
}
|
||||
|
||||
if backendLogID == "" {
|
||||
@@ -1644,43 +1634,6 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
|
||||
// In all cases the authkey is returned, sans wrapping information if any.
|
||||
//
|
||||
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
|
||||
// and private key.
|
||||
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
|
||||
authKey, suffix, found := strings.Cut(key, "--TL")
|
||||
if !found {
|
||||
return key, false, nil, nil
|
||||
}
|
||||
sigBytes, privBytes, found := strings.Cut(suffix, "-")
|
||||
if !found {
|
||||
logf("decoding wrapped auth-key: did not find delimiter")
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: signature decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: priv decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
sig = new(tka.NodeKeySignature)
|
||||
if err := sig.Unserialize([]byte(rawSig)); err != nil {
|
||||
logf("decoding wrapped auth-key: signature: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
priv = ed25519.PrivateKey(rawPriv)
|
||||
|
||||
return authKey, true, sig, priv
|
||||
}
|
||||
|
||||
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||
if !nodeKey.IsZero() {
|
||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -147,42 +146,3 @@ func TestTsmpPing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeWrappedAuthkey(t *testing.T) {
|
||||
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
|
||||
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
||||
}
|
||||
if sig != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
||||
}
|
||||
if priv != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
|
||||
}
|
||||
|
||||
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
|
||||
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if !isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
||||
}
|
||||
|
||||
if sig == nil {
|
||||
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
|
||||
t.Error("signature failed to verify")
|
||||
}
|
||||
|
||||
// Make sure the private is correct by using it.
|
||||
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
|
||||
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
|
||||
t.Error("failed to use priv")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ type Knobs struct {
|
||||
// DisableUPnP indicates whether to attempt UPnP mapping.
|
||||
DisableUPnP atomic.Bool
|
||||
|
||||
// DisableDRPO is whether control says to disable the
|
||||
// DERP route optimization (Issue 150).
|
||||
DisableDRPO atomic.Bool
|
||||
|
||||
// KeepFullWGConfig is whether we should disable the lazy wireguard
|
||||
// programming and instead give WireGuard the full netmap always, even for
|
||||
// idle peers.
|
||||
@@ -110,7 +106,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
has := capMap.Contains
|
||||
var (
|
||||
keepFullWG = has(tailcfg.NodeAttrDebugDisableWGTrim)
|
||||
disableDRPO = has(tailcfg.NodeAttrDebugDisableDRPO)
|
||||
disableUPnP = has(tailcfg.NodeAttrDisableUPnP)
|
||||
randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort)
|
||||
disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates)
|
||||
@@ -136,7 +131,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
}
|
||||
|
||||
k.KeepFullWGConfig.Store(keepFullWG)
|
||||
k.DisableDRPO.Store(disableDRPO)
|
||||
k.DisableUPnP.Store(disableUPnP)
|
||||
k.RandomizeClientPort.Store(randomizeClientPort)
|
||||
k.OneCGNAT.Store(oneCGNAT)
|
||||
@@ -163,7 +157,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
}
|
||||
return map[string]any{
|
||||
"DisableUPnP": k.DisableUPnP.Load(),
|
||||
"DisableDRPO": k.DisableDRPO.Load(),
|
||||
"KeepFullWGConfig": k.KeepFullWGConfig.Load(),
|
||||
"RandomizeClientPort": k.RandomizeClientPort.Load(),
|
||||
"OneCGNAT": k.OneCGNAT.Load(),
|
||||
|
||||
31
derp/derp.go
31
derp/derp.go
@@ -83,9 +83,16 @@ const (
|
||||
// a bug).
|
||||
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
|
||||
|
||||
// framePeerPresent is like framePeerGone, but for other
|
||||
// members of the DERP region when they're meshed up together.
|
||||
framePeerPresent = frameType(0x09) // 32B pub key of peer that's connected + optional 18B ip:port (16 byte IP + 2 byte BE uint16 port)
|
||||
// framePeerPresent is like framePeerGone, but for other members of the DERP
|
||||
// region when they're meshed up together.
|
||||
//
|
||||
// The message is at least 32 bytes (the public key of the peer that's
|
||||
// connected). If there are at least 18 bytes remaining after that, it's the
|
||||
// 16 byte IP + 2 byte BE uint16 port of the client. If there's another byte
|
||||
// remaining after that, it's a PeerPresentFlags byte.
|
||||
// While current servers send 41 bytes, old servers will send fewer, and newer
|
||||
// servers might send more.
|
||||
framePeerPresent = frameType(0x09)
|
||||
|
||||
// frameWatchConns is how one DERP node in a regional mesh
|
||||
// subscribes to the others in the region.
|
||||
@@ -124,8 +131,22 @@ const (
|
||||
type PeerGoneReasonType byte
|
||||
|
||||
const (
|
||||
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
|
||||
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
|
||||
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
|
||||
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
|
||||
PeerGoneReasonMeshConnBroke = PeerGoneReasonType(0xf0) // invented by Client.RunWatchConnectionLoop on disconnect; not sent on the wire
|
||||
)
|
||||
|
||||
// PeerPresentFlags is an optional byte of bit flags sent after a framePeerPresent message.
|
||||
//
|
||||
// For a modern server, the value should always be non-zero. If the value is zero,
|
||||
// that means the server doesn't support this field.
|
||||
type PeerPresentFlags byte
|
||||
|
||||
// PeerPresentFlags bits.
|
||||
const (
|
||||
PeerPresentIsRegular = 1 << 0
|
||||
PeerPresentIsMeshPeer = 1 << 1
|
||||
PeerPresentIsProber = 1 << 2
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -368,6 +368,8 @@ type PeerPresentMessage struct {
|
||||
Key key.NodePublic
|
||||
// IPPort is the remote IP and port of the client.
|
||||
IPPort netip.AddrPort
|
||||
// Flags is a bitmask of info about the client.
|
||||
Flags PeerPresentFlags
|
||||
}
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
@@ -547,18 +549,33 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
if n < keyLen {
|
||||
remain := b
|
||||
chunk, remain, ok := cutLeadingN(remain, keyLen)
|
||||
if !ok {
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var msg PeerPresentMessage
|
||||
msg.Key = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
|
||||
if n >= keyLen+16+2 {
|
||||
msg.IPPort = netip.AddrPortFrom(
|
||||
netip.AddrFrom16([16]byte(b[keyLen:keyLen+16])).Unmap(),
|
||||
binary.BigEndian.Uint16(b[keyLen+16:keyLen+16+2]),
|
||||
)
|
||||
msg.Key = key.NodePublicFromRaw32(mem.B(chunk))
|
||||
|
||||
const ipLen = 16
|
||||
const portLen = 2
|
||||
chunk, remain, ok = cutLeadingN(remain, ipLen+portLen)
|
||||
if !ok {
|
||||
// Older server which didn't send the IP.
|
||||
return msg, nil
|
||||
}
|
||||
msg.IPPort = netip.AddrPortFrom(
|
||||
netip.AddrFrom16([16]byte(chunk[:ipLen])).Unmap(),
|
||||
binary.BigEndian.Uint16(chunk[ipLen:]),
|
||||
)
|
||||
|
||||
chunk, _, ok = cutLeadingN(remain, 1)
|
||||
if !ok {
|
||||
// Older server which doesn't send PeerPresentFlags.
|
||||
return msg, nil
|
||||
}
|
||||
msg.Flags = PeerPresentFlags(chunk[0])
|
||||
return msg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
@@ -636,3 +653,10 @@ func (c *Client) LocalAddr() (netip.AddrPort, error) {
|
||||
}
|
||||
return netip.ParseAddrPort(a.String())
|
||||
}
|
||||
|
||||
func cutLeadingN(b []byte, n int) (chunk, remain []byte, ok bool) {
|
||||
if len(b) >= n {
|
||||
return b[:n], b[n:], true
|
||||
}
|
||||
return nil, b, false
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ type Server struct {
|
||||
removePktForwardOther expvar.Int
|
||||
avgQueueDuration *uint64 // In milliseconds; accessed atomically
|
||||
tcpRtt metrics.LabelMap // histogram
|
||||
meshUpdateBatchSize *metrics.Histogram
|
||||
meshUpdateLoopCount *metrics.Histogram
|
||||
|
||||
// verifyClientsLocalTailscaled only accepts client connections to the DERP
|
||||
// server if the clientKey is a known peer in the network, as specified by a
|
||||
@@ -323,6 +325,8 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
|
||||
avgQueueDuration: new(uint64),
|
||||
tcpRtt: metrics.LabelMap{Label: "le"},
|
||||
meshUpdateBatchSize: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000}),
|
||||
meshUpdateLoopCount: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100}),
|
||||
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
|
||||
clock: tstime.StdClock{},
|
||||
}
|
||||
@@ -566,7 +570,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
}
|
||||
s.keyOfAddr[c.remoteIPPort] = c.key
|
||||
s.curClients.Add(1)
|
||||
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, true)
|
||||
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, c.presentFlags(), true)
|
||||
}
|
||||
|
||||
// broadcastPeerStateChangeLocked enqueues a message to all watchers
|
||||
@@ -574,12 +578,13 @@ func (s *Server) registerClient(c *sclient) {
|
||||
// presence changed.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, ipPort netip.AddrPort, present bool) {
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, ipPort netip.AddrPort, flags PeerPresentFlags, present bool) {
|
||||
for w := range s.watchers {
|
||||
w.peerStateChange = append(w.peerStateChange, peerConnState{
|
||||
peer: peer,
|
||||
present: present,
|
||||
ipPort: ipPort,
|
||||
flags: flags,
|
||||
})
|
||||
go w.requestMeshUpdate()
|
||||
}
|
||||
@@ -601,7 +606,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
delete(s.clientsMesh, c.key)
|
||||
s.notePeerGoneFromRegionLocked(c.key)
|
||||
}
|
||||
s.broadcastPeerStateChangeLocked(c.key, netip.AddrPort{}, false)
|
||||
s.broadcastPeerStateChangeLocked(c.key, netip.AddrPort{}, 0, false)
|
||||
case *dupClientSet:
|
||||
c.debugLogf("removed duplicate client")
|
||||
if set.removeClient(c) {
|
||||
@@ -700,6 +705,7 @@ func (s *Server) addWatcher(c *sclient) {
|
||||
peer: peer,
|
||||
present: true,
|
||||
ipPort: ac.remoteIPPort,
|
||||
flags: ac.presentFlags(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -756,7 +762,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
c.meshUpdate = make(chan struct{})
|
||||
c.meshUpdate = make(chan struct{}, 1) // must be buffered; >1 is fine but wasteful
|
||||
}
|
||||
if clientInfo != nil {
|
||||
c.info = *clientInfo
|
||||
@@ -1141,13 +1147,18 @@ func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReaso
|
||||
}
|
||||
}
|
||||
|
||||
// requestMeshUpdate notes that a c's peerStateChange has been appended to and
|
||||
// should now be written.
|
||||
//
|
||||
// It does not block. If a meshUpdate is already pending for this client, it
|
||||
// does nothing.
|
||||
func (c *sclient) requestMeshUpdate() {
|
||||
if !c.canMesh {
|
||||
panic("unexpected requestMeshUpdate")
|
||||
}
|
||||
select {
|
||||
case c.meshUpdate <- struct{}{}:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,6 +1187,10 @@ func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, inf
|
||||
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid 'addr' parameter") {
|
||||
// Issue 12617
|
||||
return errors.New("tailscaled version is too old (out of sync with derper binary)")
|
||||
}
|
||||
return fmt.Errorf("failed to query local tailscaled status for %v: %w", clientKey, err)
|
||||
}
|
||||
}
|
||||
@@ -1435,11 +1450,26 @@ type sclient struct {
|
||||
peerGoneLim *rate.Limiter
|
||||
}
|
||||
|
||||
func (c *sclient) presentFlags() PeerPresentFlags {
|
||||
var f PeerPresentFlags
|
||||
if c.info.IsProber {
|
||||
f |= PeerPresentIsProber
|
||||
}
|
||||
if c.canMesh {
|
||||
f |= PeerPresentIsMeshPeer
|
||||
}
|
||||
if f == 0 {
|
||||
return PeerPresentIsRegular
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// peerConnState represents whether a peer is connected to the server
|
||||
// or not.
|
||||
type peerConnState struct {
|
||||
ipPort netip.AddrPort // if present, the peer's IP:port
|
||||
peer key.NodePublic
|
||||
flags PeerPresentFlags
|
||||
present bool
|
||||
}
|
||||
|
||||
@@ -1613,6 +1643,11 @@ func (c *sclient) sendPong(data [8]byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
peerGoneFrameLen = keyLen + 1
|
||||
peerPresentFrameLen = keyLen + 16 + 2 + 1 // 16 byte IP + 2 byte port + 1 byte flags
|
||||
)
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) error {
|
||||
switch reason {
|
||||
@@ -1622,7 +1657,7 @@ func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) e
|
||||
c.s.peerGoneNotHereFrames.Add(1)
|
||||
}
|
||||
c.setWriteDeadline()
|
||||
data := make([]byte, 0, keyLen+1)
|
||||
data := make([]byte, 0, peerGoneFrameLen)
|
||||
data = peer.AppendTo(data)
|
||||
data = append(data, byte(reason))
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, uint32(len(data))); err != nil {
|
||||
@@ -1634,73 +1669,62 @@ func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) e
|
||||
}
|
||||
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic, ipPort netip.AddrPort) error {
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic, ipPort netip.AddrPort, flags PeerPresentFlags) error {
|
||||
c.setWriteDeadline()
|
||||
const frameLen = keyLen + 16 + 2
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, frameLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, peerPresentFrameLen); err != nil {
|
||||
return err
|
||||
}
|
||||
payload := make([]byte, frameLen)
|
||||
payload := make([]byte, peerPresentFrameLen)
|
||||
_ = peer.AppendTo(payload[:0])
|
||||
a16 := ipPort.Addr().As16()
|
||||
copy(payload[keyLen:], a16[:])
|
||||
binary.BigEndian.PutUint16(payload[keyLen+16:], ipPort.Port())
|
||||
payload[keyLen+18] = byte(flags)
|
||||
_, err := c.bw.Write(payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendMeshUpdates drains as many mesh peerStateChange entries as
|
||||
// possible into the write buffer WITHOUT flushing or otherwise
|
||||
// blocking (as it holds c.s.mu while working). If it can't drain them
|
||||
// all, it schedules itself to be called again in the future.
|
||||
// sendMeshUpdates drains all mesh peerStateChange entries into the write buffer
|
||||
// without flushing.
|
||||
func (c *sclient) sendMeshUpdates() error {
|
||||
c.s.mu.Lock()
|
||||
defer c.s.mu.Unlock()
|
||||
var lastBatch []peerConnState // memory to best effort reuse
|
||||
|
||||
// allow all happened-before mesh update request goroutines to complete, if
|
||||
// we don't finish the task we'll queue another below.
|
||||
drainUpdates:
|
||||
for {
|
||||
select {
|
||||
case <-c.meshUpdate:
|
||||
default:
|
||||
break drainUpdates
|
||||
// takeAll returns c.peerStateChange and empties it.
|
||||
takeAll := func() []peerConnState {
|
||||
c.s.mu.Lock()
|
||||
defer c.s.mu.Unlock()
|
||||
if len(c.peerStateChange) == 0 {
|
||||
return nil
|
||||
}
|
||||
batch := c.peerStateChange
|
||||
if cap(lastBatch) > 16 {
|
||||
lastBatch = nil
|
||||
}
|
||||
c.peerStateChange = lastBatch[:0]
|
||||
return batch
|
||||
}
|
||||
|
||||
writes := 0
|
||||
for _, pcs := range c.peerStateChange {
|
||||
if c.bw.Available() <= frameHeaderLen+keyLen {
|
||||
break
|
||||
for loops := 0; ; loops++ {
|
||||
batch := takeAll()
|
||||
if len(batch) == 0 {
|
||||
c.s.meshUpdateLoopCount.Observe(float64(loops))
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if pcs.present {
|
||||
err = c.sendPeerPresent(pcs.peer, pcs.ipPort)
|
||||
} else {
|
||||
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
|
||||
}
|
||||
if err != nil {
|
||||
// Shouldn't happen, though, as we're writing
|
||||
// into available buffer space, not the
|
||||
// network.
|
||||
return err
|
||||
}
|
||||
writes++
|
||||
}
|
||||
c.s.meshUpdateBatchSize.Observe(float64(len(batch)))
|
||||
|
||||
remain := copy(c.peerStateChange, c.peerStateChange[writes:])
|
||||
c.peerStateChange = c.peerStateChange[:remain]
|
||||
|
||||
// Did we manage to write them all into the bufio buffer without flushing?
|
||||
if len(c.peerStateChange) == 0 {
|
||||
if cap(c.peerStateChange) > 16 {
|
||||
c.peerStateChange = nil
|
||||
for _, pcs := range batch {
|
||||
var err error
|
||||
if pcs.present {
|
||||
err = c.sendPeerPresent(pcs.peer, pcs.ipPort, pcs.flags)
|
||||
} else {
|
||||
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Didn't finish in the buffer space provided; schedule a future run.
|
||||
go c.requestMeshUpdate()
|
||||
lastBatch = batch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacket writes contents to the client in a RecvPacket frame. If
|
||||
@@ -1929,6 +1953,8 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
|
||||
}))
|
||||
m.Set("counter_tcp_rtt", &s.tcpRtt)
|
||||
m.Set("counter_mesh_update_batch_size", s.meshUpdateBatchSize)
|
||||
m.Set("counter_mesh_update_loop_count", s.meshUpdateLoopCount)
|
||||
var expvarVersion expvar.String
|
||||
expvarVersion.Set(version.Long())
|
||||
m.Set("version", &expvarVersion)
|
||||
|
||||
@@ -623,7 +623,13 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
|
||||
}
|
||||
}))
|
||||
}
|
||||
t.Logf("got present with IP %v", m.IPPort)
|
||||
t.Logf("got present with IP %v, flags=%v", m.IPPort, m.Flags)
|
||||
switch m.Flags {
|
||||
case PeerPresentIsMeshPeer, PeerPresentIsRegular:
|
||||
// Okay
|
||||
default:
|
||||
t.Errorf("unexpected PeerPresentIsMeshPeer flags %v", m.Flags)
|
||||
}
|
||||
delete(want, got)
|
||||
if len(want) == 0 {
|
||||
return
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -299,13 +298,13 @@ func TestBreakWatcherConnRecv(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var peers int
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) {
|
||||
t.Logf("add: %v", k.ShortString())
|
||||
add := func(m derp.PeerPresentMessage) {
|
||||
t.Logf("add: %v", m.Key.ShortString())
|
||||
peers++
|
||||
// Signal that the watcher has run
|
||||
watcherChan <- peers
|
||||
}
|
||||
remove := func(k key.NodePublic) { t.Logf("remove: %v", k.ShortString()); peers-- }
|
||||
remove := func(m derp.PeerGoneMessage) { t.Logf("remove: %v", m.Peer.ShortString()); peers-- }
|
||||
|
||||
watcher1.RunWatchConnectionLoop(ctx, serverPrivateKey1.Public(), t.Logf, add, remove)
|
||||
}()
|
||||
@@ -370,15 +369,15 @@ func TestBreakWatcherConn(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var peers int
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) {
|
||||
t.Logf("add: %v", k.ShortString())
|
||||
add := func(m derp.PeerPresentMessage) {
|
||||
t.Logf("add: %v", m.Key.ShortString())
|
||||
peers++
|
||||
// Signal that the watcher has run
|
||||
watcherChan <- peers
|
||||
// Wait for breaker to run
|
||||
<-breakerChan
|
||||
}
|
||||
remove := func(k key.NodePublic) { t.Logf("remove: %v", k.ShortString()); peers-- }
|
||||
remove := func(m derp.PeerGoneMessage) { t.Logf("remove: %v", m.Peer.ShortString()); peers-- }
|
||||
|
||||
watcher1.RunWatchConnectionLoop(ctx, serverPrivateKey1.Public(), t.Logf, add, remove)
|
||||
}()
|
||||
@@ -407,8 +406,8 @@ func TestBreakWatcherConn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func noopAdd(key.NodePublic, netip.AddrPort) {}
|
||||
func noopRemove(key.NodePublic) {}
|
||||
func noopAdd(derp.PeerPresentMessage) {}
|
||||
func noopRemove(derp.PeerGoneMessage) {}
|
||||
|
||||
func TestRunWatchConnectionLoopServeConnect(t *testing.T) {
|
||||
defer func() { testHookWatchLookConnectResult = nil }()
|
||||
|
||||
@@ -5,7 +5,6 @@ package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -35,9 +34,14 @@ var testHookWatchLookConnectResult func(connectError error, wasSelfConnect bool)
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to be
|
||||
// closed, and c itself needs to be closed.
|
||||
//
|
||||
// It is a fatal error to call this on an already-started Client withoutq having
|
||||
// It is a fatal error to call this on an already-started Client without having
|
||||
// initialized Client.WatchConnectionChanges to true.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(key.NodePublic, netip.AddrPort), remove func(key.NodePublic)) {
|
||||
//
|
||||
// If the DERP connection breaks and reconnects, remove will be called for all
|
||||
// previously seen peers, with Reason type PeerGoneReasonSynthetic. Those
|
||||
// clients are likely still connected and their add message will appear after
|
||||
// reconnect.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(derp.PeerPresentMessage), remove func(derp.PeerGoneMessage)) {
|
||||
if !c.WatchConnectionChanges {
|
||||
if c.isStarted() {
|
||||
panic("invalid use of RunWatchConnectionLoop on already-started Client without setting Client.RunWatchConnectionLoop")
|
||||
@@ -62,7 +66,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
logf("reconnected; clearing %d forwarding mappings", len(present))
|
||||
for k := range present {
|
||||
remove(k)
|
||||
remove(derp.PeerGoneMessage{Peer: k, Reason: derp.PeerGoneReasonMeshConnBroke})
|
||||
}
|
||||
present = map[key.NodePublic]bool{}
|
||||
}
|
||||
@@ -84,13 +88,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
updatePeer := func(k key.NodePublic, ipPort netip.AddrPort, isPresent bool) {
|
||||
if isPresent {
|
||||
add(k, ipPort)
|
||||
} else {
|
||||
remove(k)
|
||||
}
|
||||
|
||||
updatePeer := func(k key.NodePublic, isPresent bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if isPresent {
|
||||
@@ -148,7 +146,8 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(m.Key, m.IPPort, true)
|
||||
add(m)
|
||||
updatePeer(m.Key, true)
|
||||
case derp.PeerGoneMessage:
|
||||
switch m.Reason {
|
||||
case derp.PeerGoneReasonDisconnected:
|
||||
@@ -160,7 +159,8 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
logf("Recv: peer %s not at server %s for unknown reason %v",
|
||||
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
|
||||
}
|
||||
updatePeer(key.NodePublic(m.Peer), netip.AddrPort{}, false)
|
||||
remove(m)
|
||||
updatePeer(m.Peer, false)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type config -type counters_key -type counter_key_af -type counter_key_packets_bytes_action -type counter_key_prog_end bpf xdp.c -- -I headers
|
||||
@@ -27,6 +28,7 @@ type STUNServer struct {
|
||||
metrics *stunServerMetrics
|
||||
dstPort int
|
||||
dropSTUN bool
|
||||
link link.Link
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in xdp_linux_test.go, which has a build tag
|
||||
@@ -87,7 +89,7 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding device: %w", err)
|
||||
}
|
||||
_, err = link.AttachXDP(link.XDPOptions{
|
||||
link, err := link.AttachXDP(link.XDPOptions{
|
||||
Program: objs.XdpProgFunc,
|
||||
Interface: iface.Index,
|
||||
Flags: link.XDPAttachFlags(config.AttachFlags),
|
||||
@@ -95,6 +97,7 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error attaching XDP program to dev: %w", err)
|
||||
}
|
||||
server.link = link
|
||||
return server, nil
|
||||
}
|
||||
|
||||
@@ -102,7 +105,12 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
func (s *STUNServer) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.objs.Close()
|
||||
var errs []error
|
||||
if s.link != nil {
|
||||
errs = append(errs, s.link.Close())
|
||||
}
|
||||
errs = append(errs, s.objs.Close())
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
type stunServerMetrics struct {
|
||||
|
||||
@@ -36,13 +36,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
regStr = map[string]*string{}
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
set = map[string]string{}
|
||||
// +checklocks:mu
|
||||
regStr = map[string]*string{}
|
||||
// +checklocks:mu
|
||||
regBool = map[string]*bool{}
|
||||
// +checklocks:mu
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
// +checklocks:mu
|
||||
regDuration = map[string]*time.Duration{}
|
||||
regInt = map[string]*int{}
|
||||
// +checklocks:mu
|
||||
regInt = map[string]*int{}
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
@@ -51,6 +57,7 @@ func noteEnv(k, v string) {
|
||||
noteEnvLocked(k, v)
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func noteEnvLocked(k, v string) {
|
||||
if v != "" {
|
||||
set[k] = v
|
||||
@@ -202,6 +209,7 @@ func RegisterInt(envVar string) func() int {
|
||||
return func() int { return *p }
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setBoolLocked(p *bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -215,6 +223,7 @@ func setBoolLocked(p *bool, envVar, val string) {
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -228,6 +237,7 @@ func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
p.Set(b)
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -241,6 +251,7 @@ func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setIntLocked(p *int, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
|
||||
@@ -120,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
|
||||
# nix-direnv cache busting line: sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=
|
||||
|
||||
29
go.mod
29
go.mod
@@ -5,7 +5,6 @@ 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
|
||||
@@ -78,12 +77,12 @@ require (
|
||||
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
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9
|
||||
github.com/tc-hib/winres v0.2.1
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
@@ -113,7 +112,6 @@ require (
|
||||
k8s.io/apimachinery v0.30.1
|
||||
k8s.io/apiserver v0.30.1
|
||||
k8s.io/client-go v0.30.1
|
||||
modernc.org/sqlite v1.29.10
|
||||
nhooyr.io/websocket v1.8.10
|
||||
sigs.k8s.io/controller-runtime v0.18.4
|
||||
sigs.k8s.io/controller-tools v0.15.1-0.20240618033008-7824932b0cab
|
||||
@@ -127,21 +125,18 @@ 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/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobuffalo/flect v1.0.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // 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
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -376,7 +371,7 @@ require (
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
|
||||
sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=
|
||||
|
||||
98
go.sum
98
go.sum
@@ -56,6 +56,8 @@ github.com/Antonboom/errname v0.1.9 h1:BZDX4r3l4TBZxZ2o2LNrlGxSHran4d1u4veZdoORT
|
||||
github.com/Antonboom/errname v0.1.9/go.mod h1:nLTcJzevREuAsgTbG85UsuiWpMpAqbKD1HNZ29OzE58=
|
||||
github.com/Antonboom/nilnil v0.1.4 h1:yWIfwbCRDpJiJvs7Quz55dzeXCgORQyAG29N9/J5H2Q=
|
||||
github.com/Antonboom/nilnil v0.1.4/go.mod h1:iOov/7gRcXkeEU+EMGpBu2ORih3iyVEiWjeste1SJm8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
@@ -75,8 +77,6 @@ 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=
|
||||
@@ -194,6 +194,9 @@ github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8
|
||||
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
|
||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
|
||||
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -215,6 +218,8 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
@@ -263,10 +268,12 @@ github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaP
|
||||
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
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=
|
||||
@@ -293,6 +300,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y=
|
||||
github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -330,8 +339,11 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
@@ -512,6 +524,9 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY=
|
||||
github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -522,8 +537,6 @@ 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=
|
||||
@@ -623,10 +636,6 @@ 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 +691,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -691,6 +702,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA=
|
||||
github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -699,8 +712,6 @@ 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=
|
||||
@@ -791,8 +802,6 @@ 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=
|
||||
@@ -904,8 +913,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
|
||||
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/mkctr v0.0.0-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKfFTvR00d0b0R0bzRKBW9yrj813fhI=
|
||||
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734/go.mod h1:6v53VHLmLKUaqWMpSGDeRWhltLSCEteMItYoiKLpdJk=
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba h1:uNo1VCm/xg4alMkIKo8RWTKNx5y1otfVOcKbp+irkL4=
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba/go.mod h1:DxnqIXBplij66U2ZkL688xy07q97qQ83P+TVueLiHq4=
|
||||
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=
|
||||
@@ -914,8 +923,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:t
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
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/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/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/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
@@ -988,6 +997,22 @@ 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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
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=
|
||||
@@ -1032,8 +1057,8 @@ golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTr
|
||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1385,6 +1410,11 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1401,6 +1431,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
|
||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1475,32 +1507,6 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
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=
|
||||
|
||||
@@ -1 +1 @@
|
||||
4d101c0f2d2a234b8902bfff5fadb16070201f0a
|
||||
2f152a4eff5875655a9a84fce8f8d329f8d9a321
|
||||
|
||||
104
health/health.go
104
health/health.go
@@ -78,6 +78,7 @@ type Tracker struct {
|
||||
|
||||
latestVersion *tailcfg.ClientVersion // or nil
|
||||
checkForUpdates bool
|
||||
applyUpdates opt.Bool
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
@@ -92,7 +93,8 @@ type Tracker struct {
|
||||
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
|
||||
ipnState string
|
||||
ipnWantRunning bool
|
||||
anyInterfaceUp opt.Bool // empty means unknown (assume true)
|
||||
ipnWantRunningLastTrue time.Time // when ipnWantRunning last changed false -> true
|
||||
anyInterfaceUp opt.Bool // empty means unknown (assume true)
|
||||
udp4Unbound bool
|
||||
controlHealth []string
|
||||
lastLoginErr error
|
||||
@@ -211,8 +213,10 @@ type Warnable struct {
|
||||
// Deprecated: this is only used in one case, and will be removed in a future PR
|
||||
MapDebugFlag string
|
||||
|
||||
// If true, this warnable is related to configuration of networking stack
|
||||
// on the machine that impacts connectivity.
|
||||
// ImpactsConnectivity is whether this Warnable in an unhealthy state will impact the user's
|
||||
// ability to connect to the Internet or other nodes on the tailnet. On platforms where
|
||||
// the client GUI supports a tray icon, the client will display an exclamation mark
|
||||
// on the tray icon when ImpactsConnectivity is set to true and the Warnable is unhealthy.
|
||||
ImpactsConnectivity bool
|
||||
}
|
||||
|
||||
@@ -250,9 +254,16 @@ func (t *Tracker) nil() bool {
|
||||
type Severity string
|
||||
|
||||
const (
|
||||
SeverityHigh Severity = "high"
|
||||
// SeverityHigh is the highest severity level, used for critical errors that need immediate attention.
|
||||
// On platforms where the client GUI can deliver notifications, a SeverityHigh Warnable will trigger
|
||||
// a modal notification.
|
||||
SeverityHigh Severity = "high"
|
||||
// SeverityMedium is used for errors that are important but not critical. This won't trigger a modal
|
||||
// notification, however it will be displayed in a more visible way than a SeverityLow Warnable.
|
||||
SeverityMedium Severity = "medium"
|
||||
SeverityLow Severity = "low"
|
||||
// SeverityLow is used for less important notices that don't need immediate attention. The user will
|
||||
// have to go to a Settings window, or another "hidden" GUI location to see these messages.
|
||||
SeverityLow Severity = "low"
|
||||
)
|
||||
|
||||
// Args is a map of Args to string values that can be used to provide parameters regarding
|
||||
@@ -705,7 +716,29 @@ func (t *Tracker) SetIPNState(state string, wantRunning bool) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.ipnState = state
|
||||
prevWantRunning := t.ipnWantRunning
|
||||
t.ipnWantRunning = wantRunning
|
||||
|
||||
if state == "Running" {
|
||||
// Any time we are told the backend is Running (control+DERP are connected), the Warnable
|
||||
// should be set to healthy, no matter if 5 seconds have passed or not.
|
||||
t.setHealthyLocked(warmingUpWarnable)
|
||||
} else if wantRunning && !prevWantRunning && t.ipnWantRunningLastTrue.IsZero() {
|
||||
// The first time we see wantRunning=true and it used to be false, it means the user requested
|
||||
// the backend to start. We store this timestamp and use it to silence some warnings that are
|
||||
// expected during startup.
|
||||
t.ipnWantRunningLastTrue = time.Now()
|
||||
t.setUnhealthyLocked(warmingUpWarnable, nil)
|
||||
time.AfterFunc(warmingUpWarnableDuration, func() {
|
||||
t.mu.Lock()
|
||||
t.updateWarmingUpWarnableLocked()
|
||||
t.mu.Unlock()
|
||||
})
|
||||
} else if !wantRunning {
|
||||
// Reset the timer when the user decides to stop the backend.
|
||||
t.ipnWantRunningLastTrue = time.Time{}
|
||||
}
|
||||
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
@@ -759,17 +792,20 @@ func (t *Tracker) SetLatestVersion(v *tailcfg.ClientVersion) {
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetCheckForUpdates sets whether the client wants to check for updates.
|
||||
func (t *Tracker) SetCheckForUpdates(v bool) {
|
||||
// SetAutoUpdatePrefs sets the client auto-update preferences. The arguments
|
||||
// match the fields of ipn.AutoUpdatePrefs, but we cannot pass that struct
|
||||
// directly due to a circular import.
|
||||
func (t *Tracker) SetAutoUpdatePrefs(check bool, apply opt.Bool) {
|
||||
if t.nil() {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.checkForUpdates == v {
|
||||
if t.checkForUpdates == check && t.applyUpdates == apply {
|
||||
return
|
||||
}
|
||||
t.checkForUpdates = v
|
||||
t.checkForUpdates = check
|
||||
t.applyUpdates = apply
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
@@ -858,20 +894,16 @@ var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
// updateBuiltinWarnablesLocked performs a number of checks on the state of the backend,
|
||||
// and adds/removes Warnings from the Tracker as needed.
|
||||
func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
if t.checkForUpdates {
|
||||
if cv := t.latestVersion; cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
|
||||
if cv.UrgentSecurityUpdate {
|
||||
t.setUnhealthyLocked(securityUpdateAvailableWarnable, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: cv.LatestVersion,
|
||||
})
|
||||
} else {
|
||||
t.setUnhealthyLocked(updateAvailableWarnable, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: cv.LatestVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
t.updateWarmingUpWarnableLocked()
|
||||
|
||||
if w, show := t.showUpdateWarnable(); show {
|
||||
t.setUnhealthyLocked(w, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: t.latestVersion.LatestVersion,
|
||||
})
|
||||
} else {
|
||||
t.setHealthyLocked(updateAvailableWarnable)
|
||||
t.setHealthyLocked(securityUpdateAvailableWarnable)
|
||||
}
|
||||
|
||||
if version.IsUnstableBuild() {
|
||||
@@ -1037,6 +1069,32 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
// updateWarmingUpWarnableLocked ensures the warmingUpWarnable is healthy if wantRunning has been set to true
|
||||
// for more than warmingUpWarnableDuration.
|
||||
func (t *Tracker) updateWarmingUpWarnableLocked() {
|
||||
if !t.ipnWantRunningLastTrue.IsZero() && time.Now().After(t.ipnWantRunningLastTrue.Add(warmingUpWarnableDuration)) {
|
||||
t.setHealthyLocked(warmingUpWarnable)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) showUpdateWarnable() (*Warnable, bool) {
|
||||
if !t.checkForUpdates {
|
||||
return nil, false
|
||||
}
|
||||
cv := t.latestVersion
|
||||
if cv == nil || cv.RunningLatest || cv.LatestVersion == "" {
|
||||
return nil, false
|
||||
}
|
||||
if cv.UrgentSecurityUpdate {
|
||||
return securityUpdateAvailableWarnable, true
|
||||
}
|
||||
// Only show update warning when auto-updates are off
|
||||
if !t.applyUpdates.EqualBool(true) {
|
||||
return updateAvailableWarnable, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
|
||||
@@ -6,8 +6,12 @@ package health
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
func TestAppendWarnableDebugFlags(t *testing.T) {
|
||||
@@ -199,15 +203,103 @@ func TestCheckDependsOnAppearsInUnhealthyState(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("Expected an UnhealthyState for w1, got nothing")
|
||||
}
|
||||
if len(us1.DependsOn) != 0 {
|
||||
t.Fatalf("Expected no DependsOn in the unhealthy state, got: %v", us1.DependsOn)
|
||||
wantDependsOn := []WarnableCode{warmingUpWarnable.Code}
|
||||
if !reflect.DeepEqual(us1.DependsOn, wantDependsOn) {
|
||||
t.Fatalf("Expected DependsOn = %v in the unhealthy state, got: %v", wantDependsOn, us1.DependsOn)
|
||||
}
|
||||
ht.SetUnhealthy(w2, Args{ArgError: "w2 is also unhealthy now"})
|
||||
us2, ok := ht.CurrentState().Warnings[w2.Code]
|
||||
if !ok {
|
||||
t.Fatalf("Expected an UnhealthyState for w2, got nothing")
|
||||
}
|
||||
if !reflect.DeepEqual(us2.DependsOn, []WarnableCode{w1.Code}) {
|
||||
t.Fatalf("Expected DependsOn = [w1.Code] in the unhealthy state, got: %v", us2.DependsOn)
|
||||
wantDependsOn = slices.Concat([]WarnableCode{w1.Code}, wantDependsOn)
|
||||
if !reflect.DeepEqual(us2.DependsOn, wantDependsOn) {
|
||||
t.Fatalf("Expected DependsOn = %v in the unhealthy state, got: %v", wantDependsOn, us2.DependsOn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowUpdateWarnable(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
check bool
|
||||
apply opt.Bool
|
||||
cv *tailcfg.ClientVersion
|
||||
wantWarnable *Warnable
|
||||
wantShow bool
|
||||
}{
|
||||
{
|
||||
desc: "nil CientVersion",
|
||||
check: true,
|
||||
cv: nil,
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "RunningLatest",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: true},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "no LatestVersion",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: ""},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "show regular update",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: updateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
{
|
||||
desc: "show security update",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3", UrgentSecurityUpdate: true},
|
||||
wantWarnable: securityUpdateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
{
|
||||
desc: "update check disabled",
|
||||
check: false,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "hide update with auto-updates",
|
||||
check: true,
|
||||
apply: opt.NewBool(true),
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "show security update with auto-updates",
|
||||
check: true,
|
||||
apply: opt.NewBool(true),
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3", UrgentSecurityUpdate: true},
|
||||
wantWarnable: securityUpdateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
tr := &Tracker{
|
||||
checkForUpdates: tt.check,
|
||||
applyUpdates: tt.apply,
|
||||
latestVersion: tt.cv,
|
||||
}
|
||||
gotWarnable, gotShow := tr.showUpdateWarnable()
|
||||
if gotWarnable != tt.wantWarnable {
|
||||
t.Errorf("got warnable: %v, want: %v", gotWarnable, tt.wantWarnable)
|
||||
}
|
||||
if gotShow != tt.wantShow {
|
||||
t.Errorf("got show: %v, want: %v", gotShow, tt.wantShow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ type State struct {
|
||||
// Representation contains information to be shown to the user to inform them
|
||||
// that a Warnable is currently unhealthy.
|
||||
type UnhealthyState struct {
|
||||
WarnableCode WarnableCode
|
||||
Severity Severity
|
||||
Title string
|
||||
Text string
|
||||
BrokenSince *time.Time `json:",omitempty"`
|
||||
Args Args `json:",omitempty"`
|
||||
DependsOn []WarnableCode `json:",omitempty"`
|
||||
WarnableCode WarnableCode
|
||||
Severity Severity
|
||||
Title string
|
||||
Text string
|
||||
BrokenSince *time.Time `json:",omitempty"`
|
||||
Args Args `json:",omitempty"`
|
||||
DependsOn []WarnableCode `json:",omitempty"`
|
||||
ImpactsConnectivity bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
|
||||
@@ -41,19 +42,27 @@ func (w *Warnable) unhealthyState(ws *warningState) *UnhealthyState {
|
||||
text = w.Text(Args{})
|
||||
}
|
||||
|
||||
dependsOnWarnableCodes := make([]WarnableCode, len(w.DependsOn))
|
||||
dependsOnWarnableCodes := make([]WarnableCode, len(w.DependsOn), len(w.DependsOn)+1)
|
||||
for i, d := range w.DependsOn {
|
||||
dependsOnWarnableCodes[i] = d.Code
|
||||
}
|
||||
|
||||
if w != warmingUpWarnable {
|
||||
// Here we tell the frontend that all Warnables depend on warmingUpWarnable. GUIs will silence all warnings until all
|
||||
// their dependencies are healthy. This is a special case to prevent the GUI from showing a bunch of warnings when
|
||||
// the backend is still warming up.
|
||||
dependsOnWarnableCodes = append(dependsOnWarnableCodes, warmingUpWarnable.Code)
|
||||
}
|
||||
|
||||
return &UnhealthyState{
|
||||
WarnableCode: w.Code,
|
||||
Severity: w.Severity,
|
||||
Title: w.Title,
|
||||
Text: text,
|
||||
BrokenSince: &ws.BrokenSince,
|
||||
Args: ws.Args,
|
||||
DependsOn: dependsOnWarnableCodes,
|
||||
WarnableCode: w.Code,
|
||||
Severity: w.Severity,
|
||||
Title: w.Title,
|
||||
Text: text,
|
||||
BrokenSince: &ws.BrokenSince,
|
||||
Args: ws.Args,
|
||||
DependsOn: dependsOnWarnableCodes,
|
||||
ImpactsConnectivity: w.ImpactsConnectivity,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -17,7 +21,11 @@ var updateAvailableWarnable = Register(&Warnable{
|
||||
Title: "Update available",
|
||||
Severity: SeverityLow,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("An update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -25,9 +33,13 @@ var updateAvailableWarnable = Register(&Warnable{
|
||||
var securityUpdateAvailableWarnable = Register(&Warnable{
|
||||
Code: "security-update-available",
|
||||
Title: "Security update available",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("An urgent security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -37,15 +49,15 @@ var unstableWarnable = Register(&Warnable{
|
||||
Code: "is-using-unstable-version",
|
||||
Title: "Using an unstable version",
|
||||
Severity: SeverityLow,
|
||||
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes: please report any bugs to Tailscale."),
|
||||
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes. Please report any issues to Tailscale."),
|
||||
})
|
||||
|
||||
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
|
||||
var NetworkStatusWarnable = Register(&Warnable{
|
||||
Code: "network-status",
|
||||
Title: "Network down",
|
||||
Severity: SeverityHigh,
|
||||
Text: StaticMessage("Tailscale cannot connect because the network is down. (No network interface is up.)"),
|
||||
Severity: SeverityMedium,
|
||||
Text: StaticMessage("Tailscale cannot connect because the network is down. Check your Internet connection."),
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
@@ -82,29 +94,30 @@ var LoginStateWarnable = Register(&Warnable{
|
||||
},
|
||||
})
|
||||
|
||||
// notInMapPollWarnable is a Warnable that warns the user that they cannot connect to the control server.
|
||||
// notInMapPollWarnable is a Warnable that warns the user that we are using a stale network map.
|
||||
var notInMapPollWarnable = Register(&Warnable{
|
||||
Code: "not-in-map-poll",
|
||||
Title: "Cannot connect to control server",
|
||||
Title: "Out of sync",
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Cannot connect to the control server (not in map poll). Check your Internet connection."),
|
||||
Text: StaticMessage("Unable to connect to the Tailscale coordination server to synchronize the state of your tailnet. Peer reachability might degrade over time."),
|
||||
})
|
||||
|
||||
// noDERPHomeWarnable is a Warnable that warns the user that Tailscale doesn't have a home DERP.
|
||||
var noDERPHomeWarnable = Register(&Warnable{
|
||||
Code: "no-derp-home",
|
||||
Title: "No home relay server",
|
||||
Severity: SeverityHigh,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
|
||||
Code: "no-derp-home",
|
||||
Title: "No home relay server",
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
// noDERPConnectionWarnable is a Warnable that warns the user that Tailscale couldn't connect to a specific DERP server.
|
||||
var noDERPConnectionWarnable = Register(&Warnable{
|
||||
Code: "no-derp-connection",
|
||||
Title: "Relay server unavailable",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: func(args Args) string {
|
||||
if n := args[ArgDERPRegionName]; n != "" {
|
||||
@@ -113,6 +126,7 @@ var noDERPConnectionWarnable = Register(&Warnable{
|
||||
return fmt.Sprintf("Tailscale could not connect to the relay server with ID '%s'. Your Internet connection might be down, or the server might be temporarily unavailable.", args[ArgDERPRegionID])
|
||||
}
|
||||
},
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
// derpTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't heard from the home DERP region for a while.
|
||||
@@ -134,7 +148,7 @@ var derpTimeoutWarnable = Register(&Warnable{
|
||||
var derpRegionErrorWarnable = Register(&Warnable{
|
||||
Code: "derp-region-error",
|
||||
Title: "Relay server error",
|
||||
Severity: SeverityMedium,
|
||||
Severity: SeverityLow,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("The relay server #%v is reporting an issue: %v", args[ArgDERPRegionID], args[ArgError])
|
||||
@@ -145,7 +159,7 @@ var derpRegionErrorWarnable = Register(&Warnable{
|
||||
var noUDP4BindWarnable = Register(&Warnable{
|
||||
Code: "no-udp4-bind",
|
||||
Title: "Incoming connections may fail",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale couldn't listen for incoming UDP connections."),
|
||||
ImpactsConnectivity: true,
|
||||
@@ -212,3 +226,17 @@ var controlHealthWarnable = Register(&Warnable{
|
||||
return fmt.Sprintf("The coordination server is reporting an health issue: %v", args[ArgError])
|
||||
},
|
||||
})
|
||||
|
||||
// warmingUpWarnableDuration is the duration for which the warmingUpWarnable is reported by the backend after the user
|
||||
// has changed ipnWantRunning to true from false.
|
||||
const warmingUpWarnableDuration = 5 * time.Second
|
||||
|
||||
// warmingUpWarnable is a Warnable that is reported by the backend when it is starting up, for a maximum time of
|
||||
// warmingUpWarnableDuration. The GUIs use the presence of this Warnable to prevent showing any other warnings until
|
||||
// the backend is fully started.
|
||||
var warmingUpWarnable = Register(&Warnable{
|
||||
Code: "warming-up",
|
||||
Title: "Tailscale is starting",
|
||||
Severity: SeverityLow,
|
||||
Text: StaticMessage("Tailscale is starting. Please wait."),
|
||||
})
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
@@ -96,6 +95,7 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/util/rands"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
@@ -338,6 +338,9 @@ type LocalBackend struct {
|
||||
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
||||
// avoid unnecessary churn between multiple equally-good options.
|
||||
lastSuggestedExitNode tailcfg.StableNodeID
|
||||
|
||||
// refreshAutoExitNode indicates if the exit node should be recomputed when the next netcheck report is available.
|
||||
refreshAutoExitNode bool
|
||||
}
|
||||
|
||||
// HealthTracker returns the health tracker for the backend.
|
||||
@@ -640,7 +643,9 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
||||
hadPAC := b.prevIfState.HasPAC()
|
||||
b.prevIfState = ifst
|
||||
b.pauseOrResumeControlClientLocked()
|
||||
|
||||
if delta.Major && shouldAutoExitNode() {
|
||||
b.refreshAutoExitNode = true
|
||||
}
|
||||
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
||||
// add/remove subnets.
|
||||
if hadPAC != ifst.HasPAC() {
|
||||
@@ -1215,7 +1220,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
prefs.WantRunning = true
|
||||
prefs.LoggedOut = false
|
||||
}
|
||||
if setExitNodeID(prefs, st.NetMap) {
|
||||
if setExitNodeID(prefs, st.NetMap, b.lastSuggestedExitNode) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if applySysPolicy(prefs) {
|
||||
@@ -1418,9 +1423,8 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
b.send(*notify)
|
||||
}
|
||||
}()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
if !b.updateNetmapDeltaLocked(muts) {
|
||||
return false
|
||||
}
|
||||
@@ -1428,8 +1432,14 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
|
||||
nm := ptr.To(*b.netMap) // shallow clone
|
||||
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
|
||||
shouldAutoExitNode := shouldAutoExitNode()
|
||||
for _, p := range b.peers {
|
||||
nm.Peers = append(nm.Peers, p)
|
||||
// If the auto exit node currently set goes offline, find another auto exit node.
|
||||
if shouldAutoExitNode && b.pm.prefs.ExitNodeID() == p.StableID() && p.Online() != nil && !*p.Online() {
|
||||
b.setAutoExitNodeIDLockedOnEntry(unlock)
|
||||
return false
|
||||
}
|
||||
}
|
||||
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
@@ -1491,9 +1501,14 @@ func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (hand
|
||||
|
||||
// setExitNodeID updates prefs to reference an exit node by ID, rather
|
||||
// than by IP. It returns whether prefs was mutated.
|
||||
func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap, lastSuggestedExitNode tailcfg.StableNodeID) (prefsChanged bool) {
|
||||
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
|
||||
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
|
||||
if shouldAutoExitNode() && lastSuggestedExitNode != "" {
|
||||
exitNodeID = lastSuggestedExitNode
|
||||
}
|
||||
// Note: when exitNodeIDStr == "auto" && lastSuggestedExitNode == "", then exitNodeID is now "auto" which will never match a peer's node ID.
|
||||
// When there is no a peer matching the node ID, traffic will blackhole, preventing accidental non-exit-node usage when a policy is in effect that requires an exit node.
|
||||
changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
|
||||
prefs.ExitNodeID = exitNodeID
|
||||
prefs.ExitNodeIP = netip.Addr{}
|
||||
@@ -3357,7 +3372,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
||||
// setExitNodeID returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
setExitNodeID(newp, netMap)
|
||||
setExitNodeID(newp, netMap, b.lastSuggestedExitNode)
|
||||
// applySysPolicy does likewise so we can also ignore its return value.
|
||||
applySysPolicy(newp)
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
@@ -4850,12 +4865,44 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
refresh := b.refreshAutoExitNode
|
||||
b.refreshAutoExitNode = false
|
||||
b.mu.Unlock()
|
||||
|
||||
if cc == nil {
|
||||
return
|
||||
}
|
||||
cc.SetNetInfo(ni)
|
||||
if refresh {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
b.setAutoExitNodeIDLockedOnEntry(unlock)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setAutoExitNodeIDLockedOnEntry(unlock unlockOnce) {
|
||||
defer unlock()
|
||||
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
if !prefs.Valid() {
|
||||
b.logf("[unexpected]: received tailnet exit node ID pref change callback but current prefs are nil")
|
||||
return
|
||||
}
|
||||
prefsClone := prefs.AsStruct()
|
||||
newSuggestion, err := b.suggestExitNodeLocked()
|
||||
if err != nil {
|
||||
b.logf("setAutoExitNodeID: %v", err)
|
||||
return
|
||||
}
|
||||
prefsClone.ExitNodeID = newSuggestion.ID
|
||||
_, err = b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
|
||||
Prefs: *prefsClone,
|
||||
ExitNodeIDSet: true,
|
||||
}, unlock)
|
||||
if err != nil {
|
||||
b.logf("setAutoExitNodeID: failed to apply exit node ID preference: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// setNetMapLocked updates the LocalBackend state to reflect the newly
|
||||
@@ -5290,7 +5337,7 @@ func (b *LocalBackend) OperatorUserID() string {
|
||||
if opUserName == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := user.Lookup(opUserName)
|
||||
u, err := osuser.LookupByUsername(opUserName)
|
||||
if err != nil {
|
||||
b.logf("error looking up operator %q uid: %v", opUserName, err)
|
||||
return ""
|
||||
@@ -6526,30 +6573,33 @@ func mayDeref[T any](p *T) (v T) {
|
||||
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
||||
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
||||
|
||||
// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// suggestExitNodeLocked computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
||||
// eligible for consideration, the peer must have NodeAttrSuggestExitNode in its CapMap.
|
||||
//
|
||||
// Currently, peers with a DERP home are preferred over those without (typically this means Mullvad).
|
||||
// Peers are selected based on having a DERP home that is the lowest latency to this device. For peers
|
||||
// without a DERP home, we look for geographic proximity to this device's DERP home.
|
||||
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
b.mu.Lock()
|
||||
// b.mu.lock() must be held.
|
||||
func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
||||
netMap := b.netMap
|
||||
prevSuggestion := b.lastSuggestedExitNode
|
||||
b.mu.Unlock()
|
||||
|
||||
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, getAllowedSuggestions())
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.lastSuggestedExitNode = res.ID
|
||||
b.mu.Unlock()
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.suggestExitNodeLocked()
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -6578,7 +6628,7 @@ func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
|
||||
}
|
||||
|
||||
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
if report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
if report == nil || report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
return res, ErrNoPreferredDERP
|
||||
}
|
||||
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
|
||||
@@ -6788,6 +6838,12 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
|
||||
return earthRadiusMeters * c
|
||||
}
|
||||
|
||||
// shouldAutoExitNode checks for the auto exit node MDM policy.
|
||||
func shouldAutoExitNode() bool {
|
||||
exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, "")
|
||||
return exitNodeIDStr == "auto:any"
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
@@ -1647,16 +1648,17 @@ func (h *mockSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
|
||||
func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
tests := []struct {
|
||||
name string
|
||||
exitNodeIPKey bool
|
||||
exitNodeIDKey bool
|
||||
exitNodeID string
|
||||
exitNodeIP string
|
||||
prefs *ipn.Prefs
|
||||
exitNodeIPWant string
|
||||
exitNodeIDWant string
|
||||
prefsChanged bool
|
||||
nm *netmap.NetworkMap
|
||||
name string
|
||||
exitNodeIPKey bool
|
||||
exitNodeIDKey bool
|
||||
exitNodeID string
|
||||
exitNodeIP string
|
||||
prefs *ipn.Prefs
|
||||
exitNodeIPWant string
|
||||
exitNodeIDWant string
|
||||
prefsChanged bool
|
||||
nm *netmap.NetworkMap
|
||||
lastSuggestedExitNode tailcfg.StableNodeID
|
||||
}{
|
||||
{
|
||||
name: "ExitNodeID key is set",
|
||||
@@ -1835,6 +1837,21 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExitNodeID key is set to auto and last suggested exit node is populated",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeID: "auto:any",
|
||||
lastSuggestedExitNode: "123",
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeID key is set to auto and last suggested exit node is not populated",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeID: "auto:any",
|
||||
prefsChanged: true,
|
||||
exitNodeIDWant: "auto:any",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -1864,7 +1881,8 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
pm.prefs = test.prefs.View()
|
||||
b.netMap = test.nm
|
||||
b.pm = pm
|
||||
changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm)
|
||||
b.lastSuggestedExitNode = test.lastSuggestedExitNode
|
||||
changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm, tailcfg.StableNodeID(test.lastSuggestedExitNode))
|
||||
b.SetPrefsForTest(pm.CurrentPrefs().AsStruct())
|
||||
|
||||
if got := b.pm.prefs.ExitNodeID(); got != tailcfg.StableNodeID(test.exitNodeIDWant) {
|
||||
@@ -1885,6 +1903,222 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
|
||||
peer1 := makePeer(1, withCap(26), withSuggest(), withExitRoutes())
|
||||
peer2 := makePeer(2, withCap(26), withSuggest(), withExitRoutes())
|
||||
derpMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t1",
|
||||
RegionID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t2",
|
||||
RegionID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
report := &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 5 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 2,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
lastSuggestedExitNode tailcfg.StableNodeID
|
||||
netmap *netmap.NetworkMap
|
||||
muts []*tailcfg.PeerChange
|
||||
exitNodeIDWant tailcfg.StableNodeID
|
||||
updateNetmapDeltaResponse bool
|
||||
report *netcheck.Report
|
||||
}{
|
||||
{
|
||||
name: "selected auto exit node goes offline",
|
||||
lastSuggestedExitNode: peer1.StableID(),
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []tailcfg.NodeView{
|
||||
peer1,
|
||||
peer2,
|
||||
},
|
||||
DERPMap: derpMap,
|
||||
},
|
||||
muts: []*tailcfg.PeerChange{
|
||||
{
|
||||
NodeID: 1,
|
||||
Online: ptr.To(false),
|
||||
},
|
||||
{
|
||||
NodeID: 2,
|
||||
Online: ptr.To(true),
|
||||
},
|
||||
},
|
||||
exitNodeIDWant: peer2.StableID(),
|
||||
updateNetmapDeltaResponse: false,
|
||||
report: report,
|
||||
},
|
||||
{
|
||||
name: "other exit node goes offline doesn't change selected auto exit node that's still online",
|
||||
lastSuggestedExitNode: peer2.StableID(),
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []tailcfg.NodeView{
|
||||
peer1,
|
||||
peer2,
|
||||
},
|
||||
DERPMap: derpMap,
|
||||
},
|
||||
muts: []*tailcfg.PeerChange{
|
||||
{
|
||||
NodeID: 1,
|
||||
Online: ptr.To(false),
|
||||
},
|
||||
{
|
||||
NodeID: 2,
|
||||
Online: ptr.To(true),
|
||||
},
|
||||
},
|
||||
exitNodeIDWant: peer2.StableID(),
|
||||
updateNetmapDeltaResponse: true,
|
||||
report: report,
|
||||
},
|
||||
}
|
||||
msh := &mockSyspolicyHandler{
|
||||
t: t,
|
||||
stringPolicies: map[syspolicy.Key]*string{
|
||||
syspolicy.ExitNodeID: ptr.To("auto:any"),
|
||||
},
|
||||
}
|
||||
syspolicy.SetHandlerForTest(t, msh)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := newTestLocalBackend(t)
|
||||
b.netMap = tt.netmap
|
||||
b.updatePeersFromNetmapLocked(b.netMap)
|
||||
b.lastSuggestedExitNode = tt.lastSuggestedExitNode
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, tt.report)
|
||||
b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
||||
someTime := time.Unix(123, 0)
|
||||
muts, ok := netmap.MutationsFromMapResponse(&tailcfg.MapResponse{
|
||||
PeersChangedPatch: tt.muts,
|
||||
}, someTime)
|
||||
if !ok {
|
||||
t.Fatal("netmap.MutationsFromMapResponse failed")
|
||||
}
|
||||
if b.pm.prefs.ExitNodeID() != tt.lastSuggestedExitNode {
|
||||
t.Fatalf("did not set exit node ID to last suggested exit node despite auto policy")
|
||||
}
|
||||
|
||||
got := b.UpdateNetmapDelta(muts)
|
||||
if got != tt.updateNetmapDeltaResponse {
|
||||
t.Fatalf("got %v expected %v from UpdateNetmapDelta", got, tt.updateNetmapDeltaResponse)
|
||||
}
|
||||
if b.pm.prefs.ExitNodeID() != tt.exitNodeIDWant {
|
||||
t.Fatalf("did not get expected exit node id after UpdateNetmapDelta")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
|
||||
b := newTestLocalBackend(t)
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
b.hostinfo = hi
|
||||
k := key.NewMachine()
|
||||
var cc *mockControl
|
||||
opts := controlclient.Options{
|
||||
ServerURL: "https://example.com",
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
Dialer: tsdial.NewDialer(netmon.NewStatic()),
|
||||
Logf: b.logf,
|
||||
}
|
||||
cc = newClient(t, opts)
|
||||
b.cc = cc
|
||||
msh := &mockSyspolicyHandler{
|
||||
t: t,
|
||||
stringPolicies: map[syspolicy.Key]*string{
|
||||
syspolicy.ExitNodeID: ptr.To("auto:any"),
|
||||
},
|
||||
}
|
||||
syspolicy.SetHandlerForTest(t, msh)
|
||||
peer1 := makePeer(1, withCap(26), withDERP(3), withSuggest(), withExitRoutes())
|
||||
peer2 := makePeer(2, withCap(26), withDERP(2), withSuggest(), withExitRoutes())
|
||||
selfNode := tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
DERP: "127.3.3.40:2",
|
||||
}
|
||||
defaultDERPMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t1",
|
||||
RegionID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t2",
|
||||
RegionID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
3: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t3",
|
||||
RegionID: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b.netMap = &netmap.NetworkMap{
|
||||
SelfNode: selfNode.View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
peer1,
|
||||
peer2,
|
||||
},
|
||||
DERPMap: defaultDERPMap,
|
||||
}
|
||||
b.lastSuggestedExitNode = peer1.StableID()
|
||||
b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
||||
if eid := b.Prefs().ExitNodeID(); eid != peer1.StableID() {
|
||||
t.Errorf("got initial exit node %v, want %v", eid, peer1.StableID())
|
||||
}
|
||||
b.refreshAutoExitNode = true
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 5 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 2,
|
||||
})
|
||||
b.setNetInfo(&ni)
|
||||
if eid := b.Prefs().ExitNodeID(); eid != peer2.StableID() {
|
||||
t.Errorf("got final exit node %v, want %v", eid, peer2.StableID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplySysPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -2796,6 +3030,12 @@ func withSuggest() peerOptFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func withCap(version tailcfg.CapabilityVersion) peerOptFunc {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Cap = version
|
||||
}
|
||||
}
|
||||
|
||||
func deterministicRegionForTest(t testing.TB, want views.Slice[int], use int) selectRegionFunc {
|
||||
t.Helper()
|
||||
|
||||
@@ -3118,6 +3358,12 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
DERPMap: defaultDERPMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil report",
|
||||
lastReport: nil,
|
||||
netMap: largeNetmap,
|
||||
wantError: ErrNoPreferredDERP,
|
||||
},
|
||||
{
|
||||
name: "no preferred derp region",
|
||||
lastReport: preferredNoneReport,
|
||||
@@ -3127,6 +3373,24 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
},
|
||||
wantError: ErrNoPreferredDERP,
|
||||
},
|
||||
{
|
||||
name: "nil netmap",
|
||||
lastReport: noLatency1Report,
|
||||
netMap: nil,
|
||||
wantError: ErrNoPreferredDERP,
|
||||
},
|
||||
{
|
||||
name: "nil derpmap",
|
||||
lastReport: noLatency1Report,
|
||||
netMap: &netmap.NetworkMap{
|
||||
SelfNode: selfNode.View(),
|
||||
DERPMap: nil,
|
||||
Peers: []tailcfg.NodeView{
|
||||
dallasPeer5,
|
||||
},
|
||||
},
|
||||
wantError: ErrNoPreferredDERP,
|
||||
},
|
||||
{
|
||||
name: "missing suggestion capability",
|
||||
lastReport: noLatency1Report,
|
||||
@@ -3449,6 +3713,55 @@ func TestMinLatencyDERPregion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldAutoExitNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exitNodeIDPolicyValue string
|
||||
expectedBool bool
|
||||
}{
|
||||
{
|
||||
name: "auto:any",
|
||||
exitNodeIDPolicyValue: "auto:any",
|
||||
expectedBool: true,
|
||||
},
|
||||
{
|
||||
name: "no auto prefix",
|
||||
exitNodeIDPolicyValue: "foo",
|
||||
expectedBool: false,
|
||||
},
|
||||
{
|
||||
name: "auto prefix but empty suffix",
|
||||
exitNodeIDPolicyValue: "auto:",
|
||||
expectedBool: false,
|
||||
},
|
||||
{
|
||||
name: "auto prefix no colon",
|
||||
exitNodeIDPolicyValue: "auto",
|
||||
expectedBool: false,
|
||||
},
|
||||
{
|
||||
name: "auto prefix invalid suffix",
|
||||
exitNodeIDPolicyValue: "auto:foo",
|
||||
expectedBool: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msh := &mockSyspolicyHandler{
|
||||
t: t,
|
||||
stringPolicies: map[syspolicy.Key]*string{
|
||||
syspolicy.ExitNodeID: ptr.To(tt.exitNodeIDPolicyValue),
|
||||
},
|
||||
}
|
||||
syspolicy.SetHandlerForTest(t, msh)
|
||||
got := shouldAutoExitNode()
|
||||
if got != tt.expectedBool {
|
||||
t.Fatalf("expected %v got %v for %v policy value", tt.expectedBool, got, tt.exitNodeIDPolicyValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAutoUpdates(t *testing.T) {
|
||||
lb := newTestLocalBackend(t)
|
||||
|
||||
|
||||
@@ -142,8 +142,9 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
// - for each SigRotation signature, all previous node keys referenced by the
|
||||
// nested signatures are marked as obsolete.
|
||||
// - if there are multiple SigRotation signatures tracing back to the same
|
||||
// wrapping pubkey (e.g. if a node is cloned with all its keys), we keep
|
||||
// just one of them, marking the others as obsolete.
|
||||
// wrapping pubkey of the initial SigDirect signature (e.g. if a node is
|
||||
// cloned with all its keys), we keep just one of them, marking the others as
|
||||
// obsolete.
|
||||
type rotationTracker struct {
|
||||
// obsolete is the set of node keys that are obsolete due to key rotation.
|
||||
// users of rotationTracker should use the obsoleteKeys method for complete results.
|
||||
@@ -165,6 +166,13 @@ type sigRotationDetails struct {
|
||||
func (r *rotationTracker) addRotationDetails(np key.NodePublic, d *tka.RotationDetails) {
|
||||
r.obsolete.Make()
|
||||
r.obsolete.AddSlice(d.PrevNodeKeys)
|
||||
if d.InitialSig.SigKind != tka.SigDirect {
|
||||
// Only enforce uniqueness of chains originating from a SigDirect
|
||||
// signature. Chains that begin with a SigCredential can legitimately
|
||||
// start from the same wrapping pubkey when multiple nodes join the
|
||||
// network using the same reusable auth key.
|
||||
return
|
||||
}
|
||||
rd := sigRotationDetails{
|
||||
np: np,
|
||||
numPrevKeys: len(d.PrevNodeKeys),
|
||||
@@ -172,7 +180,7 @@ func (r *rotationTracker) addRotationDetails(np key.NodePublic, d *tka.RotationD
|
||||
if r.byWrappingKey == nil {
|
||||
r.byWrappingKey = make(map[string][]sigRotationDetails)
|
||||
}
|
||||
wp := string(d.WrappingPubkey)
|
||||
wp := string(d.InitialSig.WrappingPubkey)
|
||||
r.byWrappingKey[wp] = append(r.byWrappingKey[wp], rd)
|
||||
}
|
||||
|
||||
|
||||
@@ -556,6 +556,11 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
b := &LocalBackend{
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{authority: authority},
|
||||
}
|
||||
|
||||
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
|
||||
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
|
||||
if err != nil {
|
||||
@@ -585,6 +590,29 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
|
||||
n5Rotated, n5RotatedSig := resign(n5nl, n5InitialSig.Serialize())
|
||||
|
||||
nodeFromAuthKey := func(authKey string) (key.NodePrivate, tkatype.MarshaledSignature) {
|
||||
_, isWrapped, sig, priv := tka.DecodeWrappedAuthkey(authKey, t.Logf)
|
||||
if !isWrapped {
|
||||
t.Errorf("expected wrapped key")
|
||||
}
|
||||
|
||||
node := key.NewNode()
|
||||
nodeSig, err := tka.SignByCredential(priv, sig, node.Public())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return node, nodeSig
|
||||
}
|
||||
|
||||
preauth, err := b.NetworkLockWrapPreauthKey("tskey-auth-k7UagY1CNTRL-ZZZZZ", nlPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Two nodes created using the same auth key, both should be valid.
|
||||
n60, n60Sig := nodeFromAuthKey(preauth)
|
||||
n61, n61Sig := nodeFromAuthKey(preauth)
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
@@ -593,18 +621,18 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
|
||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
||||
{ID: 60, Key: n60.Public(), KeySignature: n60Sig},
|
||||
{ID: 61, Key: n61.Public(), KeySignature: n61Sig},
|
||||
}),
|
||||
}
|
||||
|
||||
b := &LocalBackend{
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{authority: authority},
|
||||
}
|
||||
b.tkaFilterNetmapLocked(nm)
|
||||
|
||||
want := nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
||||
{ID: 60, Key: n60.Public(), KeySignature: n60Sig},
|
||||
{ID: 61, Key: n61.Public(), KeySignature: n61Sig},
|
||||
})
|
||||
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
||||
return x.Raw32() == y.Raw32()
|
||||
@@ -1182,6 +1210,14 @@ func TestRotationTracker(t *testing.T) {
|
||||
raw32 := [32]byte{idx}
|
||||
return key.NodePublicFromRaw32(go4mem.B(raw32[:]))
|
||||
}
|
||||
|
||||
rd := func(initialKind tka.SigKind, wrappingKey []byte, prevKeys ...key.NodePublic) *tka.RotationDetails {
|
||||
return &tka.RotationDetails{
|
||||
InitialSig: &tka.NodeKeySignature{SigKind: initialKind, WrappingPubkey: wrappingKey},
|
||||
PrevNodeKeys: prevKeys,
|
||||
}
|
||||
}
|
||||
|
||||
n1, n2, n3, n4, n5 := newNK(1), newNK(2), newNK(3), newNK(4), newNK(5)
|
||||
|
||||
pk1, pk2, pk3 := []byte{1}, []byte{2}, []byte{3}
|
||||
@@ -1201,46 +1237,46 @@ func TestRotationTracker(t *testing.T) {
|
||||
{
|
||||
name: "single_prev_key",
|
||||
addDetails: []addDetails{
|
||||
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
|
||||
{np: n1, details: rd(tka.SigDirect, pk1, n2)},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n2}),
|
||||
},
|
||||
{
|
||||
name: "several_prev_keys",
|
||||
addDetails: []addDetails{
|
||||
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk2}},
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n3, n4}, WrappingPubkey: pk1}},
|
||||
{np: n1, details: rd(tka.SigDirect, pk1, n2)},
|
||||
{np: n3, details: rd(tka.SigDirect, pk2, n4)},
|
||||
{np: n2, details: rd(tka.SigDirect, pk1, n3, n4)},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n2, n3, n4}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_latest_wins",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk3}},
|
||||
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
|
||||
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2, n3)},
|
||||
{np: n5, details: rd(tka.SigDirect, pk3, n4)},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_same_chain_length_all_rejected",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
|
||||
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
{np: n5, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4, n5}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_longest_wins",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
|
||||
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
|
||||
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2)},
|
||||
{np: n5, details: rd(tka.SigDirect, pk3, n1, n2, n3)},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
|
||||
},
|
||||
|
||||
@@ -448,7 +448,7 @@ func (pm *profileManager) updateHealth() {
|
||||
if !pm.prefs.Valid() {
|
||||
return
|
||||
}
|
||||
pm.health.SetCheckForUpdates(pm.prefs.AutoUpdate().Check)
|
||||
pm.health.SetAutoUpdatePrefs(pm.prefs.AutoUpdate().Check, pm.prefs.AutoUpdate().Apply)
|
||||
}
|
||||
|
||||
// NewProfile creates and switches to a new unnamed profile. The new profile is
|
||||
|
||||
@@ -150,6 +150,14 @@ func (s *localListener) Run() {
|
||||
tcp4or6 = "tcp6"
|
||||
}
|
||||
|
||||
// while we were backing off and trying again, the context got canceled
|
||||
// so don't bind, just return, because otherwise there will be no way
|
||||
// to close this listener
|
||||
if s.ctx.Err() != nil {
|
||||
s.logf("localListener context closed before binding")
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
|
||||
if err != nil {
|
||||
if s.shouldWarnAboutListenError(err) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -668,7 +669,7 @@ func newTestBackend(t *testing.T) *LocalBackend {
|
||||
var logf logger.Logf = logger.Discard
|
||||
const debug = true
|
||||
if debug {
|
||||
logf = logger.WithPrefix(t.Logf, "... ")
|
||||
logf = logger.WithPrefix(tstest.WhileTestRunningLogger(t), "... ")
|
||||
}
|
||||
|
||||
sys := &tsd.System{}
|
||||
|
||||
@@ -538,7 +538,7 @@ func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultSch
|
||||
return "", fmt.Errorf("invalid port %q", u.Port())
|
||||
}
|
||||
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
u.Host = fmt.Sprintf("%s:%d", u.Hostname(), port)
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
@@ -137,14 +137,13 @@ func TestExpandProxyTargetDev(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "port-only", input: "8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "hostname+port", input: "localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "convert-localhost", input: "http://localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "hostname+port", input: "localhost:8080", expected: "http://localhost:8080"},
|
||||
{name: "no-change", input: "http://127.0.0.1:8080", expected: "http://127.0.0.1:8080"},
|
||||
{name: "include-path", input: "http://127.0.0.1:8080/foo", expected: "http://127.0.0.1:8080/foo"},
|
||||
{name: "https-scheme", input: "https://localhost:8080", expected: "https://127.0.0.1:8080"},
|
||||
{name: "https+insecure-scheme", input: "https+insecure://localhost:8080", expected: "https+insecure://127.0.0.1:8080"},
|
||||
{name: "change-default-scheme", input: "localhost:8080", defaultScheme: "https", expected: "https://127.0.0.1:8080"},
|
||||
{name: "change-supported-schemes", input: "localhost:8080", defaultScheme: "tcp", supportedSchemes: []string{"tcp"}, expected: "tcp://127.0.0.1:8080"},
|
||||
{name: "https-scheme", input: "https://localhost:8080", expected: "https://localhost:8080"},
|
||||
{name: "https+insecure-scheme", input: "https+insecure://localhost:8080", expected: "https+insecure://localhost:8080"},
|
||||
{name: "change-default-scheme", input: "localhost:8080", defaultScheme: "https", expected: "https://localhost:8080"},
|
||||
{name: "change-supported-schemes", input: "localhost:8080", defaultScheme: "tcp", supportedSchemes: []string{"tcp"}, expected: "tcp://localhost:8080"},
|
||||
|
||||
// errors
|
||||
{name: "invalid-port", input: "localhost:9999999", wantErr: true},
|
||||
|
||||
@@ -20,7 +20,8 @@ func New(logger.Logf, string) (ipn.StateStore, error) {
|
||||
|
||||
// Store is an ipn.StateStore that keeps state in memory only.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
cache map[ipn.StateKey][]byte
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
@@ -54,7 +54,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
@@ -71,17 +71,17 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.21.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.24.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/mobile](https://pkg.go.dev/golang.org/x/mobile) ([BSD-3-Clause](https://cs.opensource.google/go/x/mobile/+/c58ccf4b:LICENSE))
|
||||
- [golang.org/x/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.16.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.23.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.6.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.18.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.18.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/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.18.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.26.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.21.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.21.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.16.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))
|
||||
- [golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools) ([BSD-3-Clause](https://cs.opensource.google/go/x/tools/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools) ([BSD-3-Clause](https://cs.opensource.google/go/x/tools/+/v0.22.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))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](Unknown))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
|
||||
@@ -32,7 +32,7 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
@@ -60,12 +60,12 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/prometheus-community/pro-bing](https://pkg.go.dev/github.com/prometheus-community/pro-bing) ([MIT](https://github.com/prometheus-community/pro-bing/blob/v0.4.0/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/03c5a0ccf754/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cfa45674af86/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
|
||||
@@ -74,13 +74,13 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/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.23.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.24.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/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.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.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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.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.21.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.16.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))
|
||||
|
||||
@@ -39,7 +39,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/a09d6be7affa/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
|
||||
@@ -78,13 +78,13 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/d3fa0460f47e/LICENSE.md))
|
||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/3fde5e568aa4/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5db17b287bf1/LICENSE))
|
||||
- [github.com/tailscale/wf](https://pkg.go.dev/github.com/tailscale/wf) ([BSD-3-Clause](https://github.com/tailscale/wf/blob/6fbb0a674ee6/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/03c5a0ccf754/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cfa45674af86/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
@@ -95,19 +95,19 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/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.21.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.24.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.23.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.26.0:LICENSE))
|
||||
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.16.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.6.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.18.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.18.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/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.21.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.21.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.16.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))
|
||||
- [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))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.29.1/LICENSE))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.1/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/LICENSE))
|
||||
- [sigs.k8s.io/yaml/goyaml.v2](https://pkg.go.dev/sigs.k8s.io/yaml/goyaml.v2) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/goyaml.v2/LICENSE))
|
||||
|
||||
@@ -32,7 +32,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/b75a8a7d7eb0/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
@@ -57,7 +57,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/0fe267360a54/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/7601212d8e23/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/6580b55d49ca/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
|
||||
@@ -66,15 +66,15 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/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.23.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.24.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/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.18.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.18.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.26.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.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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.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.21.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.16.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))
|
||||
@@ -82,6 +82,5 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
- [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/) ([zlib/libpng](https://nsis.sourceforge.io/License))
|
||||
- [Wintun](https://www.wintun.net/) ([Prebuilt Binaries License](https://git.zx2c4.com/wintun/tree/prebuilt-binaries-license.txt))
|
||||
- [wireguard-windows](https://git.zx2c4.com/wireguard-windows/) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING))
|
||||
|
||||
@@ -276,6 +276,14 @@ func (m *directManager) rename(old, new string) error {
|
||||
return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
// Explicitly set the permissions on the new file. This ensures that
|
||||
// if we have a umask set which prevents creating world-readable files,
|
||||
// the file will still have the correct permissions once it's renamed
|
||||
// into place. See #12609.
|
||||
if err := m.fs.Chmod(new, 0644); err != nil {
|
||||
return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(old); err != nil {
|
||||
err2 := m.fs.Truncate(old)
|
||||
if err2 != nil {
|
||||
@@ -467,6 +475,14 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
if err := fs.WriteFile(tmpName, data, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
}
|
||||
// Explicitly set the permissions on the temporary file before renaming
|
||||
// it. This ensures that if we have a umask set which prevents creating
|
||||
// world-readable files, the file will still have the correct
|
||||
// permissions once it's renamed into place. See #12609.
|
||||
if err := fs.Chmod(tmpName, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: Chmod: %w", err)
|
||||
}
|
||||
|
||||
return m.rename(tmpName, filename)
|
||||
}
|
||||
|
||||
@@ -475,10 +491,11 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
//
|
||||
// All name parameters are absolute paths.
|
||||
type wholeFileFS interface {
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Rename(oldName, newName string) error
|
||||
Remove(name string) error
|
||||
Chmod(name string, mode os.FileMode) error
|
||||
ReadFile(name string) ([]byte, error)
|
||||
Remove(name string) error
|
||||
Rename(oldName, newName string) error
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Truncate(name string) error
|
||||
WriteFile(name string, contents []byte, perm os.FileMode) error
|
||||
}
|
||||
@@ -502,6 +519,10 @@ func (fs directFS) Stat(name string) (isRegular bool, err error) {
|
||||
return fi.Mode().IsRegular(), nil
|
||||
}
|
||||
|
||||
func (fs directFS) Chmod(name string, mode os.FileMode) error {
|
||||
return os.Chmod(fs.path(name), mode)
|
||||
}
|
||||
|
||||
func (fs directFS) Rename(oldName, newName string) error {
|
||||
return os.Rename(fs.path(oldName), fs.path(newName))
|
||||
}
|
||||
|
||||
43
net/dns/direct_unix_test.go
Normal file
43
net/dns/direct_unix_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build unix
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteFileUmask(t *testing.T) {
|
||||
// Set a umask that disallows world-readable files for the duration of
|
||||
// this test.
|
||||
oldUmask := syscall.Umask(0027)
|
||||
defer syscall.Umask(oldUmask)
|
||||
|
||||
tmp := t.TempDir()
|
||||
fs := directFS{prefix: tmp}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
m := directManager{logf: t.Logf, fs: fs, ctx: ctx, ctxClose: cancel}
|
||||
|
||||
const perms = 0644
|
||||
if err := m.atomicWriteFile(fs, "resolv.conf", []byte("nameserver 8.8.8.8\n"), perms); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure that the created file has the world-readable bit set.
|
||||
fi, err := os.Stat(filepath.Join(tmp, "resolv.conf"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := fi.Mode().Perm(); got != perms {
|
||||
t.Fatalf("file mode: got 0o%o, want 0o%o", got, perms)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -23,6 +24,8 @@ import (
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tstime/rate"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -55,6 +58,11 @@ type Manager struct {
|
||||
os OSConfigurator
|
||||
knobs *controlknobs.Knobs // or nil
|
||||
goos string // if empty, gets set to runtime.GOOS
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
// config is the last configuration we successfully compiled or nil if there
|
||||
// was any failure applying the last configuration.
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
@@ -80,6 +88,26 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker,
|
||||
knobs: knobs,
|
||||
goos: goos,
|
||||
}
|
||||
|
||||
// Rate limit our attempts to correct our DNS configuration.
|
||||
limiter := rate.NewLimiter(1.0/5.0, 1)
|
||||
|
||||
// This will recompile the DNS config, which in turn will requery the system
|
||||
// DNS settings. The recovery func should triggered only when we are missing
|
||||
// upstream nameservers and require them to forward a query.
|
||||
m.resolver.SetMissingUpstreamRecovery(func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
m.logf("DNS resolution failed due to missing upstream nameservers. Recompiling DNS configuration.")
|
||||
m.setLocked(*m.config)
|
||||
}
|
||||
})
|
||||
|
||||
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
|
||||
m.logf("using %T", m.os)
|
||||
return m
|
||||
@@ -89,6 +117,20 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker,
|
||||
func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
|
||||
|
||||
func (m *Manager) Set(cfg Config) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.setLocked(cfg)
|
||||
}
|
||||
|
||||
// setLocked sets the DNS configuration.
|
||||
//
|
||||
// m.mu must be held.
|
||||
func (m *Manager) setLocked(cfg Config) error {
|
||||
syncs.AssertLocked(&m.mu)
|
||||
|
||||
// On errors, the 'set' config is cleared.
|
||||
m.config = nil
|
||||
|
||||
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
cfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
@@ -112,7 +154,9 @@ func (m *Manager) Set(cfg Config) error {
|
||||
m.health.SetDNSOSHealth(err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
m.config = &cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -313,8 +313,9 @@ func (m memFS) Stat(name string) (isRegular bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m memFS) Rename(oldName, newName string) error { panic("TODO") }
|
||||
func (m memFS) Remove(name string) error { panic("TODO") }
|
||||
func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") }
|
||||
func (m memFS) Rename(oldName, newName string) error { panic("TODO") }
|
||||
func (m memFS) Remove(name string) error { panic("TODO") }
|
||||
func (m memFS) ReadFile(name string) ([]byte, error) {
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -212,6 +211,12 @@ type forwarder struct {
|
||||
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
||||
// resolver lookup.
|
||||
cloudHostFallback []resolverAndDelay
|
||||
|
||||
// missingUpstreamRecovery, if non-nil, is set called when a SERVFAIL is
|
||||
// returned due to missing upstream resolvers.
|
||||
//
|
||||
// This should attempt to properly (re)set the upstream resolvers.
|
||||
missingUpstreamRecovery func()
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, knobs *controlknobs.Knobs) *forwarder {
|
||||
@@ -219,11 +224,12 @@ func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkS
|
||||
panic("nil netMon")
|
||||
}
|
||||
f := &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
netMon: netMon,
|
||||
linkSel: linkSel,
|
||||
dialer: dialer,
|
||||
controlKnobs: knobs,
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
netMon: netMon,
|
||||
linkSel: linkSel,
|
||||
dialer: dialer,
|
||||
controlKnobs: knobs,
|
||||
missingUpstreamRecovery: func() {},
|
||||
}
|
||||
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
||||
return f
|
||||
@@ -883,21 +889,11 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
metricDNSFwdErrorNoUpstream.Add(1)
|
||||
f.logf("no upstream resolvers set, returning SERVFAIL")
|
||||
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
// On apple, having no upstream resolvers here is the result a race condition where
|
||||
// we've tried a reconfig after a major link change but the system has not yet set
|
||||
// the resolvers for the new link. We use SystemConfiguration to query nameservers, and
|
||||
// the timing of when that will give us the "right" answer is non-deterministic.
|
||||
//
|
||||
// This will typically happen on sleep-wake cycles with a Wifi interface where
|
||||
// it takes some random amount of time (after telling us that the interface exists)
|
||||
// for the system to configure the dns servers.
|
||||
//
|
||||
// Repolling the network monitor here is a bit odd, but if we're
|
||||
// seeing DNS queries, it's likely that the network is now fully configured, and it's
|
||||
// an ideal time to to requery for the nameservers.
|
||||
f.logf("injecting network monitor event to attempt to refresh the resolvers")
|
||||
f.netMon.InjectEvent()
|
||||
// Attempt to recompile the DNS configuration
|
||||
// If we are being asked to forward queries and we have no
|
||||
// nameservers, the network is in a bad state.
|
||||
if f.missingUpstreamRecovery != nil {
|
||||
f.missingUpstreamRecovery()
|
||||
}
|
||||
|
||||
res, err := servfailResponse(query)
|
||||
|
||||
@@ -244,6 +244,15 @@ func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, k
|
||||
return r
|
||||
}
|
||||
|
||||
// SetMissingUpstreamRecovery sets a callback to be called upon encountering
|
||||
// a SERVFAIL due to missing upstream resolvers.
|
||||
//
|
||||
// This call should only happen before the resolver is used. It is not safe
|
||||
// for concurrent use.
|
||||
func (r *Resolver) SetMissingUpstreamRecovery(f func()) {
|
||||
r.forwarder.missingUpstreamRecovery = f
|
||||
}
|
||||
|
||||
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
|
||||
|
||||
func (r *Resolver) SetConfig(cfg Config) error {
|
||||
|
||||
@@ -159,6 +159,10 @@ func (fs wslFS) Stat(name string) (isRegular bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fs wslFS) Chmod(name string, perm os.FileMode) error {
|
||||
return wslRun(fs.cmd("chmod", "--", fmt.Sprintf("%04o", perm), name))
|
||||
}
|
||||
|
||||
func (fs wslFS) Rename(oldName, newName string) error {
|
||||
return wslRun(fs.cmd("mv", "--", oldName, newName))
|
||||
}
|
||||
|
||||
9
net/netns/mksyscall.go
Normal file
9
net/netns/mksyscall.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netns
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
|
||||
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
|
||||
|
||||
//sys getBestInterfaceEx(sockaddr *winipcfg.RawSockaddrInet, bestIfaceIndex *uint32) (ret error) = iphlpapi.GetBestInterfaceEx
|
||||
@@ -38,7 +38,7 @@ var bindToInterfaceByRoute atomic.Bool
|
||||
// route information to bind to a particular interface. It is the same as
|
||||
// setting the TS_BIND_TO_INTERFACE_BY_ROUTE.
|
||||
//
|
||||
// Currently, this only changes the behaviour on macOS.
|
||||
// Currently, this only changes the behaviour on macOS and Windows.
|
||||
func SetBindToInterfaceByRoute(v bool) {
|
||||
bindToInterfaceByRoute.Store(v)
|
||||
}
|
||||
|
||||
@@ -89,16 +89,10 @@ func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string)
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// No port number; use the string directly.
|
||||
host = address
|
||||
}
|
||||
|
||||
// If the address doesn't parse, use the default index.
|
||||
addr, err := netip.ParseAddr(host)
|
||||
addr, err := parseAddress(address)
|
||||
if err != nil {
|
||||
logf("[unexpected] netns: error parsing address %q: %v", host, err)
|
||||
logf("[unexpected] netns: error parsing address %q: %v", address, err)
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
|
||||
21
net/netns/netns_dw.go
Normal file
21
net/netns/netns_dw.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin || windows
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
func parseAddress(address string) (addr netip.Addr, err error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// error means the string didn't contain a port number, so use the string directly
|
||||
host = address
|
||||
}
|
||||
|
||||
return netip.ParseAddr(host)
|
||||
}
|
||||
@@ -4,14 +4,18 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tsconst"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -26,20 +30,34 @@ func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 {
|
||||
return iface.IfIndex
|
||||
}
|
||||
|
||||
func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
|
||||
return controlC
|
||||
func defaultInterfaceIndex(family winipcfg.AddressFamily) (uint32, error) {
|
||||
iface, err := netmon.GetWindowsDefault(family)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return interfaceIndex(iface), nil
|
||||
}
|
||||
|
||||
func control(logf logger.Logf, _ *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
return controlC(logf, network, address, c)
|
||||
}
|
||||
}
|
||||
|
||||
var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_ROUTE")
|
||||
|
||||
// controlC binds c to the Windows interface that holds a default
|
||||
// route, and is not the Tailscale WinTun interface.
|
||||
func controlC(network, address string, c syscall.RawConn) error {
|
||||
if strings.HasPrefix(address, "127.") {
|
||||
func controlC(logf logger.Logf, network, address string, c syscall.RawConn) (err error) {
|
||||
if isLocalhost(address) {
|
||||
// Don't bind to an interface for localhost connections,
|
||||
// otherwise we get:
|
||||
// connectex: The requested address is not valid in its context
|
||||
// (The derphttp tests were failing)
|
||||
return nil
|
||||
}
|
||||
|
||||
canV4, canV6 := false, false
|
||||
switch network {
|
||||
case "tcp", "udp":
|
||||
@@ -50,29 +68,107 @@ func controlC(network, address string, c syscall.RawConn) error {
|
||||
canV6 = true
|
||||
}
|
||||
|
||||
var defIfaceIdxV4, defIfaceIdxV6 uint32
|
||||
if canV4 {
|
||||
iface, err := netmon.GetWindowsDefault(windows.AF_INET)
|
||||
defIfaceIdxV4, err = defaultInterfaceIndex(windows.AF_INET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("defaultInterfaceIndex(AF_INET): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if canV6 {
|
||||
iface, err := netmon.GetWindowsDefault(windows.AF_INET6)
|
||||
defIfaceIdxV6, err = defaultInterfaceIndex(windows.AF_INET6)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("defaultInterfaceIndex(AF_INET6): %w", err)
|
||||
}
|
||||
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ifaceIdxV4, ifaceIdxV6 uint32
|
||||
if useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv(); useRoute {
|
||||
addr, err := parseAddress(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parseAddress: %w", err)
|
||||
}
|
||||
|
||||
if canV4 && (addr.Is4() || addr.Is4In6()) {
|
||||
addrV4 := addr.Unmap()
|
||||
ifaceIdxV4, err = getInterfaceIndex(logf, addrV4, defIfaceIdxV4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getInterfaceIndex(%v): %w", addrV4, err)
|
||||
}
|
||||
}
|
||||
|
||||
if canV6 && addr.Is6() {
|
||||
ifaceIdxV6, err = getInterfaceIndex(logf, addr, defIfaceIdxV6)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getInterfaceIndex(%v): %w", addr, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ifaceIdxV4, ifaceIdxV6 = defIfaceIdxV4, defIfaceIdxV6
|
||||
}
|
||||
|
||||
if canV4 {
|
||||
if err := bindSocket4(c, ifaceIdxV4); err != nil {
|
||||
return fmt.Errorf("bindSocket4(%d): %w", ifaceIdxV4, err)
|
||||
}
|
||||
}
|
||||
|
||||
if canV6 {
|
||||
if err := bindSocket6(c, ifaceIdxV6); err != nil {
|
||||
return fmt.Errorf("bindSocket6(%d): %w", ifaceIdxV6, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInterfaceIndex(logf logger.Logf, addr netip.Addr, defaultIdx uint32) (idx uint32, err error) {
|
||||
idx, err = interfaceIndexFor(addr)
|
||||
if err != nil {
|
||||
return defaultIdx, fmt.Errorf("interfaceIndexFor: %w", err)
|
||||
}
|
||||
|
||||
isTS, err := isTailscaleInterface(idx)
|
||||
if err != nil {
|
||||
return defaultIdx, fmt.Errorf("isTailscaleInterface: %w", err)
|
||||
}
|
||||
if isTS {
|
||||
return defaultIdx, nil
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func isTailscaleInterface(ifaceIdx uint32) (bool, error) {
|
||||
ifaceLUID, err := winipcfg.LUIDFromIndex(ifaceIdx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
iface, err := ifaceLUID.Interface()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
result := iface.Type == winipcfg.IfTypePropVirtual &&
|
||||
strings.Contains(iface.Description(), tsconst.WintunInterfaceDesc)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func interfaceIndexFor(addr netip.Addr) (uint32, error) {
|
||||
var sockaddr winipcfg.RawSockaddrInet
|
||||
if err := sockaddr.SetAddr(addr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var idx uint32
|
||||
if err := getBestInterfaceEx(&sockaddr, &idx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// sockoptBoundInterface is the value of IP_UNICAST_IF and IPV6_UNICAST_IF.
|
||||
//
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
|
||||
|
||||
112
net/netns/netns_windows_test.go
Normal file
112
net/netns/netns_windows_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/tsconst"
|
||||
)
|
||||
|
||||
func TestGetInterfaceIndex(t *testing.T) {
|
||||
oldVal := bindToInterfaceByRoute.Load()
|
||||
t.Cleanup(func() { bindToInterfaceByRoute.Store(oldVal) })
|
||||
bindToInterfaceByRoute.Store(true)
|
||||
|
||||
defIfaceIdxV4, err := defaultInterfaceIndex(windows.AF_INET)
|
||||
if err != nil {
|
||||
t.Fatalf("defaultInterfaceIndex(AF_INET) failed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
addr string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "IP_and_port",
|
||||
addr: "8.8.8.8:53",
|
||||
},
|
||||
{
|
||||
name: "bare_ip",
|
||||
addr: "8.8.8.8",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
addr, err := parseAddress(tc.addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
idx, err := getInterfaceIndex(t.Logf, addr, defIfaceIdxV4)
|
||||
if err != nil {
|
||||
if tc.err == "" {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
if errstr := err.Error(); errstr != tc.err {
|
||||
t.Errorf("expected error %q, got %q", errstr, tc.err)
|
||||
}
|
||||
} else {
|
||||
t.Logf("getInterfaceIndex(%q) = %d", tc.addr, idx)
|
||||
if tc.err != "" {
|
||||
t.Fatalf("wanted error %q", tc.err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NoTailscale", func(t *testing.T) {
|
||||
tsIdx, ok, err := tailscaleInterfaceIndex()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Skip("no tailscale interface on this machine")
|
||||
}
|
||||
|
||||
defaultIdx, err := defaultInterfaceIndex(windows.AF_INET)
|
||||
if err != nil {
|
||||
t.Fatalf("defaultInterfaceIndex(AF_INET) failed: %v", err)
|
||||
}
|
||||
|
||||
addr, err := parseAddress("100.100.100.100:53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
idx, err := getInterfaceIndex(t.Logf, addr, defaultIdx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("tailscaleIdx=%d defaultIdx=%d idx=%d", tsIdx, defaultIdx, idx)
|
||||
|
||||
if idx == tsIdx {
|
||||
t.Fatalf("got idx=%d; wanted not Tailscale interface", idx)
|
||||
} else if idx != defaultIdx {
|
||||
t.Fatalf("got idx=%d, want %d", idx, defaultIdx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func tailscaleInterfaceIndex() (idx uint32, found bool, err error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_INET, winipcfg.GAAFlagIncludeAllInterfaces)
|
||||
if err != nil {
|
||||
return idx, false, err
|
||||
}
|
||||
|
||||
for _, iface := range ifs {
|
||||
if iface.IfType != winipcfg.IfTypePropVirtual {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(iface.Description(), tsconst.WintunInterfaceDesc) {
|
||||
return iface.IfIndex, true, nil
|
||||
}
|
||||
}
|
||||
return idx, false, nil
|
||||
}
|
||||
53
net/netns/zsyscall_windows.go
Normal file
53
net/netns/zsyscall_windows.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||
|
||||
procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx")
|
||||
)
|
||||
|
||||
func getBestInterfaceEx(sockaddr *winipcfg.RawSockaddrInet, bestIfaceIndex *uint32) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procGetBestInterfaceEx.Addr(), 2, uintptr(unsafe.Pointer(sockaddr)), uintptr(unsafe.Pointer(bestIfaceIndex)), 0)
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
64
net/netutil/default_interface_portable.go
Normal file
64
net/netutil/default_interface_portable.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// DefaultInterfacePortable looks up the current default interface using a portable lookup method that
|
||||
// works on most systems with a BSD style socket interface.
|
||||
//
|
||||
// Returns the interface name and IP address of the default route interface.
|
||||
//
|
||||
// If the default cannot be determined, an error is returned.
|
||||
// Requires that there is a route on the system servicing UDP IPv4.
|
||||
func DefaultInterfacePortable() (string, netip.Addr, error) {
|
||||
// Note: UDP dial just performs a connect(2), and doesn't actually send a packet.
|
||||
c, err := net.Dial("udp4", "8.8.8.8:53")
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, err
|
||||
}
|
||||
laddr := c.LocalAddr().(*net.UDPAddr)
|
||||
c.Close()
|
||||
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
iface *net.Interface
|
||||
ipnet *net.IPNet
|
||||
)
|
||||
for _, ifc := range ifs {
|
||||
addrs, err := ifc.Addrs()
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipn, ok := addr.(*net.IPNet); ok {
|
||||
if ipn.Contains(laddr.IP) {
|
||||
if ipnet == nil {
|
||||
ipnet = ipn
|
||||
iface = &ifc
|
||||
} else {
|
||||
newSize, _ := ipn.Mask.Size()
|
||||
oldSize, _ := ipnet.Mask.Size()
|
||||
if newSize > oldSize {
|
||||
ipnet = ipn
|
||||
iface = &ifc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if iface == nil {
|
||||
return "", netip.Addr{}, errors.New("no default interface")
|
||||
}
|
||||
return iface.Name, laddr.AddrPort().Addr(), nil
|
||||
}
|
||||
25
net/netutil/default_interface_portable_test.go
Normal file
25
net/netutil/default_interface_portable_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultInterfacePortable(t *testing.T) {
|
||||
ifName, addr, err := DefaultInterfacePortable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("Default interface: %s", ifName)
|
||||
t.Logf("Default address: %s", addr)
|
||||
|
||||
if ifName == "" {
|
||||
t.Fatal("Default interface name is empty")
|
||||
}
|
||||
if !addr.IsValid() {
|
||||
t.Fatal("Default address is invalid")
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,10 @@ import (
|
||||
)
|
||||
|
||||
type stunStats struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
readIPv4 int
|
||||
// +checklocks:mu
|
||||
readIPv6 int
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,11 @@ type netConn struct {
|
||||
afterReadDeadline atomic.Bool
|
||||
|
||||
readMu sync.Mutex
|
||||
eofed bool
|
||||
// eofed is true if the reader should return io.EOF from the Read call.
|
||||
//
|
||||
// +checklocks:readMu
|
||||
eofed bool
|
||||
// +checklocks:readMu
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ import (
|
||||
// given localhost:port corresponds to.
|
||||
type Mapper struct {
|
||||
mu sync.Mutex
|
||||
m map[string]map[netip.AddrPort]netip.Addr // proto ("tcp", "udp") => ephemeral => tailscale IP
|
||||
|
||||
// m holds the mapping from localhost IP:ports to Tailscale IPs. It is
|
||||
// keyed first by the protocol ("tcp" or "udp"), then by the IP:port.
|
||||
//
|
||||
// +checklocks:mu
|
||||
m map[string]map[netip.AddrPort]netip.Addr
|
||||
}
|
||||
|
||||
// RegisterIPPortIdentity registers a given node (identified by its
|
||||
|
||||
@@ -513,7 +513,6 @@ main() {
|
||||
;;
|
||||
pacman)
|
||||
set -x
|
||||
$SUDO pacman -Sy
|
||||
$SUDO pacman -S tailscale --noconfirm
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
|
||||
# nix-direnv cache busting line: sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=
|
||||
|
||||
@@ -141,7 +141,8 @@ type CapabilityVersion int
|
||||
// - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
|
||||
// - 99: 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
|
||||
// - 100: 2024-06-18: Client supports filtertype.Match.SrcCaps (issue #12542)
|
||||
const CurrentCapabilityVersion CapabilityVersion = 100
|
||||
// - 101: 2024-07-01: Client supports SSH agent forwarding when handling connections with /bin/su
|
||||
const CurrentCapabilityVersion CapabilityVersion = 101
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -2202,10 +2203,6 @@ const (
|
||||
// always giving WireGuard the full netmap, even for idle peers.
|
||||
NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim"
|
||||
|
||||
// NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization.
|
||||
// See Issue 150.
|
||||
NodeAttrDebugDisableDRPO NodeCapability = "debug-disable-drpo"
|
||||
|
||||
// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
|
||||
// disabled if WPAD is present on the network.
|
||||
NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac"
|
||||
|
||||
69
tka/sig.go
69
tka/sig.go
@@ -6,6 +6,7 @@ package tka
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/hdevalence/ed25519consensus"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
@@ -311,9 +313,9 @@ type RotationDetails struct {
|
||||
// PrevNodeKeys is a list of node keys which have been rotated out.
|
||||
PrevNodeKeys []key.NodePublic
|
||||
|
||||
// WrappingPubkey is the public key which has been authorized to sign
|
||||
// InitialSig is the first signature in the chain which led to
|
||||
// this rotating signature.
|
||||
WrappingPubkey []byte
|
||||
InitialSig *NodeKeySignature
|
||||
}
|
||||
|
||||
// rotationDetails returns the RotationDetails for a SigRotation signature.
|
||||
@@ -337,7 +339,7 @@ func (s *NodeKeySignature) rotationDetails() (*RotationDetails, error) {
|
||||
}
|
||||
nested = nested.Nested
|
||||
}
|
||||
sri.WrappingPubkey = nested.WrappingPubkey
|
||||
sri.InitialSig = nested
|
||||
return sri, nil
|
||||
}
|
||||
|
||||
@@ -379,3 +381,64 @@ func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.Marsha
|
||||
|
||||
return newSig.Serialize(), nil
|
||||
}
|
||||
|
||||
// SignByCredential signs a node public key by a private key which has its
|
||||
// signing authority delegated by a SigCredential signature. This is used by
|
||||
// wrapped auth keys.
|
||||
func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) {
|
||||
if wrapped.SigKind != SigCredential {
|
||||
return nil, fmt.Errorf("wrapped signature must be a credential, got %v", wrapped.SigKind)
|
||||
}
|
||||
|
||||
nk, err := nodeKey.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
}
|
||||
|
||||
sig := &NodeKeySignature{
|
||||
SigKind: SigRotation,
|
||||
Pubkey: nk,
|
||||
Nested: wrapped,
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
sig.Signature = ed25519.Sign(privKey, sigHash[:])
|
||||
return sig.Serialize(), nil
|
||||
}
|
||||
|
||||
// DecodeWrappedAuthkey separates wrapping information from an authkey, if any.
|
||||
// In all cases the authkey is returned, sans wrapping information if any.
|
||||
//
|
||||
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
|
||||
// and private key.
|
||||
func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) {
|
||||
authKey, suffix, found := strings.Cut(wrappedAuthKey, "--TL")
|
||||
if !found {
|
||||
return wrappedAuthKey, false, nil, nil
|
||||
}
|
||||
sigBytes, privBytes, found := strings.Cut(suffix, "-")
|
||||
if !found {
|
||||
// TODO: propagate these errors to `tailscale up` output?
|
||||
logf("decoding wrapped auth-key: did not find delimiter")
|
||||
return wrappedAuthKey, false, nil, nil
|
||||
}
|
||||
|
||||
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: signature decode: %v", err)
|
||||
return wrappedAuthKey, false, nil, nil
|
||||
}
|
||||
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: priv decode: %v", err)
|
||||
return wrappedAuthKey, false, nil, nil
|
||||
}
|
||||
|
||||
sig = new(NodeKeySignature)
|
||||
if err := sig.Unserialize(rawSig); err != nil {
|
||||
logf("decoding wrapped auth-key: signature: %v", err)
|
||||
return wrappedAuthKey, false, nil, nil
|
||||
}
|
||||
priv = ed25519.PrivateKey(rawPriv)
|
||||
|
||||
return authKey, true, sig, priv
|
||||
}
|
||||
|
||||
@@ -356,7 +356,11 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
|
||||
return sig
|
||||
},
|
||||
want: &RotationDetails{
|
||||
WrappingPubkey: cPub,
|
||||
InitialSig: &NodeKeySignature{
|
||||
SigKind: SigCredential,
|
||||
KeyID: pub,
|
||||
WrappingPubkey: cPub,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -382,8 +386,13 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
|
||||
return sig
|
||||
},
|
||||
want: &RotationDetails{
|
||||
WrappingPubkey: cPub,
|
||||
PrevNodeKeys: []key.NodePublic{n1.Public()},
|
||||
InitialSig: &NodeKeySignature{
|
||||
SigKind: SigDirect,
|
||||
Pubkey: n1pub,
|
||||
KeyID: pub,
|
||||
WrappingPubkey: cPub,
|
||||
},
|
||||
PrevNodeKeys: []key.NodePublic{n1.Public()},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -418,13 +427,23 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
|
||||
return sig
|
||||
},
|
||||
want: &RotationDetails{
|
||||
WrappingPubkey: cPub,
|
||||
PrevNodeKeys: []key.NodePublic{n2.Public(), n1.Public()},
|
||||
InitialSig: &NodeKeySignature{
|
||||
SigKind: SigDirect,
|
||||
Pubkey: n1pub,
|
||||
KeyID: pub,
|
||||
WrappingPubkey: cPub,
|
||||
},
|
||||
PrevNodeKeys: []key.NodePublic{n2.Public(), n1.Public()},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.want != nil {
|
||||
initialHash := tt.want.InitialSig.SigHash()
|
||||
tt.want.InitialSig.Signature = ed25519.Sign(priv, initialHash[:])
|
||||
}
|
||||
|
||||
sig := tt.sigFn()
|
||||
if err := sig.verifySignature(tt.nodeKey, k); err != nil {
|
||||
t.Fatalf("verifySignature(node) failed: %v", err)
|
||||
@@ -439,3 +458,42 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeWrappedAuthkey(t *testing.T) {
|
||||
k, isWrapped, sig, priv := DecodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
|
||||
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
||||
}
|
||||
if sig != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
||||
}
|
||||
if priv != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
|
||||
}
|
||||
|
||||
k, isWrapped, sig, priv = DecodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
|
||||
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if !isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
||||
}
|
||||
|
||||
if sig == nil {
|
||||
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
|
||||
t.Error("signature failed to verify")
|
||||
}
|
||||
|
||||
// Make sure the private is correct by using it.
|
||||
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
|
||||
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
|
||||
t.Error("failed to use priv")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ fi
|
||||
# case, cmd/cloner invokes go with GO111MODULE=off at some stage.
|
||||
#
|
||||
# Anyway, build gocross in a stripped down universe.
|
||||
gocross_path="gocross"
|
||||
gocross_path="./gocross"
|
||||
gocross_ok=0
|
||||
wantver="$(git rev-parse HEAD)"
|
||||
if [[ -x "$gocross_path" ]]; then
|
||||
|
||||
27
tool/gocross/gocross_wrapper_test.go
Normal file
27
tool/gocross/gocross_wrapper_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGocrossWrapper(t *testing.T) {
|
||||
for i := range 2 { // once to build gocross; second to test it's cached
|
||||
cmd := exec.Command("./gocross-wrapper.sh", "version")
|
||||
cmd.Env = append(os.Environ(), "CI=true", "NOBASHDEBUG=false") // for "set -x" verbosity
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("gocross-wrapper.sh failed: %v\n%s", err, out)
|
||||
}
|
||||
if i > 0 && !strings.Contains(string(out), "gocross_ok=1\n") {
|
||||
t.Errorf("expected to find 'gocross-ok=1'; got output:\n%s", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,7 +645,7 @@ func (s *Server) start() (reterr error) {
|
||||
s.localAPIServer = &http.Server{Handler: lah}
|
||||
s.lb.ConfigureWebClient(s.localClient)
|
||||
go func() {
|
||||
if err := s.localAPIServer.Serve(lal); err != nil {
|
||||
if err := s.localAPIServer.Serve(lal); err != nil && err != http.ErrServerClosed {
|
||||
s.logf("localapi serve error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -250,19 +250,25 @@ type HandlerOptions struct {
|
||||
// for each bucket based on the contained parameters.
|
||||
BucketedStats *BucketedStatsOptions
|
||||
|
||||
// OnStart is called inline before ServeHTTP is called. Optional.
|
||||
OnStart OnStartFunc
|
||||
|
||||
// OnError is called if the handler returned a HTTPError. This
|
||||
// is intended to be used to present pretty error pages if
|
||||
// the user agent is determined to be a browser.
|
||||
OnError ErrorHandlerFunc
|
||||
|
||||
// OnCompletion is called when ServeHTTP is finished and gets
|
||||
// useful data that the implementor can use for metrics.
|
||||
// OnCompletion is called inline when ServeHTTP is finished and gets
|
||||
// useful data that the implementor can use for metrics. Optional.
|
||||
OnCompletion OnCompletionFunc
|
||||
}
|
||||
|
||||
// ErrorHandlerFunc is called to present a error response.
|
||||
type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, HTTPError)
|
||||
|
||||
// OnStartFunc is called before ServeHTTP is called.
|
||||
type OnStartFunc func(*http.Request, AccessLogRecord)
|
||||
|
||||
// OnCompletionFunc is called when ServeHTTP is finished and gets
|
||||
// useful data that the implementor can use for metrics.
|
||||
type OnCompletionFunc func(*http.Request, AccessLogRecord)
|
||||
@@ -336,6 +342,10 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if fn := h.opts.OnStart; fn != nil {
|
||||
fn(r, msg)
|
||||
}
|
||||
|
||||
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.opts.Logf}
|
||||
|
||||
// In case the handler panics, we want to recover and continue logging the
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/vizerror"
|
||||
@@ -485,8 +486,15 @@ func TestStdHandler(t *testing.T) {
|
||||
Step: time.Second,
|
||||
})
|
||||
|
||||
var onStartRecord, onCompletionRecord AccessLogRecord
|
||||
rec := noopHijacker{httptest.NewRecorder(), false}
|
||||
h := StdHandler(test.rh, HandlerOptions{Logf: logf, Now: clock.Now, OnError: test.errHandler})
|
||||
h := StdHandler(test.rh, HandlerOptions{
|
||||
Logf: logf,
|
||||
Now: clock.Now,
|
||||
OnError: test.errHandler,
|
||||
OnStart: func(r *http.Request, alr AccessLogRecord) { onStartRecord = alr },
|
||||
OnCompletion: func(r *http.Request, alr AccessLogRecord) { onCompletionRecord = alr },
|
||||
})
|
||||
h.ServeHTTP(&rec, test.r)
|
||||
res := rec.Result()
|
||||
if res.StatusCode != test.wantCode {
|
||||
@@ -502,6 +510,13 @@ func TestStdHandler(t *testing.T) {
|
||||
}
|
||||
return e.Error()
|
||||
})
|
||||
if diff := cmp.Diff(onStartRecord, test.wantLog, errTransform, cmpopts.IgnoreFields(
|
||||
AccessLogRecord{}, "Time", "Seconds", "Code", "Err")); diff != "" {
|
||||
t.Errorf("onStart callback returned unexpected request log (-got+want):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(onCompletionRecord, test.wantLog, errTransform); diff != "" {
|
||||
t.Errorf("onCompletion callback returned incorrect request log (-got+want):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(logs[0], test.wantLog, errTransform); diff != "" {
|
||||
t.Errorf("handler wrote incorrect request log (-got+want):\n%s", diff)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
// Package lazy provides types for lazily initialized values.
|
||||
package lazy
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// nilErrPtr is a sentinel *error value for SyncValue.err to signal
|
||||
// that SyncValue.v is valid.
|
||||
var nilErrPtr = ptr.To[error](nil)
|
||||
|
||||
// SyncValue is a lazily computed value.
|
||||
//
|
||||
@@ -17,7 +26,17 @@ import "sync"
|
||||
type SyncValue[T any] struct {
|
||||
once sync.Once
|
||||
v T
|
||||
err error
|
||||
|
||||
// err is either:
|
||||
// * nil, if not yet computed
|
||||
// * nilErrPtr, if completed and nil
|
||||
// * non-nil and not nilErrPtr on error.
|
||||
//
|
||||
// It is an atomic.Pointer so it can be read outside of the sync.Once.Do.
|
||||
//
|
||||
// Writes to err must happen after a write to v so a caller seeing a non-nil
|
||||
// err can safely read v.
|
||||
err atomic.Pointer[error]
|
||||
}
|
||||
|
||||
// Set attempts to set z's value to val, and reports whether it succeeded.
|
||||
@@ -26,6 +45,7 @@ func (z *SyncValue[T]) Set(val T) bool {
|
||||
var wasSet bool
|
||||
z.once.Do(func() {
|
||||
z.v = val
|
||||
z.err.Store(nilErrPtr) // after write to z.v; see docs
|
||||
wasSet = true
|
||||
})
|
||||
return wasSet
|
||||
@@ -41,15 +61,63 @@ func (z *SyncValue[T]) MustSet(val T) {
|
||||
// Get returns z's value, calling fill to compute it if necessary.
|
||||
// f is called at most once.
|
||||
func (z *SyncValue[T]) Get(fill func() T) T {
|
||||
z.once.Do(func() { z.v = fill() })
|
||||
z.once.Do(func() {
|
||||
z.v = fill()
|
||||
z.err.Store(nilErrPtr) // after write to z.v; see docs
|
||||
})
|
||||
return z.v
|
||||
}
|
||||
|
||||
// GetErr returns z's value, calling fill to compute it if necessary.
|
||||
// f is called at most once, and z remembers both of fill's outputs.
|
||||
func (z *SyncValue[T]) GetErr(fill func() (T, error)) (T, error) {
|
||||
z.once.Do(func() { z.v, z.err = fill() })
|
||||
return z.v, z.err
|
||||
z.once.Do(func() {
|
||||
var err error
|
||||
z.v, err = fill()
|
||||
|
||||
// Update z.err after z.v; see field docs.
|
||||
if err != nil {
|
||||
z.err.Store(ptr.To(err))
|
||||
} else {
|
||||
z.err.Store(nilErrPtr)
|
||||
}
|
||||
})
|
||||
return z.v, *z.err.Load()
|
||||
}
|
||||
|
||||
// Peek returns z's value and a boolean indicating whether the value has been
|
||||
// set successfully. If a value has not been set, the zero value of T is
|
||||
// returned.
|
||||
//
|
||||
// This function is safe to call concurrently with Get/GetErr/Set, but it's
|
||||
// undefined whether a value set by a concurrent call will be visible to Peek.
|
||||
//
|
||||
// To get any error that's been set, use PeekErr.
|
||||
//
|
||||
// If GetErr's fill function returned a valid T and an non-nil error, Peek
|
||||
// discards that valid T value. PeekErr returns both.
|
||||
func (z *SyncValue[T]) Peek() (v T, ok bool) {
|
||||
if z.err.Load() == nilErrPtr {
|
||||
return z.v, true
|
||||
}
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// PeekErr returns z's value and error and a boolean indicating whether the
|
||||
// value or error has been set. If ok is false, T and err are the zero value.
|
||||
//
|
||||
// This function is safe to call concurrently with Get/GetErr/Set, but it's
|
||||
// undefined whether a value set by a concurrent call will be visible to Peek.
|
||||
//
|
||||
// Unlike Peek, PeekErr reports ok if either v or err has been set, not just v,
|
||||
// and returns both the T and err returned by GetErr's fill function.
|
||||
func (z *SyncValue[T]) PeekErr() (v T, err error, ok bool) {
|
||||
if e := z.err.Load(); e != nil {
|
||||
return z.v, *e, true
|
||||
}
|
||||
var zero T
|
||||
return zero, nil, false
|
||||
}
|
||||
|
||||
// SyncFunc wraps a function to make it lazy.
|
||||
|
||||
@@ -5,6 +5,7 @@ package lazy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
@@ -16,6 +17,11 @@ func TestSyncValue(t *testing.T) {
|
||||
if got != 42 {
|
||||
t.Fatalf("got %v; want 42", got)
|
||||
}
|
||||
if p, ok := lt.Peek(); !ok {
|
||||
t.Fatalf("Peek failed")
|
||||
} else if p != 42 {
|
||||
t.Fatalf("Peek got %v; want 42", p)
|
||||
}
|
||||
}))
|
||||
if n != 0 {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
@@ -45,6 +51,12 @@ func TestSyncValueErr(t *testing.T) {
|
||||
if got != 0 || err != wantErr {
|
||||
t.Fatalf("got %v, %v; want 0, %v", got, err, wantErr)
|
||||
}
|
||||
|
||||
if p, ok := lt.Peek(); !ok {
|
||||
t.Fatalf("Peek failed")
|
||||
} else if got != 0 {
|
||||
t.Fatalf("Peek got %v; want 0", p)
|
||||
}
|
||||
}))
|
||||
if n != 0 {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
@@ -59,6 +71,11 @@ func TestSyncValueSet(t *testing.T) {
|
||||
if lt.Set(43) {
|
||||
t.Fatalf("Set succeeded after first Set")
|
||||
}
|
||||
if p, ok := lt.Peek(); !ok {
|
||||
t.Fatalf("Peek failed")
|
||||
} else if p != 42 {
|
||||
t.Fatalf("Peek got %v; want 42", p)
|
||||
}
|
||||
n := int(testing.AllocsPerRun(1000, func() {
|
||||
got := lt.Get(fortyTwo)
|
||||
if got != 42 {
|
||||
@@ -81,6 +98,30 @@ func TestSyncValueMustSet(t *testing.T) {
|
||||
lt.MustSet(43)
|
||||
}
|
||||
|
||||
func TestSyncValueErrPeek(t *testing.T) {
|
||||
var sv SyncValue[int]
|
||||
sv.GetErr(func() (int, error) {
|
||||
return 123, errors.New("boom")
|
||||
})
|
||||
p, ok := sv.Peek()
|
||||
if ok {
|
||||
t.Error("unexpected Peek success")
|
||||
}
|
||||
if p != 0 {
|
||||
t.Fatalf("Peek got %v; want 0", p)
|
||||
}
|
||||
p, err, ok := sv.PeekErr()
|
||||
if !ok {
|
||||
t.Errorf("PeekErr ok=false; want true on error")
|
||||
}
|
||||
if got, want := fmt.Sprint(err), "boom"; got != want {
|
||||
t.Errorf("PeekErr error=%v; want %v", got, want)
|
||||
}
|
||||
if p != 123 {
|
||||
t.Fatalf("PeekErr got %v; want 123", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncValueConcurrent(t *testing.T) {
|
||||
var (
|
||||
lt SyncValue[int]
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
jsonexp "github.com/go-json-experiment/json"
|
||||
jsontext "github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
@@ -225,6 +228,13 @@ func (v Slice[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.ж)
|
||||
}
|
||||
|
||||
var _ jsonexp.MarshalerV2 = Slice[string]{nil}
|
||||
|
||||
// MarshalJSON implements github.com/go-json-experiment/json.MarshalerV2.
|
||||
func (v Slice[T]) MarshalJSONV2(e *jsontext.Encoder, opts jsonexp.Options) error {
|
||||
return jsonexp.MarshalEncode(e, v.ж, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
|
||||
return unmarshalSliceFromJSON(b, &v.ж)
|
||||
|
||||
@@ -8,6 +8,8 @@ package linuxfw
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -128,8 +130,13 @@ func (n *fakeIPTables) DeleteChain(table, chain string) error {
|
||||
|
||||
func NewFakeIPTablesRunner() *iptablesRunner {
|
||||
ipt4 := newFakeIPTables()
|
||||
ipt6 := newFakeIPTables()
|
||||
v6Available := false
|
||||
var ipt6 iptablesInterface
|
||||
if use6, err := strconv.ParseBool(os.Getenv("TS_TEST_FAKE_NETFILTER_6")); use6 || err != nil {
|
||||
ipt6 = newFakeIPTables()
|
||||
v6Available = true
|
||||
}
|
||||
|
||||
iptr := &iptablesRunner{ipt4, ipt6, true, true, true}
|
||||
iptr := &iptablesRunner{ipt4, ipt6, v6Available, v6Available, v6Available}
|
||||
return iptr
|
||||
}
|
||||
|
||||
@@ -70,15 +70,18 @@ type nftable struct {
|
||||
// https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains
|
||||
type nftablesRunner struct {
|
||||
conn *nftables.Conn
|
||||
nft4 *nftable // IPv4 tables
|
||||
nft6 *nftable // IPv6 tables
|
||||
nft4 *nftable // IPv4 tables, never nil
|
||||
nft6 *nftable // IPv6 tables or nil if the system does not support IPv6
|
||||
|
||||
v6Available bool // whether the host supports IPv6
|
||||
}
|
||||
|
||||
func (n *nftablesRunner) ensurePreroutingChain(dst netip.Addr) (*nftables.Table, *nftables.Chain, error) {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
table := n.getNFTByAddr(dst)
|
||||
table, err := n.getNFTByAddr(dst)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error setting up nftables for IP family of %v: %w", dst, err)
|
||||
}
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error ensuring nat table: %w", err)
|
||||
@@ -192,7 +195,10 @@ func (n *nftablesRunner) DNATNonTailscaleTraffic(tunname string, dst netip.Addr)
|
||||
|
||||
func (n *nftablesRunner) AddSNATRuleForDst(src, dst netip.Addr) error {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
table := n.getNFTByAddr(dst)
|
||||
table, err := n.getNFTByAddr(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %v: %w", dst, err)
|
||||
}
|
||||
nat, err := createTableIfNotExist(n.conn, table.Proto, "nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ensuring nat table exists: %w", err)
|
||||
@@ -272,7 +278,10 @@ func (n *nftablesRunner) AddSNATRuleForDst(src, dst netip.Addr) error {
|
||||
// we don't want to race with wgengine for rule ordering within chains.
|
||||
func (n *nftablesRunner) ClampMSSToPMTU(tun string, addr netip.Addr) error {
|
||||
polAccept := nftables.ChainPolicyAccept
|
||||
table := n.getNFTByAddr(addr)
|
||||
table, err := n.getNFTByAddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %v: %w", addr, err)
|
||||
}
|
||||
filterTable, err := createTableIfNotExist(n.conn, table.Proto, "filter")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ensuring filter table: %w", err)
|
||||
@@ -786,17 +795,23 @@ func insertLoopbackRule(
|
||||
|
||||
// getNFTByAddr returns the nftables with correct IP family
|
||||
// that we will be using for the given address.
|
||||
func (n *nftablesRunner) getNFTByAddr(addr netip.Addr) *nftable {
|
||||
if addr.Is6() {
|
||||
return n.nft6
|
||||
func (n *nftablesRunner) getNFTByAddr(addr netip.Addr) (*nftable, error) {
|
||||
if addr.Is6() && !n.v6Available {
|
||||
return nil, fmt.Errorf("nftables for IPv6 are not available on this host")
|
||||
}
|
||||
return n.nft4
|
||||
if addr.Is6() {
|
||||
return n.nft6, nil
|
||||
}
|
||||
return n.nft4, nil
|
||||
}
|
||||
|
||||
// AddLoopbackRule adds an nftables rule to permit loopback traffic to
|
||||
// a local Tailscale IP. This rule is added only if it does not already exist.
|
||||
func (n *nftablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
nf := n.getNFTByAddr(addr)
|
||||
nf, err := n.getNFTByAddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %v: %w", addr, err)
|
||||
}
|
||||
|
||||
inputChain, err := getChainFromTable(n.conn, nf.Filter, chainNameInput)
|
||||
if err != nil {
|
||||
@@ -813,7 +828,10 @@ func (n *nftablesRunner) AddLoopbackRule(addr netip.Addr) error {
|
||||
// DelLoopbackRule removes the nftables rule permitting loopback
|
||||
// traffic to a Tailscale IP.
|
||||
func (n *nftablesRunner) DelLoopbackRule(addr netip.Addr) error {
|
||||
nf := n.getNFTByAddr(addr)
|
||||
nf, err := n.getNFTByAddr(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up nftables for IP family of %v: %w", addr, err)
|
||||
}
|
||||
|
||||
inputChain, err := getChainFromTable(n.conn, nf.Filter, chainNameInput)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,9 +6,11 @@ package winutil
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
|
||||
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
|
||||
|
||||
//sys dsGetDcName(computerName *uint16, domainName *uint16, domainGuid *windows.GUID, siteName *uint16, flags dsGetDcNameFlag, dcInfo **_DOMAIN_CONTROLLER_INFO) (ret error) = netapi32.DsGetDcNameW
|
||||
//sys expandEnvironmentStringsForUser(token windows.Token, src *uint16, dst *uint16, dstLen uint32) (err error) [int32(failretval)==0] = userenv.ExpandEnvironmentStringsForUserW
|
||||
//sys getApplicationRestartSettings(process windows.Handle, commandLine *uint16, commandLineLen *uint32, flags *uint32) (ret wingoes.HRESULT) = kernel32.GetApplicationRestartSettings
|
||||
//sys loadUserProfile(token windows.Token, profileInfo *_PROFILEINFO) (err error) [int32(failretval)==0] = userenv.LoadUserProfileW
|
||||
//sys netValidateName(server *uint16, name *uint16, account *uint16, password *uint16, nameType _NETSETUP_NAME_TYPE) (ret error) = netapi32.NetValidateName
|
||||
//sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W
|
||||
//sys registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret wingoes.HRESULT) = kernel32.RegisterApplicationRestart
|
||||
//sys rmEndSession(session _RMHANDLE) (ret error) = rstrtmgr.RmEndSession
|
||||
|
||||
@@ -23,8 +23,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDefunctProcess is returned by (*UniqueProcess).AsRestartableProcess
|
||||
// when the process no longer exists.
|
||||
// ErrDefunctProcess is returned when the process no longer exists.
|
||||
ErrDefunctProcess = errors.New("process is defunct")
|
||||
// ErrProcessNotRestartable is returned by (*UniqueProcess).AsRestartableProcess
|
||||
// when the process has previously indicated that it must not be restarted
|
||||
@@ -799,7 +798,7 @@ func startProcessInSessionInternal(sessID SessionID, cmdLineInfo CommandLineInfo
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token environment: %w", err)
|
||||
}
|
||||
env16 := newEnvBlock(env)
|
||||
env16 := NewEnvBlock(env)
|
||||
|
||||
// The privileges in privNames are required for CreateProcessAsUser to be
|
||||
// able to start processes as other users in other logon sessions.
|
||||
@@ -826,7 +825,11 @@ func startProcessInSessionInternal(sessID SessionID, cmdLineInfo CommandLineInfo
|
||||
return &pi, nil
|
||||
}
|
||||
|
||||
func newEnvBlock(env []string) *uint16 {
|
||||
// NewEnvBlock processes a slice of strings containing "NAME=value" pairs
|
||||
// representing a process envionment into the environment block format used by
|
||||
// Windows APIs such as CreateProcess. env must be sorted case-insensitively
|
||||
// by variable name.
|
||||
func NewEnvBlock(env []string) *uint16 {
|
||||
// Intentionally using bytes.Buffer here because we're writing nul bytes (the standard library does this too).
|
||||
var buf bytes.Buffer
|
||||
for _, v := range env {
|
||||
|
||||
399
util/winutil/s4u/lsa_windows.go
Normal file
399
util/winutil/s4u/lsa_windows.go
Normal file
@@ -0,0 +1,399 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package s4u
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dblohm7/wingoes"
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/winenv"
|
||||
)
|
||||
|
||||
const (
|
||||
_MICROSOFT_KERBEROS_NAME = "Kerberos"
|
||||
_MSV1_0_PACKAGE_NAME = "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"
|
||||
)
|
||||
|
||||
type _LSAHANDLE windows.Handle
|
||||
type _LSA_OPERATIONAL_MODE uint32
|
||||
|
||||
type _KERB_LOGON_SUBMIT_TYPE int32
|
||||
|
||||
const (
|
||||
_KerbInteractiveLogon _KERB_LOGON_SUBMIT_TYPE = 2
|
||||
_KerbSmartCardLogon _KERB_LOGON_SUBMIT_TYPE = 6
|
||||
_KerbWorkstationUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 7
|
||||
_KerbSmartCardUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 8
|
||||
_KerbProxyLogon _KERB_LOGON_SUBMIT_TYPE = 9
|
||||
_KerbTicketLogon _KERB_LOGON_SUBMIT_TYPE = 10
|
||||
_KerbTicketUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 11
|
||||
_KerbS4ULogon _KERB_LOGON_SUBMIT_TYPE = 12
|
||||
_KerbCertificateLogon _KERB_LOGON_SUBMIT_TYPE = 13
|
||||
_KerbCertificateS4ULogon _KERB_LOGON_SUBMIT_TYPE = 14
|
||||
_KerbCertificateUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 15
|
||||
_KerbNoElevationLogon _KERB_LOGON_SUBMIT_TYPE = 83
|
||||
_KerbLuidLogon _KERB_LOGON_SUBMIT_TYPE = 84
|
||||
)
|
||||
|
||||
type _KERB_S4U_LOGON_FLAGS uint32
|
||||
|
||||
const (
|
||||
_KERB_S4U_LOGON_FLAG_CHECK_LOGONHOURS _KERB_S4U_LOGON_FLAGS = 0x2
|
||||
//lint:ignore U1000 maps to a win32 API
|
||||
_KERB_S4U_LOGON_FLAG_IDENTIFY _KERB_S4U_LOGON_FLAGS = 0x8
|
||||
)
|
||||
|
||||
type _KERB_S4U_LOGON struct {
|
||||
MessageType _KERB_LOGON_SUBMIT_TYPE
|
||||
Flags _KERB_S4U_LOGON_FLAGS
|
||||
ClientUpn windows.NTUnicodeString
|
||||
ClientRealm windows.NTUnicodeString
|
||||
}
|
||||
|
||||
type _MSV1_0_LOGON_SUBMIT_TYPE int32
|
||||
|
||||
const (
|
||||
_MsV1_0InteractiveLogon _MSV1_0_LOGON_SUBMIT_TYPE = 2
|
||||
_MsV1_0Lm20Logon _MSV1_0_LOGON_SUBMIT_TYPE = 3
|
||||
_MsV1_0NetworkLogon _MSV1_0_LOGON_SUBMIT_TYPE = 4
|
||||
_MsV1_0SubAuthLogon _MSV1_0_LOGON_SUBMIT_TYPE = 5
|
||||
_MsV1_0WorkstationUnlockLogon _MSV1_0_LOGON_SUBMIT_TYPE = 7
|
||||
_MsV1_0S4ULogon _MSV1_0_LOGON_SUBMIT_TYPE = 12
|
||||
_MsV1_0VirtualLogon _MSV1_0_LOGON_SUBMIT_TYPE = 82
|
||||
_MsV1_0NoElevationLogon _MSV1_0_LOGON_SUBMIT_TYPE = 83
|
||||
_MsV1_0LuidLogon _MSV1_0_LOGON_SUBMIT_TYPE = 84
|
||||
)
|
||||
|
||||
type _MSV1_0_S4U_LOGON_FLAGS uint32
|
||||
|
||||
const (
|
||||
_MSV1_0_S4U_LOGON_FLAG_CHECK_LOGONHOURS _MSV1_0_S4U_LOGON_FLAGS = 0x2
|
||||
)
|
||||
|
||||
type _MSV1_0_S4U_LOGON struct {
|
||||
MessageType _MSV1_0_LOGON_SUBMIT_TYPE
|
||||
Flags _MSV1_0_S4U_LOGON_FLAGS
|
||||
UserPrincipalName windows.NTUnicodeString
|
||||
DomainName windows.NTUnicodeString
|
||||
}
|
||||
|
||||
type _SECURITY_LOGON_TYPE int32
|
||||
|
||||
const (
|
||||
_UndefinedLogonType _SECURITY_LOGON_TYPE = 0
|
||||
_Interactive _SECURITY_LOGON_TYPE = 2
|
||||
_Network _SECURITY_LOGON_TYPE = 3
|
||||
_Batch _SECURITY_LOGON_TYPE = 4
|
||||
_Service _SECURITY_LOGON_TYPE = 5
|
||||
_Proxy _SECURITY_LOGON_TYPE = 6
|
||||
_Unlock _SECURITY_LOGON_TYPE = 7
|
||||
_NetworkCleartext _SECURITY_LOGON_TYPE = 8
|
||||
_NewCredentials _SECURITY_LOGON_TYPE = 9
|
||||
_RemoteInteractive _SECURITY_LOGON_TYPE = 10
|
||||
_CachedInteractive _SECURITY_LOGON_TYPE = 11
|
||||
_CachedRemoteInteractive _SECURITY_LOGON_TYPE = 12
|
||||
_CachedUnlock _SECURITY_LOGON_TYPE = 13
|
||||
)
|
||||
|
||||
const _TOKEN_SOURCE_LENGTH = 8
|
||||
|
||||
type _TOKEN_SOURCE struct {
|
||||
SourceName [_TOKEN_SOURCE_LENGTH]byte
|
||||
SourceIdentifier windows.LUID
|
||||
}
|
||||
|
||||
type _QUOTA_LIMITS struct {
|
||||
PagedPoolLimit uintptr
|
||||
NonPagedPoolLimit uintptr
|
||||
MinimumWorkingSetSize uintptr
|
||||
MaximumWorkingSetSize uintptr
|
||||
PagefileLimit uintptr
|
||||
TimeLimit int64
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrBadSrcName is returned if srcName contains non-ASCII characters, is
|
||||
// empty, or is too long. It may be wrapped with additional information; use
|
||||
// errors.Is when checking for it.
|
||||
ErrBadSrcName = errors.New("srcName must be ASCII with length > 0 and <= 8")
|
||||
)
|
||||
|
||||
// LSA packages (and their IDs) are always initialized during system startup,
|
||||
// so we can retain their resolved IDs for the lifetime of our process.
|
||||
var (
|
||||
authPkgIDKerberos lazy.SyncValue[uint32]
|
||||
authPkgIDMSV1_0 lazy.SyncValue[uint32]
|
||||
)
|
||||
|
||||
type lsaSession struct {
|
||||
handle _LSAHANDLE
|
||||
}
|
||||
|
||||
func newLSASessionForQuery() (lsa *lsaSession, err error) {
|
||||
var h _LSAHANDLE
|
||||
if e := wingoes.ErrorFromNTStatus(lsaConnectUntrusted(&h)); e.Failed() {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return &lsaSession{handle: h}, nil
|
||||
}
|
||||
|
||||
func newLSASessionForLogon(processName string) (lsa *lsaSession, err error) {
|
||||
// processName is used by LSA for audit logging purposes.
|
||||
// If empty, the current process name is used.
|
||||
if processName == "" {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
processName = strings.TrimSuffix(filepath.Base(exe), filepath.Ext(exe))
|
||||
}
|
||||
|
||||
if err := checkASCII(processName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logonProcessName, err := windows.NewNTString(processName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var h _LSAHANDLE
|
||||
var mode _LSA_OPERATIONAL_MODE
|
||||
if e := wingoes.ErrorFromNTStatus(lsaRegisterLogonProcess(logonProcessName, &h, &mode)); e.Failed() {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return &lsaSession{handle: h}, nil
|
||||
}
|
||||
|
||||
func (ls *lsaSession) getAuthPkgID(pkgName string) (id uint32, err error) {
|
||||
ntPkgName, err := windows.NewNTString(pkgName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if e := wingoes.ErrorFromNTStatus(lsaLookupAuthenticationPackage(ls.handle, ntPkgName, &id)); e.Failed() {
|
||||
return 0, e
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (ls *lsaSession) Close() error {
|
||||
if e := wingoes.ErrorFromNTStatus(lsaDeregisterLogonProcess(ls.handle)); e.Failed() {
|
||||
return e
|
||||
}
|
||||
ls.handle = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkASCII(s string) error {
|
||||
for _, c := range []byte(s) {
|
||||
if c > unicode.MaxASCII {
|
||||
return fmt.Errorf("%q must be ASCII but contains value 0x%02X", s, c)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
thisComputer = []uint16{'.', 0}
|
||||
computerName lazy.SyncValue[string]
|
||||
)
|
||||
|
||||
func getComputerName() (string, error) {
|
||||
var buf [windows.MAX_COMPUTERNAME_LENGTH + 1]uint16
|
||||
size := uint32(len(buf))
|
||||
if err := windows.GetComputerName(&buf[0], &size); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return windows.UTF16ToString(buf[:size]), nil
|
||||
}
|
||||
|
||||
// checkDomainAccount strips out the computer name (if any) from
|
||||
// username and returns the result in sanitizedUserName. isDomainAccount is set
|
||||
// to true if username contains a domain component that does not refer to the
|
||||
// local computer.
|
||||
func checkDomainAccount(username string) (sanitizedUserName string, isDomainAccount bool, err error) {
|
||||
before, after, hasBackslash := strings.Cut(username, `\`)
|
||||
if !hasBackslash {
|
||||
return username, false, nil
|
||||
}
|
||||
if before == "." {
|
||||
return after, false, nil
|
||||
}
|
||||
|
||||
comp, err := computerName.GetErr(getComputerName)
|
||||
if err != nil {
|
||||
return username, false, err
|
||||
}
|
||||
|
||||
if strings.EqualFold(before, comp) {
|
||||
return after, false, nil
|
||||
}
|
||||
return username, true, nil
|
||||
}
|
||||
|
||||
// logonAs performs a S4U logon for u on behalf of srcName, and returns an
|
||||
// access token for the user if successful. srcName must be non-empty, ASCII,
|
||||
// and no more than 8 characters long. If srcName does not meet this criteria,
|
||||
// LogonAs will return ErrBadSrcName wrapped with additional information; use
|
||||
// errors.Is to check for it. When capLevel == CapCreateProcess, the logon
|
||||
// enforces the user's logon hours policy (when present).
|
||||
func (ls *lsaSession) logonAs(srcName string, u *user.User, capLevel CapabilityLevel) (token windows.Token, err error) {
|
||||
if l := len(srcName); l == 0 || l > _TOKEN_SOURCE_LENGTH {
|
||||
return 0, fmt.Errorf("%w, actual length is %d", ErrBadSrcName, l)
|
||||
}
|
||||
if err := checkASCII(srcName); err != nil {
|
||||
return 0, fmt.Errorf("%w: %v", ErrBadSrcName, err)
|
||||
}
|
||||
|
||||
sanitizedUserName, isDomainUser, err := checkDomainAccount(u.Username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if isDomainUser && !winenv.IsDomainJoined() {
|
||||
return 0, fmt.Errorf("%w: cannot logon as domain user without being joined to a domain", os.ErrInvalid)
|
||||
}
|
||||
|
||||
var pkgID uint32
|
||||
var authInfo unsafe.Pointer
|
||||
var authInfoLen uint32
|
||||
enforceLogonHours := capLevel == CapCreateProcess
|
||||
if isDomainUser {
|
||||
pkgID, err = authPkgIDKerberos.GetErr(func() (uint32, error) {
|
||||
return ls.getAuthPkgID(_MICROSOFT_KERBEROS_NAME)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
upn16, err := samToUPN16(sanitizedUserName)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("samToUPN16: %w", err)
|
||||
}
|
||||
|
||||
logonInfo, logonInfoLen, slcs := winutil.AllocateContiguousBuffer[_KERB_S4U_LOGON](upn16)
|
||||
logonInfo.MessageType = _KerbS4ULogon
|
||||
if enforceLogonHours {
|
||||
logonInfo.Flags = _KERB_S4U_LOGON_FLAG_CHECK_LOGONHOURS
|
||||
}
|
||||
winutil.SetNTString(&logonInfo.ClientUpn, slcs[0])
|
||||
|
||||
authInfo = unsafe.Pointer(logonInfo)
|
||||
authInfoLen = logonInfoLen
|
||||
} else {
|
||||
pkgID, err = authPkgIDMSV1_0.GetErr(func() (uint32, error) {
|
||||
return ls.getAuthPkgID(_MSV1_0_PACKAGE_NAME)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
upn16, err := windows.UTF16FromString(sanitizedUserName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
logonInfo, logonInfoLen, slcs := winutil.AllocateContiguousBuffer[_MSV1_0_S4U_LOGON](upn16, thisComputer)
|
||||
logonInfo.MessageType = _MsV1_0S4ULogon
|
||||
if enforceLogonHours {
|
||||
logonInfo.Flags = _MSV1_0_S4U_LOGON_FLAG_CHECK_LOGONHOURS
|
||||
}
|
||||
for i, nts := range []*windows.NTUnicodeString{&logonInfo.UserPrincipalName, &logonInfo.DomainName} {
|
||||
winutil.SetNTString(nts, slcs[i])
|
||||
}
|
||||
|
||||
authInfo = unsafe.Pointer(logonInfo)
|
||||
authInfoLen = logonInfoLen
|
||||
}
|
||||
|
||||
var srcContext _TOKEN_SOURCE
|
||||
copy(srcContext.SourceName[:], []byte(srcName))
|
||||
if err := allocateLocallyUniqueId(&srcContext.SourceIdentifier); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
originName, err := windows.NewNTString(srcName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var profileBuf uintptr
|
||||
var profileBufLen uint32
|
||||
var logonID windows.LUID
|
||||
var quotas _QUOTA_LIMITS
|
||||
var subNTStatus windows.NTStatus
|
||||
ntStatus := lsaLogonUser(ls.handle, originName, _Network, pkgID, authInfo, authInfoLen, nil, &srcContext, &profileBuf, &profileBufLen, &logonID, &token, "as, &subNTStatus)
|
||||
if e := wingoes.ErrorFromNTStatus(ntStatus); e.Failed() {
|
||||
return 0, fmt.Errorf("LsaLogonUser(%q): %w, SubStatus: %v", u.Username, e, subNTStatus)
|
||||
}
|
||||
if profileBuf != 0 {
|
||||
lsaFreeReturnBuffer(profileBuf)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// samToUPN16 converts SAM-style account name samName to a UPN account name,
|
||||
// returned as a UTF-16 slice.
|
||||
func samToUPN16(samName string) (upn16 []uint16, err error) {
|
||||
_, samAccount, hasSep := strings.Cut(samName, `\`)
|
||||
if !hasSep {
|
||||
return nil, fmt.Errorf("%w: expected samName to contain a backslash", os.ErrInvalid)
|
||||
}
|
||||
|
||||
// This is essentially the same algorithm used by Win32-OpenSSH:
|
||||
// First, try obtaining a UPN directly...
|
||||
upn16, err = translateName(samName, windows.NameSamCompatible, windows.NameUserPrincipal)
|
||||
if err == nil {
|
||||
return upn16, err
|
||||
}
|
||||
|
||||
// Fallback: Try manually composing a UPN. First obtain the canonical name...
|
||||
canonical16, err := translateName(samName, windows.NameSamCompatible, windows.NameCanonical)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonical := windows.UTF16ToString(canonical16)
|
||||
|
||||
// Extract the domain name...
|
||||
domain, _, _ := strings.Cut(canonical, "/")
|
||||
|
||||
// ...and finally create the UPN by joining the samAccount and domain.
|
||||
upn := strings.Join([]string{samAccount, domain}, "@")
|
||||
return windows.UTF16FromString(upn)
|
||||
}
|
||||
|
||||
func translateName(from string, fromFmt uint32, toFmt uint32) (result []uint16, err error) {
|
||||
from16, err := windows.UTF16PtrFromString(from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var to16Len uint32
|
||||
if err := windows.TranslateName(from16, fromFmt, toFmt, nil, &to16Len); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to16Buf := make([]uint16, to16Len)
|
||||
if err := windows.TranslateName(from16, fromFmt, toFmt, unsafe.SliceData(to16Buf), &to16Len); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return to16Buf, nil
|
||||
}
|
||||
16
util/winutil/s4u/mksyscall.go
Normal file
16
util/winutil/s4u/mksyscall.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package s4u
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
|
||||
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
|
||||
|
||||
//sys allocateLocallyUniqueId(luid *windows.LUID) (err error) [int32(failretval)==0] = advapi32.AllocateLocallyUniqueId
|
||||
//sys impersonateLoggedOnUser(token windows.Token) (err error) [int32(failretval)==0] = advapi32.ImpersonateLoggedOnUser
|
||||
//sys lsaConnectUntrusted(lsaHandle *_LSAHANDLE) (ret windows.NTStatus) = secur32.LsaConnectUntrusted
|
||||
//sys lsaDeregisterLogonProcess(lsaHandle _LSAHANDLE) (ret windows.NTStatus) = secur32.LsaDeregisterLogonProcess
|
||||
//sys lsaFreeReturnBuffer(buffer uintptr) (ret windows.NTStatus) = secur32.LsaFreeReturnBuffer
|
||||
//sys lsaLogonUser(lsaHandle _LSAHANDLE, originName *windows.NTString, logonType _SECURITY_LOGON_TYPE, authenticationPackage uint32, authenticationInformation unsafe.Pointer, authenticationInformationLength uint32, localGroups *windows.Tokengroups, sourceContext *_TOKEN_SOURCE, profileBuffer *uintptr, profileBufferLength *uint32, logonID *windows.LUID, token *windows.Token, quotas *_QUOTA_LIMITS, subStatus *windows.NTStatus) (ret windows.NTStatus) = secur32.LsaLogonUser
|
||||
//sys lsaLookupAuthenticationPackage(lsaHandle _LSAHANDLE, packageName *windows.NTString, authenticationPackage *uint32) (ret windows.NTStatus) = secur32.LsaLookupAuthenticationPackage
|
||||
//sys lsaRegisterLogonProcess(logonProcessName *windows.NTString, lsaHandle *_LSAHANDLE, securityMode *_LSA_OPERATIONAL_MODE) (ret windows.NTStatus) = secur32.LsaRegisterLogonProcess
|
||||
944
util/winutil/s4u/s4u_windows.go
Normal file
944
util/winutil/s4u/s4u_windows.go
Normal file
@@ -0,0 +1,944 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package s4u is an API for accessing Service-For-User (S4U) functionality on Windows.
|
||||
package s4u
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/conpty"
|
||||
)
|
||||
|
||||
func init() {
|
||||
childproc.Add("s4u", beRelay)
|
||||
}
|
||||
|
||||
var errInsufficientCapabilityLevel = errors.New("insufficient capability level")
|
||||
|
||||
// ListGroupIDsForSSHPreAuthOnly returns user u's group memberships as a slice
|
||||
// containing group SIDs. srcName must contain the name of the service that is
|
||||
// retrieving this information. srcName must be non-empty, ASCII-only, and no
|
||||
// longer than 8 characters.
|
||||
//
|
||||
// NOTE: This should only be used by Tailscale SSH! It is not a generic
|
||||
// mechanism for access checks!
|
||||
func ListGroupIDsForSSHPreAuthOnly(srcName string, u *user.User) ([]string, error) {
|
||||
tok, err := createToken(srcName, u, tokenTypeIdentification, CapImpersonateOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tok.Close()
|
||||
|
||||
tokenGroups, err := tok.GetTokenGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]string, 0, tokenGroups.GroupCount)
|
||||
for _, group := range tokenGroups.AllGroups() {
|
||||
if group.Attributes&windows.SE_GROUP_ENABLED != 0 {
|
||||
result = append(result, group.Sid.String())
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type tokenType uint
|
||||
|
||||
const (
|
||||
tokenTypeIdentification tokenType = iota
|
||||
tokenTypeImpersonation
|
||||
)
|
||||
|
||||
// createToken creates a new S4U access token for user u for the purposes
|
||||
// specified by s4uType, with capability capLevel. srcName must contain the name
|
||||
// of the service that is intended to use the token. srcName must be non-empty,
|
||||
// ASCII-only, and no longer than 8 characters.
|
||||
//
|
||||
// When s4uType is tokenTypeImpersonation, the current OS thread's access token must have SeTcbPrivilege.
|
||||
func createToken(srcName string, u *user.User, s4uType tokenType, capLevel CapabilityLevel) (tok windows.Token, err error) {
|
||||
if u == nil {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
var lsa *lsaSession
|
||||
switch s4uType {
|
||||
case tokenTypeIdentification:
|
||||
lsa, err = newLSASessionForQuery()
|
||||
case tokenTypeImpersonation:
|
||||
lsa, err = newLSASessionForLogon("")
|
||||
default:
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer lsa.Close()
|
||||
|
||||
return lsa.logonAs(srcName, u, capLevel)
|
||||
}
|
||||
|
||||
// Session encapsulates an S4U login session.
|
||||
type Session struct {
|
||||
refCnt atomic.Int32
|
||||
logf logger.Logf
|
||||
token windows.Token
|
||||
userProfile *winutil.UserProfile
|
||||
capLevel CapabilityLevel
|
||||
}
|
||||
|
||||
// CapabilityLevel specifies the desired capabilities that will be supported by a Session.
|
||||
type CapabilityLevel uint
|
||||
|
||||
const (
|
||||
// The Session supports Do but none of the StartProcess* methods.
|
||||
CapImpersonateOnly CapabilityLevel = iota
|
||||
// The Session supports both Do and the StartProcess* methods.
|
||||
CapCreateProcess
|
||||
)
|
||||
|
||||
// Login logs user u into Windows on behalf of service srcName, loads the user's
|
||||
// profile, and returns a Session that may be used for impersonating that user,
|
||||
// or optionally creating processes as that user. Logs will be written to logf,
|
||||
// if provided. srcName must be non-empty, ASCII-only, and no longer than 8
|
||||
// characters.
|
||||
//
|
||||
// The current OS thread's access token must have SeTcbPrivilege.
|
||||
func Login(logf logger.Logf, srcName string, u *user.User, capLevel CapabilityLevel) (sess *Session, err error) {
|
||||
token, err := createToken(srcName, u, tokenTypeImpersonation, capLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
token.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sessToken := token
|
||||
if capLevel == CapCreateProcess {
|
||||
// Obtain token's security descriptor so that it may be applied to
|
||||
// a primary token.
|
||||
sd, err := windows.GetSecurityInfo(windows.Handle(token),
|
||||
windows.SE_KERNEL_OBJECT, windows.DACL_SECURITY_INFORMATION)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sa := windows.SecurityAttributes{
|
||||
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
||||
SecurityDescriptor: sd,
|
||||
}
|
||||
|
||||
// token is an impersonation token. Upgrade us to a primary token so that
|
||||
// our StartProcess* methods will work correctly.
|
||||
var dupToken windows.Token
|
||||
if err := windows.DuplicateTokenEx(token, 0, &sa, windows.SecurityImpersonation,
|
||||
windows.TokenPrimary, &dupToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessToken = dupToken
|
||||
defer func() {
|
||||
if err != nil {
|
||||
sessToken.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
userProfile, err := winutil.LoadUserProfile(sessToken, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if logf == nil {
|
||||
logf = logger.Discard
|
||||
} else {
|
||||
logf = logger.WithPrefix(logf, "(s4u) ")
|
||||
}
|
||||
|
||||
return &Session{logf: logf, token: sessToken, userProfile: userProfile, capLevel: capLevel}, nil
|
||||
}
|
||||
|
||||
// Close unloads the user profile and S4U access token associated with the
|
||||
// session. The close operation is not guaranteed to have finished when Close
|
||||
// returns; it may remain alive until all processes created by ss have
|
||||
// themselves been closed, and no more Do requests are pending.
|
||||
func (ss *Session) Close() error {
|
||||
refs := ss.refCnt.Load()
|
||||
if (refs & 1) != 0 {
|
||||
// Close already called
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the low bit to indicate that a close operation has been requested.
|
||||
// We don't have atomic OR so we need to use CAS. Sigh.
|
||||
for !ss.refCnt.CompareAndSwap(refs, refs|1) {
|
||||
refs = ss.refCnt.Load()
|
||||
}
|
||||
|
||||
if refs > 1 {
|
||||
// Still active processes, just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
return ss.closeInternal()
|
||||
}
|
||||
|
||||
func (ss *Session) closeInternal() error {
|
||||
if ss.userProfile != nil {
|
||||
if err := ss.userProfile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
ss.userProfile = nil
|
||||
}
|
||||
|
||||
if ss.token != 0 {
|
||||
if err := ss.token.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
ss.token = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CapabilityLevel returns the CapabilityLevel that was specified when the
|
||||
// session was created.
|
||||
func (ss *Session) CapabilityLevel() CapabilityLevel {
|
||||
return ss.capLevel
|
||||
}
|
||||
|
||||
// Do executes fn while impersonating ss's user. Impersonation only affects
|
||||
// the current goroutine; any new goroutines spawned by fn will not be
|
||||
// impersonated. Do may be called concurrently by multiple goroutines.
|
||||
//
|
||||
// Do returns an error if impersonation did not succeed and fn could not be run.
|
||||
// If called after ss has already been closed, it will panic.
|
||||
func (ss *Session) Do(fn func()) error {
|
||||
if fn == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
ss.addRef()
|
||||
defer ss.release()
|
||||
|
||||
// Impersonation touches thread-local state.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := impersonateLoggedOnUser(ss.token); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := windows.RevertToSelf(); err != nil {
|
||||
// This is not recoverable in any way, shape, or form!
|
||||
panic(fmt.Sprintf("RevertToSelf failed: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
fn()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *Session) addRef() {
|
||||
if (ss.refCnt.Add(2) & 1) != 0 {
|
||||
panic("addRef after Close")
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *Session) release() {
|
||||
rc := ss.refCnt.Add(-2)
|
||||
if rc < 0 {
|
||||
panic("negative refcount")
|
||||
}
|
||||
if rc == 1 {
|
||||
ss.closeInternal()
|
||||
}
|
||||
}
|
||||
|
||||
type startProcessOpts struct {
|
||||
token windows.Token
|
||||
extraEnv map[string]string
|
||||
ptySize windows.Coord
|
||||
pipes bool
|
||||
}
|
||||
|
||||
// StartProcess creates a new process running under ss via cmdLineInfo.
|
||||
// The process will either be started with its working directory set to the S4U
|
||||
// user's profile directory, or for Administrative users, the system32
|
||||
// directory. The child process will receive the S4U user's environment.
|
||||
// extraEnv, when specified, contains any additional environment
|
||||
// variables to be inserted into the environment.
|
||||
//
|
||||
// If called after ss has already been closed, StartProcess will panic.
|
||||
func (ss *Session) StartProcess(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string) (psp *Process, err error) {
|
||||
if ss.capLevel != CapCreateProcess {
|
||||
return nil, errInsufficientCapabilityLevel
|
||||
}
|
||||
|
||||
opts := startProcessOpts{
|
||||
token: ss.token,
|
||||
extraEnv: extraEnv,
|
||||
}
|
||||
return startProcessInternal(ss, ss.logf, cmdLineInfo, opts)
|
||||
}
|
||||
|
||||
// StartProcessWithPTY creates a new process running under ss via cmdLineInfo
|
||||
// with a pseudoconsole initialized to initialPtySize. The resulting Process
|
||||
// will return non-nil values from Stdin and Stdout, but Stderr will return nil.
|
||||
// The process will either be started with its working directory set to the S4U
|
||||
// user's profile directory, or for Administrative users, the system32
|
||||
// directory. The child process will receive the S4U user's environment.
|
||||
// extraEnv, when specified, contains any additional environment
|
||||
// variables to be inserted into the environment.
|
||||
//
|
||||
// If called after ss has already been closed, StartProcessWithPTY will panic.
|
||||
func (ss *Session) StartProcessWithPTY(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string, initialPtySize windows.Coord) (psp *Process, err error) {
|
||||
if ss.capLevel != CapCreateProcess {
|
||||
return nil, errInsufficientCapabilityLevel
|
||||
}
|
||||
|
||||
opts := startProcessOpts{
|
||||
token: ss.token,
|
||||
extraEnv: extraEnv,
|
||||
ptySize: initialPtySize,
|
||||
}
|
||||
return startProcessInternal(ss, ss.logf, cmdLineInfo, opts)
|
||||
}
|
||||
|
||||
// StartProcessWithPipes creates a new process running under ss via cmdLineInfo
|
||||
// with all standard handles set to pipes. The resulting Process will return
|
||||
// non-nil values from Stdin, Stdout, and Stderr.
|
||||
// The process will either be started with its working directory set to the S4U
|
||||
// user's profile directory, or for Administrative users, the system32
|
||||
// directory. The child process will receive the S4U user's environment.
|
||||
// extraEnv, when specified, contains any additional environment
|
||||
// variables to be inserted into the environment.
|
||||
//
|
||||
// If called after ss has already been closed, StartProcessWithPipes will panic.
|
||||
func (ss *Session) StartProcessWithPipes(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string) (psp *Process, err error) {
|
||||
if ss.capLevel != CapCreateProcess {
|
||||
return nil, errInsufficientCapabilityLevel
|
||||
}
|
||||
|
||||
opts := startProcessOpts{
|
||||
token: ss.token,
|
||||
extraEnv: extraEnv,
|
||||
pipes: true,
|
||||
}
|
||||
return startProcessInternal(ss, ss.logf, cmdLineInfo, opts)
|
||||
}
|
||||
|
||||
// startProcessInternal is the common implementation behind Session's exported
|
||||
// StartProcess* methods. It uses opts to distinguish between the various
|
||||
// requested modes of operation.
|
||||
//
|
||||
// A note on pseudoconsoles:
|
||||
// The conpty API currently does not provide a way to create a pseudoconsole for
|
||||
// a different user than the current process. The way we deal with this is
|
||||
// to first create a "relay" process running with the desired user token,
|
||||
// and then create the actual requested process as a child of the relay,
|
||||
// at which time we create the pseudoconsole. The relay simply copies the
|
||||
// PTY's I/O into/out of its own stdin and stdout, which are piped to the
|
||||
// parent still running as LocalSystem. We also relay pseudoconsole resize requests.
|
||||
func startProcessInternal(ss *Session, logf logger.Logf, cmdLineInfo winutil.CommandLineInfo, opts startProcessOpts) (psp *Process, err error) {
|
||||
var sib winutil.StartupInfoBuilder
|
||||
defer sib.Close()
|
||||
|
||||
var sp Process
|
||||
defer func() {
|
||||
if err != nil {
|
||||
sp.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var zeroCoord windows.Coord
|
||||
ptySizeValid := opts.ptySize != zeroCoord
|
||||
useToken := opts.token != 0
|
||||
usePty := ptySizeValid && !useToken
|
||||
useRelay := ptySizeValid && useToken
|
||||
useSystem32WD := useToken && opts.token.IsElevated()
|
||||
|
||||
if usePty {
|
||||
sp.pty, err = conpty.NewPseudoConsole(opts.ptySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := sp.pty.ConfigureStartupInfo(&sib); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sp.wStdin = sp.pty.InputPipe()
|
||||
sp.rStdout = sp.pty.OutputPipe()
|
||||
} else if useRelay || opts.pipes {
|
||||
if sp.wStdin, sp.rStdout, sp.rStderr, err = createStdPipes(&sib); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var relayStderr io.ReadCloser
|
||||
if useRelay {
|
||||
// Later on we're going to use stderr for logging instead of providing it to the caller.
|
||||
relayStderr = sp.rStderr
|
||||
sp.rStderr = nil
|
||||
defer func() {
|
||||
if err != nil {
|
||||
relayStderr.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up a pipe to send PTY resize requests.
|
||||
var resizeRead, resizeWrite windows.Handle
|
||||
if err := windows.CreatePipe(&resizeRead, &resizeWrite, nil, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.wResize = os.NewFile(uintptr(resizeWrite), "wPTYResizePipe")
|
||||
defer windows.CloseHandle(resizeRead)
|
||||
if err := sib.InheritHandles(resizeRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Revise the command line. First, get the existing one.
|
||||
_, _, strCmdLine, err := cmdLineInfo.Resolve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now rebuild it, passing the strCmdLine as the --cmd argument...
|
||||
newArgs := []string{
|
||||
"be-child", "s4u",
|
||||
"--resize", fmt.Sprintf("0x%x", uintptr(resizeRead)),
|
||||
"--x", strconv.Itoa(int(opts.ptySize.X)),
|
||||
"--y", strconv.Itoa(int(opts.ptySize.Y)),
|
||||
"--cmd", strCmdLine,
|
||||
}
|
||||
|
||||
// ...to be passed in as arguments to our own executable.
|
||||
cmdLineInfo.ExePath, err = os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmdLineInfo.SetArgs(newArgs)
|
||||
}
|
||||
|
||||
exePath, cmdLine, cmdLineStr, err := cmdLineInfo.Resolve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logf("starting %s", cmdLineStr)
|
||||
|
||||
var env []string
|
||||
var wd16 *uint16
|
||||
if useToken {
|
||||
env, err = opts.token.Environ(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folderID := windows.FOLDERID_Profile
|
||||
if useSystem32WD {
|
||||
folderID = windows.FOLDERID_System
|
||||
}
|
||||
wd, err := opts.token.KnownFolderPath(folderID, windows.KF_FLAG_DEFAULT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wd16, err = windows.UTF16PtrFromString(wd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
env = os.Environ()
|
||||
}
|
||||
|
||||
env = mergeEnv(env, opts.extraEnv)
|
||||
|
||||
var env16 *uint16
|
||||
if useToken || len(opts.extraEnv) > 0 {
|
||||
env16 = winutil.NewEnvBlock(env)
|
||||
}
|
||||
|
||||
if useToken {
|
||||
// We want the child process to be assigned to job such that when it exits,
|
||||
// its descendents within the job will be terminated as well.
|
||||
job, err := createJob()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We don't need to hang onto job beyond this func...
|
||||
defer job.Close()
|
||||
|
||||
if err := sib.AssignToJob(job.Handle()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ...because we're now gonna make a read-only copy...
|
||||
qjob, err := job.QueryOnlyClone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer qjob.Close()
|
||||
|
||||
// ...which will be inherited by the child process.
|
||||
// When the child process terminates, the job will too.
|
||||
if err := sib.InheritHandles(qjob.Handle()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
si, inheritHandles, creationFlags, err := sib.Resolve()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pi windows.ProcessInformation
|
||||
if useToken {
|
||||
// DETACHED_PROCESS so that the child does not receive a console.
|
||||
// CREATE_NEW_PROCESS_GROUP so that the child's console group is isolated from ours.
|
||||
creationFlags |= windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP
|
||||
doCreate := func() {
|
||||
err = windows.CreateProcessAsUser(opts.token, exePath, cmdLine, nil, nil, inheritHandles, creationFlags, env16, wd16, si, &pi)
|
||||
}
|
||||
switch {
|
||||
case useRelay:
|
||||
doCreate()
|
||||
case ss != nil:
|
||||
// We want to ensure that the executable is accessible via the token's
|
||||
// security context, not ours.
|
||||
if err := ss.Do(doCreate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
panic("should not have reached here")
|
||||
}
|
||||
} else {
|
||||
err = windows.CreateProcess(exePath, cmdLine, nil, nil, inheritHandles, creationFlags, env16, wd16, si, &pi)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
windows.CloseHandle(pi.Thread)
|
||||
|
||||
if relayStderr != nil {
|
||||
logw := logger.FuncWriter(logger.WithPrefix(logf, fmt.Sprintf("(s4u relay process %d [0x%x]) ", pi.ProcessId, pi.ProcessId)))
|
||||
go func() {
|
||||
defer relayStderr.Close()
|
||||
io.Copy(logw, relayStderr)
|
||||
}()
|
||||
}
|
||||
|
||||
sp.hproc = pi.Process
|
||||
sp.pid = pi.ProcessId
|
||||
if ss != nil {
|
||||
ss.addRef()
|
||||
sp.sess = ss
|
||||
}
|
||||
return &sp, nil
|
||||
}
|
||||
|
||||
type jobObject windows.Handle
|
||||
|
||||
func createJob() (job *jobObject, err error) {
|
||||
hjob, err := windows.CreateJobObject(nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(hjob)
|
||||
}
|
||||
}()
|
||||
|
||||
limitInfo := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
||||
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
||||
// We want every process within the job to terminate when the job is closed.
|
||||
// We also want to allow processes within the job to create child processes
|
||||
// that are outside the job (otherwise you couldn't leave background
|
||||
// processes running after exiting a session, for example).
|
||||
// These flags also match those used by the Win32 port of OpenSSH.
|
||||
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | windows.JOB_OBJECT_LIMIT_BREAKAWAY_OK,
|
||||
},
|
||||
}
|
||||
_, err = windows.SetInformationJobObject(hjob,
|
||||
windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limitInfo)),
|
||||
uint32(unsafe.Sizeof(limitInfo)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jo := jobObject(hjob)
|
||||
return &jo, nil
|
||||
}
|
||||
|
||||
func (job *jobObject) Close() error {
|
||||
if hjob := job.Handle(); hjob != 0 {
|
||||
windows.CloseHandle(hjob)
|
||||
*job = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job *jobObject) Handle() windows.Handle {
|
||||
if job == nil {
|
||||
return 0
|
||||
}
|
||||
return windows.Handle(*job)
|
||||
}
|
||||
|
||||
const _JOB_OBJECT_QUERY = 0x0004
|
||||
|
||||
func (job *jobObject) QueryOnlyClone() (*jobObject, error) {
|
||||
hjob := job.Handle()
|
||||
cp := windows.CurrentProcess()
|
||||
|
||||
var dupe windows.Handle
|
||||
err := windows.DuplicateHandle(cp, hjob, cp, &dupe, _JOB_OBJECT_QUERY, true, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := jobObject(dupe)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func createStdPipes(sib *winutil.StartupInfoBuilder) (stdin io.WriteCloser, stdout, stderr io.ReadCloser, err error) {
|
||||
var rStdin, wStdin windows.Handle
|
||||
if err := windows.CreatePipe(&rStdin, &wStdin, nil, 0); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(rStdin)
|
||||
windows.CloseHandle(wStdin)
|
||||
}
|
||||
}()
|
||||
|
||||
var rStdout, wStdout windows.Handle
|
||||
if err := windows.CreatePipe(&rStdout, &wStdout, nil, 0); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(rStdout)
|
||||
windows.CloseHandle(wStdout)
|
||||
}
|
||||
}()
|
||||
|
||||
var rStderr, wStderr windows.Handle
|
||||
if err := windows.CreatePipe(&rStderr, &wStderr, nil, 0); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(rStderr)
|
||||
windows.CloseHandle(wStderr)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := sib.SetStdHandles(rStdin, wStdout, wStderr); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
stdin = os.NewFile(uintptr(wStdin), "wStdin")
|
||||
stdout = os.NewFile(uintptr(rStdout), "rStdout")
|
||||
stderr = os.NewFile(uintptr(rStderr), "rStderr")
|
||||
return stdin, stdout, stderr, nil
|
||||
}
|
||||
|
||||
// Process encapsulates a child process started with a Session.
|
||||
type Process struct {
|
||||
sess *Session
|
||||
wStdin io.WriteCloser
|
||||
rStdout io.ReadCloser
|
||||
rStderr io.ReadCloser
|
||||
wResize io.WriteCloser
|
||||
pty *conpty.PseudoConsole
|
||||
hproc windows.Handle
|
||||
pid uint32
|
||||
}
|
||||
|
||||
// Stdin returns the write side of a pipe connected to the child process's
|
||||
// stdin, or nil if no I/O was requested.
|
||||
func (sp *Process) Stdin() io.WriteCloser {
|
||||
return sp.wStdin
|
||||
}
|
||||
|
||||
// Stdout returns the read side of a pipe connected to the child process's
|
||||
// stdout, or nil if no I/O was requested.
|
||||
func (sp *Process) Stdout() io.ReadCloser {
|
||||
return sp.rStdout
|
||||
}
|
||||
|
||||
// Stderr returns the read side of a pipe connected to the child process's
|
||||
// stderr, or nil if no I/O was requested.
|
||||
func (sp *Process) Stderr() io.ReadCloser {
|
||||
return sp.rStderr
|
||||
}
|
||||
|
||||
// Terminate kills the process.
|
||||
func (sp *Process) Terminate() {
|
||||
if sp.hproc != 0 {
|
||||
windows.TerminateProcess(sp.hproc, 255)
|
||||
}
|
||||
}
|
||||
|
||||
// Close waits for sp to complete and then cleans up any resources owned by it.
|
||||
// Close must wait because the Session associated with sp should not be destroyed
|
||||
// until all its processes have terminated. If necessary, call Terminate to
|
||||
// forcibly end the process.
|
||||
//
|
||||
// If the process was created with a pseudoconsole then the caller must continue
|
||||
// concurrently draining sp's stdout until either Close finishes executing, or EOF.
|
||||
func (sp *Process) Close() error {
|
||||
for _, pc := range []*io.WriteCloser{&sp.wStdin, &sp.wResize} {
|
||||
if *pc == nil {
|
||||
continue
|
||||
}
|
||||
(*pc).Close()
|
||||
(*pc) = nil
|
||||
}
|
||||
|
||||
if sp.pty != nil {
|
||||
if err := sp.pty.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
sp.pty = nil
|
||||
}
|
||||
|
||||
if sp.hproc != 0 {
|
||||
if _, err := sp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
windows.CloseHandle(sp.hproc)
|
||||
sp.hproc = 0
|
||||
sp.pid = 0
|
||||
if sp.sess != nil {
|
||||
sp.sess.release()
|
||||
sp.sess = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Order is important here. Do not close sp.rStdout until _after_
|
||||
// ss.pty (when present) has been closed! We're going to do one better by
|
||||
// doing this after the process is done.
|
||||
for _, pc := range []*io.ReadCloser{&sp.rStdout, &sp.rStderr} {
|
||||
if *pc == nil {
|
||||
continue
|
||||
}
|
||||
(*pc).Close()
|
||||
(*pc) = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks the caller until sp terminates. It returns the process exit code.
|
||||
// exitCode will be set to 254 if the process terminated but the exit code could
|
||||
// not be retrieved.
|
||||
func (sp *Process) Wait() (exitCode uint32, err error) {
|
||||
_, err = windows.WaitForSingleObject(sp.hproc, windows.INFINITE)
|
||||
if err == nil {
|
||||
if err := windows.GetExitCodeProcess(sp.hproc, &exitCode); err != nil {
|
||||
exitCode = 254
|
||||
}
|
||||
}
|
||||
return exitCode, err
|
||||
}
|
||||
|
||||
// OSProcess returns an *os.Process associated with sp. This is useful for
|
||||
// integration with external code that expects an os.Process.
|
||||
func (sp *Process) OSProcess() (*os.Process, error) {
|
||||
if sp.hproc == 0 {
|
||||
return nil, winutil.ErrDefunctProcess
|
||||
}
|
||||
return os.FindProcess(int(sp.pid))
|
||||
}
|
||||
|
||||
// PTYResizer returns a function to be called to resize the pseudoconsole.
|
||||
// It returns nil if no pseudoconsole was requested when creating sp.
|
||||
func (sp *Process) PTYResizer() func(windows.Coord) error {
|
||||
if sp.wResize != nil {
|
||||
wResize := sp.wResize
|
||||
return func(c windows.Coord) error {
|
||||
return binary.Write(wResize, binary.LittleEndian, c)
|
||||
}
|
||||
}
|
||||
|
||||
if sp.pty != nil {
|
||||
pty := sp.pty
|
||||
return func(c windows.Coord) error {
|
||||
return pty.Resize(c)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type relayArgs struct {
|
||||
command string
|
||||
resize string
|
||||
ptyX int
|
||||
ptyY int
|
||||
}
|
||||
|
||||
func parseRelayArgs(args []string) (a relayArgs) {
|
||||
flags := flag.NewFlagSet("", flag.ExitOnError)
|
||||
flags.StringVar(&a.command, "cmd", "", "the command to run")
|
||||
flags.StringVar(&a.resize, "resize", "", "handle to resize pipe")
|
||||
flags.IntVar(&a.ptyX, "x", 80, "initial width of pty")
|
||||
flags.IntVar(&a.ptyY, "y", 25, "initial height of pty")
|
||||
flags.Parse(args)
|
||||
return a
|
||||
}
|
||||
|
||||
func flagSizeErr(flagName byte) error {
|
||||
return fmt.Errorf("--%c must be greater than zero and less than %d", flagName, math.MaxInt16)
|
||||
}
|
||||
|
||||
const debugRelay = false
|
||||
|
||||
func beRelay(args []string) error {
|
||||
ra := parseRelayArgs(args)
|
||||
if ra.command == "" {
|
||||
return fmt.Errorf("--cmd must be specified")
|
||||
}
|
||||
|
||||
bitSize := int(unsafe.Sizeof(windows.Handle(0)) * 8)
|
||||
resize64, err := strconv.ParseUint(ra.resize, 0, bitSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hResize := windows.Handle(resize64)
|
||||
if ft, _ := windows.GetFileType(hResize); ft != windows.FILE_TYPE_PIPE {
|
||||
return fmt.Errorf("--resize is an invalid handle type")
|
||||
}
|
||||
resize := os.NewFile(uintptr(hResize), "rPTYResizePipe")
|
||||
defer resize.Close()
|
||||
|
||||
switch {
|
||||
case ra.ptyX <= 0 || ra.ptyX > math.MaxInt16:
|
||||
return flagSizeErr('x')
|
||||
case ra.ptyY <= 0 || ra.ptyY > math.MaxInt16:
|
||||
return flagSizeErr('y')
|
||||
default:
|
||||
}
|
||||
|
||||
logf := logger.Discard
|
||||
if debugRelay {
|
||||
// Our parent process will write our stderr to its log.
|
||||
logf = func(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
logf("starting")
|
||||
argv, err := windows.DecomposeCommandLine(ra.command)
|
||||
if err != nil {
|
||||
logf("DecomposeCommandLine failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cli := winutil.CommandLineInfo{
|
||||
ExePath: argv[0],
|
||||
}
|
||||
cli.SetArgs(argv[1:])
|
||||
|
||||
opts := startProcessOpts{
|
||||
ptySize: windows.Coord{X: int16(ra.ptyX), Y: int16(ra.ptyY)},
|
||||
}
|
||||
psp, err := startProcessInternal(nil, logf, cli, opts)
|
||||
if err != nil {
|
||||
logf("startProcessInternal failed: %v", err)
|
||||
return err
|
||||
}
|
||||
defer psp.Close()
|
||||
|
||||
go resizeLoop(logf, resize, psp.PTYResizer())
|
||||
if debugRelay {
|
||||
go debugLogPTYInput(logf, psp.wStdin, os.Stdin)
|
||||
go debugLogPTYOutput(logf, os.Stdout, psp.rStdout)
|
||||
} else {
|
||||
go io.Copy(psp.wStdin, os.Stdin)
|
||||
go io.Copy(os.Stdout, psp.rStdout)
|
||||
}
|
||||
|
||||
exitCode, err := psp.Wait()
|
||||
if err != nil {
|
||||
logf("waiting on relayed process: %v", err)
|
||||
return err
|
||||
}
|
||||
if exitCode > 0 {
|
||||
logf("relayed process returned %v", exitCode)
|
||||
}
|
||||
|
||||
if err := psp.Close(); err != nil {
|
||||
logf("s4u.Process.Close error: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resizeLoop(logf logger.Logf, resizePipe io.Reader, resizeFn func(windows.Coord) error) {
|
||||
var coord windows.Coord
|
||||
for binary.Read(resizePipe, binary.LittleEndian, &coord) == nil {
|
||||
logf("resizing pty window to %#v", coord)
|
||||
resizeFn(coord)
|
||||
}
|
||||
}
|
||||
|
||||
func debugLogPTYInput(logf logger.Logf, w io.Writer, r io.Reader) {
|
||||
logw := logger.FuncWriter(logger.WithPrefix(logf, "(pty input) "))
|
||||
io.Copy(io.MultiWriter(w, logw), r)
|
||||
}
|
||||
|
||||
func debugLogPTYOutput(logf logger.Logf, w io.Writer, r io.Reader) {
|
||||
logw := logger.FuncWriter(logger.WithPrefix(logf, "(pty output) "))
|
||||
io.Copy(w, io.TeeReader(r, logw))
|
||||
}
|
||||
|
||||
// mergeEnv returns the union of existingEnv and extraEnv, deduplicated and
|
||||
// sorted.
|
||||
func mergeEnv(existingEnv []string, extraEnv map[string]string) []string {
|
||||
if len(extraEnv) == 0 {
|
||||
return existingEnv
|
||||
}
|
||||
|
||||
mergedMap := make(map[string]string, len(existingEnv)+len(extraEnv))
|
||||
for _, line := range existingEnv {
|
||||
k, v, _ := strings.Cut(line, "=")
|
||||
mergedMap[strings.ToUpper(k)] = v
|
||||
}
|
||||
|
||||
for k, v := range extraEnv {
|
||||
mergedMap[strings.ToUpper(k)] = v
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(mergedMap))
|
||||
for k, v := range mergedMap {
|
||||
result = append(result, strings.Join([]string{k, v}, "="))
|
||||
}
|
||||
|
||||
slices.SortFunc(result, func(l, r string) int {
|
||||
kl, _, _ := strings.Cut(l, "=")
|
||||
kr, _, _ := strings.Cut(r, "=")
|
||||
return strings.Compare(kl, kr)
|
||||
})
|
||||
return result
|
||||
}
|
||||
104
util/winutil/s4u/zsyscall_windows.go
Normal file
104
util/winutil/s4u/zsyscall_windows.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package s4u
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modsecur32 = windows.NewLazySystemDLL("secur32.dll")
|
||||
|
||||
procAllocateLocallyUniqueId = modadvapi32.NewProc("AllocateLocallyUniqueId")
|
||||
procImpersonateLoggedOnUser = modadvapi32.NewProc("ImpersonateLoggedOnUser")
|
||||
procLsaConnectUntrusted = modsecur32.NewProc("LsaConnectUntrusted")
|
||||
procLsaDeregisterLogonProcess = modsecur32.NewProc("LsaDeregisterLogonProcess")
|
||||
procLsaFreeReturnBuffer = modsecur32.NewProc("LsaFreeReturnBuffer")
|
||||
procLsaLogonUser = modsecur32.NewProc("LsaLogonUser")
|
||||
procLsaLookupAuthenticationPackage = modsecur32.NewProc("LsaLookupAuthenticationPackage")
|
||||
procLsaRegisterLogonProcess = modsecur32.NewProc("LsaRegisterLogonProcess")
|
||||
)
|
||||
|
||||
func allocateLocallyUniqueId(luid *windows.LUID) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procAllocateLocallyUniqueId.Addr(), 1, uintptr(unsafe.Pointer(luid)), 0, 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func impersonateLoggedOnUser(token windows.Token) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procImpersonateLoggedOnUser.Addr(), 1, uintptr(token), 0, 0)
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lsaConnectUntrusted(lsaHandle *_LSAHANDLE) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall(procLsaConnectUntrusted.Addr(), 1, uintptr(unsafe.Pointer(lsaHandle)), 0, 0)
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func lsaDeregisterLogonProcess(lsaHandle _LSAHANDLE) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall(procLsaDeregisterLogonProcess.Addr(), 1, uintptr(lsaHandle), 0, 0)
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func lsaFreeReturnBuffer(buffer uintptr) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall(procLsaFreeReturnBuffer.Addr(), 1, uintptr(buffer), 0, 0)
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func lsaLogonUser(lsaHandle _LSAHANDLE, originName *windows.NTString, logonType _SECURITY_LOGON_TYPE, authenticationPackage uint32, authenticationInformation unsafe.Pointer, authenticationInformationLength uint32, localGroups *windows.Tokengroups, sourceContext *_TOKEN_SOURCE, profileBuffer *uintptr, profileBufferLength *uint32, logonID *windows.LUID, token *windows.Token, quotas *_QUOTA_LIMITS, subStatus *windows.NTStatus) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall15(procLsaLogonUser.Addr(), 14, uintptr(lsaHandle), uintptr(unsafe.Pointer(originName)), uintptr(logonType), uintptr(authenticationPackage), uintptr(authenticationInformation), uintptr(authenticationInformationLength), uintptr(unsafe.Pointer(localGroups)), uintptr(unsafe.Pointer(sourceContext)), uintptr(unsafe.Pointer(profileBuffer)), uintptr(unsafe.Pointer(profileBufferLength)), uintptr(unsafe.Pointer(logonID)), uintptr(unsafe.Pointer(token)), uintptr(unsafe.Pointer(quotas)), uintptr(unsafe.Pointer(subStatus)), 0)
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func lsaLookupAuthenticationPackage(lsaHandle _LSAHANDLE, packageName *windows.NTString, authenticationPackage *uint32) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall(procLsaLookupAuthenticationPackage.Addr(), 3, uintptr(lsaHandle), uintptr(unsafe.Pointer(packageName)), uintptr(unsafe.Pointer(authenticationPackage)))
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func lsaRegisterLogonProcess(logonProcessName *windows.NTString, lsaHandle *_LSAHANDLE, securityMode *_LSA_OPERATIONAL_MODE) (ret windows.NTStatus) {
|
||||
r0, _, _ := syscall.Syscall(procLsaRegisterLogonProcess.Addr(), 3, uintptr(unsafe.Pointer(logonProcessName)), uintptr(unsafe.Pointer(lsaHandle)), uintptr(unsafe.Pointer(securityMode)))
|
||||
ret = windows.NTStatus(r0)
|
||||
return
|
||||
}
|
||||
@@ -135,9 +135,36 @@ func (up *UserProfile) Close() error {
|
||||
}
|
||||
|
||||
func getRoamingProfilePath(logf logger.Logf, token windows.Token, computerName, userName *uint16) (path *uint16, err error) {
|
||||
// logf is for debugging/testing.
|
||||
if logf == nil {
|
||||
logf = logger.Discard
|
||||
// logf is for debugging/testing. While we would normally replace a nil logf
|
||||
// with logger.Discard, we're using explicit checks within this func so that
|
||||
// we don't waste time allocating and converting UTF-16 strings unnecessarily.
|
||||
var comp string
|
||||
if logf != nil {
|
||||
comp = windows.UTF16PtrToString(computerName)
|
||||
user := windows.UTF16PtrToString(userName)
|
||||
logf("BEGIN getRoamingProfilePath(%q, %q)", comp, user)
|
||||
defer logf("END getRoamingProfilePath(%q, %q)", comp, user)
|
||||
}
|
||||
|
||||
isDomainName, err := isDomainName(computerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isDomainName {
|
||||
if logf != nil {
|
||||
logf("computerName %q is a domain, resolving...", comp)
|
||||
}
|
||||
dcInfo, err := resolveDomainController(computerName, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dcInfo.Close()
|
||||
|
||||
computerName = dcInfo.DomainControllerName
|
||||
if logf != nil {
|
||||
dom := windows.UTF16PtrToString(computerName)
|
||||
logf("%q resolved to %q", comp, dom)
|
||||
}
|
||||
}
|
||||
|
||||
var pbuf *byte
|
||||
@@ -147,7 +174,9 @@ func getRoamingProfilePath(logf logger.Logf, token windows.Token, computerName,
|
||||
defer windows.NetApiBufferFree(pbuf)
|
||||
|
||||
ui4 := (*_USER_INFO_4)(unsafe.Pointer(pbuf))
|
||||
logf("getRoamingProfilePath: got %#v", *ui4)
|
||||
if logf != nil {
|
||||
logf("getRoamingProfilePath: got %#v", *ui4)
|
||||
}
|
||||
profilePath := ui4.Profile
|
||||
if profilePath == nil {
|
||||
return nil, nil
|
||||
@@ -162,6 +191,10 @@ func getRoamingProfilePath(logf logger.Logf, token windows.Token, computerName,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if logf != nil {
|
||||
logf("returning %q", windows.UTF16ToString(expanded[:]))
|
||||
}
|
||||
|
||||
// This buffer is only used briefly, so we don't bother copying it into a shorter slice.
|
||||
return &expanded[0], nil
|
||||
}
|
||||
|
||||
24
util/winutil/userprofile_windows_test.go
Normal file
24
util/winutil/userprofile_windows_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package winutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestGetRoamingProfilePath(t *testing.T) {
|
||||
token := windows.GetCurrentProcessToken()
|
||||
computerName, userName, err := getComputerAndUserName(token, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := getRoamingProfilePath(t.Logf, token, computerName, userName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// TODO(aaron): Flesh out better once can run tests under domain accounts.
|
||||
}
|
||||
@@ -647,7 +647,8 @@ func LogonSessionID(token windows.Token) (logonSessionID windows.LUID, err error
|
||||
return origin.originatingLogonSession, nil
|
||||
}
|
||||
|
||||
// BufUnit is a type constraint for buffers passed into AllocateContiguousBuffer.
|
||||
// BufUnit is a type constraint for buffers passed into AllocateContiguousBuffer
|
||||
// and SetNTString.
|
||||
type BufUnit interface {
|
||||
byte | uint16
|
||||
}
|
||||
@@ -784,3 +785,147 @@ func SetNTString[NTS NTStr, BU BufUnit](nts *NTS, buf []BU) {
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
type domainControllerAddressType uint32
|
||||
|
||||
const (
|
||||
//lint:ignore U1000 maps to a win32 API
|
||||
_DS_INET_ADDRESS domainControllerAddressType = 1
|
||||
_DS_NETBIOS_ADDRESS domainControllerAddressType = 2
|
||||
)
|
||||
|
||||
type domainControllerFlag uint32
|
||||
|
||||
const (
|
||||
//lint:ignore U1000 maps to a win32 API
|
||||
_DS_PDC_FLAG domainControllerFlag = 0x00000001
|
||||
_DS_GC_FLAG domainControllerFlag = 0x00000004
|
||||
_DS_LDAP_FLAG domainControllerFlag = 0x00000008
|
||||
_DS_DS_FLAG domainControllerFlag = 0x00000010
|
||||
_DS_KDC_FLAG domainControllerFlag = 0x00000020
|
||||
_DS_TIMESERV_FLAG domainControllerFlag = 0x00000040
|
||||
_DS_CLOSEST_FLAG domainControllerFlag = 0x00000080
|
||||
_DS_WRITABLE_FLAG domainControllerFlag = 0x00000100
|
||||
_DS_GOOD_TIMESERV_FLAG domainControllerFlag = 0x00000200
|
||||
_DS_NDNC_FLAG domainControllerFlag = 0x00000400
|
||||
_DS_SELECT_SECRET_DOMAIN_6_FLAG domainControllerFlag = 0x00000800
|
||||
_DS_FULL_SECRET_DOMAIN_6_FLAG domainControllerFlag = 0x00001000
|
||||
_DS_WS_FLAG domainControllerFlag = 0x00002000
|
||||
_DS_DS_8_FLAG domainControllerFlag = 0x00004000
|
||||
_DS_DS_9_FLAG domainControllerFlag = 0x00008000
|
||||
_DS_DS_10_FLAG domainControllerFlag = 0x00010000
|
||||
_DS_KEY_LIST_FLAG domainControllerFlag = 0x00020000
|
||||
_DS_PING_FLAGS domainControllerFlag = 0x000FFFFF
|
||||
_DS_DNS_CONTROLLER_FLAG domainControllerFlag = 0x20000000
|
||||
_DS_DNS_DOMAIN_FLAG domainControllerFlag = 0x40000000
|
||||
_DS_DNS_FOREST_FLAG domainControllerFlag = 0x80000000
|
||||
)
|
||||
|
||||
type _DOMAIN_CONTROLLER_INFO struct {
|
||||
DomainControllerName *uint16
|
||||
DomainControllerAddress *uint16
|
||||
DomainControllerAddressType domainControllerAddressType
|
||||
DomainGuid windows.GUID
|
||||
DomainName *uint16
|
||||
DnsForestName *uint16
|
||||
Flags domainControllerFlag
|
||||
DcSiteName *uint16
|
||||
ClientSiteName *uint16
|
||||
}
|
||||
|
||||
func (dci *_DOMAIN_CONTROLLER_INFO) Close() error {
|
||||
if dci == nil {
|
||||
return nil
|
||||
}
|
||||
return windows.NetApiBufferFree((*byte)(unsafe.Pointer(dci)))
|
||||
}
|
||||
|
||||
type dsGetDcNameFlag uint32
|
||||
|
||||
const (
|
||||
//lint:ignore U1000 maps to a win32 API
|
||||
_DS_FORCE_REDISCOVERY dsGetDcNameFlag = 0x00000001
|
||||
_DS_DIRECTORY_SERVICE_REQUIRED dsGetDcNameFlag = 0x00000010
|
||||
_DS_DIRECTORY_SERVICE_PREFERRED dsGetDcNameFlag = 0x00000020
|
||||
_DS_GC_SERVER_REQUIRED dsGetDcNameFlag = 0x00000040
|
||||
_DS_PDC_REQUIRED dsGetDcNameFlag = 0x00000080
|
||||
_DS_BACKGROUND_ONLY dsGetDcNameFlag = 0x00000100
|
||||
_DS_IP_REQUIRED dsGetDcNameFlag = 0x00000200
|
||||
_DS_KDC_REQUIRED dsGetDcNameFlag = 0x00000400
|
||||
_DS_TIMESERV_REQUIRED dsGetDcNameFlag = 0x00000800
|
||||
_DS_WRITABLE_REQUIRED dsGetDcNameFlag = 0x00001000
|
||||
_DS_GOOD_TIMESERV_PREFERRED dsGetDcNameFlag = 0x00002000
|
||||
_DS_AVOID_SELF dsGetDcNameFlag = 0x00004000
|
||||
_DS_ONLY_LDAP_NEEDED dsGetDcNameFlag = 0x00008000
|
||||
_DS_IS_FLAT_NAME dsGetDcNameFlag = 0x00010000
|
||||
_DS_IS_DNS_NAME dsGetDcNameFlag = 0x00020000
|
||||
_DS_TRY_NEXTCLOSEST_SITE dsGetDcNameFlag = 0x00040000
|
||||
_DS_DIRECTORY_SERVICE_6_REQUIRED dsGetDcNameFlag = 0x00080000
|
||||
_DS_WEB_SERVICE_REQUIRED dsGetDcNameFlag = 0x00100000
|
||||
_DS_DIRECTORY_SERVICE_8_REQUIRED dsGetDcNameFlag = 0x00200000
|
||||
_DS_DIRECTORY_SERVICE_9_REQUIRED dsGetDcNameFlag = 0x00400000
|
||||
_DS_DIRECTORY_SERVICE_10_REQUIRED dsGetDcNameFlag = 0x00800000
|
||||
_DS_KEY_LIST_SUPPORT_REQUIRED dsGetDcNameFlag = 0x01000000
|
||||
_DS_RETURN_DNS_NAME dsGetDcNameFlag = 0x40000000
|
||||
_DS_RETURN_FLAT_NAME dsGetDcNameFlag = 0x80000000
|
||||
)
|
||||
|
||||
func resolveDomainController(domainName *uint16, domainGUID *windows.GUID) (*_DOMAIN_CONTROLLER_INFO, error) {
|
||||
const flags = _DS_DIRECTORY_SERVICE_REQUIRED | _DS_IS_FLAT_NAME | _DS_RETURN_DNS_NAME
|
||||
var dcInfo *_DOMAIN_CONTROLLER_INFO
|
||||
if err := dsGetDcName(nil, domainName, domainGUID, nil, flags, &dcInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dcInfo, nil
|
||||
}
|
||||
|
||||
// ResolveDomainController resolves the DNS name of the nearest available
|
||||
// domain controller for the domain specified by domainName.
|
||||
func ResolveDomainController(domainName string) (string, error) {
|
||||
domainName16, err := windows.UTF16PtrFromString(domainName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dcInfo, err := resolveDomainController(domainName16, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dcInfo.Close()
|
||||
|
||||
return windows.UTF16PtrToString(dcInfo.DomainControllerName), nil
|
||||
}
|
||||
|
||||
type _NETSETUP_NAME_TYPE int32
|
||||
|
||||
const (
|
||||
_NetSetupUnknown _NETSETUP_NAME_TYPE = 0
|
||||
_NetSetupMachine _NETSETUP_NAME_TYPE = 1
|
||||
_NetSetupWorkgroup _NETSETUP_NAME_TYPE = 2
|
||||
_NetSetupDomain _NETSETUP_NAME_TYPE = 3
|
||||
_NetSetupNonExistentDomain _NETSETUP_NAME_TYPE = 4
|
||||
_NetSetupDnsMachine _NETSETUP_NAME_TYPE = 5
|
||||
)
|
||||
|
||||
func isDomainName(name *uint16) (bool, error) {
|
||||
err := netValidateName(nil, name, nil, nil, _NetSetupDomain)
|
||||
switch err {
|
||||
case nil:
|
||||
return true, nil
|
||||
case windows.ERROR_NO_SUCH_DOMAIN:
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// IsDomainName checks whether name represents an existing domain reachable by
|
||||
// the current machine.
|
||||
func IsDomainName(name string) (bool, error) {
|
||||
name16, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return isDomainName(name16)
|
||||
}
|
||||
|
||||
@@ -42,12 +42,15 @@ func errnoErr(e syscall.Errno) error {
|
||||
var (
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modnetapi32 = windows.NewLazySystemDLL("netapi32.dll")
|
||||
modrstrtmgr = windows.NewLazySystemDLL("rstrtmgr.dll")
|
||||
moduserenv = windows.NewLazySystemDLL("userenv.dll")
|
||||
|
||||
procQueryServiceConfig2W = modadvapi32.NewProc("QueryServiceConfig2W")
|
||||
procGetApplicationRestartSettings = modkernel32.NewProc("GetApplicationRestartSettings")
|
||||
procRegisterApplicationRestart = modkernel32.NewProc("RegisterApplicationRestart")
|
||||
procDsGetDcNameW = modnetapi32.NewProc("DsGetDcNameW")
|
||||
procNetValidateName = modnetapi32.NewProc("NetValidateName")
|
||||
procRmEndSession = modrstrtmgr.NewProc("RmEndSession")
|
||||
procRmGetList = modrstrtmgr.NewProc("RmGetList")
|
||||
procRmJoinSession = modrstrtmgr.NewProc("RmJoinSession")
|
||||
@@ -78,6 +81,22 @@ func registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret w
|
||||
return
|
||||
}
|
||||
|
||||
func dsGetDcName(computerName *uint16, domainName *uint16, domainGuid *windows.GUID, siteName *uint16, flags dsGetDcNameFlag, dcInfo **_DOMAIN_CONTROLLER_INFO) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procDsGetDcNameW.Addr(), 6, uintptr(unsafe.Pointer(computerName)), uintptr(unsafe.Pointer(domainName)), uintptr(unsafe.Pointer(domainGuid)), uintptr(unsafe.Pointer(siteName)), uintptr(flags), uintptr(unsafe.Pointer(dcInfo)))
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func netValidateName(server *uint16, name *uint16, account *uint16, password *uint16, nameType _NETSETUP_NAME_TYPE) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procNetValidateName.Addr(), 5, uintptr(unsafe.Pointer(server)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(account)), uintptr(unsafe.Pointer(password)), uintptr(nameType), 0)
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func rmEndSession(session _RMHANDLE) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procRmEndSession.Addr(), 1, uintptr(session), 0, 0)
|
||||
if r0 != 0 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user