Compare commits

...

43 Commits

Author SHA1 Message Date
Marwan Sulaiman
821c589f98 portlist: move sync.Once up and Close on err
Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-05-25 09:03:28 -04:00
Marwan Sulaiman
27ea062078 portlist: remove NewPoller constructor
This is a follow up on PR #8172 and a breaking change that removes NewPoller.
The issue with the previous PR was that NewPoller immediately initializes the underlying os implementation
and therefore setting IncludeLocalhost as an exported field happened too late and cannot happen early enough.
Using the zero value of Poller was also not an option from outside of the package because we need to set initial
private fields

Fixes #8171

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-05-24 18:17:29 -04:00
Marwan Sulaiman
e32e5c0d0c portlist: add Poller.IncludeLocalhost option
This PR parameterizes receiving loopback updates from the portlist package.
Callers can now include services bound to localhost if they want.
Note that this option is off by default still.

Fixes #8171

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-05-24 13:26:16 -04:00
Will Norris
3d180a16c3 VERSION.txt: this is v1.43.0
Signed-off-by: Will Norris <will@tailscale.com>
2023-05-24 09:03:59 -07:00
Charlotte Brandhorst-Satzkorn
4e86857313 ssh/tailssh: add ssh session recording failed event type
This change introduces a SSHSessionRecordingFailed event type
that is used when a session recording fails to start or fails during a
session, and the on failure indicates that it should fail open.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-22 17:39:01 -07:00
salman aljammaz
745ee97973 api.md: update device authorized API docs to allow for deauth (#8162)
The authorize device API (/api/v2/device/{deviceID}/authorized)
will soon allow device deauthorisation.

Fixes corp#10160.

Signed-off-by: salman <salman@tailscale.com>
2023-05-22 12:52:40 +01:00
Brad Fitzpatrick
a4fd4fd845 ssh/tailssh: fix regression after LDAP support
58ab66ec51 added LDAP support
for #4945 by shelling out to getdent.

It was supposed to fall back to the old method when getdent wasn't
found, but some variable name confusion (uid vs username) meant the
old path wasn't calling the right lookup function (user.LookupId
instead of user.Lookup).

Which meant that changed probably also broke FreeBSD and macOS SSH
support in addition to the reported OpenWRT regression.

The gokrazy support didn't look right either.

Fixes #8180

Change-Id: I273bbe96fe98b2517fbf0335fd476b483c051554
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-21 07:53:25 -07:00
James Tucker
e3cb982139 words: shell-ebrating crustacean chaos
Hey team! I've been diving deep into the code ocean for the past few
hours, tackling those sneaky race conditions that were threatening our
database. It was quite the crabby situation, but fear not! It's friday
and I've emerged and I'm ready to shell-ebrate with some punny word
additions. 🎉

This commit introduces a shell-shocking array of crustaceans to our word
list. From the lively lobsters to the clever prawns.

