Compare commits
16 Commits
main
...
release-br
Author | SHA1 | Date |
---|---|---|
![]() |
066193c18d | |
![]() |
0438c67e25 | |
![]() |
6842c3c194 | |
![]() |
576b08e5ee | |
![]() |
a8231b18cc | |
![]() |
6d98b5c9a8 | |
![]() |
fe33b17db3 | |
![]() |
5a98bbcbbb | |
![]() |
1e1c16bc24 | |
![]() |
ad504be066 | |
![]() |
6bdb9daec0 | |
![]() |
a3ce35d0c6 | |
![]() |
d1fc9bba7e | |
![]() |
9271e8a062 | |
![]() |
50cf21a779 | |
![]() |
ab998de989 |
|
@ -1 +1 @@
|
|||
1.35.0
|
||||
1.36.2
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
get-authkey
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
Loading…
Reference in New Issue