Compare commits
29 Commits
angott/use
...
aaron/win_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82dfe7f99 | ||
|
|
19a9d9037f | ||
|
|
4da0689c2c | ||
|
|
d06b48dd0a | ||
|
|
258f16f84b | ||
|
|
0d991249e1 | ||
|
|
d25217c9db | ||
|
|
98b5da47e8 | ||
|
|
a61caea911 | ||
|
|
3d37328af6 | ||
|
|
db2f37d7c6 | ||
|
|
9538e9f970 | ||
|
|
926c990a09 | ||
|
|
fb5ceb03e3 | ||
|
|
0f3c279b86 | ||
|
|
760b945bc0 | ||
|
|
8ab46952d4 | ||
|
|
f6845b10f6 | ||
|
|
e7727db553 | ||
|
|
335a5aaf9a | ||
|
|
4c693d2ee8 | ||
|
|
8428a64b56 | ||
|
|
1858ad65c8 | ||
|
|
85155ddaf3 | ||
|
|
dfefaa5e35 | ||
|
|
f3a5bfb1b9 | ||
|
|
7ce1c6f981 | ||
|
|
3421784e37 | ||
|
|
6e66e5beeb |
5
.github/workflows/govulncheck.yml
vendored
5
.github/workflows/govulncheck.yml
vendored
@@ -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"
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
15
ipn/serve.go
15
ipn/serve.go
@@ -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("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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] }
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
72
proxymap/proxymap.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
tsd/tsd.go
10
tsd/tsd.go
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
81
types/dnstype/dnstype_test.go
Normal file
81
types/dnstype/dnstype_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.ж)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
577
util/winutil/process_windows.go
Normal file
577
util/winutil/process_windows.go
Normal 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
|
||||
}
|
||||
17
util/winutil/process_windows_test.go
Normal file
17
util/winutil/process_windows_test.go
Normal 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: "))
|
||||
}
|
||||
}
|
||||
388
util/winutil/subprocess_windows_test.go
Normal file
388
util/winutil/subprocess_windows_test.go
Normal 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
|
||||
}
|
||||
40
util/winutil/testdata/testprocessattributes/main_windows.go
vendored
Normal file
40
util/winutil/testdata/testprocessattributes/main_windows.go
vendored
Normal 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()
|
||||
}
|
||||
57
util/winutil/testdata/testprocessattributes/tests_windows.go
vendored
Normal file
57
util/winutil/testdata/testprocessattributes/tests_windows.go
vendored
Normal 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")
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}{})
|
||||
|
||||
@@ -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{"*"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -545,3 +545,13 @@ prawn
|
||||
lobster
|
||||
chipmunk
|
||||
tails
|
||||
talpidae
|
||||
shrew
|
||||
talpa
|
||||
mogera
|
||||
scaptonyx
|
||||
dymecodon
|
||||
neurotrichini
|
||||
uropsilus
|
||||
desmana
|
||||
fynbos
|
||||
|
||||
Reference in New Issue
Block a user