Compare commits

...

16 Commits

Author SHA1 Message Date
Denton Gentry 066193c18d
cmd/tailscale: use request Schema+Host for QNAP authLogin.cgi
QNAP allows users to set the port number for the management WebUI,
which includes authLogin.cgi. If they do, then connecting to
localhost:8080 fails.

https://github.com/tailscale/tailscale-qpkg/issues/74#issuecomment-1407486911

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
(cherry picked from commit 51288221ce)
2023-03-01 18:00:58 -08:00
Denton Gentry 0438c67e25
VERSION.txt: this is v1.36.2
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-22 15:37:45 -08:00
Mihai Parparita 6842c3c194 net/interfaces: redo how we get the default interface on macOS and iOS
With #6566 we added an external mechanism for getting the default
interface, and used it on macOS and iOS (see tailscale/corp#8201).
The goal was to be able to get the default physical interface even when
using an exit node (in which case the routing table would say that the
Tailscale utun* interface is the default).

However, the external mechanism turns out to be unreliable in some
cases, e.g. when multiple cellular interfaces are present/toggled (I
have occasionally gotten my phone into a state where it reports the pdp_ip1
interface as the default, even though it can't actually route traffic).

It was observed that `ifconfig -v` on macOS reports an "effective interface"
for the Tailscale utn* interface, which seems promising. By examining
the ifconfig source code, it turns out that this is done via a
SIOCGIFDELEGATE ioctl syscall. Though this is a private API, it appears
to have been around for a long time (e.g. it's in the 10.13 xnu release
at https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/net/if_types.h.auto.html)
and thus is unlikely to go away.

We can thus use this ioctl if the routing table says that a utun*
interface is the default, and go back to the simpler mechanism that
we had before #6566.

Updates #7184
Updates #7188

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit fa932fefe7)
2023-02-15 10:44:05 -07:00
Denton Gentry 576b08e5ee
VERSION.txt: this is v1.36.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-08 15:29:24 -08:00
Mihai Parparita a8231b18cc net/interfaces, net/netns: add node attributes to control default interface getting and binding
With #6566 we started to more aggressively bind to the default interface
on Darwin. We are seeing some reports of the wrong cellular interface
being chosen on iOS. To help with the investigation, this adds to knobs
to control the behavior changes:
- CapabilityDebugDisableAlternateDefaultRouteInterface disables the
  alternate function that we use to get the default interface on macOS
  and iOS (implemented in tailscale/corp#8201). We still log what it
  would have returned so we can see if it gets things wrong.
- CapabilityDebugDisableBindConnToInterface is a bigger hammer that
  disables binding of connections to the default interface altogether.

Updates #7184
Updates #7188

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit 62f4df3257)
2023-02-08 13:56:29 -08:00
Andrew Dunham 6d98b5c9a8 net/netns: add functionality to bind outgoing sockets based on route table
When turned on via environment variable (off by default), this will use
the BSD routing APIs to query what interface index a socket should be
bound to, rather than binding to the default interface in all cases.

Updates #5719
Updates #5940

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib4c919471f377b7a08cd3413f8e8caacb29fee0b
(cherry picked from commit 2703d6916f,
with the CurrentCapabilityVersion change reverted)
2023-02-08 13:56:03 -08:00
Andrew Dunham fe33b17db3 ipn/ipnlocal: handle more edge cases in netmap expiry timer
We now handle the case where the NetworkMap.SelfNode has already expired
and do not return an expiry time in the past (which causes an ~infinite
loop of timers to fire).

Additionally, we now add an explicit check to ensure that the next
expiry time is never before the current local-to-the-system time, to
ensure that we don't end up in a similar situation due to clock skew.

Finally, we add more tests for this logic to ensure that we don't
regress on these edge cases.

Fixes #7193

Change-Id: Iaf8e3d83be1d133a7aab7f8d62939e508cc53f9c
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
(cherry picked from commit 6d84f3409b)
2023-02-08 15:59:07 -05:00
Denton Gentry 5a98bbcbbb
cmd/get-authkey: add an OAuth API client to produce an authkey
Updates https://github.com/tailscale/tailscale/issues/3243

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
(cherry picked from commit 4daba23cd4)
2023-02-08 12:17:38 -08:00
Mihai Parparita 1e1c16bc24 logtail: increase maximum log line size in low memory mode
The 255 byte limit was chosen more than 3 years ago (tailscale/corp@929635c9d9),
when iOS was operating under much more significant memory constraints.
With iOS 15 the network extension has an increased limit, so increasing
it to 4K should be fine.

The motivating factor was that the network interfaces being logged
by linkChange in wgengine/userspace.go were getting truncated, and it
would be useful to know why in some cases we're choosing the pdp_ip1
cell interface instead of the pdp_ip0 one.

Updates #7184
Updates #7188

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit f0f2b2e22b)
2023-02-07 22:01:28 -08:00
Maisem Ali ad504be066 ipn/ipnlocal: use presence of NodeID to identify logins
The profileManager was using the LoginName as a proxy to figure out if the profile
had logged in, however the LoginName is not present if the node was created with an
Auth Key that does not have an associated user.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 0fd2f71a1e)
2023-02-07 09:11:28 -08:00
Brad Fitzpatrick 6bdb9daec0
net/netstat: document the Windows netstat code a bit more
And defensively bound allocation.