Signed-off-by: James Tucker <james@tailscale.com>
2023-05-19 14:15:26 -07:00
valscale
5ae786988c derp: remove default logging of disconnecting clients (#8163)
~97% of the log messages derper outputs are related to the normal
non-error state of a client disconnecting in some manner. Add a
verbose logging feature that only logs these messages when enabled.

Fixes #8024

Signed-off-by: Val <valerie@tailscale.com>
2023-05-18 15:35:16 -07:00
Maisem Ali
0ca8bf1e26 ssh/tailssh: close tty on session close
We were only closing on side of the pty/tty pair.
Close the other side too.

Thanks to @fritterhoff for reporting and debugging the issue!

Fixes #8119

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-05-18 09:35:42 -07:00
Gabriel Martinez
03e848e3b5 cmd/k8s-operator: add support for priorityClassName
Updates #8155

Signed-off-by: Gabriel Martinez <gabrielmartinez@sisti.pt>
2023-05-17 15:28:06 -07:00
Derek Kaser
7c88eeba86 cmd/tailscale: allow Tailscale to work with Unraid web interface (#8062)
Updates tailscale/tailscale#8026

Signed-off-by: Derek Kaser <derek.kaser@gmail.com>
2023-05-17 13:26:39 -07:00
Sonia Appasamy
f0ee03dfaf cmd/tailscale/cli: [serve] add reset flag
Usage: `tailscale serve reset`

Fixes #8139

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-05-16 16:19:08 -04:00
Brad Fitzpatrick
4664318be2 client/tailscale: revert CreateKey API change, add Client.CreateKeyWithExpiry
The client/tailscale is a stable-ish API we try not to break. Revert
the Client.CreateKey method as it was and add a new
CreateKeyWithExpiry method to do the new thing. And document the
expiry field and enforce that the time.Duration can't be between in
range greater than 0 and less than a second.

Updates #7143
Updates #8124 (reverts it, effectively)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-12 21:05:12 -07:00
shayne
678bb92bb8 cmd/tailscale/cli: [up] fix CreateKey missing argument (#8124)
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-05-12 02:03:17 -04:00
Matt Brown
9b6e48658f client: allow the expiry time to be specified for new keys
Adds a parameter for create key that allows a number of seconds
(less than 90) to be specified for new keys.

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

Signed-off-by: Matthew Brown <matthew@bargrove.com>
2023-05-11 22:05:18 -07:00
Maisem Ali
85215ed58a cmd/k8s-operator: handle NotFound secrets
getSingleObject can return `nil, nil`, getDeviceInfo was not handling
that case which resulted in panics.

Fixes #7303

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-05-11 18:26:11 -07:00
Brad Fitzpatrick
b69059334b util/set: add a basic map-based Set type
We have two other types of Sets here. Add the basic obvious one too.

Needed for a change elsewhere.

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-11 15:17:34 -07:00
Joe Tsai
84c99fe0d9 logtail: be less aggressive about re-uploads (#8117)
The retry logic was pathological in the following ways:

* If we restarted the logging service, any pending uploads
would be placed in a retry-loop where it depended on backoff.Backoff,
which was too aggresive. It would retry failures within milliseconds,
taking at least 10 retries to hit a delay of 1 second.

* In the event where a logstream was rate limited,
the aggressive retry logic would severely exacerbate the problem
since each retry would also log an error message.
It is by chance that the rate of log error spam
does not happen to exceed the rate limit itself.

We modify the retry logic in the following ways:

* We now respect the "Retry-After" header sent by the logging service.

* Lacking a "Retry-After" header, we retry after a hard-coded period of
30 to 60 seconds. This avoids the thundering-herd effect when all nodes
try reconnecting to the logging service at the same time after a restart.

* We do not treat a status 400 as having been uploaded.
This is simply not the behavior of the logging service.

Updates #tailscale/corp#11213

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-05-11 12:52:35 -07:00
James Tucker
da90fab899 net/netcheck: reenable TestBasic on Windows
This test was either fixed by intermediate changes or was mis-flagged as
failing during #7876 triage.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-05-10 20:16:35 -07:00
James Tucker
ca49b29582 tsnet: reenable TestLoopbackSOCKS5 on Windows
This test was either fixed in the intermediate time or mis-flagged
during the #7876 triage, but is now passing.

Updates #7876

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-05-10 20:15:41 -07:00
Brad Fitzpatrick
cb2fd5be92 cmd/tsconnect: fix forgotten API change for wasm
Fix regression from 6e967446e4

Updates #8036

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-09 21:23:29 -07:00
David Anderson
d27a6e1c53 tool/gocross: fix incorrect relpath usage in bootstrap script
The subshell in which gocross gets built cd's to the corp checkout dir
near the top, so all future references to corp repository files should
be simple relative paths, and not reference $repo_root. When $repo_root
is an absolute path, it doesn't matter and everything works out, but on
some OSes and shells and invocations, $repo_root is a completely relative
path that is invalidated by the "cd".

Fixes tailscale/corp#11183

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-05-09 16:19:46 -07:00
Brad Fitzpatrick
4f454f4122 util/codegen: support embedded fields
I noticed cmd/{cloner,viewer} didn't support structs with embedded
fields while working on a change in another repo. This adds support.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-09 15:40:17 -07:00
Will Norris
ea84fc9ad2 net/sockstats: wait before reporting battery usage
Wait 2 minutes before we start reporting battery usage. There is always
radio activity on initial startup, which gets reported as 100% high
power usage.  Let that settle before we report usage data.

Updates tailscale/corp#9230

Signed-off-by: Will Norris <will@tailscale.com>
2023-05-09 10:18:16 -07:00
salman
1ce08256c0 release/dist: add deb/rpm arch mappings for mipses
According to https://wiki.debian.org/SupportedArchitectures Debian does
not support big-endian mips64, so that one remains disabled.

According to https://fedoraproject.org/wiki/Architectures Fedora only
supports little-endian mips, so leaving both big-endian ones out too.

Updates #8005.

Signed-off-by: salman <salman@tailscale.com>
2023-05-09 17:31:13 +01:00
Craig Rodrigues
827abbeeaa cmd/k8s-operator: print version in startup logs
Fixes: #7813

Signed-off-by: Craig Rodrigues <rodrigc@crodrigues.org>
2023-05-08 18:56:21 -07:00
License Updater
d1ecb1f43b licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-05-08 16:48:36 -07:00
Brad Fitzpatrick
a743b66f9d ssh/tailssh: move some user-related code into new user.go
The previous commit 58ab66e added ssh/tailssh/user.go as part of
working on #4945. So move some more user-related code over to it.

Updates #cleanup

Change-Id: I24de66df25ffb8f867e1a0a540d410f9ef16d7b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-08 13:47:59 -07:00
Brad Fitzpatrick
58ab66ec51 ssh/tailssh: support LDAP users for Tailscale SSH
Fixes #4945

Change-Id: Ie013cb47684cb87928a44f92c66352310bfe53f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-08 11:29:14 -07:00
Chenyang Gao
e8b06b2232 version: properly handle vcs.modified when it's "false"
Current code will set the "dirty" field of VersionInfo always "true"
if vcs.modified flag is there. No matter whether the flag is "true" or
"false".  It will make sense to set this field due to vcs.modified
value, not only the existence of the key.

Signed-off-by: Chenyang Gao <gps949@outlook.com>
2023-05-07 09:15:38 -07:00
License Updater
df8b1b2179 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-05-06 23:48:30 -07:00
License Updater
4d730e154c licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-05-06 16:33:37 -07:00
Chenyang Gao
b9fb8ac702 fix sys.Set(router) issue will crash the daemon in some OSs
Signed-off-by: Chenyang Gao <gps949@outlook.com>

in commit 6e96744, the tsd system type has been added.
Which will cause the daemon will crash on some  OSs (Windows, darwin and so on).
The root cause is that on those OSs, handleSubnetsInNetstack() will return true and set the conf.Router with a wrapper. 
Later in NewUserspaceEngine() it will do subsystem set and found that early set router mismatch to current value, then panic.
2023-05-06 14:11:43 -07:00
James Tucker
5c38f0979e tsweb/promvarz: fix repeated expvar definition in test
expvar can only be defined once, so running tests with a repeat counter
will fail if the variables are defined inside of the test function.

Observed failure:

```
--- FAIL: TestHandler (0.00s)
panic: Reuse of exported var name: gauge_promvarz_test_expvar
 [recovered]
        panic: Reuse of exported var name: gauge_promvarz_test_expvar

goroutine 9 [running]:
testing.tRunner.func1.2({0x100f267e0, 0x1400026e770})
        /usr/local/go/src/testing/testing.go:1526 +0x1c8
testing.tRunner.func1()
        /usr/local/go/src/testing/testing.go:1529 +0x364
panic({0x100f267e0, 0x1400026e770})
        /usr/local/go/src/runtime/panic.go:884 +0x1f4
log.Panicln({0x140000b8e20?, 0x1a?, 0x1400026e750?})
        /usr/local/go/src/log/log.go:398 +0x60
expvar.Publish({0x100e2b21d, 0x1a}, {0x100fd7a08?, 0x140000232c0})
        /usr/local/go/src/expvar/expvar.go:284 +0xc0
expvar.NewInt(...)
        /usr/local/go/src/expvar/expvar.go:304
tailscale.com/tsweb/promvarz.TestHandler(0x14000082b60)
        /Users/charlotte/ts-src/tailscale/tsweb/promvarz/promvarz_test.go:18 +0x5c
testing.tRunner(0x14000082b60, 0x100fd5858)
        /usr/local/go/src/testing/testing.go:1576 +0x104
created by testing.(*T).Run
        /usr/local/go/src/testing/testing.go:1629 +0x370
FAIL    tailscale.com/tsweb/promvarz    0.149s
```

Fixes #8065
Signed-off-by: James Tucker <james@tailscale.com>
2023-05-06 09:53:09 -07:00
Charlotte Brandhorst-Satzkorn
024d48d9c1 tailcfg: bump capability version
This change bumps the capability version to 62, after support for
sending SSHEventNotificationRequests to control via noise for failure
events was introduced.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-05 14:22:59 -07:00
Charlotte Brandhorst-Satzkorn
29ded8f9f9 ssh/tailssh,tailcfg: add connID to ssheventnotifyrequest and castheader
This change adds a ConnectionID field to both SSHEventNotifyRequest and
CastHeader that identifies the ID of a connection to the SSH server.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-05 14:22:59 -07:00
Charlotte Brandhorst-Satzkorn
68307c1411 ssh/tailssh: send ssh event notifications on recording failures
This change sends an SSHEventNotificationRequest over noise when a
SSH session is set to fail closed and the session is unable to start
because a recorder is not available or a session is terminated because
connection to the recorder is ended. Each of these scenarios have their
own event type.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-05 14:22:59 -07:00
Charlotte Brandhorst-Satzkorn
2804327074 tailcfg: update rename SSHFailureNotifyRequest and add EventType
This change renames SSHFailureNotifyRequest to SSHEventNotifyRequest
to better reflect the additional events we could add in the future.
This change also adds an EventType used to catagories the events.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-05 14:22:59 -07:00
Charlotte Brandhorst-Satzkorn
8d3d48e000 ipn/ipnlocal: add NodeKey func to return the public node NodeKey
This change introduces a NodeKey func on localbackend that returns the
public node key.

Updates tailscale/corp#9967

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-05-05 14:22:59 -07:00
Brad Fitzpatrick
8864112a0c ipn/ipnlocal: bound how long cert fetchher checks for existing DNS records
It was supposed to be best effort but in some cases (macsys at least,
per @marwan-at-work) it hangs and exhausts the whole context.Context
deadline so we fail to make the SetDNS call to the server.

Updates #8067
Updates #3273 etc

Change-Id: Ie1f04abe9689951484748aecdeae312afbafdb0f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-05 12:31:45 -07:00
James Tucker
9ed3a061c3 net/netns: fix segv when no tailscale interface is found
`interfaces.Tailscale()` returns all zero values when it finds no
Tailscale interface and encounters no errors. The netns package was
treating no error as a signal that it would receive a non-zero pointer
value leading to nil pointer dereference.

Observed in:

```
--- FAIL: TestGetInterfaceIndex (0.00s)
    --- FAIL: TestGetInterfaceIndex/IP_and_port (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x1029eb7d8]

goroutine 7 [running]:
testing.tRunner.func1.2({0x102a691e0, 0x102bc05c0})
        /Users/raggi/.cache/tailscale-go/src/testing/testing.go:1526 +0x1c8
testing.tRunner.func1()
        /Users/raggi/.cache/tailscale-go/src/testing/testing.go:1529 +0x384
panic({0x102a691e0, 0x102bc05c0})
        /Users/raggi/.cache/tailscale-go/src/runtime/panic.go:884 +0x204
tailscale.com/net/netns.getInterfaceIndex(0x14000073f28, 0x1028d0284?, {0x1029ef3b7, 0xa})
        /Users/raggi/src/github.com/tailscale/tailscale/net/netns/netns_darwin.go:114 +0x228
tailscale.com/net/netns.TestGetInterfaceIndex.func2(0x14000138000)
        /Users/raggi/src/github.com/tailscale/tailscale/net/netns/netns_darwin_test.go:37 +0x54
testing.tRunner(0x14000138000, 0x140000551b0)
        /Users/raggi/.cache/tailscale-go/src/testing/testing.go:1576 +0x10c
created by testing.(*T).Run
        /Users/raggi/.cache/tailscale-go/src/testing/testing.go:1629 +0x368
FAIL    tailscale.com/net/netns 0.824s
```

Fixes #8064

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-05-05 11:31:43 -07:00
Brad Fitzpatrick
6e967446e4 tsd: add package with System type to unify subsystem init, discovery
This is part of an effort to clean up tailscaled initialization between
tailscaled, tailscaled Windows service, tsnet, and the mac GUI.

Updates #8036

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-05-04 14:21:59 -07:00
72 changed files with 1480 additions and 760 deletions

View File

@@ -1 +1 @@
1.41.0
1.43.0

5
api.md
View File

@@ -503,7 +503,8 @@ Returns the enabled and advertised subnet routes for a device.
POST /api/v2/device/{deviceID}/authorized
```
Authorize a device. This call marks a device as authorized for tailnets where device authorization is required.
Authorize a device.
This call marks a device as authorized or revokes its authorization for tailnets where device authorization is required, according to the `authorized` field in the payload.
This returns a successful 2xx response with an empty JSON object in the response body.
@@ -515,7 +516,7 @@ The ID of the device.
#### `authorized` (required in `POST` body)
Specify whether the device is authorized. Only 'true' is currently supported.
Specify whether the device is authorized.
``` jsonc
{

View File

@@ -68,12 +68,32 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
}
// CreateKey creates a new key for the current user. Currently, only auth keys
// can be created. Returns the key itself, which cannot be retrieved again
// can be created. It returns the secret key itself, which cannot be retrieved again
// later, and the key metadata.
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (string, *Key, error) {
//
// To create a key with a specific expiry, use CreateKeyWithExpiry.
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (keySecret string, keyMeta *Key, _ error) {
return c.CreateKeyWithExpiry(ctx, caps, 0)
}
// CreateKeyWithExpiry is like CreateKey, but allows specifying a expiration time.
//
// The time is truncated to a whole number of seconds. If zero, that means no expiration.
func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities, expiry time.Duration) (keySecret string, keyMeta *Key, _ error) {
// convert expirySeconds to an int64 (seconds)
expirySeconds := int64(expiry.Seconds())
if expirySeconds < 0 {
return "", nil, fmt.Errorf("expiry must be positive")
}
if expirySeconds == 0 && expiry != 0 {
return "", nil, fmt.Errorf("non-zero expiry must be at least one second")
}
keyRequest := struct {
Capabilities KeyCapabilities `json:"capabilities"`
}{caps}
Capabilities KeyCapabilities `json:"capabilities"`
ExpirySeconds int64 `json:"expirySeconds,omitempty"`
}{caps, int64(expirySeconds)}
bs, err := json.Marshal(keyRequest)
if err != nil {
return "", nil, err

View File

@@ -48,6 +48,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/dnsname"
"tailscale.com/version"
)
func main() {
@@ -64,6 +65,7 @@ func main() {
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
)
@@ -200,12 +202,13 @@ waitOnline:
}
sr := &ServiceReconciler{
Client: mgr.GetClient(),
tsClient: tsClient,
defaultTags: strings.Split(tags, ","),
operatorNamespace: tsNamespace,
proxyImage: image,
logger: zlog.Named("service-reconciler"),
Client: mgr.GetClient(),
tsClient: tsClient,
defaultTags: strings.Split(tags, ","),
operatorNamespace: tsNamespace,
proxyImage: image,
proxyPriorityClassName: priorityClassName,
logger: zlog.Named("service-reconciler"),
}
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
@@ -235,7 +238,7 @@ waitOnline:
startlog.Fatalf("could not create controller: %v", err)
}
startlog.Infof("Startup complete, operator running")
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
if shouldRunAuthProxy {
cfg, err := restConfig.TransportConfig()
if err != nil {
@@ -278,11 +281,12 @@ const (
// ServiceReconciler is a simple ControllerManagedBy example implementation.
type ServiceReconciler struct {
client.Client
tsClient tsClient
defaultTags []string
operatorNamespace string
proxyImage string
logger *zap.SugaredLogger
tsClient tsClient
defaultTags []string
operatorNamespace string
proxyImage string
proxyPriorityClassName string
logger *zap.SugaredLogger
}
type tsClient interface {
@@ -566,6 +570,9 @@ func (a *ServiceReconciler) getDeviceInfo(ctx context.Context, svc *corev1.Servi
if err != nil {
return "", "", err
}
if sec == nil {
return "", "", nil
}
id = string(sec.Data["device_id"])
if id == "" {
return "", "", nil
@@ -589,6 +596,7 @@ func (a *ServiceReconciler) newAuthKey(ctx context.Context, tags []string) (stri
},
},
}
key, _, err := a.tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
@@ -633,6 +641,7 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
ss.Spec.Template.ObjectMeta.Labels = map[string]string{
"app": string(parentSvc.UID),
}
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
}

View File

@@ -64,7 +64,7 @@ func TestLoadBalancerClass(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -185,7 +185,7 @@ func TestAnnotations(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -282,7 +282,7 @@ func TestAnnotationIntoLB(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, since it would have normally happened at
@@ -326,7 +326,7 @@ func TestAnnotationIntoLB(t *testing.T) {
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
// ... but the service should have a LoadBalancer status.
want = &corev1.Service{
@@ -398,7 +398,7 @@ func TestLBIntoAnnotation(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -455,7 +455,7 @@ func TestLBIntoAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -522,7 +522,7 @@ func TestCustomHostname(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla"))
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla", ""))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -581,6 +581,51 @@ func TestCustomHostname(t *testing.T) {
expectEqual(t, fc, want)
}
func TestCustomPriorityClassName(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
proxyPriorityClassName: "tailscale-critical",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
"tailscale.com/hostname": "custom-priority-class-name",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSTS(shortName, fullName, "custom-priority-class-name", "tailscale-critical"))
}
func expectedSecret(name string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
@@ -629,7 +674,7 @@ func expectedHeadlessService(name string) *corev1.Service {
}
}
func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv1.StatefulSet {
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
@@ -658,6 +703,7 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",
PriorityClassName: priorityClassName,
InitContainers: []corev1.Container{
{
Name: "sysctler",
@@ -814,7 +860,6 @@ func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabili
k := &tailscale.Key{
ID: "key",
Created: time.Now(),
Expires: time.Now().Add(24 * time.Hour),
Capabilities: caps,
}
return "secret-authkey", k, nil

View File

@@ -40,6 +40,7 @@ serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
serve reset
`),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
@@ -87,6 +88,13 @@ EXAMPLES
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
Exec: e.runServeReset,
ShortHelp: "reset current serve/funnel config",
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
},
},
}
}
@@ -705,3 +713,15 @@ func elipticallyTruncate(s string, max int) string {
}
return s[:max-3] + "..."
}
// runServeReset clears out the current serve config.
//
// Usage:
// - tailscale serve reset
func (e *serveEnv) runServeReset(ctx context.Context, args []string) error {
if len(args) != 0 {
return flag.ErrHelp
}
sc := new(ipn.ServeConfig)
return e.lc.SetServeConfig(ctx, sc)
}

View File

@@ -224,7 +224,10 @@ func TestServeConfigMutations(t *testing.T) {
command: cmd("https:443 bar https://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{reset: true})
add(step{ // try resetting using reset command
command: cmd("reset"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{

View File

@@ -61,6 +61,8 @@ type tmplData struct {
TUNMode bool
IsSynology bool
DSMVersion int // 6 or 7, if IsSynology=true
IsUnraid bool
UnraidToken string
IPNVersion string
}
@@ -441,6 +443,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
TUNMode: st.TUN,
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
DSMVersion: distro.DSMVersion(),
IsUnraid: distro.Get() == distro.Unraid,
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
IPNVersion: versionShort,
}
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")

View File

@@ -116,10 +116,12 @@
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
</footer>
<script>(function () {
const advertiseExitNode = {{.AdvertiseExitNode}};
const advertiseExitNode = {{ .AdvertiseExitNode }};
const isUnraid = {{ .IsUnraid }};
const unraidCsrfToken = "{{ .UnraidToken }}";
let fetchingUrl = false;
var data = {
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
AdvertiseRoutes: "{{ .AdvertiseRoutes }}",
AdvertiseExitNode: advertiseExitNode,
Reauthenticate: false,
ForceLogout: false
@@ -141,15 +143,27 @@ function postData(e) {
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
const url = nextUrl.toString();
let body = JSON.stringify(data);
let contentType = "application/json";
if (isUnraid) {
const params = new URLSearchParams();
params.append("csrf_token", unraidCsrfToken);
params.append("ts_data", JSON.stringify(data));
body = params.toString();
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
}
const url = nextUrl.toString();
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Content-Type": contentType,
},
body: JSON.stringify(data)
body: body
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
@@ -158,7 +172,11 @@ function postData(e) {
}
const url = res["url"];
if (url) {
document.location.href = url;
if(isUnraid) {
window.open(url, "_blank");
} else {
document.location.href = url;
}
} else {
location.reload();
}

View File

@@ -282,7 +282,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
tailscale.com/tstime from tailscale.com/wgengine/magicsock+
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled

View File

@@ -50,6 +50,7 @@ import (
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/syncs"
"tailscale.com/tsd"
"tailscale.com/tsweb/varz"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
@@ -330,12 +331,16 @@ var debugMux *http.ServeMux
func run() error {
var logf logger.Logf = log.Printf
sys := new(tsd.System)
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}
sys.Set(netMon)
pol := logpolicy.New(logtail.CollectionNode, netMon)
pol.SetVerbosityLevel(args.verbose)
@@ -386,10 +391,10 @@ func run() error {
debugMux = newDebugMux()
}
return startIPNServer(context.Background(), logf, pol.PublicID, netMon)
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
}
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) error {
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
ln, err := safesocket.Listen(args.socketpath)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
@@ -415,7 +420,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
}
}()
srv := ipnserver.New(logf, logID, netMon)
srv := ipnserver.New(logf, logID, sys.NetMon.Get())
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
@@ -433,7 +438,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return
}
}
lb, err := getLocalBackend(ctx, logf, logID, netMon)
lb, err := getLocalBackend(ctx, logf, logID, sys)
if err == nil {
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
srv.SetLocalBackend(lb)
@@ -457,31 +462,28 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return nil
}
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (_ *ipnlocal.LocalBackend, retErr error) {
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
if logPol != nil {
logPol.Logtail.SetNetMon(netMon)
logPol.Logtail.SetNetMon(sys.NetMon.Get())
}
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
e, onlyNetstack, err := createEngine(logf, netMon, dialer)
sys.Set(dialer)
onlyNetstack, err := createEngine(logf, sys)
if err != nil {
return nil, fmt.Errorf("createEngine: %w", err)
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
if debugMux != nil {
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, _, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
if ms, ok := sys.MagicSock.GetOK(); ok {
debugMux.HandleFunc("/debug/magicsock", ms.ServeHTTPDebug)
}
go runDebugServer(debugMux, args.debug)
}
ns, err := newNetstack(logf, dialer, e)
ns, err := newNetstack(logf, sys)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
@@ -489,6 +491,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
if onlyNetstack {
e := sys.Engine.Get()
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
_, ok := e.PeerForIP(ip)
return ok
@@ -519,16 +522,15 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
tshttpproxy.SetSelfProxy(addrs...)
}
e = wgengine.NewWatchdog(e)
opts := ipnServerOpts()
store, err := store.New(logf, statePathOrDefault())
if err != nil {
return nil, fmt.Errorf("store.New: %w", err)
}
sys.Set(store)
lb, err := ipnlocal.NewLocalBackend(logf, logID, store, dialer, e, opts.LoginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
if err != nil {
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
}
@@ -554,21 +556,21 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
//
// onlyNetstack is true if the user has explicitly requested that we use netstack
// for all networking.
func createEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
func createEngine(logf logger.Logf, sys *tsd.System) (onlyNetstack bool, err error) {
if args.tunname == "" {
return nil, false, errors.New("no --tun value specified")
return false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
e, onlyNetstack, err = tryEngine(logf, netMon, dialer, name)
onlyNetstack, err = tryEngine(logf, sys, name)
if err == nil {
return e, onlyNetstack, nil
return onlyNetstack, nil
}
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
errs = append(errs, err)
}
return nil, false, multierr.New(errs...)
return false, multierr.New(errs...)
}
// handleSubnetsInNetstack reports whether netstack should handle subnet routers
@@ -593,21 +595,23 @@ func handleSubnetsInNetstack() bool {
var tstunNew = tstun.New
func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
NetMon: netMon,
Dialer: dialer,
ListenPort: args.port,
NetMon: sys.NetMon.Get(),
Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set,
}
onlyNetstack = name == "userspace-networking"
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
netns.SetEnabled(!onlyNetstack)
if args.birdSocketPath != "" && createBIRDClient != nil {
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
if err != nil {
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
return false, fmt.Errorf("createBIRDClient: %w", err)
}
}
if onlyNetstack {
@@ -620,44 +624,55 @@ func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer,
// TODO(bradfitz): add a Synology-specific DNS manager.
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
if err != nil {
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
}
} else {
dev, devName, err := tstunNew(logf, name)
if err != nil {
tstun.Diagnose(logf, name, err)
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
return false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev
if strings.HasPrefix(name, "tap:") {
conf.IsTAP = true
e, err := wgengine.NewUserspaceEngine(logf, conf)
return e, false, err
if err != nil {
return false, err
}
sys.Set(e)
return false, err
}
r, err := router.New(logf, dev, netMon)
r, err := router.New(logf, dev, sys.NetMon.Get())
if err != nil {
dev.Close()
return nil, false, fmt.Errorf("creating router: %w", err)
return false, fmt.Errorf("creating router: %w", err)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
dev.Close()
r.Close()
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
conf.DNS = d
conf.Router = r
if handleSubnetsInNetstack() {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
netstackSubnetRouter = true
}
sys.Set(conf.Router)
}
e, err = wgengine.NewUserspaceEngine(logf, conf)
e, err := wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
return nil, onlyNetstack, err
return onlyNetstack, err
}
return e, onlyNetstack, nil
e = wgengine.NewWatchdog(e)
sys.Set(e)
sys.NetstackRouter.Set(netstackSubnetRouter)
return onlyNetstack, nil
}
func newDebugMux() *http.ServeMux {
@@ -687,12 +702,8 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, dns, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn, dialer, dns)
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
return netstack.Create(logf, sys.Tun.Get(), sys.Engine.Get(), sys.MagicSock.Get(), sys.Dialer.Get(), sys.DNSManager.Get())
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -47,6 +47,7 @@ import (
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/winutil"
@@ -292,13 +293,15 @@ func beWindowsSubprocess() bool {
}
}()
sys := new(tsd.System)
netMon, err := netmon.New(log.Printf)
if err != nil {
log.Printf("Could not create netMon: %v", err)
netMon = nil
log.Fatalf("Could not create netMon: %v", err)
}
sys.Set(netMon)
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, netMon)
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}

View File

@@ -37,6 +37,7 @@ import (
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/words"
@@ -96,19 +97,19 @@ func newIPN(jsConfig js.Value) map[string]any {
logtail := logtail.NewLogger(c, log.Printf)
logf := logtail.Logf
sys := new(tsd.System)
sys.Set(store)
dialer := &tsdial.Dialer{Logf: logf}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Dialer: dialer,
Dialer: dialer,
SetSubsystem: sys.Set,
})
if err != nil {
log.Fatal(err)
}
sys.Set(eng)
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
@@ -121,10 +122,11 @@ func newIPN(jsConfig js.Value) map[string]any {
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
sys.NetstackRouter.Set(true)
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
}

View File

@@ -9,7 +9,7 @@ import (
"net/netip"
)
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone --clone-only-type=OnlyGetClone
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded --clone-only-type=OnlyGetClone
type StructWithoutPtrs struct {
Int int
@@ -61,3 +61,8 @@ type StructWithSlices struct {
type OnlyGetClone struct {
SinViewerPorFavor bool
}
type StructWithEmbedded struct {
A *StructWithPtrs
StructWithSlices
}

View File

@@ -211,3 +211,22 @@ func (src *OnlyGetClone) Clone() *OnlyGetClone {
var _OnlyGetCloneCloneNeedsRegeneration = OnlyGetClone(struct {
SinViewerPorFavor bool
}{})
// Clone makes a deep copy of StructWithEmbedded.
// The result aliases no memory with the original.
func (src *StructWithEmbedded) Clone() *StructWithEmbedded {
if src == nil {
return nil
}
dst := new(StructWithEmbedded)
*dst = *src
dst.A = src.A.Clone()
dst.StructWithSlices = *src.StructWithSlices.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithEmbeddedCloneNeedsRegeneration = StructWithEmbedded(struct {
A *StructWithPtrs
StructWithSlices
}{})

View File

@@ -14,7 +14,7 @@ import (
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded
// View returns a readonly view of StructWithPtrs.
func (p *StructWithPtrs) View() StructWithPtrsView {
@@ -325,3 +325,59 @@ var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
Prefixes []netip.Prefix
Data []byte
}{})
// View returns a readonly view of StructWithEmbedded.
func (p *StructWithEmbedded) View() StructWithEmbeddedView {
return StructWithEmbeddedView{ж: p}
}
// StructWithEmbeddedView provides a read-only view over StructWithEmbedded.
//
// Its methods should only be called if `Valid()` returns true.
type StructWithEmbeddedView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructWithEmbedded
}
// Valid reports whether underlying value is non-nil.
func (v StructWithEmbeddedView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v StructWithEmbeddedView) AsStruct() *StructWithEmbedded {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v StructWithEmbeddedView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *StructWithEmbeddedView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x StructWithEmbedded
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v StructWithEmbeddedView) A() StructWithPtrsView { return v.ж.A.View() }
func (v StructWithEmbeddedView) StructWithSlices() StructWithSlicesView {
return v.ж.StructWithSlices.View()
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithEmbeddedViewNeedsRegeneration = StructWithEmbedded(struct {
A *StructWithPtrs
StructWithSlices
}{})

View File

@@ -498,7 +498,7 @@ func (s *Server) registerClient(c *sclient) {
switch set := set.(type) {
case nil:
s.clients[c.key] = singleClient{c}
c.debug("register single client")
c.debugLogf("register single client")
case singleClient:
s.dupClientKeys.Add(1)
s.dupClientConns.Add(2) // both old and new count
@@ -514,7 +514,7 @@ func (s *Server) registerClient(c *sclient) {
},
sendHistory: []*sclient{old},
}
c.debug("register duplicate client")
c.debugLogf("register duplicate client")
case *dupClientSet:
s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter
@@ -522,7 +522,7 @@ func (s *Server) registerClient(c *sclient) {
set.set[c] = true
set.last = c
set.sendHistory = append(set.sendHistory, c)
c.debug("register another duplicate client")
c.debugLogf("register another duplicate client")
}
if _, ok := s.clientsMesh[c.key]; !ok {
@@ -555,7 +555,7 @@ func (s *Server) unregisterClient(c *sclient) {
case nil:
c.logf("[unexpected]; clients map is empty")
case singleClient:
c.logf("removed connection")
c.debugLogf("removed connection")
delete(s.clients, c.key)
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
delete(s.clientsMesh, c.key)
@@ -563,7 +563,7 @@ func (s *Server) unregisterClient(c *sclient) {
}
s.broadcastPeerStateChangeLocked(c.key, false)
case *dupClientSet:
c.debug("removed duplicate client")
c.debugLogf("removed duplicate client")
if set.removeClient(c) {
s.dupClientConns.Add(-1)
} else {
@@ -712,9 +712,12 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
if clientInfo != nil {
c.info = *clientInfo
if envknob.Bool("DERP_PROBER_DEBUG_LOGS") && clientInfo.IsProber {
c.debugLogging = true
c.debug = true
}
}
if s.debug {
c.debug = true
}
s.registerClient(c)
defer s.unregisterClient(c)
@@ -727,6 +730,12 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
return c.run(ctx)
}
func (s *Server) debugLogf(format string, v ...any) {
if s.debug {
s.logf(format, v...)
}
}
// for testing
var (
timeSleep = time.Sleep
@@ -744,16 +753,20 @@ func (c *sclient) run(ctx context.Context) error {
defer func() {
cancelSender()
if err := grp.Wait(); err != nil && !c.s.isClosed() {
c.logf("sender failed: %v", err)
if errors.Is(err, context.Canceled) {
c.debugLogf("sender canceled by reader exiting")
} else {
c.logf("sender failed: %v", err)
}
}
}()
for {
ft, fl, err := readFrameHeader(c.br)
c.debug("read frame type %d len %d err %v", ft, fl, err)
c.debugLogf("read frame type %d len %d err %v", ft, fl, err)
if err != nil {
if errors.Is(err, io.EOF) {
c.logf("read EOF")
c.debugLogf("read EOF")
return nil
}
if c.s.isClosed() {
@@ -910,7 +923,7 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
return nil
}
dst.debug("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
dst.debugLogf("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
return c.sendPkt(dst, pkt{
bs: contents,
@@ -960,7 +973,7 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
if fwd != nil {
s.packetsForwardedOut.Add(1)
err := fwd.ForwardPacket(c.key, dstKey, contents)
c.debug("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
c.debugLogf("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
if err != nil {
// TODO:
return nil
@@ -974,10 +987,10 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, c.key, dstKey, reason)
c.debug("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
c.debugLogf("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
return nil
}
c.debug("SendPacket for %s, sending directly", dstKey.ShortString())
c.debugLogf("SendPacket for %s, sending directly", dstKey.ShortString())
p := pkt{
bs: contents,
@@ -987,8 +1000,8 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
return c.sendPkt(dst, p)
}
func (c *sclient) debug(format string, v ...any) {
if c.debugLogging {
func (c *sclient) debugLogf(format string, v ...any) {
if c.debug {
c.logf(format, v...)
}
}
@@ -1011,7 +1024,8 @@ const (
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
s.packetsDropped.Add(1)
s.packetsDroppedReasonCounters[reason].Add(1)
if disco.LooksLikeDiscoWrapper(packetBytes) {
looksDisco := disco.LooksLikeDiscoWrapper(packetBytes)
if looksDisco {
s.packetsDroppedTypeDisco.Add(1)
} else {
s.packetsDroppedTypeOther.Add(1)
@@ -1024,9 +1038,7 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
if s.debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
s.debugLogf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, looksDisco)
}
func (c *sclient) sendPkt(dst *sclient, p pkt) error {
@@ -1044,13 +1056,13 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
select {
case <-dst.done:
s.recordDrop(p.bs, c.key, dstKey, dropReasonGoneDisconnected)
dst.debug("sendPkt attempt %d dropped, dst gone", attempt)
dst.debugLogf("sendPkt attempt %d dropped, dst gone", attempt)
return nil
default:
}
select {
case sendQueue <- p:
dst.debug("sendPkt attempt %d enqueued", attempt)
dst.debugLogf("sendPkt attempt %d enqueued", attempt)
return nil
default:
}
@@ -1066,7 +1078,7 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
// contended queue with racing writers. Give up and tail-drop in
// this case to keep reader unblocked.
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
dst.debug("sendPkt attempt %d dropped, queue full")
dst.debugLogf("sendPkt attempt %d dropped, queue full")
return nil
}
@@ -1304,8 +1316,7 @@ type sclient struct {
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
debugLogging bool
debug bool // turn on for verbose logging
// Owned by run, not thread-safe.
br *bufio.Reader
@@ -1593,7 +1604,7 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
c.s.packetsSent.Add(1)
c.s.bytesSent.Add(int64(len(contents)))
}
c.debug("sendPacket from %s: %v", srcKey.ShortString(), err)
c.debugLogf("sendPacket from %s: %v", srcKey.ShortString(), err)
}()
c.setWriteDeadline()

View File

@@ -31,6 +31,7 @@ import (
"time"
"golang.org/x/crypto/acme"
"golang.org/x/exp/slices"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
@@ -361,17 +362,16 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
}
key := "_acme-challenge." + domain
// Do a best-effort lookup to see if we've already created this DNS name
// in a previous attempt. Don't burn too much time on it, though. Worst
// case we ask the server to create something that already exists.
var resolver net.Resolver
var ok bool
txts, _ := resolver.LookupTXT(ctx, key)
for _, txt := range txts {
if txt == rec {
ok = true
logf("TXT record already existed")
break
}
}
if !ok {
lookupCtx, lookupCancel := context.WithTimeout(ctx, 500*time.Millisecond)
txts, _ := resolver.LookupTXT(lookupCtx, key)
lookupCancel()
if slices.Contains(txts, rec) {
logf("TXT record already existed")
} else {
logf("starting SetDNS call...")
err = b.SetDNS(ctx, key, rec)
if err != nil {

View File

@@ -60,6 +60,7 @@ import (
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tsd"
"tailscale.com/types/dnstype"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -137,10 +138,11 @@ type LocalBackend struct {
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
sys *tsd.System
e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys
pm *profileManager
store ipn.StateStore
dialer *tsdial.Dialer // non-nil
store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys
dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys
backendLogID logid.PublicID
unregisterNetMon func()
unregisterHealthWatch func()
@@ -267,10 +269,10 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
// but is not actually running.
//
// If dialer is nil, a new one is made.
func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: engine must not be nil")
}
func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
e := sys.Engine.Get()
store := sys.StateStore.Get()
dialer := sys.Dialer.Get()
pm, err := newProfileManager(store, logf)
if err != nil {
@@ -290,9 +292,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
osshare.SetFileSharingEnabled(false, logf)
ctx, cancel := context.WithCancel(context.Background())
portpoll, err := portlist.NewPoller()
portpoll := new(portlist.Poller)
err = portpoll.Check()
if err != nil {
logf("skipping portlist: %s", err)
portpoll.Close()
portpoll = nil
}
b := &LocalBackend{
@@ -301,10 +306,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
sys: sys,
e: e,
pm: pm,
store: store,
dialer: dialer,
store: store,
pm: pm,
backendLogID: logID,
state: ipn.NoState,
portpoll: portpoll,
@@ -313,7 +319,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
loginFlags: loginFlags,
}
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, e.GetNetMon())
netMon := sys.NetMon.Get()
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon)
if err != nil {
log.Printf("error setting up sockstat logger: %v", err)
}
@@ -330,7 +337,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
b.statusChanged = sync.NewCond(&b.statusLock)
b.e.SetStatusCallback(b.setWgengineStatus)
netMon := e.GetNetMon()
b.prevIfState = netMon.InterfaceState()
// Call our linkChange code once with the current state, and
// then also whenever it changes:
@@ -339,14 +345,9 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange)
wiredPeerAPIPort := false
if ig, ok := e.(wgengine.InternalsGetter); ok {
if tunWrap, _, _, ok := ig.GetInternals(); ok {
tunWrap.PeerAPIPort = b.GetPeerAPIPort
wiredPeerAPIPort = true
}
}
if !wiredPeerAPIPort {
if tunWrap, ok := b.sys.Tun.GetOK(); ok {
tunWrap.PeerAPIPort = b.GetPeerAPIPort
} else {
b.logf("[unexpected] failed to wire up PeerAPI port for engine %T", e)
}
@@ -464,6 +465,7 @@ func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
}
// Dialer returns the backend's dialer.
// It is always non-nil.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
}
@@ -644,7 +646,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
defer b.mu.Unlock()
sb.MutateStatus(func(s *ipnstate.Status) {
s.Version = version.Long()
s.TUN = !wgengine.IsNetstack(b.e)
s.TUN = !b.sys.IsNetstack()
s.BackendState = b.state.String()
s.AuthURL = b.authURLSticky
if err := health.OverallError(); err != nil {
@@ -1315,8 +1317,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
hostinfo := hostinfo.New()
hostinfo.BackendLogID = b.backendLogID.String()
hostinfo.FrontendLogID = opts.FrontendLogID
hostinfo.Userspace.Set(wgengine.IsNetstack(b.e))
hostinfo.UserspaceRouter.Set(wgengine.IsNetstackRouter(b.e))
hostinfo.Userspace.Set(b.sys.IsNetstack())
hostinfo.UserspaceRouter.Set(b.sys.IsNetstackRouter())
if b.cc != nil {
// TODO(apenwarr): avoid the need to reinit controlclient.
@@ -1401,7 +1403,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
var err error
isNetstack := wgengine.IsNetstackRouter(b.e)
isNetstack := b.sys.IsNetstackRouter()
debugFlags := controlDebugFlags
if isNetstack {
debugFlags = append([]string{"netstack"}, debugFlags...)
@@ -1423,7 +1425,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
HTTPTestClient: httpTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: debugFlags,
NetMon: b.e.GetNetMon(),
NetMon: b.sys.NetMon.Get(),
Pinger: b,
PopBrowserURL: b.tellClientToBrowseToURL,
OnClientVersion: b.onClientVersion,
@@ -3317,14 +3319,12 @@ func (b *LocalBackend) initPeerAPIListener() {
directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
}
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok {
ps.resolver = r
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
ps.resolver = dm.Resolver()
}
b.peerAPIServer = ps
isNetstack := wgengine.IsNetstack(b.e)
isNetstack := b.sys.IsNetstack()
for i, a := range b.netMap.Addresses {
var ln net.Listener
var err error
@@ -3630,6 +3630,19 @@ func (b *LocalBackend) hasNodeKey() bool {
return p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero()
}
// NodeKey returns the public node key.
func (b *LocalBackend) NodeKey() key.NodePublic {
b.mu.Lock()
defer b.mu.Unlock()
p := b.pm.CurrentPrefs()
if !p.Valid() || !p.Persist().Valid() || p.Persist().PrivateNodeKey().IsZero() {
return key.NodePublic{}
}
return p.Persist().PublicNodeKey()
}
// nextState returns the state the backend seems to be in, based on
// its internal state.
func (b *LocalBackend) nextState() ipn.State {
@@ -4040,7 +4053,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
b.setServeProxyHandlersLocked()
// don't listen on netmap addresses if we're in userspace mode
if !wgengine.IsNetstack(b.e) {
if !b.sys.IsNetstack() {
b.updateServeTCPPortNetMapAddrListenersLocked(servePorts)
}
}
@@ -4391,7 +4404,7 @@ func nodeIP(n *tailcfg.Node, pred func(netip.Addr) bool) netip.Addr {
}
func (b *LocalBackend) CheckIPForwarding() error {
if wgengine.IsNetstackRouter(b.e) {
if b.sys.IsNetstackRouter() {
return nil
}
@@ -4537,13 +4550,9 @@ func (b *LocalBackend) DebugReSTUN() error {
}
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
ig, ok := b.e.(wgengine.InternalsGetter)
mc, ok := b.sys.MagicSock.GetOK()
if !ok {
return nil, errors.New("engine isn't InternalsGetter")
}
_, mc, _, ok := ig.GetInternals()
if !ok {
return nil, errors.New("failed to get internals")
return nil, errors.New("failed to get magicsock from sys")
}
return mc, nil
}

View File

@@ -20,6 +20,7 @@ import (
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -501,13 +502,16 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
tstest.Replace(t, &panicOnMachineKeyGeneration, func() bool { return true })
var logf logger.Logf = logger.Discard
sys := new(tsd.System)
store := new(mem.Store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys.Set(store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(eng.Close)
lb, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, eng, 0)
sys.Set(eng)
lb, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
@@ -765,13 +769,16 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
func TestStatusWithoutPeers(t *testing.T) {
logf := tstest.WhileTestRunningLogger(t)
store := new(testStateStorage)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys := new(tsd.System)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
sys.Set(e)
t.Cleanup(e.Close)
b, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, e, 0)
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}

View File

@@ -12,6 +12,7 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -47,14 +48,17 @@ func TestLocalLogLines(t *testing.T) {
idA := logid(0xaa)
// set up a LocalBackend, super bare bones. No functional data.
sys := new(tsd.System)
store := new(mem.Store)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatal(err)
}
t.Cleanup(e.Close)
sys.Set(e)
lb, err := NewLocalBackend(logf, idA, store, nil, e, 0)
lb, err := NewLocalBackend(logf, idA, sys, 0)
if err != nil {
t.Fatal(err)
}

View File

@@ -50,7 +50,6 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
@@ -469,7 +468,7 @@ func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net
}
}
if wgengine.IsNetstack(s.b.e) {
if s.b.sys.IsNetstack() {
ipStr = ""
}
@@ -1239,12 +1238,9 @@ func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Req
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
eng := h.ps.b.e
if ig, ok := eng.(wgengine.InternalsGetter); ok {
if _, mc, _, ok := ig.GetInternals(); ok {
mc.ServeHTTPDebug(w, r)
return
}
if mc, ok := h.ps.b.sys.MagicSock.GetOK(); ok {
mc.ServeHTTPDebug(w, r)
return
}
http.Error(w, "miswired", 500)
}

View File

@@ -143,7 +143,7 @@ func (s *serveListener) Run() {
}
func (s *serveListener) shouldWarnAboutListenError(err error) bool {
if !s.b.e.GetNetMon().InterfaceState().HasIP(s.ap.Addr()) {
if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) {
// Machine likely doesn't have IPv6 enabled (or the IP is still being
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
return false

View File

@@ -17,6 +17,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -297,14 +298,17 @@ func TestStateMachine(t *testing.T) {
c := qt.New(t)
logf := tstest.WhileTestRunningLogger(t)
sys := new(tsd.System)
store := new(testStateStorage)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(e.Close)
sys.Set(e)
b, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, e, 0)
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
@@ -941,13 +945,16 @@ func TestStateMachine(t *testing.T) {
func TestEditPrefsHasNoKeys(t *testing.T) {
logf := tstest.WhileTestRunningLogger(t)
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys := new(tsd.System)
sys.Set(new(mem.Store))
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(e.Close)
sys.Set(e)
b, err := NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), nil, e, 0)
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
@@ -1023,10 +1030,14 @@ func TestWGEngineStatusRace(t *testing.T) {
t.Skip("test fails")
c := qt.New(t)
logf := tstest.WhileTestRunningLogger(t)
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys := new(tsd.System)
sys.Set(new(mem.Store))
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
c.Assert(err, qt.IsNil)
t.Cleanup(eng.Close)
b, err := NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), nil, eng, 0)
sys.Set(eng)
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
c.Assert(err, qt.IsNil)
var cc *mockControl

View File

@@ -37,7 +37,7 @@ import (
type Server struct {
lb atomic.Pointer[ipnlocal.LocalBackend]
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
netMon *netmon.Monitor // must be non-nil
backendLogID logid.PublicID
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 active HTTP requests. That is, this is whether the backend is
@@ -410,14 +410,15 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
}
// New returns a new Server.
// The netMon parameter is optional; if non-nil it's used to do faster interface
// lookups.
//
// To start it, use the Server.Run method.
//
// At some point, either before or after Run, the Server's SetLocalBackend
// method must also be called before Server can do anything useful.
func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server {
if netMon == nil {
panic("nil netMon")
}
return &Server{
backendLogID: logID,
logf: logf,

View File

@@ -9,22 +9,23 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [eliasnaur.com/font/roboto](https://pkg.go.dev/eliasnaur.com/font/roboto) ([BSD-3-Clause](https://git.sr.ht/~eliasnaur/font/tree/832bb8fc08c3/LICENSE))
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
- [gioui.org](https://pkg.go.dev/gioui.org) ([MIT](https://git.sr.ht/~eliasnaur/gio/tree/32c6a9b10d0b/LICENSE))
- [gioui.org/cpu](https://pkg.go.dev/gioui.org/cpu) ([MIT](https://git.sr.ht/~eliasnaur/gio-cpu/tree/8d6a761490d2/LICENSE))
- [gioui.org/shader](https://pkg.go.dev/gioui.org/shader) ([MIT](https://git.sr.ht/~eliasnaur/gio-shader/tree/v1.0.6/LICENSE))
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.11.0/config/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.6.4/credentials/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.8.2/feature/ec2/imds/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.27/internal/configsources/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.21/internal/endpoints/v2/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.2/internal/ini/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/internal/sync/singleflight/LICENSE))
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.5.2/service/internal/presigned-url/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.35.0/service/ssm/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.6.2/service/sso/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.11.1/service/sts/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
- [github.com/benoitkugler/textlayout](https://pkg.go.dev/github.com/benoitkugler/textlayout) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/LICENSE))
@@ -34,50 +35,51 @@ Client][]. 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/v0.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.4.0/LICENSE))
- [github.com/go-text/typesetting](https://pkg.go.dev/github.com/go-text/typesetting) ([BSD-3-Clause](https://github.com/go-text/typesetting/blob/0399769901d5/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.0.6/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/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.0.1/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/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))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/de60144f33f8/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.4/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.4/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.4/zstd/internal/xxhash/LICENSE.txt))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.5/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.5/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.2.0/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.1/LICENSE.md))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [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.17/LICENSE))
- [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/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/bc99ab8c2d17/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/17a3db2c30d2/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/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/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/c3537552635f/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/650dca95af54/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/50045581ed74/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/3e8cd9d6bf63/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/intern](https://pkg.go.dev/go4.org/intern) ([BSD-3-Clause](https://github.com/go4org/intern/blob/ae77deb06f29/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/927187094b94/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/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/f1b76eb4bb35/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/ee73d164e760/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.5.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.8.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.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.6.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.6.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.8.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/+/579cf78f:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.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.9.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.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.8.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.7.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.9.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.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))

View File

@@ -10,62 +10,63 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
## Go Packages
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.17.7/config/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.12.20/credentials/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.12.17/feature/ec2/imds/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.27/internal/configsources/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.21/internal/endpoints/v2/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.24/internal/ini/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/internal/sync/singleflight/LICENSE))
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.17/service/internal/presigned-url/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.35.0/service/ssm/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.11.23/service/sso/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.13.5/service/ssooidc/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.16.19/service/sts/LICENSE.txt))
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/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/v0.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.4.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.0.6/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/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))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/de60144f33f8/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.11/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.11/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.11/zstd/internal/xxhash/LICENSE.txt))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.5/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.5/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.2.0/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.1/LICENSE.md))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [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/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/bc99ab8c2d17/LICENSE))
- [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.17/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/17a3db2c30d2/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/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/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/c3537552635f/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/650dca95af54/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/50045581ed74/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/3e8cd9d6bf63/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/cafedaf6:LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.9.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.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.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.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.8.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.7.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.9.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/+/579cf78f:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/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.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))

View File

@@ -83,18 +83,18 @@ Some packages may only be included on certain architectures or operating systems
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.9.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.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.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.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.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.8.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.7.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.9.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.3.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/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
- [inet.af/wf](https://pkg.go.dev/inet.af/wf) ([BSD-3-Clause](https://github.com/inetaf/wf/blob/36129f591884/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.25.0/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.26.1/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([MIT](https://github.com/kubernetes-sigs/yaml/blob/v1.3.0/LICENSE))
- [software.sslmate.com/src/go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) ([BSD-3-Clause](https://github.com/SSLMate/go-pkcs12/blob/v0.2.0/LICENSE))

View File

@@ -9,42 +9,42 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
## Go Packages
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
- [github.com/Microsoft/go-winio](https://pkg.go.dev/github.com/Microsoft/go-winio) ([MIT](https://github.com/Microsoft/go-winio/blob/v0.6.0/LICENSE))
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
- [github.com/Microsoft/go-winio](https://pkg.go.dev/github.com/Microsoft/go-winio) ([MIT](https://github.com/Microsoft/go-winio/blob/v0.6.1/LICENSE))
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/2b26ab7fb5f9/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/LICENSE))
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/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))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.11/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.11/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.11/zstd/internal/xxhash/LICENSE.txt))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.1/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.0/LICENSE.md))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.5/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.5/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.5/zstd/internal/xxhash/LICENSE.txt))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- [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/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
- [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/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f374e3278cd0/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f63dace725d8/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/59dfb47dfef1/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.1.6/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.0/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/cafedaf6: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.5.0:LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1: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.7.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.10.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.9.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.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.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.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.8.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.7.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.9.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))

View File

@@ -13,19 +13,19 @@ import (
"fmt"
"io"
"log"
mrand "math/rand"
"net/http"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/envknob"
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/sockstats"
"tailscale.com/tstime"
tslogger "tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/set"
@@ -128,9 +128,6 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
cfg.FlushDelayFn = func() time.Duration { return 0 }
}
stdLogf := func(f string, a ...any) {
fmt.Fprintf(cfg.Stderr, strings.TrimSuffix(f, "\n")+"\n", a...)
}
var urlSuffix string
if !cfg.CopyPrivateID.IsZero() {
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
@@ -148,7 +145,6 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
sentinel: make(chan int32, 16),
flushDelayFn: cfg.FlushDelayFn,
timeNow: cfg.TimeNow,
bo: backoff.NewBackoff("logtail", stdLogf, 30*time.Second),
metricsDelta: cfg.MetricsDelta,
sockstatsLabel: sockstats.LabelLogtailLogger,
@@ -186,7 +182,6 @@ type Logger struct {
flushPending atomic.Bool
sentinel chan int32
timeNow func() time.Time
bo *backoff.Backoff
zstdEncoder Encoder
uploadCancel func()
explainedRaw bool
@@ -373,23 +368,38 @@ func (l *Logger) uploading(ctx context.Context) {
}
}
for len(body) > 0 {
select {
case <-ctx.Done():
return
default:
}
uploaded, err := l.upload(ctx, body, origlen)
var lastError string
var numFailures int
var firstFailure time.Time
for len(body) > 0 && ctx.Err() == nil {
retryAfter, err := l.upload(ctx, body, origlen)
if err != nil {
numFailures++
firstFailure = time.Now()
if !l.internetUp() {
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
l.awaitInternetUp(ctx)
continue
}
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
}
l.bo.BackOff(ctx, err)
if uploaded {
// Only print the same message once.
if currError := err.Error(); lastError != currError {
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
lastError = currError
}
// Sleep for the specified retryAfter period,
// otherwise default to some random value.
if retryAfter <= 0 {
retryAfter = time.Duration(30+mrand.Intn(30)) * time.Second
}
tstime.Sleep(ctx, retryAfter)
} else {
// Only print a success message after recovery.
if numFailures > 0 {
fmt.Fprintf(l.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, time.Since(firstFailure).Round(time.Second))
}
break
}
}
@@ -433,7 +443,7 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
// upload uploads body to the log server.
// origlen indicates the pre-compression body length.
// origlen of -1 indicates that the body is not compressed.
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAfter time.Duration, err error) {
const maxUploadTime = 45 * time.Second
ctx = sockstats.WithSockStats(ctx, l.sockstatsLabel, l.Logf)
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
@@ -460,17 +470,16 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
l.httpDoCalls.Add(1)
resp, err := l.httpc.Do(req)
if err != nil {
return false, fmt.Errorf("log upload of %d bytes %s failed: %v", len(body), compressedNote, err)
return 0, fmt.Errorf("log upload of %d bytes %s failed: %v", len(body), compressedNote, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
uploaded = resp.StatusCode == 400 // the server saved the logs anyway
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
return uploaded, fmt.Errorf("log upload of %d bytes %s failed %d: %q", len(body), compressedNote, resp.StatusCode, b)
if resp.StatusCode != http.StatusOK {
n, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return time.Duration(n) * time.Second, fmt.Errorf("log upload of %d bytes %s failed %d: %s", len(body), compressedNote, resp.StatusCode, bytes.TrimSpace(b))
}
return true, nil
return 0, nil
}
// Flush uploads all logs to the server. It blocks until complete or there is an

View File

@@ -11,7 +11,6 @@ import (
"net/http"
"net/netip"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
@@ -156,9 +155,6 @@ func TestHairpinWait(t *testing.T) {
}
func TestBasic(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
}
stunAddr, cleanup := stuntest.Serve(t)
defer cleanup()

View File

@@ -111,7 +111,7 @@ func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string)
// Verify that we didn't just choose the Tailscale interface;
// if so, we fall back to binding from the default.
_, tsif, err2 := interfaces.Tailscale()
if err2 == nil && tsif.Index == idx {
if err2 == nil && tsif != nil && tsif.Index == idx {
logf("[unexpected] netns: interfaceIndexFor returned Tailscale interface")
return defaultIdx()
}

View File

@@ -325,6 +325,10 @@ type radioMonitor struct {
// Usage is measured once per second, so this is the number of seconds of history to track.
const radioSampleSize = 3600 // 1 hour
// initStallPeriod is the minimum amount of time in seconds to collect data before reporting.
// Otherwise, all clients will report 100% radio usage on startup.
var initStallPeriod int64 = 120 // 2 minutes
var radio = &radioMonitor{
now: time.Now,
startTime: time.Now().Unix(),
@@ -375,7 +379,7 @@ func (rm *radioMonitor) radioHighPercent() int64 {
}
})
if periodLength == 0 {
if periodLength < initStallPeriod {
return 0
}
@@ -386,7 +390,7 @@ func (rm *radioMonitor) radioHighPercent() int64 {
}
// forEachSample calls f for each sample in the past hour (or less if less time
// has passed -- the evaluated period is returned)
// has passed -- the evaluated period is returned, measured in seconds)
func (rm *radioMonitor) forEachSample(f func(c int, isActive bool)) (periodLength int64) {
now := rm.now().Unix()
periodLength = radioSampleSize

View File

@@ -33,6 +33,14 @@ func TestRadioMonitor(t *testing.T) {
func(_ *testTime, _ *radioMonitor) {},
0,
},
{
"active less than init stall period",
func(tt *testTime, rm *radioMonitor) {
rm.active()
tt.Add(1 * time.Second)
},
0, // radio on, but not long enough to report data
},
{
"active, 10 sec idle",
func(tt *testTime, rm *radioMonitor) {
@@ -42,13 +50,13 @@ func TestRadioMonitor(t *testing.T) {
50, // radio on 5 seconds of 10 seconds
},
{
"active, spanning two seconds",
"active, spanning three seconds",
func(tt *testTime, rm *radioMonitor) {
rm.active()
tt.Add(1100 * time.Millisecond)
tt.Add(2100 * time.Millisecond)
rm.active()
},
100, // radio on for 2 seconds
100, // radio on for 3 seconds
},
{
"400 iterations: 2 sec active, 1 min idle",
@@ -66,13 +74,17 @@ func TestRadioMonitor(t *testing.T) {
{
"activity at end of time window",
func(tt *testTime, rm *radioMonitor) {
tt.Add(1 * time.Second)
tt.Add(3 * time.Second)
rm.active()
},
50,
25,
},
}
oldStallPeriod := initStallPeriod
initStallPeriod = 3
t.Cleanup(func() { initStallPeriod = oldStallPeriod })
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tm := &testTime{time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)}

View File

@@ -47,8 +47,11 @@ func (t *fakeTUN) Write(b [][]byte, n int) (int, error) {
return 1, nil
}
// FakeTUNName is the name of the fake TUN device.
const FakeTUNName = "FakeTUN"
func (t *fakeTUN) Flush() error { return nil }
func (t *fakeTUN) MTU() (int, error) { return 1500, nil }
func (t *fakeTUN) Name() (string, error) { return "FakeTUN", nil }
func (t *fakeTUN) Name() (string, error) { return FakeTUNName, nil }
func (t *fakeTUN) Events() <-chan tun.Event { return t.evchan }
func (t *fakeTUN) BatchSize() int { return 1 }

View File

@@ -67,7 +67,7 @@ type nothing struct{}
// Unfortunately, options to filter by proto or state are non-portable,
// so we'll filter for ourselves.
// Nowadays, though, we only use it for macOS as of 2022-11-04.
func appendParsePortsNetstat(base []Port, br *bufio.Reader) ([]Port, error) {
func appendParsePortsNetstat(base []Port, br *bufio.Reader, includeLocalhost bool) ([]Port, error) {
ret := base
var fieldBuf [10]mem.RO
for {
@@ -99,7 +99,7 @@ func appendParsePortsNetstat(base []Port, br *bufio.Reader) ([]Port, error) {
// not interested in non-listener sockets
continue
}
if isLoopbackAddr(laddr) {
if !includeLocalhost && isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}
@@ -110,7 +110,7 @@ func appendParsePortsNetstat(base []Port, br *bufio.Reader) ([]Port, error) {
proto = "udp"
laddr = cols[len(cols)-2]
raddr = cols[len(cols)-1]
if isLoopbackAddr(laddr) {
if !includeLocalhost && isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}

View File

@@ -8,6 +8,7 @@ package portlist
import (
"bufio"
"encoding/json"
"fmt"
"strings"
"testing"
@@ -52,30 +53,40 @@ udp46 0 0 *.146 *.*
`
func TestParsePortsNetstat(t *testing.T) {
want := List{
Port{"tcp", 23, ""},
Port{"tcp", 24, ""},
Port{"udp", 104, ""},
Port{"udp", 106, ""},
Port{"udp", 146, ""},
Port{"tcp", 8185, ""}, // but not 8186, 8187, 8188 on localhost
}
pl, err := appendParsePortsNetstat(nil, bufio.NewReader(strings.NewReader(netstatOutput)))
if err != nil {
t.Fatal(err)
}
pl = sortAndDedup(pl)
jgot, _ := json.MarshalIndent(pl, "", "\t")
jwant, _ := json.MarshalIndent(want, "", "\t")
if len(pl) != len(want) {
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
for i := range pl {
if pl[i] != want[i] {
t.Errorf("row#%d\n got: %+v\n\nwant: %+v\n",
i, pl[i], want[i])
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
for _, loopBack := range [...]bool{false, true} {
t.Run(fmt.Sprintf("loopback_%v", loopBack), func(t *testing.T) {
want := List{
{"tcp", 23, "", 0},
{"tcp", 24, "", 0},
{"udp", 104, "", 0},
{"udp", 106, "", 0},
{"udp", 146, "", 0},
{"tcp", 8185, "", 0}, // but not 8186, 8187, 8188 on localhost, when loopback is false
}
if loopBack {
want = append(want,
Port{"tcp", 8186, "", 0},
Port{"tcp", 8187, "", 0},
Port{"tcp", 8188, "", 0},
)
}
pl, err := appendParsePortsNetstat(nil, bufio.NewReader(strings.NewReader(netstatOutput)), loopBack)
if err != nil {
t.Fatal(err)
}
pl = sortAndDedup(pl)
jgot, _ := json.MarshalIndent(pl, "", "\t")
jwant, _ := json.MarshalIndent(want, "", "\t")
if len(pl) != len(want) {
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
for i := range pl {
if pl[i] != want[i] {
t.Errorf("row#%d\n got: %+v\n\nwant: %+v\n",
i, pl[i], want[i])
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
}
})
}
}

View File

@@ -24,7 +24,13 @@ var debugDisablePortlist = envknob.RegisterBool("TS_DEBUG_DISABLE_PORTLIST")
// Poller scans the systems for listening ports periodically and sends
// the results to C.
type Poller struct {
c chan List // unbuffered
// IncludeLocalhost controls whether services bound to localhost are included.
//
// This field should only be changed before calling Run.
IncludeLocalhost bool
initOnce sync.Once // guards init of private fields
c chan List // unbuffered
// os, if non-nil, is an OS-specific implementation of the portlist getting
// code. When non-nil, it's responsible for getting the complete list of
@@ -32,8 +38,7 @@ type Poller struct {
// addProcesses is not used.
// A nil values means we don't have code for getting the list on the current
// operating system.
os osImpl
osOnce sync.Once // guards init of os
os osImpl
// closeCtx is the context that's canceled on Close.
closeCtx context.Context
@@ -62,58 +67,48 @@ type osImpl interface {
}
// newOSImpl, if non-nil, constructs a new osImpl.
var newOSImpl func() osImpl
var newOSImpl func(includeLocalhost bool) osImpl
var errUnimplemented = errors.New("portlist poller not implemented on " + runtime.GOOS)
// NewPoller returns a new portlist Poller. It returns an error
// if the portlist couldn't be obtained.
func NewPoller() (*Poller, error) {
if debugDisablePortlist() {
return nil, errors.New("portlist disabled by envknob")
}
p := &Poller{
c: make(chan List),
runDone: make(chan struct{}),
}
p.closeCtx, p.closeCtxCancel = context.WithCancel(context.Background())
p.osOnce.Do(p.initOSField)
if p.os == nil {
return nil, errUnimplemented
}
// Do one initial poll synchronously so we can return an error
// early.
if pl, err := p.getList(); err != nil {
return nil, err
} else {
p.setPrev(pl)
}
return p, nil
}
func (p *Poller) setPrev(pl List) {
// Make a copy, as the pass in pl slice aliases pl.scratch and we don't want
// that to except to the caller.
p.prev = slices.Clone(pl)
}
func (p *Poller) initOSField() {
if newOSImpl != nil {
p.os = newOSImpl()
// init sets the os implementation if exists. It also sets
// all private fields. All exported methods must call this in a
// Once, otherwise they may panic.
func (p *Poller) init() {
if debugDisablePortlist() {
return
}
if newOSImpl != nil {
p.os = newOSImpl(p.IncludeLocalhost)
}
p.closeCtx, p.closeCtxCancel = context.WithCancel(context.Background())
p.c = make(chan List)
p.runDone = make(chan struct{})
}
// Updates return the channel that receives port list updates.
//
// The channel is closed when the Poller is closed.
func (p *Poller) Updates() <-chan List { return p.c }
func (p *Poller) Updates() <-chan List {
p.initOnce.Do(p.init)
return p.c
}
// Close closes the Poller.
// Run will return with a nil error.
func (p *Poller) Close() error {
p.initOnce.Do(p.init)
p.closeCtxCancel()
<-p.runDone
if p.os == nil {
return nil
}
<-p.runDone // if caller of Close never called Run, this can hang.
if p.os != nil {
p.os.Close()
}
@@ -142,7 +137,33 @@ func (p *Poller) Run(ctx context.Context) error {
return p.runWithTickChan(ctx, tick.C)
}
// Check makes sure that the Poller is enabled and
// the undelrying OS implementation is working properly.
//
// An error returned from Check is non-fatal and means
// that it's been administratively disabled or the underlying
// OS is not implemented.
func (p *Poller) Check() error {
p.initOnce.Do(p.init)
if p.os == nil {
return errUnimplemented
}
// Do one initial poll synchronously so we can return an error
// early.
if pl, err := p.getList(); err != nil {
return err
} else {
p.setPrev(pl)
}
return nil
}
func (p *Poller) runWithTickChan(ctx context.Context, tickChan <-chan time.Time) error {
p.initOnce.Do(p.init)
if p.os == nil {
return errUnimplemented
}
defer close(p.runDone)
defer close(p.c)
@@ -177,7 +198,7 @@ func (p *Poller) getList() (List, error) {
if debugDisablePortlist() {
return nil, nil
}
p.osOnce.Do(p.initOSField)
p.initOnce.Do(p.init)
var err error
p.scratch, err = p.os.AppendListeningPorts(p.scratch[:0])
return p.scratch, err

View File

@@ -18,6 +18,7 @@ type Port struct {
Proto string // "tcp" or "udp"
Port uint16 // port number
Process string // optional process name, if found
Pid int // process id, if known
}
// List is a list of Ports.
@@ -69,12 +70,11 @@ func sortAndDedup(ps List) List {
out := ps[:0]
var last Port
for _, p := range ps {
protoPort := Port{Proto: p.Proto, Port: p.Port}
if last == protoPort {
if last.Proto == p.Proto && last.Port == p.Port {
continue
}
out = append(out, p)
last = protoPort
last = p
}
return out
}

View File

@@ -35,25 +35,28 @@ type linuxImpl struct {
procNetFiles []*os.File // seeked to start & reused between calls
readlinkPathBuf []byte
known map[string]*portMeta // inode string => metadata
br *bufio.Reader
known map[string]*portMeta // inode string => metadata
br *bufio.Reader
includeLocalhost bool
}
type portMeta struct {
port Port
pid int
keep bool
needsProcName bool
}
func newLinuxImplBase() *linuxImpl {
func newLinuxImplBase(includeLocalhost bool) *linuxImpl {
return &linuxImpl{
br: bufio.NewReader(eofReader),
known: map[string]*portMeta{},
br: bufio.NewReader(eofReader),
known: map[string]*portMeta{},
includeLocalhost: includeLocalhost,
}
}
func newLinuxImpl() osImpl {
li := newLinuxImplBase()
func newLinuxImpl(includeLocalhost bool) osImpl {
li := newLinuxImplBase(includeLocalhost)
for _, name := range []string{
"/proc/net/tcp",
"/proc/net/tcp6",
@@ -220,7 +223,7 @@ func (li *linuxImpl) parseProcNetFile(r *bufio.Reader, fileBase string) error {
// If a port is bound to localhost, ignore it.
// TODO: localhost is bigger than 1 IP, we need to ignore
// more things.
if mem.HasPrefix(local, mem.S(v4Localhost)) || mem.HasPrefix(local, mem.S(v6Localhost)) {
if !li.includeLocalhost && (mem.HasPrefix(local, mem.S(v4Localhost)) || mem.HasPrefix(local, mem.S(v6Localhost))) {
continue
}
@@ -315,6 +318,9 @@ func (li *linuxImpl) findProcessNames(need map[string]*portMeta) error {
}
argv := strings.Split(strings.TrimSuffix(string(bs), "\x00"), "\x00")
if p, err := mem.ParseInt(pid, 10, 0); err == nil {
pe.pid = int(p)
}
pe.port.Process = argvSubject(argv...)
pe.needsProcName = false
delete(need, string(targetBuf[:n]))

View File

@@ -89,7 +89,7 @@ func TestParsePorts(t *testing.T) {
if tt.file != "" {
file = tt.file
}
li := newLinuxImplBase()
li := newLinuxImplBase(false)
err := li.parseProcNetFile(r, file)
if err != nil {
t.Fatal(err)
@@ -118,7 +118,7 @@ func BenchmarkParsePorts(b *testing.B) {
contents.WriteString(" 3: 69050120005716BC64906EBE009ECD4D:D506 0047062600000000000000006E171268:01BB 01 00000000:00000000 02:0000009E 00000000 1000 0 151042856 2 0000000000000000 21 4 28 10 -1\n")
}
li := newLinuxImplBase()
li := newLinuxImplBase(false)
r := bytes.NewReader(contents.Bytes())
br := bufio.NewReader(&contents)

View File

@@ -29,8 +29,9 @@ type macOSImpl struct {
known map[protoPort]*portMeta // inode string => metadata
netstatPath string // lazily populated
br *bufio.Reader // reused
portsBuf []Port
br *bufio.Reader // reused
portsBuf []Port
includeLocalhost bool
}
type protoPort struct {
@@ -43,10 +44,11 @@ type portMeta struct {
keep bool
}
func newMacOSImpl() osImpl {
func newMacOSImpl(includeLocalhost bool) osImpl {
return &macOSImpl{
known: map[protoPort]*portMeta{},
br: bufio.NewReader(bytes.NewReader(nil)),
known: map[protoPort]*portMeta{},
br: bufio.NewReader(bytes.NewReader(nil)),
includeLocalhost: includeLocalhost,
}
}
@@ -119,7 +121,7 @@ func (im *macOSImpl) appendListeningPortsNetstat(base []Port) ([]Port, error) {
defer cmd.Process.Wait()
defer cmd.Process.Kill()
return appendParsePortsNetstat(base, im.br)
return appendParsePortsNetstat(base, im.br, im.includeLocalhost)
}
var lsofFailed atomic.Bool
@@ -170,6 +172,7 @@ func (im *macOSImpl) addProcesses() error {
im.br.Reset(outPipe)
var cmd, proto string
var pid int
for {
line, err := im.br.ReadBytes('\n')
if err != nil {
@@ -184,6 +187,10 @@ func (im *macOSImpl) addProcesses() error {
// starting a new process
cmd = ""
proto = ""
pid = 0
if p, err := mem.ParseInt(mem.B(val), 10, 0); err == nil {
pid = int(p)
}
case 'c':
cmd = string(val) // TODO(bradfitz): avoid garbage; cache process names between runs?
case 'P':
@@ -202,6 +209,7 @@ func (im *macOSImpl) addProcesses() error {
switch {
case m != nil:
m.port.Process = cmd
m.port.Pid = pid
default:
// ignore: processes and ports come and go
}

View File

@@ -5,9 +5,7 @@ package portlist
import (
"context"
"flag"
"net"
"runtime"
"sync"
"testing"
"time"
@@ -51,16 +49,9 @@ func TestIgnoreLocallyBoundPorts(t *testing.T) {
}
}
var flagRunUnspecTests = flag.Bool("run-unspec-tests",
runtime.GOOS == "linux", // other OSes have annoying firewall GUI confirmation dialogs
"run tests that require listening on the the unspecified address")
func TestChangesOverTime(t *testing.T) {
if !*flagRunUnspecTests {
t.Skip("skipping test without --run-unspec-tests")
}
var p Poller
p.IncludeLocalhost = true
get := func(t *testing.T) []Port {
t.Helper()
s, err := p.getList()
@@ -71,7 +62,7 @@ func TestChangesOverTime(t *testing.T) {
}
p1 := get(t)
ln, err := net.Listen("tcp", ":0")
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Skipf("failed to bind: %v", err)
}
@@ -185,7 +176,8 @@ func TestEqualLessThan(t *testing.T) {
}
func TestPoller(t *testing.T) {
p, err := NewPoller()
var p Poller
err := p.Check()
if err != nil {
t.Skipf("not running test: %v", err)
}

View File

@@ -25,7 +25,8 @@ type famPort struct {
}
type windowsImpl struct {
known map[famPort]*portMeta // inode string => metadata
known map[famPort]*portMeta // inode string => metadata
includeLocalhost bool
}
type portMeta struct {
@@ -33,9 +34,10 @@ type portMeta struct {
keep bool
}
func newWindowsImpl() osImpl {
func newWindowsImpl(includeLocalhost bool) osImpl {
return &windowsImpl{
known: map[famPort]*portMeta{},
known: map[famPort]*portMeta{},
includeLocalhost: includeLocalhost,
}
}
@@ -58,7 +60,7 @@ func (im *windowsImpl) AppendListeningPorts(base []Port) ([]Port, error) {
if e.State != "LISTEN" {
continue
}
if !e.Local.Addr().IsUnspecified() {
if !im.includeLocalhost && !e.Local.Addr().IsUnspecified() {
continue
}
fp := famPort{
@@ -83,6 +85,7 @@ func (im *windowsImpl) AppendListeningPorts(base []Port) ([]Port, error) {
Proto: "tcp",
Port: e.Local.Port(),
Process: process,
Pid: e.Pid,
},
}
im.known[fp] = pm

View File

@@ -354,6 +354,10 @@ func debArch(arch string) string {
// can ship more than 1 ARM deb, so for now match redo's behavior of
// shipping armv5 binaries in an armv7 trenchcoat.
return "armhf"
case "mipsle":
return "mipsel"
case "mips64le":
return "mips64el"
default:
return arch
}
@@ -372,6 +376,10 @@ func rpmArch(arch string) string {
return "armv7hl"
case "arm64":
return "aarch64"
case "mipsle":
return "mipsel"
case "mips64le":
return "mips64el"
default:
return arch
}

View File

@@ -82,31 +82,31 @@ var (
}
debs = map[string]bool{
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
// TODO: maybe mipses, we accidentally started building them at some
// point even though they probably don't work right.
// "linux/mips": true,
// "linux/mipsle": true,
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
"linux/mipsle": true,
"linux/mips64le": true,
"linux/mips": true,
// Debian does not support big endian mips64. Leave that out until we know
// we need it.
// "linux/mips64": true,
// "linux/mips64le": true,
}
rpms = map[string]bool{
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
// TODO: maybe mipses, we accidentally started building them at some
// point even though they probably don't work right.
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
"linux/mipsle": true,
"linux/mips64le": true,
// Fedora only supports little endian mipses. Maybe some other distribution
// supports big-endian? Leave them out for now.
// "linux/mips": true,
// "linux/mipsle": true,
// "linux/mips64": true,
// "linux/mips64le": true,
}
)

View File

@@ -20,7 +20,6 @@ import (
"log/syslog"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"sort"
@@ -31,16 +30,12 @@ import (
"github.com/creack/pty"
"github.com/pkg/sftp"
"github.com/u-root/u-root/pkg/termios"
"go4.org/mem"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
@@ -83,7 +78,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
case "sftp":
isSFTP = true
case "":
name = loginShell(ss.conn.localUser)
name = ss.conn.localUser.LoginShell()
if rawCmd := ss.RawCommand(); rawCmd != "" {
args = append(args, "-c", rawCmd)
} else {
@@ -457,7 +452,7 @@ func (ss *sshSession) launchProcess() error {
return ss.startWithStdPipes()
}
ss.ptyReq = &ptyReq
pty, err := ss.startWithPTY()
pty, tty, err := ss.startWithPTY()
if err != nil {
return err
}
@@ -466,10 +461,13 @@ func (ss *sshSession) launchProcess() error {
// dup.
ptyDup, err := syscall.Dup(int(pty.Fd()))
if err != nil {
pty.Close()
tty.Close()
return err
}
go resizeWindow(ptyDup /* arbitrary fd */, winCh)
ss.tty = tty
ss.stdin = pty
ss.stdout = os.NewFile(uintptr(ptyDup), pty.Name())
ss.stderr = nil // not available for pty
@@ -549,17 +547,16 @@ var opcodeShortName = map[uint8]string{
}
// startWithPTY starts cmd with a pseudo-terminal attached to Stdin, Stdout and Stderr.
func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
func (ss *sshSession) startWithPTY() (ptyFile, tty *os.File, err error) {
ptyReq := ss.ptyReq
cmd := ss.cmd
if cmd == nil {
return nil, errors.New("nil ss.cmd")
return nil, nil, errors.New("nil ss.cmd")
}
if ptyReq == nil {
return nil, errors.New("nil ss.ptyReq")
return nil, nil, errors.New("nil ss.ptyReq")
}
var tty *os.File
ptyFile, tty, err = pty.Open()
if err != nil {
err = fmt.Errorf("pty.Open: %w", err)
@@ -573,7 +570,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
}()
ptyRawConn, err := tty.SyscallConn()
if err != nil {
return nil, fmt.Errorf("SyscallConn: %w", err)
return nil, nil, fmt.Errorf("SyscallConn: %w", err)
}
var ctlErr error
if err := ptyRawConn.Control(func(fd uintptr) {
@@ -620,10 +617,10 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
return
}
}); err != nil {
return nil, fmt.Errorf("ptyRawConn.Control: %w", err)
return nil, nil, fmt.Errorf("ptyRawConn.Control: %w", err)
}
if ctlErr != nil {
return nil, fmt.Errorf("ptyRawConn.Control func: %w", ctlErr)
return nil, nil, fmt.Errorf("ptyRawConn.Control func: %w", ctlErr)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Setctty: true,
@@ -647,7 +644,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
if err = cmd.Start(); err != nil {
return
}
return ptyFile, nil
return ptyFile, tty, nil
}
// startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
@@ -688,117 +685,15 @@ func (ss *sshSession) startWithStdPipes() (err error) {
return nil
}
func loginShell(u *user.User) string {
switch runtime.GOOS {
case "linux":
if distro.Get() == distro.Gokrazy {
return "/tmp/serial-busybox/ash"
}
out, _ := exec.Command("getent", "passwd", u.Uid).Output()
// out is "root:x:0:0:root:/root:/bin/bash"
f := strings.SplitN(string(out), ":", 10)
if len(f) > 6 {
return strings.TrimSpace(f[6]) // shell
}
case "darwin":
// Note: /Users/username is key, and not the same as u.HomeDir.
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", u.Username), "UserShell").Output()
// out is "UserShell: /bin/bash"
s, ok := strings.CutPrefix(string(out), "UserShell: ")
if ok {
return strings.TrimSpace(s)
}
}
if e := os.Getenv("SHELL"); e != "" {
return e
}
return "/bin/sh"
}
func envForUser(u *user.User) []string {
func envForUser(u *userMeta) []string {
return []string{
fmt.Sprintf("SHELL=" + loginShell(u)),
fmt.Sprintf("SHELL=" + u.LoginShell()),
fmt.Sprintf("USER=" + u.Username),
fmt.Sprintf("HOME=" + u.HomeDir),
fmt.Sprintf("PATH=" + defaultPathForUser(u)),
fmt.Sprintf("PATH=" + defaultPathForUser(&u.User)),
}
}
// defaultPathTmpl specifies the default PATH template to use for new sessions.
//
// If empty, a default value is used based on the OS & distro to match OpenSSH's
// usually-hardcoded behavior. (see
// https://github.com/tailscale/tailscale/issues/5285 for background).
//
// The template may contain @{HOME} or @{PAM_USER} which expand to the user's
// home directory and username, respectively. (PAM is not used, despite the
// name)
var defaultPathTmpl = envknob.RegisterString("TAILSCALE_SSH_DEFAULT_PATH")
func defaultPathForUser(u *user.User) string {
if s := defaultPathTmpl(); s != "" {
return expandDefaultPathTmpl(s, u)
}
isRoot := u.Uid == "0"
switch distro.Get() {
case distro.Debian:
hi := hostinfo.New()
if hi.Distro == "ubuntu" {
// distro.Get's Debian includes Ubuntu. But see if it's actually Ubuntu.
// Ubuntu doesn't empirically seem to distinguish between root and non-root for the default.
// And it includes /snap/bin.
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
}
if isRoot {
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
return "/usr/local/bin:/usr/bin:/bin:/usr/bn/games"
case distro.NixOS:
return defaultPathForUserOnNixOS(u)
}
if isRoot {
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
return "/usr/local/bin:/usr/bin:/bin"
}
func defaultPathForUserOnNixOS(u *user.User) string {
var path string
lineread.File("/etc/pam/environment", func(lineb []byte) error {
if v := pathFromPAMEnvLine(lineb, u); v != "" {
path = v
return io.EOF // stop iteration
}
return nil
})
return path
}
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {
if !mem.HasPrefix(mem.B(line), mem.S("PATH")) {
return ""
}
rest := strings.TrimSpace(strings.TrimPrefix(string(line), "PATH"))
if quoted, ok := strings.CutPrefix(rest, "DEFAULT="); ok {
if path, err := strconv.Unquote(quoted); err == nil {
return expandDefaultPathTmpl(path, u)
}
}
return ""
}
func expandDefaultPathTmpl(t string, u *user.User) string {
p := strings.NewReplacer(
"@{HOME}", u.HomeDir,
"@{PAM_USER}", u.Username,
).Replace(t)
if strings.Contains(p, "@{") {
// If there are unknown expansions, conservatively fail closed.
return ""
}
return p
}
// updateStringInSlice mutates ss to change the first occurrence of a
// to b.
func updateStringInSlice(ss []string, a, b string) {

View File

@@ -22,13 +22,13 @@ import (
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
@@ -39,12 +39,12 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/version/distro"
)
var (
@@ -68,6 +68,7 @@ type ipnLocalBackend interface {
DoNoiseRequest(req *http.Request) (*http.Response, error)
Dialer() *tsdial.Dialer
TailscaleVarRoot() string
NodeKey() key.NodePublic
}
type server struct {
@@ -105,6 +106,7 @@ func init() {
logf: logf,
tailscaledPath: tsd,
}
return srv, nil
})
}
@@ -219,7 +221,7 @@ type conn struct {
finalActionErr error // set by doPolicyAuth or resolveNextAction
info *sshConnInfo // set by setInfo
localUser *user.User // set by doPolicyAuth
localUser *userMeta // set by doPolicyAuth
userGroupIDs []string // set by doPolicyAuth
pubKey gossh.PublicKey // set by doPolicyAuth
@@ -376,16 +378,7 @@ func (c *conn) doPolicyAuth(ctx ssh.Context, pubKey ssh.PublicKey) error {
if a.Accept {
c.finalAction = a
}
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy {
// Gokrazy is a single-user appliance with ~no userspace.
// There aren't users to look up (no /etc/passwd, etc)
// so rather than fail below, just hardcode root.
// TODO(bradfitz): fix os/user upstream instead?
c.userGroupIDs = []string{"0"}
c.localUser = &user.User{Uid: "0", Gid: "0", Username: "root"}
return nil
}
lu, err := user.Lookup(localUser)
lu, err := userLookup(localUser)
if err != nil {
c.logf("failed to look up %v: %v", localUser, err)
ctx.SendAuthBanner(fmt.Sprintf("failed to look up %v\r\n", localUser))
@@ -819,6 +812,7 @@ type sshSession struct {
stdout io.ReadCloser
stderr io.Reader // nil for pty sessions
ptyReq *ssh.Pty // non-nil for pty sessions
tty *os.File // non-nil for pty sessions, must be closed after process exits
// We use this sync.Once to ensure that we only terminate the process once,
// either it exits itself or is terminated
@@ -967,7 +961,7 @@ var errSessionDone = errors.New("session is done")
// handleSSHAgentForwarding starts a Unix socket listener and in the background
// forwards agent connections between the listener and the ssh.Session.
// On success, it assigns ss.agentListener.
func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *user.User) error {
func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) error {
if !ssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
return nil
}
@@ -1095,6 +1089,7 @@ func (ss *sshSession) run() {
}
go ss.killProcessOnContextDone()
var processDone atomic.Bool
go func() {
defer ss.stdin.Close()
if _, err := io.Copy(rec.writer("i", ss.stdin), ss); err != nil {
@@ -1112,8 +1107,11 @@ func (ss *sshSession) run() {
defer ss.stdout.Close()
_, err := io.Copy(rec.writer("o", ss), ss.stdout)
if err != nil && !errors.Is(err, io.EOF) {
logf("stdout copy: %v", err)
ss.cancelCtx(err)
isErrBecauseProcessExited := processDone.Load() && errors.Is(err, syscall.EIO)
if !isErrBecauseProcessExited {
logf("stdout copy: %v, %T", err)
ss.cancelCtx(err)
}
}
if openOutputStreams.Add(-1) == 0 {
ss.CloseWrite()
@@ -1132,7 +1130,12 @@ func (ss *sshSession) run() {
}()
}
if ss.tty != nil {
// If running a tty session, close the tty when the session is done.
defer ss.tty.Close()
}
err = ss.cmd.Wait()
processDone.Store(true)
// This will either make the SSH Termination goroutine be a no-op,
// or itself will be a no-op because the process was killed by the
// aforementioned goroutine.
@@ -1407,6 +1410,11 @@ type CastHeader struct {
// LocalUser is the effective username on the server.
LocalUser string `json:"localUser"`
// ConnectionID uniquely identifies a connection made to the SSH server.
// It may be shared across multiple sessions over the same connection in
// case of SSH multiplexing.
ConnectionID string `json:"connectionID"`
}
// sessionRecordingClient returns an http.Client that uses srv.lb.Dialer() to
@@ -1445,11 +1453,16 @@ func (ss *sshSession) sessionRecordingClient(dialCtx context.Context) (*http.Cli
// On success, it returns a WriteCloser that can be used to upload the
// recording, and a channel that will be sent an error (or nil) when the upload
// fails or completes.
func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPort) (io.WriteCloser, <-chan error, error) {
//
// In both cases, a slice of SSHRecordingAttempts is returned which detail the
// attempted recorder IP and the error message, if the attempt failed. The
// attempts are in order the recorder(s) was attempted. If successful a
// successful connection is made, the last attempt in the slice is the
// attempt for connected recorder.
func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPort) (io.WriteCloser, []*tailcfg.SSHRecordingAttempt, <-chan error, error) {
if len(recs) == 0 {
return nil, nil, errors.New("no recorders configured")
return nil, nil, nil, errors.New("no recorders configured")
}
// We use a special context for dialing the recorder, so that we can
// limit the time we spend dialing to 30 seconds and still have an
// unbounded context for the upload.
@@ -1457,10 +1470,17 @@ func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPo
defer dialCancel()
hc, err := ss.sessionRecordingClient(dialCtx)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
var errs []error
var attempts []*tailcfg.SSHRecordingAttempt
for _, ap := range recs {
attempt := &tailcfg.SSHRecordingAttempt{
Recorder: ap,
}
attempts = append(attempts, attempt)
// We dial the recorder and wait for it to send a 100-continue
// response before returning from this function. This ensures that
// the recorder is ready to accept the recording.
@@ -1476,7 +1496,9 @@ func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPo
pr, pw := io.Pipe()
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s:%d/record", ap.Addr(), ap.Port()), pr)
if err != nil {
errs = append(errs, fmt.Errorf("recording: error starting recording: %w", err))
err = fmt.Errorf("recording: error starting recording: %w", err)
attempt.FailureMessage = err.Error()
errs = append(errs, err)
continue
}
// We set the Expect header to 100-continue, so that the recorder
@@ -1508,12 +1530,13 @@ func (ss *sshSession) connectToRecorder(ctx context.Context, recs []netip.AddrPo
// is unexpected as we haven't sent any data yet.
err = errors.New("recording: unexpected EOF")
}
attempt.FailureMessage = err.Error()
errs = append(errs, err)
continue
}
return pw, errChan, nil
return pw, attempts, errChan, nil
}
return nil, nil, multierr.New(errs...)
return nil, attempts, nil, multierr.New(errs...)
}
func (ss *sshSession) openFileForRecording(now time.Time) (_ io.WriteCloser, err error) {
@@ -1535,6 +1558,13 @@ func (ss *sshSession) openFileForRecording(now time.Time) (_ io.WriteCloser, err
// startNewRecording starts a new SSH session recording.
// It may return a nil recording if recording is not available.
func (ss *sshSession) startNewRecording() (_ *recording, err error) {
// We store the node key as soon as possible when creating
// a new recording incase of FUS.
nodeKey := ss.conn.srv.lb.NodeKey()
if nodeKey.IsZero() {
return nil, errors.New("ssh server is unavailable: no node key")
}
recorders, onFailure := ss.recorders()
var localRecording bool
if len(recorders) == 0 {
@@ -1573,9 +1603,17 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
}
} else {
var errChan <-chan error
rec.out, errChan, err = ss.connectToRecorder(ctx, recorders)
var attempts []*tailcfg.SSHRecordingAttempt
rec.out, attempts, errChan, err = ss.connectToRecorder(ctx, recorders)
if err != nil {
// TODO(catzkorn): notify control here.
if onFailure != nil && onFailure.NotifyURL != "" && len(attempts) > 0 {
eventType := tailcfg.SSHSessionRecordingFailed
if onFailure.RejectSessionWithMessage != "" {
eventType = tailcfg.SSHSessionRecordingRejected
}
ss.notifyControl(ctx, nodeKey, eventType, attempts, onFailure.NotifyURL)
}
if onFailure != nil && onFailure.RejectSessionWithMessage != "" {
ss.logf("recording: error starting recording (rejecting session): %v", err)
return nil, userVisibleError{
@@ -1592,7 +1630,17 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
// Success.
return
}
// TODO(catzkorn): notify control here.
if onFailure != nil && onFailure.NotifyURL != "" && len(attempts) > 0 {
lastAttempt := attempts[len(attempts)-1]
lastAttempt.FailureMessage = err.Error()
eventType := tailcfg.SSHSessionRecordingFailed
if onFailure.TerminateSessionWithMessage != "" {
eventType = tailcfg.SSHSessionRecordingTerminated
}
ss.notifyControl(ctx, nodeKey, eventType, attempts, onFailure.NotifyURL)
}
if onFailure != nil && onFailure.TerminateSessionWithMessage != "" {
ss.logf("recording: error uploading recording (closing session): %v", err)
ss.cancelCtx(userVisibleError{
@@ -1622,10 +1670,11 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
// it. Then we can (1) make the cmd, (2) start the
// recording, (3) start the process.
},
SSHUser: ss.conn.info.sshUser,
LocalUser: ss.conn.localUser.Username,
SrcNode: strings.TrimSuffix(ss.conn.info.node.Name, "."),
SrcNodeID: ss.conn.info.node.StableID,
SSHUser: ss.conn.info.sshUser,
LocalUser: ss.conn.localUser.Username,
SrcNode: strings.TrimSuffix(ss.conn.info.node.Name, "."),
SrcNodeID: ss.conn.info.node.StableID,
ConnectionID: ss.conn.connID,
}
if !ss.conn.info.node.IsTagged() {
ch.SrcNodeUser = ss.conn.info.uprof.LoginName
@@ -1650,6 +1699,45 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
return rec, nil
}
// notifyControl sends a SSHEventNotifyRequest to control over noise.
// A SSHEventNotifyRequest is sent when an action or state reached during
// an SSH session is a defined EventType.
func (ss *sshSession) notifyControl(ctx context.Context, nodeKey key.NodePublic, notifyType tailcfg.SSHEventType, attempts []*tailcfg.SSHRecordingAttempt, url string) {
re := tailcfg.SSHEventNotifyRequest{
EventType: notifyType,
ConnectionID: ss.conn.connID,
CapVersion: tailcfg.CurrentCapabilityVersion,
NodeKey: nodeKey,
SrcNode: ss.conn.info.node.ID,
SSHUser: ss.conn.info.sshUser,
LocalUser: ss.conn.localUser.Username,
RecordingAttempts: attempts,
}
body, err := json.Marshal(re)
if err != nil {
ss.logf("notifyControl: unable to marshal SSHNotifyRequest:", err)
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
ss.logf("notifyControl: unable to create request:", err)
return
}
resp, err := ss.conn.srv.lb.DoNoiseRequest(req)
if err != nil {
ss.logf("notifyControl: unable to send noise request:", err)
return
}
if resp.StatusCode != http.StatusCreated {
ss.logf("notifyControl: noise request returned status code %v", resp.StatusCode)
return
}
}
// recording is the state for an SSH session recording.
type recording struct {
ss *sshSession

View File

@@ -38,7 +38,9 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
@@ -312,6 +314,10 @@ func (ts *localState) TailscaleVarRoot() string {
return ""
}
func (ts *localState) NodeKey() key.NodePublic {
return key.NewNode().Public()
}
func newSSHRule(action *tailcfg.SSHAction) *tailcfg.SSHRule {
return &tailcfg.SSHRule{
SSHUsers: map[string]string{
@@ -815,14 +821,14 @@ func TestSSHAuthFlow(t *testing.T) {
func TestSSH(t *testing.T) {
var logf logger.Logf = t.Logf
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
sys := &tsd.System{}
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatal(err)
}
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{},
new(mem.Store),
new(tsdial.Dialer),
eng, 0)
sys.Set(eng)
sys.Set(new(mem.Store))
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatal(err)
}
@@ -845,7 +851,11 @@ func TestSSH(t *testing.T) {
if err != nil {
t.Fatal(err)
}
sc.localUser = u
um, err := userLookup(u.Username)
if err != nil {
t.Fatal(err)
}
sc.localUser = um
sc.info = &sshConnInfo{
sshUser: "test",
src: netip.MustParseAddrPort("1.2.3.4:32342"),
@@ -1129,3 +1139,10 @@ func TestPathFromPAMEnvLineOnNixOS(t *testing.T) {
}
t.Logf("success; got=%q", got)
}
func TestStdOsUserUserAssumptions(t *testing.T) {
v := reflect.TypeOf(user.User{})
if got, want := v.NumField(), 5; got != want {
t.Errorf("os/user.User has %v fields; this package assumes %v", got, want)
}
}

230
ssh/tailssh/user.go Normal file
View File

@@ -0,0 +1,230 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || (darwin && !ios) || freebsd || openbsd
package tailssh
import (
"context"
"errors"
"io"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
)
// userMeta is a wrapper around *user.User with extra fields.
type userMeta struct {
user.User
// loginShellCached is the user's login shell, if known
// at the time of userLookup.
loginShellCached string
}
// GroupIds returns the list of group IDs that the user is a member of.
func (u *userMeta) GroupIds() ([]string, error) {
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy {
// Gokrazy is a single-user appliance with ~no userspace.
// There aren't users to look up (no /etc/passwd, etc)
// so rather than fail below, just hardcode root.
// TODO(bradfitz): fix os/user upstream instead?
return []string{"0"}, nil
}
return u.User.GroupIds()
}
// userLookup is like os/user.Lookup but it returns a *userMeta wrapper
// around a *user.User with extra fields.
func userLookup(username string) (*userMeta, error) {
if runtime.GOOS != "linux" {
return userLookupStd(username)
}
// No getent on Gokrazy. So hard-code the login shell.
if distro.Get() == distro.Gokrazy {
um, err := userLookupStd(username)
if err != nil {
um.User = user.User{
Uid: "0",
Gid: "0",
Username: "root",
Name: "Gokrazy",
HomeDir: "/",
}
}
um.loginShellCached = "/tmp/serial-busybox/ash"
return um, err
}
// On Linux, default to using "getent" to look up users so that
// even with static tailscaled binaries without cgo (as we distribute),
// we can still look up PAM/NSS users which the standard library's
// os/user without cgo won't get (because of no libc hooks).
// But if "getent" fails, userLookupGetent falls back to the standard
// library anyway.
return userLookupGetent(username)
}
func validUsername(uid string) bool {
if len(uid) > 32 || len(uid) == 0 {
return false
}
for _, r := range uid {
if r < ' ' || r == 0x7f || r == utf8.RuneError { // TODO(bradfitz): more?
return false
}
}
return true
}
func userLookupGetent(username string) (*userMeta, error) {
// Do some basic validation before passing this string to "getent", even though
// getent should do its own validation.
if !validUsername(username) {
return nil, errors.New("invalid username")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "getent", "passwd", username).Output()
if err != nil {
log.Printf("error calling getent for user %q: %v", username, err)
return userLookupStd(username)
}
// output is "alice:x:1001:1001:Alice Smith,,,:/home/alice:/bin/bash"
f := strings.SplitN(strings.TrimSpace(string(out)), ":", 10)
for len(f) < 7 {
f = append(f, "")
}
um := &userMeta{
User: user.User{
Username: f[0],
Uid: f[2],
Gid: f[3],
Name: f[4],
HomeDir: f[5],
},
loginShellCached: f[6],
}
return um, nil
}
func userLookupStd(username string) (*userMeta, error) {
u, err := user.Lookup(username)
if err != nil {
return nil, err
}
return &userMeta{User: *u}, nil
}
func (u *userMeta) LoginShell() string {
if u.loginShellCached != "" {
// This field should be populated on Linux, at least, because
// func userLookup on Linux uses "getent" to look up the user
// and that populates it.
return u.loginShellCached
}
switch runtime.GOOS {
case "darwin":
// Note: /Users/username is key, and not the same as u.HomeDir.
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", u.Username), "UserShell").Output()
// out is "UserShell: /bin/bash"
s, ok := strings.CutPrefix(string(out), "UserShell: ")
if ok {
return strings.TrimSpace(s)
}
}
if e := os.Getenv("SHELL"); e != "" {
return e
}
return "/bin/sh"
}
// defaultPathTmpl specifies the default PATH template to use for new sessions.
//
// If empty, a default value is used based on the OS & distro to match OpenSSH's
// usually-hardcoded behavior. (see
// https://github.com/tailscale/tailscale/issues/5285 for background).
//
// The template may contain @{HOME} or @{PAM_USER} which expand to the user's
// home directory and username, respectively. (PAM is not used, despite the
// name)
var defaultPathTmpl = envknob.RegisterString("TAILSCALE_SSH_DEFAULT_PATH")
func defaultPathForUser(u *user.User) string {
if s := defaultPathTmpl(); s != "" {
return expandDefaultPathTmpl(s, u)
}
isRoot := u.Uid == "0"
switch distro.Get() {
case distro.Debian:
hi := hostinfo.New()
if hi.Distro == "ubuntu" {
// distro.Get's Debian includes Ubuntu. But see if it's actually Ubuntu.
// Ubuntu doesn't empirically seem to distinguish between root and non-root for the default.
// And it includes /snap/bin.
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
}
if isRoot {
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
return "/usr/local/bin:/usr/bin:/bin:/usr/bn/games"
case distro.NixOS:
return defaultPathForUserOnNixOS(u)
}
if isRoot {
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
}
return "/usr/local/bin:/usr/bin:/bin"
}
func defaultPathForUserOnNixOS(u *user.User) string {
var path string
lineread.File("/etc/pam/environment", func(lineb []byte) error {
if v := pathFromPAMEnvLine(lineb, u); v != "" {
path = v
return io.EOF // stop iteration
}
return nil
})
return path
}
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {
if !mem.HasPrefix(mem.B(line), mem.S("PATH")) {
return ""
}
rest := strings.TrimSpace(strings.TrimPrefix(string(line), "PATH"))
if quoted, ok := strings.CutPrefix(rest, "DEFAULT="); ok {
if path, err := strconv.Unquote(quoted); err == nil {
return expandDefaultPathTmpl(path, u)
}
}
return ""
}
func expandDefaultPathTmpl(t string, u *user.User) string {
p := strings.NewReplacer(
"@{HOME}", u.HomeDir,
"@{PAM_USER}", u.Username,
).Replace(t)
if strings.Contains(p, "@{") {
// If there are unknown expansions, conservatively fail closed.
return ""
}
return p
}

View File

@@ -98,7 +98,8 @@ type CapabilityVersion int
// - 59: 2023-03-16: Client understands Peers[].SelfNodeV4MasqAddrForThisPeer
// - 60: 2023-04-06: Client understands IsWireGuardOnly
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
const CurrentCapabilityVersion CapabilityVersion = 61
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events
const CurrentCapabilityVersion CapabilityVersion = 62
type StableID string
@@ -2075,9 +2076,17 @@ type SSHRecorderFailureAction struct {
NotifyURL string `json:",omitempty"`
}
// SSHRecordingFailureNotifyRequest is the JSON payload sent to the NotifyURL
// when a recording fails.
type SSHRecordingFailureNotifyRequest struct {
// SSHEventNotifyRequest is the JSON payload sent to the NotifyURL
// for an SSH event.
type SSHEventNotifyRequest struct {
// EventType is the type of notify request being sent.
EventType SSHEventType
// ConnectionID uniquely identifies a connection made to the SSH server.
// It may be shared across multiple sessions over the same connection in
// case a single connection creates multiple sessions.
ConnectionID string
// CapVersion is the client's current CapabilityVersion.
CapVersion CapabilityVersion
@@ -2093,10 +2102,33 @@ type SSHRecordingFailureNotifyRequest struct {
// LocalUser is the user that was resolved from the SSHUser for the local machine.
LocalUser string
// Attempts is the list of recorders that were attempted, in order.
Attempts []SSHRecordingAttempt
// RecordingAttempts is the list of recorders that were attempted, in order.
RecordingAttempts []*SSHRecordingAttempt
}
// SSHEventType defines the event type linked to a SSH action or state.
type SSHEventType int
const (
UnspecifiedSSHEventType SSHEventType = 0
// SSHSessionRecordingRejected is the event that
// defines when a SSH session cannot be started
// because no recorder is available for session
// recording, and the SSHRecorderFailureAction
// RejectSessionWithMessage is not empty.
SSHSessionRecordingRejected SSHEventType = 1
// SSHSessionRecordingTerminated is the event that
// defines when session recording has failed
// during the session and the SSHRecorderFailureAction
// TerminateSessionWithMessage is not empty.
SSHSessionRecordingTerminated SSHEventType = 2
// SSHSessionRecordingFailed is the event that
// defines when session recording is unavailable and
// the SSHRecorderFailureAction RejectSessionWithMessage
// or TerminateSessionWithMessage is empty.
SSHSessionRecordingFailed SSHEventType = 3
)
// SSHRecordingAttempt is a single attempt to start a recording.
type SSHRecordingAttempt struct {
// Recorder is the address of the recorder that was attempted.

View File

@@ -31,7 +31,7 @@ toolchain="$HOME/.cache/tailscale-go"
if [ -d "$toolchain" ]; then
# A toolchain exists, but is it recent enough to compile gocross? If not,
# wipe it out so that the next if block fetches a usable one.
want_go_minor=$(grep -E '^go ' "$repo_root/go.mod" | cut -f2 -d'.')
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
have_go_minor=$(cut -f2 -d'.' <$toolchain/VERSION)
if [ -z "$have_go_minor" -o "$have_go_minor" -lt "$want_go_minor" ]; then
rm -rf "$toolchain" "$toolchain.extracted"
@@ -45,7 +45,7 @@ if [ ! -d "$toolchain" ]; then
# build with Go N-1. So, if we have no cached tailscale toolchain at all,
# fetch the initial one in shell. Once gocross is built, it'll manage
# updates.
read -r REV <$repo_root/go.toolchain.rev
read -r REV <go.toolchain.rev
case "$REV" in
/*)
@@ -80,7 +80,7 @@ fi
# case, cmd/cloner invokes go with GO111MODULE=off at some stage.
#
# Anyway, build gocross in a stripped down universe.
gocross_path="$repo_root/gocross"
gocross_path="gocross"
gocross_ok=0
wantver="$(git rev-parse HEAD)"
if [ -x "$gocross_path" ]; then

135
tsd/tsd.go Normal file
View File

@@ -0,0 +1,135 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tsd (short for "Tailscale Daemon") contains a System type that
// containing all the subsystems a Tailscale node (tailscaled or platform
// equivalent) uses.
//
// The goal of this package (as of 2023-05-03) is to eventually unify
// initialization across tailscaled, tailscaled as a Windows services, the mac
// GUI, tsnet, wasm, tests, and other places that wire up all the subsystems.
// And doing so without weird optional interface accessors on some subsystems
// that return other subsystems. It's all a work in progress.
//
// This package depends on nearly all parts of Tailscale, so it should not be
// imported by (or thus passed to) any package that does not want to depend on
// the world. In practice this means that only things like cmd/tailscaled,
// ipn/ipnlocal, and ipn/ipnserver should import this package.
package tsd
import (
"fmt"
"reflect"
"tailscale.com/ipn"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
)
// System contains all the subsystems of a Tailscale node (tailscaled, etc.)
type System struct {
Dialer SubSystem[*tsdial.Dialer]
DNSManager SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
Engine SubSystem[wgengine.Engine]
NetMon SubSystem[*netmon.Monitor]
MagicSock SubSystem[*magicsock.Conn]
NetstackRouter SubSystem[bool] // using Netstack at all (either entirely or at least for subnets)
Router SubSystem[router.Router]
Tun SubSystem[*tstun.Wrapper]
StateStore SubSystem[ipn.StateStore]
}
// Set is a convenience method to set a subsystem value.
// It panics if the type is unknown or has that type
// has already been set.
func (s *System) Set(v any) {
switch v := v.(type) {
case *netmon.Monitor:
s.NetMon.Set(v)
case *dns.Manager:
s.DNSManager.Set(v)
case *tsdial.Dialer:
s.Dialer.Set(v)
case wgengine.Engine:
s.Engine.Set(v)
case router.Router:
s.Router.Set(v)
case *tstun.Wrapper:
s.Tun.Set(v)
case *magicsock.Conn:
s.MagicSock.Set(v)
case ipn.StateStore:
s.StateStore.Set(v)
default:
panic(fmt.Sprintf("unknown type %T", v))
}
}
// IsNetstackRouter reports whether Tailscale is either fully netstack based
// (without TUN) or is at least using netstack for routing.
func (s *System) IsNetstackRouter() bool {
if v, ok := s.NetstackRouter.GetOK(); ok && v {
return true
}
return s.IsNetstack()
}
// IsNetstack reports whether Tailscale is running as a netstack-based TUN-free engine.
func (s *System) IsNetstack() bool {
name, _ := s.Tun.Get().Name()
return name == tstun.FakeTUNName
}
// SubSystem represents some subsystem of the Tailscale node daemon.
//
// A subsystem can be set to a value, and then later retrieved. A subsystem
// value tracks whether it's been set and, once set, doesn't allow the value to
// change.
type SubSystem[T any] struct {
set bool
v T
}
// Set sets p to v.
//
// It panics if p is already set to a different value.
//
// Set must not be called concurrently with other Sets or Gets.
func (p *SubSystem[T]) Set(v T) {
if p.set {
var oldVal any = p.v
var newVal any = v
if oldVal == newVal {
// Allow setting to the same value.
// Note we had to box them through "any" to force them to be comparable.
// We can't set the type constraint T to be "comparable" because the interfaces
// aren't comparable. (See https://github.com/golang/go/issues/52531 and
// https://github.com/golang/go/issues/52614 for some background)
return
}
var z *T
panic(fmt.Sprintf("%v is already set", reflect.TypeOf(z).Elem().String()))
}
p.v = v
p.set = true
}
// Get returns the value of p, panicking if it hasn't been set.
func (p *SubSystem[T]) Get() T {
if !p.set {
var z *T
panic(fmt.Sprintf("%v is not set", reflect.TypeOf(z).Elem().String()))
}
return p.v
}
// GetOK returns the value of p (if any) and whether it's been set.
func (p *SubSystem[T]) GetOK() (_ T, ok bool) {
return p.v, p.set
}

View File

@@ -47,6 +47,7 @@ import (
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/smallzstd"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/nettype"
@@ -482,23 +483,21 @@ func (s *Server) start() (reterr error) {
}
closePool.add(s.netMon)
sys := new(tsd.System)
s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
ListenPort: 0,
NetMon: s.netMon,
Dialer: s.dialer,
ListenPort: 0,
NetMon: s.netMon,
Dialer: s.dialer,
SetSubsystem: sys.Set,
})
if err != nil {
return err
}
closePool.add(s.dialer)
sys.Set(eng)
tunDev, magicConn, dns, ok := eng.(wgengine.InternalsGetter).GetInternals()
if !ok {
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, s.dialer, dns)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get())
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
@@ -522,12 +521,13 @@ func (s *Server) start() (reterr error) {
return err
}
}
sys.Set(s.Store)
loginFlags := controlclient.LoginDefault
if s.Ephemeral {
loginFlags = controlclient.LoginEphemeral
}
lb, err := ipnlocal.NewLocalBackend(logf, s.logid, s.Store, s.dialer, eng, loginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, s.logid, sys, loginFlags)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}

View File

@@ -24,7 +24,6 @@ import (
"net/netip"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
@@ -357,10 +356,6 @@ func TestLoopbackLocalAPI(t *testing.T) {
}
func TestLoopbackSOCKS5(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

View File

@@ -37,6 +37,7 @@ import (
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/syncs"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsd"
_ "tailscale.com/tsweb/varz"
_ "tailscale.com/types/flagtype"
_ "tailscale.com/types/key"

View File

@@ -37,6 +37,7 @@ import (
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/syncs"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsd"
_ "tailscale.com/tsweb/varz"
_ "tailscale.com/types/flagtype"
_ "tailscale.com/types/key"

View File

@@ -37,6 +37,7 @@ import (
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/syncs"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsd"
_ "tailscale.com/tsweb/varz"
_ "tailscale.com/types/flagtype"
_ "tailscale.com/types/key"

View File

@@ -37,6 +37,7 @@ import (
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/syncs"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsd"
_ "tailscale.com/tsweb/varz"
_ "tailscale.com/types/flagtype"
_ "tailscale.com/types/key"

View File

@@ -44,6 +44,7 @@ import (
_ "tailscale.com/smallzstd"
_ "tailscale.com/syncs"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsd"
_ "tailscale.com/tsweb/varz"
_ "tailscale.com/types/flagtype"
_ "tailscale.com/types/key"

View File

@@ -14,11 +14,14 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil"
)
var (
testVar1 = expvar.NewInt("gauge_promvarz_test_expvar")
testVar2 = promauto.NewGauge(prometheus.GaugeOpts{Name: "promvarz_test_native"})
)
func TestHandler(t *testing.T) {
test1 := expvar.NewInt("gauge_promvarz_test_expvar")
test1.Set(42)
test2 := promauto.NewGauge(prometheus.GaugeOpts{Name: "promvarz_test_native"})
test2.Set(4242)
testVar1.Set(42)
testVar2.Set(4242)
svr := httptest.NewServer(http.HandlerFunc(Handler))
defer svr.Close()

View File

@@ -202,13 +202,18 @@ func AssertStructUnchanged(t *types.Struct, tname, ctx string, it *ImportTracker
w("var _%s%sNeedsRegeneration = %s(struct {", tname, ctx, tname)
for i := 0; i < t.NumFields(); i++ {
fname := t.Field(i).Name()
st := t.Field(i)
fname := st.Name()
ft := t.Field(i).Type()
if IsInvalid(ft) {
continue
}
qname := it.QualifiedName(ft)
w("\t%s %s", fname, qname)
if st.Anonymous() {
w("\t%s ", fname)
} else {
w("\t%s %s", fname, qname)
}
}
w("}{})\n")

View File

@@ -4,6 +4,21 @@
// Package set contains set types.
package set
// Set is a set of T.
type Set[T comparable] map[T]struct{}
// Add adds e to the set.
func (s Set[T]) Add(e T) { s[e] = struct{}{} }
// Contains reports whether s contains e.
func (s Set[T]) Contains(e T) bool {
_, ok := s[e]
return ok
}
// Len reports the number of items in s.
func (s Set[T]) Len() int { return len(s) }
// HandleSet is a set of T.
//
// It is not safe for concurrent use.

24
util/set/set_test.go Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package set
import "testing"
func TestSet(t *testing.T) {
s := Set[int]{}
s.Add(1)
s.Add(2)
if !s.Contains(1) {
t.Error("missing 1")
}
if !s.Contains(2) {
t.Error("missing 2")
}
if s.Contains(3) {
t.Error("shouldn't have 3")
}
if s.Len() != 2 {
t.Errorf("wrong len %d; want 2", s.Len())
}
}

View File

@@ -131,7 +131,7 @@ var getEmbeddedInfo = lazy.SyncFunc(func() embeddedInfo {
ret.commitDate = strings.ReplaceAll(ret.commitDate, "-", "")
}
case "vcs.modified":
ret.dirty = true
ret.dirty = s.Value == "true"
}
}
if ret.commit == "" || ret.commitDate == "" {

View File

@@ -16,6 +16,7 @@ import (
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/ipproto"
"tailscale.com/types/logid"
@@ -33,29 +34,26 @@ func TestInjectInboundLeak(t *testing.T) {
t.Logf(format, args...)
}
}
sys := new(tsd.System)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: tunDev,
Dialer: dialer,
Tun: tunDev,
Dialer: dialer,
SetSubsystem: sys.Set,
})
if err != nil {
t.Fatal(err)
}
defer eng.Close()
ig, ok := eng.(wgengine.InternalsGetter)
if !ok {
t.Fatal("not an InternalsGetter")
}
tunWrap, magicSock, dns, ok := ig.GetInternals()
if !ok {
t.Fatal("failed to get internals")
}
sys.Set(eng)
sys.Set(new(mem.Store))
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), dialer, eng, 0)
tunWrap := sys.Tun.Get()
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatal(err)
}
ns, err := Create(logf, tunWrap, eng, magicSock, dialer, dns)
ns, err := Create(logf, tunWrap, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
if err != nil {
t.Fatal(err)
}
@@ -89,32 +87,28 @@ func getMemStats() (ms runtime.MemStats) {
func makeNetstack(t *testing.T, config func(*Impl)) *Impl {
tunDev := tstun.NewFake()
sys := &tsd.System{}
sys.Set(new(mem.Store))
dialer := new(tsdial.Dialer)
logf := tstest.WhileTestRunningLogger(t)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: tunDev,
Dialer: dialer,
Tun: tunDev,
Dialer: dialer,
SetSubsystem: sys.Set,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { eng.Close() })
ig, ok := eng.(wgengine.InternalsGetter)
if !ok {
t.Fatal("not an InternalsGetter")
}
tunWrap, magicSock, dns, ok := ig.GetInternals()
if !ok {
t.Fatal("failed to get internals")
}
sys.Set(eng)
ns, err := Create(logf, tunWrap, eng, magicSock, dialer, dns)
ns, err := Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ns.Close() })
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), dialer, eng, 0)
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}

View File

@@ -4,16 +4,9 @@
package netstack
import (
"reflect"
"tailscale.com/wgengine"
"tailscale.com/wgengine/router"
)
func init() {
wgengine.NetstackRouterType = reflect.TypeOf(&subnetRouter{})
}
type subnetRouter struct {
router.Router
}

View File

@@ -10,8 +10,8 @@ import (
"errors"
"fmt"
"io"
"math"
"net/netip"
"reflect"
"runtime"
"strings"
"sync"
@@ -25,7 +25,6 @@ import (
"tailscale.com/health"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/flowtrack"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
@@ -150,29 +149,6 @@ type userspaceEngine struct {
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
}
// InternalsGetter is implemented by Engines that can export their internals.
type InternalsGetter interface {
GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, _ *dns.Manager, ok bool)
}
func (e *userspaceEngine) GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, _ *dns.Manager, ok bool) {
return e.tundev, e.magicConn, e.dns, true
}
// ResolvingEngine is implemented by Engines that have DNS resolvers.
type ResolvingEngine interface {
GetResolver() (_ *resolver.Resolver, ok bool)
}
var (
_ ResolvingEngine = (*userspaceEngine)(nil)
_ ResolvingEngine = (*watchdogEngine)(nil)
)
func (e *userspaceEngine) GetResolver() (r *resolver.Resolver, ok bool) {
return e.dns.Resolver(), true
}
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
type BIRDClient interface {
EnableProtocol(proto string) error
@@ -219,47 +195,37 @@ type Config struct {
// BIRDClient, if non-nil, will be used to configure BIRD whenever
// this node is a primary subnet router.
BIRDClient BIRDClient
// SetSubsystem, if non-nil, is called for each new subsystem created, just before a successful return.
SetSubsystem func(any)
}
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
logf("Starting userspace WireGuard engine (with fake TUN device)")
return NewUserspaceEngine(logf, Config{
ListenPort: listenPort,
// NewFakeUserspaceEngine returns a new userspace engine for testing.
//
// The opts may contain the following types:
//
// - int or uint16: to set the ListenPort.
func NewFakeUserspaceEngine(logf logger.Logf, opts ...any) (Engine, error) {
conf := Config{
RespondToPing: true,
})
}
// NetstackRouterType is a gross cross-package init-time registration
// from netstack to here, informing this package of netstack's router
// type.
var NetstackRouterType reflect.Type
// IsNetstackRouter reports whether e is either fully netstack based
// (without TUN) or is at least using netstack for routing.
func IsNetstackRouter(e Engine) bool {
switch e := e.(type) {
case *userspaceEngine:
if reflect.TypeOf(e.router) == NetstackRouterType {
return true
}
for _, o := range opts {
switch v := o.(type) {
case uint16:
conf.ListenPort = v
case int:
if v < 0 || v > math.MaxUint16 {
return nil, fmt.Errorf("invalid ListenPort: %d", v)
}
conf.ListenPort = uint16(v)
case func(any):
conf.SetSubsystem = v
default:
return nil, fmt.Errorf("unknown option type %T", v)
}
case *watchdogEngine:
return IsNetstackRouter(e.wrap)
}
return IsNetstack(e)
}
// IsNetstack reports whether e is a netstack-based TUN-free engine.
func IsNetstack(e Engine) bool {
ig, ok := e.(InternalsGetter)
if !ok {
return false
}
tw, _, _, ok := ig.GetInternals()
if !ok {
return false
}
name, err := tw.Name()
return err == nil && name == "FakeTUN"
logf("Starting userspace WireGuard engine (with fake TUN device)")
return NewUserspaceEngine(logf, conf)
}
// NewUserspaceEngine creates the named tun device and returns a
@@ -458,6 +424,15 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
e.logf("Starting network monitor...")
e.netMon.Start()
if conf.SetSubsystem != nil {
conf.SetSubsystem(e.tundev)
conf.SetSubsystem(e.magicConn)
conf.SetSubsystem(e.dns)
conf.SetSubsystem(conf.Router)
conf.SetSubsystem(conf.Dialer)
conf.SetSubsystem(e.netMon)
}
e.logf("Engine created.")
return e, nil
}
@@ -1119,10 +1094,6 @@ func (e *userspaceEngine) Wait() {
<-e.waitCh
}
func (e *userspaceEngine) GetNetMon() *netmon.Monitor {
return e.netMon
}
// LinkChange signals a network change event. It's currently
// (2021-03-03) only called on Android. On other platforms, netMon
// generates link change events for us.

View File

@@ -8,6 +8,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
@@ -15,21 +16,23 @@ import (
)
func TestIsNetstack(t *testing.T) {
e, err := wgengine.NewUserspaceEngine(t.Logf, wgengine.Config{})
sys := new(tsd.System)
e, err := wgengine.NewUserspaceEngine(t.Logf, wgengine.Config{SetSubsystem: sys.Set})
if err != nil {
t.Fatal(err)
}
defer e.Close()
if !wgengine.IsNetstack(e) {
if !sys.IsNetstack() {
t.Errorf("IsNetstack = false; want true")
}
}
func TestIsNetstackRouter(t *testing.T) {
tests := []struct {
name string
conf wgengine.Config
want bool
name string
conf wgengine.Config
setNetstackRouter bool
want bool
}{
{
name: "no_netstack",
@@ -50,23 +53,26 @@ func TestIsNetstackRouter(t *testing.T) {
Tun: newFakeOSTUN(),
Router: netstack.NewSubnetRouterWrapper(newFakeOSRouter()),
},
want: true,
setNetstackRouter: true,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e, err := wgengine.NewUserspaceEngine(logger.Discard, tt.conf)
sys := &tsd.System{}
if tt.setNetstackRouter {
sys.NetstackRouter.Set(true)
}
conf := tt.conf
conf.SetSubsystem = sys.Set
e, err := wgengine.NewUserspaceEngine(logger.Discard, conf)
if err != nil {
t.Fatal(err)
}
defer e.Close()
if got := wgengine.IsNetstackRouter(e); got != tt.want {
if got := sys.IsNetstackRouter(); got != tt.want {
t.Errorf("IsNetstackRouter = %v; want %v", got, tt.want)
}
if got := wgengine.IsNetstackRouter(wgengine.NewWatchdog(e)); got != tt.want {
t.Errorf("IsNetstackRouter(watchdog-wrapped) = %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -17,15 +17,11 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
)
@@ -126,9 +122,6 @@ func (e *watchdogEngine) watchdog(name string, fn func()) {
func (e *watchdogEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config, debug *tailcfg.Debug) error {
return e.watchdogErr("Reconfig", func() error { return e.wrap.Reconfig(cfg, routerCfg, dnsCfg, debug) })
}
func (e *watchdogEngine) GetNetMon() *netmon.Monitor {
return e.wrap.GetNetMon()
}
func (e *watchdogEngine) GetFilter() *filter.Filter {
return e.wrap.GetFilter()
}
@@ -181,18 +174,6 @@ func (e *watchdogEngine) WhoIsIPPort(ipp netip.AddrPort) (tsIP netip.Addr, ok bo
func (e *watchdogEngine) Close() {
e.watchdog("Close", e.wrap.Close)
}
func (e *watchdogEngine) GetInternals() (tw *tstun.Wrapper, c *magicsock.Conn, d *dns.Manager, ok bool) {
if ig, ok := e.wrap.(InternalsGetter); ok {
return ig.GetInternals()
}
return
}
func (e *watchdogEngine) GetResolver() (r *resolver.Resolver, ok bool) {
if re, ok := e.wrap.(ResolvingEngine); ok {
return re.GetResolver()
}
return nil, false
}
func (e *watchdogEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) {
e.watchdog("PeerForIP", func() { ret, ok = e.wrap.PeerForIP(ip) })
return ret, ok

View File

@@ -10,7 +10,6 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
@@ -92,9 +91,6 @@ type Engine interface {
// WireGuard status changes.
SetStatusCallback(StatusCallback)
// GetNetMon returns the network monitor.
GetNetMon() *netmon.Monitor
// RequestStatus requests a WireGuard status update right
// away, sent to the callback registered via SetStatusCallback.
RequestStatus()

View File

@@ -541,3 +541,6 @@ vimba
wahoo
coelacanth
llama
shrimp
prawn
lobster