Compare commits

...

29 Commits

Author SHA1 Message Date
Aaron Klotz
a82dfe7f99 start testing
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-09-19 14:18:20 -06:00
Maisem Ali
19a9d9037f tailcfg: add NodeCapMap
Like PeerCapMap, add a field to `tailcfg.Node` which provides
a map of Capability to raw JSON messages which are deferred to be
parsed later by the application code which cares about the specific
capabilities. This effectively allows us to prototype new behavior
without having to commit to a schema in tailcfg, and it also opens up
the possibilities to develop custom behavior in tsnet applications w/o
having to plumb through application specific data in the MapResponse.

Updates #4217

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Maisem Ali
4da0689c2c tailcfg: add Node.HasCap helpers
This makes a follow up change less noisy.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Maisem Ali
d06b48dd0a tailcfg: add RawMessage
This adds a new RawMessage type backed by string instead of the
json.RawMessage which is backed by []byte. The byte slice makes
the generated views be a lot more defensive than the need to be
which we can get around by using a string instead.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Sonia Appasamy
258f16f84b ipn/ipnlocal: add tailnet MagicDNS name to ipn.LoginProfile
Start backfilling MagicDNS suffixes on LoginProfiles.

Updates #9286

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-09-18 13:58:32 -04:00
Brad Fitzpatrick
0d991249e1 types/netmap: remove NetworkMap.{Addresses,MachineStatus}
And convert all callers over to the methods that check SelfNode.

Now we don't have multiple ways to express things in tests (setting
fields on SelfNode vs NetworkMap, sometimes inconsistently) and don't
have multiple ways to check those two fields (often only checking one
or the other).

Updates #9443

Change-Id: I2d7ba1cf6556142d219fae2be6f484f528756e3c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-18 17:08:11 +01:00
Marwan Sulaiman
d25217c9db cmd/tailscale/cli: error when serving foreground if bg already exists
This PR fixes a bug to make sure that we don't allow two configs
exist with duplicate ports

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-18 11:16:01 -04:00
Brad Fitzpatrick
98b5da47e8 types/views: add SliceContainsFunc like slices.ContainsFunc
Needed for a future change.

Updates #cleanup

Change-Id: I6d89ee8a048b3bb1eb9cfb2e5a53c93aed30b021
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-18 16:09:59 +01:00
Maisem Ali
a61caea911 tailcfg: define a type for NodeCapability
Instead of untyped string, add a type to identify these.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-17 13:16:29 -07:00
Brad Fitzpatrick
3d37328af6 wgengine, proxymap: split out port mapping from Engine to new type
(Continuing quest to remove rando stuff from the "Engine")

Updates #cleanup

Change-Id: I77f39902c2194410c10c054b545d70c9744250b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 20:06:43 +01:00
Brad Fitzpatrick
db2f37d7c6 ipn/ipnlocal: add some test accessors
Updates tailscale/corp#12990

Change-Id: I82801ac4c003d2c7e1352c514adb908dbf01be87
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:35:17 +01:00
Brad Fitzpatrick
9538e9f970 ipn/ipnlocal: keep internal map updated of latest Nodes post mutations
We have some flaky integration tests elsewhere that have no one place
to ask about the state of the world. This makes LocalBackend be that
place (as it's basically there anyway) but doesn't yet add the ForTest
accessor method.

This adds a LocalBackend.peers map[NodeID]NodeView that is
incrementally updated as mutations arrive. And then we start moving
away from using NetMap.Peers at runtime (UpdateStatus no longer uses
it now). And remove another copy of NodeView in the LocalBackend
nodeByAddr map. Change that to point into b.peers instead.

Future changes will then start streaming whole-node-granularity peer
change updates to WatchIPNBus clients, tracking statefully per client
what each has seen. This will get the GUI clients from receiving less
of a JSON storm of updates all the time.

Updates #1909

Change-Id: I14a976ca9f493bdf02ba7e6e05217363dcf422e5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:35:17 +01:00
Brad Fitzpatrick
926c990a09 types/netmap: start phasing out Addresses, add GetAddresses method
NetworkMap.Addresses is redundant with the SelfNode.Addresses. This
works towards a TODO to delete NetworkMap.Addresses and replace it
with a method.

This is similar to #9389.

Updates #cleanup

Change-Id: Id000509ca5d16bb636401763d41bdb5f38513ba0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:16:43 +01:00
Brad Fitzpatrick
fb5ceb03e3 types/netmap: deprecate NetworkMap.MachineStatus, add accessor method
Step 1 of deleting it, per TODO.

Updates #cleanup

Change-Id: I1d3d0165ae5d8b20610227d60640997b73568733
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:09:11 +01:00
Brad Fitzpatrick
0f3c279b86 ipn/ipnlocal: delete some unused code
Updates #cleanup

Change-Id: I90b46c476f135124d97288e776c2b428b351b8b8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 16:32:57 +01:00
Brad Fitzpatrick
760b945bc0 ipn/{ipnlocal,ipnstate}: start simplifying UpdateStatus/StatusBuilder
* Remove unnecessary mutexes (there's no concurrency)
* Simplify LocalBackend.UpdateStatus using the StatusBuilder.WantPeers
  field that was added in 0f604923d3, removing passing around some
  method values into func args. And then merge two methods.

More remains, but this is a start.

Updates #9433

Change-Id: Iaf2d7ec6e4e590799f00bae185465a4fd089b822
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 15:38:54 +01:00
James Tucker
8ab46952d4 net/ping: fix ICMP echo code field to 0
The code was trying to pass the ICMP protocol number here (1), which is
not a valid code. Many servers will not respond to echo messages with
codes other than 0.

https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes-8

Updates #9299
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 17:08:39 -07:00
James Tucker
f6845b10f6 ipn/ipnlocal: plumb ExitNodeDNSResolvers for IsWireGuardOnly exit nodes
This enables installing default resolvers specified by
tailcfg.Node.ExitNodeDNSResolvers when the exit node is selected.

Updates #9377

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 13:58:38 -07:00
James Tucker
e7727db553 tailcfg: add DNS address list for IsWireGuardOnly nodes
Tailscale exit nodes provide DNS service over the peer API, however
IsWireGuardOnly nodes do not have a peer API, and instead need client
DNS parameters passed in their node description.

For Mullvad nodes this will contain the in network 10.64.0.1 address.

Updates #9377

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 13:15:18 -07:00
Maisem Ali
335a5aaf9a cmd/k8s-operator: add APISERVER_PROXY env
The kube-apiserver proxy in the operator would only run in
auth proxy mode but thats not always desirable. There are
situations where the proxy should just be a transparent
proxy and not inject auth headers, so do that using a new
env var APISERVER_PROXY and deprecate the AUTH_PROXY env.

THe new env var has three options `false`, `true` and `noauth`.

Updates #8317

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-15 09:18:18 -05:00
James Tucker
4c693d2ee8 net/dns/publicdns: update Mullvad DoH server list
The following IPs are not used anymore: 193.19.108.2 and 193.19.108.3.
All of the servers are now named consistently under dns.mullvad.net.
Several new servers were added.

https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/

Updates #5416
Updates #9345

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 17:48:01 -07:00
Charlotte Brandhorst-Satzkorn
8428a64b56 words: holy mole we need some more mammals
Particularly of the polydactyl kind.

Updates #14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-09-14 14:25:33 -07:00
James Tucker
1858ad65c8 cmd/cloner: do not allocate slices when the source is nil
tailcfg.Node zero-value clone equality checks failed when I added a
[]*foo to the structure, as the zero value and it's clone contained a
different slice header.

Updates #9377
Updates #9408
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 11:36:34 -07:00
James Tucker
85155ddaf3 tailcfg: remove completed TODO from IsWireGuardOnly
Updates #7826
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 10:16:18 -07:00
Tyler Smalley
dfefaa5e35 Use parent serve config
Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-09-14 10:22:38 -05:00
Marwan Sulaiman
f3a5bfb1b9 cmd/tailscale/cli: add set serve validations
This PR adds validations for the new new funnel/serve
commands under the following rules:
1. There is always a single config for one port (bg or fg).
2. Foreground configs under the same port cannot co-exists (for now).
3. Background configs can change as long as the serve type is the same.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-14 10:22:38 -05:00
Andrew Lytvynov
7ce1c6f981 .github/workflows: fix slack-action format in govulncheck.yml (#9390)
Currently slack messages for errors fail:
https://github.com/tailscale/tailscale/actions/runs/6159104272/job/16713248204

```
Error: Unexpected token
 in JSON at position 151
```

This is likely due to the line break in the text. Restructure the
message to use separate title/text and fix the slack webhook body.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-09-13 14:36:40 -07:00
Marwan Sulaiman
3421784e37 cmd/tailscale/cli: use optimistic concurrency control on SetServeConfig
This PR uses the etag/if-match pattern to ensure multiple calls
to SetServeConfig are synchronized. It currently errors out and
asks the user to retry but we can add occ retries as a follow up.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-13 15:08:41 -05:00
Brad Fitzpatrick
6e66e5beeb cmd/tsconnect/wasm: pass a netmon to ipnserver.New
It became required as of 6e967446e4

Updates #8052

Change-Id: I08d100534254865293c1beca5beff8e529e4e9ac
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-13 12:52:55 -07:00
83 changed files with 2830 additions and 864 deletions

View File

@@ -27,8 +27,9 @@ jobs:
payload: >
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks>
(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|commit>) of ${{ github.repository }}@${{ github.ref_name }} by ${{ github.event.head_commit.committer.name }}",
"title": "${{ job.status }}: ${{ github.workflow }}",
"title_link": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks",
"text": "${{ github.repository }}@${{ github.sha }}",
"color": "danger"
}]
}

View File