Updates tailscale/corp#8878

Change-Id: Iaa07479ea2ea28ee1ac3326ab025046d6d785b00
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 2fc8de485c)
2023-01-27 10:12:46 -08:00
Aaron Klotz a3ce35d0c6
net/netstat: add nil checks to Windows OSMetadata implementation
The API documentation does claim to output empty strings under certain
conditions, but we're sometimes seeing nil pointers in the wild, not empty
strings.

Fixes https://github.com/tailscale/corp/issues/8878

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
(cherry picked from commit 4a869048bf)
2023-01-27 10:12:39 -08:00
Brad Fitzpatrick d1fc9bba7e
go.mod: bump wintun-go
For https://git.zx2c4.com/wintun-go/commit/?id=0fa3db229ce2036ae779de8ba03d336b7aa826bd

Updates #6647

Change-Id: I1686b2c77df331fcb9fa4fae88e08232bb95f10b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 0d794a10ff)
2023-01-27 10:12:27 -08:00
shayne 9271e8a062
hostinfo: [windows] check if running via Scoop (#7068)
You can now install Tailscale on Windows via [Scoop](https://scoop.sh).

This change adds a check to `packageTypeWindows()`, looking at the exe's path, and
checking if it starts with: `C:\User\<NAME>\scoop\apps\tailscale`. If so, it
returns `"scoop"` as the package type.

Fixes: #6988

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
(cherry picked from commit 6f992909f0)
2023-01-25 14:32:21 -08:00
phirework 50cf21a779
cmd/tailscale/cli: fix TUNmode display on synology web page (#7064)
Fixes #7059

Signed-off-by: Jenny Zhang <jz@tailscale.com>

Signed-off-by: Jenny Zhang <jz@tailscale.com>
(cherry picked from commit 2d29f77b24)
2023-01-25 10:24:04 -08:00
Denton Gentry ab998de989
VERSION.txt: this is v1.36.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-01-24 15:18:50 -08:00
26 changed files with 829 additions and 83 deletions

View File

@ -1 +1 @@
1.35.0
1.36.2

View File

@ -79,7 +79,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/syncs
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/strs from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+

1
cmd/get-authkey/.gitignore vendored 100644
View File

@ -0,0 +1 @@
get-authkey

View File

@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// get-authkey allocates an authkey using an OAuth API client
// https://tailscale.com/kb/1215/oauth-clients/ and prints it
// to stdout for scripts to capture and use.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"strings"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
)
func main() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
reusable := flag.Bool("reusable", false, "allocate a reusable authkey")
ephemeral := flag.Bool("ephemeral", false, "allocate an ephemeral authkey")
preauth := flag.Bool("preauth", true, "set the authkey as pre-authorized")
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
flag.Parse()
clientId := os.Getenv("TS_API_CLIENT_ID")
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
if clientId == "" || clientSecret == "" {
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
}
baseUrl := os.Getenv("TS_BASE_URL")
if baseUrl == "" {
baseUrl = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientId,
ClientSecret: clientSecret,
TokenURL: baseUrl + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
ctx := context.Background()
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseUrl
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: *reusable,
Ephemeral: *ephemeral,
Preauthorized: *preauth,
Tags: strings.Split(*tags, ","),
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(authkey)
}

View File

@ -220,33 +220,48 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
return "", nil, fmt.Errorf("not authenticated by any mechanism")
}
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
// running based on the request URL. This is necessary because QNAP has so
// many options, see https://github.com/tailscale/tailscale/issues/7108
// and https://github.com/tailscale/tailscale/issues/6903
func qnapAuthnURL(requestUrl string, query url.Values) string {
in, err := url.Parse(requestUrl)
scheme := ""
host := ""
if err != nil || in.Scheme == "" {
log.Printf("Cannot parse QNAP login URL %v", err)
// try localhost and hope for the best
scheme = "http"
host = "localhost"
} else {
scheme = in.Scheme
host = in.Host
}
u := url.URL{
Scheme: scheme,
Host: host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return u.String()
}
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
query := url.Values{
"qtoken": []string{token},
"user": []string{user},
}
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
}
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
query := url.Values{
"sid": []string{sid},
}
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
}
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {

View File

@ -100,7 +100,7 @@
</div>
{{ if .IsSynology }}
<div class="border border-gray-200 bg-orange-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full text-orange-800">
Outgoing access {{ if true }}enabled{{ else }}not configured{{ end }}.
Outgoing access {{ if .TUNMode }}enabled{{ else }}not configured{{ end }}.
<nobr><a href="https://tailscale.com/kb/1152/synology-outbound/"
class="font-medium link"
target="_blank"

View File

@ -4,7 +4,10 @@
package cli
import "testing"
import (
"net/url"
"testing"
)
func TestUrlOfListenAddr(t *testing.T) {
tests := []struct {
@ -35,9 +38,65 @@ func TestUrlOfListenAddr(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := urlOfListenAddr(tt.in)
if url != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, url)
u := urlOfListenAddr(tt.in)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
}
})
}
}
func TestQnapAuthnURL(t *testing.T) {
query := url.Values{
"qtoken": []string{"token"},
}
tests := []struct {
name string
in string
want string
}{
{
name: "localhost http",
in: "http://localhost:8088/",
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "localhost https",
in: "https://localhost:5000/",
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP http",
in: "http://10.1.20.4:80/",
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP6 https",
in: "https://[ff7d:0:1:2::1]/",
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "hostname https",
in: "https://qnap.example.com/",
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "invalid URL",
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "err != nil",
in: "http://192.168.0.%31/",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := qnapAuthnURL(tt.in, query)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
}
})
}

