Compare commits

...

62 Commits

Author SHA1 Message Date
Brad Fitzpatrick
1f845922bf types/views: add MarshalJSONV2 to Slice
It helps, but not all the way.

                               │   before   │                  after                  │
                               │    B/op    │    B/op      vs base                    │
    JSONMarshalView/v1_slice-8   64.00 ± 0%   64.00 ± ∞ ¹        ~ (p=1.000 n=10+1) ²
    JSONMarshalView/v1_view-8    136.0 ± 0%   136.0 ± ∞ ¹        ~ (p=1.000 n=10+1) ²
    JSONMarshalView/v2_slice-8   64.00 ± 0%   64.00 ± ∞ ¹        ~ (p=1.000 n=10+1) ²
    JSONMarshalView/v2_view-8    136.0 ± 0%   112.0 ± ∞ ¹  -17.65% (n=10+1)

Updates tailscale/corp#14379
Updates tailscale/corp#21429 (new benchmark)

Change-Id: I891dd6a4db1a995ae1be14f8c5085629bb30b8da
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-07 13:34:11 -07:00
Brad Fitzpatrick
d2fef01206 control/controlknobs,tailcfg,wgengine/magicsock: remove DRPO shutoff switch
The DERP Return Path Optimization (DRPO) is over four years old (and
on by default for over two) and we haven't had problems, so time to
remove the emergency shutoff code (controlknob) which we've never
used. The controlknobs are only meant for new features, to mitigate
risk. But we don't want to keep them forever, as they kinda pollute
the code.

Updates #150

Change-Id: If021bc8fd1b51006d8bddd1ffab639bb1abb0ad1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-06 19:50:53 -07:00
Brad Fitzpatrick
9df107f4f0 wgengine/magicsock: use derp-region-as-magic-AddrPort hack in fewer places
And fix up a bogus comment and flesh out some other comments.

Updates #cleanup

Change-Id: Ia60a1c04b0f5e44e8d9587914af819df8e8f442a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-06 19:38:59 -07:00
Aaron Klotz
e181f12a7b util/winutil/s4u: fix some doc comments in the s4u package
This is #cleanup

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-07-05 13:19:47 -07:00
Brad Fitzpatrick
c4b20c5411 go.mod: bump github.com/tailscale/wireguard-go
Updates tailscale/corp#20732

Change-Id: Ic0272fe9a226afef4e23dfca5da8cd1d550c1cd6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-05 09:44:15 -07:00
Tom Proctor
01a7726cf7 cmd/containerboot,cmd/k8s-operator: enable IPv6 for fqdn egress proxies (#12577)
cmd/containerboot,cmd/k8s-operator: enable IPv6 for fqdn egress proxies

Don't skip installing egress forwarding rules for IPv6 (as long as the host
supports IPv6), and set headless services `ipFamilyPolicy` to
`PreferDualStack` to optionally enable both IP families when possible. Note
that even with `PreferDualStack` set, testing a dual-stack GKE cluster with
the default DNS setup of kube-dns did not correctly set both A and
AAAA records for the headless service, and instead only did so when
switching the cluster DNS to Cloud DNS. For both IPv4 and IPv6 to work
simultaneously in a dual-stack cluster, we require headless services to
return both A and AAAA records.

If the host doesn't support IPv6 but the FQDN specified only has IPv6
addresses available, containerboot will exit with error code 1 and an
error message because there is no viable egress route.