@@ -140,6 +140,10 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
all, _ := io.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
if res.StatusCode == http.StatusPreconditionFailed {
all, _ := io.ReadAll(res.Body)
return nil, &PreconditionsFailedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
@@ -170,6 +174,24 @@ func IsAccessDeniedError(err error) bool {
return errors.As(err, &ae)
}
// PreconditionsFailedError is returned when the server responds
// with an HTTP 412 status code.
type PreconditionsFailedError struct {
err error
}
func (e *PreconditionsFailedError) Error() string {
return fmt.Sprintf("Preconditions failed: %v", e.err)
}
func (e *PreconditionsFailedError) Unwrap() error { return e.err }
// IsPreconditionsFailedError reports whether err is or wraps an PreconditionsFailedError.
func IsPreconditionsFailedError(err error) bool {
var ae *PreconditionsFailedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
@@ -198,27 +220,42 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, nil)
return slurp, err
}
func (lc *LocalClient) sendWithHeaders(
ctx context.Context,
method,
path string,
wantStatus int,
body io.Reader,
h http.Header,
) ([]byte, http.Header, error) {
if jr, ok := body.(jsonReader); ok && jr.err != nil {
return nil, jr.err // fail early if there was a JSON marshaling error
return nil, nil, jr.err // fail early if there was a JSON marshaling error
}
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
if err != nil {
return nil, err
return nil, nil, err
}
if h != nil {
req.Header = h
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
return nil, nil, err
}
defer res.Body.Close()
slurp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
return nil, nil, err
}
if res.StatusCode != wantStatus {
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
return nil, bestError(err, slurp)
return nil, nil, bestError(err, slurp)
}
return slurp, nil
return slurp, res.Header, nil
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
@@ -1093,7 +1130,11 @@ func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config))
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
}
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
@@ -1112,11 +1153,19 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
//
// If the serve config is empty, it returns (nil, nil).
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/serve-config", 200, nil)
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
return getServeConfigFromJSON(body)
sc, err := getServeConfigFromJSON(body)
if err != nil {
return nil, err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
sc.ETag = h.Get("Etag")
return sc, nil
}
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {

View File

@@ -122,6 +122,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
case *types.Slice:
if codegen.ContainsPointers(ft.Elem()) {
n := it.QualifiedName(ft.Elem())
writef("if src.%s != nil {", fname)
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
@@ -137,6 +138,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
writef("}")
writef("}")
} else {
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
}

View File

@@ -169,6 +169,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/maps from tailscale.com/tailcfg
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http

View File

@@ -47,12 +47,11 @@ func main() {
tailscale.I_Acknowledge_This_API_Is_Unstable = true
var (
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
)
var opts []kzap.Opts
@@ -70,10 +69,8 @@ func main() {
s, tsClient := initTSNet(zlog)
defer s.Close()
restConfig := config.GetConfigOrDie()
if shouldRunAuthProxy {
launchAuthProxy(zlog, restConfig, s)
}
startReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags)
maybeLaunchAPIServerProxy(zlog, restConfig, s)
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags)
}
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
@@ -180,9 +177,9 @@ waitOnline:
return s, tsClient
}
// startReconcilers starts the controller-runtime manager and registers the
// ServiceReconciler.
func startReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) {
// runReconcilers starts the controller-runtime manager and registers the
// ServiceReconciler. It blocks forever.
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) {
var (
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
)

View File

@@ -45,12 +45,52 @@ func addWhoIsToRequest(r *http.Request, who *apitype.WhoIsResponse) *http.Reques
var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
// launchAuthProxy launches the auth proxy, which is a small HTTP server that
// authenticates requests using the Tailscale LocalAPI and then proxies them to
// the kube-apiserver.
func launchAuthProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server) {
type apiServerProxyMode int
const (
apiserverProxyModeDisabled apiServerProxyMode = iota
apiserverProxyModeEnabled
apiserverProxyModeNoAuth
)
func parseAPIProxyMode() apiServerProxyMode {
haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
switch {
case haveAPIProxyEnv && haveAuthProxyEnv:
log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
case haveAuthProxyEnv:
var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
if authProxyEnv {
return apiserverProxyModeEnabled
}
return apiserverProxyModeDisabled
case haveAPIProxyEnv:
var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
switch apiProxyEnv {
case "true":
return apiserverProxyModeEnabled
case "false", "":
return apiserverProxyModeDisabled
case "noauth":
return apiserverProxyModeNoAuth
default:
panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
}
}
return apiserverProxyModeDisabled
}
// maybeLaunchAPIServerProxy launches the auth proxy, which is a small HTTP server
// that authenticates requests using the Tailscale LocalAPI and then proxies
// them to the kube-apiserver.
func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server) {
mode := parseAPIProxyMode()
if mode == apiserverProxyModeDisabled {
return
}
hostinfo.SetApp("k8s-operator-proxy")
startlog := zlog.Named("launchAuthProxy")
startlog := zlog.Named("launchAPIProxy")
cfg, err := restConfig.TransportConfig()
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
@@ -69,18 +109,18 @@ func launchAuthProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy").Infof, mode)
}
// authProxy is an http.Handler that authenticates requests using the Tailscale
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
// LocalAPI and then proxies them to the Kubernetes API.
type authProxy struct {
type apiserverProxy struct {
logf logger.Logf
lc *tailscale.LocalClient
rp *httputil.ReverseProxy
}
func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
h.logf("failed to authenticate caller: %v", err)
@@ -91,28 +131,38 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.rp.ServeHTTP(w, addWhoIsToRequest(r, who))
}
// runAuthProxy runs an HTTP server that authenticates requests using the
// runAPIServerProxy runs an HTTP server that authenticates requests using the
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
// It listens on :443 and uses the Tailscale HTTPS certificate.
// s will be started if it is not already running.
// rt is used to proxy requests to the Kubernetes API.
//
// mode controls how the proxy behaves:
// - apiserverProxyModeDisabled: the proxy is not started.
// - apiserverProxyModeEnabled: the proxy is started and requests are impersonated using the
// caller's identity from the Tailscale LocalAPI.
// - apiserverProxyModeNoAuth: the proxy is started and requests are not impersonated and
// are passed through to the Kubernetes API.
//
// It never returns.
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, mode apiServerProxyMode) {
if mode == apiserverProxyModeDisabled {
return
}
ln, err := s.Listen("tcp", ":443")
if err != nil {
log.Fatalf("could not listen on :443: %v", err)
}
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
if err != nil {
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
}
lc, err := s.LocalClient()
if err != nil {
log.Fatalf("could not get local client: %v", err)
}
ap := &authProxy{
ap := &apiserverProxy{
logf: logf,
lc: lc,
rp: &httputil.ReverseProxy{
@@ -120,6 +170,12 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
// Replace the URL with the Kubernetes APIServer.
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
if mode == apiserverProxyModeNoAuth {
// If we are not providing authentication, then we are just
// proxying to the Kubernetes API, so we don't need to do
// anything else.
return
}
// We want to proxy to the Kubernetes API, but we want to use
// the caller's identity to do so. We do this by impersonating
@@ -157,7 +213,7 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
Handler: ap,
}
if err := hs.ServeTLS(ln, "", ""); err != nil {
log.Fatalf("runAuthProxy: failed to serve %v", err)
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
}
}
@@ -177,7 +233,7 @@ type impersonateRule struct {
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
// in the context by the authProxy.
// in the context by the apiserverProxy.
func addImpersonationHeaders(r *http.Request) error {
who := whoIsFromRequest(r)
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)

View File

@@ -45,15 +45,15 @@ func TestImpersonationHeaders(t *testing.T) {
emailish: "foo@example.com",
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`{"impersonate":{"groups":["group1","group2"]}}`),
[]byte(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
[]byte(`{"impersonate":{"groups":["group4"]}}`),
[]byte(`{"impersonate":{"groups":["group2"]}}`), // duplicate
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group2"]}}`), // duplicate
// These should be ignored, but should parse correctly.
[]byte(`{}`),
[]byte(`{"impersonate":{}}`),
[]byte(`{"impersonate":{"groups":[]}}`),
tailcfg.RawMessage(`{}`),
tailcfg.RawMessage(`{"impersonate":{}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":[]}}`),
},
},
wantHeaders: http.Header{
@@ -67,7 +67,7 @@ func TestImpersonationHeaders(t *testing.T) {
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`{"impersonate":{"groups":["group1"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`),
},
},
wantHeaders: http.Header{
@@ -81,7 +81,7 @@ func TestImpersonationHeaders(t *testing.T) {
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`[]`),
tailcfg.RawMessage(`[]`),
},
},
wantHeaders: http.Header{},

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"net"
"os"
"slices"
"strconv"
"strings"
@@ -147,15 +146,13 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
//
// verifyFunnelEnabled may refresh the local state and modify the st input.
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error {
hasFunnelAttrs := func(attrs []string) bool {
hasHTTPS := slices.Contains(attrs, tailcfg.CapabilityHTTPS)
hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel)
return hasHTTPS && hasFunnel
hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool {
return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel)
}
if hasFunnelAttrs(st.Self.Capabilities) {
if hasFunnelAttrs(st.Self) {
return nil // already enabled
}
enableErr := e.enableFeatureInteractive(ctx, "funnel", hasFunnelAttrs)
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
switch {
case statusErr != nil:

View File

@@ -18,7 +18,6 @@ import (
"path/filepath"
"reflect"
"runtime"
"slices"
"sort"
"strconv"
"strings"
@@ -269,9 +268,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
// on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs.
e.enableFeatureInteractive(ctx, "serve", func(caps []string) bool {
return slices.Contains(caps, tailcfg.CapabilityHTTPS)
})
e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS)
}
srcPort, err := parseServePort(srcPortStr)
@@ -829,7 +826,7 @@ func parseServePort(s string) (uint16, error) {
//
// 2023-08-09: The only valid feature values are "serve" and "funnel".
// This can be moved to some CLI lib when expanded past serve/funnel.
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, hasRequiredCapabilities func(caps []string) bool) (err error) {
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) {
info, err := e.lc.QueryFeature(ctx, feature)
if err != nil {
return err
@@ -875,7 +872,16 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
return err
}
if nm := n.NetMap; nm != nil && nm.SelfNode.Valid() {
if hasRequiredCapabilities(nm.SelfNode.Capabilities().AsSlice()) {
gotAll := true
for _, c := range caps {
if !nm.SelfNode.HasCap(c) {
// The feature is not yet enabled.
// Continue blocking until it is.
gotAll = false
break
}
}
if gotAll {
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_enabled", feature), 1)
fmt.Fprintln(os.Stdout, "Success.")
return nil

View File

@@ -15,7 +15,6 @@ import (
"os/signal"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
@@ -233,9 +232,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs.
if err := e.enableFeatureInteractive(ctx, "serve", func(caps []string) bool {
return slices.Contains(caps, tailcfg.CapabilityHTTPS)
}); err != nil {
if err := e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS); err != nil {
return fmt.Errorf("error enabling https feature: %w", err)
}
}
@@ -266,6 +263,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
if turnOff {
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount)
} else {
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
return err
}
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
msg = e.messageForPort(sc, st, dnsName, srvPort)
}
@@ -275,6 +275,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
}
if err := e.lc.SetServeConfig(ctx, parentSC); err != nil {
if tailscale.IsPreconditionsFailedError(err) {
fmt.Fprintln(os.Stderr, "Another client is changing the serve config; please try again.")
}
return err
}
@@ -298,6 +301,57 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
}
}
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
sc, isFg := findConfig(sc, port)
if sc == nil {
return nil
}
if isFg {
return errors.New("foreground already exists under this port")
}
if !e.bg {
return errors.New("background serve already exists under this port")
}
existingServe := serveFromPortHandler(sc.TCP[port])
if wantServe != existingServe {
return fmt.Errorf("want %q but port is already serving %q", wantServe, existingServe)
}
return nil
}
func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
switch {
case tcp.HTTP:
return serveTypeHTTP
case tcp.HTTPS:
return serveTypeHTTPS
case tcp.TerminateTLS != "":
return serveTypeTLSTerminatedTCP
case tcp.TCPForward != "":
return serveTypeTCP
default:
return -1
}
}
// findConfig finds a config that contains the given port, which can be
// the top level background config or an inner foreground one. The second
// result is true if it's foreground
func findConfig(sc *ipn.ServeConfig, port uint16) (*ipn.ServeConfig, bool) {
if sc == nil {
return nil, false
}
if _, ok := sc.TCP[port]; ok {
return sc, false
}
for _, sc := range sc.Foreground {
if _, ok := sc.TCP[port]; ok {
return sc, true
}
}
return nil, false
}
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
// update serve config based on the type
switch srvType {
@@ -742,13 +796,13 @@ func cleanURLPath(urlPath string) (string, error) {
func (s serveType) String() string {
switch s {
case serveTypeHTTP:
return "httpListener"
return "http"
case serveTypeHTTPS:
return "httpsListener"
return "https"
case serveTypeTCP:
return "tcpListener"
return "tcp"
case serveTypeTLSTerminatedTCP:
return "tlsTerminatedTCPListener"
return "tls-terminated-tcp"
default:
return "unknownServeType"
}

View File

@@ -697,7 +697,7 @@ func TestServeDevConfigMutations(t *testing.T) {
})
add(step{ // try to start a web handler on the same port
command: cmd("serve --https=443 --bg localhost:3000"),
wantErr: exactErr(errHelp, "errHelp"),
wantErr: anyErr(),
})
add(step{reset: true})
add(step{ // start a web handler on port 443
@@ -781,6 +781,111 @@ func TestServeDevConfigMutations(t *testing.T) {
}
}
func TestValidateConfig(t *testing.T) {
tests := [...]struct {
name string
desc string
cfg *ipn.ServeConfig
servePort uint16
serveType serveType
bg bool
wantErr bool
}{
{
name: "nil_config",
desc: "when config is nil, all requests valid",
cfg: nil,
servePort: 3000,
serveType: serveTypeHTTPS,
},
{
name: "new_bg_tcp",
desc: "no error when config exists but we're adding a new bg tcp port",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
},
bg: true,
servePort: 10000,
serveType: serveTypeHTTPS,
},
{
name: "override_bg_tcp",
desc: "no error when overwriting previous port under the same serve type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "http://localhost:4545"},
},
},
bg: true,
servePort: 443,
serveType: serveTypeTCP,
},
{
name: "override_bg_tcp",
desc: "error when overwriting previous port under a different serve type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
},
bg: true,
servePort: 443,
serveType: serveTypeHTTP,
wantErr: true,
},
{
name: "new_fg_port",
desc: "no error when serving a new foreground port",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
Foreground: map[string]*ipn.ServeConfig{
"abc123": {
TCP: map[uint16]*ipn.TCPPortHandler{
3000: {HTTPS: true},
},
},
},
},
servePort: 4040,
serveType: serveTypeTCP,
},
{
name: "same_fg_port",
desc: "error when overwriting a previous fg port",
cfg: &ipn.ServeConfig{
Foreground: map[string]*ipn.ServeConfig{
"abc123": {
TCP: map[uint16]*ipn.TCPPortHandler{
3000: {HTTPS: true},
},
},
},
},
servePort: 3000,
serveType: serveTypeTCP,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
se := serveEnv{bg: tc.bg}
err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType)
if err == nil && tc.wantErr {
t.Fatal("expected an error but got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestSrcTypeFromFlags(t *testing.T) {
tests := []struct {
name string

View File

@@ -763,7 +763,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
// queryFeatureResponse is the mock response desired from the
// call made to lc.QueryFeature by verifyFunnelEnabled.
queryFeatureResponse mockQueryFeatureResponse
caps []string // optionally set at fakeStatus.Capabilities
caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities
wantErr string
wantPanic string
}{
@@ -780,13 +780,13 @@ func TestVerifyFunnelEnabled(t *testing.T) {
{
name: "fallback-flow-missing-acl-rule",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
caps: []string{tailcfg.CapabilityHTTPS},
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS},
wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`,
},
{
name: "fallback-flow-enabled",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
caps: []string{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
wantErr: "", // no error, success
},
{
@@ -858,7 +858,7 @@ var fakeStatus = &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
Capabilities: []tailcfg.NodeCapability{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
},
}

View File

@@ -175,7 +175,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+

View File

@@ -289,6 +289,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/control/controlclient+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
@@ -387,7 +388,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+

View File

@@ -711,7 +711,14 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
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())
return netstack.Create(logf,
sys.Tun.Get(),
sys.Engine.Get(),
sys.MagicSock.Get(),
sys.Dialer.Get(),
sys.DNSManager.Get(),
sys.ProxyMapper(),
)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -37,6 +37,7 @@ import (
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/views"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/words"
@@ -109,7 +110,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
sys.Set(eng)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
@@ -126,7 +127,7 @@ func newIPN(jsConfig js.Value) map[string]any {
sys.NetstackRouter.Set(true)
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
srv := ipnserver.New(logf, logid, sys.NetMon.Get())
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
@@ -249,11 +250,11 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
Self: jsNetMapSelfNode{
jsNetMapNode: jsNetMapNode{
Name: nm.Name,
Addresses: mapSlice(nm.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
Addresses: mapSliceView(nm.GetAddresses(), func(a netip.Prefix) string { return a.Addr().String() }),
NodeKey: nm.NodeKey.String(),
MachineKey: nm.MachineKey.String(),
},
MachineStatus: jsMachineStatus[nm.MachineStatus],
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
},
Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
name := p.Name()
@@ -578,6 +579,14 @@ func mapSlice[T any, M any](a []T, f func(T) M) []M {
return n
}
func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
n := make([]M, a.Len())
for i := range a.LenIter() {
n[i] = f(a.At(i))
}
return n
}
func filterSlice[T any](a []T, f func(T) bool) []T {
n := make([]T, 0, len(a))
for _, e := range a {

View File

@@ -136,21 +136,29 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
dst := new(StructWithSlices)
*dst = *src
dst.Values = append(src.Values[:0:0], src.Values...)
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
for i := range dst.ValuePointers {
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
if src.ValuePointers != nil {
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
for i := range dst.ValuePointers {
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
}
}
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
for i := range dst.StructPointers {
dst.StructPointers[i] = src.StructPointers[i].Clone()
if src.StructPointers != nil {
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
for i := range dst.StructPointers {
dst.StructPointers[i] = src.StructPointers[i].Clone()
}
}
dst.Structs = make([]StructWithPtrs, len(src.Structs))
for i := range dst.Structs {
dst.Structs[i] = *src.Structs[i].Clone()
if src.Structs != nil {
dst.Structs = make([]StructWithPtrs, len(src.Structs))
for i := range dst.Structs {
dst.Structs[i] = *src.Structs[i].Clone()
}
}
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
dst.Ints[i] = ptr.To(*src.Ints[i])
if src.Ints != nil {
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
dst.Ints[i] = ptr.To(*src.Ints[i])
}
}
dst.Slice = append(src.Slice[:0:0], src.Slice...)
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)

View File

@@ -237,9 +237,10 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
slice := u
sElem := slice.Elem()
switch x := sElem.(type) {
case *types.Basic:
case *types.Basic, *types.Named:
sElem := it.QualifiedName(sElem)
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
args.MapValueType = "[]" + sElem.String()
args.MapValueType = "[]" + sElem
args.MapFn = "views.SliceOf(t)"
template = "mapFnField"
case *types.Pointer:

View File

@@ -187,8 +187,9 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
if resp.Node != nil {
if DevKnob.StripCaps() {
resp.Node.Capabilities = nil
resp.Node.CapMap = nil
}
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities)
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities, resp.Node.CapMap)
}
// Call Node.InitDisplayNames on any changed nodes.
@@ -324,6 +325,7 @@ var (
patchLastSeen = clientmetric.NewCounter("controlclient_patch_lastseen")
patchKeyExpiry = clientmetric.NewCounter("controlclient_patch_keyexpiry")
patchCapabilities = clientmetric.NewCounter("controlclient_patch_capabilities")
patchCapMap = clientmetric.NewCounter("controlclient_patch_capmap")
patchKeySignature = clientmetric.NewCounter("controlclient_patch_keysig")
patchifiedPeer = clientmetric.NewCounter("controlclient_patchified_peer")
@@ -452,6 +454,10 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
mut.KeySignature = v
patchKeySignature.Add(1)
}
if v := pc.CapMap; v != nil {
mut.CapMap = v
patchCapMap.Add(1)
}
*vp = mut.View()
}
@@ -647,6 +653,10 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if was.Cap() != n.Cap {
pc().Cap = n.Cap
}
case "CapMap":
if n.CapMap != nil {
pc().CapMap = n.CapMap
}
case "Tags":
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
return nil, false
@@ -693,6 +703,19 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if va == nil || vb == nil || *va != *vb {
return nil, false
}
case "ExitNodeDNSResolvers":
va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers)
if va.Len() != vb.Len() {
return nil, false
}
for i := range va.LenIter() {
if !va.At(i).Equal(vb.At(i)) {
return nil, false
}
}
}
}
if ret != nil {
@@ -739,12 +762,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
nm.SelfNode = node
nm.Expiry = node.KeyExpiry()
nm.Name = node.Name()
nm.Addresses = filterSelfAddresses(node.Addresses().AsSlice())
if node.MachineAuthorized() {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
}
ms.addUserProfile(nm, nm.User())

View File

@@ -20,6 +20,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -328,13 +329,13 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
Capabilities: ptr.To([]string{"foo"}),
Capabilities: ptr.To([]tailcfg.NodeCapability{"foo"}),
}},
},
want: peers(&tailcfg.Node{
ID: 1,
Name: "foo",
Capabilities: []string{"foo"},
Capabilities: []tailcfg.NodeCapability{"foo"},
}),
wantStats: updateStats{changed: 1},
}}
@@ -684,15 +685,15 @@ func TestPeerChangeDiff(t *testing.T) {
},
{
name: "patch-capabilities-to-nonempty",
a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}},
b: &tailcfg.Node{ID: 1, Capabilities: []string{"bar"}},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string{"bar"})},
a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}},
b: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"bar"}},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability{"bar"})},
},
{
name: "patch-capabilities-to-empty",
a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}},
a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}},
b: &tailcfg.Node{ID: 1},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string(nil))},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability(nil))},
},
{
name: "patch-online-to-true",
@@ -835,6 +836,40 @@ func TestPatchifyPeersChanged(t *testing.T) {
},
},
},
{
name: "change_exitnodednsresolvers",
mr0: &tailcfg.MapResponse{
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
Peers: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
},
},
want: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
},
},
},
{
name: "same_exitnoderesolvers",
mr0: &tailcfg.MapResponse{
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
Peers: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
want: &tailcfg.MapResponse{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -6,6 +6,7 @@
package controlknobs
import (
"slices"
"sync/atomic"
"tailscale.com/syncs"
@@ -48,39 +49,30 @@ type Knobs struct {
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
// node attributes (Node.Capabilities).
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []string) {
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability, capMap tailcfg.NodeCapMap) {
if k == nil {
return
}
var (
keepFullWG bool
disableDRPO bool
disableUPnP bool
randomizeClientPort bool
disableDeltaUpdates bool
oneCGNAT opt.Bool
forceBackgroundSTUN bool
)
for _, attr := range selfNodeAttrs {
switch attr {
case tailcfg.NodeAttrDebugDisableWGTrim:
keepFullWG = true
case tailcfg.NodeAttrDebugDisableDRPO:
disableDRPO = true
case tailcfg.NodeAttrDisableUPnP:
disableUPnP = true
case tailcfg.NodeAttrRandomizeClientPort:
randomizeClientPort = true
case tailcfg.NodeAttrOneCGNATEnable:
oneCGNAT.Set(true)
case tailcfg.NodeAttrOneCGNATDisable:
oneCGNAT.Set(false)
case tailcfg.NodeAttrDebugForceBackgroundSTUN:
forceBackgroundSTUN = true
case tailcfg.NodeAttrDisableDeltaUpdates:
disableDeltaUpdates = true
}
has := func(attr tailcfg.NodeCapability) bool {
_, ok := capMap[attr]
return ok || slices.Contains(selfNodeAttrs, attr)
}
var (
keepFullWG = has(tailcfg.NodeAttrDebugDisableWGTrim)
disableDRPO = has(tailcfg.NodeAttrDebugDisableDRPO)
disableUPnP = has(tailcfg.NodeAttrDisableUPnP)
randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort)
disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates)
oneCGNAT opt.Bool
forceBackgroundSTUN = has(tailcfg.NodeAttrDebugForceBackgroundSTUN)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
oneCGNAT.Set(true)
} else if has(tailcfg.NodeAttrOneCGNATDisable) {
oneCGNAT.Set(false)
}
k.KeepFullWGConfig.Store(keepFullWG)
k.DisableDRPO.Store(disableDRPO)
k.DisableUPnP.Store(disableUPnP)

View File

@@ -9,6 +9,7 @@ import (
"sync/atomic"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/views"
)
@@ -22,7 +23,7 @@ import (
// c2n log level changes), and via capabilities from a NetMap (so users can
// enable logging via the ACL JSON).
type LogKnob struct {
capName string
capName tailcfg.NodeCapability
cap atomic.Bool
env func() bool
manual atomic.Bool
@@ -30,7 +31,7 @@ type LogKnob struct {
// NewLogKnob creates a new LogKnob, with the provided environment variable
// name and/or NetMap capability.
func NewLogKnob(env, cap string) *LogKnob {
func NewLogKnob(env string, cap tailcfg.NodeCapability) *LogKnob {
if env == "" && cap == "" {
panic("must provide either an environment variable or capability")
}
@@ -58,7 +59,7 @@ func (lk *LogKnob) Set(v bool) {
// about; we use this rather than a concrete type to avoid a circular
// dependency.
type NetMap interface {
SelfCapabilities() views.Slice[string]
SelfCapabilities() views.Slice[tailcfg.NodeCapability]
}
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap

View File

@@ -64,7 +64,7 @@ func TestLogKnob(t *testing.T) {
testKnob.UpdateFromNetMap(&netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Capabilities: []string{
Capabilities: []tailcfg.NodeCapability{
"https://tailscale.com/cap/testing",
},
}).View(),

View File

@@ -91,6 +91,7 @@ var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
Web map[HostPort]*WebServerConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// Clone makes a deep copy of TCPPortHandler.

View File

@@ -182,6 +182,7 @@ func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeCon
return t.View()
})
}
func (v ServeConfigView) ETag() string { return v.ж.ETag }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
@@ -189,6 +190,7 @@ var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
Web map[HostPort]*WebServerConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// View returns a readonly view of TCPPortHandler.

View File

@@ -50,6 +50,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
peers []tailcfg.NodeView
os string // version.OS value; empty means linux
cloud cloudenv.Cloud
prefs *ipn.Prefs
@@ -68,23 +69,28 @@ func TestDNSConfigForNetmap(t *testing.T) {
{
name: "self_name_and_peers",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("100.101.101.101"),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
{
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
@@ -102,23 +108,28 @@ func TestDNSConfigForNetmap(t *testing.T) {
// even if they have IPv4.
name: "v6_only_self",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("fe75::1"),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
},
{
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("fe75::1"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
OnlyIPv6: true,
@@ -134,8 +145,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
{
name: "extra_records",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("100.101.101.101"),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
}).View(),
DNS: tailcfg.DNSConfig{
ExtraRecords: []tailcfg.DNSRecord{
{Name: "foo.com", Value: "1.2.3.4"},
@@ -319,7 +332,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
verOS := cmpx.Or(tt.os, "linux")
var log tstest.MemLogger
got := dnsConfigForNetmap(tt.nm, tt.prefs.View(), log.Logf, verOS)
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), log.Logf, verOS)
if !reflect.DeepEqual(got, tt.want) {
gotj, _ := json.MarshalIndent(got, "", "\t")
wantj, _ := json.MarshalIndent(tt.want, "", "\t")
@@ -332,6 +345,17 @@ func TestDNSConfigForNetmap(t *testing.T) {
}
}
func peersMap(s []tailcfg.NodeView) map[tailcfg.NodeID]tailcfg.NodeView {
m := make(map[tailcfg.NodeID]tailcfg.NodeView)
for _, n := range s {
if n.ID() == 0 {
panic("zero Node.ID")
}
m[n.ID()] = n
}
return m
}
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
b := &LocalBackend{}
if b.allowExitNodeDNSProxyToServeName("google.com") {

View File

@@ -31,6 +31,7 @@ import (
"go4.org/mem"
"go4.org/netipx"
xmaps "golang.org/x/exp/maps"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
@@ -203,11 +204,21 @@ type LocalBackend struct {
capFileSharing bool // whether netMap contains the file sharing capability
capTailnetLock bool // whether netMap contains the tailnet lock capability
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
netMap *netmap.NetworkMap // not mutated in place once set (except for Peers slice)
hostinfo *tailcfg.Hostinfo
// netMap is the most recently set full netmap from the controlclient.
// It can't be mutated in place once set. Because it can't be mutated in place,
// delta updates from the control server don't apply to it. Instead, use
// the peers map to get up-to-date information on the state of peers.
// In general, avoid using the netMap.Peers slice. We'd like it to go away
// as of 2023-09-17.
netMap *netmap.NetworkMap
// peers is the set of current peers and their current values after applying
// delta node mutations as they come in (with mu held). The map values can
// be given out to callers, but the map itself must not escape the LocalBackend.
peers map[tailcfg.NodeID]tailcfg.NodeView
nodeByAddr map[netip.Addr]tailcfg.NodeID
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil
nodeByAddr map[netip.Addr]tailcfg.NodeView
activeLogin string // last logged LoginName from netMap
activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
@@ -541,7 +552,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs())
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := len(b.netMap.Addresses)
want := b.netMap.GetAddresses().Len()
if len(b.peerAPIListeners) < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
go b.initPeerAPIListener()
@@ -650,18 +661,8 @@ func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
// UpdateStatus implements ipnstate.StatusUpdater.
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
b.e.UpdateStatus(sb)
var extraLocked func(*ipnstate.StatusBuilder)
if sb.WantPeers {
extraLocked = b.populatePeerStatusLocked
}
b.updateStatus(sb, extraLocked)
}
b.e.UpdateStatus(sb) // does wireguard + magicsock status
// updateStatus populates sb with status.
//
// extraLocked, if non-nil, is called while b.mu is still held.
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -721,8 +722,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
var tailscaleIPs []netip.Addr
if b.netMap != nil {
for _, addr := range b.netMap.Addresses {
if addr.IsSingleIP() {
addrs := b.netMap.GetAddresses()
for i := range addrs.LenIter() {
if addr := addrs.At(i); addr.IsSingleIP() {
sb.AddTailscaleIP(addr.Addr())
tailscaleIPs = append(tailscaleIPs, addr.Addr())
}
@@ -744,6 +746,13 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
if c := sn.Capabilities(); c.Len() > 0 {
ss.Capabilities = c.AsSlice()
}
if cm := sn.CapMap(); cm.Len() > 0 {
ss.CapMap = make(tailcfg.NodeCapMap, sn.CapMap().Len())
cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
ss.CapMap[k] = v.AsSlice()
return true
})
}
}
for _, addr := range tailscaleIPs {
ss.TailscaleIPs = append(ss.TailscaleIPs, addr)
@@ -759,8 +768,8 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
// TODO: hostinfo, and its networkinfo
// TODO: EngineStatus copy (and deprecate it?)
if extraLocked != nil {
extraLocked(sb)
if sb.WantPeers {
b.populatePeerStatusLocked(sb)
}
}
@@ -772,7 +781,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
sb.AddUser(id, up)
}
exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
for _, p := range b.netMap.Peers {
for _, p := range b.peers {
var lastSeen time.Time
if p.LastSeen() != nil {
lastSeen = *p.LastSeen()
@@ -845,20 +854,24 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.
var zero tailcfg.NodeView
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ipp.Addr()]
nid, ok := b.nodeByAddr[ipp.Addr()]
if !ok {
var ip netip.Addr
if ipp.Port() != 0 {
ip, ok = b.e.WhoIsIPPort(ipp)
ip, ok = b.sys.ProxyMapper().WhoIsIPPort(ipp)
}
if !ok {
return zero, u, false
}
n, ok = b.nodeByAddr[ip]
nid, ok = b.nodeByAddr[ip]
if !ok {
return zero, u, false
}
}
n, ok = b.peers[nid]
if !ok {
return zero, u, false
}
u, ok = b.netMap.UserProfiles[n.User()]
if !ok {
return zero, u, false
@@ -882,7 +895,9 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
if filt == nil {
return nil
}
for _, a := range b.netMap.Addresses {
addrs := b.netMap.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
if !a.IsSingleIP() {
continue
}
@@ -1030,7 +1045,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
// Perform all mutations of prefs based on the netmap here.
if prefsChanged {
// Prefs will be written out if stale; this is not safe unless locked or cloned.
if err := b.pm.SetPrefs(prefs.View()); err != nil {
if err := b.pm.SetPrefs(prefs.View(), st.NetMap.MagicDNSSuffix()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
@@ -1081,7 +1096,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.mu.Lock()
prefs.WantRunning = false
p := prefs.View()
if err := b.pm.SetPrefs(p); err != nil {
if err := b.pm.SetPrefs(p, st.NetMap.MagicDNSSuffix()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
b.mu.Unlock()
@@ -1125,40 +1140,79 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
return false
}
var notify *ipn.Notify // non-nil if we need to send a Notify
defer func() {
if notify != nil {
b.send(*notify)
}
}()
b.mu.Lock()
defer b.mu.Unlock()
return b.updateNetmapDeltaLocked(muts)
if !b.updateNetmapDeltaLocked(muts) {
return false
}
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
nm := ptr.To(*b.netMap) // shallow clone
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
for _, p := range b.peers {
nm.Peers = append(nm.Peers, p)
}
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
})
notify = &ipn.Notify{NetMap: nm}
} else if testenv.InTest() {
// In tests, send an empty Notify as a wake-up so end-to-end
// integration tests in another repo can check on the status of
// LocalBackend after processing deltas.
notify = new(ipn.Notify)
}
return true
}
// mutationsAreWorthyOfTellingIPNBus reports whether any mutation type in muts is
// worthy of spamming the IPN bus (the Windows & Mac GUIs, basically) to tell them
// about the update.
func mutationsAreWorthyOfTellingIPNBus(muts []netmap.NodeMutation) bool {
for _, m := range muts {
switch m.(type) {
case netmap.NodeMutationLastSeen,
netmap.NodeMutationOnline:
// The GUI clients might render peers differently depending on whether
// they're online.
return true
}
}
return false
}
func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (handled bool) {
if b.netMap == nil {
if b.netMap == nil || len(b.peers) == 0 {
return false
}
peers := b.netMap.Peers
// Locally cloned mutable nodes, to avoid calling AsStruct (clone)
// multiple times on a node if it's mutated multiple times in this
// call (e.g. its endpoints + online status both change)
var mutableNodes map[tailcfg.NodeID]*tailcfg.Node
for _, m := range muts {
// LocalBackend only cares about some types of mutations.
// (magicsock cares about different ones.)
switch m.(type) {
case netmap.NodeMutationOnline, netmap.NodeMutationLastSeen:
default:
continue
n, ok := mutableNodes[m.NodeIDBeingMutated()]
if !ok {
nv, ok := b.peers[m.NodeIDBeingMutated()]
if !ok {
// TODO(bradfitz): unexpected metric?
return false
}
n = nv.AsStruct()
mak.Set(&mutableNodes, nv.ID(), n)
}
nodeID := m.NodeIDBeingMutated()
idx := b.netMap.PeerIndexByNodeID(nodeID)
if idx == -1 {
continue
}
mut := peers[idx].AsStruct()
switch m := m.(type) {
case netmap.NodeMutationOnline:
mut.Online = ptr.To(m.Online)
case netmap.NodeMutationLastSeen:
mut.LastSeen = ptr.To(m.LastSeen)
}
peers[idx] = mut.View()
m.Apply(n)
}
for nid, n := range mutableNodes {
b.peers[nid] = n.View()
}
return true
}
@@ -1289,6 +1343,27 @@ func (b *LocalBackend) SetControlClientGetterForTesting(newControlClient func(co
b.ccGen = newControlClient
}
// NodeViewByIDForTest returns the state of the node with the given ID
// for integration tests in another repo.
func (b *LocalBackend) NodeViewByIDForTest(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok := b.peers[id]
return n, ok
}
// PeersForTest returns all the current peers, sorted by Node.ID,
// for integration tests in another repo.
func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
b.mu.Lock()
defer b.mu.Unlock()
ret := xmaps.Values(b.peers)
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
})
return ret
}
func (b *LocalBackend) getNewControlClientFunc() clientGen {
b.mu.Lock()
defer b.mu.Unlock()
@@ -1416,7 +1491,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
newPrefs := opts.UpdatePrefs.Clone()
newPrefs.Persist = oldPrefs.Persist().AsStruct()
pv := newPrefs.View()
if err := b.pm.SetPrefs(pv); err != nil {
if err := b.pm.SetPrefs(pv, b.netMap.MagicDNSSuffix()); err != nil {
b.logf("failed to save UpdatePrefs state: %v", err)
}
b.setAtomicValuesFromPrefsLocked(pv)
@@ -1576,7 +1651,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
// quite hard to debug, so save yourself the trouble.
var (
haveNetmap = netMap != nil
addrs []netip.Prefix
addrs views.Slice[netip.Prefix]
packetFilter []filter.Match
localNetsB netipx.IPSetBuilder
logNetsB netipx.IPSetBuilder
@@ -1587,13 +1662,13 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
logNetsB.AddPrefix(tsaddr.TailscaleULARange())
logNetsB.RemovePrefix(tsaddr.ChromeOSVMRange())
if haveNetmap {
addrs = netMap.Addresses
for _, p := range addrs {
localNetsB.AddPrefix(p)
addrs = netMap.GetAddresses()
for i := range addrs.LenIter() {
localNetsB.AddPrefix(addrs.At(i))
}
packetFilter = netMap.PacketFilter
if packetFilterPermitsUnlockedNodes(netMap.Peers, packetFilter) {
if packetFilterPermitsUnlockedNodes(b.peers, packetFilter) {
err := errors.New("server sent invalid packet filter permitting traffic to unlocked nodes; rejecting all packets for safety")
warnInvalidUnsignedNodes.Set(err)
packetFilter = nil
@@ -1641,7 +1716,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
changed := deephash.Update(&b.filterHash, &struct {
HaveNetmap bool
Addrs []netip.Prefix
Addrs views.Slice[netip.Prefix]
FilterMatch []filter.Match
LocalNets []netipx.IPRange
LogNets []netipx.IPRange
@@ -1678,7 +1753,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
//
// If this reports true, the packet filter is invalid (the server is either broken
// or malicious) and should be ignored for safety.
func packetFilterPermitsUnlockedNodes(peers []tailcfg.NodeView, packetFilter []filter.Match) bool {
func packetFilterPermitsUnlockedNodes(peers map[tailcfg.NodeID]tailcfg.NodeView, packetFilter []filter.Match) bool {
var b netipx.IPSetBuilder
var numUnlocked int
for _, p := range peers {
@@ -1843,53 +1918,6 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet,
return b.IPSet()
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
// for DNS map construction purposes (that is, only the first entry counts).
func dnsCIDRsEqual(newAddr, oldAddr views.Slice[netip.Prefix]) bool {
if newAddr.Len() != oldAddr.Len() {
return false
}
if newAddr.Len() == 0 || newAddr.At(0) == oldAddr.At(0) {
return true
}
return false
}
// dnsMapsEqual determines whether the new and the old network map
// induce the same DNS map. It does so without allocating memory,
// at the expense of giving false negatives if peers are reordered.
func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
if (old == nil) != (new == nil) {
return false
}
if old == nil && new == nil {
return true
}
if len(new.Peers) != len(old.Peers) {
return false
}
if new.Name != old.Name {
return false
}
if !dnsCIDRsEqual(views.SliceOf(new.Addresses), views.SliceOf(old.Addresses)) {
return false
}
for i, newPeer := range new.Peers {
oldPeer := old.Peers[i]
if newPeer.Name() != oldPeer.Name() {
return false
}
if !dnsCIDRsEqual(newPeer.Addresses(), oldPeer.Addresses()) {
return false
}
}
return true
}
// readPoller is a goroutine that receives service lists from
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
@@ -2337,7 +2365,7 @@ func (b *LocalBackend) migrateStateLocked(prefs *ipn.Prefs) (err error) {
// Backend owns the state, but frontend is trying to migrate
// state into the backend.
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
if err := b.pm.SetPrefs(prefs.View()); err != nil {
if err := b.pm.SetPrefs(prefs.View(), b.netMap.MagicDNSSuffix()); err != nil {
return fmt.Errorf("store.WriteState: %v", err)
}
}
@@ -2390,13 +2418,13 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD())
if !p.Valid() {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
b.containsViaIPFuncAtomic.Store(tsaddr.FalseContainsIPFunc())
b.setTCPPortsIntercepted(nil)
b.lastServeConfJSON = mem.B(nil)
b.serveConfig = ipn.ServeConfigView{}
} else {
filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix)
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered))
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(views.SliceOf(filtered)))
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
}
}
@@ -2885,7 +2913,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
}
prefs := newp.View()
if err := b.pm.SetPrefs(prefs); err != nil {
if err := b.pm.SetPrefs(prefs, b.netMap.MagicDNSSuffix()); err != nil {
b.logf("failed to save new controlclient state: %v", err)
}
b.lastProfileID = b.pm.CurrentProfile().ID
@@ -2950,7 +2978,7 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
nm := b.NetMap()
return nm != nil && slices.Contains(nm.Addresses, netip.PrefixFrom(ip, ip.BitLen()))
return nm != nil && views.SliceContains(nm.GetAddresses(), netip.PrefixFrom(ip, ip.BitLen()))
}
var (
@@ -3084,6 +3112,8 @@ func (b *LocalBackend) authReconfig() {
nm := b.netMap
hasPAC := b.prevIfState.HasPAC()
disableSubnetsIfPAC := hasCapability(nm, tailcfg.NodeAttrDisableSubnetsIfPAC)
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS())
b.mu.Unlock()
if blocked {
@@ -3116,7 +3146,7 @@ func (b *LocalBackend) authReconfig() {
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
if dohURLOK {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
@@ -3130,7 +3160,6 @@ func (b *LocalBackend) authReconfig() {
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.ControlKnobs(), version.OS())
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
dcfg := dnsConfigForNetmap(nm, prefs, b.logf, version.OS())
err = b.e.Reconfig(cfg, rcfg, dcfg)
if err == wgengine.ErrNoChanges {
@@ -3179,15 +3208,18 @@ func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs,
//
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
// a runtime.GOOS.
func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config {
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config {
if nm == nil {
return nil
}
dcfg := &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{},
}
// selfV6Only is whether we only have IPv6 addresses ourselves.
selfV6Only := slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
!slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
selfV6Only := views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs6) &&
!views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs4)
dcfg.OnlyIPv6 = selfV6Only
// Populate MagicDNS records. We do this unconditionally so that
@@ -3234,8 +3266,8 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
}
dcfg.Hosts[fqdn] = ips
}
set(nm.Name, views.SliceOf(nm.Addresses))
for _, peer := range nm.Peers {
set(nm.Name, nm.GetAddresses())
for _, peer := range peers {
set(peer.Name(), peer.Addresses())
}
for _, rec := range nm.DNS.ExtraRecords {
@@ -3283,11 +3315,18 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it.
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
return dcfg
}
// If we're using an exit node and that exit node is IsWireGuardOnly with
// ExitNodeDNSResolver set, then add that as the default.
if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
addDefault(resolvers)
return dcfg
}
addDefault(nm.DNS.Resolvers)
for suffix, resolvers := range nm.DNS.Routes {
fqdn, err := dnsname.ToFQDN(suffix)
@@ -3449,10 +3488,11 @@ func (b *LocalBackend) initPeerAPIListener() {
return
}
if len(b.netMap.Addresses) == len(b.peerAPIListeners) {
addrs := b.netMap.GetAddresses()
if addrs.Len() == len(b.peerAPIListeners) {
allSame := true
for i, pln := range b.peerAPIListeners {
if pln.ip != b.netMap.Addresses[i].Addr() {
if pln.ip != addrs.At(i).Addr() {
allSame = false
break
}
@@ -3466,7 +3506,7 @@ func (b *LocalBackend) initPeerAPIListener() {
b.closePeerAPIListenersLocked()
selfNode := b.netMap.SelfNode
if len(b.netMap.Addresses) == 0 || !selfNode.Valid() {
if !selfNode.Valid() || b.netMap.GetAddresses().Len() == 0 {
return
}
@@ -3487,7 +3527,8 @@ func (b *LocalBackend) initPeerAPIListener() {
b.peerAPIServer = ps
isNetstack := b.sys.IsNetstack()
for i, a := range b.netMap.Addresses {
for i := range addrs.LenIter() {
a := addrs.At(i)
var ln net.Listener
var err error
skipListen := i > 0 && isNetstack
@@ -3771,11 +3812,12 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
// Needed so that UpdateEndpoints can run
b.e.RequestStatus()
case ipn.Running:
var addrs []string
for _, addr := range netMap.Addresses {
addrs = append(addrs, addr.Addr().String())
var addrStrs []string
addrs := netMap.GetAddresses()
for i := range addrs.LenIter() {
addrStrs = append(addrStrs, addrs.At(i).Addr().String())
}
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrStrs, " "))
case ipn.NoState:
// Do nothing.
default:
@@ -3862,7 +3904,7 @@ func (b *LocalBackend) nextStateLocked() ipn.State {
// NetMap must be non-nil for us to get here.
// The node key expired, need to relogin.
return ipn.NeedsLogin
case netMap.MachineStatus != tailcfg.MachineAuthorized:
case netMap.GetMachineStatus() != tailcfg.MachineAuthorized:
// TODO(crawshaw): handle tailcfg.MachineInvalid
return ipn.NeedsMachineAuth
case state == ipn.NeedsMachineAuth:
@@ -4058,9 +4100,9 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
cc.SetNetInfo(ni)
}
func hasCapability(nm *netmap.NetworkMap, cap string) bool {
if nm != nil && nm.SelfNode.Valid() {
return views.SliceContains(nm.SelfNode.Capabilities(), cap)
func hasCapability(nm *netmap.NetworkMap, cap tailcfg.NodeCapability) bool {
if nm != nil {
return nm.SelfNode.HasCap(cap)
}
return false
}
@@ -4078,6 +4120,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
login = cmpx.Or(nm.UserProfiles[nm.User()].LoginName, "<missing-profile>")
}
b.netMap = nm
b.updatePeersFromNetmapLocked(nm)
if login != b.activeLogin {
b.logf("active login: %v", login)
b.activeLogin = login
@@ -4112,16 +4155,16 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
// Update the nodeByAddr index.
if b.nodeByAddr == nil {
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeID{}
}
// First pass, mark everything unwanted.
for k := range b.nodeByAddr {
b.nodeByAddr[k] = tailcfg.NodeView{}
b.nodeByAddr[k] = 0
}
addNode := func(n tailcfg.NodeView) {
for i := range n.Addresses().LenIter() {
if ipp := n.Addresses().At(i); ipp.IsSingleIP() {
b.nodeByAddr[ipp.Addr()] = n
b.nodeByAddr[ipp.Addr()] = n.ID()
}
}
}
@@ -4133,12 +4176,33 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
// Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr {
if !v.Valid() {
if v == 0 {
delete(b.nodeByAddr, k)
}
}
}
func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
if nm == nil {
b.peers = nil
return
}
// First pass, mark everything unwanted.
for k := range b.peers {
b.peers[k] = tailcfg.NodeView{}
}
// Second pass, add everything wanted.
for _, p := range nm.Peers {
mak.Set(&b.peers, p.ID(), p)
}
// Third pass, remove deleted things.
for k, v := range b.peers {
if !v.Valid() {
delete(b.peers, k)
}
}
}
// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
// capabilities in the provided NetMap.
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
@@ -4412,7 +4476,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
if !b.capFileSharing {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
for _, p := range b.peers {
if !b.peerIsTaildropTargetLocked(p) {
continue
}
@@ -4425,7 +4489,9 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
PeerAPIURL: peerAPI,
})
}
// TODO: sort a different way than the netmap already is?
slices.SortFunc(ret, func(a, b *apitype.FileTarget) int {
return cmpx.Compare(a.Node.Name, b.Node.Name)
})
return ret, nil
}
@@ -4536,7 +4602,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
}
var have4, have6 bool
for _, a := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
if !a.IsSingleIP() {
continue
}
@@ -4664,11 +4732,11 @@ func (b *LocalBackend) SetExpirySooner(ctx context.Context, expiry time.Time) er
// to exitNodeID's DoH service, if available.
//
// If exitNodeID is the zero valid, it returns "", false.
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
if exitNodeID.IsZero() {
return "", false
}
for _, p := range nm.Peers {
for _, p := range peers {
if p.StableID() == exitNodeID && peerCanProxyDNS(p) {
return peerAPIBase(nm, p) + "/dns-query", true
}
@@ -4676,6 +4744,30 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
return "", false
}
// wireguardExitNodeDNSResolvers returns the DNS resolvers to use for a
// WireGuard-only exit node, if it has resolver addresses.
func wireguardExitNodeDNSResolvers(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, exitNodeID tailcfg.StableNodeID) ([]*dnstype.Resolver, bool) {
if exitNodeID.IsZero() {
return nil, false
}
for _, p := range peers {
if p.StableID() == exitNodeID && p.IsWireGuardOnly() {
resolvers := p.ExitNodeDNSResolvers()
if !resolvers.IsNil() && resolvers.Len() > 0 {
copies := make([]*dnstype.Resolver, resolvers.Len())
for i := range resolvers.LenIter() {
copies[i] = resolvers.At(i).AsStruct()
}
return copies, true
}
return nil, false
}
}
return nil, false
}
func peerCanProxyDNS(p tailcfg.NodeView) bool {
if p.Cap() >= 26 {
// Actually added at 25
@@ -4864,13 +4956,14 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
io.WriteString(w, "No netmap.\n")
return
}
if len(b.netMap.Addresses) == 0 {
addrs := b.netMap.GetAddresses()
if addrs.Len() == 0 {
io.WriteString(w, "No local addresses.\n")
return
}
io.WriteString(w, "<p>Local addresses:</p><ul>\n")
for _, ipp := range b.netMap.Addresses {
fmt.Fprintf(w, "<li>%v</li>\n", ipp.Addr())
for i := range addrs.LenIter() {
fmt.Fprintf(w, "<li>%v</li>\n", addrs.At(i).Addr())
}
io.WriteString(w, "</ul>\n")
}

View File

@@ -22,6 +22,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
@@ -33,113 +34,6 @@ import (
"tailscale.com/wgengine/wgcfg"
)
func TestNetworkMapCompare(t *testing.T) {
prefix1, err := netip.ParsePrefix("192.168.0.0/24")
if err != nil {
t.Fatal(err)
}
node1 := &tailcfg.Node{Addresses: []netip.Prefix{prefix1}}
prefix2, err := netip.ParsePrefix("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}
node2 := &tailcfg.Node{Addresses: []netip.Prefix{prefix2}}
tests := []struct {
name string
a, b *netmap.NetworkMap
want bool
}{
{
"both nil",
nil,
nil,
true,
},
{
"b nil",
&netmap.NetworkMap{},
nil,
false,
},
{
"a nil",
nil,
&netmap.NetworkMap{},
false,
},
{
"both default",
&netmap.NetworkMap{},
&netmap.NetworkMap{},
true,
},
{
"names identical",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map1"},
true,
},
{
"names differ",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map2"},
false,
},
{
"Peers identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
true,
},
{
"Peer list length",
// length of Peers list differs
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
false,
},
{
"Node names identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
true,
},
{
"Node names differ",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "B"}})},
false,
},
{
"Node lists identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
true,
},
{
"Node lists differ",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node2})},
false,
},
{
"Node Users differ",
// User field is not checked.
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 0}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 1}})},
true,
},
}
for _, tt := range tests {
got := dnsMapsEqual(tt.a, tt.b)
if got != tt.want {
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
}
}
}
func inRemove(ip netip.Addr) bool {
for _, pfx := range removeFromDefaultRoute {
if pfx.Contains(ip) {
@@ -385,9 +279,11 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_only_4_them_both",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -406,9 +302,11 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_only_6_them_both",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -427,10 +325,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_only_4",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -448,10 +348,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_only_6",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -469,10 +371,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_no_peerapi_service",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -760,7 +664,7 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := packetFilterPermitsUnlockedNodes(nodeViews(tt.peers), tt.filter); got != tt.want {
if got := packetFilterPermitsUnlockedNodes(peersMap(nodeViews(tt.peers)), tt.filter); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
@@ -795,10 +699,9 @@ func TestStatusWithoutPeers(t *testing.T) {
b.Start(ipn.Options{})
b.Login(nil)
cc.send(nil, "", false, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
Addresses: ipps("100.101.101.101"),
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
MachineAuthorized: true,
Addresses: ipps("100.101.101.101"),
}).View(),
})
got := b.StatusWithoutPeers()
@@ -892,6 +795,7 @@ func TestUpdateNetmapDelta(t *testing.T) {
for i := 0; i < 5; i++ {
b.netMap.Peers = append(b.netMap.Peers, (&tailcfg.Node{ID: (tailcfg.NodeID(i) + 1)}).View())
}
b.updatePeersFromNetmapLocked(b.netMap)
someTime := time.Unix(123, 0)
muts, ok := netmap.MutationsFromMapResponse(&tailcfg.MapResponse{
@@ -925,7 +829,7 @@ func TestUpdateNetmapDelta(t *testing.T) {
wants := []*tailcfg.Node{
{
ID: 1,
DERP: "", // unmodified by the delta
DERP: "127.3.3.40:1",
},
{
ID: 2,
@@ -941,14 +845,120 @@ func TestUpdateNetmapDelta(t *testing.T) {
},
}
for _, want := range wants {
idx := b.netMap.PeerIndexByNodeID(want.ID)
if idx == -1 {
t.Errorf("ID %v not found in netmap", want.ID)
gotv, ok := b.peers[want.ID]
if !ok {
t.Errorf("netmap.Peer %v missing from b.peers", want.ID)
continue
}
got := b.netMap.Peers[idx].AsStruct()
got := gotv.AsStruct()
if !reflect.DeepEqual(got, want) {
t.Errorf("netmap.Peer %v wrong.\n got: %v\nwant: %v", want.ID, logger.AsJSON(got), logger.AsJSON(want))
}
}
}
func TestWireguardExitNodeDNSResolvers(t *testing.T) {
type tc struct {
name string
id tailcfg.StableNodeID
peers []*tailcfg.Node
wantOK bool
wantResolvers []*dnstype.Resolver
}
tests := []tc{
{
name: "no peers",
id: "1",
wantOK: false,
wantResolvers: nil,
},
{
name: "non wireguard peer",
id: "1",
peers: []*tailcfg.Node{
{
ID: 1,
StableID: "1",
IsWireGuardOnly: false,
ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.example.com"}},
},
},
wantOK: false,
wantResolvers: nil,
},
{
name: "no matching IDs",
id: "2",
peers: []*tailcfg.Node{
{
ID: 1,
StableID: "1",
IsWireGuardOnly: true,
ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.example.com"}},
},
},
wantOK: false,
wantResolvers: nil,
},
{
name: "wireguard peer",
id: "1",
peers: []*tailcfg.Node{
{
ID: 1,
StableID: "1",
IsWireGuardOnly: true,
ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.example.com"}},
},
},
wantOK: true,
wantResolvers: []*dnstype.Resolver{{Addr: "dns.example.com"}},
},
}
for _, tc := range tests {
peers := peersMap(nodeViews(tc.peers))
nm := &netmap.NetworkMap{}
gotResolvers, gotOK := wireguardExitNodeDNSResolvers(nm, peers, tc.id)
if gotOK != tc.wantOK || !resolversEqual(gotResolvers, tc.wantResolvers) {
t.Errorf("case: %s: got %v, %v, want %v, %v", tc.name, gotOK, gotResolvers, tc.wantOK, tc.wantResolvers)
}
}
}
func TestDNSConfigForNetmapForWireguardExitNode(t *testing.T) {
resolvers := []*dnstype.Resolver{{Addr: "dns.example.com"}}
nm := &netmap.NetworkMap{}
peers := map[tailcfg.NodeID]tailcfg.NodeView{
1: (&tailcfg.Node{
ID: 1,
StableID: "1",
IsWireGuardOnly: true,
ExitNodeDNSResolvers: resolvers,
Hostinfo: (&tailcfg.Hostinfo{}).View(),
}).View(),
}
prefs := &ipn.Prefs{
ExitNodeID: "1",
CorpDNS: true,
}
got := dnsConfigForNetmap(nm, peers, prefs.View(), t.Logf, "")
if !resolversEqual(got.DefaultResolvers, resolvers) {
t.Errorf("got %v, want %v", got.DefaultResolvers, resolvers)
}
}
func resolversEqual(a, b []*dnstype.Resolver) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].Equal(b[i]) {
return false
}
}
return true
}

View File

@@ -578,7 +578,7 @@ func (b *LocalBackend) NetworkLockForceLocalDisable() error {
newPrefs := b.pm.CurrentPrefs().AsStruct().Clone() // .Persist should always be initialized here.
newPrefs.Persist.DisallowedTKAStateIDs = append(newPrefs.Persist.DisallowedTKAStateIDs, stateID)
if err := b.pm.SetPrefs(newPrefs.View()); err != nil {
if err := b.pm.SetPrefs(newPrefs.View(), b.netMap.MagicDNSSuffix()); err != nil {
return fmt.Errorf("saving prefs: %w", err)
}

View File

@@ -151,7 +151,7 @@ func TestTKAEnablementFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
b := LocalBackend{
capTailnetLock: true,
varRoot: temp,
@@ -191,7 +191,7 @@ func TestTKADisablementFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -383,7 +383,7 @@ func TestTKASync(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
@@ -605,7 +605,7 @@ func TestTKADisable(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -696,7 +696,7 @@ func TestTKASign(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -785,7 +785,7 @@ func TestTKAForceDisable(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -880,7 +880,7 @@ func TestTKAAffectedSigs(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -1013,7 +1013,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -1104,7 +1104,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: cosignPriv,
},
}).View()))
}).View(), ""))
b := LocalBackend{
varRoot: temp,
logf: t.Logf,

View File

@@ -1035,7 +1035,7 @@ func (h *peerAPIHandler) canPutFile() bool {
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
func (h *peerAPIHandler) canDebug() bool {
if !views.SliceContains(h.selfNode.Capabilities(), tailcfg.CapabilityDebug) {
if !h.selfNode.HasCap(tailcfg.CapabilityDebug) {
// This node does not expose debug info.
return false
}

View File

@@ -660,7 +660,7 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
}).View())
}).View(), "")
if !h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly not offering exit node")
}

View File

@@ -206,7 +206,12 @@ func init() {
// SetPrefs sets the current profile's prefs to the provided value.
// It also saves the prefs to the StateStore. It stores a copy of the
// provided prefs, which may be accessed via CurrentPrefs.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
//
// If tailnetMagicDNSName is provided non-empty, it will be used to
// enrich the profile with the tailnet's MagicDNS name. The MagicDNS
// name cannot be pulled from prefsIn directly because it is not saved
// on ipn.Prefs (since it's not a field that is configurable by nodes).
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, tailnetMagicDNSName string) error {
prefs := prefsIn.AsStruct()
newPersist := prefs.Persist
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
@@ -250,6 +255,9 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
cp.ControlURL = prefs.ControlURL
cp.UserProfile = newPersist.UserProfile
cp.NodeID = newPersist.NodeID
if tailnetMagicDNSName != "" {
cp.TailnetMagicDNSName = tailnetMagicDNSName
}
pm.knownProfiles[cp.ID] = cp
pm.currentProfile = cp
if err := pm.writeKnownProfiles(); err != nil {
@@ -589,7 +597,7 @@ func (pm *profileManager) migrateFromLegacyPrefs() error {
return fmt.Errorf("load legacy prefs: %w", err)
}
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
if err := pm.SetPrefs(prefs); err != nil {
if err := pm.SetPrefs(prefs, ""); err != nil {
metricMigrationError.Add(1)
return fmt.Errorf("migrating _daemon profile: %w", err)
}

View File

@@ -41,7 +41,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
LoginName: loginName,
},
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -96,7 +96,7 @@ func TestProfileList(t *testing.T) {
LoginName: loginName,
},
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -157,7 +157,7 @@ func TestProfileDupe(t *testing.T) {
reauth := func(pm *profileManager, p *persist.Persist) {
prefs := ipn.NewPrefs()
prefs.Persist = p
must.Do(pm.SetPrefs(prefs.View()))
must.Do(pm.SetPrefs(prefs.View(), ""))
}
login := func(pm *profileManager, p *persist.Persist) {
pm.NewProfile()
@@ -379,7 +379,7 @@ func TestProfileManagement(t *testing.T) {
},
NodeID: nid,
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -506,7 +506,7 @@ func TestProfileManagementWindows(t *testing.T) {
},
NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))),
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()

View File

@@ -205,7 +205,9 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
return
}
for _, a := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
for _, p := range ports {
addrPort := netip.AddrPortFrom(a.Addr(), p)
if _, ok := b.serveListeners[addrPort]; ok {

View File

@@ -378,17 +378,23 @@ func newTestBackend(t *testing.T) *LocalBackend {
},
},
}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{
netip.MustParseAddr("100.150.151.152"): (&tailcfg.Node{
b.peers = map[tailcfg.NodeID]tailcfg.NodeView{
152: (&tailcfg.Node{
ID: 152,
ComputedName: "some-peer",
User: tailcfg.UserID(1),
}).View(),
netip.MustParseAddr("100.150.151.153"): (&tailcfg.Node{
153: (&tailcfg.Node{
ID: 153,
ComputedName: "some-tagged-peer",
Tags: []string{"tag:server", "tag:test"},
User: tailcfg.UserID(1),
}).View(),
}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeID{
netip.MustParseAddr("100.150.151.152"): 152,
netip.MustParseAddr("100.150.151.153"): 153,
}
return b
}

View File

@@ -501,7 +501,7 @@ func TestStateMachine(t *testing.T) {
// (ie. I suspect it would be better to change false->true in send()
// below, and do the same in the real controlclient.)
cc.send(nil, "", false, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -665,7 +665,7 @@ func TestStateMachine(t *testing.T) {
cc.persist.UserProfile.LoginName = "user2"
cc.persist.NodeID = "node2"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(3)
@@ -732,7 +732,7 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nStart4 -> netmap")
notifies.expect(0)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
notifies.drain(0)
@@ -801,7 +801,7 @@ func TestStateMachine(t *testing.T) {
cc.persist.UserProfile.LoginName = "user3"
cc.persist.NodeID = "node3"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(3)
@@ -845,7 +845,7 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nLoginFinished5")
notifies.expect(2)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(2)
@@ -862,8 +862,8 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nExpireKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(-time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
Expiry: time.Now().Add(-time.Minute),
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -877,8 +877,8 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nExtendKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
Expiry: time.Now().Add(time.Minute),
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -923,7 +923,7 @@ func TestEditPrefsHasNoKeys(t *testing.T) {
LegacyFrontendPrivateMachineKey: key.NewMachine(),
},
}).View())
}).View(), "")
if p := b.pm.CurrentPrefs().Persist(); !p.Valid() || p.PrivateNodeKey().IsZero() {
t.Fatalf("PrivateNodeKey not set")
}
@@ -1023,7 +1023,7 @@ func TestWGEngineStatusRace(t *testing.T) {
// Assert that we are logged in and authorized.
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
wantState(ipn.Starting)

View File

@@ -15,7 +15,6 @@ import (
"slices"
"sort"
"strings"
"sync"
"time"
"tailscale.com/tailcfg"
@@ -257,7 +256,10 @@ type PeerStatus struct {
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
// "funnel"
Capabilities []string `json:",omitempty"`
Capabilities []tailcfg.NodeCapability `json:",omitempty"`
// CapMap is a map of capabilities to their values.
CapMap tailcfg.NodeCapMap `json:",omitempty"`
// SSH_HostKeys are the node's SSH host keys, if known.
SSH_HostKeys []string `json:"sshHostKeys,omitempty"`
@@ -292,13 +294,17 @@ type PeerStatus struct {
Location *tailcfg.Location `json:",omitempty"`
}
// HasCap reports whether ps has the given capability.
func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
return ps.CapMap.Contains(cap) || slices.Contains(ps.Capabilities, cap)
}
// StatusBuilder is a request to construct a Status. A new StatusBuilder is
// passed to various subsystems which then call methods on it to populate state.
// Call its Status method to return the final constructed Status.
type StatusBuilder struct {
WantPeers bool // whether caller wants peers
mu sync.Mutex
locked bool
st Status
}
@@ -307,19 +313,13 @@ type StatusBuilder struct {
//
// It may not assume other fields of status are already populated, and
// may not retain or write to the Status after f returns.
//
// MutateStatus acquires a lock so f must not call back into sb.
func (sb *StatusBuilder) MutateStatus(f func(*Status)) {
sb.mu.Lock()
defer sb.mu.Unlock()
f(&sb.st)
}
// Status returns the status that has been built up so far from previous
// calls to MutateStatus, MutateSelfStatus, AddPeer, etc.
func (sb *StatusBuilder) Status() *Status {
sb.mu.Lock()
defer sb.mu.Unlock()
sb.locked = true
return &sb.st
}
@@ -331,8 +331,6 @@ func (sb *StatusBuilder) Status() *Status {
//
// MutateStatus acquires a lock so f must not call back into sb.
func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.st.Self == nil {
sb.st.Self = new(PeerStatus)
}
@@ -341,8 +339,6 @@ func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
// AddUser adds a user profile to the status.
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddUser after Locked")
return
@@ -357,8 +353,6 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
// AddIP adds a Tailscale IP address to the status.
func (sb *StatusBuilder) AddTailscaleIP(ip netip.Addr) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddIP after Locked")
return
@@ -375,8 +369,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
panic("nil PeerStatus")
}
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddPeer after Locked")
return

View File

@@ -755,6 +755,14 @@ type LoginProfile struct {
// It is filled in from the UserProfile.LoginName field.
Name string
// TailnetMagicDNSName is filled with the MagicDNS suffix for this
// profile's node (even if MagicDNS isn't necessarily in use).
// It will neither start nor end with a period.
//
// TailnetMagicDNSName is only filled from 2023-09-09 forward,
// and will only get backfilled when a profile is the current profile.
TailnetMagicDNSName string
// Key is the StateKey under which the profile is stored.
// It is assigned once at profile creation time and never changes.
Key StateKey

View File

@@ -44,6 +44,12 @@ type ServeConfig struct {
// of either the client or the LocalBackend does not expose ports
// that users are not aware of.
Foreground map[string]*ServeConfig `json:",omitempty"`
// ETag is the checksum of the serve config that's populated
// by the LocalClient through the HTTP ETag header during a
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
ETag string `json:"-"`
}
// HostPort is an SNI name and port number, joined by a colon.
@@ -234,7 +240,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for
// Funnel.
func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error {
if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
}
@@ -247,7 +253,7 @@ func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
// CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports.
func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error {
deny := func(allowedPorts string) error {
if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
@@ -256,7 +262,8 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
}
var portsStr string
for _, attr := range nodeAttrs {
if !strings.HasPrefix(attr, tailcfg.CapabilityFunnelPorts) {
attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue
}
u, err := url.Parse(attr)
@@ -268,7 +275,7 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
return deny("")
}
u.RawQuery = ""
if u.String() != tailcfg.CapabilityFunnelPorts {
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
return deny("")
}
}

View File

@@ -9,20 +9,21 @@ import (
)
func TestCheckFunnelAccess(t *testing.T) {
portAttr := "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c }
const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
tests := []struct {
port uint16
caps []string
caps []tailcfg.NodeCapability
wantErr bool
}{
{443, []string{portAttr}, true}, // No "funnel" attribute
{443, []string{portAttr, tailcfg.NodeAttrFunnel}, true},
{443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8321, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{8083, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8091, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{3000, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{443, caps(portAttr), true}, // No "funnel" attribute
{443, caps(portAttr, tailcfg.NodeAttrFunnel), true},
{443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
}
for _, tt := range tests {
err := CheckFunnelAccess(tt.port, tt.caps)

View File

@@ -182,14 +182,22 @@ func populate() {
addDoH("2620:fe::fe:10", "https://dns10.quad9.net/dns-query")
// Mullvad
addDoH("194.242.2.2", "https://doh.mullvad.net/dns-query")
addDoH("193.19.108.2", "https://doh.mullvad.net/dns-query")
addDoH("2a07:e340::2", "https://doh.mullvad.net/dns-query")
// Mullvad -Ads
addDoH("194.242.2.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("193.19.108.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.doh.mullvad.net/dns-query")
// See https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
// Mullvad (default)
addDoH("194.242.2.2", "https://dns.mullvad.net/dns-query")
addDoH("2a07:e340::2", "https://dns.mullvad.net/dns-query")
// Mullvad (adblock)
addDoH("194.242.2.3", "https://adblock.dns.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.dns.mullvad.net/dns-query")
// Mullvad (base)
addDoH("194.242.2.4", "https://base.dns.mullvad.net/dns-query")
addDoH("2a07:e340::4", "https://base.dns.mullvad.net/dns-query")
// Mullvad (extended)
addDoH("194.242.2.5", "https://extended.dns.mullvad.net/dns-query")
addDoH("2a07:e340::5", "https://extended.dns.mullvad.net/dns-query")
// Mullvad (all)
addDoH("194.242.2.9", "https://all.dns.mullvad.net/dns-query")
addDoH("2a07:e340::9", "https://all.dns.mullvad.net/dns-query")
// Wikimedia
addDoH(wikimediaDNSv4, "https://wikimedia-dns.org/dns-query")

View File

@@ -303,7 +303,7 @@ func (p *Pinger) Send(ctx context.Context, dest net.Addr, data []byte) (time.Dur
m := icmp.Message{
Type: icmpType,
Code: icmpType.Protocol(),
Code: 0,
Body: &icmp.Echo{
ID: int(p.id),
Seq: int(seq),

View File

@@ -161,6 +161,11 @@ type oncePrefix struct {
v netip.Prefix
}
// FalseContainsIPFunc is shorthand for NewContainsIPFunc(views.Slice[netip.Prefix]{}).
func FalseContainsIPFunc() func(ip netip.Addr) bool {
return func(ip netip.Addr) bool { return false }
}
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
//
// It's optimized for the cases of addrs being empty and addrs
@@ -168,20 +173,17 @@ type oncePrefix struct {
// one IPv6 address).
//
// Otherwise the implementation is somewhat slow.
func NewContainsIPFunc(addrs []netip.Prefix) func(ip netip.Addr) bool {
func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool {
// Specialize the three common cases: no address, just IPv4
// (or just IPv6), and both IPv4 and IPv6.
if len(addrs) == 0 {
if addrs.Len() == 0 {
return func(netip.Addr) bool { return false }
}
// If any addr is more than a single IP, then just do the slow
// linear thing until
// https://github.com/inetaf/netaddr/issues/139 is done.
for _, a := range addrs {
if a.IsSingleIP() {
continue
}
acopy := append([]netip.Prefix(nil), addrs...)
if views.SliceContainsFunc(addrs, func(p netip.Prefix) bool { return !p.IsSingleIP() }) {
acopy := addrs.AsSlice()
return func(ip netip.Addr) bool {
for _, a := range acopy {
if a.Contains(ip) {
@@ -192,18 +194,18 @@ func NewContainsIPFunc(addrs []netip.Prefix) func(ip netip.Addr) bool {
}
}
// Fast paths for 1 and 2 IPs:
if len(addrs) == 1 {
a := addrs[0]
if addrs.Len() == 1 {
a := addrs.At(0)
return func(ip netip.Addr) bool { return ip == a.Addr() }
}
if len(addrs) == 2 {
a, b := addrs[0], addrs[1]
if addrs.Len() == 2 {
a, b := addrs.At(0), addrs.At(1)
return func(ip netip.Addr) bool { return ip == a.Addr() || ip == b.Addr() }
}
// General case:
m := map[netip.Addr]bool{}
for _, a := range addrs {
m[a.Addr()] = true
for i := range addrs.LenIter() {
m[addrs.At(i).Addr()] = true
}
return func(ip netip.Addr) bool { return m[ip] }
}

View File

@@ -8,6 +8,7 @@ import (
"testing"
"tailscale.com/net/netaddr"
"tailscale.com/types/views"
)
func TestInCrostiniRange(t *testing.T) {
@@ -66,29 +67,29 @@ func TestCGNATRange(t *testing.T) {
}
func TestNewContainsIPFunc(t *testing.T) {
f := NewContainsIPFunc([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")})
f := NewContainsIPFunc(views.SliceOf([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}))
if f(netip.MustParseAddr("8.8.8.8")) {
t.Fatal("bad")
}
if !f(netip.MustParseAddr("10.1.2.3")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{netip.MustParsePrefix("10.1.2.3/32")})
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{netip.MustParsePrefix("10.1.2.3/32")}))
if !f(netip.MustParseAddr("10.1.2.3")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{
netip.MustParsePrefix("10.1.2.3/32"),
netip.MustParsePrefix("::2/128"),
})
}))
if !f(netip.MustParseAddr("::2")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{
netip.MustParsePrefix("10.1.2.3/32"),
netip.MustParsePrefix("10.1.2.4/32"),
netip.MustParsePrefix("::2/128"),
})
}))
if !f(netip.MustParseAddr("10.1.2.4")) {
t.Fatal("bad")
}

View File

@@ -35,14 +35,15 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
ret := make(dnsMap)
suffix := nm.MagicDNSSuffix()
have4 := false
if nm.Name != "" && len(nm.Addresses) > 0 {
ip := nm.Addresses[0].Addr()
addrs := nm.GetAddresses()
if nm.Name != "" && addrs.Len() > 0 {
ip := addrs.At(0).Addr()
ret[canonMapKey(nm.Name)] = ip
if dnsname.HasSuffix(nm.Name, suffix) {
ret[canonMapKey(dnsname.TrimSuffix(nm.Name, suffix))] = ip
}
for _, a := range nm.Addresses {
if a.Addr().Is4() {
for i := range addrs.LenIter() {
if addrs.At(i).Addr().Is4() {
have4 = true
}
}

View File

@@ -32,10 +32,12 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
}).View(),
},
want: dnsMap{
"foo": ip("100.102.103.104"),
@@ -46,10 +48,12 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self_and_peers",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
}).View(),
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
Name: "a.tailnet",
@@ -79,9 +83,11 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self_has_v6_only",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100::123/128"),
},
}).View(),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "a.tailnet",

72
proxymap/proxymap.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package proxymap contains a mapping table for ephemeral localhost ports used
// by tailscaled on behalf of remote Tailscale IPs for proxied connections.
package proxymap
import (
"net/netip"
"sync"
"time"
"tailscale.com/util/mak"
)
// Mapper tracks which localhost ip:ports correspond to which remote Tailscale
// IPs for connections proxied by tailscaled.
//
// This is then used (via the WhoIsIPPort method) by localhost applications to
// ask tailscaled (via the LocalAPI WhoIs method) the Tailscale identity that a
// given localhost:port corresponds to.
type Mapper struct {
mu sync.Mutex
m map[netip.AddrPort]netip.Addr
}
// RegisterIPPortIdentity registers a given node (identified by its
// Tailscale IP) as temporarily having the given IP:port for whois lookups.
// The IP:port is generally a localhost IP and an ephemeral port, used
// while proxying connections to localhost when tailscaled is running
// in netstack mode.
func (m *Mapper) RegisterIPPortIdentity(ipport netip.AddrPort, tsIP netip.Addr) {
m.mu.Lock()
defer m.mu.Unlock()
mak.Set(&m.m, ipport, tsIP)
}
// UnregisterIPPortIdentity removes a temporary IP:port registration
// made previously by RegisterIPPortIdentity.
func (m *Mapper) UnregisterIPPortIdentity(ipport netip.AddrPort) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.m, ipport)
}
var whoIsSleeps = [...]time.Duration{
0,
10 * time.Millisecond,
20 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
}
// WhoIsIPPort looks up an IP:port in the temporary registrations,
// and returns a matching Tailscale IP, if it exists.
func (m *Mapper) WhoIsIPPort(ipport netip.AddrPort) (tsIP netip.Addr, ok bool) {
// We currently have a registration race,
// https://github.com/tailscale/tailscale/issues/1616,
// so loop a few times for now waiting for the registration
// to appear.
// TODO(bradfitz,namansood): remove this once #1616 is fixed.
for _, d := range whoIsSleeps {
time.Sleep(d)
m.mu.Lock()
tsIP, ok = m.m[ipport]
m.mu.Unlock()
if ok {
return tsIP, true
}
}
return tsIP, false
}

View File

@@ -12,9 +12,11 @@ import (
"fmt"
"net/netip"
"reflect"
"slices"
"strings"
"time"
"golang.org/x/exp/maps"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
@@ -111,7 +113,8 @@ type CapabilityVersion int
// - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
// - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
// - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
const CurrentCapabilityVersion CapabilityVersion = 73
// - 74: 2023-09-18: Client understands NodeCapMap
const CurrentCapabilityVersion CapabilityVersion = 74
type StableID string
@@ -215,6 +218,31 @@ func (emptyStructJSONSlice) MarshalJSON() ([]byte, error) {
func (emptyStructJSONSlice) UnmarshalJSON([]byte) error { return nil }
// RawMessage is a raw encoded JSON value. It implements Marshaler and
// Unmarshaler and can be used to delay JSON decoding or precompute a JSON
// encoding.
//
// It is like json.RawMessage but is a string instead of a []byte to better
// portray immutable data.
type RawMessage string
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == "" {
return []byte("null"), nil
}
return []byte(m), nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("RawMessage: UnmarshalJSON on nil pointer")
}
*m = RawMessage(data)
return nil
}
type Node struct {
ID NodeID
StableID StableNodeID
@@ -289,7 +317,21 @@ type Node struct {
// such as:
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
Capabilities []string `json:",omitempty"`
//
// Deprecated: use CapMap instead.
Capabilities []NodeCapability `json:",omitempty"`
// CapMap is a map of capabilities to their optional argument/data values.
//
// It is valid for a capability to not have any argument/data values; such
// capabilities can be tested for using the HasCap method. These type of
// capabilities are used to indicate that a node has a capability, but there
// is no additional data associated with it. These were previously
// represented by the Capabilities field, but can now be represented by
// CapMap with an empty value.
//
// See NodeCapability for more information on keys.
CapMap NodeCapMap `json:",omitempty"`
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
@@ -334,9 +376,24 @@ type Node struct {
// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it
// is not expected to speak Disco or DERP, and it must have Endpoints in
// order to be reachable. TODO(#7826): 2023-04-06: only the first parseable
// Endpoint is used, see #7826 for updates.
// order to be reachable.
IsWireGuardOnly bool `json:",omitempty"`
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
// node is marked IsWireGuardOnly and being used as an exit node.
ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
}
// HasCap reports whether the node has the given capability.
// It is safe to call on an invalid NodeView.
func (v NodeView) HasCap(cap NodeCapability) bool {
return v.ж.HasCap(cap)
}
// HasCap reports whether the node has the given capability.
// It is safe to call on a nil Node.
func (v *Node) HasCap(cap NodeCapability) bool {
return v != nil && (v.CapMap.Contains(cap) || slices.Contains(v.Capabilities, cap))
}
// DisplayName returns the user-facing name for a node which should
@@ -1223,10 +1280,11 @@ type CapGrant struct {
CapMap PeerCapMap `json:",omitempty"`
}
// PeerCapability is a capability granted to a node by a FilterRule.
// It's a string, but its meaning is application-defined.
// It must be a URL, like "https://tailscale.com/cap/file-sharing-target" or
// "https://example.com/cap/read-access".
// PeerCapability represents a capability granted to a peer by a FilterRule when
// the peer communicates with the node that has this rule. Its meaning is
// application-defined.
//
// It must be a URL like "https://tailscale.com/cap/file-send".
type PeerCapability string
const (
@@ -1245,13 +1303,52 @@ const (
PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress"
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the Contains method.
//
// See [NodeCapability] for more information on keys.
type NodeCapMap map[NodeCapability][]RawMessage
// Equal reports whether c and c2 are equal.
func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
return maps.EqualFunc(c, c2, slices.Equal)
}
// UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) {
vals, ok := cm[cap]
if !ok {
return nil, nil
}
out := make([]T, 0, len(vals))
for _, v := range vals {
var t T
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
// Contains reports whether c has the capability cap. This is used to test for
// the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c NodeCapMap) Contains(cap NodeCapability) bool {
_, ok := c[cap]
return ok
}
// PeerCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the HasCapability method.
//
// The values are opaque to Tailscale, but are passed through from the ACLs to
// the application via the WhoIs API.
type PeerCapMap map[PeerCapability][]json.RawMessage
type PeerCapMap map[PeerCapability][]RawMessage
// UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
@@ -1264,7 +1361,7 @@ func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
out := make([]T, 0, len(vals))
for _, v := range vals {
var t T
if err := json.Unmarshal(v, &t); err != nil {
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
@@ -1272,9 +1369,9 @@ func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
return out, nil
}
// HasCapability reports whether c has the capability cap.
// This is used to test for the existence of a capability, especially
// when the capability has no values.
// HasCapability reports whether c has the capability cap. This is used to test
// for the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c PeerCapMap) HasCapability(cap PeerCapability) bool {
_, ok := c[cap]
return ok
@@ -1835,7 +1932,8 @@ func (n *Node) Equal(n2 *Node) bool {
n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) &&
n.MachineAuthorized == n2.MachineAuthorized &&
eqStrings(n.Capabilities, n2.Capabilities) &&
slices.Equal(n.Capabilities, n2.Capabilities) &&
n.CapMap.Equal(n2.CapMap) &&
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
@@ -1908,112 +2006,117 @@ type Oauth2Token struct {
Expiry time.Time `json:"expiry,omitempty"`
}
const (
// These are the capabilities that the self node has as listed in
// MapResponse.Node.Capabilities.
//
// We've since started referring to these as "Node Attributes" ("nodeAttrs"
// in the ACL policy file).
// NodeCapability represents a capability granted to the self node as listed in
// MapResponse.Node.Capabilities.
//
// It must be a URL like "https://tailscale.com/cap/file-sharing", or a
// well-known capability name like "funnel". The latter is only allowed for
// Tailscale-defined capabilities.
//
// Unlike PeerCapability, NodeCapability is not in context of a peer and is
// granted to the node itself.
//
// These are also referred to as "Node Attributes" in the ACL policy file.
type NodeCapability string
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
CapabilityHTTPS = "https" // https cert provisioning enabled on tailnet
const (
CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin"
CapabilitySSH NodeCapability = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilitySSHRuleIn NodeCapability = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug NodeCapability = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
CapabilityHTTPS NodeCapability = "https" // https cert provisioning enabled on tailnet
// 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"
CapabilityBindToInterfaceByRoute NodeCapability = "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"
CapabilityDebugDisableAlternateDefaultRouteInterface NodeCapability = "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"
CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"
// CapabilityTailnetLock indicates the node may initialize tailnet lock.
CapabilityTailnetLock = "https://tailscale.com/cap/tailnet-lock"
CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock"
// Funnel warning capabilities used for reporting errors to the user.
// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
CapabilityWarnFunnelNoInvite = "https://tailscale.com/cap/warn-funnel-no-invite"
CapabilityWarnFunnelNoInvite NodeCapability = "https://tailscale.com/cap/warn-funnel-no-invite"
// CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
CapabilityWarnFunnelNoHTTPS = "https://tailscale.com/cap/warn-funnel-no-https"
CapabilityWarnFunnelNoHTTPS NodeCapability = "https://tailscale.com/cap/warn-funnel-no-https"
// Debug logging capabilities
// CapabilityDebugTSDNSResolution enables verbose debug logging for DNS
// resolution for Tailscale-controlled domains (the control server, log
// server, DERP servers, etc.)
CapabilityDebugTSDNSResolution = "https://tailscale.com/cap/debug-ts-dns-resolution"
CapabilityDebugTSDNSResolution NodeCapability = "https://tailscale.com/cap/debug-ts-dns-resolution"
// CapabilityFunnelPorts specifies the ports that the Funnel is available on.
// The ports are specified as a comma-separated list of port numbers or port
// ranges (e.g. "80,443,8080-8090") in the ports query parameter.
// e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090
CapabilityFunnelPorts = "https://tailscale.com/cap/funnel-ports"
)
CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports"
const (
// NodeAttrFunnel grants the ability for a node to host ingress traffic.
NodeAttrFunnel = "funnel"
NodeAttrFunnel NodeCapability = "funnel"
// NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions.
NodeAttrSSHAggregator = "ssh-aggregator"
NodeAttrSSHAggregator NodeCapability = "ssh-aggregator"
// NodeAttrDebugForceBackgroundSTUN forces a node to always do background
// STUN queries regardless of inactivity.
NodeAttrDebugForceBackgroundSTUN = "debug-always-stun"
NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun"
// NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration,
// always giving WireGuard the full netmap, even for idle peers.
NodeAttrDebugDisableWGTrim = "debug-no-wg-trim"
NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim"
// NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization.
// See Issue 150.
NodeAttrDebugDisableDRPO = "debug-disable-drpo"
NodeAttrDebugDisableDRPO NodeCapability = "debug-disable-drpo"
// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network.
NodeAttrDisableSubnetsIfPAC = "debug-disable-subnets-if-pac"
NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac"
// NodeAttrDisableUPnP makes the client not perform a UPnP portmapping.
// By default, we want to enable it to see if it works on more clients.
//
// If UPnP catastrophically fails for people, this should be set kill
// new attempts at UPnP connections.
NodeAttrDisableUPnP = "debug-disable-upnp"
NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp"
// NodeAttrDisableDeltaUpdates makes the client not process updates via the
// delta update mechanism and should instead treat all netmap changes as
// "full" ones as tailscaled did in 1.48.x and earlier.
NodeAttrDisableDeltaUpdates = "disable-delta-updates"
NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates"
// NodeAttrRandomizeClientPort makes magicsock UDP bind to
// :0 to get a random local port, ignoring any configured
// fixed port.
NodeAttrRandomizeClientPort = "randomize-client-port"
NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port"
// NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route
// rather than a /32 per peer. At most one of this or
// NodeAttrOneCGNATDisable may be set; if neither are, it's automatic.
NodeAttrOneCGNATEnable = "one-cgnat?v=true"
NodeAttrOneCGNATEnable NodeCapability = "one-cgnat?v=true"
// NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer
// rather than one big /10 CGNAT route. At most one of this or
// NodeAttrOneCGNATEnable may be set; if neither are, it's automatic.
NodeAttrOneCGNATDisable = "one-cgnat?v=false"
NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false"
)
// SetDNSRequest is a request to add a DNS record.
@@ -2405,6 +2508,9 @@ type PeerChange struct {
// Cap, if non-zero, means that NodeID's capability version has changed.
Cap CapabilityVersion `json:",omitempty"`
// CapMap, if non-nil, means that NodeID's capability map has changed.
CapMap NodeCapMap `json:",omitempty"`
// Endpoints, if non-empty, means that NodeID's UDP Endpoints
// have changed to these.
Endpoints []string `json:",omitempty"`
@@ -2431,7 +2537,7 @@ type PeerChange struct {
// Capabilities, if non-nil, means that the NodeID's capabilities changed.
// It's a pointer to a slice for "omitempty", to allow differentiating
// a change to empty from no change.
Capabilities *[]string `json:",omitempty"`
Capabilities *[]NodeCapability `json:",omitempty"`
}
// DerpMagicIP is a fake WireGuard endpoint IP address that means to

View File

@@ -62,9 +62,21 @@ func (src *Node) Clone() *Node {
dst.Online = ptr.To(*src.Online)
}
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
if dst.CapMap != nil {
dst.CapMap = map[NodeCapability][]RawMessage{}
for k := range src.CapMap {
dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...)
}
}
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
}
if src.ExitNodeDNSResolvers != nil {
dst.ExitNodeDNSResolvers = make([]*dnstype.Resolver, len(src.ExitNodeDNSResolvers))
for i := range dst.ExitNodeDNSResolvers {
dst.ExitNodeDNSResolvers[i] = src.ExitNodeDNSResolvers[i].Clone()
}
}
return dst
}
@@ -92,7 +104,8 @@ var _NodeCloneNeedsRegeneration = Node(struct {
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []string
Capabilities []NodeCapability
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -101,6 +114,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// Clone makes a deep copy of Hostinfo.
@@ -219,9 +233,11 @@ func (src *DNSConfig) Clone() *DNSConfig {
}
dst := new(DNSConfig)
*dst = *src
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers {
dst.Resolvers[i] = src.Resolvers[i].Clone()
if src.Resolvers != nil {
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers {
dst.Resolvers[i] = src.Resolvers[i].Clone()
}
}
if dst.Routes != nil {
dst.Routes = map[string][]*dnstype.Resolver{}
@@ -229,9 +245,11 @@ func (src *DNSConfig) Clone() *DNSConfig {
dst.Routes[k] = append([]*dnstype.Resolver{}, src.Routes[k]...)
}
}
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
for i := range dst.FallbackResolvers {
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
if src.FallbackResolvers != nil {
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
for i := range dst.FallbackResolvers {
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
}
}
dst.Domains = append(src.Domains[:0:0], src.Domains...)
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
@@ -365,9 +383,11 @@ func (src *DERPRegion) Clone() *DERPRegion {
}
dst := new(DERPRegion)
*dst = *src
dst.Nodes = make([]*DERPNode, len(src.Nodes))
for i := range dst.Nodes {
dst.Nodes[i] = src.Nodes[i].Clone()
if src.Nodes != nil {
dst.Nodes = make([]*DERPNode, len(src.Nodes))
for i := range dst.Nodes {
dst.Nodes[i] = src.Nodes[i].Clone()
}
}
return dst
}
@@ -444,9 +464,11 @@ func (src *SSHRule) Clone() *SSHRule {
if dst.RuleExpires != nil {
dst.RuleExpires = ptr.To(*src.RuleExpires)
}
dst.Principals = make([]*SSHPrincipal, len(src.Principals))
for i := range dst.Principals {
dst.Principals[i] = src.Principals[i].Clone()
if src.Principals != nil {
dst.Principals = make([]*SSHPrincipal, len(src.Principals))
for i := range dst.Principals {
dst.Principals[i] = src.Principals[i].Clone()
}
}
dst.SSHUsers = maps.Clone(src.SSHUsers)
dst.Action = src.Action.Clone()

View File

@@ -346,11 +346,11 @@ func TestNodeEqual(t *testing.T) {
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "Cap", "Tags", "PrimaryRoutes",
"LastSeen", "Online", "MachineAuthorized",
"Capabilities",
"Capabilities", "CapMap",
"UnsignedPeerAPIOnly",
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
"DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer",
"IsWireGuardOnly",
"IsWireGuardOnly", "ExitNodeDNSResolvers",
}
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
@@ -545,6 +545,45 @@ func TestNodeEqual(t *testing.T) {
&Node{SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))},
true,
},
{
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"foo"`},
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"foo"`},
},
},
true,
},
{
&Node{
CapMap: NodeCapMap{
"bar": []RawMessage{`"foo"`},
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"bar"`},
},
},
false,
},
{
&Node{
CapMap: NodeCapMap{
"foo": nil,
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"bar"`},
},
},
false,
},
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)
@@ -726,3 +765,82 @@ func TestUnmarshalHealth(t *testing.T) {
}
}
}
func TestRawMessage(t *testing.T) {
// Create a few types of json.RawMessages and then marshal them back and
// forth to make sure they round-trip.
type rule struct {
Ports []int `json:",omitempty"`
}
tests := []struct {
name string
val map[string][]rule
wire map[string][]RawMessage
}{
{
name: "nil",
val: nil,
wire: nil,
},
{
name: "empty",
val: map[string][]rule{},
wire: map[string][]RawMessage{},
},
{
name: "one",
val: map[string][]rule{
"foo": {{Ports: []int{1, 2, 3}}},
},
wire: map[string][]RawMessage{
"foo": {
`{"Ports":[1,2,3]}`,
},
},
},
{
name: "many",
val: map[string][]rule{
"foo": {{Ports: []int{1, 2, 3}}},
"bar": {{Ports: []int{4, 5, 6}}, {Ports: []int{7, 8, 9}}},
"baz": nil,
"abc": {},
"def": {{}},
},
wire: map[string][]RawMessage{
"foo": {
`{"Ports":[1,2,3]}`,
},
"bar": {
`{"Ports":[4,5,6]}`,
`{"Ports":[7,8,9]}`,
},
"baz": nil,
"abc": {},
"def": {"{}"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
j := must.Get(json.Marshal(tc.val))
var gotWire map[string][]RawMessage
if err := json.Unmarshal(j, &gotWire); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(gotWire, tc.wire) {
t.Errorf("got %#v; want %#v", gotWire, tc.wire)
}
j = must.Get(json.Marshal(tc.wire))
var gotVal map[string][]rule
if err := json.Unmarshal(j, &gotVal); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(gotVal, tc.val) {
t.Errorf("got %#v; want %#v", gotVal, tc.val)
}
})
}
}

View File

@@ -165,13 +165,19 @@ func (v NodeView) Online() *bool {
return &x
}
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
func (v NodeView) Expired() bool { return v.ж.Expired }
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) CapMap() views.MapFn[NodeCapability, []RawMessage, views.Slice[RawMessage]] {
return views.MapFnOf(v.ж.CapMap, func(t []RawMessage) views.Slice[RawMessage] {
return views.SliceOf(t)
})
}
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
func (v NodeView) Expired() bool { return v.ж.Expired }
func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
if v.ж.SelfNodeV4MasqAddrForThisPeer == nil {
return nil
@@ -180,7 +186,10 @@ func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
return &x
}
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
func (v NodeView) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers)
}
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -207,7 +216,8 @@ var _NodeViewNeedsRegeneration = Node(struct {
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []string
Capabilities []NodeCapability
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -216,6 +226,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// View returns a readonly view of Hostinfo.

View File

@@ -27,6 +27,7 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/proxymap"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
@@ -45,7 +46,9 @@ type System struct {
Tun SubSystem[*tstun.Wrapper]
StateStore SubSystem[ipn.StateStore]
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
controlKnobs controlknobs.Knobs
controlKnobs controlknobs.Knobs
proxyMap proxymap.Mapper
}
// NetstackImpl is the interface that *netstack.Impl implements.
@@ -103,6 +106,11 @@ func (s *System) ControlKnobs() *controlknobs.Knobs {
return &s.controlKnobs
}
// ProxyMapper returns the ephemeral ip:port mapper.
func (s *System) ProxyMapper() *proxymap.Mapper {
return &s.proxyMap
}
// SubSystem represents some subsystem of the Tailscale node daemon.
//
// A subsystem can be set to a value, and then later retrieved. A subsystem

View File

@@ -408,7 +408,9 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
if nm == nil {
return
}
for _, addr := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
addr := addrs.At(i)
ip := addr.Addr()
if ip.Is6() {
ip6 = ip
@@ -509,7 +511,7 @@ func (s *Server) start() (reterr error) {
closePool.add(s.dialer)
sys.Set(eng)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get())
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}

View File

@@ -585,7 +585,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.
AllowedIPs: allowedIPs,
Hostinfo: req.Hostinfo.View(),
Name: req.Hostinfo.Hostname,
Capabilities: []string{
Capabilities: []tailcfg.NodeCapability{
tailcfg.CapabilityHTTPS,
tailcfg.NodeAttrFunnel,
tailcfg.CapabilityFunnelPorts + "?ports=8080,443",

View File

@@ -8,6 +8,7 @@ package dnstype
import (
"net/netip"
"slices"
)
// Resolver is the configuration for one DNS resolver.
@@ -51,3 +52,15 @@ func (r *Resolver) IPPort() (ipp netip.AddrPort, ok bool) {
}
return
}
// Equal reports whether r and other are equal.
func (r *Resolver) Equal(other *Resolver) bool {
if r == nil || other == nil {
return r == other
}
if r == other {
return true
}
return r.Addr == other.Addr && slices.Equal(r.BootstrapResolution, other.BootstrapResolution)
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package dnstype
import (
"net/netip"
"reflect"
"slices"
"sort"
"testing"
)
func TestResolverEqual(t *testing.T) {
var fieldNames []string
for _, field := range reflect.VisibleFields(reflect.TypeOf(Resolver{})) {
fieldNames = append(fieldNames, field.Name)
}
sort.Strings(fieldNames)
if !slices.Equal(fieldNames, []string{"Addr", "BootstrapResolution"}) {
t.Errorf("Resolver fields changed; update test")
}
tests := []struct {
name string
a, b *Resolver
want bool
}{
{
name: "nil",
a: nil,
b: nil,
want: true,
},
{
name: "nil vs non-nil",
a: nil,
b: &Resolver{},
want: false,
},
{
name: "non-nil vs nil",
a: &Resolver{},
b: nil,
want: false,
},
{
name: "equal",
a: &Resolver{Addr: "dns.example.com"},
b: &Resolver{Addr: "dns.example.com"},
want: true,
},
{
name: "not equal addrs",
a: &Resolver{Addr: "dns.example.com"},
b: &Resolver{Addr: "dns2.example.com"},
want: false,
},
{
name: "not equal bootstrap",
a: &Resolver{
Addr: "dns.example.com",
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
},
b: &Resolver{
Addr: "dns.example.com",
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.4.4")},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.a.Equal(tt.b)
if got != tt.want {
t.Errorf("got %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -64,6 +64,7 @@ func (v ResolverView) Addr() string { return v.ж.Addr }
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
return views.SliceOf(v.ж.BootstrapResolution)
}
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ResolverViewNeedsRegeneration = Resolver(struct {

View File

@@ -33,16 +33,6 @@ type NetworkMap struct {
// It is the MapResponse.Node.Name value and ends with a period.
Name string
// Addresses is SelfNode.Addresses. (IP addresses of this Node directly)
//
// TODO(bradfitz): remove this field and make this a method.
Addresses []netip.Prefix
// MachineStatus is either tailcfg.MachineAuthorized or tailcfg.MachineUnauthorized,
// depending on SelfNode.MachineAuthorized.
// TODO(bradfitz): remove this field and make it a method.
MachineStatus tailcfg.MachineStatus
MachineKey key.MachinePublic
Peers []tailcfg.NodeView // sorted by Node.ID
@@ -96,6 +86,16 @@ func (nm *NetworkMap) User() tailcfg.UserID {
return 0
}
// GetAddresses returns the self node's addresses, or the zero value
// if SelfNode is invalid.
func (nm *NetworkMap) GetAddresses() views.Slice[netip.Prefix] {
var zero views.Slice[netip.Prefix]
if !nm.SelfNode.Valid() {
return zero
}
return nm.SelfNode.Addresses()
}
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
for _, p := range nm.Peers {
@@ -106,6 +106,17 @@ func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
return false
}
// GetMachineStatus returns the MachineStatus of the local node.
func (nm *NetworkMap) GetMachineStatus() tailcfg.MachineStatus {
if !nm.SelfNode.Valid() {
return tailcfg.MachineUnknown
}
if nm.SelfNode.MachineAuthorized() {
return tailcfg.MachineAuthorized
}
return tailcfg.MachineUnauthorized
}
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.
//
// If nm is nil or no peer is found, ok is false.
@@ -160,19 +171,27 @@ func MagicDNSSuffixOfNodeName(nodeName string) string {
//
// It will neither start nor end with a period.
func (nm *NetworkMap) MagicDNSSuffix() string {
if nm == nil {
return ""
}
return MagicDNSSuffixOfNodeName(nm.Name)
}
// SelfCapabilities returns SelfNode.Capabilities if nm and nm.SelfNode are
// non-nil. This is a method so we can use it in envknob/logknob without a
// circular dependency.
func (nm *NetworkMap) SelfCapabilities() views.Slice[string] {
var zero views.Slice[string]
func (nm *NetworkMap) SelfCapabilities() views.Slice[tailcfg.NodeCapability] {
var zero views.Slice[tailcfg.NodeCapability]
if nm == nil || !nm.SelfNode.Valid() {
return zero
}
out := nm.SelfNode.Capabilities().AsSlice()
nm.SelfNode.CapMap().Range(func(k tailcfg.NodeCapability, _ views.Slice[tailcfg.RawMessage]) (cont bool) {
out = append(out, k)
return true
})
return nm.SelfNode.Capabilities()
return views.SliceOf(out)
}
func (nm *NetworkMap) String() string {
@@ -211,7 +230,7 @@ func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ tailcfg.Node
// in equalConciseHeader in sync.
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
nm.NodeKey.ShortString(), nm.MachineStatus)
nm.NodeKey.ShortString(), nm.GetMachineStatus())
login := nm.UserProfiles[nm.User()].LoginName
if login == "" {
if nm.User().IsZero() {
@@ -221,25 +240,17 @@ func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
}
}
fmt.Fprintf(buf, " u=%s", login)
fmt.Fprintf(buf, " %v", nm.Addresses)
fmt.Fprintf(buf, " %v", nm.GetAddresses().AsSlice())
buf.WriteByte('\n')
}
// equalConciseHeader reports whether a and b are equal for the fields
// used by printConciseHeader.
func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
if a.NodeKey != b.NodeKey ||
a.MachineStatus != b.MachineStatus ||
a.User() != b.User() ||
len(a.Addresses) != len(b.Addresses) {
return false
}
for i, a := range a.Addresses {
if b.Addresses[i] != a {
return false
}
}
return true
return a.NodeKey == b.NodeKey &&
a.GetMachineStatus() == b.GetMachineStatus() &&
a.User() == b.User() &&
views.SliceEqual(a.GetAddresses(), b.GetAddresses())
}
// printPeerConcise appends to buf a line representing the peer p.

View File

@@ -4,6 +4,7 @@
package netmap
import (
"fmt"
"net/netip"
"reflect"
"slices"
@@ -11,6 +12,7 @@ import (
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
"tailscale.com/util/cmpx"
)
@@ -18,6 +20,7 @@ import (
// the change of a node's state.
type NodeMutation interface {
NodeIDBeingMutated() tailcfg.NodeID
Apply(*tailcfg.Node)
}
type mutatingNodeID tailcfg.NodeID
@@ -31,12 +34,24 @@ type NodeMutationDERPHome struct {
DERPRegion int
}
func (m NodeMutationDERPHome) Apply(n *tailcfg.Node) {
n.DERP = fmt.Sprintf("127.3.3.40:%v", m.DERPRegion)
}
// NodeMutation is a NodeMutation that says a node's endpoints have changed.
type NodeMutationEndpoints struct {
mutatingNodeID
Endpoints []netip.AddrPort
}
func (m NodeMutationEndpoints) Apply(n *tailcfg.Node) {
eps := make([]string, len(m.Endpoints))
for i, ep := range m.Endpoints {
eps[i] = ep.String()
}
n.Endpoints = eps
}
// NodeMutationOnline is a NodeMutation that says a node is now online or
// offline.
type NodeMutationOnline struct {
@@ -44,6 +59,10 @@ type NodeMutationOnline struct {
Online bool
}
func (m NodeMutationOnline) Apply(n *tailcfg.Node) {
n.Online = ptr.To(m.Online)
}
// NodeMutationLastSeen is a NodeMutation that says a node's LastSeen
// value should be set to the current time.
type NodeMutationLastSeen struct {
@@ -51,6 +70,10 @@ type NodeMutationLastSeen struct {
LastSeen time.Time
}
func (m NodeMutationLastSeen) Apply(n *tailcfg.Node) {
n.LastSeen = ptr.To(m.LastSeen)
}
var peerChangeFields = sync.OnceValue(func() []reflect.StructField {
var fields []reflect.StructField
rt := reflect.TypeOf((*tailcfg.PeerChange)(nil)).Elem()

View File

@@ -276,6 +276,16 @@ func SliceContains[T comparable](v Slice[T], e T) bool {
return false
}
// SliceContainsFunc reports whether f reports true for any element in v.
func SliceContainsFunc[T any](v Slice[T], f func(T) bool) bool {
for i := 0; i < v.Len(); i++ {
if f(v.At(i)) {
return true
}
}
return false
}
// SliceEqual is like the standard library's slices.Equal, but for two views.
func SliceEqual[T comparable](a, b Slice[T]) bool {
return slices.Equal(a.ж, b.ж)

View File

@@ -124,6 +124,8 @@ func TestViewUtils(t *testing.T) {
c.Check(v.IndexFunc(func(s string) bool { return strings.HasPrefix(s, "z") }), qt.Equals, -1)
c.Check(SliceContains(v, "bar"), qt.Equals, true)
c.Check(SliceContains(v, "baz"), qt.Equals, false)
c.Check(SliceContainsFunc(v, func(s string) bool { return strings.HasPrefix(s, "f") }), qt.Equals, true)
c.Check(SliceContainsFunc(v, func(s string) bool { return len(s) > 3 }), qt.Equals, false)
c.Check(SliceEqualAnyOrder(v, v), qt.Equals, true)
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"bar", "foo"})), qt.Equals, true)
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"foo"})), qt.Equals, false)

View File

@@ -6,5 +6,7 @@ package winutil
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
//sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W
//sys getProcessMitigationPolicy(hProcess windows.Handle, mitigationPolicy _PROCESS_MITIGATION_POLICY, buf unsafe.Pointer, bufLen uintptr) (err error) [int32(failretval)==0] = kernel32.GetProcessMitigationPolicy
//sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [int32(failretval)==0] = advapi32.QueryServiceConfig2W
//sys registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret wingoes.HRESULT) = kernel32.RegisterApplicationRestart
//sys setProcessMitigationPolicy(mitigationPolicy _PROCESS_MITIGATION_POLICY, buf unsafe.Pointer, bufLen uintptr) (err error) [int32(failretval)==0] = kernel32.SetProcessMitigationPolicy

View File

@@ -0,0 +1,577 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package winutil
import (
"bytes"
"fmt"
"os"
"runtime"
"strings"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
// StartProcessAsChild starts exePath process as a child of parentPID.
// StartProcessAsChild copies parentPID's environment variables into
// the new process, along with any optional environment variables in extraEnv.
func StartProcessAsChild(parentPID uint32, exePath string, extraEnv []string) error {
return StartProcessWithAttributes(exePath, ProcessAttributeEnvExtra{Slice: extraEnv}, ProcessAttributeParentProcessID(parentPID))
}
func StartProcessWithAttributes(exePath string, attrs ...any) (err error) {
var desktop string
var parentPID uint32
var mitigationBits uint64
var inheritableHandleList ProcessAttributeExplicitInheritableHandleList
var useStdHandles bool
var useToken windows.Token
var wd string
var procSA *windows.SecurityAttributes
var threadSA *windows.SecurityAttributes
var envExtra ProcessAttributeEnvExtra
var args []string
creationFlags := uint32(windows.CREATE_UNICODE_ENVIRONMENT | windows.EXTENDED_STARTUPINFO_PRESENT)
for _, attr := range attrs {
switch v := attr.(type) {
case ProcessAttributeExplicitInheritableHandleList:
inheritableHandleList, useStdHandles, err = v.filtered()
if err != nil {
return err
}
case *ProcessAttributeExplicitInheritableHandleList:
inheritableHandleList, useStdHandles, err = v.filtered()
if err != nil {
return err
}
case ProcessAttributeParentProcessID:
parentPID = uint32(v)
case *ProcessAttributeParentProcessID:
parentPID = uint32(*v)
case ProcessMitigationPolicies:
mitigationBits = v.asMitigationBits()
case *ProcessMitigationPolicies:
mitigationBits = v.asMitigationBits()
case windows.Token:
useToken = v
case ProcessAttributeGUIBindInfo:
desktop = v.String()
case *ProcessAttributeGUIBindInfo:
desktop = v.String()
case ProcessAttributeWorkingDirectory:
wd = v.String()
case *ProcessAttributeWorkingDirectory:
wd = v.String()
case ProcessAttributeSecurity:
procSA = v.Process
threadSA = v.Thread
case *ProcessAttributeSecurity:
procSA = v.Process
threadSA = v.Thread
case ProcessAttributeEnvExtra:
envExtra = v
case *ProcessAttributeEnvExtra:
envExtra = *v
case ProcessAttributeArgs:
args = []string(v)
case *ProcessAttributeArgs:
args = []string(*v)
case ProcessAttributeFlags:
creationFlags |= v.creationFlags()
case *ProcessAttributeFlags:
creationFlags |= v.creationFlags()
default:
return os.ErrInvalid
}
}
var attrCount uint32
if len(inheritableHandleList.Handles) > 0 {
attrCount++
}
if parentPID != 0 {
attrCount++
}
if mitigationBits != 0 {
attrCount++
}
var ph windows.Handle
var env []string
if parentPID == 0 {
env = os.Environ()
} else {
// According to https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
//
// ... To open a handle to another process and obtain full access rights,
// you must enable the SeDebugPrivilege privilege. ...
//
// But we only need PROCESS_CREATE_PROCESS. So perhaps SeDebugPrivilege is too much.
//
// https://devblogs.microsoft.com/oldnewthing/20080314-00/?p=23113
//
// TODO: try look for something less than SeDebugPrivilege
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := windows.ImpersonateSelf(windows.SecurityImpersonation)
if err != nil {
return err
}
defer windows.RevertToSelf()
err = EnableCurrentThreadPrivilege("SeDebugPrivilege")
if err != nil {
return err
}
ph, err = windows.OpenProcess(
windows.PROCESS_CREATE_PROCESS|windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_DUP_HANDLE,
false, parentPID)
if err != nil {
return err
}
defer windows.CloseHandle(ph)
var pt windows.Token
if err := windows.OpenProcessToken(ph, windows.TOKEN_QUERY, &pt); err != nil {
return err
}
defer pt.Close()
env, err = pt.Environ(false)
if err != nil {
return err
}
}
env16 := envExtra.envBlock(env)
var inheritHandles bool
var attrList *windows.ProcThreadAttributeList
if attrCount > 0 {
attrListContainer, err := windows.NewProcThreadAttributeList(attrCount)
if err != nil {
return err
}
defer attrListContainer.Delete()
if ph != 0 {
attrListContainer.Update(windows.PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, unsafe.Pointer(&ph), unsafe.Sizeof(ph))
}
if hll := uintptr(len(inheritableHandleList.Handles)); hll > 0 {
attrListContainer.Update(windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&inheritableHandleList.Handles[0]), hll*unsafe.Sizeof(windows.Handle(0)))
inheritHandles = true
}
if mitigationBits != 0 {
attrListContainer.Update(windows.PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, unsafe.Pointer(&mitigationBits), unsafe.Sizeof(mitigationBits))
}
attrList = attrListContainer.List()
}
var desktop16 *uint16
if desktop != "" {
desktop16, err = windows.UTF16PtrFromString(desktop)
if err != nil {
return err
}
}
var startupInfoFlags uint32
if useStdHandles {
startupInfoFlags |= windows.STARTF_USESTDHANDLES
}
siex := windows.StartupInfoEx{
StartupInfo: windows.StartupInfo{
Cb: uint32(unsafe.Sizeof(windows.StartupInfoEx{})),
Desktop: desktop16,
Flags: startupInfoFlags,
StdInput: inheritableHandleList.Stdin,
StdOutput: inheritableHandleList.Stdout,
StdErr: inheritableHandleList.Stderr,
},
ProcThreadAttributeList: attrList,
}
var wd16 *uint16
if wd != "" {
wd16, err = windows.UTF16PtrFromString(wd)
if err != nil {
return err
}
}
exePath16, err := windows.UTF16PtrFromString(exePath)
if err != nil {
return err
}
cmdLine, err := makeCmdLine(exePath, args)
if err != nil {
return err
}
var pi windows.ProcessInformation
if useToken == 0 {
err = windows.CreateProcess(exePath16, cmdLine, procSA, threadSA, inheritHandles, creationFlags, env16, wd16, &siex.StartupInfo, &pi)
} else {
err = windows.CreateProcessAsUser(useToken, exePath16, cmdLine, procSA, threadSA, inheritHandles, creationFlags, env16, wd16, &siex.StartupInfo, &pi)
}
runtime.KeepAlive(siex)
if err != nil {
return err
}
defer windows.CloseHandle(pi.Thread)
defer windows.CloseHandle(pi.Process)
return err
}
func makeCmdLine(exePath string, args []string) (*uint16, error) {
var buf strings.Builder
buf.WriteString(windows.EscapeArg(exePath))
for _, arg := range args {
if buf.Len() > 0 {
buf.WriteByte(' ')
}
buf.WriteString(windows.EscapeArg(arg))
}
return windows.UTF16PtrFromString(buf.String())
}
// StartProcessAsCurrentGUIUser is like StartProcessAsChild, but if finds
// current logged in user desktop process (normally explorer.exe),
// and passes found PID to StartProcessAsChild.
func StartProcessAsCurrentGUIUser(exePath string, extraEnv []string) error {
// as described in https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
desktop, err := GetDesktopPID()
if err != nil {
return fmt.Errorf("failed to find desktop: %v", err)
}
err = StartProcessAsChild(desktop, exePath, extraEnv)
if err != nil {
return fmt.Errorf("failed to start executable: %v", err)
}
return nil
}
type ProcessAttributeEnvExtra struct {
Map map[string]string
Slice []string
}
func (ee *ProcessAttributeEnvExtra) envBlock(env []string) *uint16 {
var buf bytes.Buffer
for _, s := range [][]string{env, ee.Slice} {
for _, v := range s {
buf.WriteString(v)
buf.WriteByte(0)
}
}
for k, v := range ee.Map {
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v)
buf.WriteByte(0)
}
if buf.Len() == 0 {
// So that we end with a double-null in the empty env case (unlikely)
buf.WriteByte(0)
}
buf.WriteByte(0)
return &utf16.Encode([]rune(string(buf.Bytes())))[0]
}
type ProcessAttributeFlags struct {
BreakawayFromJob bool
CreateNewConsole bool
CreateNewProcessGroup bool
Detached bool
InheritParentAffinity bool
NoConsoleWindow bool
}
func (paf *ProcessAttributeFlags) creationFlags() (result uint32) {
if paf.BreakawayFromJob {
result |= windows.CREATE_BREAKAWAY_FROM_JOB
}
if paf.CreateNewConsole {
result |= windows.CREATE_NEW_CONSOLE
}
if paf.CreateNewProcessGroup {
result |= windows.CREATE_NEW_PROCESS_GROUP
}
if paf.Detached {
result |= windows.DETACHED_PROCESS
}
if paf.InheritParentAffinity {
result |= windows.INHERIT_PARENT_AFFINITY
}
if paf.NoConsoleWindow {
result |= windows.CREATE_NO_WINDOW
}
return result
}
type ProcessAttributeArgs []string
type ProcessAttributeSecurity struct {
Process *windows.SecurityAttributes
Thread *windows.SecurityAttributes
}
type ProcessAttributeWorkingDirectory string
func (wd *ProcessAttributeWorkingDirectory) String() string {
return string(*wd)
}
type ProcessAttributeGUIBindInfo struct {
WindowStation string
Desktop string
}
func (gbi *ProcessAttributeGUIBindInfo) String() string {
winsta := gbi.WindowStation
if winsta == "" {
winsta = "Winsta0"
}
desktop := gbi.Desktop
if desktop == "" {
desktop = "default"
}
var buf strings.Builder
buf.WriteString(winsta)
buf.WriteByte('\\')
buf.WriteString(desktop)
return buf.String()
}
type ProcessAttributeParentProcessID uint32
type ProcessAttributeExplicitInheritableHandleList struct {
Stdin windows.Handle
Stdout windows.Handle
Stderr windows.Handle
Handles []windows.Handle
}
func (eihl *ProcessAttributeExplicitInheritableHandleList) filtered() (result ProcessAttributeExplicitInheritableHandleList, containsStd bool, err error) {
result = ProcessAttributeExplicitInheritableHandleList{
Stdin: eihl.Stdin,
Stdout: eihl.Stdout,
Stderr: eihl.Stderr,
Handles: make([]windows.Handle, 0, len(eihl.Handles)+3),
}
handles := make([]windows.Handle, 0, len(eihl.Handles)+3)
if result.Stdin == 0 {
result.Stdin = windows.Stdin
}
handles = append(handles, result.Stdin)
if result.Stdout == 0 {
result.Stdout = windows.Stdout
}
handles = append(handles, result.Stdout)
if result.Stderr == 0 {
result.Stderr = windows.Stderr
}
handles = append(handles, result.Stderr)
handles = append(handles, eihl.Handles...)
for i, h := range handles {
fileType, err := windows.GetFileType(h)
if err != nil {
return result, false, err
}
if fileType != windows.FILE_TYPE_DISK && fileType != windows.FILE_TYPE_PIPE {
continue
}
if err := windows.SetHandleInformation(h, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); err != nil {
return result, false, err
}
result.Handles = append(result.Handles, h)
if i < 3 {
// Standard handle
containsStd = true
}
}
return result, containsStd, nil
}
type _PROCESS_MITIGATION_POLICY int32
const (
processDEPPolicy _PROCESS_MITIGATION_POLICY = 0
processASLRPolicy _PROCESS_MITIGATION_POLICY = 1
processDynamicCodePolicy _PROCESS_MITIGATION_POLICY = 2
processStrictHandleCheckPolicy _PROCESS_MITIGATION_POLICY = 3
processSystemCallDisablePolicy _PROCESS_MITIGATION_POLICY = 4
processMitigationOptionsMask _PROCESS_MITIGATION_POLICY = 5
processExtensionPointDisablePolicy _PROCESS_MITIGATION_POLICY = 6
processControlFlowGuardPolicy _PROCESS_MITIGATION_POLICY = 7
processSignaturePolicy _PROCESS_MITIGATION_POLICY = 8
processFontDisablePolicy _PROCESS_MITIGATION_POLICY = 9
processImageLoadPolicy _PROCESS_MITIGATION_POLICY = 10
processSystemCallFilterPolicy _PROCESS_MITIGATION_POLICY = 11
processPayloadRestrictionPolicy _PROCESS_MITIGATION_POLICY = 12
processChildProcessPolicy _PROCESS_MITIGATION_POLICY = 13
processSideChannelIsolationPolicy _PROCESS_MITIGATION_POLICY = 14
processUserShadowStackPolicy _PROCESS_MITIGATION_POLICY = 15
processRedirectionTrustPolicy _PROCESS_MITIGATION_POLICY = 16
processUserPointerAuthPolicy _PROCESS_MITIGATION_POLICY = 17
processSEHOPPolicy _PROCESS_MITIGATION_POLICY = 18
)
type processMitigationPolicyFlags struct {
Flags uint32
}
const (
_NoRemoteImages = 1
_NoLowMandatoryLabelImages = (1 << 1)
_PreferSystem32Images = (1 << 2)
_MicrosoftSignedOnly = 1
_DisableExtensionPoints = 1
_ProhibitDynamicCode = 1
)
type ProcessMitigationPolicies struct {
DisableExtensionPoints bool
PreferSystem32Images bool
ProhibitDynamicCode bool
ProhibitLowMandatoryLabelImages bool
ProhibitNonMicrosoftSignedDLLs bool
ProhibitRemoteImages bool
}
func CurrentProcessMitigationPolicies() (result ProcessMitigationPolicies, _ error) {
var flags processMitigationPolicyFlags
cp := windows.CurrentProcess()
if err := getProcessMitigationPolicy(cp, processExtensionPointDisablePolicy, unsafe.Pointer(&flags), unsafe.Sizeof(flags)); err != nil {
return result, err
}
result.DisableExtensionPoints = flags.Flags&_DisableExtensionPoints != 0
if err := getProcessMitigationPolicy(cp, processSystemCallDisablePolicy, unsafe.Pointer(&flags), unsafe.Sizeof(flags)); err != nil {
return result, err
}
result.ProhibitNonMicrosoftSignedDLLs = flags.Flags&_MicrosoftSignedOnly != 0
if err := getProcessMitigationPolicy(cp, processDynamicCodePolicy, unsafe.Pointer(&flags), unsafe.Sizeof(flags)); err != nil {
return result, err
}
result.ProhibitDynamicCode = flags.Flags&_ProhibitDynamicCode != 0
if err := getProcessMitigationPolicy(cp, processImageLoadPolicy, unsafe.Pointer(&flags), unsafe.Sizeof(flags)); err != nil {
return result, err
}
result.ProhibitRemoteImages = flags.Flags&_NoRemoteImages != 0
result.ProhibitLowMandatoryLabelImages = flags.Flags&_NoLowMandatoryLabelImages != 0
result.PreferSystem32Images = flags.Flags&_PreferSystem32Images != 0
return result, nil
}
func (pmp *ProcessMitigationPolicies) SetOnCurrentProcess() error {
if pmp.DisableExtensionPoints {
v := processMitigationPolicyFlags{
Flags: _DisableExtensionPoints,
}
if err := setProcessMitigationPolicy(processExtensionPointDisablePolicy, unsafe.Pointer(&v), unsafe.Sizeof(v)); err != nil {
return err
}
}
if pmp.ProhibitNonMicrosoftSignedDLLs {
v := processMitigationPolicyFlags{
Flags: _MicrosoftSignedOnly,
}
if err := setProcessMitigationPolicy(processSystemCallDisablePolicy, unsafe.Pointer(&v), unsafe.Sizeof(v)); err != nil {
return err
}
}
if pmp.ProhibitDynamicCode {
v := processMitigationPolicyFlags{
Flags: _ProhibitDynamicCode,
}
if err := setProcessMitigationPolicy(processDynamicCodePolicy, unsafe.Pointer(&v), unsafe.Sizeof(v)); err != nil {
return err
}
}
var imageLoadFlags uint32
if pmp.PreferSystem32Images {
imageLoadFlags |= _PreferSystem32Images
}
if pmp.ProhibitLowMandatoryLabelImages {
imageLoadFlags |= _NoLowMandatoryLabelImages
}
if pmp.ProhibitRemoteImages {
imageLoadFlags |= _NoRemoteImages
}
if imageLoadFlags != 0 {
v := processMitigationPolicyFlags{
Flags: imageLoadFlags,
}
if err := setProcessMitigationPolicy(processImageLoadPolicy, unsafe.Pointer(&v), unsafe.Sizeof(v)); err != nil {
return err
}
}
return nil
}
func (pmp *ProcessMitigationPolicies) asMitigationBits() (result uint64) {
if pmp.DisableExtensionPoints {
result |= (1 << 32)
}
if pmp.PreferSystem32Images {
result |= (1 << 60)
}
if pmp.ProhibitDynamicCode {
result |= (1 << 36)
}
if pmp.ProhibitLowMandatoryLabelImages {
result |= (1 << 56)
}
if pmp.ProhibitNonMicrosoftSignedDLLs {
result |= (1 << 44)
}
if pmp.ProhibitRemoteImages {
result |= (1 << 52)
}
return result
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package winutil
import (
"strings"
"testing"
)
func TestMitigateSelf(t *testing.T) {
output := strings.TrimSpace(runTestProg(t, "testprocessattributes", "MitigateSelf"))
want := "OK"
if output != want {
t.Errorf("%s\n", strings.TrimPrefix(output, "error: "))
}
}

View File

@@ -0,0 +1,388 @@
// Copyright 2015 The Go 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 winutil
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"
)
// The code in this file is adapted from internal/testenv in the Go source tree
// and is used for writing tests that require spawning subprocesses.
var toRemove []string
func TestMain(m *testing.M) {
status := m.Run()
for _, file := range toRemove {
os.RemoveAll(file)
}
os.Exit(status)
}
var testprog struct {
sync.Mutex
dir string
target map[string]*buildexe
}
type buildexe struct {
once sync.Once
exe string
err error
}
func runTestProg(t *testing.T, binary, name string, env ...string) string {
exe, err := buildTestProg(t, binary)
if err != nil {
t.Fatal(err)
}
return runBuiltTestProg(t, exe, name, env...)
}
func runBuiltTestProg(t *testing.T, exe, name string, env ...string) string {
cmd := exec.Command(exe, name)
cmd.Env = append(cmd.Env, env...)
if testing.Short() {
cmd.Env = append(cmd.Env, "RUNTIME_TEST_SHORT=1")
}
out, _ := runWithTimeout(t, cmd)
return string(out)
}
var serializeBuild = make(chan bool, 2)
func buildTestProg(t *testing.T, binary string, flags ...string) (string, error) {
testprog.Lock()
if testprog.dir == "" {
dir, err := os.MkdirTemp("", "go-build")
if err != nil {
t.Fatalf("failed to create temp directory: %v", err)
}
testprog.dir = dir
toRemove = append(toRemove, dir)
}
if testprog.target == nil {
testprog.target = make(map[string]*buildexe)
}
name := binary
if len(flags) > 0 {
name += "_" + strings.Join(flags, "_")
}
target, ok := testprog.target[name]
if !ok {
target = &buildexe{}
testprog.target[name] = target
}
dir := testprog.dir
// Unlock testprog while actually building, so that other
// tests can look up executables that were already built.
testprog.Unlock()
target.once.Do(func() {
// Only do two "go build"'s at a time,
// to keep load from getting too high.
serializeBuild <- true
defer func() { <-serializeBuild }()
// Don't get confused if goToolPath calls t.Skip.
target.err = errors.New("building test called t.Skip")
exe := filepath.Join(dir, name+".exe")
t.Logf("running go build -o %s %s", exe, strings.Join(flags, " "))
cmd := exec.Command(goToolPath(t), append([]string{"build", "-o", exe}, flags...)...)
cmd.Dir = "testdata/" + binary
out, err := cmd.CombinedOutput()
if err != nil {
target.err = fmt.Errorf("building %s %v: %v\n%s", binary, flags, err, out)
} else {
target.exe = exe
target.err = nil
}
})
return target.exe, target.err
}
// goTool reports the path to the Go tool.
func goTool() (string, error) {
if !hasGoBuild() {
return "", errors.New("platform cannot run go tool")
}
exeSuffix := ".exe"
goroot, err := findGOROOT()
if err != nil {
return "", fmt.Errorf("cannot find go tool: %w", err)
}
path := filepath.Join(goroot, "bin", "go"+exeSuffix)
if _, err := os.Stat(path); err == nil {
return path, nil
}
goBin, err := exec.LookPath("go" + exeSuffix)
if err != nil {
return "", errors.New("cannot find go tool: " + err.Error())
}
return goBin, nil
}
// knownEnv is a list of environment variables that affect the operation
// of the Go command.
const knownEnv = `
AR
CC
CGO_CFLAGS
CGO_CFLAGS_ALLOW
CGO_CFLAGS_DISALLOW
CGO_CPPFLAGS
CGO_CPPFLAGS_ALLOW
CGO_CPPFLAGS_DISALLOW
CGO_CXXFLAGS
CGO_CXXFLAGS_ALLOW
CGO_CXXFLAGS_DISALLOW
CGO_ENABLED
CGO_FFLAGS
CGO_FFLAGS_ALLOW
CGO_FFLAGS_DISALLOW
CGO_LDFLAGS
CGO_LDFLAGS_ALLOW
CGO_LDFLAGS_DISALLOW
CXX
FC
GCCGO
GO111MODULE
GO386
GOAMD64
GOARCH
GOARM
GOBIN
GOCACHE
GOENV
GOEXE
GOEXPERIMENT
GOFLAGS
GOGCCFLAGS
GOHOSTARCH
GOHOSTOS
GOINSECURE
GOMIPS
GOMIPS64
GOMODCACHE
GONOPROXY
GONOSUMDB
GOOS
GOPATH
GOPPC64
GOPRIVATE
GOPROXY
GOROOT
GOSUMDB
GOTMPDIR
GOTOOLDIR
GOVCS
GOWASM
GOWORK
GO_EXTLINK_ENABLED
PKG_CONFIG
`
// goToolPath reports the path to the Go tool.
// It is a convenience wrapper around goTool.
// If the tool is unavailable goToolPath calls t.Skip.
// If the tool should be available and isn't, goToolPath calls t.Fatal.
func goToolPath(t testing.TB) string {
mustHaveGoBuild(t)
path, err := goTool()
if err != nil {
t.Fatal(err)
}
// Add all environment variables that affect the Go command to test metadata.
// Cached test results will be invalidate when these variables change.
// See golang.org/issue/32285.
for _, envVar := range strings.Fields(knownEnv) {
os.Getenv(envVar)
}
return path
}
// hasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
func hasGoBuild() bool {
if os.Getenv("GO_GCFLAGS") != "" {
// It's too much work to require every caller of the go command
// to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
// For now, if $GO_GCFLAGS is set, report that we simply can't
// run go build.
return false
}
return true
}
// mustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
// If not, mustHaveGoBuild calls t.Skip with an explanation.
func mustHaveGoBuild(t testing.TB) {
if os.Getenv("GO_GCFLAGS") != "" {
t.Skipf("skipping test: 'go build' not compatible with setting $GO_GCFLAGS")
}
if !hasGoBuild() {
t.Skipf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
// hasGoRun reports whether the current system can run programs with “go run.”
func hasGoRun() bool {
// For now, having go run and having go build are the same.
return hasGoBuild()
}
// mustHaveGoRun checks that the current system can run programs with “go run.”
// If not, mustHaveGoRun calls t.Skip with an explanation.
func mustHaveGoRun(t testing.TB) {
if !hasGoRun() {
t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
var (
gorootOnce sync.Once
gorootPath string
gorootErr error
)
func findGOROOT() (string, error) {
gorootOnce.Do(func() {
gorootPath = runtime.GOROOT()
if gorootPath != "" {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
// rare, and if that happens the user can fix what they broke.)
return
}
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
// without GOROOT and the tree hasn't been moved there yet).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
// runs the test in the directory containing the packaged under test.) That
// means that if we start walking up the tree, we should eventually find
// GOROOT/src/go.mod, and we can report the parent directory of that.
cwd, err := os.Getwd()
if err != nil {
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
return
}
if base := filepath.Base(dir); base != "src" {
dir = parent
continue // dir cannot be GOROOT/src if it doesn't end in "src".
}
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
if os.IsNotExist(err) {
dir = parent
continue
}
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
goMod := string(b)
for goMod != "" {
var line string
line, goMod, _ = strings.Cut(goMod, "\n")
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
// Found "module std", which is the module declaration in GOROOT/src!
gorootPath = parent
return
}
}
}
})
return gorootPath, gorootErr
}
// runWithTimeout runs cmd and returns its combined output. If the
// subprocess exits with a non-zero status, it will log that status
// and return a non-nil error, but this is not considered fatal.
func runWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) {
args := cmd.Args
if args == nil {
args = []string{cmd.Path}
}
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
if err := cmd.Start(); err != nil {
t.Fatalf("starting %s: %v", args, err)
}
// If the process doesn't complete within 1 minute,
// assume it is hanging and kill it to get a stack trace.
p := cmd.Process
done := make(chan bool)
go func() {
scale := 2
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
if sc, err := strconv.Atoi(s); err == nil {
scale = sc
}
}
select {
case <-done:
case <-time.After(time.Duration(scale) * time.Minute):
p.Signal(os.Kill)
// If SIGQUIT doesn't do it after a little
// while, kill the process.
select {
case <-done:
case <-time.After(time.Duration(scale) * 30 * time.Second):
p.Signal(os.Kill)
}
}
}()
err := cmd.Wait()
if err != nil {
t.Logf("%s exit status: %v", args, err)
}
close(done)
return b.Bytes(), err
}

View File

@@ -0,0 +1,40 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package main
import "os"
var (
cmds = map[string]func(){}
err error
)
func register(name string, f func()) {
if cmds[name] != nil {
panic("duplicate registration: " + name)
}
cmds[name] = f
}
func registerInit(name string, f func()) {
if len(os.Args) >= 2 && os.Args[1] == name {
f()
}
}
func main() {
if len(os.Args) < 2 {
println("usage: " + os.Args[0] + " name-of-test")
return
}
f := cmds[os.Args[1]]
if f == nil {
println("unknown function: " + os.Args[1])
return
}
f()
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"fmt"
"tailscale.com/util/winutil"
)
func init() {
// registerInit("Foo", FooInit)
// register("Foo", Foo)
register("MitigateSelf", MitigateSelf)
}
func MitigateSelf() {
var zero winutil.ProcessMitigationPolicies
initialPolicies, err := winutil.CurrentProcessMitigationPolicies()
if err != nil {
fmt.Printf("error: CurrentProcessMitigationPolicies: %v\n", err)
return
}
if initialPolicies != zero {
fmt.Println("error: initialPolicies not zero value")
return
}
setTo := winutil.ProcessMitigationPolicies{
DisableExtensionPoints: true,
PreferSystem32Images: true,
ProhibitDynamicCode: true,
ProhibitLowMandatoryLabelImages: true,
ProhibitNonMicrosoftSignedDLLs: true,
ProhibitRemoteImages: true,
}
if err := setTo.SetOnCurrentProcess(); err != nil {
fmt.Printf("error: SetOnCurrentProcess: %v\n", err)
return
}
checkPolicies, err := winutil.CurrentProcessMitigationPolicies()
if err != nil {
fmt.Printf("error: CurrentProcessMitigationPolicies: %v\n", err)
return
}
if checkPolicies != setTo {
fmt.Printf("error: checkPolicies got %#v, want %#v\n", checkPolicies, setTo)
return
}
fmt.Println("OK")
}

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"log"
"os"
"os/exec"
"os/user"
"runtime"
"strings"
@@ -248,84 +247,6 @@ func EnableCurrentThreadPrivilege(name string) error {
return windows.AdjustTokenPrivileges(t, false, &tp, 0, nil, nil)
}
// StartProcessAsChild starts exePath process as a child of parentPID.
// StartProcessAsChild copies parentPID's environment variables into
// the new process, along with any optional environment variables in extraEnv.
func StartProcessAsChild(parentPID uint32, exePath string, extraEnv []string) error {
// The rest of this function requires SeDebugPrivilege to be held.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := windows.ImpersonateSelf(windows.SecurityImpersonation)
if err != nil {
return err
}
defer windows.RevertToSelf()
// According to https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
//
// ... To open a handle to another process and obtain full access rights,
// you must enable the SeDebugPrivilege privilege. ...
//
// But we only need PROCESS_CREATE_PROCESS. So perhaps SeDebugPrivilege is too much.
//
// https://devblogs.microsoft.com/oldnewthing/20080314-00/?p=23113
//
// TODO: try look for something less than SeDebugPrivilege
err = EnableCurrentThreadPrivilege("SeDebugPrivilege")
if err != nil {
return err
}
ph, err := windows.OpenProcess(
windows.PROCESS_CREATE_PROCESS|windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_DUP_HANDLE,
false, parentPID)
if err != nil {
return err
}
defer windows.CloseHandle(ph)
var pt windows.Token
err = windows.OpenProcessToken(ph, windows.TOKEN_QUERY, &pt)
if err != nil {
return err
}
defer pt.Close()
env, err := pt.Environ(false)
if err != nil {
return err
}
env = append(env, extraEnv...)
sys := &syscall.SysProcAttr{ParentProcess: syscall.Handle(ph)}
cmd := exec.Command(exePath)
cmd.Env = env
cmd.SysProcAttr = sys
return cmd.Start()
}
// StartProcessAsCurrentGUIUser is like StartProcessAsChild, but if finds
// current logged in user desktop process (normally explorer.exe),
// and passes found PID to StartProcessAsChild.
func StartProcessAsCurrentGUIUser(exePath string, extraEnv []string) error {
// as described in https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
desktop, err := GetDesktopPID()
if err != nil {
return fmt.Errorf("failed to find desktop: %v", err)
}
err = StartProcessAsChild(desktop, exePath, extraEnv)
if err != nil {
return fmt.Errorf("failed to start executable: %v", err)
}
return nil
}
// CreateAppMutex creates a named Windows mutex, returning nil if the mutex
// is created successfully or an error if the mutex already exists or could not
// be created for some other reason.

View File

@@ -43,12 +43,22 @@ var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procQueryServiceConfig2W = modadvapi32.NewProc("QueryServiceConfig2W")
procGetProcessMitigationPolicy = modkernel32.NewProc("GetProcessMitigationPolicy")
procRegisterApplicationRestart = modkernel32.NewProc("RegisterApplicationRestart")
procSetProcessMitigationPolicy = modkernel32.NewProc("SetProcessMitigationPolicy")
)
func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procQueryServiceConfig2W.Addr(), 5, uintptr(hService), uintptr(infoLevel), uintptr(unsafe.Pointer(buf)), uintptr(bufLen), uintptr(unsafe.Pointer(bytesNeeded)), 0)
if r1 == 0 {
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}
func getProcessMitigationPolicy(hProcess windows.Handle, mitigationPolicy _PROCESS_MITIGATION_POLICY, buf unsafe.Pointer, bufLen uintptr) (err error) {
r1, _, e1 := syscall.Syscall6(procGetProcessMitigationPolicy.Addr(), 4, uintptr(hProcess), uintptr(mitigationPolicy), uintptr(buf), uintptr(bufLen), 0, 0)
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
@@ -59,3 +69,11 @@ func registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret w
ret = wingoes.HRESULT(r0)
return
}
func setProcessMitigationPolicy(mitigationPolicy _PROCESS_MITIGATION_POLICY, buf unsafe.Pointer, bufLen uintptr) (err error) {
r1, _, e1 := syscall.Syscall(procSetProcessMitigationPolicy.Addr(), 3, uintptr(mitigationPolicy), uintptr(buf), uintptr(bufLen))
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}

View File

@@ -6,7 +6,6 @@
package filter
import (
"encoding/json"
"net/netip"
"tailscale.com/tailcfg"
@@ -24,9 +23,11 @@ func (src *Match) Clone() *Match {
dst.IPProto = append(src.IPProto[:0:0], src.IPProto...)
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
dst.Caps = make([]CapMatch, len(src.Caps))
for i := range dst.Caps {
dst.Caps[i] = *src.Caps[i].Clone()
if src.Caps != nil {
dst.Caps = make([]CapMatch, len(src.Caps))
for i := range dst.Caps {
dst.Caps[i] = *src.Caps[i].Clone()
}
}
return dst
}
@@ -47,10 +48,7 @@ func (src *CapMatch) Clone() *CapMatch {
}
dst := new(CapMatch)
*dst = *src
dst.Values = make([]json.RawMessage, len(src.Values))
for i := range dst.Values {
dst.Values[i] = append(src.Values[i][:0:0], src.Values[i]...)
}
dst.Values = append(src.Values[:0:0], src.Values...)
return dst
}
@@ -58,5 +56,5 @@ func (src *CapMatch) Clone() *CapMatch {
var _CapMatchCloneNeedsRegeneration = CapMatch(struct {
Dst netip.Prefix
Cap tailcfg.PeerCapability
Values []json.RawMessage
Values []tailcfg.RawMessage
}{})

View File

@@ -873,7 +873,7 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
}
}
func TestCaps(t *testing.T) {
func TestPeerCaps(t *testing.T) {
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
{
SrcIPs: []string{"*"},

View File

@@ -4,7 +4,6 @@
package filter
import (
"encoding/json"
"fmt"
"net/netip"
"strings"
@@ -60,7 +59,7 @@ type CapMatch struct {
// Values are the raw JSON values of the capability.
// See tailcfg.PeerCapability and tailcfg.PeerCapMap for details.
Values []json.RawMessage
Values []tailcfg.RawMessage
}
// Match matches packets from any IP address in Srcs to any ip:port in

View File

@@ -1816,8 +1816,8 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
c.peers = curPeers
flags := c.debugFlagsLocked()
if len(nm.Addresses) > 0 {
c.firstAddrForTest = nm.Addresses[0].Addr()
if addrs := nm.GetAddresses(); addrs.Len() > 0 {
c.firstAddrForTest = addrs.At(0).Addr()
} else {
c.firstAddrForTest = netip.Addr{}
}

View File

@@ -274,7 +274,9 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
nm := &netmap.NetworkMap{
PrivateKey: me.privateKey,
NodeKey: me.privateKey.Public(),
Addresses: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(1, 0, 0, byte(myIdx+1)), 32)},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(1, 0, 0, byte(myIdx+1)), 32)},
}).View(),
}
for i, peer := range ms {
if i == myIdx {
@@ -2267,7 +2269,9 @@ func TestIsWireGuardOnlyPeer(t *testing.T) {
Name: "ts",
PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{tsaip},
}).View(),
Peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
@@ -2326,7 +2330,9 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
Name: "ts",
PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{tsaip},
}).View(),
Peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
@@ -2453,7 +2459,9 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) {
Name: "ts",
PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{tsaip},
}).View(),
Peers: nodeViews([]*tailcfg.Node{
{
Key: wgkey.Public(),

View File

@@ -43,6 +43,7 @@ import (
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/proxymap"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
@@ -121,6 +122,7 @@ type Impl struct {
linkEP *channel.Endpoint
tundev *tstun.Wrapper
e wgengine.Engine
pm *proxymap.Mapper
mc *magicsock.Conn
logf logger.Logf
dialer *tsdial.Dialer
@@ -154,7 +156,7 @@ const nicID = 1
const maxUDPPacketSize = 1500
// Create creates and populates a new Impl.
func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, dialer *tsdial.Dialer, dns *dns.Manager) (*Impl, error) {
func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, dialer *tsdial.Dialer, dns *dns.Manager, pm *proxymap.Mapper) (*Impl, error) {
if mc == nil {
return nil, errors.New("nil magicsock.Conn")
}
@@ -167,6 +169,9 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
if e == nil {
return nil, errors.New("nil Engine")
}
if pm == nil {
return nil, errors.New("nil proxymap.Mapper")
}
if dialer == nil {
return nil, errors.New("nil Dialer")
}
@@ -209,13 +214,14 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
linkEP: linkEP,
tundev: tundev,
e: e,
pm: pm,
mc: mc,
dialer: dialer,
connsOpenBySubnetIP: make(map[netip.Addr]int),
dns: dns,
}
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nil))
ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
ns.tundev.PostFilterPacketInboundFromWireGaurd = ns.injectInbound
ns.tundev.PreFilterPacketOutboundToWireGuardNetstackIntercept = ns.handleLocalPackets
return ns, nil
@@ -318,10 +324,10 @@ var v4broadcast = netaddr.IPv4(255, 255, 255, 255)
func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
var selfNode tailcfg.NodeView
if nm != nil {
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nm.Addresses))
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nm.GetAddresses()))
selfNode = nm.SelfNode
} else {
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nil))
ns.atomicIsLocalIPFunc.Store(tsaddr.FalseContainsIPFunc())
}
oldIPs := make(map[tcpip.AddressWithPrefix]bool)
@@ -984,8 +990,8 @@ func (ns *Impl) forwardTCP(getClient func(...tcpip.SettableSocketOption) *gonet.
backendLocalAddr := server.LocalAddr().(*net.TCPAddr)
backendLocalIPPort := netaddr.Unmap(backendLocalAddr.AddrPort())
ns.e.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP)
defer ns.e.UnregisterIPPortIdentity(backendLocalIPPort)
ns.pm.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP)
defer ns.pm.UnregisterIPPortIdentity(backendLocalIPPort)
connClosed := make(chan error, 2)
go func() {
_, err := io.Copy(server, client)
@@ -1135,7 +1141,7 @@ func (ns *Impl) forwardUDP(client *gonet.UDPConn, clientAddr, dstAddr netip.Addr
ns.logf("could not get backend local IP:port from %v:%v", backendLocalAddr.IP, backendLocalAddr.Port)
}
if isLocal {
ns.e.RegisterIPPortIdentity(backendLocalIPPort, dstAddr.Addr())
ns.pm.RegisterIPPortIdentity(backendLocalIPPort, dstAddr.Addr())
}
ctx, cancel := context.WithCancel(context.Background())
@@ -1151,7 +1157,7 @@ func (ns *Impl) forwardUDP(client *gonet.UDPConn, clientAddr, dstAddr netip.Addr
}
timer := time.AfterFunc(idleTimeout, func() {
if isLocal {
ns.e.UnregisterIPPortIdentity(backendLocalIPPort)
ns.pm.UnregisterIPPortIdentity(backendLocalIPPort)
}
ns.logf("netstack: UDP session between %s and %s timed out", backendListenAddr, backendRemoteAddr)
cancel()

View File

@@ -53,7 +53,7 @@ func TestInjectInboundLeak(t *testing.T) {
t.Fatal(err)
}
ns, err := Create(logf, tunWrap, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
ns, err := Create(logf, tunWrap, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
t.Fatal(err)
}
@@ -102,7 +102,7 @@ func makeNetstack(t *testing.T, config func(*Impl)) *Impl {
t.Cleanup(func() { eng.Close() })
sys.Set(eng)
ns, err := Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
ns, err := Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
t.Fatal(err)
}

View File

@@ -133,7 +133,6 @@ type userspaceEngine struct {
peerSequence []key.NodePublic
endpoints []tailcfg.Endpoint
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
tsIPByIPPort map[netip.AddrPort]netip.Addr // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups
// pongCallback is the map of response handlers waiting for disco or TSMP
// pong callbacks. The map key is a random slice of bytes.
@@ -287,8 +286,8 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
return nil, err
}
}
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(nil))
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(nil))
e.isLocalAddr.Store(tsaddr.FalseContainsIPFunc())
e.isDNSIPOverTailscale.Store(tsaddr.FalseContainsIPFunc())
if conf.NetMon != nil {
e.netMon = conf.NetMon
@@ -783,7 +782,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
panic("dnsCfg must not be nil")
}
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(routerCfg.LocalAddrs))
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(views.SliceOf(routerCfg.LocalAddrs)))
e.wgLock.Lock()
defer e.wgLock.Unlock()
@@ -837,7 +836,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
// instead have ipnlocal populate a map of DNS IP => linkName and
// put that in the *dns.Config instead, and plumb it down to the
// dns.Manager. Maybe also with isLocalAddr above.
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(dnsIPsOverTailscale(dnsCfg, routerCfg)))
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(views.SliceOf(dnsIPsOverTailscale(dnsCfg, routerCfg))))
// See if any peers have changed disco keys, which means they've restarted.
// If so, we need to update the wireguard-go/device.Device in two phases:
@@ -1206,20 +1205,22 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size in
}
func (e *userspaceEngine) mySelfIPMatchingFamily(dst netip.Addr) (src netip.Addr, err error) {
var zero netip.Addr
e.mu.Lock()
defer e.mu.Unlock()
if e.netMap == nil {
return netip.Addr{}, errors.New("no netmap")
return zero, errors.New("no netmap")
}
for _, a := range e.netMap.Addresses {
if a.IsSingleIP() && a.Addr().BitLen() == dst.BitLen() {
addrs := e.netMap.GetAddresses()
if addrs.Len() == 0 {
return zero, errors.New("no self address in netmap")
}
for i := range addrs.LenIter() {
if a := addrs.At(i); a.IsSingleIP() && a.Addr().BitLen() == dst.BitLen() {
return a.Addr(), nil
}
}
if len(e.netMap.Addresses) == 0 {
return netip.Addr{}, errors.New("no self address in netmap")
}
return netip.Addr{}, errors.New("no self address in netmap matching address family")
return zero, errors.New("no self address in netmap matching address family")
}
func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer tailcfg.NodeView, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) {
@@ -1341,50 +1342,6 @@ func (e *userspaceEngine) setICMPEchoResponseCallback(idSeq uint32, cb func()) {
}
}
func (e *userspaceEngine) RegisterIPPortIdentity(ipport netip.AddrPort, tsIP netip.Addr) {
e.mu.Lock()
defer e.mu.Unlock()
if e.tsIPByIPPort == nil {
e.tsIPByIPPort = make(map[netip.AddrPort]netip.Addr)
}
e.tsIPByIPPort[ipport] = tsIP
}
func (e *userspaceEngine) UnregisterIPPortIdentity(ipport netip.AddrPort) {
e.mu.Lock()
defer e.mu.Unlock()
if e.tsIPByIPPort == nil {
return
}
delete(e.tsIPByIPPort, ipport)
}
var whoIsSleeps = [...]time.Duration{
0,
10 * time.Millisecond,
20 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
}
func (e *userspaceEngine) WhoIsIPPort(ipport netip.AddrPort) (tsIP netip.Addr, ok bool) {
// We currently have a registration race,
// https://github.com/tailscale/tailscale/issues/1616,
// so loop a few times for now waiting for the registration
// to appear.
// TODO(bradfitz,namansood): remove this once #1616 is fixed.
for _, d := range whoIsSleeps {
time.Sleep(d)
e.mu.Lock()
tsIP, ok = e.tsIPByIPPort[ipport]
e.mu.Unlock()
if ok {
return tsIP, true
}
}
return tsIP, false
}
// PeerForIP returns the Node in the wireguard config
// that's responsible for handling the given IP address.
//
@@ -1411,8 +1368,9 @@ func (e *userspaceEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) {
}
}
}
for _, a := range nm.Addresses {
if a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
if a := addrs.At(i); a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
return PeerForIP{Node: nm.SelfNode, IsSelf: true, Route: a}, true
}
}

View File

@@ -142,16 +142,6 @@ func (e *watchdogEngine) SetNetworkMap(nm *netmap.NetworkMap) {
func (e *watchdogEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult)) {
e.watchdog("Ping", func() { e.wrap.Ping(ip, pingType, size, cb) })
}
func (e *watchdogEngine) RegisterIPPortIdentity(ipp netip.AddrPort, tsIP netip.Addr) {
e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) })
}
func (e *watchdogEngine) UnregisterIPPortIdentity(ipp netip.AddrPort) {
e.watchdog("UnregisterIPPortIdentity", func() { e.wrap.UnregisterIPPortIdentity(ipp) })
}
func (e *watchdogEngine) WhoIsIPPort(ipp netip.AddrPort) (tsIP netip.Addr, ok bool) {
e.watchdog("UnregisterIPPortIdentity", func() { tsIP, ok = e.wrap.WhoIsIPPort(ipp) })
return tsIP, ok
}
func (e *watchdogEngine) Close() {
e.watchdog("Close", e.wrap.Close)
}

View File

@@ -15,7 +15,6 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/types/views"
"tailscale.com/wgengine/wgcfg"
)
@@ -56,14 +55,14 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: nm.PrivateKey,
Addresses: nm.Addresses,
Addresses: nm.GetAddresses().AsSlice(),
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
}
// Setup log IDs for data plane audit logging.
if nm.SelfNode.Valid() {
cfg.NodeID = nm.SelfNode.StableID()
canNetworkLog := views.SliceContains(nm.SelfNode.Capabilities(), tailcfg.CapabilityDataPlaneAuditLogs)
canNetworkLog := nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs)
if canNetworkLog && nm.SelfNode.DataPlaneAuditLogID() != "" && nm.DomainAuditLogID != "" {
nodeID, errNode := logid.ParsePrivateID(nm.SelfNode.DataPlaneAuditLogID())
if errNode != nil {

View File

@@ -24,9 +24,11 @@ func (src *Config) Clone() *Config {
*dst = *src
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
dst.DNS = append(src.DNS[:0:0], src.DNS...)
dst.Peers = make([]Peer, len(src.Peers))
for i := range dst.Peers {
dst.Peers[i] = *src.Peers[i].Clone()
if src.Peers != nil {
dst.Peers = make([]Peer, len(src.Peers))
for i := range dst.Peers {
dst.Peers[i] = *src.Peers[i].Clone()
}
}
return dst
}

View File

@@ -111,21 +111,6 @@ type Engine interface {
// If size is zero too small, it is ignored. See tailscale.PingOpts for details.
Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult))
// RegisterIPPortIdentity registers a given node (identified by its
// Tailscale IP) as temporarily having the given IP:port for whois lookups.
// The IP:port is generally a localhost IP and an ephemeral port, used
// while proxying connections to localhost when tailscaled is running
// in netstack mode.
RegisterIPPortIdentity(netip.AddrPort, netip.Addr)
// UnregisterIPPortIdentity removes a temporary IP:port registration
// made previously by RegisterIPPortIdentity.
UnregisterIPPortIdentity(netip.AddrPort)
// WhoIsIPPort looks up an IP:port in the temporary registrations,
// and returns a matching Tailscale IP, if it exists.
WhoIsIPPort(netip.AddrPort) (netip.Addr, bool)
// InstallCaptureHook registers a function to be called to capture
// packets traversing the data path. The hook can be uninstalled by
// calling this function with a nil value.

View File

@@ -545,3 +545,13 @@ prawn
lobster
chipmunk
tails
talpidae
shrew
talpa
mogera
scaptonyx
dymecodon
neurotrichini
uropsilus
desmana
fynbos