2
go.mod
View File

@ -78,7 +78,7 @@ require (
golang.org/x/term v0.4.0
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
golang.org/x/tools v0.2.0
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
honnef.co/go/tools v0.4.0-0.dev.0.20220517111757-f4a2f64ce238

4
go.sum
View File

@ -1725,8 +1725,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=

View File

@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
@ -69,6 +70,10 @@ func packageTypeWindows() string {
if err != nil {
return ""
}
home, _ := os.UserHomeDir()
if strings.HasPrefix(exe, filepath.Join(home, "scoop", "apps", "tailscale")) {
return "scoop"
}
dir := filepath.Dir(exe)
nsisUninstaller := filepath.Join(dir, "Uninstall-Tailscale.exe")
_, err = os.Stat(nsisUninstaller)

View File

@ -70,15 +70,16 @@ func (em *expiryManager) onControlTime(t time.Time) {
// Node.Expired so other parts of the codebase can provide more clear error
// messages when attempting to e.g. ping an expired node.
//
// The localNow time should be the output of time.Now for the local system; it
// will be adjusted by any stored clock skew from ControlTime.
//
// This is additionally a defense-in-depth against something going wrong with
// control such that we start seeing expired peers with a valid Endpoints or
// DERP field.
//
// This function is safe to call concurrently with onControlTime but not
// concurrently with any other call to flagExpiredPeers.
func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap) {
localNow := em.timeNow()
func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow time.Time) {
// Adjust our current time by any saved delta to adjust for clock skew.
controlNow := localNow.Add(em.clockDelta.Load())
if controlNow.Before(flagExpiredPeersEpoch) {
@ -120,3 +121,95 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap) {
peer.Key = key.NodePublicWithBadOldPrefix(peer.Key)
}
}
// nextPeerExpiry returns the time that the next node in the netmap expires
// (including the self node), based on their KeyExpiry. It skips nodes that are
// already marked as Expired. If there are no nodes expiring in the future,
// then the zero Time will be returned.
//
// The localNow time should be the output of time.Now for the local system; it
// will be adjusted by any stored clock skew from ControlTime.
//
// This function is safe to call concurrently with other methods of this expiryManager.
func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Time) time.Time {
if nm == nil {
return time.Time{}
}
controlNow := localNow.Add(em.clockDelta.Load())
if controlNow.Before(flagExpiredPeersEpoch) {
em.logf("netmap: nextPeerExpiry: [unexpected] delta-adjusted current time is before hardcoded epoch; skipping")
return time.Time{}
}
var nextExpiry time.Time // zero if none
for _, peer := range nm.Peers {
if peer.KeyExpiry.IsZero() {
continue // tagged node
} else if peer.Expired {
// Peer already expired; Expired is set by the
// flagExpiredPeers function, above.
continue
} else if peer.KeyExpiry.Before(controlNow) {
// This peer already expired, and peer.Expired
// isn't set for some reason. Skip this node.
continue
}
// nextExpiry being zero is a sentinel that we haven't yet set
// an expiry; otherwise, only update if this node's expiry is
// sooner than the currently-stored one (since we want the
// soonest-occuring expiry time).
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) {
nextExpiry = peer.KeyExpiry
}
}
// Ensure that we also fire this timer if our own node key expires.
if nm.SelfNode != nil {
selfExpiry := nm.SelfNode.KeyExpiry
if selfExpiry.IsZero() {
// No expiry for self node
} else if selfExpiry.Before(controlNow) {
// Self node already expired; we don't want to return a
// time in the past, so skip this.
} else if nextExpiry.IsZero() || selfExpiry.Before(nextExpiry) {
// Self node expires after now, but before the soonest
// peer in the netmap; update our next expiry to this
// time.
nextExpiry = selfExpiry
}
}
// As an additional defense in depth, never return a time that is
// before the current time from the perspective of the local system
// (since timers with a zero or negative duration will fire
// immediately and can cause unnecessary reconfigurations).
//
// This can happen if the local clock is running fast; for example:
// localTime = 2pm
// controlTime = 1pm (real time)
// nextExpiry = 1:30pm (real time)
//
// In the above case, we'd return a nextExpiry of 1:30pm while the
// current clock reads 2pm; in this case, setting a timer for
// nextExpiry.Sub(now) would result in a negative duration and a timer
// that fired immediately.
//
// In this particular edge-case, return an expiry time 30 seconds after
// the local time so that any timers created based on this expiry won't
// fire too quickly.
//
// The alternative would be to do all comparisons in local time,
// unadjusted for clock skew, but that doesn't handle cases where the
// local clock is "fixed" between netmap updates.
if !nextExpiry.IsZero() && nextExpiry.Before(localNow) {
em.logf("netmap: nextPeerExpiry: skipping nextExpiry %q before local time %q due to clock skew",
nextExpiry.UTC().Format(time.RFC3339),
localNow.UTC().Format(time.RFC3339))
return localNow.Add(30 * time.Second)
}
return nextExpiry
}