Fixes #12215

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2024-07-05 12:21:48 +01:00
Andrea Gottardo
309afa53cf health: send ImpactsConnectivity value over LocalAPI (#12700)
Updates tailscale/tailscale#4136

We should make sure to send the value of ImpactsConnectivity over to the clients using LocalAPI as they need it to display alerts in the GUI properly.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-07-03 20:19:06 +00:00
Charlotte Brandhorst-Satzkorn
42f01afe26 cmd/tailscale/cli: exit node filter should display all exit node options (#12699)
This change expands the `exit-node list -filter` command to display all
location based exit nodes for the filtered country. This allows users
to switch to alternative servers when our recommended exit node is not
working as intended.

This change also makes the country filter matching case insensitive,
e.g. both USA and usa will work.

Updates #12698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-07-03 11:48:20 -07:00
Chris Palmer
59936e6d4a scripts: don't refresh the pacman repository on Arch (#12194)
Fixes #12186

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
Co-authored-by: Chris Palmer <cpalmer@tailscale.com>
2024-07-03 09:58:01 -07:00
Andrea Gottardo
732af2f6e0 health: reduce severity of some warnings, improve update messages (#12689)
Updates tailscale/tailscale#4136

High severity health warning = a system notification will appear, which can be quite disruptive to the user and cause unnecessary concern in the event of a temporary network issue.

Per design decision (@sonovawolf), the severity of all warnings but "network is down" should be tuned down to medium/low. ImpactsConnectivity should be set, to change the icon to an exclamation mark in some cases, but without a notification bubble.

I also tweaked the messaging for update-available, to reflect how each platform gets updates in different ways.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-07-02 23:11:28 -07:00
Andrew Lytvynov
458decdeb0 go.toolchain.rev: update to Go 1.22.5 (#12690)
Updates https://github.com/tailscale/corp/issues/21304

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-07-02 14:39:30 -07:00
Jonathan Nobels
4e5ef5b628 net/dns: fix broken dns benchmark tests (#12686)
Updates tailscale/corp#20677

The recover function wasn't getting set in the benchmark
tests.  Default changed to an empty func.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-07-02 14:22:13 -04:00
Flakes Updater
012933635b go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-07-01 16:58:27 -07:00
Brad Fitzpatrick
da32468988 version/mkversion: allow env config of oss git cache dir
Updates tailscale/corp#21262

Change-Id: I80bd880b53f6d851c15479f39fad62b25f1095f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-01 16:22:55 -07:00
Jordan Whited
ddf94a7b39 cmd/stunstamp: fix handling of invalid DERP map resp (#12679)
Updates tailscale/corp#20344

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-07-01 16:07:48 -07:00
Brad Fitzpatrick
b56058d7e3 tool/gocross: fix regression detecting when gocross needs rebuild
Fix regression from #8108 (Mar 2023). Since that change, gocross has
always been rebuilt on each run of ./tool/go (gocross-wrapper.sh),
adding ~100ms.  (Well, not totally rebuilt; cmd/go's caching still
ends up working fine.)

The problem was $gocross_path was just "gocross", which isn't in my
path (and "." isn't in my $PATH, as it shouldn't be), so this line was
always evaluating to the empty string:

    gotver="$($gocross_path gocross-version 2>/dev/null || echo '')"

The ./gocross is fine because of the earlier `cd "$repo_root"`

Updates tailscale/corp#21262
Updates tailscale/corp#21263

Change-Id: I80d25446097a3bb3423490c164352f0b569add5f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-07-01 14:40:51 -07:00
License Updater
d780755340 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-07-01 10:31:21 -07:00
Percy Wegmann
489b990240 tailcfg: bump CurrentCapabilityVersion to capture SSH agent forwarding fix
Updates #12467

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-07-01 11:57:55 -05:00
Tom Proctor
d15250aae9 go.{mod,sum}: bump mkctr (#12654)
go get github.com/tailscale/mkctr@main

Pulls in changes to support a local target that only pushes
a single-platform image to the machine's local image store.

Fixes tailscale/mkctr#18

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2024-07-01 10:23:46 +01:00
Claire Wang
8965e87fa8 ipn/ipnlocal: handle auto value for ExitNodeID syspolicy (#12512)
Updates tailscale/corp#19681

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-06-28 23:17:31 -04:00
James Tucker
114d1caf55 derp/xdp: retain the link so that the fd is not closed
BPF links require that the owning FD remains open, this FD is embedded
into the RawLink returned by the attach function and must live for the
duration of the server.

Updates ENG-4274

Signed-off-by: James Tucker <james@tailscale.com>
2024-06-28 14:38:21 -07:00
James Tucker
b565a9faa7 cmd/xdpderper: add autodetection for default interface name
This makes deployment easier in hetrogenous environments.

Updates ENG-4274

Signed-off-by: James Tucker <james@tailscale.com>
2024-06-27 15:42:11 -07:00
Anton Tolchanov
781f79408d ipn/ipnlocal: allow multiple signature chains from the same SigCredential
Detection of duplicate Network Lock signature chains added in
01847e0123 failed to account for chains
originating with a SigCredential signature, which is used for wrapped
auth keys. This results in erroneous removal of signatures that
originate from the same re-usable auth key.

This change ensures that multiple nodes created by the same re-usable
auth key are not getting filtered out by the network lock.

Updates tailscale/corp#19764

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-06-27 19:28:57 +01:00
Anton Tolchanov
4651827f20 tka: test SigCredential signatures and netmap filtering
This change moves handling of wrapped auth keys to the `tka` package and
adds a test covering auth key originating signatures (SigCredential) in
netmap.

Updates tailscale/corp#19764

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-06-27 19:28:57 +01:00
Adrian Dewhurst
8f7588900a ipn/ipnlocal: fix nil pointer dereference and add related test
Fixes #12644

Change-Id: I3589b01a9c671937192caaedbb1312fd906ca712
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-06-27 14:21:59 -04:00
Jordan Whited
0bb82561ba go.mod: update wireguard-go (#12645)
This pulls in device.WaitPool fixes from tailscale/wireguard-go@1e08883
and tailscale/wireguard-go@cfa4567.

Updates tailscale/corp#21095

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-06-27 10:32:14 -07:00
Andrew Lytvynov
2064dc20d4 health,ipn/ipnlocal: hide update warning when auto-updates are enabled (#12631)
When auto-udpates are enabled, we don't need to nag users to update
after a new release, before we release auto-updates.

Updates https://github.com/tailscale/corp/issues/20081

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-06-27 09:36:29 -07:00
Anton Tolchanov
23c5870bd3 tsnet: do not log an error on shutdown
Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-06-27 13:28:19 +01:00
Josh McKinney
18939df0a7 fix: broken tests for localhost
Signed-off-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-26 20:57:19 -07:00
Josh McKinney
1d6ab9f9db cmd/serve: don't convert localhost to 127.0.0.1
This is not valid in many situations, specifically when running a local astro site that listens on localhost, but ignores 127.0.0.1

Fixes: https://github.com/tailscale/tailscale/issues/12201

Signed-off-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-26 20:57:19 -07:00
Brad Fitzpatrick
210264f942 cmd/derper: clarify that derper and tailscaled need to be in sync
Fixes #12617

Change-Id: Ifc87b7d9cf699635087afb57febd01fb9a6d11b7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-26 19:46:42 -07:00
Brad Fitzpatrick
6b801a8e9e cmd/derper: link to various derper docs in more places
In hopes it'll be found more.

Updates tailscale/corp#20844

Change-Id: Ic92ee9908f45b88f8770de285f838333f9467465
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-26 19:46:35 -07:00
Flakes Updater
b3f91845dc go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-06-26 19:43:06 -07:00
James Tucker
46fda6bf4c cmd/derper: add some DERP diagnostics pointers
A few other minor language updates.

Updates tailscale/corp#20844

Change-Id: Idba85941baa0e2714688cc8a4ec3e242e7d1a362
Signed-off-by: James Tucker <james@tailscale.com>
2024-06-26 19:18:28 -07:00
Brad Fitzpatrick
9766f0e110 net/dns: move mutex before the field it guards
And some misc doc tweaks for idiomatic Go style.

Updates #cleanup

Change-Id: I3ca45f78aaca037f433538b847fd6a9571a2d918
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-26 16:56:02 -07:00
dependabot[bot]
94defc4056 build(deps): bump golang.org/x/image from 0.15.0 to 0.18.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.15.0 to 0.18.0.
- [Commits](https://github.com/golang/image/compare/v0.15.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-26 16:19:35 -07:00
Aaron Klotz
b292f7f9ac util/winutil/s4u: fix incorrect token type specified in s4u Login
This was correct before, I think I just made a copy/paste error when
updating that PR.

Updates #12383

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-06-26 14:28:56 -06:00
Aaron Klotz
5f177090e3 util/winutil: ensure domain controller address is used when retrieving remote profile information
We cannot directly pass a flat domain name into NetUserGetInfo; we must
resolve the address of a domain controller first.

This PR implements the appropriate resolution mechanisms to do that, and
also exposes a couple of new utility APIs for future needs.

Fixes #12627

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-06-26 13:10:10 -06:00
Andrew Dunham
0323dd01b2 ci: enable checklocks workflow for specific packages
This turns the checklocks workflow into a real check, and adds
annotations to a few basic packages as a starting point.

Updates #12625

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I2b0185bae05a843b5257980fc6bde732b1bdd93f
2024-06-26 13:55:07 -04:00
Andrew Dunham
8487fd2ec2 wgengine/magicsock: add more DERP home clientmetrics
Updates tailscale/corp#18095

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I423adca2de0730092394bb5fd5796cd35557d352
2024-06-26 11:44:26 -04:00
Adrian Dewhurst
a6b13e6972 cmd/tailscale/cli: correct command emitted by exit node suggestion
The exit node suggestion CLI command was written with the assumption
that it's possible to provide a stableid on the command line, but this
is incorrect. Instead, it will now emit the name of the exit node.

Fixes #12618

Change-Id: Id7277f395b5fca090a99b0d13bfee7b215bc9802
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-06-26 11:29:14 -04:00
Naman Sood
75254178a0 ipn/ipnlocal: don't bind localListener if its context is canceled (#12621)
The context can get canceled during backoff, and binding after that
makes the listener impossible to close afterwards.

Fixes #12620.

Signed-off-by: Naman Sood <mail@nsood.in>
2024-06-26 11:18:45 -04:00
Anton Tolchanov
787ead835f tsweb: accept a function to call before request handling
To complement the existing `onCompletion` callback, which is called
after request handler.

Updates tailscale/corp#17075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-06-26 11:27:26 +01:00
Andrea Gottardo
6e55d8f6a1 health: add warming-up warnable (#12553) 2024-06-25 22:02:38 -07:00
Andrew Dunham
30f8d8199a ipn/ipnlocal: fix data race in tests
We can observe a data race in tests when logging after a test is
finished. `b.onHealthChange` is called in a goroutine after being
registered with `health.Tracker.RegisterWatcher`, which calls callbacks
in `setUnhealthyLocked` in a new goroutine.

See: https://github.com/tailscale/tailscale/actions/runs/9672919302/job/26686038740

Updates #12054

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ibf22cc994965d88a9e7236544878d5373f91229e
2024-06-25 21:43:22 -07:00
Aaron Klotz
da078b4c09 util/winutil: add package for logging into Windows via Service-for-User (S4U)
This PR ties together pseudoconsoles, user profiles, s4u logons, and
process creation into what is (hopefully) a simple API for various
Tailscale services to obtain Windows access tokens without requiring
knowledge of any Windows passwords. It works both for domain-joined
machines (Kerberos) and non-domain-joined machines. The former case
is fairly straightforward as it is fully documented. OTOH, the latter
case is not documented, though it is fully defined in the C headers in
the Windows SDK. The documentation blanks were filled in by reading
the source code of Microsoft's Win32 port of OpenSSH.

We need to do a bit of acrobatics to make conpty work correctly while
creating a child process with an s4u token; see the doc comments above
startProcessInternal for details.

Updates #12383

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-06-25 22:05:52 -06:00
Andrew Dunham
53a5d00fff net/dns: ensure /etc/resolv.conf is world-readable even with a umask
Previously, if we had a umask set (e.g. 0027) that prevented creating a
world-readable file, /etc/resolv.conf would be created without the o+r
bit and thus other users may be unable to resolve DNS.

Since a umask only applies to file creation, chmod the file after
creation and before renaming it to ensure that it has the appropriate
permissions.

Updates #12609

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I2a05d64f4f3a8ee8683a70be17a7da0e70933137
2024-06-26 00:02:05 -04:00
Andrew Dunham
8161024176 wgengine/magicsock: always set home DERP if no control conn
The logic we added in #11378 would prevent selecting a home DERP if we
have no control connection.

Updates tailscale/corp#18095

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I44bb6ac4393989444e4961b8cfa27dc149a33c6e
2024-06-25 23:31:14 -04:00
Andrew Dunham
a475c435ec net/dns/resolver: fix test failure
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0e815a69ee44ca0ff7c0ea0ca3c6904bbf67ed1f
2024-06-25 23:08:08 -04:00
Jonathan Nobels
27033c6277 net/dns: recheck DNS config on SERVFAIL errors (#12547)
Fixes tailscale/corp#20677

Replaces the original attempt to rectify this (by injecting a netMon
event) which was both heavy handed, and missed cases where the
netMon event was "minor".

On apple platforms, the fetching the interface's nameservers can
and does return an empty list in certain situations.   Apple's API
in particular is very limiting here.  The header hints at notifications
for dns changes which would let us react ahead of time, but it's all
private APIs.

To avoid remaining in the state where we end up with no
nameservers but we absolutely need them, we'll react
to a lack of upstream nameservers by attempting to re-query
the OS.

We'll rate limit this to space out the attempts.   It seems relatively
harmless to attempt a reconfig every 5 seconds (triggered
by an incoming query) if the network is in this broken state.

Missing nameservers might possibly be a persistent condition
(vs a transient error), but that would  also imply that something
out of our control is badly misconfigured.

Tested by randomly returning [] for the nameservers.   When switching
between Wifi networks, or cell->wifi, this will randomly trigger
the bug, and we appear to reliably heal the DNS state.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-06-25 14:56:13 -04:00
Brad Fitzpatrick
d5e692f7e7 ipn/ipnlocal: check operator user via osuser package
So non-local users (e.g. Kerberos on FreeIPA) on Linux can be looked
up. Our default binaries are built with pure Go os/user which only
supports the classic /etc/passwd and not any libc-hooked lookups.

Updates #12601

Change-Id: I9592db89e6ca58bf972f2dcee7a35fbf44608a4f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-25 10:56:32 -07:00
Jordan Whited
94415e8029 cmd/stunstamp: remove sqlite DB and API (#12604)
stunstamp now sends data to Prometheus via remote write, and Prometheus
can serve the same data. Retaining and cleaning up old data in sqlite
leads to long probing pauses, and it's not worth investing more effort
to optimize the schema and/or concurrency model.

Updates tailscale/corp#20344

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-06-25 10:21:40 -07:00
Brad Fitzpatrick
3485e4bf5a derp: make RunConnectionLoop funcs take Messages, support PeerPresentFlags
PeerPresentFlags was added in 5ffb2668ef but wasn't plumbed through to
the RunConnectionLoop. Rather than add yet another parameter (as
IP:port was added earlier), pass in the raw PeerPresentMessage and
PeerGoneMessage struct values, which are the same things, plus two
fields: PeerGoneReasonType for gone and the PeerPresentFlags from
5ffb2668ef.

Updates tailscale/corp#17816

Change-Id: Ib19d9f95353651ada90656071fc3656cf58b7987
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-25 09:47:25 -07:00
Fran Bull
7eb8a77ac8 appc: don't schedule advertisement of 0 routes
When the store-appc-routes flag is on for a tailnet we are writing the
routes more often than seems necessary. Investigation reveals that we
are doing so ~every time we observe a dns response, even if this causes
us not to advertise any new routes. So when we have no new routes,
instead do not advertise routes.

Fixes #12593

Signed-off-by: Fran Bull <fran@tailscale.com>
2024-06-25 08:12:51 -07:00
Irbe Krumina
24a40f54d9 util/linuxfw: verify that IPv6 if available if (#12598)
nftable runner for an IPv6 address gets requested.

Updates tailscale/tailscale#12215

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-06-25 14:13:49 +01:00
Brad Fitzpatrick
d91e5c25ce derp: redo, simplify how mesh update writes are queued/written
I couldn't convince myself the old way was safe and couldn't lose
writes.

And it seemed too complicated.

Updates tailscale/corp#21104

Change-Id: I17ba7c7d6fd83458a311ac671146a1f6a458a5c1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-24 21:42:14 -07:00
Brad Fitzpatrick
ded7734c36 derp: account for increased size of peerPresent messages in mesh updates
sendMeshUpdates tries to write as much as possible without blocking,
being careful to check the bufio.Writer.Available size before writes.

Except that regressed in 6c791f7d60 which made those messages larger, which
meants we were doing network I/O with the Server mutex held.

Updates tailscale/corp#13945

Change-Id: Ic327071d2e37de262931b9b390cae32084811919
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-24 16:21:01 -07:00
Andrew Dunham
200d92121f types/lazy: add Peek method to SyncValue
This adds the ability to "peek" at the value of a SyncValue, so that
it's possible to observe a value without computing this.

Updates tailscale/corp#17122

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I06f88c22a1f7ffcbc7ff82946335356bb0ef4622
2024-06-24 12:41:00 -07:00
Aaron Klotz
7dd76c3411 net/netns: add Windows support for bind-to-interface-by-route
This is implemented via GetBestInterfaceEx. Should we encounter errors
or fail to resolve a valid, non-Tailscale interface, we fall back to
returning the index for the default interface instead.

Fixes #12551

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-06-24 10:43:34 -06:00
tailscale-license-updater[bot]
591979b95f licenses: update license notices (#12414)
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
Co-authored-by: License Updater <noreply+license-updater@tailscale.com>
2024-06-24 09:20:34 -07:00
Brad Fitzpatrick
91786ff958 cmd/derper: add debug endpoint to adjust mutex profiling rate
Updates #3560

Change-Id: I474421ce75c79fb66e1c306ed47daebc5a0e069e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-24 09:05:31 -07:00
Brad Fitzpatrick
5ffb2668ef derp: add PeerPresentFlags bitmask to Watch messages
Updates tailscale/corp#17816

Change-Id: Ib5baf6c981a6a4c279f8bbfef02048cfbfb3323b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-22 20:38:25 -07:00
106 changed files with 3989 additions and 933 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -219,7 +219,7 @@ func TestFilterFormatAndSortExitNodes(t *testing.T) {
{
Name: "Rainier",
Peers: []*ipnstate.PeerStatus{
ps[2],
ps[2], ps[3],
},
},
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1 +1 @@
sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=

98
go.sum
View File

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

View File

@@ -1 +1 @@
4d101c0f2d2a234b8902bfff5fadb16070201f0a
2f152a4eff5875655a9a84fce8f8d329f8d9a321

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

@@ -21,8 +21,10 @@ import (
)
type stunStats struct {
mu sync.Mutex
mu sync.Mutex
// +checklocks:mu
readIPv4 int
// +checklocks:mu
readIPv6 int
}

View File

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

View File

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

View File

@@ -513,7 +513,6 @@ main() {
;;
pacman)
set -x
$SUDO pacman -Sy
$SUDO pacman -S tailscale --noconfirm
$SUDO systemctl enable --now tailscaled
set +x

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
# nix-direnv cache busting line: sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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, &quotas, &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
}

View 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

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

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

View File

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

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

View File

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

View File

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