View File

@ -116,7 +116,7 @@ func TestFlagExpiredPeers(t *testing.T) {
if tt.controlTime != nil {
em.onControlTime(*tt.controlTime)
}
em.flagExpiredPeers(tt.netmap)
em.flagExpiredPeers(tt.netmap, now)
if !reflect.DeepEqual(tt.netmap.Peers, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.netmap.Peers), formatNodes(tt.want))
}
@ -124,6 +124,158 @@ func TestFlagExpiredPeers(t *testing.T) {
}
}
func TestNextPeerExpiry(t *testing.T) {
n := func(id tailcfg.NodeID, name string, expiry time.Time, mod ...func(*tailcfg.Node)) *tailcfg.Node {
n := &tailcfg.Node{ID: id, Name: name, KeyExpiry: expiry}
for _, f := range mod {
f(n)
}
return n
}
now := time.Unix(1675725516, 0)
noExpiry := time.Time{}
timeInPast := now.Add(-1 * time.Hour)
timeInFuture := now.Add(1 * time.Hour)
timeInMoreFuture := now.Add(2 * time.Hour)
tests := []struct {
name string
netmap *netmap.NetworkMap
want time.Time
}{
{
name: "no_expiry",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", noExpiry),
n(2, "bar", noExpiry),
},
SelfNode: n(3, "self", noExpiry),
},
want: noExpiry,
},
{
name: "future_expiry_from_peer",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", noExpiry),
n(2, "bar", timeInFuture),
},
SelfNode: n(3, "self", noExpiry),
},
want: timeInFuture,
},
{
name: "future_expiry_from_self",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", noExpiry),
n(2, "bar", noExpiry),
},
SelfNode: n(3, "self", timeInFuture),
},
want: timeInFuture,
},
{
name: "future_expiry_from_multiple_peers",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInFuture),
n(2, "bar", timeInMoreFuture),
},
SelfNode: n(3, "self", noExpiry),
},
want: timeInFuture,
},
{
name: "future_expiry_from_peer_and_self",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInMoreFuture),
},
SelfNode: n(2, "self", timeInFuture),
},
want: timeInFuture,
},
{
name: "only_self",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{},
SelfNode: n(1, "self", timeInFuture),
},
want: timeInFuture,
},
{
name: "peer_already_expired",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInPast),
},
SelfNode: n(2, "self", timeInFuture),
},
want: timeInFuture,
},
{
name: "self_already_expired",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInFuture),
},
SelfNode: n(2, "self", timeInPast),
},
want: timeInFuture,
},
{
name: "all_nodes_already_expired",
netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInPast),
},
SelfNode: n(2, "self", timeInPast),
},
want: noExpiry,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
em := newExpiryManager(t.Logf)
em.timeNow = func() time.Time { return now }
got := em.nextPeerExpiry(tt.netmap, now)
if got != tt.want {
t.Errorf("got %q, want %q", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
} else if !got.IsZero() && got.Before(now) {
t.Errorf("unexpectedly got expiry %q before now %q", got.Format(time.RFC3339), now.Format(time.RFC3339))
}
})
}
t.Run("ClockSkew", func(t *testing.T) {
t.Logf("local time: %q", now.Format(time.RFC3339))
em := newExpiryManager(t.Logf)
em.timeNow = func() time.Time { return now }
// The local clock is "running fast"; our clock skew is -2h
em.clockDelta.Store(-2 * time.Hour)
t.Logf("'real' time: %q", now.Add(-2*time.Hour).Format(time.RFC3339))
// If we don't adjust for the local time, this would return a
// time in the past.
nm := &netmap.NetworkMap{
Peers: []*tailcfg.Node{
n(1, "foo", timeInPast),
},
}
got := em.nextPeerExpiry(nm, now)
want := now.Add(30 * time.Second)
if got != want {
t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
}
})
}
func formatNodes(nodes []*tailcfg.Node) string {
var sb strings.Builder
for i, n := range nodes {

View File

@ -48,6 +48,7 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
@ -823,7 +824,8 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Handle node expiry in the netmap
if st.NetMap != nil {
b.em.flagExpiredPeers(st.NetMap)
now := time.Now()
b.em.flagExpiredPeers(st.NetMap, now)
// Always stop the existing netmap timer if we have a netmap;
// it's possible that we have no nodes expiring, so we should
@ -837,31 +839,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.nmExpiryTimer = nil
}
now := time.Now()
// Figure out when the next node in the netmap is expiring so we can
// start a timer to reconfigure at that point.
var nextExpiry time.Time // zero if none
for _, peer := range st.NetMap.Peers {
if peer.KeyExpiry.IsZero() {
continue // tagged node
} else if peer.Expired {
// Peer already expired; Expired is set by the
// flagExpiredPeers function, above.
continue
}
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) {
nextExpiry = peer.KeyExpiry
}
}
// Ensure that we also fire this timer if our own node key expires.
if st.NetMap.SelfNode != nil {
if selfExpiry := st.NetMap.SelfNode.KeyExpiry; !selfExpiry.IsZero() && selfExpiry.Before(nextExpiry) {
nextExpiry = selfExpiry
}
}
nextExpiry := b.em.nextPeerExpiry(st.NetMap, now)
if !nextExpiry.IsZero() {
tmrDuration := nextExpiry.Sub(now) + 10*time.Second
b.nmExpiryTimer = time.AfterFunc(tmrDuration, func() {
@ -3823,6 +3803,10 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.setDebugLogsByCapabilityLocked(nm)
// See the netns package for documentation on what this capability does.
netns.SetBindToInterfaceByRoute(hasCapability(nm, tailcfg.CapabilityBindToInterfaceByRoute))
netns.SetDisableBindConnToInterface(hasCapability(nm, tailcfg.CapabilityDebugDisableBindConnToInterface))
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
if nm == nil {
b.nodeByAddr = nil

View File

@ -186,7 +186,7 @@ func init() {
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
prefs := prefsIn.AsStruct().View()
newPersist := prefs.Persist().AsStruct()
if newPersist == nil || newPersist.LoginName == "" {
if newPersist == nil || newPersist.NodeID == "" {
return pm.setPrefsLocked(prefs)
}
up := newPersist.UserProfile

View File

@ -6,6 +6,7 @@ package ipnlocal
import (
"fmt"
"strconv"
"testing"
"tailscale.com/ipn"
@ -338,6 +339,7 @@ func TestProfileManagementWindows(t *testing.T) {
ID: id,
LoginName: loginName,
},
NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))),
}
if err := pm.SetPrefs(p.View()); err != nil {
t.Fatal(err)

View File

@ -474,6 +474,7 @@ func TestStateMachine(t *testing.T) {
notifies.expect(3)
cc.persist.LoginName = "user1"
cc.persist.UserProfile.LoginName = "user1"
cc.persist.NodeID = "node1"
cc.send(nil, "", true, &netmap.NetworkMap{})
{
nn := notifies.drain(3)
@ -700,6 +701,7 @@ func TestStateMachine(t *testing.T) {
notifies.expect(3)
cc.persist.LoginName = "user2"
cc.persist.UserProfile.LoginName = "user2"
cc.persist.NodeID = "node2"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})
@ -836,6 +838,7 @@ func TestStateMachine(t *testing.T) {
notifies.expect(3)
cc.persist.LoginName = "user3"
cc.persist.UserProfile.LoginName = "user3"
cc.persist.NodeID = "node3"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
})

View File

@ -559,7 +559,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, proc
// Put a sanity cap on buf's size.
max := 16 << 10
if l.lowMem {
max = 255
max = 4 << 10
}
var nTruncated int
if len(buf) > max {

View File

@ -190,7 +190,7 @@ func TestEncodeSpecialCases(t *testing.T) {
// lowMem + long string
l.skipClientTime = false
l.lowMem = true
longStr := strings.Repeat("0", 512)
longStr := strings.Repeat("0", 5120)
io.WriteString(l, longStr)
body = <-ts.uploaded
data = unmarshalOne(t, body)
@ -198,8 +198,8 @@ func TestEncodeSpecialCases(t *testing.T) {
if !ok {
t.Errorf("lowMem: no text %v", data)
}
if n := len(text.(string)); n > 300 {
t.Errorf("lowMem: got %d chars; want <300 chars", n)
if n := len(text.(string)); n > 4500 {
t.Errorf("lowMem: got %d chars; want <4500 chars", n)
}
// -------------------------------------------------------------------------
@ -333,10 +333,10 @@ func unmarshalOne(t *testing.T, body []byte) map[string]any {
func TestEncodeTextTruncation(t *testing.T) {
lg := &Logger{timeNow: time.Now, lowMem: true}
in := bytes.Repeat([]byte("a"), 300)
in := bytes.Repeat([]byte("a"), 5120)
b := lg.encodeText(in, true, 0, 0, 0)
got := string(b)
want := `{"text": "` + strings.Repeat("a", 255) + `…+45"}` + "\n"
want := `{"text": "` + strings.Repeat("a", 4096) + `…+1024"}` + "\n"
if got != want {
t.Errorf("got:\n%qwant:\n%q\n", got, want)
}

View File

@ -20,7 +20,6 @@ import (
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/syncs"
)
func defaultRoute() (d DefaultRouteDetails, err error) {
@ -41,13 +40,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
// owns the default route. It returns the first IPv4 or IPv6 default route it
// finds (it does not prefer one or the other).
func DefaultRouteInterfaceIndex() (int, error) {
if f := defaultRouteInterfaceIndexFunc.Load(); f != nil {
if ifIndex := f(); ifIndex != 0 {
return ifIndex, nil
}
// Fallthrough if we can't use the alternate implementation.
}
// $ netstat -nr
// Routing tables
// Internet:
@ -76,22 +68,17 @@ func DefaultRouteInterfaceIndex() (int, error) {
continue
}
if isDefaultGateway(rm) {
if delegatedIndex, err := getDelegatedInterface(rm.Index); err == nil && delegatedIndex != 0 {
return delegatedIndex, nil
} else if err != nil {
log.Printf("interfaces_bsd: could not get delegated interface: %v", err)
}
return rm.Index, nil
}
}
return 0, errors.New("no gateway index found")
}
var defaultRouteInterfaceIndexFunc syncs.AtomicValue[func() int]
// SetDefaultRouteInterfaceIndexFunc allows an alternate implementation of
// DefaultRouteInterfaceIndex to be provided. If none is set, or if f() returns a 0
// (indicating an unknown interface index), then the default implementation (that parses
// the routing table) will be used.
func SetDefaultRouteInterfaceIndexFunc(f func() int) {
defaultRouteInterfaceIndexFunc.Store(f)
}
func init() {
likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB
}

View File

@ -5,9 +5,15 @@
package interfaces
import (
"net"
"strings"
"sync"
"syscall"
"unsafe"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/util/mak"
)
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
@ -18,3 +24,73 @@ func fetchRoutingTable() (rib []byte, err error) {
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
}
var ifNames struct {
sync.Mutex
m map[int]string // ifindex => name
}
// getDelegatedInterface returns the interface index of the underlying interface
// for the given interface index. 0 is returned if the interface does not
// delegate.
func getDelegatedInterface(ifIndex int) (int, error) {
ifNames.Lock()
defer ifNames.Unlock()
// To get the delegated interface, we do what ifconfig does and use the
// SIOCGIFDELEGATE ioctl. It operates in term of a ifreq struct, which
// has to be populated with a interface name. To avoid having to do a
// interface index -> name lookup every time, we cache interface names
// (since indexes and names are stable after boot).
ifName, ok := ifNames.m[ifIndex]
if !ok {
iface, err := net.InterfaceByIndex(ifIndex)
if err != nil {
return 0, err
}
ifName = iface.Name
mak.Set(&ifNames.m, ifIndex, ifName)
}
// Only tunnels (like Tailscale itself) have a delegated interface, avoid
// the ioctl if we can.
if !strings.HasPrefix(ifName, "utun") {
return 0, nil
}
// We don't cache the result of the ioctl, since the delegated interface can
// change, e.g. if the user changes the preferred service order in the
// network preference pane.
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
return 0, err
}
defer unix.Close(fd)
// Match the ifreq struct/union from the bsd/net/if.h header in the Darwin
// open source release.
var ifr struct {
ifr_name [unix.IFNAMSIZ]byte
ifr_delegated uint32
}
copy(ifr.ifr_name[:], ifName)
// SIOCGIFDELEGATE is not in the Go x/sys package or in the public macOS
// <sys/sockio.h> headers. However, it is in the Darwin/xnu open source
// release (and is used by ifconfig, see
// https://github.com/apple-oss-distributions/network_cmds/blob/6ccdc225ad5aa0d23ea5e7d374956245d2462427/ifconfig.tproj/ifconfig.c#L2183-L2187).
// We generate its value by evaluating the `_IOWR('i', 157, struct ifreq)`
// macro, which is how it's defined in
// https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/sys/sockio.h#L264
const SIOCGIFDELEGATE = 0xc020699d
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(fd),
uintptr(SIOCGIFDELEGATE),
uintptr(unsafe.Pointer(&ifr)))
if errno != 0 {
return 0, errno
}
return int(ifr.ifr_delegated), nil
}

View File

@ -23,3 +23,7 @@ func fetchRoutingTable() (rib []byte, err error) {
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
}
func getDelegatedInterface(ifIndex int) (int, error) {
return 0, nil
}

View File

@ -32,6 +32,27 @@ func SetEnabled(on bool) {
disabled.Store(!on)
}
var bindToInterfaceByRoute atomic.Bool
// SetBindToInterfaceByRoute enables or disables whether we use the system's
// route information to bind to a particular interface. It is the same as
// setting the TS_BIND_TO_INTERFACE_BY_ROUTE.
//
// Currently, this only changes the behaviour on macOS.
func SetBindToInterfaceByRoute(v bool) {
bindToInterfaceByRoute.Store(v)
}
var disableBindConnToInterface atomic.Bool
// SetDisableBindConnToInterface disables the (normal) behavior of binding
// connections to the default network interface.
//
// Currently, this only has an effect on Darwin.
func SetDisableBindConnToInterface(v bool) {
disableBindConnToInterface.Store(v)
}
// Listener returns a new net.Listener with its Control hook func
// initialized as necessary to run in logical network namespace that
// doesn't route back into Tailscale.

View File

@ -11,10 +11,14 @@ import (
"fmt"
"log"
"net"
"net/netip"
"os"
"strings"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/envknob"
"tailscale.com/net/interfaces"
"tailscale.com/types/logger"
)
@ -25,6 +29,10 @@ func control(logf logger.Logf) func(network, address string, c syscall.RawConn)
}
}
var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_ROUTE")
var errInterfaceIndexInvalid = errors.New("interface index invalid")
// controlLogf marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
@ -34,15 +42,150 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e
// Don't bind to an interface for localhost connections.
return nil
}
idx, err := interfaces.DefaultRouteInterfaceIndex()
if disableBindConnToInterface.Load() {
logf("netns_darwin: binding connection to interfaces disabled")
return nil
}
idx, err := getInterfaceIndex(logf, address)
if err != nil {
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
// callee logged
return nil
}
return bindConnToInterface(c, network, address, idx, logf)
}
func getInterfaceIndex(logf logger.Logf, address string) (int, error) {
// Helper so we can log errors.
defaultIdx := func() (int, error) {
idx, err := interfaces.DefaultRouteInterfaceIndex()
if err != nil {
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
return -1, err
}
return idx, nil
}
useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv()
if !useRoute {
return defaultIdx()
}
host, _, err := net.SplitHostPort(address)
if err != nil {
// No port number; use the string directly.
host = address
}
// If the address doesn't parse, use the default index.
addr, err := netip.ParseAddr(host)
if err != nil {
logf("[unexpected] netns: error parsing address %q: %v", host, err)
return defaultIdx()
}
idx, err := interfaceIndexFor(addr, true /* canRecurse */)
if err != nil {
logf("netns: error in interfaceIndexFor: %v", err)
return defaultIdx()
}
// 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 {
logf("[unexpected] netns: interfaceIndexFor returned Tailscale interface")
return defaultIdx()
}
return idx, err
}
// interfaceIndexFor returns the interface index that we should bind to in
// order to send traffic to the provided address.
func interfaceIndexFor(addr netip.Addr, canRecurse bool) (int, error) {
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
if err != nil {
return 0, fmt.Errorf("creating AF_ROUTE socket: %w", err)
}
defer unix.Close(fd)
var routeAddr route.Addr
if addr.Is4() {
routeAddr = &route.Inet4Addr{IP: addr.As4()}
} else {
routeAddr = &route.Inet6Addr{IP: addr.As16()}
}
rm := route.RouteMessage{
Version: unix.RTM_VERSION,
Type: unix.RTM_GET,
Flags: unix.RTF_UP,
ID: uintptr(os.Getpid()),
Seq: 1,
Addrs: []route.Addr{
unix.RTAX_DST: routeAddr,
},
}
b, err := rm.Marshal()
if err != nil {
return 0, fmt.Errorf("marshaling RouteMessage: %w", err)
}
_, err = unix.Write(fd, b)
if err != nil {
return 0, fmt.Errorf("writing message: %w", err)
}
var buf [2048]byte
n, err := unix.Read(fd, buf[:])
if err != nil {
return 0, fmt.Errorf("reading message: %w", err)
}
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf[:n])
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
if len(msgs) == 0 {
return 0, fmt.Errorf("no messages")
}
for _, msg := range msgs {
rm, ok := msg.(*route.RouteMessage)
if !ok {
continue
}
if rm.Version < 3 || rm.Version > 5 || rm.Type != unix.RTM_GET {
continue
}
if len(rm.Addrs) < unix.RTAX_GATEWAY {
continue
}
switch addr := rm.Addrs[unix.RTAX_GATEWAY].(type) {
case *route.LinkAddr:
return addr.Index, nil
case *route.Inet4Addr:
// We can get a gateway IP; recursively call ourselves
// (exactly once) to get the link (and thus index) for
// the gateway IP.
if canRecurse {
return interfaceIndexFor(netip.AddrFrom4(addr.IP), false)
}
case *route.Inet6Addr:
// As above.
if canRecurse {
return interfaceIndexFor(netip.AddrFrom16(addr.IP), false)
}
default:
// Unknown type; skip it
continue
}
}
return 0, fmt.Errorf("no valid address found")
}
// SetListenConfigInterfaceIndex sets lc.Control such that sockets are bound
// to the provided interface index.
func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error {

View File

@ -0,0 +1,85 @@
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package netns
import (
"testing"
"tailscale.com/net/interfaces"
)
func TestGetInterfaceIndex(t *testing.T) {
oldVal := bindToInterfaceByRoute.Load()
t.Cleanup(func() { bindToInterfaceByRoute.Store(oldVal) })
bindToInterfaceByRoute.Store(true)
tests := []struct {
name string
addr string
err string
}{
{
name: "IP_and_port",
addr: "8.8.8.8:53",
},
{
name: "bare_ip",
addr: "8.8.8.8",
},
{
name: "invalid",
addr: "!!!!!",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
idx, err := getInterfaceIndex(t.Logf, tc.addr)
if err != nil {
if tc.err == "" {
t.Fatalf("got unexpected error: %v", err)
}
if errstr := err.Error(); errstr != tc.err {
t.Errorf("expected error %q, got %q", errstr, tc.err)
}
} else {
t.Logf("getInterfaceIndex(%q) = %d", tc.addr, idx)
if tc.err != "" {
t.Fatalf("wanted error %q", tc.err)
}
if idx < 0 {
t.Fatalf("got invalid index %d", idx)
}
}
})
}
t.Run("NoTailscale", func(t *testing.T) {
_, tsif, err := interfaces.Tailscale()
if err != nil {
t.Fatal(err)
}
if tsif == nil {
t.Skip("no tailscale interface on this machine")
}
defaultIdx, err := interfaces.DefaultRouteInterfaceIndex()
if err != nil {
t.Fatal(err)
}
idx, err := getInterfaceIndex(t.Logf, "100.100.100.100:53")
if err != nil {
t.Fatal(err)
}
t.Logf("tailscaleIdx=%d defaultIdx=%d idx=%d", tsif.Index, defaultIdx, idx)
if idx == tsif.Index {
t.Fatalf("got idx=%d; wanted not Tailscale interface", idx)
} else if idx != defaultIdx {
t.Fatalf("got idx=%d, want %d", idx, defaultIdx)
}
})
}

View File

@ -20,6 +20,13 @@ import (
// OSMetadata includes any additional OS-specific information that may be
// obtained during the retrieval of a given Entry.
type OSMetadata interface {
// GetModule returns the entry's module name.
//
// It returns ("", nil) if no entry is found. As of 2023-01-27, any returned
// error is silently discarded by its sole caller in portlist_windows.go and
// treated equivalently as returning ("", nil), but this may change in the
// future. An error should only be returned in casees that are worthy of
// being logged at least.
GetModule() (string, error)
}
@ -224,6 +231,13 @@ type moduleInfoConstraint interface {
_MIB_TCPROW_OWNER_MODULE | _MIB_TCP6ROW_OWNER_MODULE
}
// moduleInfo implements OSMetadata.GetModule. It calls
// getOwnerModuleFromTcpEntry or getOwnerModuleFromTcp6Entry.
//
// See
// https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getownermodulefromtcpentry
//
// It may return "", nil indicating a successful call but with empty data.
func moduleInfo[entryType moduleInfoConstraint](entry *entryType, proc *windows.LazyProc) (string, error) {
var buf []byte
var desiredLen uint32
@ -240,22 +254,36 @@ func moduleInfo[entryType moduleInfoConstraint](entry *entryType, proc *windows.
if err == windows.ERROR_SUCCESS {
break
}
if err == windows.ERROR_NOT_FOUND {
return "", nil
}
if err != windows.ERROR_INSUFFICIENT_BUFFER {
return "", err
}
if desiredLen > 1<<20 {
// Sanity check before allocating too much.
return "", nil
}
buf = make([]byte, desiredLen)
addr = unsafe.Pointer(&buf[0])
}
if addr == nil {
// GetOwnerModuleFromTcp*Entry can apparently return ERROR_SUCCESS
// (NO_ERROR) on the first call without the usual first
// ERROR_INSUFFICIENT_BUFFER result. Windows said success, so interpret
// that was sucessfully not having data.
return "", nil
}
basicInfo := (*_TCPIP_OWNER_MODULE_BASIC_INFO)(addr)
return windows.UTF16PtrToString(basicInfo.moduleName), nil
}
// GetModule implements OSMetadata.
func (m *_MIB_TCPROW_OWNER_MODULE) GetModule() (string, error) {
return moduleInfo(m, getOwnerModuleFromTcpEntry)
}
// GetModule implements OSMetadata.
func (m *_MIB_TCP6ROW_OWNER_MODULE) GetModule() (string, error) {
return moduleInfo(m, getOwnerModuleFromTcp6Entry)
}

View File

@ -1726,6 +1726,22 @@ const (
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
// CapabilityBindToInterfaceByRoute changes how Darwin nodes create
// sockets (in the net/netns package). See that package for more
// details on the behaviour of this capability.
CapabilityBindToInterfaceByRoute = "https://tailscale.com/cap/bind-to-interface-by-route"
// CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin
// nodes get the default interface. There is an optional hook (used by the
// macOS and iOS clients) to override the default interface, this capability
// disables that and uses the default behavior (of parsing the routing
// table).
CapabilityDebugDisableAlternateDefaultRouteInterface = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface"
// CapabilityDebugDisableBindConnToInterface disables the automatic binding
// of connections to the default network interface on Darwin nodes.
CapabilityDebugDisableBindConnToInterface = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"
// CapabilityTailnetLockAlpha indicates the node is in the tailnet lock alpha,
// and initialization of tailnet lock may proceed.
//