diff --git a/api.md b/api.md
index 51b75719d8a55..e40d427553bea 100644
--- a/api.md
+++ b/api.md
@@ -209,10 +209,6 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
"192.68.0.21:59128"
],
- // derp (string) is the IP:port of the DERP server currently being used.
- // Learn about DERP servers at https://tailscale.com/kb/1232/.
- "derp":"",
-
// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
// vary based on the destination IP.
"mappingVariesByDestIP":false,
diff --git a/client/web/qnap.go b/client/web/qnap.go
index d3b1d8dd7a4c8..5ccd7679836a5 100644
--- a/client/web/qnap.go
+++ b/client/web/qnap.go
@@ -16,21 +16,23 @@ import (
"net/url"
)
-// authorizeQNAP authenticates the logged-in QNAP user and verifies
-// that they are authorized to use the web client. It returns true if the
-// request was handled and no further processing is required.
-func authorizeQNAP(w http.ResponseWriter, r *http.Request) (handled bool) {
+// authorizeQNAP authenticates the logged-in QNAP user and verifies that they
+// are authorized to use the web client.
+// It reports true if the request is authorized to continue, and false otherwise.
+// authorizeQNAP manages writing out any relevant authorization errors to the
+// ResponseWriter itself.
+func authorizeQNAP(w http.ResponseWriter, r *http.Request) (ok bool) {
_, resp, err := qnapAuthn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
- return true
+ return false
}
if resp.IsAdmin == 0 {
http.Error(w, "user is not an admin", http.StatusForbidden)
- return true
+ return false
}
- return false
+ return true
}
type qnapAuthResponse struct {
diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx
index 28350e52e5bc5..eb403a5e73d90 100644
--- a/client/web/src/components/app.tsx
+++ b/client/web/src/components/app.tsx
@@ -1,30 +1,123 @@
import React from "react"
import { Footer, Header, IP, State } from "src/components/legacy"
-import useNodeData from "src/hooks/node-data"
+import useNodeData, { NodeData } from "src/hooks/node-data"
+import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
+import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
+import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
export default function App() {
// TODO(sonia): use isPosting value from useNodeData
// to fill loading states.
const { data, refreshData, updateNode } = useNodeData()
- return (
+ if (!data) {
+ // TODO(sonia): add a loading view
+ return
Loading...
+ }
+
+ const needsLogin = data?.Status === "NeedsLogin" || data?.Status === "NoState"
+
+ return !needsLogin &&
+ (data.DebugMode === "login" || data.DebugMode === "full") ? (
+
+ {data.DebugMode === "login" ? (
+
+ ) : (
+
+ )}
+
+
+ ) : (
+ // Legacy client UI
- {!data ? (
- // TODO(sonia): add a loading view
-
Loading...
+
+
+
+
+
+
+
+ )
+}
+
+function LoginView(props: NodeData) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Owned by
+
+
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+ {props.Profile.LoginName}
+
+
+
+
+
+
+
+
+ {props.DeviceName}
+
+
{props.IP}
+
+
+
Access
+
+
+ >
+ )
+}
+
+function ManageView(props: NodeData) {
+ return (
+
+
+
+
+
{props.Profile.LoginName}
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+
+
+
+
This device
+
+
+ Tailscale is up and running. You can connect to this device from devices
+ in your tailnet by using its name or IP address.
+
+
+ )
+}
+
+function ProfilePic({ url }: { url: string }) {
+ return (
+
+ {url ? (
+
) : (
- <>
-
-
-
-
-
-
- >
+
)}
)
diff --git a/client/web/src/components/legacy.tsx b/client/web/src/components/legacy.tsx
index 5d7e269f12593..a6f8ca7de59b7 100644
--- a/client/web/src/components/legacy.tsx
+++ b/client/web/src/components/legacy.tsx
@@ -282,14 +282,14 @@ export function State({
}
}
-export function Footer(props: { data: NodeData }) {
- const { data } = props
-
+export function Footer(props: { licensesURL: string; className?: string }) {
return (
-
+
Open Source Licenses
diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts
index 53c92c6ecaa77..316c69b647e8d 100644
--- a/client/web/src/hooks/node-data.ts
+++ b/client/web/src/hooks/node-data.ts
@@ -15,6 +15,8 @@ export type NodeData = {
IsUnraid: boolean
UnraidToken: string
IPNVersion: string
+
+ DebugMode: "" | "login" | "full" // empty when not running in any debug mode
}
export type UserProfile = {
diff --git a/client/web/src/icons/connected-device.svg b/client/web/src/icons/connected-device.svg
new file mode 100644
index 0000000000000..74f6be8bc7146
--- /dev/null
+++ b/client/web/src/icons/connected-device.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/src/icons/tailscale-icon.svg b/client/web/src/icons/tailscale-icon.svg
new file mode 100644
index 0000000000000..d6052fe5e7cd6
--- /dev/null
+++ b/client/web/src/icons/tailscale-icon.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/src/icons/tailscale-logo.svg b/client/web/src/icons/tailscale-logo.svg
new file mode 100644
index 0000000000000..6d5c7ce0caae3
--- /dev/null
+++ b/client/web/src/icons/tailscale-logo.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/synology.go b/client/web/synology.go
index 7c3f82c11274c..5f36cc93e55a0 100644
--- a/client/web/synology.go
+++ b/client/web/synology.go
@@ -16,11 +16,13 @@ import (
)
// authorizeSynology authenticates the logged-in Synology user and verifies
-// that they are authorized to use the web client. It returns true if the
-// request was handled and no further processing is required.
-func authorizeSynology(w http.ResponseWriter, r *http.Request) (handled bool) {
+// that they are authorized to use the web client.
+// It reports true if the request is authorized to continue, and false otherwise.
+// authorizeSynology manages writing out any relevant authorization errors to the
+// ResponseWriter itself.
+func authorizeSynology(w http.ResponseWriter, r *http.Request) (ok bool) {
if synoTokenRedirect(w, r) {
- return true
+ return false
}
// authenticate the Synology user
@@ -28,7 +30,7 @@ func authorizeSynology(w http.ResponseWriter, r *http.Request) (handled bool) {
out, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, fmt.Sprintf("auth: %v: %s", err, out), http.StatusUnauthorized)
- return true
+ return false
}
user := strings.TrimSpace(string(out))
@@ -36,14 +38,14 @@ func authorizeSynology(w http.ResponseWriter, r *http.Request) (handled bool) {
isAdmin, err := groupmember.IsMemberOfGroup("administrators", user)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
- return true
+ return false
}
if !isAdmin {
http.Error(w, "not a member of administrators group", http.StatusForbidden)
- return true
+ return false
}
- return false
+ return true
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
diff --git a/client/web/tsconfig.json b/client/web/tsconfig.json
index 51c3de9d8c550..f87dd305812e6 100644
--- a/client/web/tsconfig.json
+++ b/client/web/tsconfig.json
@@ -10,6 +10,7 @@
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
+ "types": ["vite-plugin-svgr/client", "vite/client"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
diff --git a/client/web/web.go b/client/web/web.go
index d65ce09c37fa9..c76cd49555734 100644
--- a/client/web/web.go
+++ b/client/web/web.go
@@ -8,6 +8,7 @@ import (
"context"
"crypto/rand"
"encoding/json"
+ "errors"
"fmt"
"io"
"log"
@@ -17,6 +18,8 @@ import (
"path/filepath"
"slices"
"strings"
+ "sync"
+ "time"
"github.com/gorilla/csrf"
"tailscale.com/client/tailscale"
@@ -35,19 +38,81 @@ import (
type Server struct {
lc *tailscale.LocalClient
- devMode bool
+ devMode bool
+ tsDebugMode string
cgiMode bool
pathPrefix string
assetsHandler http.Handler // serves frontend assets
apiHandler http.Handler // serves api endpoints; csrf-protected
+
+ // browserSessions is an in-memory cache of browser sessions for the
+ // full management web client, which is only accessible over Tailscale.
+ //
+ // Users obtain a valid browser session by connecting to the web client
+ // over Tailscale and verifying their identity by authenticating on the
+ // control server.
+ //
+ // browserSessions get reset on every Server restart.
+ //
+ // The map provides a lookup of the session by cookie value
+ // (browserSession.ID => browserSession).
+ browserSessions sync.Map
+}
+
+const (
+ sessionCookieName = "TS-Web-Session"
+ sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
+)
+
+// browserSession holds data about a user's browser session
+// on the full management web client.
+type browserSession struct {
+ // ID is the unique identifier for the session.
+ // It is passed in the user's "TS-Web-Session" browser cookie.
+ ID string
+ SrcNode tailcfg.StableNodeID
+ SrcUser tailcfg.UserID
+ AuthURL string // control server URL for user to authenticate the session
+ Authenticated time.Time // when zero, authentication not complete
+}
+
+// isAuthorized reports true if the given session is authorized
+// to be used by its associated user to access the full management
+// web client.
+//
+// isAuthorized is true only when s.Authenticated is non-zero
+// (i.e. the user has authenticated the session) and the session
+// is not expired.
+// 2023-10-05: Sessions expire by default after 30 days.
+func (s *browserSession) isAuthorized() bool {
+ switch {
+ case s == nil:
+ return false
+ case s.Authenticated.IsZero():
+ return false // awaiting auth
+ case s.isExpired(): // TODO: add time field to server?
+ return false // expired
+ }
+ return true
+}
+
+// isExpired reports true if s is expired.
+// 2023-10-05: Sessions expire by default after 30 days.
+// If s.Authenticated is zero, isExpired reports false.
+func (s *browserSession) isExpired() bool {
+ return !s.Authenticated.IsZero() && s.Authenticated.Before(time.Now().Add(-sessionCookieExpiry)) // TODO: add time field to server?
}
// ServerOpts contains options for constructing a new Server.
type ServerOpts struct {
DevMode bool
+ // LoginOnly indicates that the server should only serve the minimal
+ // login client and not the full web client.
+ LoginOnly bool
+
// CGIMode indicates if the server is running as a CGI script.
CGIMode bool
@@ -68,9 +133,9 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
s = &Server{
devMode: opts.DevMode,
lc: opts.LocalClient,
- cgiMode: opts.CGIMode,
pathPrefix: opts.PathPrefix,
}
+ s.tsDebugMode = s.debugMode()
s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
// Create handler for "/api" requests with CSRF protection.
@@ -79,12 +144,33 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
// The client is secured by limiting the interface it listens on,
// or by authenticating requests before they reach the web client.
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
- s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
+ if s.tsDebugMode == "login" {
+ // For the login client, we don't serve the full web client API,
+ // only the login endpoints.
+ s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
+ s.lc.IncrementCounter(context.Background(), "web_login_client_initialization", 1)
+ } else {
+ s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
+ s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1)
+ }
- s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1)
return s, cleanup
}
+// debugMode returns the debug mode the web client is being run in.
+// The empty string is returned in the case that this instance is
+// not running in any debug mode.
+func (s *Server) debugMode() string {
+ if !s.devMode {
+ return "" // debug modes only available in dev
+ }
+ switch mode := os.Getenv("TS_DEBUG_WEB_CLIENT_MODE"); mode {
+ case "login", "full": // valid debug modes
+ return mode
+ }
+ return ""
+}
+
// ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := s.serve
@@ -97,53 +183,194 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler(w, r)
}
-// authorize checks if the request is authorized to access the web client for those platforms that support it.
-func authorize(w http.ResponseWriter, r *http.Request) (handled bool) {
- if strings.HasPrefix(r.URL.Path, "/assets/") {
- // don't require authorization for static assets
- return false
+func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/api/") {
+ // Pass API requests through to the API handler.
+ s.apiHandler.ServeHTTP(w, r)
+ return
+ }
+ if !s.devMode {
+ s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
}
+ s.assetsHandler.ServeHTTP(w, r)
+}
+// authorizePlatformRequest reports whether the request from the web client
+// is authorized to access the client for those platforms that support it.
+// It reports true if the request is authorized, and false otherwise.
+// authorizePlatformRequest manages writing out any relevant authorization
+// errors to the ResponseWriter itself.
+func authorizePlatformRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
switch distro.Get() {
case distro.Synology:
return authorizeSynology(w, r)
case distro.QNAP:
return authorizeQNAP(w, r)
}
-
- return false
+ return true
}
-func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
- switch {
- case authorize(w, r):
- // Authenticate and authorize the request for platforms that support it.
- // Return if the request was processed.
+// serveLoginAPI serves requests for the web login client.
+// It should only be called by Server.ServeHTTP, via Server.apiHandler,
+// which protects the handler using gorilla csrf.
+func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
+ // The login client is run directly from client plugins,
+ // so first authenticate and authorize the request for the host platform.
+ if ok := authorizePlatformRequest(w, r); !ok {
return
- case strings.HasPrefix(r.URL.Path, "/api/"):
- // Pass API requests through to the API handler.
- s.apiHandler.ServeHTTP(w, r)
+ }
+
+ w.Header().Set("X-CSRF-Token", csrf.Token(r))
+ if r.URL.Path != "/api/data" { // only endpoint allowed for login client
+ http.Error(w, "invalid endpoint", http.StatusNotFound)
return
+ }
+ switch r.Method {
+ case httpm.GET:
+ // TODO(soniaappasamy): we may want a minimal node data response here
+ s.serveGetNodeData(w, r)
+ case httpm.POST:
+ // TODO(soniaappasamy): implement
default:
- if !s.devMode {
- s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
- }
- s.assetsHandler.ServeHTTP(w, r)
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ }
+ return
+}
+
+var (
+ errNoSession = errors.New("no-browser-session")
+ errNotUsingTailscale = errors.New("not-using-tailscale")
+ errTaggedSource = errors.New("tagged-source")
+ errNotOwner = errors.New("not-owner")
+)
+
+// getTailscaleBrowserSession retrieves the browser session associated with
+// the request, if one exists.
+//
+// An error is returned in any of the following cases:
+//
+// - (errNotUsingTailscale) The request was not made over tailscale.
+//
+// - (errNoSession) The request does not have a session.
+//
+// - (errTaggedSource) The source is a tagged node. Users must use their
+// own user-owned devices to manage other nodes' web clients.
+//
+// - (errNotOwner) The source is not the owner of this client (if the
+// client is user-owned). Only the owner is allowed to manage the
+// node via the web client.
+//
+// If no error is returned, the browserSession is always non-nil.
+// getTailscaleBrowserSession does not check whether the session has been
+// authorized by the user. Callers can use browserSession.isAuthorized.
+func (s *Server) getTailscaleBrowserSession(r *http.Request) (*browserSession, error) {
+ whoIs, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
+ switch {
+ case err != nil:
+ return nil, errNotUsingTailscale
+ case whoIs.Node.IsTagged():
+ return nil, errTaggedSource
+ }
+ srcNode := whoIs.Node.StableID
+ srcUser := whoIs.UserProfile.ID
+
+ status, err := s.lc.StatusWithoutPeers(r.Context())
+ switch {
+ case err != nil:
+ return nil, err
+ case status.Self == nil:
+ return nil, errors.New("missing self node in tailscale status")
+ case !status.Self.IsTagged() && status.Self.UserID != srcUser:
+ return nil, errNotOwner
+ }
+
+ cookie, err := r.Cookie(sessionCookieName)
+ if errors.Is(err, http.ErrNoCookie) {
+ return nil, errNoSession
+ } else if err != nil {
+ return nil, err
+ }
+ v, ok := s.browserSessions.Load(cookie.Value)
+ if !ok {
+ return nil, errNoSession
+ }
+ session := v.(*browserSession)
+ if session.SrcNode != srcNode || session.SrcUser != srcUser {
+ // In this case the browser cookie is associated with another tailscale node.
+ // Maybe the source browser's machine was logged out and then back in as a different node.
+ // Return errNoSession because there is no session for this user.
+ return nil, errNoSession
+ } else if session.isExpired() {
+ // Session expired, remove from session map and return errNoSession.
+ s.browserSessions.Delete(session.ID)
+ return nil, errNoSession
+ }
+ return session, nil
+}
+
+type authResponse struct {
+ OK bool `json:"ok"` // true when user has valid auth session
+ AuthURL string `json:"authUrl,omitempty"` // filled when user has control auth action to take
+ Error string `json:"error,omitempty"` // filled when Ok is false
+}
+
+func (s *Server) serveTailscaleAuth(w http.ResponseWriter, r *http.Request) {
+ var resp authResponse
+
+ session, err := s.getTailscaleBrowserSession(r)
+ switch {
+ case err != nil && !errors.Is(err, errNoSession):
+ resp = authResponse{OK: false, Error: err.Error()}
+ case session == nil:
+ // TODO(tailscale/corp#14335): Create a new auth path from control,
+ // and store back to s.browserSessions and request cookie.
+ case !session.isAuthorized():
+ // TODO(tailscale/corp#14335): Check on the session auth path status from control,
+ // and store back to s.browserSessions.
+ default:
+ resp = authResponse{OK: true}
+ }
+
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+ w.Header().Set("Content-Type", "application/json")
}
// serveAPI serves requests for the web client api.
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
// which protects the handler using gorilla csrf.
func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
+ if s.tsDebugMode == "full" {
+ // tailscale/corp#14335: Only restrict to tailscale auth in debug "full" web client mode.
+ // TODO(sonia,will): Switch serveAPI over to always require TS auth when we're ready
+ // to remove the debug flags.
+ // For now, existing client uses platform auth (else case below).
+
+ if r.URL.Path == "/api/auth" {
+ // Serve auth, which creates a new session for the user to authenticate,
+ // in the case that the request doesn't already have one.
+ s.serveTailscaleAuth(w, r)
+ return
+ }
+ // For all other endpoints, require a valid session to proceed.
+ session, err := s.getTailscaleBrowserSession(r)
+ if err != nil || !session.isAuthorized() {
+ http.Error(w, "no valid session", http.StatusUnauthorized)
+ return
+ }
+ } else if ok := authorizePlatformRequest(w, r); !ok {
+ return
+ }
+
w.Header().Set("X-CSRF-Token", csrf.Token(r))
path := strings.TrimPrefix(r.URL.Path, "/api")
switch {
case path == "/data":
switch r.Method {
case httpm.GET:
- s.serveGetNodeDataJSON(w, r)
+ s.serveGetNodeData(w, r)
case httpm.POST:
s.servePostNodeUpdate(w, r)
default:
@@ -171,16 +398,19 @@ type nodeData struct {
IsUnraid bool
UnraidToken string
IPNVersion string
+ DebugMode string // empty when not running in any debug mode
}
-func (s *Server) getNodeData(ctx context.Context) (*nodeData, error) {
- st, err := s.lc.Status(ctx)
+func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
+ st, err := s.lc.Status(r.Context())
if err != nil {
- return nil, err
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
- prefs, err := s.lc.GetPrefs(ctx)
+ prefs, err := s.lc.GetPrefs(r.Context())
if err != nil {
- return nil, err
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
}
profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0]
@@ -196,6 +426,7 @@ func (s *Server) getNodeData(ctx context.Context) (*nodeData, error) {
IsUnraid: distro.Get() == distro.Unraid,
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
IPNVersion: versionShort,
+ DebugMode: s.tsDebugMode,
}
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
@@ -212,15 +443,6 @@ func (s *Server) getNodeData(ctx context.Context) (*nodeData, error) {
if len(st.TailscaleIPs) != 0 {
data.IP = st.TailscaleIPs[0].String()
}
- return data, nil
-}
-
-func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request) {
- data, err := s.getNodeData(r.Context())
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
if err := json.NewEncoder(w).Encode(*data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/client/web/web_test.go b/client/web/web_test.go
index 8289d553690ac..0c49d2109874e 100644
--- a/client/web/web_test.go
+++ b/client/web/web_test.go
@@ -4,6 +4,8 @@
package web
import (
+ "encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -11,9 +13,15 @@ import (
"net/url"
"strings"
"testing"
+ "time"
+ "github.com/google/go-cmp/cmp"
"tailscale.com/client/tailscale"
+ "tailscale.com/client/tailscale/apitype"
+ "tailscale.com/ipn/ipnstate"
"tailscale.com/net/memnet"
+ "tailscale.com/tailcfg"
+ "tailscale.com/types/views"
)
func TestQnapAuthnURL(t *testing.T) {
@@ -129,3 +137,191 @@ func TestServeAPI(t *testing.T) {
})
}
}
+
+func TestGetTailscaleBrowserSession(t *testing.T) {
+ userA := &tailcfg.UserProfile{ID: tailcfg.UserID(1)}
+ userB := &tailcfg.UserProfile{ID: tailcfg.UserID(2)}
+
+ userANodeIP := "100.100.100.101"
+ userBNodeIP := "100.100.100.102"
+ taggedNodeIP := "100.100.100.103"
+
+ var selfNode *ipnstate.PeerStatus
+ tags := views.SliceOf([]string{"tag:server"})
+ tailnetNodes := map[string]*apitype.WhoIsResponse{
+ userANodeIP: {
+ Node: &tailcfg.Node{StableID: "Node1"},
+ UserProfile: userA,
+ },
+ userBNodeIP: {
+ Node: &tailcfg.Node{StableID: "Node2"},
+ UserProfile: userB,
+ },
+ taggedNodeIP: {
+ Node: &tailcfg.Node{StableID: "Node3", Tags: tags.AsSlice()},
+ },
+ }
+
+ lal := memnet.Listen("local-tailscaled.sock:80")
+ defer lal.Close()
+ // Serve a testing localapi handler so we can simulate
+ // whois responses without a functioning tailnet.
+ localapi := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/localapi/v0/whois":
+ addr := r.URL.Query().Get("addr")
+ if addr == "" {
+ t.Fatalf("/whois call missing \"addr\" query")
+ }
+ if node := tailnetNodes[addr]; node != nil {
+ if err := json.NewEncoder(w).Encode(&node); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ return
+ }
+ http.Error(w, "not a node", http.StatusUnauthorized)
+ return
+ case "/localapi/v0/status":
+ status := ipnstate.Status{Self: selfNode}
+ if err := json.NewEncoder(w).Encode(status); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ return
+ default:
+ // Only the above two endpoints get triggered from getTailscaleBrowserSession.
+ // No need to mock any of the other localapi endpoint.
+ t.Fatalf("unhandled localapi test endpoint %q, add to localapi handler func in test", r.URL.Path)
+ }
+ })}
+ defer localapi.Close()
+ go localapi.Serve(lal)
+
+ s := &Server{lc: &tailscale.LocalClient{Dial: lal.Dial}}
+
+ // Add some browser sessions to cache state.
+ userASession := &browserSession{
+ ID: "cookie1",
+ SrcNode: "Node1",
+ SrcUser: userA.ID,
+ Authenticated: time.Time{}, // not yet authenticated
+ }
+ userBSession := &browserSession{
+ ID: "cookie2",
+ SrcNode: "Node2",
+ SrcUser: userB.ID,
+ Authenticated: time.Now().Add(-2 * sessionCookieExpiry), // expired
+ }
+ userASessionAuthorized := &browserSession{
+ ID: "cookie3",
+ SrcNode: "Node1",
+ SrcUser: userA.ID,
+ Authenticated: time.Now(), // authenticated and not expired
+ }
+ s.browserSessions.Store(userASession.ID, userASession)
+ s.browserSessions.Store(userBSession.ID, userBSession)
+ s.browserSessions.Store(userASessionAuthorized.ID, userASessionAuthorized)
+
+ tests := []struct {
+ name string
+ selfNode *ipnstate.PeerStatus
+ remoteAddr string
+ cookie string
+
+ wantSession *browserSession
+ wantError error
+ wantIsAuthorized bool // response from session.isAuthorized
+ }{
+ {
+ name: "not-connected-over-tailscale",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: "77.77.77.77",
+ wantSession: nil,
+ wantError: errNotUsingTailscale,
+ },
+ {
+ name: "no-session-user-self-node",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: userANodeIP,
+ cookie: "not-a-cookie",
+ wantSession: nil,
+ wantError: errNoSession,
+ },
+ {
+ name: "no-session-tagged-self-node",
+ selfNode: &ipnstate.PeerStatus{ID: "self", Tags: &tags},
+ remoteAddr: userANodeIP,
+ wantSession: nil,
+ wantError: errNoSession,
+ },
+ {
+ name: "not-owner",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: userBNodeIP,
+ wantSession: nil,
+ wantError: errNotOwner,
+ },
+ {
+ name: "tagged-source",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: taggedNodeIP,
+ wantSession: nil,
+ wantError: errTaggedSource,
+ },
+ {
+ name: "has-session",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: userANodeIP,
+ cookie: userASession.ID,
+ wantSession: userASession,
+ wantError: nil,
+ },
+ {
+ name: "has-authorized-session",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userA.ID},
+ remoteAddr: userANodeIP,
+ cookie: userASessionAuthorized.ID,
+ wantSession: userASessionAuthorized,
+ wantError: nil,
+ wantIsAuthorized: true,
+ },
+ {
+ name: "session-associated-with-different-source",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userB.ID},
+ remoteAddr: userBNodeIP,
+ cookie: userASession.ID,
+ wantSession: nil,
+ wantError: errNoSession,
+ },
+ {
+ name: "session-expired",
+ selfNode: &ipnstate.PeerStatus{ID: "self", UserID: userB.ID},
+ remoteAddr: userBNodeIP,
+ cookie: userBSession.ID,
+ wantSession: nil,
+ wantError: errNoSession,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ selfNode = tt.selfNode
+ r := &http.Request{RemoteAddr: tt.remoteAddr, Header: http.Header{}}
+ if tt.cookie != "" {
+ r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tt.cookie})
+ }
+ session, err := s.getTailscaleBrowserSession(r)
+ if !errors.Is(err, tt.wantError) {
+ t.Errorf("wrong error; want=%v, got=%v", tt.wantError, err)
+ }
+ if diff := cmp.Diff(session, tt.wantSession); diff != "" {
+ t.Errorf("wrong session; (-got+want):%v", diff)
+ }
+ if gotIsAuthorized := session.isAuthorized(); gotIsAuthorized != tt.wantIsAuthorized {
+ t.Errorf("wrong isAuthorized; want=%v, got=%v", tt.wantIsAuthorized, gotIsAuthorized)
+ }
+ })
+ }
+}
diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go
index b788c73212f9b..ccd67246b44b2 100644
--- a/clientupdate/clientupdate.go
+++ b/clientupdate/clientupdate.go
@@ -30,6 +30,7 @@ import (
"github.com/google/uuid"
"tailscale.com/clientupdate/distsign"
"tailscale.com/types/logger"
+ "tailscale.com/util/cmpver"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -77,6 +78,10 @@ type Arguments struct {
AppStore bool
// Logf is a logger for update progress messages.
Logf logger.Logf
+ // Stdout and Stderr should be used for output instead of os.Stdout and
+ // os.Stderr.
+ Stdout io.Writer
+ Stderr io.Writer
// Confirm is called when a new version is available and should return true
// if this new version should be installed. When Confirm returns false, the
// update is aborted.
@@ -108,6 +113,12 @@ func NewUpdater(args Arguments) (*Updater, error) {
up := Updater{
Arguments: args,
}
+ if up.Stdout == nil {
+ up.Stdout = os.Stdout
+ }
+ if up.Stderr == nil {
+ up.Stderr = os.Stderr
+ }
up.Update = up.getUpdateFunction()
if up.Update == nil {
return nil, errors.ErrUnsupported
@@ -201,9 +212,13 @@ func Update(args Arguments) error {
}
func (up *Updater) confirm(ver string) bool {
- if version.Short() == ver {
+ switch cmpver.Compare(version.Short(), ver) {
+ case 0:
up.Logf("already running %v; no update needed", ver)
return false
+ case 1:
+ up.Logf("installed version %v is newer than the latest available version %v; no update needed", version.Short(), ver)
+ return false
}
if up.Confirm != nil {
return up.Confirm(ver)
@@ -256,9 +271,9 @@ func (up *Updater) updateSynology() error {
// connected over tailscale ssh and this parent process dies. Otherwise, if
// you abort synopkg install mid-way, tailscaled is not restarted.
cmd := exec.Command("nohup", "synopkg", "install", spkPath)
- // Don't attach cmd.Stdout to os.Stdout because nohup will redirect that
- // into nohup.out file. synopkg doesn't have any progress output anyway, it
- // just spits out a JSON result when done.
+ // Don't attach cmd.Stdout to Stdout because nohup will redirect that into
+ // nohup.out file. synopkg doesn't have any progress output anyway, it just
+ // spits out a JSON result when done.
out, err := cmd.CombinedOutput()
if err != nil {
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
@@ -369,15 +384,15 @@ func (up *Updater) updateDebLike() error {
// we're not updating them:
"-o", "APT::Get::List-Cleanup=0",
)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -491,8 +506,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -577,8 +592,8 @@ func (up *Updater) updateAlpineLike() (err error) {
}
cmd := exec.Command("apk", "upgrade", "tailscale")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using apk: %w", err)
}
@@ -634,8 +649,8 @@ func (up *Updater) updateMacAppStore() error {
}
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
@@ -726,8 +741,8 @@ func (up *Updater) updateWindows() error {
cmd := exec.Command(selfCopy, "update")
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
- cmd.Stdout = os.Stderr
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stderr
+ cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
return err
@@ -743,8 +758,8 @@ func (up *Updater) installMSI(msi string) error {
for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
cmd.Dir = filepath.Dir(msi)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err == nil {
@@ -757,8 +772,8 @@ func (up *Updater) installMSI(msi string) error {
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
up.Logf("msiexec uninstall: %v", err)
@@ -846,8 +861,8 @@ func (up *Updater) updateFreeBSD() (err error) {
}
cmd := exec.Command("pkg", "upgrade", "tailscale")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ cmd.Stdout = up.Stdout
+ cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pkg: %w", err)
}
diff --git a/clientupdate/distsign/distsign.go b/clientupdate/distsign/distsign.go
index b48321f8f5f10..eba4b9267b119 100644
--- a/clientupdate/distsign/distsign.go
+++ b/clientupdate/distsign/distsign.go
@@ -57,6 +57,7 @@ import (
"golang.org/x/crypto/blake2s"
"tailscale.com/net/tshttpproxy"
"tailscale.com/types/logger"
+ "tailscale.com/util/httpm"
"tailscale.com/util/must"
)
@@ -335,7 +336,7 @@ func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]
quickCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
- headReq := must.Get(http.NewRequestWithContext(quickCtx, http.MethodHead, url, nil))
+ headReq := must.Get(http.NewRequestWithContext(quickCtx, httpm.HEAD, url, nil))
res, err := hc.Do(headReq)
if err != nil {
@@ -349,7 +350,7 @@ func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]
}
c.logf("Download size: %v", res.ContentLength)
- dlReq := must.Get(http.NewRequestWithContext(ctx, http.MethodGet, url, nil))
+ dlReq := must.Get(http.NewRequestWithContext(ctx, httpm.GET, url, nil))
dlRes, err := hc.Do(dlReq)
if err != nil {
return nil, 0, err
diff --git a/cmd/cloner/cloner.go b/cmd/cloner/cloner.go
index 5a94fa97dae05..8e2944115892c 100644
--- a/cmd/cloner/cloner.go
+++ b/cmd/cloner/cloner.go
@@ -128,7 +128,9 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic {
it.Import("tailscale.com/types/ptr")
+ writef("if src.%s[i] == nil { dst.%s[i] = nil } else {", fname, fname)
writef("\tdst.%s[i] = ptr.To(*src.%s[i])", fname, fname)
+ writef("}")
} else {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
}
diff --git a/cmd/cloner/cloner_test.go b/cmd/cloner/cloner_test.go
new file mode 100644
index 0000000000000..d8d5df3cb040c
--- /dev/null
+++ b/cmd/cloner/cloner_test.go
@@ -0,0 +1,60 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+package main
+
+import (
+ "reflect"
+ "testing"
+
+ "tailscale.com/cmd/cloner/clonerex"
+)
+
+func TestSliceContainer(t *testing.T) {
+ num := 5
+ examples := []struct {
+ name string
+ in *clonerex.SliceContainer
+ }{
+ {
+ name: "nil",
+ in: nil,
+ },
+ {
+ name: "zero",
+ in: &clonerex.SliceContainer{},
+ },
+ {
+ name: "empty",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{},
+ },
+ },
+ {
+ name: "nils",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{nil, nil, nil, nil, nil},
+ },
+ },
+ {
+ name: "one",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{&num},
+ },
+ },
+ {
+ name: "several",
+ in: &clonerex.SliceContainer{
+ Slice: []*int{&num, &num, &num, &num, &num},
+ },
+ },
+ }
+
+ for _, ex := range examples {
+ t.Run(ex.name, func(t *testing.T) {
+ out := ex.in.Clone()
+ if !reflect.DeepEqual(ex.in, out) {
+ t.Errorf("Clone() = %v, want %v", out, ex.in)
+ }
+ })
+ }
+}
diff --git a/cmd/cloner/clonerex/clonerex.go b/cmd/cloner/clonerex/clonerex.go
new file mode 100644
index 0000000000000..7b96278e08c57
--- /dev/null
+++ b/cmd/cloner/clonerex/clonerex.go
@@ -0,0 +1,10 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer
+
+package clonerex
+
+type SliceContainer struct {
+ Slice []*int
+}
diff --git a/cmd/cloner/clonerex/clonerex_clone.go b/cmd/cloner/clonerex/clonerex_clone.go
new file mode 100644
index 0000000000000..e334a4e3a1bf4
--- /dev/null
+++ b/cmd/cloner/clonerex/clonerex_clone.go
@@ -0,0 +1,54 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
+
+package clonerex
+
+import (
+ "tailscale.com/types/ptr"
+)
+
+// Clone makes a deep copy of SliceContainer.
+// The result aliases no memory with the original.
+func (src *SliceContainer) Clone() *SliceContainer {
+ if src == nil {
+ return nil
+ }
+ dst := new(SliceContainer)
+ *dst = *src
+ if src.Slice != nil {
+ dst.Slice = make([]*int, len(src.Slice))
+ for i := range dst.Slice {
+ if src.Slice[i] == nil {
+ dst.Slice[i] = nil
+ } else {
+ dst.Slice[i] = ptr.To(*src.Slice[i])
+ }
+ }
+ }
+ return dst
+}
+
+// A compilation failure here means this code must be regenerated, with the command at the top of this file.
+var _SliceContainerCloneNeedsRegeneration = SliceContainer(struct {
+ Slice []*int
+}{})
+
+// Clone duplicates src into dst and reports whether it succeeded.
+// To succeed, must be of types <*T, *T> or <*T, **T>,
+// where T is one of SliceContainer.
+func Clone(dst, src any) bool {
+ switch src := src.(type) {
+ case *SliceContainer:
+ switch dst := dst.(type) {
+ case *SliceContainer:
+ *dst = *src.Clone()
+ return true
+ case **SliceContainer:
+ *dst = src.Clone()
+ return true
+ }
+ }
+ return false
+}
diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go
index 8934950635e19..09dc5f146b160 100644
--- a/cmd/containerboot/main.go
+++ b/cmd/containerboot/main.go
@@ -36,9 +36,15 @@
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
// be created.
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
-// logged in. If false (the default, for backwards
-// compatibility), forcibly log in every time the
-// container starts.
+// logged in. If false, forcibly log in every time the container starts.
+// The default until 1.50.0 was false, but that was misleading: until
+// 1.50, containerboot used `tailscale up` which would ignore an authkey
+// argument if there was already a node key. Effectively, this behaved
+// as though TS_AUTH_ONCE were always true.
+// In 1.50.0 the change was made to use `tailscale login` instead of `up`,
+// and login will reauthenticate every time it is given an authkey.
+// In 1.50.1 we set the TS_AUTH_ONCE to true, to match the previously
+// observed behavior.
// - TS_SERVE_CONFIG: if specified, is the file path where the ipn.ServeConfig is located.
// It will be applied once tailscaled is up and running. If the file contains
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
@@ -103,7 +109,7 @@ func main() {
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
- AuthOnce: defaultBool("TS_AUTH_ONCE", false),
+ AuthOnce: defaultBool("TS_AUTH_ONCE", true),
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
}
@@ -252,10 +258,13 @@ authLoop:
if err := tailscaleSet(ctx, cfg); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
- // Remove any serve config that may have been set by a previous
- // run of containerboot.
- if err := client.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
- log.Fatalf("failed to unset serve config: %v", err)
+
+ if cfg.ServeConfigPath != "" {
+ // Remove any serve config that may have been set by a previous run of
+ // containerboot, but only if we're providing a new one.
+ if err := client.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
+ log.Fatalf("failed to unset serve config: %v", err)
+ }
}
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go
index 2561e6724b4c0..b7147b75fcde5 100644
--- a/cmd/containerboot/main_test.go
+++ b/cmd/containerboot/main_test.go
@@ -129,7 +129,10 @@ func TestContainerBoot(t *testing.T) {
{
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
Name: "no_args",
- Env: nil,
+ Env: map[string]string{
+ "TS_AUTH_ONCE": "false",
+ },
+
Phases: []phase{
{
WantCmds: []string{
@@ -149,7 +152,8 @@ func TestContainerBoot(t *testing.T) {
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey",
Env: map[string]string{
- "TS_AUTHKEY": "tskey-key",
+ "TS_AUTHKEY": "tskey-key",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -170,7 +174,8 @@ func TestContainerBoot(t *testing.T) {
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey-old-flag",
Env: map[string]string{
- "TS_AUTH_KEY": "tskey-key",
+ "TS_AUTH_KEY": "tskey-key",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -192,6 +197,7 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -211,8 +217,9 @@ func TestContainerBoot(t *testing.T) {
{
Name: "routes",
Env: map[string]string{
- "TS_AUTHKEY": "tskey-key",
- "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
+ "TS_AUTHKEY": "tskey-key",
+ "TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -239,6 +246,7 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
"TS_USERSPACE": "false",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -265,6 +273,7 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1::/64",
"TS_USERSPACE": "false",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -291,6 +300,7 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1.2.3.0/24",
"TS_USERSPACE": "false",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -317,6 +327,7 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_DEST_IP": "1.2.3.4",
"TS_USERSPACE": "false",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -341,6 +352,7 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_TAILNET_TARGET_IP": "100.99.99.99",
"TS_USERSPACE": "false",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -393,6 +405,7 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
+ "TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -430,6 +443,7 @@ func TestContainerBoot(t *testing.T) {
"TS_KUBE_SECRET": "",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
"TS_AUTHKEY": "tskey-key",
+ "TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{},
Phases: []phase{
@@ -455,6 +469,7 @@ func TestContainerBoot(t *testing.T) {
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTHKEY": "tskey-key",
+ "TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{},
KubeDenyPatch: true,
@@ -524,6 +539,7 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
+ "TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -575,6 +591,7 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_SOCKS5_SERVER": "localhost:1080",
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -595,6 +612,7 @@ func TestContainerBoot(t *testing.T) {
Name: "dns",
Env: map[string]string{
"TS_ACCEPT_DNS": "true",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -616,6 +634,7 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_EXTRA_ARGS": "--widget=rotated",
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
@@ -635,7 +654,8 @@ func TestContainerBoot(t *testing.T) {
{
Name: "hostname",
Env: map[string]string{
- "TS_HOSTNAME": "my-server",
+ "TS_HOSTNAME": "my-server",
+ "TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt
index a147524e178f2..0e40377c75ec2 100644
--- a/cmd/derper/depaware.txt
+++ b/cmd/derper/depaware.txt
@@ -16,7 +16,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
- github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
+ github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil
+ github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@@ -78,6 +79,22 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
+ gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
+ gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
+ 💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
+ gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
+ 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
+ gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
+ gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
+ gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
+ 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
+ gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
+ 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
+ gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
+ gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
+ gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
+ gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
+ gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@@ -169,7 +186,6 @@ 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
@@ -221,7 +237,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
- encoding/base32 from tailscale.com/tka
+ encoding/base32 from tailscale.com/tka+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
@@ -270,7 +286,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
- slices from tailscale.com/ipn+
+ slices from tailscale.com/ipn/ipnstate+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
diff --git a/cmd/k8s-operator/ingress.go b/cmd/k8s-operator/ingress.go
index 72291afe803af..a3c7e1cd53d54 100644
--- a/cmd/k8s-operator/ingress.go
+++ b/cmd/k8s-operator/ingress.go
@@ -8,11 +8,11 @@ package main
import (
"context"
"fmt"
+ "slices"
"strings"
"sync"
"go.uber.org/zap"
- "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/cmd/k8s-operator/svc.go b/cmd/k8s-operator/svc.go
index 4fe8e44de1bd7..701c3fb18739f 100644
--- a/cmd/k8s-operator/svc.go
+++ b/cmd/k8s-operator/svc.go
@@ -9,11 +9,11 @@ import (
"context"
"fmt"
"net/netip"
+ "slices"
"strings"
"sync"
"go.uber.org/zap"
- "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
diff --git a/cmd/sniproxy/sniproxy.go b/cmd/sniproxy/sniproxy.go
index 6a410418da3dd..5be6e5afe0454 100644
--- a/cmd/sniproxy/sniproxy.go
+++ b/cmd/sniproxy/sniproxy.go
@@ -75,6 +75,7 @@ func main() {
wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
+ hostname = fs.String("hostname", "", "Hostname to register the service under")
)
err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
@@ -89,6 +90,7 @@ func main() {
var s server
s.ts.Port = uint16(*wgPort)
+ s.ts.Hostname = *hostname
defer s.ts.Close()
lc, err := s.ts.LocalClient()
diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go
index c8c0f75c0efd5..3a429a82aeaa6 100644
--- a/cmd/tailscale/cli/debug.go
+++ b/cmd/tailscale/cli/debug.go
@@ -63,9 +63,10 @@ var debugCmd = &ffcli.Command{
ShortHelp: "print DERP map",
},
{
- Name: "component-logs",
- Exec: runDebugComponentLogs,
- ShortHelp: "enable/disable debug logs for a component",
+ Name: "component-logs",
+ Exec: runDebugComponentLogs,
+ ShortHelp: "enable/disable debug logs for a component",
+ ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
@@ -138,6 +139,11 @@ var debugCmd = &ffcli.Command{
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
},
+ {
+ Name: "pick-new-derp",
+ Exec: localAPIAction("pick-new-derp"),
+ ShortHelp: "switch to some other random DERP home region for a short time",
+ },
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
@@ -724,7 +730,7 @@ var debugComponentLogsArgs struct {
func runDebugComponentLogs(ctx context.Context, args []string) error {
if len(args) != 1 {
- return errors.New("usage: debug component-logs ")
+ return errors.New("usage: debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]")
}
component := args[0]
dur := debugComponentLogsArgs.forDur
diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go
index 875c5f38e991b..9c0dea1b87a14 100644
--- a/cmd/tailscale/cli/funnel.go
+++ b/cmd/tailscale/cli/funnel.go
@@ -164,12 +164,12 @@ func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status,
// the feature flag on.
// TODO(sonia,tailscale/corp#10577): Remove this fallback once the
// control flag is turned on for all domains.
- if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
+ if err := ipn.CheckFunnelAccess(port, st.Self); err != nil {
return err
}
default:
// Done with enablement, make sure the requested port is allowed.
- if err := ipn.CheckFunnelPort(port, st.Self.Capabilities); err != nil {
+ if err := ipn.CheckFunnelPort(port, st.Self); err != nil {
return err
}
}
diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go
index d589da3ae8c34..faa2a1ec23b3e 100644
--- a/cmd/tailscale/cli/set.go
+++ b/cmd/tailscale/cli/set.go
@@ -49,6 +49,7 @@ type setArgsT struct {
forceDaemon bool
updateCheck bool
updateApply bool
+ postureChecking bool
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -66,6 +67,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
+ setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
+
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -108,6 +111,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
Check: setArgs.updateCheck,
Apply: setArgs.updateApply,
},
+ PostureChecking: setArgs.postureChecking,
},
}
diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go
index d70463b8f1f4f..a414e95d507db 100644
--- a/cmd/tailscale/cli/status.go
+++ b/cmd/tailscale/cli/status.go
@@ -25,6 +25,7 @@ import (
"tailscale.com/net/interfaces"
"tailscale.com/util/cmpx"
"tailscale.com/util/dnsname"
+ "tailscale.com/version"
)
var statusCmd = &ffcli.Command{
@@ -237,7 +238,7 @@ func runStatus(ctx context.Context, args []string) error {
}
printFunnelStatus(ctx)
if cv := st.ClientVersion; cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
- printf("# New Tailscale version is available: %q, run `tailscale update` to update.\n", cv.LatestVersion)
+ printf("# Update available: %v -> %v, run `tailscale update` or `tailscale set --auto-update` to update.\n", version.Short(), cv.LatestVersion)
}
return nil
}
diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go
index 1ff498214ea16..8dab79e9fa40b 100644
--- a/cmd/tailscale/cli/up.go
+++ b/cmd/tailscale/cli/up.go
@@ -114,6 +114,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
+
if safesocket.GOOSUsesPeerCreds(goos) {
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -499,6 +500,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
startLoginInteractive := func() { loginOnce.Do(func() { localClient.StartLoginInteractive(ctx) }) }
go func() {
+ var cv *tailcfg.ClientVersion
for {
n, err := watcher.Next()
if err != nil {
@@ -509,6 +511,9 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
msg := *n.ErrMessage
fatalf("backend error: %v\n", msg)
}
+ if n.ClientVersion != nil {
+ cv = n.ClientVersion
+ }
if s := n.State; s != nil {
switch *s {
case ipn.NeedsLogin:
@@ -527,6 +532,11 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
} else if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(Stderr, "Success.\n")
+ if cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
+ fmt.Fprintf(Stderr, "\nUpdate available: %v -> %v\n", version.Short(), cv.LatestVersion)
+ fmt.Fprintln(Stderr, "Changelog: https://tailscale.com/changelog/#client")
+ fmt.Fprintln(Stderr, "Run `tailscale update` or `tailscale set --auto-update` to update")
+ }
}
select {
case running <- true:
@@ -716,6 +726,7 @@ func init() {
addPrefFlagMapping("nickname", "ProfileName")
addPrefFlagMapping("update-check", "AutoUpdate")
addPrefFlagMapping("auto-update", "AutoUpdate")
+ addPrefFlagMapping("posture-checking", "PostureChecking")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {
diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go
index 5ee47ac972641..84742a7d6be1a 100644
--- a/cmd/tailscale/cli/update.go
+++ b/cmd/tailscale/cli/update.go
@@ -63,7 +63,9 @@ func runUpdate(ctx context.Context, args []string) error {
err := clientupdate.Update(clientupdate.Arguments{
Version: ver,
AppStore: updateArgs.appStore,
- Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
+ Logf: func(f string, a ...any) { printf(f+"\n", a...) },
+ Stdout: Stdout,
+ Stderr: Stderr,
Confirm: confirmUpdate,
})
if errors.Is(err, errors.ErrUnsupported) {
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index fcda084e44bff..715596c43cddc 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -17,6 +17,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/fxamacker/cbor/v2 from tailscale.com/tka
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
+ github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@@ -42,6 +43,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
+ github.com/peterbourgon/ff/v3/internal from github.com/peterbourgon/ff/v3
github.com/pkg/errors from github.com/gorilla/csrf
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
@@ -63,6 +65,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
go4.org/netipx from tailscale.com/wgengine/filter+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
+ gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
+ gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
+ 💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
+ gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
+ 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
+ gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
+ gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
+ gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
+ 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
+ gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
+ 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
+ gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
+ gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
+ gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
+ gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
+ gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
@@ -140,7 +158,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/views from tailscale.com/tailcfg+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
- W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
+ tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
@@ -176,7 +194,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+
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index 8e2cebf21cddd..b1cb0d2956bf6 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -34,7 +34,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
+ L github.com/aws/aws-sdk-go-v2/internal/auth from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
+ L github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
@@ -65,6 +67,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
+ L github.com/aws/smithy-go/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
@@ -83,6 +86,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
+ LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
@@ -167,14 +171,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
- gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2
- 💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+
+ gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
+ 💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
- gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/bufferv2+
+ gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer+
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
@@ -182,7 +186,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
- gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/bufferv2+
+ gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -235,13 +239,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
- 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnserver+
+ 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
- tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
+ tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
@@ -289,12 +293,14 @@ 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/posture 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
tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
+ tailscale.com/taildrop from tailscale.com/ipn/ipnlocal
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
@@ -325,7 +331,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
- LW tailscale.com/util/cmpver from tailscale.com/net/dns+
+ tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
@@ -343,12 +349,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
+ tailscale.com/util/race from tailscale.com/net/dns/resolver
tailscale.com/util/racebuild from tailscale.com/logpolicy
- tailscale.com/util/rands from tailscale.com/ipn/localapi+
+ tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
+ tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
@@ -380,7 +388,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
- LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
+ LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
@@ -465,7 +473,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
flag from net/http/httptest+
fmt from compress/flate+
hash from crypto+
- hash/adler32 from tailscale.com/ipn/ipnlocal+
+ hash/adler32 from compress/zlib+
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem
@@ -504,7 +512,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from github.com/klauspost/compress/zstd+
- runtime/pprof from net/http/pprof+
+ runtime/pprof from tailscale.com/ipn/ipnlocal+
runtime/trace from net/http/pprof
slices from tailscale.com/wgengine/magicsock+
sort from compress/flate+
diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go
index 0d056250e8d86..e3e08f28a5e2b 100644
--- a/cmd/tailscaled/tailscaled_windows.go
+++ b/cmd/tailscaled/tailscaled_windows.go
@@ -51,6 +51,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/osdiag"
+ "tailscale.com/util/syspolicy"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
@@ -131,7 +132,7 @@ func runWindowsService(pol *logpolicy.Policy) error {
osdiag.LogSupportInfo(logger.WithPrefix(log.Printf, "Support Info: "), osdiag.LogSupportInfoReasonStartup)
}()
- if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {
+ if logSCMInteractions, _ := syspolicy.GetBoolean(syspolicy.LogSCMInteractions, false); logSCMInteractions {
syslog, err := eventlog.Open(serviceName)
if err == nil {
syslogf = func(format string, args ...any) {
@@ -158,7 +159,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
syslogf("Service start pending")
svcAccepts := svc.AcceptStop
- if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
+ if flushDNSOnSessionUnlock, _ := syspolicy.GetBoolean(syspolicy.FlushDNSOnSessionUnlock, false); flushDNSOnSessionUnlock {
svcAccepts |= svc.AcceptSessionChange
}
diff --git a/cmd/testwrapper/flakytest/flakytest.go b/cmd/testwrapper/flakytest/flakytest.go
index 8b9dcd9801a5d..494ed080b26a1 100644
--- a/cmd/testwrapper/flakytest/flakytest.go
+++ b/cmd/testwrapper/flakytest/flakytest.go
@@ -38,7 +38,7 @@ func Mark(t testing.TB, issue string) {
// We're being run under cmd/testwrapper so send our sentinel message
// to stderr. (We avoid doing this when the env is absent to avoid
// spamming people running tests without the wrapper)
- fmt.Fprintln(os.Stderr, FlakyTestLogMessage)
+ fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
}
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
}
diff --git a/cmd/testwrapper/testwrapper.go b/cmd/testwrapper/testwrapper.go
index 92cad5072e9cf..4878a94488cc0 100644
--- a/cmd/testwrapper/testwrapper.go
+++ b/cmd/testwrapper/testwrapper.go
@@ -19,6 +19,7 @@ import (
"log"
"os"
"os/exec"
+ "slices"
"sort"
"strings"
"time"
@@ -34,18 +35,25 @@ type testAttempt struct {
testName string // "TestFoo"
outcome string // "pass", "fail", "skip"
logs bytes.Buffer
- isMarkedFlaky bool // set if the test is marked as flaky
+ isMarkedFlaky bool // set if the test is marked as flaky
+ issueURL string // set if the test is marked as flaky
pkgFinished bool
}
+// packageTests describes what to run.
+// It's also JSON-marshalled to output for analysys tools to parse
+// so the fields are all exported.
+// TODO(bradfitz): move this type to its own types package?
type packageTests struct {
- // pattern is the package pattern to run.
- // Must be a single pattern, not a list of patterns.
- pattern string // "./...", "./types/key"
- // tests is a list of tests to run. If empty, all tests in the package are
+ // Pattern is the package Pattern to run.
+ // Must be a single Pattern, not a list of patterns.
+ Pattern string // "./...", "./types/key"
+ // Tests is a list of Tests to run. If empty, all Tests in the package are
// run.
- tests []string // ["TestFoo", "TestBar"]
+ Tests []string // ["TestFoo", "TestBar"]
+ // IssueURLs maps from a test name to a URL tracking its flake.
+ IssueURLs map[string]string // "TestFoo" => "https://github.com/foo/bar/issue/123"
}
type goTestOutput struct {
@@ -65,10 +73,10 @@ var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error {
defer close(ch)
- args := []string{"test", "-json", pt.pattern}
+ args := []string{"test", "-json", pt.Pattern}
args = append(args, otherArgs...)
- if len(pt.tests) > 0 {
- runArg := strings.Join(pt.tests, "|")
+ if len(pt.Tests) > 0 {
+ runArg := strings.Join(pt.Tests, "|")
args = append(args, "-run", runArg)
}
if debug {
@@ -152,8 +160,9 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
pkgTests[testName].outcome = goOutput.Action
ch <- pkgTests[testName]
case "output":
- if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
+ if suffix, ok := strings.CutPrefix(strings.TrimSpace(goOutput.Output), flakytest.FlakyTestLogMessage); ok {
pkgTests[testName].isMarkedFlaky = true
+ pkgTests[testName].issueURL = strings.TrimPrefix(suffix, ": ")
} else {
pkgTests[testName].logs.WriteString(goOutput.Output)
}
@@ -208,12 +217,12 @@ func main() {
type nextRun struct {
tests []*packageTests
- attempt int
+ attempt int // starting at 1
}
toRun := []*nextRun{
{
- tests: []*packageTests{{pattern: pattern}},
+ tests: []*packageTests{{Pattern: pattern}},
attempt: 1,
},
}
@@ -244,10 +253,11 @@ func main() {
os.Exit(1)
}
if thisRun.attempt > 1 {
- fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
+ j, _ := json.Marshal(thisRun.tests)
+ fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\nflakytest failures JSON: %s\n\n", thisRun.attempt, j)
}
- toRetry := make(map[string][]string) // pkg -> tests to retry
+ toRetry := make(map[string][]*testAttempt) // pkg -> tests to retry
for _, pt := range thisRun.tests {
ch := make(chan *testAttempt)
runErr := make(chan error, 1)
@@ -282,7 +292,7 @@ func main() {
continue
}
if tr.isMarkedFlaky {
- toRetry[tr.pkg] = append(toRetry[tr.pkg], tr.testName)
+ toRetry[tr.pkg] = append(toRetry[tr.pkg], tr)
} else {
failed = true
}
@@ -315,10 +325,17 @@ func main() {
}
for _, pkg := range pkgs {
tests := toRetry[pkg]
- sort.Strings(tests)
+ slices.SortFunc(tests, func(a, b *testAttempt) int { return strings.Compare(a.testName, b.testName) })
+ issueURLs := map[string]string{} // test name => URL
+ var testNames []string
+ for _, ta := range tests {
+ issueURLs[ta.testName] = ta.issueURL
+ testNames = append(testNames, ta.testName)
+ }
nextRun.tests = append(nextRun.tests, &packageTests{
- pattern: pkg,
- tests: tests,
+ Pattern: pkg,
+ Tests: testNames,
+ IssueURLs: issueURLs,
})
}
toRun = append(toRun, nextRun)
diff --git a/cmd/tsconnect/common.go b/cmd/tsconnect/common.go
index dae8b73a24b70..a387c00c9758e 100644
--- a/cmd/tsconnect/common.go
+++ b/cmd/tsconnect/common.go
@@ -71,7 +71,7 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
},
},
},
- JSXMode: esbuild.JSXModeAutomatic,
+ JSX: esbuild.JSXAutomatic,
}, nil
}
@@ -137,16 +137,19 @@ func runEsbuildServe(buildOptions esbuild.BuildOptions) {
if err != nil {
log.Fatalf("Cannot parse port: %v", err)
}
- result, err := esbuild.Serve(esbuild.ServeOptions{
+ buildContext, ctxErr := esbuild.Context(buildOptions)
+ if ctxErr != nil {
+ log.Fatalf("Cannot create esbuild context: %v", err)
+ }
+ result, err := buildContext.Serve(esbuild.ServeOptions{
Port: uint16(port),
Host: host,
Servedir: "./",
- }, buildOptions)
+ })
if err != nil {
log.Fatalf("Cannot start esbuild server: %v", err)
}
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
- result.Wait()
}
func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {
diff --git a/cmd/viewer/tests/tests_clone.go b/cmd/viewer/tests/tests_clone.go
index 2b41639fd9271..2d8c1ba313b5d 100644
--- a/cmd/viewer/tests/tests_clone.go
+++ b/cmd/viewer/tests/tests_clone.go
@@ -157,7 +157,11 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
if src.Ints != nil {
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
- dst.Ints[i] = ptr.To(*src.Ints[i])
+ if src.Ints[i] == nil {
+ dst.Ints[i] = nil
+ } else {
+ dst.Ints[i] = ptr.To(*src.Ints[i])
+ }
}
}
dst.Slice = append(src.Slice[:0:0], src.Slice...)
diff --git a/control/controlclient/client.go b/control/controlclient/client.go
index b809f81927548..ef5af68c64852 100644
--- a/control/controlclient/client.go
+++ b/control/controlclient/client.go
@@ -14,12 +14,20 @@ import (
"tailscale.com/tailcfg"
)
+// LoginFlags is a bitmask of options to change the behavior of Client.Login
+// and LocalBackend.
type LoginFlags int
const (
LoginDefault = LoginFlags(0)
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
LoginEphemeral // set RegisterRequest.Ephemeral
+
+ // LocalBackendStartKeyOSNeutral instructs NewLocalBackend to start the
+ // LocalBackend without any OS-dependent StateStore StartKey behavior.
+ //
+ // See https://github.com/tailscale/tailscale/issues/6973.
+ LocalBackendStartKeyOSNeutral
)
// Client represents a client connection to the control server.
diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index a19c030b4a849..81310e4beb44c 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -845,8 +845,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
var epStrs []string
+ var eps []netip.AddrPort
var epTypes []tailcfg.EndpointType
for _, ep := range c.endpoints {
+ eps = append(eps, ep.Addr)
epStrs = append(epStrs, ep.Addr.String())
epTypes = append(epTypes, ep.Type)
}
@@ -881,7 +883,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
KeepAlive: true,
NodeKey: persist.PublicNodeKey(),
DiscoKey: c.discoPubKey,
- Endpoints: epStrs,
+ Endpoints: eps,
EndpointTypes: epTypes,
Stream: isStreaming,
Hostinfo: hi,
diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go
index 26d01d4242603..50d659a5ab74f 100644
--- a/control/controlclient/map_test.go
+++ b/control/controlclient/map_test.go
@@ -29,6 +29,14 @@ import (
"tailscale.com/util/must"
)
+func eps(s ...string) []netip.AddrPort {
+ var eps []netip.AddrPort
+ for _, ep := range s {
+ eps = append(eps, netip.MustParseAddrPort(ep))
+ }
+ return eps
+}
+
func TestUpdatePeersStateFromResponse(t *testing.T) {
var curTime time.Time
@@ -49,7 +57,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
}
withEP := func(ep string) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
- n.Endpoints = []string{ep}
+ n.Endpoints = []netip.AddrPort{netip.MustParseAddrPort(ep)}
}
}
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
@@ -197,7 +205,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
- Endpoints: []string{"1.2.3.4:56"},
+ Endpoints: eps("1.2.3.4:56"),
}},
},
want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
@@ -209,7 +217,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
- Endpoints: []string{"1.2.3.4:56"},
+ Endpoints: eps("1.2.3.4:56"),
}},
},
want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))),
@@ -222,7 +230,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
DERPRegion: 2,
- Endpoints: []string{"1.2.3.4:56"},
+ Endpoints: eps("1.2.3.4:56"),
}},
},
want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))),
@@ -667,9 +675,9 @@ func TestPeerChangeDiff(t *testing.T) {
},
{
name: "patch-endpoints",
- a: &tailcfg.Node{ID: 1, Endpoints: []string{"10.0.0.1:1"}},
- b: &tailcfg.Node{ID: 1, Endpoints: []string{"10.0.0.2:2"}},
- want: &tailcfg.PeerChange{NodeID: 1, Endpoints: []string{"10.0.0.2:2"}},
+ a: &tailcfg.Node{ID: 1, Endpoints: eps("10.0.0.1:1")},
+ b: &tailcfg.Node{ID: 1, Endpoints: eps("10.0.0.2:2")},
+ want: &tailcfg.PeerChange{NodeID: 1, Endpoints: eps("10.0.0.2:2")},
},
{
name: "patch-cap",
@@ -809,13 +817,13 @@ func TestPatchifyPeersChanged(t *testing.T) {
},
mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
- {ID: 1, Endpoints: []string{"10.0.0.1:1111"}, Hostinfo: hi},
+ {ID: 1, Endpoints: eps("10.0.0.1:1111"), Hostinfo: hi},
},
},
want: &tailcfg.MapResponse{
PeersChanged: nil,
PeersChangedPatch: []*tailcfg.PeerChange{
- {NodeID: 1, Endpoints: []string{"10.0.0.1:1111"}},
+ {NodeID: 1, Endpoints: eps("10.0.0.1:1111")},
},
},
},
@@ -891,7 +899,10 @@ func TestPatchifyPeersChanged(t *testing.T) {
mr1 := new(tailcfg.MapResponse)
must.Do(json.Unmarshal(must.Get(json.Marshal(tt.mr1)), mr1))
ms.patchifyPeersChanged(mr1)
- if diff := cmp.Diff(tt.want, mr1); diff != "" {
+ opts := []cmp.Option{
+ cmp.Comparer(func(a, b netip.AddrPort) bool { return a == b }),
+ }
+ if diff := cmp.Diff(tt.want, mr1, opts...); diff != "" {
t.Errorf("wrong result (-want +got):\n%s", diff)
}
})
@@ -917,7 +928,7 @@ func BenchmarkMapSessionDelta(b *testing.B) {
DERP: "127.3.3.40:10",
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
- Endpoints: []string{"192.168.1.2:345", "192.168.1.3:678"},
+ Endpoints: eps("192.168.1.2:345", "192.168.1.3:678"),
Hostinfo: (&tailcfg.Hostinfo{
OS: "fooOS",
Hostname: "MyHostname",
diff --git a/control/controlclient/noise.go b/control/controlclient/noise.go
index a9dd2018005e0..660760995157f 100644
--- a/control/controlclient/noise.go
+++ b/control/controlclient/noise.go
@@ -177,6 +177,7 @@ type NoiseClient struct {
// mu only protects the following variables.
mu sync.Mutex
+ closed bool
last *noiseConn // or nil
nextID int
connPool map[int]*noiseConn // active connections not yet closed; see noiseConn.Close
@@ -373,6 +374,7 @@ func (nc *NoiseClient) connClosed(id int) {
// It is a no-op and returns nil if the connection is already closed.
func (nc *NoiseClient) Close() error {
nc.mu.Lock()
+ nc.closed = true
conns := nc.connPool
nc.connPool = nil
nc.mu.Unlock()
@@ -471,6 +473,11 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
ncc.h2cc = h2cc
nc.mu.Lock()
+ if nc.closed {
+ nc.mu.Unlock()
+ ncc.Close() // Needs to be called without holding the lock.
+ return nil, errors.New("noise client closed")
+ }
defer nc.mu.Unlock()
mak.Set(&nc.connPool, ncc.id, ncc)
nc.last = ncc
diff --git a/control/controlclient/sign_supported.go b/control/controlclient/sign_supported.go
index 2dc8efa1ee805..0c792545224e6 100644
--- a/control/controlclient/sign_supported.go
+++ b/control/controlclient/sign_supported.go
@@ -40,7 +40,7 @@ var getMachineCertificateSubjectOnce struct {
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
func getMachineCertificateSubject() string {
getMachineCertificateSubjectOnce.Do(func() {
- getMachineCertificateSubjectOnce.v = winutil.GetRegString("MachineCertificateSubject", "")
+ getMachineCertificateSubjectOnce.v, _ = winutil.GetRegString("MachineCertificateSubject")
})
return getMachineCertificateSubjectOnce.v
diff --git a/disco/disco.go b/disco/disco.go
index 46379b9d298f7..8c83480df71d8 100644
--- a/disco/disco.go
+++ b/disco/disco.go
@@ -261,7 +261,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
func MessageSummary(m Message) string {
switch m := m.(type) {
case *Ping:
- return fmt.Sprintf("ping tx=%x", m.TxID[:6])
+ return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case *CallMeMaybe:
diff --git a/docs/sysv/tailscale.init b/docs/sysv/tailscale.init
new file mode 100755
index 0000000000000..ca21033df7b27
--- /dev/null
+++ b/docs/sysv/tailscale.init
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright (c) Tailscale Inc & AUTHORS
+# SPDX-License-Identifier: BSD-3-Clause
+
+### BEGIN INIT INFO
+# Provides: tailscaled
+# Required-Start:
+# Required-Stop:
+# Default-Start:
+# Default-Stop:
+# Short-Description: Tailscale Mesh Wireguard VPN
+### END INIT INFO
+
+set -e
+
+# /etc/init.d/tailscale: start and stop the Tailscale VPN service
+
+test -x /usr/sbin/tailscaled || exit 0
+
+umask 022
+
+. /lib/lsb/init-functions
+
+# Are we running from init?
+run_by_init() {
+ ([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
+}
+
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+
+case "$1" in
+ start)
+ log_daemon_msg "Starting Tailscale VPN" "tailscaled" || true
+ if start-stop-daemon --start --oknodo --name tailscaled -m --pidfile /run/tailscaled.pid --background \
+ --exec /usr/sbin/tailscaled -- \
+ --state=/var/lib/tailscale/tailscaled.state \
+ --socket=/run/tailscale/tailscaled.sock \
+ --port 41641;
+ then
+ log_end_msg 0 || true
+ else
+ log_end_msg 1 || true
+ fi
+ ;;
+ stop)
+ log_daemon_msg "Stopping Tailscale VPN" "tailscaled" || true
+ if start-stop-daemon --stop --remove-pidfile --pidfile /run/tailscaled.pid --exec /usr/sbin/tailscaled; then
+ log_end_msg 0 || true
+ else
+ log_end_msg 1 || true
+ fi
+ ;;
+
+ status)
+ status_of_proc -p /run/tailscaled.pid /usr/sbin/tailscaled tailscaled && exit 0 || exit $?
+ ;;
+
+ *)
+ log_action_msg "Usage: /etc/init.d/tailscaled {start|stop|status}" || true
+ exit 1
+esac
+
+exit 0
diff --git a/flake.nix b/flake.nix
index 643aa91ae33eb..8ba2b5be79c13 100644
--- a/flake.nix
+++ b/flake.nix
@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
-# nix-direnv cache busting line: sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=
+# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
diff --git a/go.mod b/go.mod
index 127ee74839c20..04430379a836a 100644
--- a/go.mod
+++ b/go.mod
@@ -9,53 +9,54 @@ require (
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
github.com/andybalholm/brotli v1.0.5
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
- github.com/aws/aws-sdk-go-v2 v1.18.0
- github.com/aws/aws-sdk-go-v2/config v1.18.22
+ github.com/aws/aws-sdk-go-v2 v1.21.0
+ github.com/aws/aws-sdk-go-v2/config v1.18.42
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0
- github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3
- github.com/coreos/go-iptables v0.6.0
+ github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0
+ github.com/coreos/go-iptables v0.7.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
- github.com/coreos/go-systemd/v22 v22.4.0
+ github.com/coreos/go-systemd/v22 v22.5.0
github.com/creack/pty v1.1.18
- github.com/dave/jennifer v1.6.1
- github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0
+ github.com/dave/jennifer v1.7.0
+ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
+ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/dsnet/try v0.0.3
- github.com/evanw/esbuild v0.14.53
+ github.com/evanw/esbuild v0.19.4
github.com/frankban/quicktest v1.14.5
- github.com/fxamacker/cbor/v2 v2.4.0
- github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37
+ github.com/fxamacker/cbor/v2 v2.5.0
+ github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533
github.com/go-logr/zapr v1.2.4
- github.com/go-ole/go-ole v1.2.6
+ github.com/go-ole/go-ole v1.3.0
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golangci/golangci-lint v1.52.2
github.com/google/go-cmp v0.5.9
- github.com/google/go-containerregistry v0.14.0
+ github.com/google/go-containerregistry v0.16.1
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c
- github.com/google/uuid v1.3.0
- github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf
+ github.com/google/uuid v1.3.1
+ github.com/goreleaser/nfpm/v2 v2.33.1
github.com/hdevalence/ed25519consensus v0.1.0
- github.com/iancoleman/strcase v0.2.0
+ github.com/iancoleman/strcase v0.3.0
github.com/illarion/gonotify v1.0.1
- github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
+ github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
- github.com/jsimonetti/rtnetlink v1.3.2
+ github.com/jsimonetti/rtnetlink v1.3.5
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
- github.com/klauspost/compress v1.16.7
+ github.com/klauspost/compress v1.17.0
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
- github.com/mattn/go-isatty v0.0.18
+ github.com/mattn/go-isatty v0.0.19
github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/sdnotify v1.0.0
- github.com/miekg/dns v1.1.55
+ github.com/miekg/dns v1.1.56
github.com/mitchellh/go-ps v1.0.0
- github.com/peterbourgon/ff/v3 v3.3.0
+ github.com/peterbourgon/ff/v3 v3.4.0
github.com/pkg/errors v0.9.1
- github.com/pkg/sftp v1.13.5
- github.com/prometheus/client_golang v1.15.1
- github.com/prometheus/common v0.42.0
+ github.com/pkg/sftp v1.13.6
+ github.com/prometheus/client_golang v1.17.0
+ github.com/prometheus/common v0.44.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
@@ -65,44 +66,47 @@ require (
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
- github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109
- github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8
- github.com/tc-hib/winres v0.2.0
+ github.com/tailscale/web-client-prebuilt v0.0.0-20230919211114-7bcd7bca7bc5
+ github.com/tailscale/wireguard-go v0.0.0-20230929223258-2f6748dc88e7
+ github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.11.0
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vishvananda/netns v0.0.4
- go.uber.org/zap v1.24.0
+ go.uber.org/zap v1.26.0
go4.org/mem v0.0.0-20220726221520-4f986261bf13
- go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
- golang.org/x/crypto v0.12.0
- golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
- golang.org/x/mod v0.11.0
- golang.org/x/net v0.14.0
- golang.org/x/oauth2 v0.7.0
- golang.org/x/sync v0.2.0
- golang.org/x/sys v0.11.0
- golang.org/x/term v0.11.0
+ go4.org/netipx v0.0.0-20230824141953-6213f710f925
+ golang.org/x/crypto v0.14.0
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9
+ golang.org/x/mod v0.12.0
+ golang.org/x/net v0.17.0
+ golang.org/x/oauth2 v0.12.0
+ golang.org/x/sync v0.3.0
+ golang.org/x/sys v0.13.0
+ golang.org/x/term v0.13.0
golang.org/x/time v0.3.0
- golang.org/x/tools v0.9.1
+ golang.org/x/tools v0.13.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
- gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f
- honnef.co/go/tools v0.4.3
+ gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c
+ honnef.co/go/tools v0.4.6
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
inet.af/wf v0.0.0-20221017222439-36129f591884
- k8s.io/api v0.27.2
- k8s.io/apimachinery v0.27.2
- k8s.io/client-go v0.27.2
+ k8s.io/api v0.28.2
+ k8s.io/apimachinery v0.28.2
+ k8s.io/client-go v0.28.2
nhooyr.io/websocket v1.8.7
- sigs.k8s.io/controller-runtime v0.15.0
+ sigs.k8s.io/controller-runtime v0.16.2
sigs.k8s.io/yaml v1.3.0
- software.sslmate.com/src/go-pkcs12 v0.2.0
+ software.sslmate.com/src/go-pkcs12 v0.2.1
)
-require github.com/gorilla/securecookie v1.1.1 // indirect
+require (
+ github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
+ github.com/gorilla/securecookie v1.1.1 // indirect
+)
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
@@ -113,7 +117,7 @@ require (
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Antonboom/errname v0.1.9 // indirect
github.com/Antonboom/nilnil v0.1.4 // indirect
- github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -121,27 +125,27 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/OpenPeeDeeP/depguard v1.1.1 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.5.1 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.13.21 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.40 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 // indirect
- github.com/aws/smithy-go v1.13.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect
+ github.com/aws/smithy-go v1.14.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
@@ -160,16 +164,16 @@ require (
github.com/daixiang0/gci v0.10.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
- github.com/docker/cli v23.0.5+incompatible // indirect
+ github.com/docker/cli v24.0.6+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
- github.com/docker/docker v23.0.5+incompatible // indirect
- github.com/docker/docker-credential-helpers v0.7.0 // indirect
- github.com/emicklei/go-restful/v3 v3.10.2 // indirect
+ github.com/docker/docker v24.0.6+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.8.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/esimonov/ifshort v1.0.4 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
- github.com/evanphx/json-patch/v5 v5.6.0 // indirect
+ github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.4 // indirect
@@ -178,11 +182,11 @@ require (
github.com/go-critic/go-critic v0.8.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
- github.com/go-git/go-git/v5 v5.7.0 // indirect
+ github.com/go-git/go-git/v5 v5.8.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
- github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
- github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.1.0 // indirect
@@ -205,7 +209,6 @@ require (
github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/btree v1.1.2 // indirect
- github.com/google/gnostic v0.6.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
github.com/google/rpmpack v0.5.0 // indirect
@@ -257,7 +260,7 @@ require (
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
- github.com/mdlayher/socket v0.4.1 // indirect
+ github.com/mdlayher/socket v0.5.0 // indirect
github.com/mgechev/revive v1.3.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -275,21 +278,21 @@ require (
github.com/nunnatsa/ginkgolinter v0.11.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
- github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
+ github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
- github.com/pierrec/lz4/v4 v4.1.17 // indirect
+ github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.4.1 // indirect
- github.com/prometheus/client_model v0.4.0 // indirect
- github.com/prometheus/procfs v0.9.0 // indirect
+ github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasilyte/go-ruleguard v0.3.19 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
- github.com/rogpeppe/go-internal v1.10.0 // indirect
+ github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/ryancurrah/gomodguard v1.3.0 // indirect
github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
@@ -298,12 +301,12 @@ require (
github.com/securego/gosec/v2 v2.15.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
- github.com/shopspring/decimal v1.2.0 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/shopspring/decimal v1.3.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect
- github.com/skeema/knownhosts v1.1.1 // indirect
+ github.com/skeema/knownhosts v1.2.1 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
@@ -329,36 +332,35 @@ require (
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.5 // indirect
github.com/uudashr/gocognit v1.0.6 // indirect
- github.com/vbatts/tar-split v0.11.2 // indirect
+ github.com/vbatts/tar-split v0.11.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.2.0 // indirect
gitlab.com/bosi/decorder v0.2.3 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
- go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53 // indirect
- golang.org/x/image v0.7.0 // indirect
- golang.org/x/text v0.12.0 // indirect
- gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
- google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
+ golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/image v0.12.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+ gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
- k8s.io/apiextensions-apiserver v0.27.2 // indirect
- k8s.io/component-base v0.27.2 // indirect
+ k8s.io/apiextensions-apiserver v0.28.2 // indirect
+ k8s.io/component-base v0.28.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
- k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
- k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
+ k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833 // indirect
+ k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
- sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
)
diff --git a/go.mod.sri b/go.mod.sri
index ce7a675fc49c0..6bed6a4fd13a7 100644
--- a/go.mod.sri
+++ b/go.mod.sri
@@ -1 +1 @@
-sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=
+sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
diff --git a/go.sum b/go.sum
index 5a19bbe322a48..c480f28bb86e4 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,8 @@ github.com/Antonboom/errname v0.1.9/go.mod h1:nLTcJzevREuAsgTbG85UsuiWpMpAqbKD1H
github.com/Antonboom/nilnil v0.1.4 h1:yWIfwbCRDpJiJvs7Quz55dzeXCgORQyAG29N9/J5H2Q=
github.com/Antonboom/nilnil v0.1.4/go.mod h1:iOov/7gRcXkeEU+EMGpBu2ORih3iyVEiWjeste1SJm8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
@@ -76,11 +76,10 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenPeeDeeP/depguard v1.1.1 h1:TSUznLjvp/4IUP+OQ0t/4jF4QUyxIcVX8YnghZdunyA=
github.com/OpenPeeDeeP/depguard v1.1.1/go.mod h1:JtAMzWkmFEzDPyAd+W0NHl1lvpQKTvT9jnRVsohBKpc=
-github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec h1:vV3RryLxt42+ZIVOFbYJCH1jsZNTNmj2NYru5zfx+4E=
-github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
+github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
@@ -104,7 +103,6 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -112,47 +110,58 @@ github.com/ashanbrown/forbidigo v1.5.1 h1:WXhzLjOlnuDYPYQo/eFlcFMi8X/kLfvWLYu6CS
github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s=
github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
-github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
+github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
-github.com/aws/aws-sdk-go-v2/config v1.18.22 h1:7vkUEmjjv+giht4wIROqLs+49VWmiQMMHSduxmoNKLU=
github.com/aws/aws-sdk-go-v2/config v1.18.22/go.mod h1:mN7Li1wxaPxSSy4Xkr6stFuinJGf3VZW3ZSNvO0q6sI=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.21 h1:VRiXnPEaaPeGeoFcXvMZOB5K/yfIXOYE3q97Kgb0zbU=
+github.com/aws/aws-sdk-go-v2/config v1.18.42 h1:28jHROB27xZwU0CB88giDSjz7M1Sba3olb5JBGwina8=
+github.com/aws/aws-sdk-go-v2/config v1.18.42/go.mod h1:4AZM3nMMxwlG+eZlxvBKqwVbkDLlnN2a4UGTL6HjaZI=
github.com/aws/aws-sdk-go-v2/credentials v1.13.21/go.mod h1:90Dk1lJoMyspa/EDUrldTxsPns0wn6+KpRKpdAWc0uA=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.40 h1:s8yOkDh+5b1jUDhMBtngF6zKWLDs84chUk2Vk0c38Og=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.40/go.mod h1:VtEHVAAqDWASwdOqj/1huyT6uHbs5s8FUHfDQdky/Rs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64 h1:9QJQs36z61YB8nxGwRDfWXEDYbU6H7jdI6zFiAX1vag=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64/go.mod h1:4Q7R9MFpXRdjO3YnAfUTdnuENs32WzBkASt6VxSYDYQ=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 h1:g+qlObJH4Kn4n21g69DjspU0hKTjWtq7naZ9OLCv0ew=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0 h1:L5h2fymEdVJYvn6hYO8Jx48YmC6xVmjmgHJV3oGKgmc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3 h1:TQZH0Djie8VVgTBDOQ02M4zVHJFrNzLMsYMbNfRitVM=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.36.3/go.mod h1:p6MaesK9061w6NTiFmZpUzEkKUY5blKlwD2zYyErxKA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 h1:GAiaQWuQhQQui76KjuXeShmyXqECwQ0mGRMc/rwsL+c=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0 h1:JON9MBvwUlM8HXylfB2caZuH3VXz9RxO4SMp2+TNc3Q=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0/go.mod h1:JjBzoceyKkpQY3v1GPIdg6kHqUFHRJ7SDlwtwoH0Qh8=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.9/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 h1:TraLwncRJkWqtIBVKI/UqBymq4+hL+3MzUOtUATuzkA=
+github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 h1:YkNzx1RLS0F5qdf9v1Q8Cuv9NXCL2TkosOxhzlUPV64=
+github.com/aws/aws-sdk-go-v2/service/sso v1.14.1/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
-github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 h1:6UbNM/KJhMBfOI5+lpVcJ/8OA7cBSz0O6OX37SRKlSw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 h1:8lKOidPkmSmfUtiTgtdXWgaKItCZ/g75/jEk6Ql6GsA=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.10/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
-github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 h1:s4bioTgjSFRwOoyEFzAVCmFmoowBgjTR8gkrF/sQ4wk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.22.0/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
-github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
+github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -170,7 +179,6 @@ github.com/breml/bidichk v0.2.4 h1:i3yedFWWQ7YzjdZJHnPo9d/xURinSq3OM+gyM43K4/8=
github.com/breml/bidichk v0.2.4/go.mod h1:7Zk0kRFt1LIZxtQdl9W9JwGAcLTTkOs+tN7wuEYGJ3s=
github.com/breml/errchkjson v0.3.1 h1:hlIeXuspTyt8Y/UmP5qy1JocGNR00KQHgfaNtRAjoxQ=
github.com/breml/errchkjson v0.3.1/go.mod h1:XroxrzKjdiutFyW3nWhw34VGg7kiMsDQox73yWCGI2U=
-github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/butuzov/ireturn v0.2.0 h1:kCHi+YzC150GE98WFuZQu9yrTn6GEydO2AuPLbTgnO4=
github.com/butuzov/ireturn v0.2.0/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -181,7 +189,6 @@ github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTli
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -193,24 +200,22 @@ github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8/go.mod h1:gakxgyXa
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
-github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE=
+github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
+github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
-github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
-github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
+github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
+github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
-github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -219,30 +224,31 @@ github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDU
github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
github.com/daixiang0/gci v0.10.1 h1:eheNA3ljF6SxnPD/vE4lCBusVHmV3Rs3dkKvFrJ7MR0=
github.com/daixiang0/gci v0.10.1/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI=
-github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
-github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
+github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE=
+github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0 h1:/dgKwHVTI0J+A0zd/BHOF2CTn1deN0735cJrb+w2hbE=
-github.com/dblohm7/wingoes v0.0.0-20230821191801-fc76608aecf0/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
+github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0oZOHPaOjpmN/BsgNYF+gW/hxZXXc=
+github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
-github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE=
-github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
+github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
+github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k=
-github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
+github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
+github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
-github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
-github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -250,7 +256,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA=
github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0=
@@ -258,26 +263,24 @@ github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
-github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
-github.com/evanw/esbuild v0.14.53 h1:9uU73SZUmP1jRQhaC6hPm9aoqFGYlPwfk7OrhG6AhpQ=
-github.com/evanw/esbuild v0.14.53/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
+github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
+github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/evanw/esbuild v0.19.4 h1:Etk+6ZCjtNxZZLEgMKSqpO0/oM0k1WYKJabaPMJ39iQ=
+github.com/evanw/esbuild v0.19.4/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y=
github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI=
-github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
-github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
+github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
@@ -294,13 +297,13 @@ github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
-github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
-github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
+github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
+github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37 h1:JFXgl/SzxYP9ULDFS97xkKB/Bfdywrc3rre1nYEwMj8=
-github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
+github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533 h1:1SRqDZauC9fz6vMIDLCUOULPNfOnZ0rmvZo8quraoy4=
+github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -312,14 +315,16 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
-github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
-github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
+github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
+github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -421,8 +426,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
-github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -437,8 +442,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw=
-github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk=
+github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
+github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -466,8 +471,8 @@ github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A=
github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -479,8 +484,8 @@ github.com/goreleaser/chglog v0.5.0 h1:Sk6BMIpx8+vpAf8KyPit34OgWui8c7nKTMHhYx88j
github.com/goreleaser/chglog v0.5.0/go.mod h1:Ri46M3lrMuv76FHszs3vtABR8J8k1w9JHYAzxeeOl28=
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
-github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf h1:X8rzot0Te1TYSoADyMZfPt95Afhptpj0VqicKPAcmjM=
-github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf/go.mod h1:Z7rAxucnQGMGfAhpxm/UIrdH0/EcxEt91RW3mmVzx2U=
+github.com/goreleaser/nfpm/v2 v2.33.1 h1:EkdAzZyVhAI9JC1vjmjjbmnNzyH1J6Cu4JCsA7YcQuc=
+github.com/goreleaser/nfpm/v2 v2.33.1/go.mod h1:8wwWWvJWmn84xo/Sqiv0aMvEGTHlHZTXTEuVSgQpkIM=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -500,7 +505,6 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY=
github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -520,8 +524,8 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
-github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
@@ -531,8 +535,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
-github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
+github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ=
+github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -552,8 +556,8 @@ github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/ra
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
-github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
+github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=
+github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -582,8 +586,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8=
github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
-github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
+github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -594,7 +598,6 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -635,8 +638,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
-github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -651,12 +654,12 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
-github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
-github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
+github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
+github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4=
github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I=
-github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
+github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
+github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -697,14 +700,14 @@ github.com/nunnatsa/ginkgolinter v0.11.2 h1:xzQpAsEyZe5F1RMy2Z5kn8UFCGiWfKqJOUd2
github.com/nunnatsa/ginkgolinter v0.11.2/go.mod h1:dJIGXYXbkBswqa/pIzG0QlVTTDSBMxDoCFwhsl4Uras=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
-github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
-github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
-github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
+github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
+github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
-github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
+github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
+github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
@@ -713,11 +716,11 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
-github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24=
-github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
+github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
+github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
-github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
+github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
@@ -728,8 +731,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
-github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
+github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
+github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polyfloyd/go-errorlint v1.4.1 h1:r8ru5FhXSn34YU1GJDOuoJv2LdsQkPmK325EOpPMJlM=
@@ -739,27 +742,27 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
-github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
+github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
+github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
+github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
-github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
+github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
-github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc=
github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw=
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
@@ -771,12 +774,10 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw=
github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50=
@@ -795,25 +796,25 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8=
github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY=
github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak=
github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg=
-github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
-github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
+github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
+github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
@@ -824,7 +825,6 @@ github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00=
github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -842,7 +842,6 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc=
github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I=
-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -881,12 +880,12 @@ github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
-github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109 h1:QPRZXpvopDySnmNobTe7Dyc/w6ULt1uCN+3/9cTJwjo=
-github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
-github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8 h1:V9kSpiTzFp7OTgJinu/kSJlsI6EfRs8wJgQ+Q+5a8v4=
-github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw=
-github.com/tc-hib/winres v0.2.0 h1:gly/ivDWGvlhl7ENtEmA7wPQ6dWab1LlLq/DgcZECKE=
-github.com/tc-hib/winres v0.2.0/go.mod h1:uG6S5M2Q0/kThoqsCSYvGJODUQP9O9R0SNxUPmFIegw=
+github.com/tailscale/web-client-prebuilt v0.0.0-20230919211114-7bcd7bca7bc5 h1:wKUtQPRpjhZZvAuwYRMcjMZnpWSUEJWIbNJmLtDbR0k=
+github.com/tailscale/web-client-prebuilt v0.0.0-20230919211114-7bcd7bca7bc5/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
+github.com/tailscale/wireguard-go v0.0.0-20230929223258-2f6748dc88e7 h1:P1od5W+cX/LZZyvbKrNUXuuzxensnKEywLhxhPOeHuY=
+github.com/tailscale/wireguard-go v0.0.0-20230929223258-2f6748dc88e7/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
+github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
+github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM=
@@ -923,11 +922,10 @@ github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iL
github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI=
github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
-github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y=
github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
-github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
-github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
+github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
+github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
@@ -937,9 +935,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
@@ -963,22 +958,20 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
-go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
-go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE=
-go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
+go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
+go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -987,16 +980,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1007,17 +998,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
-golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53 h1:w/MOPdQ1IoYoDou3L55ZbTx2Nhn7JAhX1BBZor8qChU=
-golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k=
+golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
-golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
-golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
+golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
+golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1046,8 +1036,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
-golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1083,7 +1073,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -1093,8 +1082,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1105,8 +1094,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
-golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
+golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1120,8 +1109,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1134,7 +1123,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1175,7 +1163,6 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1192,8 +1179,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1202,25 +1189,24 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1299,8 +1285,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
-golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1309,8 +1295,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
-gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
-gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -1336,8 +1322,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1361,7 +1348,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
@@ -1375,7 +1361,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1389,12 +1374,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1407,9 +1389,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1426,7 +1407,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1434,13 +1414,12 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f h1:8GE2MRjGiFmfpon8dekPI08jEuNMQzSffVHgdupcO4E=
-gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q=
+gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c h1:bYb98Ra11fJ8F2xFbZx0zg2VQ28lYqC1JxfaaF53xqY=
+gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1448,8 +1427,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw=
-honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
+honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
+honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
@@ -1458,22 +1437,22 @@ inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfz
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=
inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q=
inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
-k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo=
-k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4=
-k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo=
-k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ=
-k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg=
-k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
-k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE=
-k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ=
-k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo=
-k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo=
+k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw=
+k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg=
+k8s.io/apiextensions-apiserver v0.28.2 h1:J6/QRWIKV2/HwBhHRVITMLYoypCoPY1ftigDM0Kn+QU=
+k8s.io/apiextensions-apiserver v0.28.2/go.mod h1:5tnkxLGa9nefefYzWuAlWZ7RZYuN/765Au8cWLA6SRg=
+k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ=
+k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU=
+k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY=
+k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY=
+k8s.io/component-base v0.28.2 h1:Yc1yU+6AQSlpJZyvehm/NkJBII72rzlEsd6MkBQ+G0E=
+k8s.io/component-base v0.28.2/go.mod h1:4IuQPQviQCg3du4si8GpMrhAIegxpsgPngPRR/zWpzc=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
-k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
-k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
-k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833 h1:iFFEmmB7szQhJP42AvRD2+gzdVP7EuIKY1rJgxf0JZY=
+k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
@@ -1487,13 +1466,13 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU=
-sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
+sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU=
+sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
-software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
-software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
+software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8=
+software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/go.toolchain.rev b/go.toolchain.rev
index 7a45a9b2105ee..b7cef5ff6165c 100644
--- a/go.toolchain.rev
+++ b/go.toolchain.rev
@@ -1 +1 @@
-2071f43f327a8d544cd2df4b19398ed681e825c7
+d1c91593484a1db2d4de2564f2ef2669814af9c8
diff --git a/gomod_test.go b/gomod_test.go
new file mode 100644
index 0000000000000..f984b5d6f27a5
--- /dev/null
+++ b/gomod_test.go
@@ -0,0 +1,25 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package tailscaleroot
+
+import (
+ "os"
+ "testing"
+
+ "golang.org/x/mod/modfile"
+)
+
+func TestGoMod(t *testing.T) {
+ goMod, err := os.ReadFile("go.mod")
+ if err != nil {
+ t.Fatal(err)
+ }
+ f, err := modfile.Parse("go.mod", goMod, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(f.Replace) > 0 {
+ t.Errorf("go.mod has %d replace directives; expect zero in this repo", len(f.Replace))
+ }
+}
diff --git a/hostinfo/hostinfo.go b/hostinfo/hostinfo.go
index 0ae72f29562b0..2c0d15cb81a5d 100644
--- a/hostinfo/hostinfo.go
+++ b/hostinfo/hostinfo.go
@@ -57,6 +57,7 @@ func New() *tailcfg.Hostinfo {
Cloud: string(cloudenv.Get()),
NoLogsNoSupport: envknob.NoLogsNoSupport(),
AllowsUpdate: envknob.AllowsRemoteUpdate(),
+ WoLMACs: getWoLMACs(),
}
}
diff --git a/hostinfo/hostinfo_windows.go b/hostinfo/hostinfo_windows.go
index 3401655f4c838..d74d1db421424 100644
--- a/hostinfo/hostinfo_windows.go
+++ b/hostinfo/hostinfo_windows.go
@@ -62,7 +62,8 @@ func packageTypeWindows() string {
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
- if msiSentinel := winutil.GetRegInteger("MSI", 0); msiSentinel == 1 {
+ msiSentinel, _ := winutil.GetRegInteger("MSI")
+ if msiSentinel == 1 {
return "msi"
}
exe, err := os.Executable()
diff --git a/hostinfo/wol.go b/hostinfo/wol.go
new file mode 100644
index 0000000000000..3a30af2fe3a37
--- /dev/null
+++ b/hostinfo/wol.go
@@ -0,0 +1,106 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package hostinfo
+
+import (
+ "log"
+ "net"
+ "runtime"
+ "strings"
+ "unicode"
+
+ "tailscale.com/envknob"
+)
+
+// TODO(bradfitz): this is all too simplistic and static. It needs to run
+// continuously in response to netmon events (USB ethernet adapaters might get
+// plugged in) and look for the media type/status/etc. Right now on macOS it
+// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
+// have any media. We should only report the one that's actually connected.
+// But it works for now (2023-10-05) for fleshing out the rest.
+
+var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
+
+// getWoLMACs returns up to 10 MAC address of the local machine to send
+// wake-on-LAN packets to in order to wake it up. The returned MACs are in
+// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
+//
+// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
+// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
+// set to a MAC address, that sole MAC address is returned.
+func getWoLMACs() (macs []string) {
+ switch runtime.GOOS {
+ case "ios", "android":
+ return nil
+ }
+ if s := wakeMAC(); s != "" {
+ switch s {
+ case "auto":
+ ifs, _ := net.Interfaces()
+ for _, iface := range ifs {
+ if iface.Flags&net.FlagLoopback != 0 {
+ continue
+ }
+ if iface.Flags&net.FlagBroadcast == 0 ||
+ iface.Flags&net.FlagRunning == 0 ||
+ iface.Flags&net.FlagUp == 0 {
+ continue
+ }
+ if keepMAC(iface.Name, iface.HardwareAddr) {
+ macs = append(macs, iface.HardwareAddr.String())
+ }
+ if len(macs) == 10 {
+ break
+ }
+ }
+ return macs
+ case "false", "off": // fast path before ParseMAC error
+ return nil
+ }
+ mac, err := net.ParseMAC(s)
+ if err != nil {
+ log.Printf("invalid MAC %q", s)
+ return nil
+ }
+ return []string{mac.String()}
+ }
+ return nil
+}
+
+var ignoreWakeOUI = map[[3]byte]bool{
+ {0x00, 0x15, 0x5d}: true, // Hyper-V
+ {0x00, 0x50, 0x56}: true, // VMware
+ {0x00, 0x1c, 0x14}: true, // VMware
+ {0x00, 0x05, 0x69}: true, // VMware
+ {0x00, 0x0c, 0x29}: true, // VMware
+ {0x00, 0x1c, 0x42}: true, // Parallels
+ {0x08, 0x00, 0x27}: true, // VirtualBox
+ {0x00, 0x21, 0xf6}: true, // VirtualBox
+ {0x00, 0x14, 0x4f}: true, // VirtualBox
+ {0x00, 0x0f, 0x4b}: true, // VirtualBox
+ {0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
+}
+
+func keepMAC(ifName string, mac []byte) bool {
+ if len(mac) != 6 {
+ return false
+ }
+ base := strings.TrimRightFunc(ifName, unicode.IsNumber)
+ switch runtime.GOOS {
+ case "darwin":
+ switch base {
+ case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
+ return false
+ }
+ }
+ if mac[0] == 0x02 && mac[1] == 0x42 {
+ // Docker container.
+ return false
+ }
+ oui := [3]byte{mac[0], mac[1], mac[2]}
+ if ignoreWakeOUI[oui] {
+ return false
+ }
+ return true
+}
diff --git a/ipn/backend.go b/ipn/backend.go
index 8da7e6a5c8769..ad5dbd4bf658e 100644
--- a/ipn/backend.go
+++ b/ipn/backend.go
@@ -192,6 +192,13 @@ type PartialFile struct {
// - "_debug__until" with value being a unix timestamp stringified
type StateKey string
+// DebuggableComponents is a list of components whose debugging can be turned on
+// and off individually using the tailscale debug command.
+var DebuggableComponents = []string{
+ "magicsock",
+ "sockstats",
+}
+
type Options struct {
// FrontendLogID is the public logtail id used by the frontend.
FrontendLogID string
diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go
index 68c942dfbf3c7..7f38f939799b4 100644
--- a/ipn/ipn_clone.go
+++ b/ipn/ipn_clone.go
@@ -52,6 +52,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
+ PostureChecking bool
Persist *persist.Persist
}{})
diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go
index dbbf374768cb5..a3ef660fa7168 100644
--- a/ipn/ipn_view.go
+++ b/ipn/ipn_view.go
@@ -87,6 +87,7 @@ func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.Netfilte
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
+func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -113,6 +114,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
+ PostureChecking bool
Persist *persist.Persist
}{})
diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go
index cae75df963827..344a0938aaf6a 100644
--- a/ipn/ipnlocal/c2n.go
+++ b/ipn/ipnlocal/c2n.go
@@ -9,31 +9,38 @@ import (
"errors"
"fmt"
"io"
+ "net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
+ "sort"
"strconv"
"strings"
"time"
+ "github.com/kortschak/wol"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/net/sockstats"
+ "tailscale.com/posture"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
+ "tailscale.com/util/httpm"
+ "tailscale.com/util/syspolicy"
"tailscale.com/version"
)
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go)
+func writeJSON(w http.ResponseWriter, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(v)
+}
+
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
- writeJSON := func(v any) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(v)
- }
switch r.URL.Path {
case "/echo":
// Test handler.
@@ -41,14 +48,17 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
w.Write(body)
case "/update":
switch r.Method {
- case http.MethodGet:
+ case httpm.GET:
b.handleC2NUpdateGet(w, r)
- case http.MethodPost:
+ case httpm.POST:
b.handleC2NUpdatePost(w, r)
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
+ case "/wol":
+ b.handleC2NWoL(w, r)
+ return
case "/logtail/flush":
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
@@ -59,11 +69,19 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
} else {
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
}
+ case "/posture/identity":
+ switch r.Method {
+ case httpm.GET:
+ b.handleC2NPostureIdentityGet(w, r)
+ default:
+ http.Error(w, "bad method", http.StatusMethodNotAllowed)
+ return
+ }
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump(true))
case "/debug/prefs":
- writeJSON(b.Prefs())
+ writeJSON(w, b.Prefs())
case "/debug/metrics":
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
@@ -81,7 +99,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
if err != nil {
res.Error = err.Error()
}
- writeJSON(res)
+ writeJSON(w, res)
case "/debug/logheap":
if c2nLogHeap != nil {
c2nLogHeap(w, r)
@@ -102,7 +120,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
}
- writeJSON(res)
+ writeJSON(w, res)
case "/sockstats":
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
@@ -207,6 +225,37 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques
}()
}
+func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) {
+ b.logf("c2n: GET /posture/identity received")
+
+ res := tailcfg.C2NPostureIdentityResponse{}
+
+ // Only collect serial numbers if enabled on the client,
+ // this will first check syspolicy, MDM settings like Registry
+ // on Windows or defaults on macOS. If they are not set, it falls
+ // back to the cli-flag, `--posture-checking`.
+ enabled, err := syspolicy.GetBoolean(syspolicy.PostureChecking, b.Prefs().PostureChecking())
+ if err != nil {
+ enabled = b.Prefs().PostureChecking()
+ b.logf("c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s", enabled, err)
+ }
+
+ if enabled {
+ sns, err := posture.GetSerialNumbers(b.logf)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ res.SerialNumbers = sns
+ } else {
+ res.PostureDisabled = true
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(res)
+}
+
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// If NewUpdater does not return an error, we can update the installation.
// Exception: When version.IsMacSysExt returns true, we don't support that
@@ -269,3 +318,56 @@ func findCmdTailscale() (string, error) {
}
return "", fmt.Errorf("unsupported OS %v", runtime.GOOS)
}
+
+func (b *LocalBackend) handleC2NWoL(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "bad method", http.StatusMethodNotAllowed)
+ return
+ }
+ r.ParseForm()
+ var macs []net.HardwareAddr
+ for _, macStr := range r.Form["mac"] {
+ mac, err := net.ParseMAC(macStr)
+ if err != nil {
+ http.Error(w, "bad 'mac' param", http.StatusBadRequest)
+ return
+ }
+ macs = append(macs, mac)
+ }
+ var res struct {
+ SentTo []string
+ Errors []string
+ }
+ st := b.sys.NetMon.Get().InterfaceState()
+ if st == nil {
+ res.Errors = append(res.Errors, "no interface state")
+ writeJSON(w, &res)
+ return
+ }
+ var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
+ for _, mac := range macs {
+ for ifName, ips := range st.InterfaceIPs {
+ for _, ip := range ips {
+ if ip.Addr().IsLoopback() || ip.Addr().Is6() {
+ continue
+ }
+ local := &net.UDPAddr{
+ IP: ip.Addr().AsSlice(),
+ Port: 0,
+ }
+ remote := &net.UDPAddr{
+ IP: net.IPv4bcast,
+ Port: 0,
+ }
+ if err := wol.Wake(mac, password, local, remote); err != nil {
+ res.Errors = append(res.Errors, err.Error())
+ } else {
+ res.SentTo = append(res.SentTo, ifName)
+ }
+ break // one per interface is enough
+ }
+ }
+ }
+ sort.Strings(res.SentTo)
+ writeJSON(w, &res)
+}
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index d7fad76dd9002..e948ba953ec8e 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -62,6 +62,7 @@ import (
"tailscale.com/portlist"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
+ "tailscale.com/taildrop"
"tailscale.com/tka"
"tailscale.com/tsd"
"tailscale.com/tstime"
@@ -238,7 +239,6 @@ type LocalBackend struct {
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
- incomingFiles map[*incomingFile]bool
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
notifyWatchers set.HandleSet[*watchSession]
lastStatusTime time.Time // status.AsOf value of the last processed status update
@@ -308,7 +308,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
dialer := sys.Dialer.Get()
_ = sys.MagicSock.Get() // or panic
- pm, err := newProfileManager(store, logf)
+ goos := envknob.GOOS()
+ if loginFlags&controlclient.LocalBackendStartKeyOSNeutral != 0 {
+ goos = ""
+ }
+ pm, err := newProfileManagerWithGOOS(store, logf, goos)
if err != nil {
return nil, err
}
@@ -382,7 +386,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
b.logf("[unexpected] failed to wire up PeerAPI port for engine %T", e)
}
- for _, component := range debuggableComponents {
+ for _, component := range ipn.DebuggableComponents {
key := componentStateKey(component)
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
if until := time.Unix(ut, 0); until.After(b.clock.Now()) {
@@ -400,11 +404,6 @@ type componentLogState struct {
timer tstime.TimerController // if non-nil, the AfterFunc to disable it
}
-var debuggableComponents = []string{
- "magicsock",
- "sockstats",
-}
-
func componentStateKey(component string) ipn.StateKey {
return ipn.StateKey("_debug_" + component + "_until")
}
@@ -436,7 +435,7 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
}
}
}
- if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
+ if setEnabled == nil || !slices.Contains(ipn.DebuggableComponents, component) {
return fmt.Errorf("unknown component %q", component)
}
timeUnixOrZero := func(t time.Time) int64 {
@@ -2157,6 +2156,12 @@ func (b *LocalBackend) DebugForceNetmapUpdate() {
b.setNetMapLocked(nm)
}
+// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP.
+// See its docs.
+func (b *LocalBackend) DebugPickNewDERP() error {
+ return b.sys.MagicSock.Get().DebugPickNewDERP()
+}
+
// send delivers n to the connected frontend and any API watchers from
// LocalBackend.WatchNotifications (via the LocalAPI).
//
@@ -2177,7 +2182,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
b.mu.Lock()
notifyFunc := b.notify
apiSrv := b.peerAPIServer
- if apiSrv.hasFilesWaiting() {
+ if mayDeref(apiSrv).taildrop.HasFilesWaiting() {
n.FilesWaiting = &empty.Message{}
}
@@ -2213,10 +2218,7 @@ func (b *LocalBackend) sendFileNotify() {
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
// in JSON to clients. They distinguish between empty and non-nil
// to know whether a Notify should be able about files.
- n.IncomingFiles = make([]ipn.PartialFile, 0)
- for f := range b.incomingFiles {
- n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
- }
+ n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
b.mu.Unlock()
sort.Slice(n.IncomingFiles, func(i, j int) bool {
@@ -3061,7 +3063,7 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
})
}
switch runtime.GOOS {
- case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
+ case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows", "android", "ios":
// These are the platforms currently supported by
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
ret = append(ret, tailcfg.Service{
@@ -3339,9 +3341,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
}
addDefault := func(resolvers []*dnstype.Resolver) {
- for _, r := range resolvers {
- dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
- }
+ dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, resolvers...)
}
// If we're using an exit node and that exit node is new enough (1.19.x+)
@@ -3351,7 +3351,17 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
return dcfg
}
- addDefault(nm.DNS.Resolvers)
+ // If the user has set default resolvers ("override local DNS"), prefer to
+ // use those resolvers as the default, otherwise if there are WireGuard exit
+ // node resolvers, use those as the default.
+ if len(nm.DNS.Resolvers) > 0 {
+ addDefault(nm.DNS.Resolvers)
+ } else {
+ if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
+ addDefault(resolvers)
+ }
+ }
+
for suffix, resolvers := range nm.DNS.Routes {
fqdn, err := dnsname.ToFQDN(suffix)
if err != nil {
@@ -3366,11 +3376,10 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
//
// While we're already populating it, might as well size the
// slice appropriately.
+ // Per #9498 the exact requirements of nil vs empty slice remain
+ // unclear, this is a haunted graveyard to be resolved.
dcfg.Routes[fqdn] = make([]*dnstype.Resolver, 0, len(resolvers))
-
- for _, r := range resolvers {
- dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], r)
- }
+ dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], resolvers...)
}
// Set FallbackResolvers as the default resolvers in the
@@ -3540,10 +3549,14 @@ func (b *LocalBackend) initPeerAPIListener() {
}
ps := &peerAPIServer{
- b: b,
- rootDir: fileRoot,
- directFileMode: b.directFileRoot != "",
- directFileDoFinalRename: b.directFileDoFinalRename,
+ b: b,
+ taildrop: &taildrop.Handler{
+ Logf: b.logf,
+ Clock: b.clock,
+ Dir: fileRoot,
+ DirectFileMode: b.directFileRoot != "",
+ AvoidFinalRename: !b.directFileDoFinalRename,
+ },
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
ps.resolver = dm.Resolver()
@@ -4427,7 +4440,7 @@ func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
- return apiSrv.WaitingFiles()
+ return mayDeref(apiSrv).taildrop.WaitingFiles()
}
// AwaitWaitingFiles is like WaitingFiles but blocks while ctx is not done,
@@ -4469,14 +4482,14 @@ func (b *LocalBackend) DeleteFile(name string) error {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
- return apiSrv.DeleteFile(name)
+ return mayDeref(apiSrv).taildrop.DeleteFile(name)
}
func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err error) {
b.mu.Lock()
apiSrv := b.peerAPIServer
b.mu.Unlock()
- return apiSrv.OpenFile(name)
+ return mayDeref(apiSrv).taildrop.OpenFile(name)
}
// hasCapFileSharing reports whether the current node has the file
@@ -4579,19 +4592,6 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
return cc.SetDNS(ctx, req)
}
-func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.incomingFiles == nil {
- b.incomingFiles = make(map[*incomingFile]bool)
- }
- if active {
- b.incomingFiles[inf] = true
- } else {
- delete(b.incomingFiles, inf)
- }
-}
-
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo().Services()
for i := range svcs.LenIter() {
@@ -4768,6 +4768,32 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg
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 {
+ if 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
@@ -5265,3 +5291,11 @@ func (b *LocalBackend) DebugBreakTCPConns() error {
func (b *LocalBackend) DebugBreakDERPConns() error {
return b.magicConn().DebugBreakDERPConns()
}
+
+// mayDeref dereferences p if non-nil, otherwise it returns the zero value.
+func mayDeref[T any](p *T) (v T) {
+ if p == nil {
+ return v
+ }
+ return *p
+}
diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go
index 757f2254b3863..2bb037a307546 100644
--- a/ipn/ipnlocal/local_test.go
+++ b/ipn/ipnlocal/local_test.go
@@ -22,11 +22,13 @@ 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"
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
+ "tailscale.com/util/dnsname"
"tailscale.com/util/set"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
@@ -899,3 +901,259 @@ func TestWhoIs(t *testing.T) {
})
}
}
+
+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(t, gotResolvers, tc.wantResolvers) {
+ t.Errorf("case: %s: got %v, %v, want %v, %v", tc.name, gotOK, gotResolvers, tc.wantOK, tc.wantResolvers)
+ }
+ }
+}
+
+func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
+ type tc struct {
+ name string
+ exitNode tailcfg.StableNodeID
+ peers []tailcfg.NodeView
+ dnsConfig *tailcfg.DNSConfig
+ wantDefaultResolvers []*dnstype.Resolver
+ wantRoutes map[dnsname.FQDN][]*dnstype.Resolver
+ }
+
+ defaultResolvers := []*dnstype.Resolver{{Addr: "default.example.com"}}
+ wgResolvers := []*dnstype.Resolver{{Addr: "wg.example.com"}}
+ peers := []tailcfg.NodeView{
+ (&tailcfg.Node{
+ ID: 1,
+ StableID: "wg",
+ IsWireGuardOnly: true,
+ ExitNodeDNSResolvers: wgResolvers,
+ Hostinfo: (&tailcfg.Hostinfo{}).View(),
+ }).View(),
+ // regular tailscale exit node with DNS capabilities
+ (&tailcfg.Node{
+ Cap: 26,
+ ID: 2,
+ StableID: "ts",
+ Hostinfo: (&tailcfg.Hostinfo{}).View(),
+ }).View(),
+ }
+ exitDOH := peerAPIBase(&netmap.NetworkMap{Peers: peers}, peers[0]) + "/dns-query"
+ routes := map[dnsname.FQDN][]*dnstype.Resolver{
+ "route.example.com.": {{Addr: "route.example.com"}},
+ }
+ stringifyRoutes := func(routes map[dnsname.FQDN][]*dnstype.Resolver) map[string][]*dnstype.Resolver {
+ if routes == nil {
+ return nil
+ }
+ m := make(map[string][]*dnstype.Resolver)
+ for k, v := range routes {
+ m[string(k)] = v
+ }
+ return m
+ }
+
+ tests := []tc{
+ {
+ name: "noExit/noRoutes/noResolver",
+ exitNode: "",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{},
+ wantDefaultResolvers: nil,
+ wantRoutes: nil,
+ },
+ {
+ name: "tsExit/noRoutes/noResolver",
+ exitNode: "ts",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{},
+ wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
+ wantRoutes: nil,
+ },
+ {
+ name: "tsExit/noRoutes/defaultResolver",
+ exitNode: "ts",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers},
+ wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
+ wantRoutes: nil,
+ },
+
+ // The following two cases may need to be revisited. For a shared-in
+ // exit node split-DNS may effectively break, furthermore in the future
+ // if different nodes observe different DNS configurations, even a
+ // tailnet local exit node may present a different DNS configuration,
+ // which may not meet expectations in some use cases.
+ // In the case where a default resolver is set, the default resolver
+ // should also perhaps take precedence also.
+ {
+ name: "tsExit/routes/noResolver",
+ exitNode: "ts",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)},
+ wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
+ wantRoutes: nil,
+ },
+ {
+ name: "tsExit/routes/defaultResolver",
+ exitNode: "ts",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes), Resolvers: defaultResolvers},
+ wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
+ wantRoutes: nil,
+ },
+
+ // WireGuard exit nodes with DNS capabilities provide a "fallback" type
+ // behavior, they have a lower precedence than a default resolver, but
+ // otherwise allow split-DNS to operate as normal, and are used when
+ // there is no default resolver.
+ {
+ name: "wgExit/noRoutes/noResolver",
+ exitNode: "wg",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{},
+ wantDefaultResolvers: wgResolvers,
+ wantRoutes: nil,
+ },
+ {
+ name: "wgExit/noRoutes/defaultResolver",
+ exitNode: "wg",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers},
+ wantDefaultResolvers: defaultResolvers,
+ wantRoutes: nil,
+ },
+ {
+ name: "wgExit/routes/defaultResolver",
+ exitNode: "wg",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes), Resolvers: defaultResolvers},
+ wantDefaultResolvers: defaultResolvers,
+ wantRoutes: routes,
+ },
+ {
+ name: "wgExit/routes/noResolver",
+ exitNode: "wg",
+ peers: peers,
+ dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)},
+ wantDefaultResolvers: wgResolvers,
+ wantRoutes: routes,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ nm := &netmap.NetworkMap{
+ Peers: tc.peers,
+ DNS: *tc.dnsConfig,
+ }
+
+ prefs := &ipn.Prefs{ExitNodeID: tc.exitNode, CorpDNS: true}
+ got := dnsConfigForNetmap(nm, peersMap(tc.peers), prefs.View(), t.Logf, "")
+ if !resolversEqual(t, got.DefaultResolvers, tc.wantDefaultResolvers) {
+ t.Errorf("DefaultResolvers: got %#v, want %#v", got.DefaultResolvers, tc.wantDefaultResolvers)
+ }
+ if !routesEqual(t, got.Routes, tc.wantRoutes) {
+ t.Errorf("Routes: got %#v, want %#v", got.Routes, tc.wantRoutes)
+ }
+ })
+ }
+}
+
+func resolversEqual(t *testing.T, a, b []*dnstype.Resolver) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ t.Errorf("resolversEqual: a == nil || b == nil : %#v != %#v", a, b)
+ return false
+ }
+ if len(a) != len(b) {
+ t.Errorf("resolversEqual: len(a) != len(b) : %#v != %#v", a, b)
+ return false
+ }
+ for i := range a {
+ if !a[i].Equal(b[i]) {
+ t.Errorf("resolversEqual: a != b [%d]: %v != %v", i, *a[i], *b[i])
+ return false
+ }
+ }
+ return true
+}
+
+func routesEqual(t *testing.T, a, b map[dnsname.FQDN][]*dnstype.Resolver) bool {
+ if len(a) != len(b) {
+ t.Logf("routes: len(a) != len(b): %d != %d", len(a), len(b))
+ return false
+ }
+ for name := range a {
+ if !resolversEqual(t, a[name], b[name]) {
+ t.Logf("routes: a != b [%s]: %v != %v", name, a[name], b[name])
+ return false
+ }
+ }
+ return true
+}
diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go
index a02ee2e400be2..f9c194fc81598 100644
--- a/ipn/ipnlocal/peerapi.go
+++ b/ipn/ipnlocal/peerapi.go
@@ -9,48 +9,37 @@ import (
"encoding/json"
"errors"
"fmt"
- "hash/adler32"
"hash/crc32"
"html"
"io"
- "io/fs"
"net"
"net/http"
"net/netip"
- "net/url"
"os"
- "path"
- "path/filepath"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"sync"
- "sync/atomic"
"time"
- "unicode"
- "unicode/utf8"
"github.com/kortschak/wol"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http/httpguts"
- "tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
- "tailscale.com/logtail/backoff"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/interfaces"
"tailscale.com/net/netaddr"
"tailscale.com/net/netutil"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
+ "tailscale.com/taildrop"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
- "tailscale.com/util/multierr"
- "tailscale.com/version/distro"
"tailscale.com/wgengine/filter"
)
@@ -61,393 +50,16 @@ var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, stri
var addH2C func(*http.Server)
type peerAPIServer struct {
- b *LocalBackend
- rootDir string // empty means file receiving unavailable
- knownEmpty atomic.Bool
- resolver *resolver.Resolver
-
- // directFileMode is whether we're writing files directly to a
- // download directory (as *.partial files), rather than making
- // the frontend retrieve it over localapi HTTP and write it
- // somewhere itself. This is used on the GUI macOS versions
- // and on Synology.
- // In directFileMode, the peerapi doesn't do the final rename
- // from "foo.jpg.partial" to "foo.jpg" unless
- // directFileDoFinalRename is set.
- directFileMode bool
-
- // directFileDoFinalRename is whether in directFileMode we
- // additionally move the *.direct file to its final name after
- // it's received.
- directFileDoFinalRename bool
-}
-
-const (
- // partialSuffix is the suffix appended to files while they're
- // still in the process of being transferred.
- partialSuffix = ".partial"
-
- // deletedSuffix is the suffix for a deleted marker file
- // that's placed next to a file (without the suffix) that we
- // tried to delete, but Windows wouldn't let us. These are
- // only written on Windows (and in tests), but they're not
- // permitted to be uploaded directly on any platform, like
- // partial files.
- deletedSuffix = ".deleted"
-)
+ b *LocalBackend
+ resolver *resolver.Resolver
-func validFilenameRune(r rune) bool {
- switch r {
- case '/':
- return false
- case '\\', ':', '*', '"', '<', '>', '|':
- // Invalid stuff on Windows, but we reject them everywhere
- // for now.
- // TODO(bradfitz): figure out a better plan. We initially just
- // wrote things to disk URL path-escaped, but that's gross
- // when debugging, and just moves the problem to callers.
- // So now we put the UTF-8 filenames on disk directly as
- // sent.
- return false
- }
- return unicode.IsPrint(r)
-}
-
-func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
- if !utf8.ValidString(baseName) {
- return "", false
- }
- if strings.TrimSpace(baseName) != baseName {
- return "", false
- }
- if len(baseName) > 255 {
- return "", false
- }
- // TODO: validate unicode normalization form too? Varies by platform.
- clean := path.Clean(baseName)
- if clean != baseName ||
- clean == "." || clean == ".." ||
- strings.HasSuffix(clean, deletedSuffix) ||
- strings.HasSuffix(clean, partialSuffix) {
- return "", false
- }
- for _, r := range baseName {
- if !validFilenameRune(r) {
- return "", false
- }
- }
- if !filepath.IsLocal(baseName) {
- return "", false
- }
- return filepath.Join(s.rootDir, baseName), true
-}
-
-// hasFilesWaiting reports whether any files are buffered in the
-// tailscaled daemon storage.
-func (s *peerAPIServer) hasFilesWaiting() bool {
- if s == nil || s.rootDir == "" || s.directFileMode {
- return false
- }
- if s.knownEmpty.Load() {
- // Optimization: this is usually empty, so avoid opening
- // the directory and checking. We can't cache the actual
- // has-files-or-not values as the macOS/iOS client might
- // in the future use+delete the files directly. So only
- // keep this negative cache.
- return false
- }
- f, err := os.Open(s.rootDir)
- if err != nil {
- return false
- }
- defer f.Close()
- for {
- des, err := f.ReadDir(10)
- for _, de := range des {
- name := de.Name()
- if strings.HasSuffix(name, partialSuffix) {
- continue
- }
- if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
- // After we're done looping over files, then try
- // to delete this file. Don't do it proactively,
- // as the OS may return "foo.jpg.deleted" before "foo.jpg"
- // and we don't want to delete the ".deleted" file before
- // enumerating to the "foo.jpg" file.
- defer tryDeleteAgain(filepath.Join(s.rootDir, name))
- continue
- }
- if de.Type().IsRegular() {
- _, err := os.Stat(filepath.Join(s.rootDir, name+deletedSuffix))
- if os.IsNotExist(err) {
- return true
- }
- if err == nil {
- tryDeleteAgain(filepath.Join(s.rootDir, name))
- continue
- }
- }
- }
- if err == io.EOF {
- s.knownEmpty.Store(true)
- }
- if err != nil {
- break
- }
- }
- return false
-}
-
-// WaitingFiles returns the list of files that have been sent by a
-// peer that are waiting in the buffered "pick up" directory owned by
-// the Tailscale daemon.
-//
-// As a side effect, it also does any lazy deletion of files as
-// required by Windows.
-func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
- if s == nil {
- return nil, errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return nil, errNoTaildrop
- }
- if s.directFileMode {
- return nil, nil
- }
- f, err := os.Open(s.rootDir)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
- for {
- des, err := f.ReadDir(10)
- for _, de := range des {
- name := de.Name()
- if strings.HasSuffix(name, partialSuffix) {
- continue
- }
- if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
- if deleted == nil {
- deleted = map[string]bool{}
- }
- deleted[name] = true
- continue
- }
- if de.Type().IsRegular() {
- fi, err := de.Info()
- if err != nil {
- continue
- }
- ret = append(ret, apitype.WaitingFile{
- Name: filepath.Base(name),
- Size: fi.Size(),
- })
- }
- }
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- }
- if len(deleted) > 0 {
- // Filter out any return values "foo.jpg" where a
- // "foo.jpg.deleted" marker file exists on disk.
- all := ret
- ret = ret[:0]
- for _, wf := range all {
- if !deleted[wf.Name] {
- ret = append(ret, wf)
- }
- }
- // And do some opportunistic deleting while we're here.
- // Maybe Windows is done virus scanning the file we tried
- // to delete a long time ago and will let us delete it now.
- for name := range deleted {
- tryDeleteAgain(filepath.Join(s.rootDir, name))
- }
- }
- sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
- return ret, nil
+ taildrop *taildrop.Handler
}
var (
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
- errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
)
-// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
-// it failed earlier. This happens on Windows when various anti-virus
-// tools hook into filesystem operations and have the file open still
-// while we're trying to delete it. In that case we instead mark it as
-// deleted (writing a "foo.jpg.deleted" marker file), but then we
-// later try to clean them up.
-//
-// fullPath is the full path to the file without the deleted suffix.
-func tryDeleteAgain(fullPath string) {
- if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
- os.Remove(fullPath + deletedSuffix)
- }
-}
-
-func (s *peerAPIServer) DeleteFile(baseName string) error {
- if s == nil {
- return errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return errNoTaildrop
- }
- if s.directFileMode {
- return errors.New("deletes not allowed in direct mode")
- }
- path, ok := s.diskPath(baseName)
- if !ok {
- return errors.New("bad filename")
- }
- var bo *backoff.Backoff
- logf := s.b.logf
- t0 := s.b.clock.Now()
- for {
- err := os.Remove(path)
- if err != nil && !os.IsNotExist(err) {
- err = redactErr(err)
- // Put a retry loop around deletes on Windows. Windows
- // file descriptor closes are effectively asynchronous,
- // as a bunch of hooks run on/after close, and we can't
- // necessarily delete the file for a while after close,
- // as we need to wait for everybody to be done with
- // it. (on Windows, unlike Unix, a file can't be deleted
- // if it's open anywhere)
- // So try a few times but ultimately just leave a
- // "foo.jpg.deleted" marker file to note that it's
- // deleted and we clean it up later.
- if runtime.GOOS == "windows" {
- if bo == nil {
- bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
- }
- if s.b.clock.Since(t0) < 5*time.Second {
- bo.BackOff(context.Background(), err)
- continue
- }
- if err := touchFile(path + deletedSuffix); err != nil {
- logf("peerapi: failed to leave deleted marker: %v", err)
- }
- }
- logf("peerapi: failed to DeleteFile: %v", err)
- return err
- }
- return nil
- }
-}
-
-// redacted is a fake path name we use in errors, to avoid
-// accidentally logging actual filenames anywhere.
-const redacted = "redacted"
-
-type redactedErr struct {
- msg string
- inner error
-}
-
-func (re *redactedErr) Error() string {
- return re.msg
-}
-
-func (re *redactedErr) Unwrap() error {
- return re.inner
-}
-
-func redactString(s string) string {
- hash := adler32.Checksum([]byte(s))
-
- var buf [len(redacted) + len(".12345678")]byte
- b := append(buf[:0], []byte(redacted)...)
- b = append(b, '.')
- b = strconv.AppendUint(b, uint64(hash), 16)
- return string(b)
-}
-
-func redactErr(root error) error {
- // redactStrings is a list of sensitive strings that were redacted.
- // It is not sufficient to just snub out sensitive fields in Go errors
- // since some wrapper errors like fmt.Errorf pre-cache the error string,
- // which would unfortunately remain unaffected.
- var redactStrings []string
-
- // Redact sensitive fields in known Go error types.
- var unknownErrors int
- multierr.Range(root, func(err error) bool {
- switch err := err.(type) {
- case *os.PathError:
- redactStrings = append(redactStrings, err.Path)
- err.Path = redactString(err.Path)
- case *os.LinkError:
- redactStrings = append(redactStrings, err.New, err.Old)
- err.New = redactString(err.New)
- err.Old = redactString(err.Old)
- default:
- unknownErrors++
- }
- return true
- })
-
- // If there are no redacted strings or no unknown error types,
- // then we can return the possibly modified root error verbatim.
- // Otherwise, we must replace redacted strings from any wrappers.
- if len(redactStrings) == 0 || unknownErrors == 0 {
- return root
- }
-
- // Stringify and replace any paths that we found above, then return
- // the error wrapped in a type that uses the newly-redacted string
- // while also allowing Unwrap()-ing to the inner error type(s).
- s := root.Error()
- for _, toRedact := range redactStrings {
- s = strings.ReplaceAll(s, toRedact, redactString(toRedact))
- }
- return &redactedErr{msg: s, inner: root}
-}
-
-func touchFile(path string) error {
- f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
- if err != nil {
- return redactErr(err)
- }
- return f.Close()
-}
-
-func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
- if s == nil {
- return nil, 0, errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return nil, 0, errNoTaildrop
- }
- if s.directFileMode {
- return nil, 0, errors.New("opens not allowed in direct mode")
- }
- path, ok := s.diskPath(baseName)
- if !ok {
- return nil, 0, errors.New("bad filename")
- }
- if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
- tryDeleteAgain(path)
- return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
- }
- f, err := os.Open(path)
- if err != nil {
- return nil, 0, redactErr(err)
- }
- fi, err := f.Stat()
- if err != nil {
- f.Close()
- return nil, 0, redactErr(err)
- }
- return f, fi.Size(), nil
-}
-
func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net.Listener, err error) {
// Android for whatever reason often has problems creating the peerapi listener.
// But since we started intercepting it with netstack, it's not even important that
@@ -971,64 +583,6 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "")
}
-type incomingFile struct {
- name string // "foo.jpg"
- started time.Time
- size int64 // or -1 if unknown; never 0
- w io.Writer // underlying writer
- ph *peerAPIHandler
- partialPath string // non-empty in direct mode
-
- mu sync.Mutex
- copied int64
- done bool
- lastNotify time.Time
-}
-
-func (f *incomingFile) markAndNotifyDone() {
- f.mu.Lock()
- f.done = true
- f.mu.Unlock()
- b := f.ph.ps.b
- b.sendFileNotify()
-}
-
-func (f *incomingFile) Write(p []byte) (n int, err error) {
- n, err = f.w.Write(p)
-
- b := f.ph.ps.b
- var needNotify bool
- defer func() {
- if needNotify {
- b.sendFileNotify()
- }
- }()
- if n > 0 {
- f.mu.Lock()
- defer f.mu.Unlock()
- f.copied += int64(n)
- now := b.clock.Now()
- if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
- f.lastNotify = now
- needNotify = true
- }
- }
- return n, err
-}
-
-func (f *incomingFile) PartialFile() ipn.PartialFile {
- f.mu.Lock()
- defer f.mu.Unlock()
- return ipn.PartialFile{
- Name: f.name,
- Started: f.started,
- DeclaredSize: f.size,
- Received: f.copied,
- PartialPath: f.partialPath,
- Done: f.done,
- }
-}
-
// canPutFile reports whether h can put a file ("Taildrop") to this node.
func (h *peerAPIHandler) canPutFile() bool {
if h.peerNode.UnsignedPeerAPIOnly() {
@@ -1072,10 +626,6 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
- if !envknob.CanTaildrop() {
- http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
- return
- }
if !h.canPutFile() {
http.Error(w, "Taildrop access denied", http.StatusForbidden)
return
@@ -1084,109 +634,12 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
return
}
- if r.Method != "PUT" {
- http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
- return
- }
- if h.ps.rootDir == "" {
- http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
- return
- }
- if distro.Get() == distro.Unraid && !h.ps.directFileMode {
- http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
- return
- }
- rawPath := r.URL.EscapedPath()
- suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
- if !ok {
- http.Error(w, "misconfigured internals", 500)
- return
- }
- if suffix == "" {
- http.Error(w, "empty filename", 400)
- return
- }
- if strings.Contains(suffix, "/") {
- http.Error(w, "directories not supported", 400)
- return
- }
- baseName, err := url.PathUnescape(suffix)
- if err != nil {
- http.Error(w, "bad path encoding", 400)
- return
- }
- dstFile, ok := h.ps.diskPath(baseName)
- if !ok {
- http.Error(w, "bad filename", 400)
- return
- }
t0 := h.ps.b.clock.Now()
- // TODO(bradfitz): prevent same filename being sent by two peers at once
- partialFile := dstFile + partialSuffix
- f, err := os.Create(partialFile)
- if err != nil {
- h.logf("put Create error: %v", redactErr(err))
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ n, ok := h.ps.taildrop.HandlePut(w, r)
+ if ok {
+ d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
+ h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
}
- var success bool
- defer func() {
- if !success {
- os.Remove(partialFile)
- }
- }()
- var finalSize int64
- var inFile *incomingFile
- if r.ContentLength != 0 {
- inFile = &incomingFile{
- name: baseName,
- started: h.ps.b.clock.Now(),
- size: r.ContentLength,
- w: f,
- ph: h,
- }
- if h.ps.directFileMode {
- inFile.partialPath = partialFile
- }
- h.ps.b.registerIncomingFile(inFile, true)
- defer h.ps.b.registerIncomingFile(inFile, false)
- n, err := io.Copy(inFile, r.Body)
- if err != nil {
- err = redactErr(err)
- f.Close()
- h.logf("put Copy error: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- finalSize = n
- }
- if err := redactErr(f.Close()); err != nil {
- h.logf("put Close error: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
- if inFile != nil { // non-zero length; TODO: notify even for zero length
- inFile.markAndNotifyDone()
- }
- } else {
- if err := os.Rename(partialFile, dstFile); err != nil {
- err = redactErr(err)
- h.logf("put final rename: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-
- d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
- h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
-
- // TODO: set modtime
- // TODO: some real response
- success = true
- io.WriteString(w, "{}\n")
- h.ps.knownEmpty.Store(false)
- h.ps.b.sendFileNotify()
}
func approxSize(n int64) string {
@@ -1284,7 +737,7 @@ func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request)
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
- var password []byte // TODO(bradfitz): support?
+ var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
st := h.ps.b.sys.NetMon.Get().InterfaceState()
if st == nil {
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go
index d6b4eafc907bf..7996dc62ec457 100644
--- a/ipn/ipnlocal/peerapi_test.go
+++ b/ipn/ipnlocal/peerapi_test.go
@@ -5,7 +5,6 @@ package ipnlocal
import (
"bytes"
- "errors"
"fmt"
"io"
"io/fs"
@@ -15,7 +14,6 @@ import (
"net/netip"
"os"
"path/filepath"
- "runtime"
"strings"
"testing"
@@ -23,6 +21,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
+ "tailscale.com/taildrop"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -67,7 +66,7 @@ func bodyNotContains(sub string) check {
func fileHasSize(name string, size int) check {
return func(t *testing.T, e *peerAPITestEnv) {
- root := e.ph.ps.rootDir
+ root := e.ph.ps.taildrop.Dir
if root == "" {
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
return
@@ -83,7 +82,7 @@ func fileHasSize(name string, size int) check {
func fileHasContents(name string, want string) check {
return func(t *testing.T, e *peerAPITestEnv) {
- root := e.ph.ps.rootDir
+ root := e.ph.ps.taildrop.Dir
if root == "" {
t.Errorf("no rootdir; can't check contents of %q", name)
return
@@ -116,14 +115,14 @@ func TestHandlePeerAPI(t *testing.T) {
capSharing bool // self node has file sharing capability
debugCap bool // self node has debug capability
omitRoot bool // don't configure
- req *http.Request
+ reqs []*http.Request
checks []check
}{
{
name: "not_peer_api",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("GET", "/", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
@@ -134,7 +133,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "not_peer_api_not_owner",
isSelf: false,
capSharing: true,
- req: httptest.NewRequest("GET", "/", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
checks: checks(
httpStatus(200),
bodyContains("This is my Tailscale device."),
@@ -145,21 +144,21 @@ func TestHandlePeerAPI(t *testing.T) {
name: "goroutines/deny-self-no-cap",
isSelf: true,
debugCap: false,
- req: httptest.NewRequest("GET", "/v0/goroutines", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(httpStatus(403)),
},
{
name: "goroutines/deny-nonself",
isSelf: false,
debugCap: true,
- req: httptest.NewRequest("GET", "/v0/goroutines", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(httpStatus(403)),
},
{
name: "goroutines/accept-self",
isSelf: true,
debugCap: true,
- req: httptest.NewRequest("GET", "/v0/goroutines", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
checks: checks(
httpStatus(200),
bodyContains("ServeHTTP"),
@@ -169,7 +168,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "reject_non_owner_put",
isSelf: false,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("Taildrop access denied"),
@@ -179,7 +178,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "owner_without_cap",
isSelf: true,
capSharing: false,
- req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("file sharing not enabled by Tailscale admin"),
@@ -190,7 +189,7 @@ func TestHandlePeerAPI(t *testing.T) {
omitRoot: true,
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(http.StatusInternalServerError),
bodyContains("Taildrop disabled; no storage directory"),
@@ -200,7 +199,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_method",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("POST", "/v0/put/foo", nil),
+ reqs: []*http.Request{httptest.NewRequest("POST", "/v0/put/foo", nil)},
checks: checks(
httpStatus(405),
bodyContains("expected method PUT"),
@@ -210,7 +209,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_zero_length",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(200),
bodyContains("{}"),
@@ -222,7 +221,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_non_zero_length_content_length",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
checks: checks(
httpStatus(200),
bodyContains("{}"),
@@ -234,7 +233,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_non_zero_length_chunked",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo", struct{ io.Reader }{strings.NewReader("contents")}),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", struct{ io.Reader }{strings.NewReader("contents")})},
checks: checks(
httpStatus(200),
bodyContains("{}"),
@@ -246,7 +245,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_partial",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo.partial", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.partial", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -256,7 +255,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_deleted",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -266,7 +265,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_dot",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/.", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/.", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -276,7 +275,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_empty",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/", nil)},
checks: checks(
httpStatus(400),
bodyContains("empty filename"),
@@ -286,7 +285,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_slash",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/foo/bar", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo/bar", nil)},
checks: checks(
httpStatus(400),
bodyContains("directories not supported"),
@@ -296,7 +295,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_encoded_dot",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -306,7 +305,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_encoded_slash",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -316,7 +315,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_encoded_backslash",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -326,7 +325,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_encoded_dotdot",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -336,7 +335,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "bad_filename_encoded_dotdot_out",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -346,7 +345,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_spaces_and_caps",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Foo Bar.dat"), strings.NewReader("baz")),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("Foo Bar.dat"), strings.NewReader("baz"))},
checks: checks(
httpStatus(200),
bodyContains("{}"),
@@ -357,7 +356,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_unicode",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Томас и его друзья.mp3"), strings.NewReader("главный озорник")),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("Томас и его друзья.mp3"), strings.NewReader("главный озорник"))},
checks: checks(
httpStatus(200),
bodyContains("{}"),
@@ -368,7 +367,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_invalid_utf8",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -378,7 +377,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_invalid_null",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/%00", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%00", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -388,7 +387,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_invalid_non_printable",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/%01", nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%01", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -398,7 +397,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_invalid_colon",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -408,7 +407,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "put_invalid_surrounding_whitespace",
isSelf: true,
capSharing: true,
- req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil),
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
@@ -418,7 +417,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "host-val/bad-ip",
isSelf: true,
debugCap: true,
- req: httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil)},
checks: checks(
httpStatus(403),
),
@@ -427,7 +426,7 @@ func TestHandlePeerAPI(t *testing.T) {
name: "host-val/no-port",
isSelf: true,
debugCap: true,
- req: httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil)},
checks: checks(
httpStatus(403),
),
@@ -436,11 +435,31 @@ func TestHandlePeerAPI(t *testing.T) {
name: "host-val/peer",
isSelf: true,
debugCap: true,
- req: httptest.NewRequest("GET", "http://peer/v0/env", nil),
+ reqs: []*http.Request{httptest.NewRequest("GET", "http://peer/v0/env", nil)},
checks: checks(
httpStatus(200),
),
},
+ {
+ name: "bad_duplicate_zero_length",
+ isSelf: true,
+ capSharing: true,
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil), httptest.NewRequest("PUT", "/v0/put/foo", nil)},
+ checks: checks(
+ httpStatus(409),
+ bodyContains("file exists"),
+ ),
+ },
+ {
+ name: "bad_duplicate_non_zero_length_content_length",
+ isSelf: true,
+ capSharing: true,
+ reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")), httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
+ checks: checks(
+ httpStatus(409),
+ bodyContains("file exists"),
+ ),
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -472,13 +491,21 @@ func TestHandlePeerAPI(t *testing.T) {
var rootDir string
if !tt.omitRoot {
rootDir = t.TempDir()
- e.ph.ps.rootDir = rootDir
+ if e.ph.ps.taildrop == nil {
+ e.ph.ps.taildrop = &taildrop.Handler{
+ Logf: e.logBuf.Logf,
+ Clock: &tstest.Clock{},
+ }
+ }
+ e.ph.ps.taildrop.Dir = rootDir
}
- e.rr = httptest.NewRecorder()
- if tt.req.Host == "example.com" {
- tt.req.Host = "100.100.100.101:12345"
+ for _, req := range tt.reqs {
+ e.rr = httptest.NewRecorder()
+ if req.Host == "example.com" {
+ req.Host = "100.100.100.101:12345"
+ }
+ e.ph.ServeHTTP(e.rr, req)
}
- e.ph.ServeHTTP(e.rr, tt.req)
for _, f := range tt.checks {
f(t, &e)
}
@@ -509,7 +536,11 @@ func TestFileDeleteRace(t *testing.T) {
capFileSharing: true,
clock: &tstest.Clock{},
},
- rootDir: dir,
+ taildrop: &taildrop.Handler{
+ Logf: t.Logf,
+ Clock: &tstest.Clock{},
+ Dir: dir,
+ },
}
ph := &peerAPIHandler{
isSelf: true,
@@ -528,7 +559,7 @@ func TestFileDeleteRace(t *testing.T) {
if res := rr.Result(); res.StatusCode != 200 {
t.Fatal(res.Status)
}
- wfs, err := ps.WaitingFiles()
+ wfs, err := ps.taildrop.WaitingFiles()
if err != nil {
t.Fatal(err)
}
@@ -536,10 +567,10 @@ func TestFileDeleteRace(t *testing.T) {
t.Fatalf("waiting files = %d; want 1", len(wfs))
}
- if err := ps.DeleteFile("foo.txt"); err != nil {
+ if err := ps.taildrop.DeleteFile("foo.txt"); err != nil {
t.Fatal(err)
}
- wfs, err = ps.WaitingFiles()
+ wfs, err = ps.taildrop.WaitingFiles()
if err != nil {
t.Fatal(err)
}
@@ -549,90 +580,6 @@ func TestFileDeleteRace(t *testing.T) {
}
}
-// Tests "foo.jpg.deleted" marks (for Windows).
-func TestDeletedMarkers(t *testing.T) {
- dir := t.TempDir()
- ps := &peerAPIServer{
- b: &LocalBackend{
- logf: t.Logf,
- capFileSharing: true,
- },
- rootDir: dir,
- }
-
- nothingWaiting := func() {
- t.Helper()
- ps.knownEmpty.Store(false)
- if ps.hasFilesWaiting() {
- t.Fatal("unexpected files waiting")
- }
- }
- touch := func(base string) {
- t.Helper()
- if err := touchFile(filepath.Join(dir, base)); err != nil {
- t.Fatal(err)
- }
- }
- wantEmptyTempDir := func() {
- t.Helper()
- if fis, err := os.ReadDir(dir); err != nil {
- t.Fatal(err)
- } else if len(fis) > 0 && runtime.GOOS != "windows" {
- for _, fi := range fis {
- t.Errorf("unexpected file in tempdir: %q", fi.Name())
- }
- }
- }
-
- nothingWaiting()
- wantEmptyTempDir()
-
- touch("foo.jpg.deleted")
- nothingWaiting()
- wantEmptyTempDir()
-
- touch("foo.jpg.deleted")
- touch("foo.jpg")
- nothingWaiting()
- wantEmptyTempDir()
-
- touch("foo.jpg.deleted")
- touch("foo.jpg")
- wf, err := ps.WaitingFiles()
- if err != nil {
- t.Fatal(err)
- }
- if len(wf) != 0 {
- t.Fatalf("WaitingFiles = %d; want 0", len(wf))
- }
- wantEmptyTempDir()
-
- touch("foo.jpg.deleted")
- touch("foo.jpg")
- if rc, _, err := ps.OpenFile("foo.jpg"); err == nil {
- rc.Close()
- t.Fatal("unexpected foo.jpg open")
- }
- wantEmptyTempDir()
-
- // And verify basics still work in non-deleted cases.
- touch("foo.jpg")
- touch("bar.jpg.deleted")
- if wf, err := ps.WaitingFiles(); err != nil {
- t.Error(err)
- } else if len(wf) != 1 {
- t.Errorf("WaitingFiles = %d; want 1", len(wf))
- } else if wf[0].Name != "foo.jpg" {
- t.Errorf("unexpected waiting file %+v", wf[0])
- }
- if rc, _, err := ps.OpenFile("foo.jpg"); err != nil {
- t.Fatal(err)
- } else {
- rc.Close()
- }
-
-}
-
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
var h peerAPIHandler
@@ -687,67 +634,3 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
}
}
-
-func TestRedactErr(t *testing.T) {
- testCases := []struct {
- name string
- err func() error
- want string
- }{
- {
- name: "PathError",
- err: func() error {
- return &os.PathError{
- Op: "open",
- Path: "/tmp/sensitive.txt",
- Err: fs.ErrNotExist,
- }
- },
- want: `open redacted.41360718: file does not exist`,
- },
- {
- name: "LinkError",
- err: func() error {
- return &os.LinkError{
- Op: "symlink",
- Old: "/tmp/sensitive.txt",
- New: "/tmp/othersensitive.txt",
- Err: fs.ErrNotExist,
- }
- },
- want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
- },
- {
- name: "something else",
- err: func() error { return errors.New("i am another error type") },
- want: `i am another error type`,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // For debugging
- var i int
- for err := tc.err(); err != nil; err = errors.Unwrap(err) {
- t.Logf("%d: %T @ %p", i, err, err)
- i++
- }
-
- t.Run("Root", func(t *testing.T) {
- got := redactErr(tc.err()).Error()
- if got != tc.want {
- t.Errorf("err = %q; want %q", got, tc.want)
- }
- })
- t.Run("Wrapped", func(t *testing.T) {
- wrapped := fmt.Errorf("wrapped error: %w", tc.err())
- want := "wrapped error: " + tc.want
-
- got := redactErr(wrapped).Error()
- if got != want {
- t.Errorf("err = %q; want %q", got, want)
- }
- })
- })
- }
-}
diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go
index 30f4c59f84a2b..74e5c52bd6ac1 100644
--- a/ipn/ipnlocal/profiles.go
+++ b/ipn/ipnlocal/profiles.go
@@ -452,20 +452,24 @@ var defaultPrefs = func() ipn.PrefsView {
prefs.LoggedOut = true
prefs.WantRunning = false
- prefs.ControlURL = winutil.GetPolicyString("LoginURL", "")
+ controlURL, _ := winutil.GetPolicyString("LoginURL")
+ prefs.ControlURL = controlURL
+
prefs.ExitNodeIP = resolveExitNodeIP(netip.Addr{})
// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
// backend), so this has to convert between the two conventions.
- prefs.ShieldsUp = winutil.GetPolicyString("AllowIncomingConnections", "") == "never"
- prefs.ForceDaemon = winutil.GetPolicyString("UnattendedMode", "") == "always"
+ shieldsUp, _ := winutil.GetPolicyString("AllowIncomingConnections")
+ prefs.ShieldsUp = shieldsUp == "never"
+ forceDaemon, _ := winutil.GetPolicyString("UnattendedMode")
+ prefs.ForceDaemon = forceDaemon == "always"
return prefs.View()
}()
func resolveExitNodeIP(defIP netip.Addr) (ret netip.Addr) {
ret = defIP
- if exitNode := winutil.GetPolicyString("ExitNodeIP", ""); exitNode != "" {
+ if exitNode, _ := winutil.GetPolicyString("ExitNodeIP"); exitNode != "" {
if ip, err := netip.ParseAddr(exitNode); err == nil {
ret = ip
}
diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go
index 03b41d6a91554..07fcd08d1e8b1 100644
--- a/ipn/ipnstate/ipnstate.go
+++ b/ipn/ipnstate/ipnstate.go
@@ -299,6 +299,11 @@ func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
return ps.CapMap.Contains(cap) || slices.Contains(ps.Capabilities, cap)
}
+// IsTagged reports whether ps is tagged.
+func (ps *PeerStatus) IsTagged() bool {
+ return ps.Tags != nil && ps.Tags.Len() > 0
+}
+
// 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.
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index e68e6adafc4b5..3ac5449088cba 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -408,18 +408,32 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
+ h.serveWhoIsWithBackend(w, r, h.b)
+}
+
+// localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
+// by the localapi WhoIs method.
+type localBackendWhoIsMethods interface {
+ WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
+ PeerCaps(netip.Addr) tailcfg.PeerCapMap
+}
+
+func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
- b := h.b
var ipp netip.AddrPort
if v := r.FormValue("addr"); v != "" {
- var err error
- ipp, err = netip.ParseAddrPort(v)
- if err != nil {
- http.Error(w, "invalid 'addr' parameter", 400)
- return
+ if ip, err := netip.ParseAddr(v); err == nil {
+ ipp = netip.AddrPortFrom(ip, 0)
+ } else {
+ var err error
+ ipp, err = netip.ParseAddrPort(v)
+ if err != nil {
+ http.Error(w, "invalid 'addr' parameter", 400)
+ return
+ }
}
} else {
http.Error(w, "missing 'addr' parameter", 400)
@@ -433,7 +447,9 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
res := &apitype.WhoIsResponse{
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
UserProfile: &u, // always non-nil per WhoIsResponse contract
- CapMap: b.PeerCaps(ipp.Addr()),
+ }
+ if n.Addresses().Len() > 0 {
+ res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
@@ -566,6 +582,8 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if err == nil {
return
}
+ case "pick-new-derp":
+ err = h.b.DebugPickNewDERP()
case "":
err = fmt.Errorf("missing parameter 'action'")
default:
diff --git a/ipn/localapi/localapi_test.go b/ipn/localapi/localapi_test.go
index 057da9039665e..2741dc0efd3a3 100644
--- a/ipn/localapi/localapi_test.go
+++ b/ipn/localapi/localapi_test.go
@@ -9,11 +9,15 @@ import (
"io"
"net/http"
"net/http/httptest"
+ "net/netip"
+ "net/url"
+ "strings"
"testing"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnlocal"
+ "tailscale.com/tailcfg"
"tailscale.com/tstest"
)
@@ -77,3 +81,68 @@ func TestSetPushDeviceToken(t *testing.T) {
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
}
}
+
+type whoIsBackend struct {
+ whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
+ peerCaps map[netip.Addr]tailcfg.PeerCapMap
+}
+
+func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+ return b.whoIs(ipp)
+}
+
+func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
+ return b.peerCaps[ip]
+}
+
+// Tests that the WhoIs handler accepts either IPs or IP:ports.
+//
+// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
+func TestWhoIsJustIP(t *testing.T) {
+ h := &Handler{
+ PermitRead: true,
+ }
+ for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
+ rec := httptest.NewRecorder()
+ t.Run(input, func(t *testing.T) {
+ b := whoIsBackend{
+ whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
+ if !strings.Contains(input, ":") {
+ want := netip.MustParseAddrPort("100.101.102.103:0")
+ if ipp != want {
+ t.Fatalf("backend called with %v; want %v", ipp, want)
+ }
+ }
+ return (&tailcfg.Node{
+ ID: 123,
+ Addresses: []netip.Prefix{
+ netip.MustParsePrefix("100.101.102.103/32"),
+ },
+ }).View(),
+ tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
+ true
+ },
+ peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
+ netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
+ "foo": {`"bar"`},
+ },
+ },
+ }
+ h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
+
+ var res apitype.WhoIsResponse
+ if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
+ t.Fatal(err)
+ }
+ if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
+ t.Errorf("res.Node.ID=%v, want %v", got, want)
+ }
+ if got, want := res.UserProfile.DisplayName, "foo"; got != want {
+ t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
+ }
+ if got, want := len(res.CapMap), 1; got != want {
+ t.Errorf("capmap size=%v, want %v", got, want)
+ }
+ })
+ }
+}
diff --git a/ipn/prefs.go b/ipn/prefs.go
index 356359533ea8f..1482934266a8e 100644
--- a/ipn/prefs.go
+++ b/ipn/prefs.go
@@ -200,6 +200,10 @@ type Prefs struct {
// AutoUpdatePrefs docs for more details.
AutoUpdate AutoUpdatePrefs
+ // PostureChecking enables the collection of information used for device
+ // posture checks.
+ PostureChecking bool
+
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
@@ -246,6 +250,7 @@ type MaskedPrefs struct {
OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet bool `json:",omitempty"`
+ PostureCheckingSet bool `json:",omitempty"`
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
@@ -439,7 +444,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) &&
p.ProfileName == p2.ProfileName &&
- p.AutoUpdate == p2.AutoUpdate
+ p.AutoUpdate == p2.AutoUpdate &&
+ p.PostureChecking == p2.PostureChecking
}
func (au AutoUpdatePrefs) Pretty() string {
diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go
index e963d32654495..20d1bbfac0e82 100644
--- a/ipn/prefs_test.go
+++ b/ipn/prefs_test.go
@@ -57,6 +57,7 @@ func TestPrefsEqual(t *testing.T) {
"OperatorUser",
"ProfileName",
"AutoUpdate",
+ "PostureChecking",
"Persist",
}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
@@ -304,6 +305,16 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
true,
},
+ {
+ &Prefs{PostureChecking: true},
+ &Prefs{PostureChecking: true},
+ true,
+ },
+ {
+ &Prefs{PostureChecking: true},
+ &Prefs{PostureChecking: false},
+ false,
+ },
}
for i, tt := range tests {
got := tt.a.Equals(tt.b)
diff --git a/ipn/serve.go b/ipn/serve.go
index 74975d6b0b26b..b22a5bdb77d18 100644
--- a/ipn/serve.go
+++ b/ipn/serve.go
@@ -9,10 +9,10 @@ import (
"net"
"net/netip"
"net/url"
- "slices"
"strconv"
"strings"
+ "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
@@ -237,23 +237,21 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// 2. the node has the "funnel" nodeAttr
// 3. the port is allowed for Funnel
//
-// 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 []tailcfg.NodeCapability) error {
- if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
+// The node arg should be the ipnstate.Status.Self node.
+func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
+ if !node.HasCap(tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
}
- if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
+ if !node.HasCap(tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
}
- return CheckFunnelPort(port, nodeAttrs)
+ return CheckFunnelPort(port, node)
}
// 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 []tailcfg.NodeCapability) error {
+func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
deny := func(allowedPorts string) error {
if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
@@ -261,24 +259,50 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) erro
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
}
var portsStr string
- for _, attr := range nodeAttrs {
- attr := string(attr)
- if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
- continue
- }
+ parseAttr := func(attr string) (string, error) {
u, err := url.Parse(attr)
if err != nil {
- return deny("")
+ return "", deny("")
}
- portsStr = u.Query().Get("ports")
+ portsStr := u.Query().Get("ports")
if portsStr == "" {
- return deny("")
+ return "", deny("")
}
u.RawQuery = ""
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
- return deny("")
+ return "", deny("")
+ }
+ return portsStr, nil
+ }
+ for attr := range node.CapMap {
+ attr := string(attr)
+ if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
+ continue
+ }
+ var err error
+ portsStr, err = parseAttr(attr)
+ if err != nil {
+ return err
+ }
+ break
+ }
+ if portsStr == "" {
+ for _, attr := range node.Capabilities {
+ attr := string(attr)
+ if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
+ continue
+ }
+ var err error
+ portsStr, err = parseAttr(attr)
+ if err != nil {
+ return err
+ }
+ break
}
}
+ if portsStr == "" {
+ return deny("")
+ }
wantedPortString := strconv.Itoa(int(wantedPort))
for _, ps := range strings.Split(portsStr, ",") {
if ps == "" {
diff --git a/ipn/serve_test.go b/ipn/serve_test.go
index 87ae2eba414f0..2576b2b66a005 100644
--- a/ipn/serve_test.go
+++ b/ipn/serve_test.go
@@ -5,6 +5,7 @@ package ipn
import (
"testing"
+ "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
@@ -26,7 +27,11 @@ func TestCheckFunnelAccess(t *testing.T) {
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
}
for _, tt := range tests {
- err := CheckFunnelAccess(tt.port, tt.caps)
+ cm := tailcfg.NodeCapMap{}
+ for _, c := range tt.caps {
+ cm[c] = nil
+ }
+ err := CheckFunnelAccess(tt.port, &ipnstate.PeerStatus{CapMap: cm})
switch {
case err != nil && tt.wantErr,
err == nil && !tt.wantErr:
diff --git a/licenses/apple.md b/licenses/apple.md
index de4ff6059c7f6..9a3e661509dfd 100644
--- a/licenses/apple.md
+++ b/licenses/apple.md
@@ -11,66 +11,66 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0/LICENSE))
- - [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
- - [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
- - [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
- - [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
- - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- - [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.4.0/LICENSE))
- - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
+ - [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.40/credentials/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.11/feature/ec2/imds/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.41/internal/configsources/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.35/internal/endpoints/v2/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.43/internal/ini/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/internal/sync/singleflight/LICENSE))
+ - [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.35/service/internal/presigned-url/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.38.0/service/ssm/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.14.1/service/sso/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.1/service/ssooidc/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.22.0/service/sts/LICENSE.txt))
+ - [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.14.2/LICENSE))
+ - [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.14.2/internal/sync/singleflight/LICENSE))
+ - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
+ - [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
+ - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
+ - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.1/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- - [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
+ - [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/65c27093e38a/LICENSE))
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
- - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
- - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
+ - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
+ - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
+ - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
+ - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
+ - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
+ - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.56/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- - [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
+ - [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- - [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/93bd5cbf7fd8/LICENSE))
+ - [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
+ - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
- - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b0cb94b8:LICENSE))
+ - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
- - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- - [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
+ - [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
diff --git a/licenses/tailscale.md b/licenses/tailscale.md
index 2e0380d35c263..12185be8a0014 100644
--- a/licenses/tailscale.md
+++ b/licenses/tailscale.md
@@ -18,64 +18,65 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/akutz/memconn](https://pkg.go.dev/github.com/akutz/memconn) ([Apache-2.0](https://github.com/akutz/memconn/blob/v0.1.0/LICENSE))
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
- [github.com/anmitsu/go-shlex](https://pkg.go.dev/github.com/anmitsu/go-shlex) ([MIT](https://github.com/anmitsu/go-shlex/blob/38f4b401e2be/LICENSE))
- - [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.22/config/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.21/credentials/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.3/feature/ec2/imds/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.33/internal/configsources/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.27/internal/endpoints/v2/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.34/internal/ini/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.18.0/internal/sync/singleflight/LICENSE))
- - [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.27/service/internal/presigned-url/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.36.3/service/ssm/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.12.9/service/sso/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.14.9/service/ssooidc/LICENSE.txt))
- - [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.18.10/service/sts/LICENSE.txt))
- - [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
- - [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
- - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- - [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.4.0/LICENSE))
+ - [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.18.42/config/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.13.40/credentials/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.13.11/feature/ec2/imds/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.41/internal/configsources/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.35/internal/endpoints/v2/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.43/internal/ini/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.21.0/internal/sync/singleflight/LICENSE))
+ - [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.35/service/internal/presigned-url/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.38.0/service/ssm/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.14.1/service/sso/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.17.1/service/ssooidc/LICENSE.txt))
+ - [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.22.0/service/sts/LICENSE.txt))
+ - [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.14.2/LICENSE))
+ - [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.14.2/internal/sync/singleflight/LICENSE))
+ - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
+ - [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.18/LICENSE))
- - [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/fc76608aecf0/LICENSE))
- - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
- - [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.2.6/LICENSE))
+ - [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/LICENSE))
+ - [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
+ - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
+ - [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
+ - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.1/LICENSE))
- [github.com/gorilla/csrf](https://pkg.go.dev/github.com/gorilla/csrf) ([BSD-3-Clause](https://github.com/gorilla/csrf/blob/v1.7.1/LICENSE))
- [github.com/gorilla/securecookie](https://pkg.go.dev/github.com/gorilla/securecookie) ([BSD-3-Clause](https://github.com/gorilla/securecookie/blob/v1.1.1/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- - [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
+ - [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/65c27093e38a/LICENSE))
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
+ - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
- - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
- - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
- - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
+ - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
+ - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
+ - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/kr/fs](https://pkg.go.dev/github.com/kr/fs) ([BSD-3-Clause](https://github.com/kr/fs/blob/v0.1.0/LICENSE))
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.13/LICENSE))
- - [github.com/mattn/go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty) ([MIT](https://github.com/mattn/go-isatty/blob/v0.0.18/LICENSE))
+ - [github.com/mattn/go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty) ([MIT](https://github.com/mattn/go-isatty/blob/v0.0.19/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
+ - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
+ - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.56/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- - [github.com/peterbourgon/ff/v3](https://pkg.go.dev/github.com/peterbourgon/ff/v3) ([Apache-2.0](https://github.com/peterbourgon/ff/blob/v3.3.0/LICENSE))
- - [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.17/LICENSE))
+ - [github.com/peterbourgon/ff/v3](https://pkg.go.dev/github.com/peterbourgon/ff/v3) ([Apache-2.0](https://github.com/peterbourgon/ff/blob/v3.4.0/LICENSE))
+ - [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
- - [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.5/LICENSE))
+ - [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.6/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- - [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/68bd39ee4109/LICENSE))
- - [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/93bd5cbf7fd8/LICENSE))
+ - [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/7bcd7bca7bc5/LICENSE))
+ - [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f6748dc88e7/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/v0.11.0/LICENSE))
@@ -84,26 +85,26 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
- - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.12.0:LICENSE))
- - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
- - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.14.0:LICENSE))
- - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.7.0:LICENSE))
- - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.11.0:LICENSE))
- - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.11.0:LICENSE))
- - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.12.0:LICENSE))
+ - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
+ - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
+ - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.12.0:LICENSE))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
+ - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
- - [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
+ - [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
- [inet.af/wf](https://pkg.go.dev/inet.af/wf) ([BSD-3-Clause](https://github.com/inetaf/wf/blob/36129f591884/LICENSE))
- - [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.27.2/LICENSE))
+ - [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.28.2/LICENSE))
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([MIT](https://github.com/kubernetes-sigs/yaml/blob/v1.3.0/LICENSE))
- - [software.sslmate.com/src/go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) ([BSD-3-Clause](https://github.com/SSLMate/go-pkcs12/blob/v0.2.0/LICENSE))
+ - [software.sslmate.com/src/go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) ([BSD-3-Clause](https://github.com/SSLMate/go-pkcs12/blob/v0.2.1/LICENSE))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
- [tailscale.com/tempfork/device](https://pkg.go.dev/tailscale.com/tempfork/device) ([MIT](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/device/LICENSE))
- [tailscale.com/tempfork/gliderlabs/ssh](https://pkg.go.dev/tailscale.com/tempfork/gliderlabs/ssh) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/gliderlabs/ssh/LICENSE))
diff --git a/licenses/windows.md b/licenses/windows.md
index 6dd21872c85b2..fdd033e048ec7 100644
--- a/licenses/windows.md
+++ b/licenses/windows.md
@@ -14,42 +14,42 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
- - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- - [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/fc76608aecf0/LICENSE))
- - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
+ - [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
+ - [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/LICENSE))
+ - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
+ - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.1/LICENSE))
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.2/LICENSE.md))
- - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.16.7/LICENSE))
- - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.16.7/internal/snapref/LICENSE))
- - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.16.7/zstd/internal/xxhash/LICENSE.txt))
+ - [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
+ - [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
+ - [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
+ - [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
- - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.1/LICENSE.md))
- - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.55/LICENSE))
+ - [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
+ - [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.56/LICENSE))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- - [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/a3cf94ed774a/LICENSE))
+ - [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/df3128d017f4/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/84569fd814a9/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
+ - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
- - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b0cb94b8:LICENSE))
+ - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.12.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
- - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go
index c11aaf3bce4f2..7d22362ae6caa 100644
--- a/logpolicy/logpolicy.go
+++ b/logpolicy/logpolicy.go
@@ -65,7 +65,8 @@ func getLogTarget() string {
getLogTargetOnce.v = val
} else {
if runtime.GOOS == "windows" {
- getLogTargetOnce.v = winutil.GetRegString("LogTarget", "")
+ logTarget, _ := winutil.GetRegString("LogTarget")
+ getLogTargetOnce.v = logTarget
}
}
})
diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go
index edcf9bbe6372a..4a0bbc7fcd87d 100644
--- a/net/dns/resolver/forwarder.go
+++ b/net/dns/resolver/forwarder.go
@@ -18,6 +18,7 @@ import (
"sort"
"strings"
"sync"
+ "sync/atomic"
"time"
dns "golang.org/x/net/dns/dnsmessage"
@@ -35,6 +36,7 @@ import (
"tailscale.com/types/nettype"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
+ "tailscale.com/util/race"
"tailscale.com/version"
)
@@ -70,6 +72,10 @@ const (
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
wellKnownHostBackupDelay = 200 * time.Millisecond
+ // udpRaceTimeout is the timeout after which we will start a DNS query
+ // over TCP while waiting for the UDP query to complete.
+ udpRaceTimeout = 2 * time.Second
+
// tcpQueryTimeout is the timeout for a DNS query performed over TCP.
// It matches the default 5sec timeout of the 'dig' utility.
tcpQueryTimeout = 5 * time.Second
@@ -488,47 +494,97 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
return nil, fmt.Errorf("tls:// resolvers not supported yet")
}
- ret, err = f.sendUDP(ctx, fq, rr)
- if err != nil {
- return nil, err
- }
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ isUDPQuery := fq.family == "udp"
+ skipTCP := skipTCPRetry() || (f.controlKnobs != nil && f.controlKnobs.DisableDNSForwarderTCPRetries.Load())
+
+ // Print logs about retries if this was because of a truncated response.
+ var explicitRetry atomic.Bool // true if truncated UDP response retried
+ defer func() {
+ if !explicitRetry.Load() {
+ return
+ }
+ if err == nil {
+ f.logf("forwarder.send(%q): successfully retried via TCP", rr.name.Addr)
+ } else {
+ f.logf("forwarder.send(%q): could not retry via TCP: %v", rr.name.Addr, err)
+ }
+ }()
+
+ firstUDP := func(ctx context.Context) ([]byte, error) {
+ resp, err := f.sendUDP(ctx, fq, rr)
+ if err != nil {
+ return nil, err
+ }
+ if !truncatedFlagSet(resp) {
+ // Successful, non-truncated response; no retry.
+ return resp, nil
+ }
- if !truncatedFlagSet(ret) {
- // Successful, non-truncated response; return it.
- return ret, nil
- }
- if fq.family == "udp" {
// If this is a UDP query, return it regardless of whether the
// response is truncated or not; the client can retry
// communicating with tailscaled over TCP. There's no point
// falling back to TCP for a truncated query if we can't return
// the results to the client.
- return ret, nil
+ if isUDPQuery {
+ return resp, nil
+ }
+
+ if skipTCP {
+ // Envknob or control knob disabled the TCP retry behaviour;
+ // just return what we have.
+ return resp, nil
+ }
+
+ // This is a TCP query from the client, and the UDP response
+ // from the upstream DNS server is truncated; map this to an
+ // error to cause our retry helper to immediately kick off the
+ // TCP retry.
+ explicitRetry.Store(true)
+ return nil, truncatedResponseError{resp}
+ }
+ thenTCP := func(ctx context.Context) ([]byte, error) {
+ // If we're skipping the TCP fallback, then wait until the
+ // context is canceled and return that error (i.e. not
+ // returning anything).
+ if skipTCP {
+ <-ctx.Done()
+ return nil, ctx.Err()
+ }
+
+ return f.sendTCP(ctx, fq, rr)
}
- if skipTCPRetry() || (f.controlKnobs != nil && f.controlKnobs.DisableDNSForwarderTCPRetries.Load()) {
- // Envknob or control knob disabled the TCP retry behaviour;
- // just return what we have.
- return ret, nil
+
+ // If the input query is TCP, then don't have a timeout between
+ // starting UDP and TCP.
+ timeout := udpRaceTimeout
+ if !isUDPQuery {
+ timeout = 0
}
- // Don't retry if our context is done.
- if err := ctx.Err(); err != nil {
- return nil, err
+ // Kick off the race between the UDP and TCP queries.
+ rh := race.New[[]byte](timeout, firstUDP, thenTCP)
+ resp, err := rh.Start(ctx)
+ if err == nil {
+ return resp, nil
}
- // Retry over TCP, best-effort; return the truncated UDP response if we
- // cannot query via TCP.
- if ret2, err2 := f.sendTCP(ctx, fq, rr); err2 == nil {
- if verboseDNSForward() {
- f.logf("forwarder.send(%q): successfully retried via TCP", rr.name.Addr)
- }
- return ret2, nil
- } else if verboseDNSForward() {
- f.logf("forwarder.send(%q): could not retry via TCP: %v", rr.name.Addr, err2)
+ // If we got a truncated UDP response, return that instead of an error.
+ var trErr truncatedResponseError
+ if errors.As(err, &trErr) {
+ return trErr.res, nil
}
- return ret, nil
+ return nil, err
}
+type truncatedResponseError struct {
+ res []byte
+}
+
+func (tr truncatedResponseError) Error() string { return "response truncated" }
+
var errServerFailure = errors.New("response code indicates server issue")
var errTxIDMismatch = errors.New("txid doesn't match")
@@ -875,7 +931,7 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
numErr++
if numErr == len(resolvers) {
- if firstErr == errServerFailure {
+ if errors.Is(firstErr, errServerFailure) {
res, err := servfailResponse(query)
if err != nil {
f.logf("building servfail response: %v", err)
diff --git a/net/dns/resolver/forwarder_test.go b/net/dns/resolver/forwarder_test.go
index b78e26c959abd..9ac64a6bb69de 100644
--- a/net/dns/resolver/forwarder_test.go
+++ b/net/dns/resolver/forwarder_test.go
@@ -7,6 +7,7 @@ import (
"bytes"
"context"
"encoding/binary"
+ "errors"
"flag"
"fmt"
"io"
@@ -21,6 +22,7 @@ import (
"time"
dns "golang.org/x/net/dns/dnsmessage"
+ "tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
@@ -253,7 +255,16 @@ func FuzzClampEDNSSize(f *testing.F) {
})
}
-func runDNSServer(tb testing.TB, response []byte, onRequest func(bool, []byte)) (port uint16) {
+type testDNSServerOptions struct {
+ SkipUDP bool
+ SkipTCP bool
+}
+
+func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, onRequest func(bool, []byte)) (port uint16) {
+ if opts != nil && opts.SkipUDP && opts.SkipTCP {
+ tb.Fatal("cannot skip both UDP and TCP servers")
+ }
+
tcpResponse := make([]byte, len(response)+2)
binary.BigEndian.PutUint16(tcpResponse, uint16(len(response)))
copy(tcpResponse[2:], response)
@@ -327,17 +338,20 @@ func runDNSServer(tb testing.TB, response []byte, onRequest func(bool, []byte))
}
var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- for {
- conn, err := tcpLn.Accept()
- if err != nil {
- return
+
+ if opts == nil || !opts.SkipTCP {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for {
+ conn, err := tcpLn.Accept()
+ if err != nil {
+ return
+ }
+ go handleConn(conn)
}
- go handleConn(conn)
- }
- }()
+ }()
+ }
handleUDP := func(addr netip.AddrPort, req []byte) {
onRequest(false, req)
@@ -346,19 +360,21 @@ func runDNSServer(tb testing.TB, response []byte, onRequest func(bool, []byte))
}
}
- wg.Add(1)
- go func() {
- defer wg.Done()
- for {
- buf := make([]byte, 65535)
- n, addr, err := udpLn.ReadFromUDPAddrPort(buf)
- if err != nil {
- return
+ if opts == nil || !opts.SkipUDP {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for {
+ buf := make([]byte, 65535)
+ n, addr, err := udpLn.ReadFromUDPAddrPort(buf)
+ if err != nil {
+ return
+ }
+ buf = buf[:n]
+ go handleUDP(addr, buf)
}
- buf = buf[:n]
- go handleUDP(addr, buf)
- }
- }()
+ }()
+ }
tb.Cleanup(func() {
tcpLn.Close()
@@ -369,89 +385,78 @@ func runDNSServer(tb testing.TB, response []byte, onRequest func(bool, []byte))
return
}
-func TestForwarderTCPFallback(t *testing.T) {
+func enableDebug(tb testing.TB) {
const debugKnob = "TS_DEBUG_DNS_FORWARD_SEND"
oldVal := os.Getenv(debugKnob)
envknob.Setenv(debugKnob, "true")
- t.Cleanup(func() { envknob.Setenv(debugKnob, oldVal) })
-
- const domain = "large-dns-response.tailscale.com."
+ tb.Cleanup(func() { envknob.Setenv(debugKnob, oldVal) })
+}
- // Make a response that's very large, containing a bunch of localhost addresses.
- largeResponse := func() []byte {
- name := dns.MustNewName(domain)
+func makeLargeResponse(tb testing.TB, domain string) (request, response []byte) {
+ name := dns.MustNewName(domain)
- builder := dns.NewBuilder(nil, dns.Header{})
- builder.StartQuestions()
- builder.Question(dns.Question{
+ builder := dns.NewBuilder(nil, dns.Header{})
+ builder.StartQuestions()
+ builder.Question(dns.Question{
+ Name: name,
+ Type: dns.TypeA,
+ Class: dns.ClassINET,
+ })
+ builder.StartAnswers()
+ for i := 0; i < 120; i++ {
+ builder.AResource(dns.ResourceHeader{
Name: name,
- Type: dns.TypeA,
Class: dns.ClassINET,
+ TTL: 300,
+ }, dns.AResource{
+ A: [4]byte{127, 0, 0, byte(i)},
})
- builder.StartAnswers()
- for i := 0; i < 120; i++ {
- builder.AResource(dns.ResourceHeader{
- Name: name,
- Class: dns.ClassINET,
- TTL: 300,
- }, dns.AResource{
- A: [4]byte{127, 0, 0, byte(i)},
- })
- }
+ }
- msg, err := builder.Finish()
- if err != nil {
- t.Fatal(err)
- }
- return msg
- }()
- if len(largeResponse) <= maxResponseBytes {
- t.Fatalf("got len(largeResponse)=%d, want > %d", len(largeResponse), maxResponseBytes)
+ var err error
+ response, err = builder.Finish()
+ if err != nil {
+ tb.Fatal(err)
+ }
+ if len(response) <= maxResponseBytes {
+ tb.Fatalf("got len(largeResponse)=%d, want > %d", len(response), maxResponseBytes)
}
// Our request is a single A query for the domain in the answer, above.
- request := func() []byte {
- builder := dns.NewBuilder(nil, dns.Header{})
- builder.StartQuestions()
- builder.Question(dns.Question{
- Name: dns.MustNewName(domain),
- Type: dns.TypeA,
- Class: dns.ClassINET,
- })
- msg, err := builder.Finish()
- if err != nil {
- t.Fatal(err)
- }
- return msg
- }()
-
- var sawUDPRequest, sawTCPRequest atomic.Bool
- port := runDNSServer(t, largeResponse, func(isTCP bool, gotRequest []byte) {
- if isTCP {
- sawTCPRequest.Store(true)
- } else {
- sawUDPRequest.Store(true)
- }
-
- if !bytes.Equal(request, gotRequest) {
- t.Errorf("invalid request\ngot: %+v\nwant: %+v", gotRequest, request)
- }
+ builder = dns.NewBuilder(nil, dns.Header{})
+ builder.StartQuestions()
+ builder.Question(dns.Question{
+ Name: dns.MustNewName(domain),
+ Type: dns.TypeA,
+ Class: dns.ClassINET,
})
+ request, err = builder.Finish()
+ if err != nil {
+ tb.Fatal(err)
+ }
+
+ return
+}
- netMon, err := netmon.New(t.Logf)
+func runTestQuery(tb testing.TB, port uint16, request []byte, modify func(*forwarder)) ([]byte, error) {
+ netMon, err := netmon.New(tb.Logf)
if err != nil {
- t.Fatal(err)
+ tb.Fatal(err)
}
var dialer tsdial.Dialer
dialer.SetNetMon(netMon)
- fwd := newForwarder(t.Logf, netMon, nil, &dialer, nil)
+ fwd := newForwarder(tb.Logf, netMon, nil, &dialer, nil)
+ if modify != nil {
+ modify(fwd)
+ }
fq := &forwardQuery{
txid: getTxID(request),
packet: request,
closeOnCtxDone: new(closePool),
+ family: "tcp",
}
defer fq.closeOnCtxDone.Close()
@@ -459,17 +464,186 @@ func TestForwarderTCPFallback(t *testing.T) {
name: &dnstype.Resolver{Addr: fmt.Sprintf("127.0.0.1:%d", port)},
}
- resp, err := fwd.send(context.Background(), fq, rr)
+ return fwd.send(context.Background(), fq, rr)
+}
+
+func mustRunTestQuery(tb testing.TB, port uint16, request []byte, modify func(*forwarder)) []byte {
+ resp, err := runTestQuery(tb, port, request, modify)
if err != nil {
- t.Fatalf("error making request: %v", err)
+ tb.Fatalf("error making request: %v", err)
+ }
+ return resp
+}
+
+func TestForwarderTCPFallback(t *testing.T) {
+ enableDebug(t)
+
+ const domain = "large-dns-response.tailscale.com."
+
+ // Make a response that's very large, containing a bunch of localhost addresses.
+ request, largeResponse := makeLargeResponse(t, domain)
+
+ var sawTCPRequest atomic.Bool
+ port := runDNSServer(t, nil, largeResponse, func(isTCP bool, gotRequest []byte) {
+ if isTCP {
+ t.Logf("saw TCP request")
+ sawTCPRequest.Store(true)
+ } else {
+ t.Logf("saw UDP request")
+ }
+
+ if !bytes.Equal(request, gotRequest) {
+ t.Errorf("invalid request\ngot: %+v\nwant: %+v", gotRequest, request)
+ }
+ })
+
+ resp := mustRunTestQuery(t, port, request, nil)
+ if !bytes.Equal(resp, largeResponse) {
+ t.Errorf("invalid response\ngot: %+v\nwant: %+v", resp, largeResponse)
+ }
+ if !sawTCPRequest.Load() {
+ t.Errorf("DNS server never saw TCP request")
}
+
+ // NOTE: can't assert that we see a UDP request here since we might
+ // race and run the TCP query first. We test the UDP codepath in
+ // TestForwarderTCPFallbackDisabled below, though.
+}
+
+// Test to ensure that if the UDP listener is unresponsive, we always make a
+// TCP request even if we never get a response.
+func TestForwarderTCPFallbackTimeout(t *testing.T) {
+ enableDebug(t)
+
+ const domain = "large-dns-response.tailscale.com."
+
+ // Make a response that's very large, containing a bunch of localhost addresses.
+ request, largeResponse := makeLargeResponse(t, domain)
+
+ var sawTCPRequest atomic.Bool
+ opts := &testDNSServerOptions{SkipUDP: true}
+ port := runDNSServer(t, opts, largeResponse, func(isTCP bool, gotRequest []byte) {
+ if isTCP {
+ t.Logf("saw TCP request")
+ sawTCPRequest.Store(true)
+ } else {
+ t.Error("saw unexpected UDP request")
+ }
+
+ if !bytes.Equal(request, gotRequest) {
+ t.Errorf("invalid request\ngot: %+v\nwant: %+v", gotRequest, request)
+ }
+ })
+
+ resp := mustRunTestQuery(t, port, request, nil)
if !bytes.Equal(resp, largeResponse) {
t.Errorf("invalid response\ngot: %+v\nwant: %+v", resp, largeResponse)
}
if !sawTCPRequest.Load() {
t.Errorf("DNS server never saw TCP request")
}
+}
+
+func TestForwarderTCPFallbackDisabled(t *testing.T) {
+ enableDebug(t)
+
+ const domain = "large-dns-response.tailscale.com."
+
+ // Make a response that's very large, containing a bunch of localhost addresses.
+ request, largeResponse := makeLargeResponse(t, domain)
+
+ var sawUDPRequest atomic.Bool
+ port := runDNSServer(t, nil, largeResponse, func(isTCP bool, gotRequest []byte) {
+ if isTCP {
+ t.Error("saw unexpected TCP request")
+ } else {
+ t.Logf("saw UDP request")
+ sawUDPRequest.Store(true)
+ }
+
+ if !bytes.Equal(request, gotRequest) {
+ t.Errorf("invalid request\ngot: %+v\nwant: %+v", gotRequest, request)
+ }
+ })
+
+ resp := mustRunTestQuery(t, port, request, func(fwd *forwarder) {
+ // Disable retries for this test.
+ fwd.controlKnobs = &controlknobs.Knobs{}
+ fwd.controlKnobs.DisableDNSForwarderTCPRetries.Store(true)
+ })
+
+ wantResp := append([]byte(nil), largeResponse[:maxResponseBytes]...)
+
+ // Set the truncated flag on the expected response, since that's what we expect.
+ flags := binary.BigEndian.Uint16(wantResp[2:4])
+ flags |= dnsFlagTruncated
+ binary.BigEndian.PutUint16(wantResp[2:4], flags)
+
+ if !bytes.Equal(resp, wantResp) {
+ t.Errorf("invalid response\ngot (%d): %+v\nwant (%d): %+v", len(resp), resp, len(wantResp), wantResp)
+ }
if !sawUDPRequest.Load() {
t.Errorf("DNS server never saw UDP request")
}
}
+
+// Test to ensure that we propagate DNS errors
+func TestForwarderTCPFallbackError(t *testing.T) {
+ enableDebug(t)
+
+ const domain = "error-response.tailscale.com."
+
+ // Our response is a SERVFAIL
+ response := func() []byte {
+ name := dns.MustNewName(domain)
+
+ builder := dns.NewBuilder(nil, dns.Header{
+ RCode: dns.RCodeServerFailure,
+ })
+ builder.StartQuestions()
+ builder.Question(dns.Question{
+ Name: name,
+ Type: dns.TypeA,
+ Class: dns.ClassINET,
+ })
+ response, err := builder.Finish()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return response
+ }()
+
+ // Our request is a single A query for the domain in the answer, above.
+ request := func() []byte {
+ builder := dns.NewBuilder(nil, dns.Header{})
+ builder.StartQuestions()
+ builder.Question(dns.Question{
+ Name: dns.MustNewName(domain),
+ Type: dns.TypeA,
+ Class: dns.ClassINET,
+ })
+ request, err := builder.Finish()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return request
+ }()
+
+ var sawRequest atomic.Bool
+ port := runDNSServer(t, nil, response, func(isTCP bool, gotRequest []byte) {
+ sawRequest.Store(true)
+ if !bytes.Equal(request, gotRequest) {
+ t.Errorf("invalid request\ngot: %+v\nwant: %+v", gotRequest, request)
+ }
+ })
+
+ _, err := runTestQuery(t, port, request, nil)
+ if !sawRequest.Load() {
+ t.Error("did not see DNS request")
+ }
+ if err == nil {
+ t.Error("wanted error, got nil")
+ } else if !errors.Is(err, errServerFailure) {
+ t.Errorf("wanted errServerFailure, got: %v", err)
+ }
+}
diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go
index 7c2af5b165d9f..ddb7b6cdba713 100644
--- a/net/dns/resolver/tsdns.go
+++ b/net/dns/resolver/tsdns.go
@@ -350,7 +350,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
// but for now that's probably good enough. Later we'll
// want to blend in everything from scutil --dns.
fallthrough
- case "linux", "freebsd", "openbsd", "illumos":
+ case "linux", "freebsd", "openbsd", "illumos", "ios":
nameserver, err := stubResolverForOS()
if err != nil {
r.logf("stubResolverForOS: %v", err)
@@ -360,12 +360,15 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
// TODO: more than 1 resolver from /etc/resolv.conf?
var resolvers []resolverAndDelay
- if nameserver == tsaddr.TailscaleServiceIP() || nameserver == tsaddr.TailscaleServiceIPv6() {
+ switch nameserver {
+ case tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6():
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
// so avoid the loop through the kernel and just do what we
// would've done anyway. By not passing any resolvers, the forwarder
// will use its default ones from our DNS config.
- } else {
+ case netip.Addr{}:
+ // Likewise, if the platform has no resolv.conf, just use our defaults.
+ default:
resolvers = []resolverAndDelay{{
name: &dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
}}
@@ -392,7 +395,7 @@ var debugExitNodeDNSNetPkg = envknob.RegisterBool("TS_DEBUG_EXIT_NODE_DNS_NET_PK
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
// return a reply (for the ExitDNS DoH service) using the net package's
-// native APIs. This is only used on Windows for now.
+// native APIs.
//
// If resolver is nil, the net.Resolver zero value is used.
//
@@ -531,7 +534,13 @@ var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
// stubResolverForOS returns the IP address of the first nameserver in
// /etc/resolv.conf.
+//
+// It may also return the netip.Addr zero value and a nil error to mean
+// that the platform has no resolv.conf.
func stubResolverForOS() (ip netip.Addr, err error) {
+ if runtime.GOOS == "ios" {
+ return netip.Addr{}, nil // no resolv.conf on iOS
+ }
fi, err := os.Stat(resolvconffile.Path)
if err != nil {
return netip.Addr{}, err
diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go
index c135c92a14941..8824620126e0f 100644
--- a/net/dns/resolver/tsdns_test.go
+++ b/net/dns/resolver/tsdns_test.go
@@ -1449,7 +1449,7 @@ func TestServfail(t *testing.T) {
r.SetConfig(cfg)
pkt, err := syncRespond(r, dnspacket("test.site.", dns.TypeA, noEdns))
- if err != errServerFailure {
+ if !errors.Is(err, errServerFailure) {
t.Errorf("err = %v, want %v", err, errServerFailure)
}
diff --git a/net/dnsfallback/dnsfallback.go b/net/dnsfallback/dnsfallback.go
index de58fa38cc447..d60404ddc66f9 100644
--- a/net/dnsfallback/dnsfallback.go
+++ b/net/dnsfallback/dnsfallback.go
@@ -1,10 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
-//go:generate go run update-dns-fallbacks.go
-
// Package dnsfallback contains a DNS fallback mechanism
// for starting up Tailscale when the system DNS is broken or otherwise unavailable.
+//
+// The data is backed by a JSON file `dns-fallback-servers.json` that is updated
+// by `update-dns-fallbacks.go`:
+//
+// (cd net/dnsfallback; go run update-dns-fallbacks.go)
package dnsfallback
import (
@@ -23,7 +26,6 @@ import (
"sync/atomic"
"time"
- "go4.org/netipx"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/net/dns/recursive"
@@ -77,11 +79,13 @@ func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.C
metricRecursiveErrors.Add(1)
return
}
- slices.SortFunc(addrs, netipx.CompareAddr)
+
+ compareAddr := func(a, b netip.Addr) int { return a.Compare(b) }
+ slices.SortFunc(addrs, compareAddr)
// Wait for a response from the main function
oldAddrs := <-addrsCh
- slices.SortFunc(oldAddrs, netipx.CompareAddr)
+ slices.SortFunc(oldAddrs, compareAddr)
matches := slices.Equal(addrs, oldAddrs)
diff --git a/net/packet/packet.go b/net/packet/packet.go
index e3784a571dbf8..9760005ec572e 100644
--- a/net/packet/packet.go
+++ b/net/packet/packet.go
@@ -10,6 +10,8 @@ import (
"net/netip"
"strings"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/net/netaddr"
"tailscale.com/types/ipproto"
)
@@ -453,11 +455,14 @@ func (q *Parsed) IsEchoResponse() bool {
}
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
-// SNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
-// over IPv4 is supported. It panics if called with IPv6 addr.
+// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
+// is supported. It panics if provided with an address in a different
+// family to the parsed packet.
func (q *Parsed) UpdateSrcAddr(src netip.Addr) {
- if q.IPVersion != 4 || src.Is6() {
- panic("UpdateSrcAddr: only IPv4 is supported")
+ if src.Is6() && q.IPVersion != 6 {
+ panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
+ } else if src.Is4() && q.IPVersion != 4 {
+ panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
}
q.CaptureMeta.DidSNAT = true
q.CaptureMeta.OriginalSrc = q.Src
@@ -466,19 +471,27 @@ func (q *Parsed) UpdateSrcAddr(src netip.Addr) {
q.Src = netip.AddrPortFrom(src, q.Src.Port())
b := q.Buffer()
- v4 := src.As4()
- copy(b[12:16], v4[:])
- updateV4PacketChecksums(q, old, src)
+ if src.Is6() {
+ v6 := src.As16()
+ copy(b[8:24], v6[:])
+ updateV6PacketChecksums(q, old, src)
+ } else {
+ v4 := src.As4()
+ copy(b[12:16], v4[:])
+ updateV4PacketChecksums(q, old, src)
+ }
}
-// UpdateDstAddr updates the source address in the packet buffer (e.g. during
+// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
-// over IPv4 is supported. It panics if called with IPv6 addr.
+// is supported. It panics if provided with an address in a different
+// family to the parsed packet.
func (q *Parsed) UpdateDstAddr(dst netip.Addr) {
- if q.IPVersion != 4 || dst.Is6() {
- panic("UpdateDstAddr: only IPv4 is supported")
+ if dst.Is6() && q.IPVersion != 6 {
+ panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
+ } else if dst.Is4() && q.IPVersion != 4 {
+ panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
}
-
q.CaptureMeta.DidDNAT = true
q.CaptureMeta.OriginalDst = q.Dst
@@ -486,9 +499,15 @@ func (q *Parsed) UpdateDstAddr(dst netip.Addr) {
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
b := q.Buffer()
- v4 := dst.As4()
- copy(b[16:20], v4[:])
- updateV4PacketChecksums(q, old, dst)
+ if dst.Is6() {
+ v6 := dst.As16()
+ copy(b[24:36], v6[:])
+ updateV6PacketChecksums(q, old, dst)
+ } else {
+ v4 := dst.As4()
+ copy(b[16:20], v4[:])
+ updateV4PacketChecksums(q, old, dst)
+ }
}
// EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response,
@@ -572,13 +591,13 @@ func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) {
tr := p.Transport()
switch p.IPProto {
case ipproto.UDP, ipproto.DCCP:
- if len(tr) < 8 {
+ if len(tr) < header.UDPMinimumSize {
// Not enough space for a UDP header.
return
}
updateV4Checksum(tr[6:8], o4[:], n4[:])
case ipproto.TCP:
- if len(tr) < 18 {
+ if len(tr) < header.TCPMinimumSize {
// Not enough space for a TCP header.
return
}
@@ -596,6 +615,39 @@ func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) {
}
}
+// updateV6PacketChecksums updates the checksums in the packet buffer.
+// p is modified in place.
+// If p.IPProto is unknown, no checksums are updated.
+func updateV6PacketChecksums(p *Parsed, old, new netip.Addr) {
+ if len(p.Buffer()) < 40 {
+ // Not enough space for an IPv6 header.
+ return
+ }
+ o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
+
+ // Now update the transport layer checksums, where applicable.
+ tr := p.Transport()
+ switch p.IPProto {
+ case ipproto.ICMPv6:
+ if len(tr) < header.ICMPv6MinimumSize {
+ return
+ }
+ header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
+ case ipproto.UDP, ipproto.DCCP:
+ if len(tr) < header.UDPMinimumSize {
+ return
+ }
+ header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
+ case ipproto.TCP:
+ if len(tr) < header.TCPMinimumSize {
+ return
+ }
+ header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
+ case ipproto.SCTP:
+ // No transport layer update required.
+ }
+}
+
// updateV4Checksum calculates and updates the checksum in the packet buffer for
// a change between old and new. The oldSum must point to the 16-bit checksum
// field in the packet buffer that holds the old checksum value, it will be
diff --git a/net/packet/packet_test.go b/net/packet/packet_test.go
index 4bc2be77acdad..553fd11f43281 100644
--- a/net/packet/packet_test.go
+++ b/net/packet/packet_test.go
@@ -13,6 +13,9 @@ import (
"testing"
"unicode"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/checksum"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/tstest"
"tailscale.com/types/ipproto"
"tailscale.com/util/must"
@@ -45,7 +48,7 @@ func fullHeaderChecksumV4(b []byte) uint16 {
return ^uint16(s)
}
-func TestHeaderChecksums(t *testing.T) {
+func TestHeaderChecksumsV4(t *testing.T) {
// This is not a good enough test, because it doesn't
// check the various packet types or the many edge cases
// of the checksum algorithm. But it's a start.
@@ -109,6 +112,108 @@ func TestHeaderChecksums(t *testing.T) {
}
}
+func TestNatChecksumsV6UDP(t *testing.T) {
+ a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
+
+ // Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
+ b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
+ b.Encode(&header.IPv6Fields{
+ PayloadLength: header.UDPMinimumSize + 32,
+ TransportProtocol: header.UDPProtocolNumber,
+ HopLimit: 16,
+ SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
+ DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
+ })
+ udp := header.UDP(b[header.IPv6MinimumSize:])
+ udp.Encode(&header.UDPFields{
+ SrcPort: 42,
+ DstPort: 43,
+ Length: header.UDPMinimumSize + 32,
+ })
+ xsum := header.PseudoHeaderChecksum(
+ header.UDPProtocolNumber,
+ tcpip.AddrFrom16Slice(a1.AsSlice()),
+ tcpip.AddrFrom16Slice(a2.AsSlice()),
+ uint16(header.UDPMinimumSize+32),
+ )
+ xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
+ udp.SetChecksum(^udp.CalculateChecksum(xsum))
+ if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
+ t.Fatal("test broken; initial packet has incorrect checksum")
+ }
+
+ // Parse the packet.
+ var p Parsed
+ p.Decode(b)
+ t.Log(p.String())
+
+ // Update the source address of the packet to be the same as the dest.
+ p.UpdateSrcAddr(a2)
+ if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
+ t.Fatal("incorrect checksum after updating source address")
+ }
+
+ // Update the dest address of the packet to be the original source address.
+ p.UpdateDstAddr(a1)
+ if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
+ t.Fatal("incorrect checksum after updating destination address")
+ }
+}
+
+func TestNatChecksumsV6TCP(t *testing.T) {
+ a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
+
+ // Make a fake TCP packet with no payload.
+ b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
+ b.Encode(&header.IPv6Fields{
+ PayloadLength: header.TCPMinimumSize,
+ TransportProtocol: header.TCPProtocolNumber,
+ HopLimit: 16,
+ SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
+ DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
+ })
+ tcp := header.TCP(b[header.IPv6MinimumSize:])
+ tcp.Encode(&header.TCPFields{
+ SrcPort: 42,
+ DstPort: 43,
+ SeqNum: 1,
+ AckNum: 2,
+ DataOffset: header.TCPMinimumSize,
+ Flags: 3,
+ WindowSize: 4,
+ Checksum: 0,
+ UrgentPointer: 5,
+ })
+ xsum := header.PseudoHeaderChecksum(
+ header.TCPProtocolNumber,
+ tcpip.AddrFrom16Slice(a1.AsSlice()),
+ tcpip.AddrFrom16Slice(a2.AsSlice()),
+ uint16(header.TCPMinimumSize),
+ )
+ tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
+
+ if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
+ t.Fatal("test broken; initial packet has incorrect checksum")
+ }
+
+ // Parse the packet.
+ var p Parsed
+ p.Decode(b)
+ t.Log(p.String())
+
+ // Update the source address of the packet to be the same as the dest.
+ p.UpdateSrcAddr(a2)
+ if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
+ t.Fatal("incorrect checksum after updating source address")
+ }
+
+ // Update the dest address of the packet to be the original source address.
+ p.UpdateDstAddr(a1)
+ if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
+ t.Fatal("incorrect checksum after updating destination address")
+ }
+}
+
func mustIPPort(s string) netip.AddrPort {
ipp, err := netip.ParseAddrPort(s)
if err != nil {
diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go
index 880d9d486f45a..3fde487cd0f52 100644
--- a/net/portmapper/portmapper.go
+++ b/net/portmapper/portmapper.go
@@ -1040,7 +1040,16 @@ func getUPnPErrorsMetric(code int) *clientmetric.Metric {
return mm
}
- mm = clientmetric.NewCounter(fmt.Sprintf("portmap_upnp_errors_with_code_%d", code))
+ // Metric names cannot contain a hyphen, so we handle negative numbers
+ // by prefixing the name with a "minus_".
+ var codeStr string
+ if code < 0 {
+ codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
+ } else {
+ codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
+ }
+
+ mm = clientmetric.NewCounter(codeStr)
mak.Set(&metricUPnPErrorsByCode, code, mm)
return mm
}
diff --git a/net/portmapper/portmapper_test.go b/net/portmapper/portmapper_test.go
index 83a2afe669cb5..575ddd6f1086d 100644
--- a/net/portmapper/portmapper_test.go
+++ b/net/portmapper/portmapper_test.go
@@ -124,3 +124,14 @@ func TestPCPIntegration(t *testing.T) {
t.Errorf("got nil mapping after successful createOrGetMapping")
}
}
+
+// Test to ensure that metric names generated by this function do not contain
+// invalid characters.
+//
+// See https://github.com/tailscale/tailscale/issues/9551
+func TestGetUPnPErrorsMetric(t *testing.T) {
+ // This will panic if the metric name is invalid.
+ getUPnPErrorsMetric(100)
+ getUPnPErrorsMetric(0)
+ getUPnPErrorsMetric(-100)
+}
diff --git a/net/tstun/mtu.go b/net/tstun/mtu.go
index 9af548798b2b8..004529c205f9e 100644
--- a/net/tstun/mtu.go
+++ b/net/tstun/mtu.go
@@ -79,14 +79,16 @@ const (
safeTUNMTU TUNMTU = 1280
)
-// MaxProbedWireMTU is the largest MTU we will test for path MTU
-// discovery.
-var MaxProbedWireMTU WireMTU = 9000
-
-func init() {
- if MaxProbedWireMTU > WireMTU(maxTUNMTU) {
- MaxProbedWireMTU = WireMTU(maxTUNMTU)
- }
+// WireMTUsToProbe is a list of the on-the-wire MTUs we want to probe. Each time
+// magicsock discovery begins, it will send a set of pings, one of each size
+// listed below.
+var WireMTUsToProbe = []WireMTU{
+ WireMTU(safeTUNMTU), // Tailscale over Tailscale :)
+ TUNToWireMTU(safeTUNMTU), // Smallest MTU allowed for IPv6, current default
+ 1400, // Most common MTU minus a few bytes for tunnels
+ 1500, // Most common MTU
+ 8000, // Should fit inside all jumbo frame sizes
+ 9000, // Most jumbo frames are this size or larger
}
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
@@ -125,7 +127,7 @@ func WireToTUNMTU(w WireMTU) TUNMTU {
// MTU. It is also the path MTU that we default to if we have no
// information about the path to a peer.
//
-// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTunMTU
+// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTUNMTU
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg overhead
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
func DefaultTUNMTU() TUNMTU {
@@ -135,16 +137,21 @@ func DefaultTUNMTU() TUNMTU {
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
if debugPMTUD {
- return WireToTUNMTU(MaxProbedWireMTU)
+ // TODO: While we are just probing MTU but not generating PTB,
+ // this has to continue to return the safe MTU. When we add the
+ // code to generate PTB, this will be:
+ //
+ // return WireToTUNMTU(maxProbedWireMTU)
+ return safeTUNMTU
}
return safeTUNMTU
}
-// Temporary workaround for code on corp that uses this function name.
-// TODO(val): Remove as soon as corp OSS is updated.
-func DefaultMTU() uint32 {
- return uint32(DefaultTUNMTU())
+// SafeWireMTU returns the wire MTU that is safe to use if we have no
+// information about the path MTU to this peer.
+func SafeWireMTU() WireMTU {
+ return TUNToWireMTU(safeTUNMTU)
}
// DefaultWireMTU returns the default TUN MTU, adjusted for wireguard
diff --git a/net/tstun/mtu_test.go b/net/tstun/mtu_test.go
index 3708a91f4ca20..8d165bfd341a9 100644
--- a/net/tstun/mtu_test.go
+++ b/net/tstun/mtu_test.go
@@ -39,15 +39,18 @@ func TestDefaultTunMTU(t *testing.T) {
t.Errorf("default TUN MTU = %d, want %d, clamping failed", DefaultTUNMTU(), maxTUNMTU)
}
- // If PMTUD is enabled, the MTU should default to the largest probed
- // MTU, but only if the user hasn't requested a specific MTU.
+ // If PMTUD is enabled, the MTU should default to the safe MTU, but only
+ // if the user hasn't requested a specific MTU.
+ //
+ // TODO: When PMTUD is generating PTB responses, this will become the
+ // largest MTU we probe.
os.Setenv("TS_DEBUG_MTU", "")
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
- if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
- t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
+ if DefaultTUNMTU() != safeTUNMTU {
+ t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
}
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
- mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
+ mtu = WireToTUNMTU(MaxPacketSize - 1)
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
if DefaultTUNMTU() != mtu {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)
diff --git a/net/tstun/tap_linux.go b/net/tstun/tap_linux.go
index 7dcb5ee5d8ec7..c721e6e2734b5 100644
--- a/net/tstun/tap_linux.go
+++ b/net/tstun/tap_linux.go
@@ -296,9 +296,9 @@ func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst net
payloadStart := len(buf) - len(payload)
copy(buf[payloadStart:], payload)
srcB := src.Addr().As4()
- srcIP := tcpip.Address(srcB[:])
+ srcIP := tcpip.AddrFromSlice(srcB[:])
dstB := dst.Addr().As4()
- dstIP := tcpip.Address(dstB[:])
+ dstIP := tcpip.AddrFromSlice(dstB[:])
// Ethernet header
eth := header.Ethernet(buf)
eth.Encode(&header.EthernetFields{
diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go
index ab0a4a3db9b7e..2caa241886636 100644
--- a/net/tstun/wrap.go
+++ b/net/tstun/wrap.go
@@ -98,8 +98,8 @@ type Wrapper struct {
// timeNow, if non-nil, will be used to obtain the current time.
timeNow func() time.Time
- // natV4Config stores the current IPv4 NAT configuration.
- natV4Config atomic.Pointer[natV4Config]
+ // natConfig stores the current NAT configuration.
+ natConfig atomic.Pointer[natConfig]
// vectorBuffer stores the oldest unconsumed packet vector from tdev. It is
// allocated in wrap() and the underlying arrays should never grow.
@@ -481,14 +481,9 @@ func (t *Wrapper) sendVectorOutbound(r tunVectorReadResult) {
t.vectorOutbound <- r
}
-// snatV4 does SNAT on p if it's an IPv4 packet and the destination
-// address requires a different source address.
-func (t *Wrapper) snatV4(p *packet.Parsed) {
- if p.IPVersion != 4 {
- return
- }
-
- nc := t.natV4Config.Load()
+// snat does SNAT on p if the destination address requires a different source address.
+func (t *Wrapper) snat(p *packet.Parsed) {
+ nc := t.natConfig.Load()
oldSrc := p.Src.Addr()
newSrc := nc.selectSrcIP(oldSrc, p.Dst.Addr())
if oldSrc != newSrc {
@@ -496,13 +491,9 @@ func (t *Wrapper) snatV4(p *packet.Parsed) {
}
}
-// dnatV4 does destination NAT on p if it's an IPv4 packet.
-func (t *Wrapper) dnatV4(p *packet.Parsed) {
- if p.IPVersion != 4 {
- return
- }
-
- nc := t.natV4Config.Load()
+// dnat does destination NAT on p.
+func (t *Wrapper) dnat(p *packet.Parsed) {
+ nc := t.natConfig.Load()
oldDst := p.Dst.Addr()
newDst := nc.mapDstIP(oldDst)
if newDst != oldDst {
@@ -521,15 +512,79 @@ func findV4(addrs []netip.Prefix) netip.Addr {
return netip.Addr{}
}
-// natV4Config is the configuration for IPv4 NAT.
+// findV6 returns the first Tailscale IPv6 address in addrs.
+func findV6(addrs []netip.Prefix) netip.Addr {
+ for _, ap := range addrs {
+ a := ap.Addr()
+ if a.Is6() && tsaddr.IsTailscaleIP(a) {
+ return a
+ }
+ }
+ return netip.Addr{}
+}
+
+// natConfig is the configuration for NAT.
+// It should be treated as immutable.
+//
+// The nil value is a valid configuration.
+type natConfig struct {
+ v4, v6 *natFamilyConfig
+}
+
+func (c *natConfig) String() string {
+ if c == nil {
+ return ""
+ }
+
+ var b strings.Builder
+ b.WriteString("natConfig{")
+ fmt.Fprintf(&b, "v4: %v, ", c.v4)
+ fmt.Fprintf(&b, "v6: %v", c.v6)
+ b.WriteString("}")
+ return b.String()
+}
+
+// mapDstIP returns the destination IP to use for a packet to dst.
+// If dst is not one of the listen addresses, it is returned as-is,
+// otherwise the native address is returned.
+func (c *natConfig) mapDstIP(oldDst netip.Addr) netip.Addr {
+ if c == nil {
+ return oldDst
+ }
+ if oldDst.Is4() {
+ return c.v4.mapDstIP(oldDst)
+ }
+ if oldDst.Is6() {
+ return c.v6.mapDstIP(oldDst)
+ }
+ return oldDst
+}
+
+// selectSrcIP returns the source IP to use for a packet to dst.
+// If the packet is not from the native address, it is returned as-is.
+func (c *natConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
+ if c == nil {
+ return oldSrc
+ }
+ if oldSrc.Is4() {
+ return c.v4.selectSrcIP(oldSrc, dst)
+ }
+ if oldSrc.Is6() {
+ return c.v6.selectSrcIP(oldSrc, dst)
+ }
+ return oldSrc
+}
+
+// natFamilyConfig is the NAT configuration for a particular
+// address family.
// It should be treated as immutable.
//
// The nil value is a valid configuration.
-type natV4Config struct {
- // nativeAddr is the IPv4 Tailscale Address of the current node.
+type natFamilyConfig struct {
+ // nativeAddr is the Tailscale Address of the current node.
nativeAddr netip.Addr
- // listenAddrs is the set of IPv4 addresses that should be
+ // listenAddrs is the set of addresses that should be
// mapped to the native address. These are the addresses that
// peers will use to connect to this node.
listenAddrs views.Map[netip.Addr, struct{}] // masqAddr -> struct{}
@@ -545,12 +600,12 @@ type natV4Config struct {
dstAddrToPeerKeyMapper *table.RoutingTable
}
-func (c *natV4Config) String() string {
+func (c *natFamilyConfig) String() string {
if c == nil {
- return ""
+ return "natFamilyConfig(nil)"
}
var b strings.Builder
- b.WriteString("natV4Config{")
+ b.WriteString("natFamilyConfig{")
fmt.Fprintf(&b, "nativeAddr: %v, ", c.nativeAddr)
fmt.Fprint(&b, "listenAddrs: [")
@@ -586,7 +641,7 @@ func (c *natV4Config) String() string {
// mapDstIP returns the destination IP to use for a packet to dst.
// If dst is not one of the listen addresses, it is returned as-is,
// otherwise the native address is returned.
-func (c *natV4Config) mapDstIP(oldDst netip.Addr) netip.Addr {
+func (c *natFamilyConfig) mapDstIP(oldDst netip.Addr) netip.Addr {
if c == nil {
return oldDst
}
@@ -598,7 +653,7 @@ func (c *natV4Config) mapDstIP(oldDst netip.Addr) netip.Addr {
// selectSrcIP returns the source IP to use for a packet to dst.
// If the packet is not from the native address, it is returned as-is.
-func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
+func (c *natFamilyConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
if c == nil {
return oldSrc
}
@@ -615,16 +670,25 @@ func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
return oldSrc
}
-// natV4ConfigFromWGConfig generates a natV4Config from nm.
-// If v4 NAT is not required, it returns nil.
-func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
+// natConfigFromWGConfig generates a natFamilyConfig from nm,
+// for the indicated address family.
+// If NAT is not required for that address family, it returns nil.
+func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *natFamilyConfig {
if wcfg == nil {
return nil
}
- nativeAddr := findV4(wcfg.Addresses)
+
+ var nativeAddr netip.Addr
+ switch addrFam {
+ case ipproto.IPProtoVersion4:
+ nativeAddr = findV4(wcfg.Addresses)
+ case ipproto.IPProtoVersion6:
+ nativeAddr = findV6(wcfg.Addresses)
+ }
if !nativeAddr.IsValid() {
return nil
}
+
var (
rt table.RoutingTableBuilder
dstMasqAddrs map[key.NodePublic]netip.Addr
@@ -637,17 +701,25 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
exitNodeRequiresMasq := false // true if using an exit node and it requires masquerading
for _, p := range wcfg.Peers {
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
- if isExitNode && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
- exitNodeRequiresMasq = true
+ if isExitNode {
+ hasMasqAddrsForFamily := false ||
+ (addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
+ (addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
+ if hasMasqAddrsForFamily {
+ exitNodeRequiresMasq = true
+ }
break
}
}
for i := range wcfg.Peers {
p := &wcfg.Peers[i]
var addrToUse netip.Addr
- if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
+ if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
addrToUse = *p.V4MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
+ } else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
+ addrToUse = *p.V6MasqAddr
+ mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if exitNodeRequiresMasq {
addrToUse = nativeAddr
} else {
@@ -659,7 +731,7 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
if len(listenAddrs) == 0 && len(dstMasqAddrs) == 0 {
return nil
}
- return &natV4Config{
+ return &natFamilyConfig{
nativeAddr: nativeAddr,
listenAddrs: views.MapOf(listenAddrs),
dstMasqAddrs: views.MapOf(dstMasqAddrs),
@@ -668,10 +740,14 @@ func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
}
// SetNetMap is called when a new NetworkMap is received.
-// It currently (2023-03-01) only updates the IPv4 NAT configuration.
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
- cfg := natV4ConfigFromWGConfig(wcfg)
- old := t.natV4Config.Swap(cfg)
+ v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
+ var cfg *natConfig
+ if v4 != nil || v6 != nil {
+ cfg = &natConfig{v4: v4, v6: v6}
+ }
+
+ old := t.natConfig.Swap(cfg)
if !reflect.DeepEqual(old, cfg) {
t.logf("nat config: %v", cfg)
}
@@ -786,7 +862,7 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
- t.snatV4(p)
+ t.snat(p)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
@@ -843,7 +919,7 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, buf []byte, offset int) (int
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
p.Decode(buf[offset : offset+n])
- t.snatV4(p)
+ t.snat(p)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
@@ -965,7 +1041,7 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
captHook := t.captureHook.Load()
for _, buff := range buffs {
p.Decode(buff[offset:])
- t.dnatV4(p)
+ t.dnat(p)
if !t.disableFilter {
if t.filterPacketInboundFromWireGuard(p, captHook) != filter.Accept {
metricPacketInDrop.Add(1)
@@ -1030,7 +1106,7 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt stack.PacketBufferPtr) error {
if captHook != nil {
captHook(capture.SynthesizedToLocal, t.now(), p.Buffer(), p.CaptureMeta)
}
- t.dnatV4(p)
+ t.dnat(p)
return t.InjectInboundDirect(buf, PacketStartOffset)
}
diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go
index 7560c9aee5643..7dd3fd123ea65 100644
--- a/net/tstun/wrap_test.go
+++ b/net/tstun/wrap_test.go
@@ -23,7 +23,7 @@ import (
"github.com/tailscale/wireguard-go/tun/tuntest"
"go4.org/mem"
"go4.org/netipx"
- "gvisor.dev/gvisor/pkg/bufferv2"
+ "gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/disco"
"tailscale.com/net/connstats"
@@ -608,191 +608,217 @@ func TestNATCfg(t *testing.T) {
AllowedIPs: []netip.Prefix{
netip.PrefixFrom(ip, ip.BitLen()),
},
- V4MasqAddr: ptr.To(masqIP),
+ }
+ if masqIP.Is4() {
+ p.V4MasqAddr = ptr.To(masqIP)
+ } else {
+ p.V6MasqAddr = ptr.To(masqIP)
}
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
return p
}
- var (
- noIP netip.Addr
+ test := func(addrFam ipproto.IPProtoVersion) {
+ var (
+ noIP netip.Addr
- selfNativeIP = netip.MustParseAddr("100.64.0.1")
- selfEIP1 = netip.MustParseAddr("100.64.1.1")
- selfEIP2 = netip.MustParseAddr("100.64.1.2")
- selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
+ selfNativeIP = netip.MustParseAddr("100.64.0.1")
+ selfEIP1 = netip.MustParseAddr("100.64.1.1")
+ selfEIP2 = netip.MustParseAddr("100.64.1.2")
+ selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
- peer1IP = netip.MustParseAddr("100.64.0.2")
- peer2IP = netip.MustParseAddr("100.64.0.3")
+ peer1IP = netip.MustParseAddr("100.64.0.2")
+ peer2IP = netip.MustParseAddr("100.64.0.3")
- subnet = netip.MustParsePrefix("192.168.0.0/24")
- subnetIP = netip.MustParseAddr("192.168.0.1")
+ subnet = netip.MustParsePrefix("192.168.0.0/24")
+ subnetIP = netip.MustParseAddr("192.168.0.1")
- exitRoute = netip.MustParsePrefix("0.0.0.0/0")
- publicIP = netip.MustParseAddr("8.8.8.8")
- )
+ exitRoute = netip.MustParsePrefix("0.0.0.0/0")
+ publicIP = netip.MustParseAddr("8.8.8.8")
+ )
+ if addrFam == ipproto.IPProtoVersion6 {
+ selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
+ selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
+ selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
+ selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())}
- tests := []struct {
- name string
- wcfg *wgcfg.Config
- snatMap map[netip.Addr]netip.Addr // dst -> src
- dnatMap map[netip.Addr]netip.Addr
- }{
- {
- name: "no-cfg",
- wcfg: nil,
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfNativeIP,
- peer2IP: selfNativeIP,
- subnetIP: selfNativeIP,
- },
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfEIP1,
- selfEIP2: selfEIP2,
- },
- },
- {
- name: "single-peer-requires-nat",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, noIP),
- node(peer2IP, selfEIP1),
+ peer1IP = netip.MustParseAddr("fd7a:115c:a1e0::b")
+ peer2IP = netip.MustParseAddr("fd7a:115c:a1e0::c")
+
+ subnet = netip.MustParsePrefix("2001:db8::/32")
+ subnetIP = netip.MustParseAddr("2001:db8::FFFF")
+
+ exitRoute = netip.MustParsePrefix("::/0")
+ publicIP = netip.MustParseAddr("2001:4860:4860::8888")
+ }
+
+ tests := []struct {
+ name string
+ wcfg *wgcfg.Config
+ snatMap map[netip.Addr]netip.Addr // dst -> src
+ dnatMap map[netip.Addr]netip.Addr
+ }{
+ {
+ name: "no-cfg",
+ wcfg: nil,
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfNativeIP,
+ peer2IP: selfNativeIP,
+ subnetIP: selfNativeIP,
},
- },
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfNativeIP,
- peer2IP: selfEIP1,
- subnetIP: selfNativeIP,
- },
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfNativeIP,
- selfEIP2: selfEIP2,
- subnetIP: subnetIP,
- },
- },
- {
- name: "multiple-peers-require-nat",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, selfEIP1),
- node(peer2IP, selfEIP2),
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfEIP1,
+ selfEIP2: selfEIP2,
},
},
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfEIP1,
- peer2IP: selfEIP2,
- subnetIP: selfNativeIP,
- },
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfNativeIP,
- selfEIP2: selfNativeIP,
- subnetIP: subnetIP,
- },
- },
- {
- name: "multiple-peers-require-nat-with-subnet",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, selfEIP1),
- node(peer2IP, selfEIP2, subnet),
+ {
+ name: "single-peer-requires-nat",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, noIP),
+ node(peer2IP, selfEIP1),
+ },
},
- },
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfEIP1,
- peer2IP: selfEIP2,
- subnetIP: selfEIP2,
- },
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfNativeIP,
- selfEIP2: selfNativeIP,
- subnetIP: subnetIP,
- },
- },
- {
- name: "multiple-peers-require-nat-with-default-route",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, selfEIP1),
- node(peer2IP, selfEIP2, exitRoute),
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfNativeIP,
+ peer2IP: selfEIP1,
+ subnetIP: selfNativeIP,
},
- },
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfEIP1,
- peer2IP: selfEIP2,
- publicIP: selfEIP2,
- },
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfNativeIP,
- selfEIP2: selfNativeIP,
- subnetIP: subnetIP,
- },
- },
- {
- name: "no-nat",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, noIP),
- node(peer2IP, noIP),
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfNativeIP,
+ selfEIP2: selfEIP2,
+ subnetIP: subnetIP,
},
},
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfNativeIP,
- peer2IP: selfNativeIP,
- subnetIP: selfNativeIP,
+ {
+ name: "multiple-peers-require-nat",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, selfEIP1),
+ node(peer2IP, selfEIP2),
+ },
+ },
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfEIP1,
+ peer2IP: selfEIP2,
+ subnetIP: selfNativeIP,
+ },
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfNativeIP,
+ selfEIP2: selfNativeIP,
+ subnetIP: subnetIP,
+ },
},
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP1: selfEIP1,
- selfEIP2: selfEIP2,
- subnetIP: subnetIP,
+ {
+ name: "multiple-peers-require-nat-with-subnet",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, selfEIP1),
+ node(peer2IP, selfEIP2, subnet),
+ },
+ },
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfEIP1,
+ peer2IP: selfEIP2,
+ subnetIP: selfEIP2,
+ },
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfNativeIP,
+ selfEIP2: selfNativeIP,
+ subnetIP: subnetIP,
+ },
},
- },
- {
- name: "exit-node-require-nat-peer-doesnt",
- wcfg: &wgcfg.Config{
- Addresses: selfAddrs,
- Peers: []wgcfg.Peer{
- node(peer1IP, noIP),
- node(peer2IP, selfEIP2, exitRoute),
+ {
+ name: "multiple-peers-require-nat-with-default-route",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, selfEIP1),
+ node(peer2IP, selfEIP2, exitRoute),
+ },
+ },
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfEIP1,
+ peer2IP: selfEIP2,
+ publicIP: selfEIP2,
+ },
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfNativeIP,
+ selfEIP2: selfNativeIP,
+ subnetIP: subnetIP,
},
},
- snatMap: map[netip.Addr]netip.Addr{
- peer1IP: selfNativeIP,
- peer2IP: selfEIP2,
- publicIP: selfEIP2,
+ {
+ name: "no-nat",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, noIP),
+ node(peer2IP, noIP),
+ },
+ },
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfNativeIP,
+ peer2IP: selfNativeIP,
+ subnetIP: selfNativeIP,
+ },
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP1: selfEIP1,
+ selfEIP2: selfEIP2,
+ subnetIP: subnetIP,
+ },
},
- dnatMap: map[netip.Addr]netip.Addr{
- selfNativeIP: selfNativeIP,
- selfEIP2: selfNativeIP,
- subnetIP: subnetIP,
+ {
+ name: "exit-node-require-nat-peer-doesnt",
+ wcfg: &wgcfg.Config{
+ Addresses: selfAddrs,
+ Peers: []wgcfg.Peer{
+ node(peer1IP, noIP),
+ node(peer2IP, selfEIP2, exitRoute),
+ },
+ },
+ snatMap: map[netip.Addr]netip.Addr{
+ peer1IP: selfNativeIP,
+ peer2IP: selfEIP2,
+ publicIP: selfEIP2,
+ },
+ dnatMap: map[netip.Addr]netip.Addr{
+ selfNativeIP: selfNativeIP,
+ selfEIP2: selfNativeIP,
+ subnetIP: subnetIP,
+ },
},
- },
- }
+ }
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- ncfg := natV4ConfigFromWGConfig(tc.wcfg)
- for peer, want := range tc.snatMap {
- if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
- t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) {
+ ncfg := natConfigFromWGConfig(tc.wcfg, addrFam)
+ for peer, want := range tc.snatMap {
+ if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
+ t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)
+ }
}
- }
- for dstIP, want := range tc.dnatMap {
- if got := ncfg.mapDstIP(dstIP); got != want {
- t.Errorf("mapDstIP[%v]: got %v; want %v", dstIP, got, want)
+ for dstIP, want := range tc.dnatMap {
+ if got := ncfg.mapDstIP(dstIP); got != want {
+ t.Errorf("mapDstIP[%v]: got %v; want %v", dstIP, got, want)
+ }
}
- }
- })
+ if t.Failed() {
+ t.Logf("%v", ncfg)
+ }
+ })
+ }
}
+ test(ipproto.IPProtoVersion4)
+ test(ipproto.IPProtoVersion6)
}
// TestCaptureHook verifies that the Wrapper.captureHook callback is called
@@ -845,12 +871,12 @@ func TestCaptureHook(t *testing.T) {
[]byte("Write2"),
}, 0)
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
- Payload: bufferv2.MakeWithData([]byte("InjectInboundPacketBuffer")),
+ Payload: buffer.MakeWithData([]byte("InjectInboundPacketBuffer")),
})
w.InjectInboundPacketBuffer(packetBuf)
packetBuf = stack.NewPacketBuffer(stack.PacketBufferOptions{
- Payload: bufferv2.MakeWithData([]byte("InjectOutboundPacketBuffer")),
+ Payload: buffer.MakeWithData([]byte("InjectOutboundPacketBuffer")),
})
w.InjectOutboundPacketBuffer(packetBuf)
diff --git a/portlist/portlist.go b/portlist/portlist.go
index 81c1664a7724e..9f7af40d08dc1 100644
--- a/portlist/portlist.go
+++ b/portlist/portlist.go
@@ -17,8 +17,8 @@ import (
type Port struct {
Proto string // "tcp" or "udp"
Port uint16 // port number
- Process string // optional process name, if found
- Pid int // process id, if known
+ Process string // optional process name, if found (requires suitable permissions)
+ Pid int // process ID, if known (requires suitable permissions)
}
// List is a list of Ports.
diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go
index eb1e53f9eb7b1..68b8ed7ba4b16 100644
--- a/portlist/portlist_linux.go
+++ b/portlist/portlist_linux.go
@@ -322,6 +322,8 @@ func (li *linuxImpl) findProcessNames(need map[string]*portMeta) error {
pe.pid = int(p)
}
pe.port.Process = argvSubject(argv...)
+ pid64, _ := mem.ParseInt(pid, 10, 0)
+ pe.port.Pid = int(pid64)
pe.needsProcName = false
delete(need, string(targetBuf[:n]))
if len(need) == 0 {
diff --git a/posture/serialnumber_macos.go b/posture/serialnumber_macos.go
new file mode 100644
index 0000000000000..48355d31393ee
--- /dev/null
+++ b/posture/serialnumber_macos.go
@@ -0,0 +1,74 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build cgo && darwin && !ios
+
+package posture
+
+// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
+// #include
+// #include
+//
+// #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
+// #define kIOMainPortDefault kIOMasterPortDefault
+// #endif
+//
+// const char *
+// getSerialNumber()
+// {
+// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice");
+// if (!matching) {
+// return "err: failed to create dictionary to match IOServices";
+// }
+//
+// io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching);
+// if (!service) {
+// return "err: failed to look up registered IOService objects that match a matching dictionary";
+// }
+//
+// CFStringRef serialNumberRef = IORegistryEntryCreateCFProperty(
+// service,
+// CFSTR("IOPlatformSerialNumber"),
+// kCFAllocatorDefault,
+// 0
+// );
+// if (!serialNumberRef) {
+// return "err: failed to look up serial number in IORegistry";
+// }
+//
+// CFIndex length = CFStringGetLength(serialNumberRef);
+// CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
+// char *serialNumberBuf = (char *)malloc(max_size);
+//
+// bool result = CFStringGetCString(serialNumberRef, serialNumberBuf, max_size, kCFStringEncodingUTF8);
+//
+// CFRelease(serialNumberRef);
+// IOObjectRelease(service);
+//
+// if (!result) {
+// free(serialNumberBuf);
+//
+// return "err: failed to convert serial number reference to string";
+// }
+//
+// return serialNumberBuf;
+// }
+import "C"
+import (
+ "fmt"
+ "strings"
+
+ "tailscale.com/types/logger"
+)
+
+// GetSerialNumber returns the platform serial sumber as reported by IOKit.
+func GetSerialNumbers(_ logger.Logf) ([]string, error) {
+ csn := C.getSerialNumber()
+ serialNumber := C.GoString(csn)
+
+ if err, ok := strings.CutPrefix(serialNumber, "err: "); ok {
+ return nil, fmt.Errorf("failed to get serial number from IOKit: %s", err)
+ }
+
+ return []string{serialNumber}, nil
+}
diff --git a/posture/serialnumber_macos_test.go b/posture/serialnumber_macos_test.go
new file mode 100644
index 0000000000000..4ca22edf32ea7
--- /dev/null
+++ b/posture/serialnumber_macos_test.go
@@ -0,0 +1,37 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build cgo && darwin && !ios
+
+package posture
+
+import (
+ "fmt"
+ "testing"
+
+ "tailscale.com/types/logger"
+ "tailscale.com/util/cibuild"
+)
+
+func TestGetSerialNumberMac(t *testing.T) {
+ // Do not run this test on CI, it can only be ran on macOS
+ // and we currenty only use Linux runners.
+ if cibuild.On() {
+ t.Skip()
+ }
+
+ sns, err := GetSerialNumbers(logger.Discard)
+ if err != nil {
+ t.Fatalf("failed to get serial number: %s", err)
+ }
+
+ if len(sns) != 1 {
+ t.Errorf("expected list of one serial number, got %v", sns)
+ }
+
+ if len(sns[0]) <= 0 {
+ t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
+ }
+
+ fmt.Printf("serials: %v\n", sns)
+}
diff --git a/posture/serialnumber_notmacos.go b/posture/serialnumber_notmacos.go
new file mode 100644
index 0000000000000..2e03fac4c049d
--- /dev/null
+++ b/posture/serialnumber_notmacos.go
@@ -0,0 +1,143 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Build on Windows, Linux and *BSD
+
+//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
+
+package posture
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/digitalocean/go-smbios/smbios"
+ "tailscale.com/types/logger"
+ "tailscale.com/util/multierr"
+)
+
+// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
+func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
+ // the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
+ // so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
+ index := specOffset - 4
+ if index >= len(s.Formatted) || index < 0 {
+ return 0
+ }
+
+ return s.Formatted[index]
+}
+
+// getStringFromSmbiosStructure retrieves a string at the given specOffset.
+// Returns an empty string if no string was present.
+func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) {
+ index := getByteFromSmbiosStructure(s, specOffset)
+
+ if index == 0 || int(index) > len(s.Strings) {
+ return "", errors.New("specified offset does not exist in smbios structure")
+ }
+
+ str := s.Strings[index-1]
+ trimmed := strings.TrimSpace(str)
+
+ return trimmed, nil
+}
+
+// Product Table (Type 1) structure
+// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
+// Page 34 and onwards.
+const (
+ // Serial is present at the same offset in all IDs
+ serialNumberOffset = 0x07
+
+ productID = 1
+ baseboardID = 2
+ chassisID = 3
+)
+
+var (
+ idToTableName = map[int]string{
+ 1: "product",
+ 2: "baseboard",
+ 3: "chassis",
+ }
+ validTables []string
+ numOfTables int
+)
+
+func init() {
+ for _, table := range idToTableName {
+ validTables = append(validTables, table)
+ }
+ numOfTables = len(validTables)
+
+}
+
+// serialFromSmbiosStructure extracts a serial number from a product,
+// baseboard or chassis SMBIOS table.
+func serialFromSmbiosStructure(s *smbios.Structure) (string, error) {
+ id := s.Header.Type
+ if (id != productID) && (id != baseboardID) && (id != chassisID) {
+ return "", fmt.Errorf(
+ "cannot get serial table type %d, supported tables are %v",
+ id,
+ validTables,
+ )
+ }
+
+ serial, err := getStringFromSmbiosStructure(s, serialNumberOffset)
+ if err != nil {
+ return "", fmt.Errorf(
+ "failed to get serial from %s table: %w",
+ idToTableName[int(s.Header.Type)],
+ err,
+ )
+ }
+
+ return serial, nil
+}
+
+func GetSerialNumbers(logf logger.Logf) ([]string, error) {
+ // Find SMBIOS data in operating system-specific location.
+ rc, _, err := smbios.Stream()
+ if err != nil {
+ return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
+ }
+ defer rc.Close()
+
+ // Decode SMBIOS structures from the stream.
+ d := smbios.NewDecoder(rc)
+ ss, err := d.Decode()
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
+ }
+
+ serials := make([]string, 0, numOfTables)
+ errs := make([]error, 0, numOfTables)
+
+ for _, s := range ss {
+ switch s.Header.Type {
+ case productID, baseboardID, chassisID:
+ serial, err := serialFromSmbiosStructure(s)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ serials = append(serials, serial)
+ }
+ }
+
+ err = multierr.New(errs...)
+
+ // if there were no serial numbers, check if any errors were
+ // returned and combine them.
+ if len(serials) == 0 && err != nil {
+ return nil, err
+ }
+
+ logf("got serial numbers %v (errors: %s)", serials, err)
+
+ return serials, nil
+}
diff --git a/posture/serialnumber_notmacos_test.go b/posture/serialnumber_notmacos_test.go
new file mode 100644
index 0000000000000..f2a15e0373caf
--- /dev/null
+++ b/posture/serialnumber_notmacos_test.go
@@ -0,0 +1,38 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Build on Windows, Linux and *BSD
+
+//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
+
+package posture
+
+import (
+ "fmt"
+ "testing"
+
+ "tailscale.com/types/logger"
+)
+
+func TestGetSerialNumberNotMac(t *testing.T) {
+ // This test is intentionally skipped as it will
+ // require root on Linux to get access to the serials.
+ // The test case is intended for local testing.
+ // Comment out skip for local testing.
+ t.Skip()
+
+ sns, err := GetSerialNumbers(logger.Discard)
+ if err != nil {
+ t.Fatalf("failed to get serial number: %s", err)
+ }
+
+ if len(sns) == 0 {
+ t.Fatalf("expected at least one serial number, got %v", sns)
+ }
+
+ if len(sns[0]) <= 0 {
+ t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
+ }
+
+ fmt.Printf("serials: %v\n", sns)
+}
diff --git a/posture/serialnumber_stub.go b/posture/serialnumber_stub.go
new file mode 100644
index 0000000000000..5ad12677e299e
--- /dev/null
+++ b/posture/serialnumber_stub.go
@@ -0,0 +1,24 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// ios: Apple does not allow getting serials on iOS
+// android: not implemented
+// js: not implemented
+// plan9: not implemented
+// solaris: currently unsupported by go-smbios:
+// https://github.com/digitalocean/go-smbios/pull/21
+
+//go:build ios || android || solaris || plan9 || js || wasm || (darwin && !cgo)
+
+package posture
+
+import (
+ "errors"
+
+ "tailscale.com/types/logger"
+)
+
+// GetSerialNumber returns client machine serial number(s).
+func GetSerialNumbers(_ logger.Logf) ([]string, error) {
+ return nil, errors.New("not implemented")
+}
diff --git a/posture/serialnumber_test.go b/posture/serialnumber_test.go
new file mode 100644
index 0000000000000..fac4392fab7d3
--- /dev/null
+++ b/posture/serialnumber_test.go
@@ -0,0 +1,16 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package posture
+
+import (
+ "testing"
+
+ "tailscale.com/types/logger"
+)
+
+func TestGetSerialNumber(t *testing.T) {
+ // ensure GetSerialNumbers is implemented
+ // or covered by a stub on a given platform.
+ _, _ = GetSerialNumbers(logger.Discard)
+}
diff --git a/release/dist/unixpkgs/pkgs.go b/release/dist/unixpkgs/pkgs.go
index 8fb2c7ef38a1d..bad6ce572e675 100644
--- a/release/dist/unixpkgs/pkgs.go
+++ b/release/dist/unixpkgs/pkgs.go
@@ -250,14 +250,21 @@ func (t *debTarget) Build(b *dist.Build) ([]string, error) {
PreRemove: filepath.Join(repoDir, "release/deb/debian.prerm.sh"),
PostRemove: filepath.Join(repoDir, "release/deb/debian.postrm.sh"),
},
- Depends: []string{},
+ Depends: []string{
+ // iptables is almost always required but not strictly needed.
+ // Even if you can technically run Tailscale without it (by
+ // manually configuring nftables or userspace mode), we still
+ // mark this as "Depends" because our previous experiment in
+ // https://github.com/tailscale/tailscale/issues/9236 of making
+ // it only Recommends caused too many problems. Until our
+ // nftables table is more mature, we'd rather err on the side of
+ // wasting a little disk by including iptables for people who
+ // might not need it rather than handle reports of it being
+ // missing.
+ "iptables",
+ },
Recommends: []string{
"tailscale-archive-keyring (>= 1.35.181)",
- // iptables is often required but not strictly needed; see
- // https://github.com/tailscale/tailscale/issues/9236.
- // We want to let people be able to install without it
- // or remove it after the fact if they want.
- "iptables",
// The "ip" command isn't needed since 2021-11-01 in
// 408b0923a61972ed but kept as an option as of
// 2021-11-18 in d24ed3f68e35e802d531371. See
diff --git a/shell.nix b/shell.nix
index c81b04e0967cf..7e04059463cd7 100644
--- a/shell.nix
+++ b/shell.nix
@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
-# nix-direnv cache busting line: sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=
+# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go
index a4a024f8ec77f..8dfb8a6714db0 100644
--- a/ssh/tailssh/tailssh.go
+++ b/ssh/tailssh/tailssh.go
@@ -43,6 +43,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/clientmetric"
+ "tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
)
@@ -1752,7 +1753,7 @@ func (ss *sshSession) notifyControl(ctx context.Context, nodeKey key.NodePublic,
return
}
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
+ req, err := http.NewRequestWithContext(ctx, httpm.POST, url, bytes.NewReader(body))
if err != nil {
ss.logf("notifyControl: unable to create request:", err)
return
diff --git a/syncs/syncs.go b/syncs/syncs.go
index abfaba5d88f39..274e83b655f48 100644
--- a/syncs/syncs.go
+++ b/syncs/syncs.go
@@ -192,6 +192,26 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
return actual, loaded
}
+// LoadOrInit returns the value for the given key if it exists
+// otherwise f is called to construct the value to be set.
+// The lock is held for the duration to prevent duplicate initialization.
+func (m *Map[K, V]) LoadOrInit(key K, f func() V) (actual V, loaded bool) {
+ if actual, loaded := m.Load(key); loaded {
+ return actual, loaded
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if actual, loaded = m.m[key]; loaded {
+ return actual, loaded
+ }
+
+ loaded = false
+ actual = f()
+ mak.Set(&m.m, key, actual)
+ return actual, loaded
+}
+
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
m.mu.Lock()
defer m.mu.Unlock()
diff --git a/syncs/syncs_test.go b/syncs/syncs_test.go
index bb289862b04aa..b061aebaffdf3 100644
--- a/syncs/syncs_test.go
+++ b/syncs/syncs_test.go
@@ -91,8 +91,11 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadOrStore("two", 2); v != 2 || ok {
t.Errorf(`LoadOrStore("two", 2) = (%v, %v), want (2, false)`, v, ok)
}
+ if v, ok := m.LoadOrInit("three", func() int { return 3 }); v != 3 || ok {
+ t.Errorf(`LoadOrInit("three", 3) = (%v, %v), want (3, true)`, v, ok)
+ }
got := map[string]int{}
- want := map[string]int{"one": 1, "two": 2}
+ want := map[string]int{"one": 1, "two": 2, "three": 3}
m.Range(func(k string, v int) bool {
got[k] = v
return true
@@ -106,6 +109,7 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadAndDelete("two"); v != 0 || ok {
t.Errorf(`LoadAndDelete("two) = (%v, %v), want (0, false)`, v, ok)
}
+ m.Delete("three")
m.Delete("one")
m.Delete("noexist")
got = map[string]int{}
diff --git a/tailcfg/c2ntypes.go b/tailcfg/c2ntypes.go
index 44f3ac70c085c..042595e228086 100644
--- a/tailcfg/c2ntypes.go
+++ b/tailcfg/c2ntypes.go
@@ -52,3 +52,15 @@ type C2NUpdateResponse struct {
// Started indicates whether the update has started.
Started bool
}
+
+// C2NPostureIdentityResponse contains either a set of identifying serial number
+// from the client or a boolean indicating that the machine has opted out of
+// posture collection.
+type C2NPostureIdentityResponse struct {
+ // SerialNumbers is a list of serial numbers of the client machine.
+ SerialNumbers []string `json:",omitempty"`
+
+ // PostureDisabled indicates if the machine has opted out of
+ // device posture collection.
+ PostureDisabled bool `json:",omitempty"`
+}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index 9ead3525b2d12..2dcf8ab888755 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -10,13 +10,13 @@ import (
"encoding/json"
"errors"
"fmt"
+ "maps"
"net/netip"
"reflect"
"slices"
"strings"
"time"
- "golang.org/x/exp/maps"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
@@ -24,6 +24,7 @@ import (
"tailscale.com/types/tkatype"
"tailscale.com/util/cmpx"
"tailscale.com/util/dnsname"
+ "tailscale.com/util/slicesx"
)
// CapabilityVersion represents the client's capability level. That
@@ -115,7 +116,10 @@ type CapabilityVersion int
// - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
// - 74: 2023-09-18: Client understands NodeCapMap
// - 75: 2023-09-12: Client understands NodeAttrDNSForwarderDisableTCPRetries
-const CurrentCapabilityVersion CapabilityVersion = 75
+// - 76: 2023-09-20: Client understands ExitNodeDNSResolvers for IsWireGuardOnly nodes
+// - 77: 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer
+// - 78: 2023-10-05: can handle c2n Wake-on-LAN sending
+const CurrentCapabilityVersion CapabilityVersion = 78
type StableID string
@@ -267,9 +271,9 @@ type Node struct {
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
Machine key.MachinePublic
DiscoKey key.DiscoPublic
- Addresses []netip.Prefix // IP addresses of this Node directly
- AllowedIPs []netip.Prefix // range of IP addresses to route to this node
- Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
+ Addresses []netip.Prefix // IP addresses of this Node directly
+ AllowedIPs []netip.Prefix // range of IP addresses to route to this node
+ Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
// DERP is this node's home DERP region ID integer, but shoved into an
// IP:port string for legacy reasons. The IP address is always "127.3.3.40"
@@ -732,6 +736,7 @@ type Hostinfo struct {
GoVersion string `json:",omitempty"` // Go version binary was built with
RoutableIPs []netip.Prefix `json:",omitempty"` // set of IP ranges this client can route
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
+ WoLMACs []string `json:",omitempty"` // MAC address(es) to send Wake-on-LAN packets to wake this node (lowercase hex w/ colons)
Services []Service `json:",omitempty"` // services advertised by this machine
NetInfo *NetInfo `json:",omitempty"`
SSH_HostKeys []string `json:"sshHostKeys,omitempty"` // if advertised
@@ -1209,7 +1214,7 @@ type MapRequest struct {
// Endpoints are the client's magicsock UDP ip:port endpoints (IPv4 or IPv6).
// These can be ignored if Stream is true and Version >= 68.
- Endpoints []string
+ Endpoints []netip.AddrPort `json:",omitempty"`
// EndpointTypes are the types of the corresponding endpoints in Endpoints.
EndpointTypes []EndpointType `json:",omitempty"`
@@ -1938,10 +1943,10 @@ func (n *Node) Equal(n2 *Node) bool {
n.Machine == n2.Machine &&
n.DiscoKey == n2.DiscoKey &&
eqPtr(n.Online, n2.Online) &&
- eqCIDRs(n.Addresses, n2.Addresses) &&
- eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
- eqCIDRs(n.PrimaryRoutes, n2.PrimaryRoutes) &&
- eqStrings(n.Endpoints, n2.Endpoints) &&
+ slicesx.EqualSameNil(n.Addresses, n2.Addresses) &&
+ slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
+ slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
+ slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
n.DERP == n2.DERP &&
n.Cap == n2.Cap &&
n.Hostinfo.Equal(n2.Hostinfo) &&
@@ -1953,7 +1958,7 @@ func (n *Node) Equal(n2 *Node) bool {
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
- eqStrings(n.Tags, n2.Tags) &&
+ slicesx.EqualSameNil(n.Tags, n2.Tags) &&
n.Expired == n2.Expired &&
eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) &&
eqPtr(n.SelfNodeV6MasqAddrForThisPeer, n2.SelfNodeV6MasqAddrForThisPeer) &&
@@ -1970,30 +1975,6 @@ func eqPtr[T comparable](a, b *T) bool {
return *a == *b
}
-func eqStrings(a, b []string) bool {
- if len(a) != len(b) || ((a == nil) != (b == nil)) {
- return false
- }
- for i, v := range a {
- if v != b[i] {
- return false
- }
- }
- return true
-}
-
-func eqCIDRs(a, b []netip.Prefix) bool {
- if len(a) != len(b) || ((a == nil) != (b == nil)) {
- return false
- }
- for i, v := range a {
- if v != b[i] {
- return false
- }
- }
- return true
-}
-
func eqTimePtr(a, b *time.Time) bool {
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}
@@ -2455,6 +2436,22 @@ type QueryFeatureResponse struct {
ShouldWait bool `json:",omitempty"`
}
+// WebClientAuthResponse is the response to a web client authentication request
+// sent to "/machine/webclient/action" or "/machine/webclient/wait".
+// See client/web for usage.
+type WebClientAuthResponse struct {
+ // Message, if non-empty, provides a message for the user.
+ Message string `json:",omitempty"`
+
+ // Complete is true when the session authentication has been completed.
+ Complete bool `json:",omitempty"`
+
+ // URL is the link for the user to visit to authenticate the session.
+ //
+ // When empty, there is no action for the user to take.
+ URL string `json:",omitempty"`
+}
+
// OverTLSPublicKeyResponse is the JSON response to /key?v=
// over HTTPS (regular TLS) to the Tailscale control plane server,
// where the 'v' argument is the client's current capability version
@@ -2538,7 +2535,7 @@ type PeerChange struct {
// Endpoints, if non-empty, means that NodeID's UDP Endpoints
// have changed to these.
- Endpoints []string `json:",omitempty"`
+ Endpoints []netip.AddrPort `json:",omitempty"`
// Key, if non-nil, means that the NodeID's wireguard public key changed.
Key *key.NodePublic `json:",omitempty"`
diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go
index 6a2292149a554..da5a11b77f5f8 100644
--- a/tailcfg/tailcfg_clone.go
+++ b/tailcfg/tailcfg_clone.go
@@ -97,7 +97,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
DiscoKey key.DiscoPublic
Addresses []netip.Prefix
AllowedIPs []netip.Prefix
- Endpoints []string
+ Endpoints []netip.AddrPort
DERP string
Hostinfo HostinfoView
Created time.Time
@@ -131,6 +131,7 @@ func (src *Hostinfo) Clone() *Hostinfo {
*dst = *src
dst.RoutableIPs = append(src.RoutableIPs[:0:0], src.RoutableIPs...)
dst.RequestTags = append(src.RequestTags[:0:0], src.RequestTags...)
+ dst.WoLMACs = append(src.WoLMACs[:0:0], src.WoLMACs...)
dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone()
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...)
@@ -169,6 +170,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
GoVersion string
RoutableIPs []netip.Prefix
RequestTags []string
+ WoLMACs []string
Services []Service
NetInfo *NetInfo
SSH_HostKeys []string
diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go
index 86f8f45adb8ef..261008fc61423 100644
--- a/tailcfg/tailcfg_test.go
+++ b/tailcfg/tailcfg_test.go
@@ -57,6 +57,7 @@ func TestHostinfoEqual(t *testing.T) {
"GoVersion",
"RoutableIPs",
"RequestTags",
+ "WoLMACs",
"Services",
"NetInfo",
"SSH_HostKeys",
@@ -466,13 +467,13 @@ func TestNodeEqual(t *testing.T) {
true,
},
{
- &Node{Endpoints: []string{}},
+ &Node{Endpoints: []netip.AddrPort{}},
&Node{Endpoints: nil},
false,
},
{
- &Node{Endpoints: []string{}},
- &Node{Endpoints: []string{}},
+ &Node{Endpoints: []netip.AddrPort{}},
+ &Node{Endpoints: []netip.AddrPort{}},
true,
},
{
@@ -677,7 +678,7 @@ func TestCloneNode(t *testing.T) {
{"zero_fields", &Node{
Addresses: make([]netip.Prefix, 0),
AllowedIPs: make([]netip.Prefix, 0),
- Endpoints: make([]string, 0),
+ Endpoints: make([]netip.AddrPort, 0),
}},
}
for _, tt := range tests {
diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go
index 4a51f03f70bf0..74991eb11060f 100644
--- a/tailcfg/tailcfg_view.go
+++ b/tailcfg/tailcfg_view.go
@@ -142,7 +142,7 @@ func (v NodeView) Machine() key.MachinePublic { return v.ж.Machin
func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey }
func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) }
func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) }
-func (v NodeView) Endpoints() views.Slice[string] { return views.SliceOf(v.ж.Endpoints) }
+func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) }
func (v NodeView) DERP() string { return v.ж.DERP }
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
func (v NodeView) Created() time.Time { return v.ж.Created }
@@ -214,7 +214,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
DiscoKey key.DiscoPublic
Addresses []netip.Prefix
AllowedIPs []netip.Prefix
- Endpoints []string
+ Endpoints []netip.AddrPort
DERP string
Hostinfo HostinfoView
Created time.Time
@@ -310,6 +310,7 @@ func (v HostinfoView) GoArchVar() string { return v.ж.GoAr
func (v HostinfoView) GoVersion() string { return v.ж.GoVersion }
func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) }
func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) }
+func (v HostinfoView) WoLMACs() views.Slice[string] { return views.SliceOf(v.ж.WoLMACs) }
func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) }
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
@@ -355,6 +356,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
GoVersion string
RoutableIPs []netip.Prefix
RequestTags []string
+ WoLMACs []string
Services []Service
NetInfo *NetInfo
SSH_HostKeys []string
diff --git a/taildrop/retrieve.go b/taildrop/retrieve.go
new file mode 100644
index 0000000000000..7a773c950abc5
--- /dev/null
+++ b/taildrop/retrieve.go
@@ -0,0 +1,254 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package taildrop
+
+import (
+ "context"
+ "errors"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+
+ "tailscale.com/client/tailscale/apitype"
+ "tailscale.com/logtail/backoff"
+)
+
+// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
+// This always returns false when [Handler.DirectFileMode] is false.
+func (s *Handler) HasFilesWaiting() bool {
+ if s == nil || s.Dir == "" || s.DirectFileMode {
+ return false
+ }
+ if s.knownEmpty.Load() {
+ // Optimization: this is usually empty, so avoid opening
+ // the directory and checking. We can't cache the actual
+ // has-files-or-not values as the macOS/iOS client might
+ // in the future use+delete the files directly. So only
+ // keep this negative cache.
+ return false
+ }
+ f, err := os.Open(s.Dir)
+ if err != nil {
+ return false
+ }
+ defer f.Close()
+ for {
+ des, err := f.ReadDir(10)
+ for _, de := range des {
+ name := de.Name()
+ if strings.HasSuffix(name, partialSuffix) {
+ continue
+ }
+ if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
+ // After we're done looping over files, then try
+ // to delete this file. Don't do it proactively,
+ // as the OS may return "foo.jpg.deleted" before "foo.jpg"
+ // and we don't want to delete the ".deleted" file before
+ // enumerating to the "foo.jpg" file.
+ defer tryDeleteAgain(filepath.Join(s.Dir, name))
+ continue
+ }
+ if de.Type().IsRegular() {
+ _, err := os.Stat(filepath.Join(s.Dir, name+deletedSuffix))
+ if os.IsNotExist(err) {
+ return true
+ }
+ if err == nil {
+ tryDeleteAgain(filepath.Join(s.Dir, name))
+ continue
+ }
+ }
+ }
+ if err == io.EOF {
+ s.knownEmpty.Store(true)
+ }
+ if err != nil {
+ break
+ }
+ }
+ return false
+}
+
+// WaitingFiles returns the list of files that have been sent by a
+// peer that are waiting in [Handler.Dir].
+// This always returns nil when [Handler.DirectFileMode] is false.
+func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
+ if s == nil {
+ return nil, errNilHandler
+ }
+ if s.Dir == "" {
+ return nil, errNoTaildrop
+ }
+ if s.DirectFileMode {
+ return nil, nil
+ }
+ f, err := os.Open(s.Dir)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
+ for {
+ des, err := f.ReadDir(10)
+ for _, de := range des {
+ name := de.Name()
+ if strings.HasSuffix(name, partialSuffix) {
+ continue
+ }
+ if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
+ if deleted == nil {
+ deleted = map[string]bool{}
+ }
+ deleted[name] = true
+ continue
+ }
+ if de.Type().IsRegular() {
+ fi, err := de.Info()
+ if err != nil {
+ continue
+ }
+ ret = append(ret, apitype.WaitingFile{
+ Name: filepath.Base(name),
+ Size: fi.Size(),
+ })
+ }
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ if len(deleted) > 0 {
+ // Filter out any return values "foo.jpg" where a
+ // "foo.jpg.deleted" marker file exists on disk.
+ all := ret
+ ret = ret[:0]
+ for _, wf := range all {
+ if !deleted[wf.Name] {
+ ret = append(ret, wf)
+ }
+ }
+ // And do some opportunistic deleting while we're here.
+ // Maybe Windows is done virus scanning the file we tried
+ // to delete a long time ago and will let us delete it now.
+ for name := range deleted {
+ tryDeleteAgain(filepath.Join(s.Dir, name))
+ }
+ }
+ sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
+ return ret, nil
+}
+
+// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
+// it failed earlier. This happens on Windows when various anti-virus
+// tools hook into filesystem operations and have the file open still
+// while we're trying to delete it. In that case we instead mark it as
+// deleted (writing a "foo.jpg.deleted" marker file), but then we
+// later try to clean them up.
+//
+// fullPath is the full path to the file without the deleted suffix.
+func tryDeleteAgain(fullPath string) {
+ if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
+ os.Remove(fullPath + deletedSuffix)
+ }
+}
+
+// DeleteFile deletes a file of the given baseName from [Handler.Dir].
+// This method is only allowed when [Handler.DirectFileMode] is false.
+func (s *Handler) DeleteFile(baseName string) error {
+ if s == nil {
+ return errNilHandler
+ }
+ if s.Dir == "" {
+ return errNoTaildrop
+ }
+ if s.DirectFileMode {
+ return errors.New("deletes not allowed in direct mode")
+ }
+ path, ok := s.diskPath(baseName)
+ if !ok {
+ return errors.New("bad filename")
+ }
+ var bo *backoff.Backoff
+ logf := s.Logf
+ t0 := s.Clock.Now()
+ for {
+ err := os.Remove(path)
+ if err != nil && !os.IsNotExist(err) {
+ err = redactErr(err)
+ // Put a retry loop around deletes on Windows. Windows
+ // file descriptor closes are effectively asynchronous,
+ // as a bunch of hooks run on/after close, and we can't
+ // necessarily delete the file for a while after close,
+ // as we need to wait for everybody to be done with
+ // it. (on Windows, unlike Unix, a file can't be deleted
+ // if it's open anywhere)
+ // So try a few times but ultimately just leave a
+ // "foo.jpg.deleted" marker file to note that it's
+ // deleted and we clean it up later.
+ if runtime.GOOS == "windows" {
+ if bo == nil {
+ bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
+ }
+ if s.Clock.Since(t0) < 5*time.Second {
+ bo.BackOff(context.Background(), err)
+ continue
+ }
+ if err := touchFile(path + deletedSuffix); err != nil {
+ logf("peerapi: failed to leave deleted marker: %v", err)
+ }
+ }
+ logf("peerapi: failed to DeleteFile: %v", err)
+ return err
+ }
+ return nil
+ }
+}
+
+func touchFile(path string) error {
+ f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
+ if err != nil {
+ return redactErr(err)
+ }
+ return f.Close()
+}
+
+// OpenFile opens a file of the given baseName from [Handler.Dir].
+// This method is only allowed when [Handler.DirectFileMode] is false.
+func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
+ if s == nil {
+ return nil, 0, errNilHandler
+ }
+ if s.Dir == "" {
+ return nil, 0, errNoTaildrop
+ }
+ if s.DirectFileMode {
+ return nil, 0, errors.New("opens not allowed in direct mode")
+ }
+ path, ok := s.diskPath(baseName)
+ if !ok {
+ return nil, 0, errors.New("bad filename")
+ }
+ if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
+ tryDeleteAgain(path)
+ return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
+ }
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, 0, redactErr(err)
+ }
+ fi, err := f.Stat()
+ if err != nil {
+ f.Close()
+ return nil, 0, redactErr(err)
+ }
+ return f, fi.Size(), nil
+}
diff --git a/taildrop/send.go b/taildrop/send.go
new file mode 100644
index 0000000000000..bee750d88b470
--- /dev/null
+++ b/taildrop/send.go
@@ -0,0 +1,184 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package taildrop
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "tailscale.com/envknob"
+ "tailscale.com/tstime"
+ "tailscale.com/version/distro"
+)
+
+type incomingFile struct {
+ clock tstime.Clock
+
+ name string // "foo.jpg"
+ started time.Time
+ size int64 // or -1 if unknown; never 0
+ w io.Writer // underlying writer
+ sendFileNotify func() // called when done
+ partialPath string // non-empty in direct mode
+
+ mu sync.Mutex
+ copied int64
+ done bool
+ lastNotify time.Time
+}
+
+func (f *incomingFile) markAndNotifyDone() {
+ f.mu.Lock()
+ f.done = true
+ f.mu.Unlock()
+ f.sendFileNotify()
+}
+
+func (f *incomingFile) Write(p []byte) (n int, err error) {
+ n, err = f.w.Write(p)
+
+ var needNotify bool
+ defer func() {
+ if needNotify {
+ f.sendFileNotify()
+ }
+ }()
+ if n > 0 {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ f.copied += int64(n)
+ now := f.clock.Now()
+ if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
+ f.lastNotify = now
+ needNotify = true
+ }
+ }
+ return n, err
+}
+
+// HandlePut receives a file.
+// It handles an HTTP PUT request to the "/v0/put/{filename}" endpoint,
+// where {filename} is a base filename.
+// It returns the number of bytes received and whether it was received successfully.
+func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize int64, success bool) {
+ if !envknob.CanTaildrop() {
+ http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
+ return finalSize, success
+ }
+ if r.Method != "PUT" {
+ http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
+ return finalSize, success
+ }
+ if h == nil || h.Dir == "" {
+ http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
+ return finalSize, success
+ }
+ if distro.Get() == distro.Unraid && !h.DirectFileMode {
+ http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
+ return finalSize, success
+ }
+ rawPath := r.URL.EscapedPath()
+ suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
+ if !ok {
+ http.Error(w, "misconfigured internals", http.StatusInternalServerError)
+ return finalSize, success
+ }
+ if suffix == "" {
+ http.Error(w, "empty filename", http.StatusBadRequest)
+ return finalSize, success
+ }
+ if strings.Contains(suffix, "/") {
+ http.Error(w, "directories not supported", http.StatusBadRequest)
+ return finalSize, success
+ }
+ baseName, err := url.PathUnescape(suffix)
+ if err != nil {
+ http.Error(w, "bad path encoding", http.StatusBadRequest)
+ return finalSize, success
+ }
+ dstFile, ok := h.diskPath(baseName)
+ if !ok {
+ http.Error(w, "bad filename", http.StatusBadRequest)
+ return finalSize, success
+ }
+ // TODO(bradfitz): prevent same filename being sent by two peers at once
+
+ // prevent same filename being sent twice
+ if _, err := os.Stat(dstFile); err == nil {
+ http.Error(w, "file exists", http.StatusConflict)
+ return finalSize, success
+ }
+
+ partialFile := dstFile + partialSuffix
+ f, err := os.Create(partialFile)
+ if err != nil {
+ h.Logf("put Create error: %v", redactErr(err))
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return finalSize, success
+ }
+ defer func() {
+ if !success {
+ os.Remove(partialFile)
+ }
+ }()
+ var inFile *incomingFile
+ sendFileNotify := h.SendFileNotify
+ if sendFileNotify == nil {
+ sendFileNotify = func() {} // avoid nil panics below
+ }
+ if r.ContentLength != 0 {
+ inFile = &incomingFile{
+ clock: h.Clock,
+ name: baseName,
+ started: h.Clock.Now(),
+ size: r.ContentLength,
+ w: f,
+ sendFileNotify: sendFileNotify,
+ }
+ if h.DirectFileMode {
+ inFile.partialPath = partialFile
+ }
+ h.incomingFiles.Store(inFile, struct{}{})
+ defer h.incomingFiles.Delete(inFile)
+ n, err := io.Copy(inFile, r.Body)
+ if err != nil {
+ err = redactErr(err)
+ f.Close()
+ h.Logf("put Copy error: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return finalSize, success
+ }
+ finalSize = n
+ }
+ if err := redactErr(f.Close()); err != nil {
+ h.Logf("put Close error: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return finalSize, success
+ }
+ if h.DirectFileMode && h.AvoidFinalRename {
+ if inFile != nil { // non-zero length; TODO: notify even for zero length
+ inFile.markAndNotifyDone()
+ }
+ } else {
+ if err := os.Rename(partialFile, dstFile); err != nil {
+ err = redactErr(err)
+ h.Logf("put final rename: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return finalSize, success
+ }
+ }
+
+ // TODO: set modtime
+ // TODO: some real response
+ success = true
+ io.WriteString(w, "{}\n")
+ h.knownEmpty.Store(false)
+ sendFileNotify()
+ return finalSize, success
+}
diff --git a/taildrop/taildrop.go b/taildrop/taildrop.go
new file mode 100644
index 0000000000000..b482655cdd2c3
--- /dev/null
+++ b/taildrop/taildrop.go
@@ -0,0 +1,222 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package taildrop contains the implementation of the Taildrop
+// functionality including sending and retrieving files.
+// This package does not validate permissions, the caller should
+// be responsible for ensuring correct authorization.
+//
+// For related documentation see: http://go/taildrop-how-does-it-work
+package taildrop
+
+import (
+ "errors"
+ "hash/adler32"
+ "os"
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "unicode"
+ "unicode/utf8"
+
+ "tailscale.com/ipn"
+ "tailscale.com/syncs"
+ "tailscale.com/tstime"
+ "tailscale.com/types/logger"
+ "tailscale.com/util/multierr"
+)
+
+// Handler manages the state for receiving and managing taildropped files.
+type Handler struct {
+ Logf logger.Logf
+ Clock tstime.Clock
+
+ // Dir is the directory to store received files.
+ // This main either be the final location for the files
+ // or just a temporary staging directory (see DirectFileMode).
+ Dir string
+
+ // DirectFileMode reports whether we are writing files
+ // directly to a download directory, rather than writing them to
+ // a temporary staging directory.
+ //
+ // The following methods:
+ // - HasFilesWaiting
+ // - WaitingFiles
+ // - DeleteFile
+ // - OpenFile
+ // have no purpose in DirectFileMode.
+ // They are only used to check whether files are in the staging directory,
+ // copy them out, and then delete them.
+ DirectFileMode bool
+
+ // AvoidFinalRename specifies whether in DirectFileMode
+ // we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
+ AvoidFinalRename bool
+
+ // SendFileNotify is called periodically while a file is actively
+ // receiving the contents for the file. There is a final call
+ // to the function when reception completes.
+ // It is not called if nil.
+ SendFileNotify func()
+
+ knownEmpty atomic.Bool
+
+ incomingFiles syncs.Map[*incomingFile, struct{}]
+}
+
+var (
+ errNilHandler = errors.New("handler unavailable; not listening")
+ errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
+)
+
+const (
+ // partialSuffix is the suffix appended to files while they're
+ // still in the process of being transferred.
+ partialSuffix = ".partial"
+
+ // deletedSuffix is the suffix for a deleted marker file
+ // that's placed next to a file (without the suffix) that we
+ // tried to delete, but Windows wouldn't let us. These are
+ // only written on Windows (and in tests), but they're not
+ // permitted to be uploaded directly on any platform, like
+ // partial files.
+ deletedSuffix = ".deleted"
+)
+
+// redacted is a fake path name we use in errors, to avoid
+// accidentally logging actual filenames anywhere.
+const redacted = "redacted"
+
+func validFilenameRune(r rune) bool {
+ switch r {
+ case '/':
+ return false
+ case '\\', ':', '*', '"', '<', '>', '|':
+ // Invalid stuff on Windows, but we reject them everywhere
+ // for now.
+ // TODO(bradfitz): figure out a better plan. We initially just
+ // wrote things to disk URL path-escaped, but that's gross
+ // when debugging, and just moves the problem to callers.
+ // So now we put the UTF-8 filenames on disk directly as
+ // sent.
+ return false
+ }
+ return unicode.IsPrint(r)
+}
+
+func (s *Handler) diskPath(baseName string) (fullPath string, ok bool) {
+ if !utf8.ValidString(baseName) {
+ return "", false
+ }
+ if strings.TrimSpace(baseName) != baseName {
+ return "", false
+ }
+ if len(baseName) > 255 {
+ return "", false
+ }
+ // TODO: validate unicode normalization form too? Varies by platform.
+ clean := path.Clean(baseName)
+ if clean != baseName ||
+ clean == "." || clean == ".." ||
+ strings.HasSuffix(clean, deletedSuffix) ||
+ strings.HasSuffix(clean, partialSuffix) {
+ return "", false
+ }
+ for _, r := range baseName {
+ if !validFilenameRune(r) {
+ return "", false
+ }
+ }
+ if !filepath.IsLocal(baseName) {
+ return "", false
+ }
+ return filepath.Join(s.Dir, baseName), true
+}
+
+func (s *Handler) IncomingFiles() []ipn.PartialFile {
+ // Make sure we always set n.IncomingFiles non-nil so it gets encoded
+ // in JSON to clients. They distinguish between empty and non-nil
+ // to know whether a Notify should be able about files.
+ files := make([]ipn.PartialFile, 0)
+ s.incomingFiles.Range(func(f *incomingFile, _ struct{}) bool {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ files = append(files, ipn.PartialFile{
+ Name: f.name,
+ Started: f.started,
+ DeclaredSize: f.size,
+ Received: f.copied,
+ PartialPath: f.partialPath,
+ Done: f.done,
+ })
+ return true
+ })
+ return files
+}
+
+type redactedErr struct {
+ msg string
+ inner error
+}
+
+func (re *redactedErr) Error() string {
+ return re.msg
+}
+
+func (re *redactedErr) Unwrap() error {
+ return re.inner
+}
+
+func redactString(s string) string {
+ hash := adler32.Checksum([]byte(s))
+
+ var buf [len(redacted) + len(".12345678")]byte
+ b := append(buf[:0], []byte(redacted)...)
+ b = append(b, '.')
+ b = strconv.AppendUint(b, uint64(hash), 16)
+ return string(b)
+}
+
+func redactErr(root error) error {
+ // redactStrings is a list of sensitive strings that were redacted.
+ // It is not sufficient to just snub out sensitive fields in Go errors
+ // since some wrapper errors like fmt.Errorf pre-cache the error string,
+ // which would unfortunately remain unaffected.
+ var redactStrings []string
+
+ // Redact sensitive fields in known Go error types.
+ var unknownErrors int
+ multierr.Range(root, func(err error) bool {
+ switch err := err.(type) {
+ case *os.PathError:
+ redactStrings = append(redactStrings, err.Path)
+ err.Path = redactString(err.Path)
+ case *os.LinkError:
+ redactStrings = append(redactStrings, err.New, err.Old)
+ err.New = redactString(err.New)
+ err.Old = redactString(err.Old)
+ default:
+ unknownErrors++
+ }
+ return true
+ })
+
+ // If there are no redacted strings or no unknown error types,
+ // then we can return the possibly modified root error verbatim.
+ // Otherwise, we must replace redacted strings from any wrappers.
+ if len(redactStrings) == 0 || unknownErrors == 0 {
+ return root
+ }
+
+ // Stringify and replace any paths that we found above, then return
+ // the error wrapped in a type that uses the newly-redacted string
+ // while also allowing Unwrap()-ing to the inner error type(s).
+ s := root.Error()
+ for _, toRedact := range redactStrings {
+ s = strings.ReplaceAll(s, toRedact, redactString(toRedact))
+ }
+ return &redactedErr{msg: s, inner: root}
+}
diff --git a/taildrop/taildrop_test.go b/taildrop/taildrop_test.go
new file mode 100644
index 0000000000000..29c88e8c82974
--- /dev/null
+++ b/taildrop/taildrop_test.go
@@ -0,0 +1,155 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package taildrop
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+)
+
+// Tests "foo.jpg.deleted" marks (for Windows).
+func TestDeletedMarkers(t *testing.T) {
+ dir := t.TempDir()
+ h := &Handler{Dir: dir}
+
+ nothingWaiting := func() {
+ t.Helper()
+ h.knownEmpty.Store(false)
+ if h.HasFilesWaiting() {
+ t.Fatal("unexpected files waiting")
+ }
+ }
+ touch := func(base string) {
+ t.Helper()
+ if err := touchFile(filepath.Join(dir, base)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ wantEmptyTempDir := func() {
+ t.Helper()
+ if fis, err := os.ReadDir(dir); err != nil {
+ t.Fatal(err)
+ } else if len(fis) > 0 && runtime.GOOS != "windows" {
+ for _, fi := range fis {
+ t.Errorf("unexpected file in tempdir: %q", fi.Name())
+ }
+ }
+ }
+
+ nothingWaiting()
+ wantEmptyTempDir()
+
+ touch("foo.jpg.deleted")
+ nothingWaiting()
+ wantEmptyTempDir()
+
+ touch("foo.jpg.deleted")
+ touch("foo.jpg")
+ nothingWaiting()
+ wantEmptyTempDir()
+
+ touch("foo.jpg.deleted")
+ touch("foo.jpg")
+ wf, err := h.WaitingFiles()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(wf) != 0 {
+ t.Fatalf("WaitingFiles = %d; want 0", len(wf))
+ }
+ wantEmptyTempDir()
+
+ touch("foo.jpg.deleted")
+ touch("foo.jpg")
+ if rc, _, err := h.OpenFile("foo.jpg"); err == nil {
+ rc.Close()
+ t.Fatal("unexpected foo.jpg open")
+ }
+ wantEmptyTempDir()
+
+ // And verify basics still work in non-deleted cases.
+ touch("foo.jpg")
+ touch("bar.jpg.deleted")
+ if wf, err := h.WaitingFiles(); err != nil {
+ t.Error(err)
+ } else if len(wf) != 1 {
+ t.Errorf("WaitingFiles = %d; want 1", len(wf))
+ } else if wf[0].Name != "foo.jpg" {
+ t.Errorf("unexpected waiting file %+v", wf[0])
+ }
+ if rc, _, err := h.OpenFile("foo.jpg"); err != nil {
+ t.Fatal(err)
+ } else {
+ rc.Close()
+ }
+}
+
+func TestRedactErr(t *testing.T) {
+ testCases := []struct {
+ name string
+ err func() error
+ want string
+ }{
+ {
+ name: "PathError",
+ err: func() error {
+ return &os.PathError{
+ Op: "open",
+ Path: "/tmp/sensitive.txt",
+ Err: fs.ErrNotExist,
+ }
+ },
+ want: `open redacted.41360718: file does not exist`,
+ },
+ {
+ name: "LinkError",
+ err: func() error {
+ return &os.LinkError{
+ Op: "symlink",
+ Old: "/tmp/sensitive.txt",
+ New: "/tmp/othersensitive.txt",
+ Err: fs.ErrNotExist,
+ }
+ },
+ want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
+ },
+ {
+ name: "something else",
+ err: func() error { return errors.New("i am another error type") },
+ want: `i am another error type`,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // For debugging
+ var i int
+ for err := tc.err(); err != nil; err = errors.Unwrap(err) {
+ t.Logf("%d: %T @ %p", i, err, err)
+ i++
+ }
+
+ t.Run("Root", func(t *testing.T) {
+ got := redactErr(tc.err()).Error()
+ if got != tc.want {
+ t.Errorf("err = %q; want %q", got, tc.want)
+ }
+ })
+ t.Run("Wrapped", func(t *testing.T) {
+ wrapped := fmt.Errorf("wrapped error: %w", tc.err())
+ want := "wrapped error: " + tc.want
+
+ got := redactErr(wrapped).Error()
+ if got != want {
+ t.Errorf("err = %q; want %q", got, want)
+ }
+ })
+ })
+ }
+}
diff --git a/tool/gocross/autoflags.go b/tool/gocross/autoflags.go
index 45bcb972d77bf..10c977b32459a 100644
--- a/tool/gocross/autoflags.go
+++ b/tool/gocross/autoflags.go
@@ -165,14 +165,18 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
// Finished computing the settings we want. Generate the modified
// commandline and environment modifications.
newArgv = append(newArgv, argv[:2]...) // Program name and `go` tool subcommand
+
+ filteredArgvPostSubcmd, originalTags := extractTags(argv[1], argv[2:])
+
newArgv = append(newArgv, buildFlags...)
+ tags = append(tags, originalTags...)
if len(tags) > 0 {
newArgv = append(newArgv, fmt.Sprintf("-tags=%s", strings.Join(tags, ",")))
}
if len(ldflags) > 0 {
newArgv = append(newArgv, "-ldflags", strings.Join(ldflags, " "))
}
- newArgv = append(newArgv, argv[2:]...)
+ newArgv = append(newArgv, filteredArgvPostSubcmd...)
env.Set("GOOS", targetOS)
env.Set("GOARCH", targetArch)
@@ -194,6 +198,48 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
return newArgv, env, nil
}
+// extractTags parses out "-tags=foo,bar" (or double hyphen or "-tags",
+// "foo,bar") in its various forms and returns v filtered to remove the 0, 1 or
+// 2 build tag elements, then the tags parsed, split on commas ("foo", "bar").
+func extractTags(gocmd string, v []string) (filtered, tags []string) {
+ for len(v) > 0 {
+ e := v[0]
+ if strings.HasPrefix(e, "--tags=") {
+ e = e[1:] // remove one of the hyphens for the next line
+ }
+ if suf, ok := strings.CutPrefix(e, "-tags="); ok {
+ v = v[1:]
+ if suf != "" {
+ tags = strings.Split(suf, ",")
+ }
+ continue
+ }
+ if e == "-tags" || e == "--tags" {
+ v = v[1:]
+ if len(v) > 0 {
+ tagStr := v[0]
+ v = v[1:]
+ if tagStr != "" {
+ tags = strings.Split(tagStr, ",")
+ }
+ }
+ continue
+ }
+ if gocmd == "run" && !strings.HasPrefix(e, "-") {
+ // go run can include arguments to pass to the program
+ // being run. They all appear after the name of the
+ // package or Go file to run, so when we hit the first
+ // non-flag positional argument, stop extracting tags and
+ // wrap up.
+ filtered = append(filtered, v...)
+ break
+ }
+ filtered = append(filtered, e)
+ v = v[1:]
+ }
+ return filtered, tags
+}
+
// boolStr formats v as a string 0 or 1.
// Used because CGO_ENABLED doesn't strconv.ParseBool, so
// strconv.FormatBool breaks.
diff --git a/tool/gocross/autoflags_test.go b/tool/gocross/autoflags_test.go
index c8007c40df20b..e5202efc9cfa0 100644
--- a/tool/gocross/autoflags_test.go
+++ b/tool/gocross/autoflags_test.go
@@ -416,6 +416,33 @@ TS_LINK_FAIL_REFLECT=0 (was )`,
"./cmd/tailcontrol",
},
},
+ {
+ name: "linux_amd64_to_linux_amd64_go_run_tags",
+
+ argv: []string{"go", "run", "./cmd/mkctr", "--tags=foo"},
+ goroot: "/goroot",
+ nativeGOOS: "linux",
+ nativeGOARCH: "amd64",
+
+ envDiff: `CC=cc (was )
+CGO_CFLAGS=-O3 -std=gnu11 (was )
+CGO_ENABLED=1 (was )
+CGO_LDFLAGS= (was )
+GOARCH=amd64 (was )
+GOARM=5 (was )
+GOMIPS=softfloat (was )
+GOOS=linux (was )
+GOROOT=/goroot (was )
+TS_LINK_FAIL_REFLECT=0 (was )`,
+ wantArgv: []string{
+ "go", "run",
+ "-trimpath",
+ "-tags=tailscale_go,osusergo,netgo",
+ "-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
+ "./cmd/mkctr",
+ "--tags=foo",
+ },
+ },
}
for _, test := range tests {
@@ -437,3 +464,82 @@ TS_LINK_FAIL_REFLECT=0 (was )`,
})
}
}
+
+func TestExtractTags(t *testing.T) {
+ s := func(ss ...string) []string { return ss }
+ tests := []struct {
+ name string
+ cmd string
+ in []string
+ filt []string // want filtered
+ tags []string // want tags
+ }{
+ {
+ name: "one_hyphen_tags",
+ cmd: "build",
+ in: s("foo", "-tags=a,b", "bar"),
+ filt: s("foo", "bar"),
+ tags: s("a", "b"),
+ },
+ {
+ name: "two_hyphen_tags",
+ cmd: "build",
+ in: s("foo", "--tags=a,b", "bar"),
+ filt: s("foo", "bar"),
+ tags: s("a", "b"),
+ },
+ {
+ name: "one_hypen_separate_arg",
+ cmd: "build",
+ in: s("foo", "-tags", "a,b", "bar"),
+ filt: s("foo", "bar"),
+ tags: s("a", "b"),
+ },
+ {
+ name: "two_hypen_separate_arg",
+ cmd: "build",
+ in: s("foo", "--tags", "a,b", "bar"),
+ filt: s("foo", "bar"),
+ tags: s("a", "b"),
+ },
+ {
+ name: "equal_empty",
+ cmd: "build",
+ in: s("foo", "--tags=", "bar"),
+ filt: s("foo", "bar"),
+ tags: s(),
+ },
+ {
+ name: "arg_empty",
+ cmd: "build",
+ in: s("foo", "--tags", "", "bar"),
+ filt: s("foo", "bar"),
+ tags: s(),
+ },
+ {
+ name: "arg_empty_truncated",
+ cmd: "build",
+ in: s("foo", "--tags"),
+ filt: s("foo"),
+ tags: s(),
+ },
+ {
+ name: "go_run_with_program_tags",
+ cmd: "run",
+ in: s("--foo", "--tags", "bar", "my/package/name", "--tags", "qux"),
+ filt: s("--foo", "my/package/name", "--tags", "qux"),
+ tags: s("bar"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filt, tags := extractTags(tt.cmd, tt.in)
+ if !reflect.DeepEqual(filt, tt.filt) {
+ t.Errorf("extractTags(%q, %q) filtered = %q; want %q", tt.cmd, tt.in, filt, tt.filt)
+ }
+ if !reflect.DeepEqual(tags, tt.tags) {
+ t.Errorf("extractTags(%q, %q) tags = %q; want %q", tt.cmd, tt.in, tags, tt.tags)
+ }
+ })
+ }
+}
diff --git a/tool/gocross/gocross-wrapper.sh b/tool/gocross/gocross-wrapper.sh
index fc4a942252b6c..6a9104dd17736 100755
--- a/tool/gocross/gocross-wrapper.sh
+++ b/tool/gocross/gocross-wrapper.sh
@@ -32,7 +32,10 @@ if [[ -d "$toolchain" ]]; then
# A toolchain exists, but is it recent enough to compile gocross? If not,
# wipe it out so that the next if block fetches a usable one.
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
- have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
+ have_go_minor=""
+ if [[ -f "$toolchain/VERSION" ]]; then
+ have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
+ fi
# Shortly before stable releases, we run release candidate
# toolchains, which have a non-numeric suffix on the version
# number. Remove the rc qualifier, we just care about the minor
diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go
index f49f2bd935daa..a82a60722776e 100644
--- a/tsnet/tsnet.go
+++ b/tsnet/tsnet.go
@@ -542,7 +542,7 @@ func (s *Server) start() (reterr error) {
if s.Ephemeral {
loginFlags = controlclient.LoginEphemeral
}
- lb, err := ipnlocal.NewLocalBackend(logf, s.logid, sys, loginFlags)
+ lb, err := ipnlocal.NewLocalBackend(logf, s.logid, sys, loginFlags|controlclient.LocalBackendStartKeyOSNeutral)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -926,7 +926,7 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
// flow here instead of CheckFunnelAccess to allow the user to turn on Funnel
// if not already on. Specifically when running from a terminal.
// See cli.serveEnv.verifyFunnelEnabled.
- if err := ipn.CheckFunnelAccess(uint16(port), st.Self.Capabilities); err != nil {
+ if err := ipn.CheckFunnelAccess(uint16(port), st.Self); err != nil {
return nil, err
}
diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go
index 0c215ee5b4a12..981e8c9b22018 100644
--- a/tsnet/tsnet_test.go
+++ b/tsnet/tsnet_test.go
@@ -23,6 +23,7 @@ import (
"net/netip"
"os"
"path/filepath"
+ "reflect"
"strings"
"sync"
"testing"
@@ -470,6 +471,57 @@ func TestListenerCleanup(t *testing.T) {
}
}
+// tests https://github.com/tailscale/tailscale/issues/6973 -- that we can start a tsnet server,
+// stop it, and restart it, even on Windows.
+func TestStartStopStartGetsSameIP(t *testing.T) {
+ controlURL := startControl(t)
+
+ tmp := t.TempDir()
+ tmps1 := filepath.Join(tmp, "s1")
+ os.MkdirAll(tmps1, 0755)
+
+ newServer := func() *Server {
+ return &Server{
+ Dir: tmps1,
+ ControlURL: controlURL,
+ Hostname: "s1",
+ Logf: logger.TestLogger(t),
+ }
+ }
+ s1 := newServer()
+ defer s1.Close()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ s1status, err := s1.Up(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ firstIPs := s1status.TailscaleIPs
+ t.Logf("IPs: %v", firstIPs)
+
+ if err := s1.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+
+ s2 := newServer()
+ defer s2.Close()
+
+ s2status, err := s2.Up(ctx)
+ if err != nil {
+ t.Fatalf("second Up: %v", err)
+ }
+
+ secondIPs := s2status.TailscaleIPs
+ t.Logf("IPs: %v", secondIPs)
+
+ if !reflect.DeepEqual(firstIPs, secondIPs) {
+ t.Fatalf("got %v but later %v", firstIPs, secondIPs)
+ }
+}
+
func TestFunnel(t *testing.T) {
ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dialCancel()
diff --git a/tstest/deptest/deptest.go b/tstest/deptest/deptest.go
index 6c8614593fc5f..8ea39e2438e77 100644
--- a/tstest/deptest/deptest.go
+++ b/tstest/deptest/deptest.go
@@ -10,7 +10,10 @@ import (
"encoding/json"
"os"
"os/exec"
+ "path/filepath"
+ "regexp"
"runtime"
+ "strings"
"testing"
)
@@ -52,5 +55,30 @@ func (c DepChecker) Check(t *testing.T) {
}
}
t.Logf("got %d dependencies", len(res.Deps))
+}
+
+// ImportAliasCheck checks that all packages are imported according to Tailscale
+// conventions.
+func ImportAliasCheck(t testing.TB, relDir string) {
+ dir, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ dir = filepath.Join(dir, relDir)
+ cmd := exec.Command("git", "grep", "-n", "-F", `"golang.org/x/exp/`)
+ cmd.Dir = dir
+ matches, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Logf("ignoring error: %v, %s", err, matches)
+ return
+ }
+ badRx := regexp.MustCompile(`^([^:]+:\d+):\s+"golang.org/x/exp/(slices|maps)"`)
+ if s := strings.TrimSpace(string(matches)); s != "" {
+ for _, line := range strings.Split(s, "\n") {
+ if m := badRx.FindStringSubmatch(line); m != nil {
+ t.Errorf("%s: the x/exp/%s package should be imported as x%s", m[1], m[2], m[2])
+ }
+ }
+ }
}
diff --git a/tstest/deptest/deptest_test.go b/tstest/deptest/deptest_test.go
new file mode 100644
index 0000000000000..ebafa56849efb
--- /dev/null
+++ b/tstest/deptest/deptest_test.go
@@ -0,0 +1,10 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package deptest
+
+import "testing"
+
+func TestImports(t *testing.T) {
+ ImportAliasCheck(t, "../../")
+}
diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go
index 415ee8a94f5a3..cec6d469ac6fe 100644
--- a/tstest/integration/integration_test.go
+++ b/tstest/integration/integration_test.go
@@ -76,7 +76,7 @@ func TestOneNodeUpNoAuth(t *testing.T) {
n1.AwaitResponding()
n1.MustUp()
- t.Logf("Got IP: %v", n1.AwaitIP())
+ t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
@@ -130,7 +130,7 @@ func TestControlKnobs(t *testing.T) {
n1.AwaitResponding()
n1.MustUp()
- t.Logf("Got IP: %v", n1.AwaitIP())
+ t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
cmd := n1.Tailscale("debug", "control-knobs")
@@ -212,7 +212,7 @@ func TestStateSavedOnStart(t *testing.T) {
n1.AwaitResponding()
n1.MustUp()
- t.Logf("Got IP: %v", n1.AwaitIP())
+ t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
p1 := n1.diskPrefs()
@@ -271,7 +271,7 @@ func TestOneNodeUpAuth(t *testing.T) {
if err := cmd.Run(); err != nil {
t.Fatalf("up: %v", err)
}
- t.Logf("Got IP: %v", n1.AwaitIP())
+ t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
@@ -574,7 +574,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
// Come up the first time.
n1.MustUp()
- ip1 := n1.AwaitIP()
+ ip1 := n1.AwaitIP4()
n1.AwaitRunning()
// Then bring it down and stop the daemon.
@@ -590,7 +590,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
t.Fatalf("after restart, state = %q; want %q", got, want)
}
- ip2 := n1.AwaitIP()
+ ip2 := n1.AwaitIP4()
if ip1 != ip2 {
t.Errorf("IPs different: %q vs %q", ip1, ip2)
}
@@ -615,7 +615,7 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
n1.AwaitResponding()
n1.MustUp("--unattended")
- t.Logf("Got IP: %v", n1.AwaitIP())
+ t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
@@ -625,111 +625,128 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
// tries to do bi-directional pings between them.
func TestNATPing(t *testing.T) {
t.Parallel()
- env := newTestEnv(t)
- registerNode := func() (*testNode, key.NodePublic) {
- n := newTestNode(t, env)
- n.StartDaemon()
- n.AwaitListening()
- n.MustUp()
- n.AwaitRunning()
- k := n.MustStatus().Self.PublicKey
- return n, k
- }
- n1, k1 := registerNode()
- n2, k2 := registerNode()
-
- n1IP := n1.AwaitIP()
- n2IP := n2.AwaitIP()
-
- n1ExternalIP := netip.MustParseAddr("100.64.1.1")
- n2ExternalIP := netip.MustParseAddr("100.64.2.1")
-
- tests := []struct {
- name string
- pairs []testcontrol.MasqueradePair
- n1SeesN2IP netip.Addr
- n2SeesN1IP netip.Addr
- }{
- {
- name: "no_nat",
- n1SeesN2IP: n2IP,
- n2SeesN1IP: n1IP,
- },
- {
- name: "n1_has_external_ip",
- pairs: []testcontrol.MasqueradePair{
- {
- Node: k1,
- Peer: k2,
- NodeMasqueradesAs: n1ExternalIP,
- },
+ for _, v6 := range []bool{false, true} {
+ env := newTestEnv(t)
+ registerNode := func() (*testNode, key.NodePublic) {
+ n := newTestNode(t, env)
+ n.StartDaemon()
+ n.AwaitListening()
+ n.MustUp()
+ n.AwaitRunning()
+ k := n.MustStatus().Self.PublicKey
+ return n, k
+ }
+ n1, k1 := registerNode()
+ n2, k2 := registerNode()
+
+ var n1IP, n2IP netip.Addr
+ if v6 {
+ n1IP = n1.AwaitIP6()
+ n2IP = n2.AwaitIP6()
+ } else {
+ n1IP = n1.AwaitIP4()
+ n2IP = n2.AwaitIP4()
+ }
+
+ n1ExternalIP := netip.MustParseAddr("100.64.1.1")
+ n2ExternalIP := netip.MustParseAddr("100.64.2.1")
+ if v6 {
+ n1ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1a")
+ n2ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1b")
+ }
+
+ tests := []struct {
+ name string
+ pairs []testcontrol.MasqueradePair
+ n1SeesN2IP netip.Addr
+ n2SeesN1IP netip.Addr
+ }{
+ {
+ name: "no_nat",
+ n1SeesN2IP: n2IP,
+ n2SeesN1IP: n1IP,
},
- n1SeesN2IP: n2IP,
- n2SeesN1IP: n1ExternalIP,
- },
- {
- name: "n2_has_external_ip",
- pairs: []testcontrol.MasqueradePair{
- {
- Node: k2,
- Peer: k1,
- NodeMasqueradesAs: n2ExternalIP,
+ {
+ name: "n1_has_external_ip",
+ pairs: []testcontrol.MasqueradePair{
+ {
+ Node: k1,
+ Peer: k2,
+ NodeMasqueradesAs: n1ExternalIP,
+ },
},
+ n1SeesN2IP: n2IP,
+ n2SeesN1IP: n1ExternalIP,
},
- n1SeesN2IP: n2ExternalIP,
- n2SeesN1IP: n1IP,
- },
- {
- name: "both_have_external_ips",
- pairs: []testcontrol.MasqueradePair{
- {
- Node: k1,
- Peer: k2,
- NodeMasqueradesAs: n1ExternalIP,
+ {
+ name: "n2_has_external_ip",
+ pairs: []testcontrol.MasqueradePair{
+ {
+ Node: k2,
+ Peer: k1,
+ NodeMasqueradesAs: n2ExternalIP,
+ },
},
- {
- Node: k2,
- Peer: k1,
- NodeMasqueradesAs: n2ExternalIP,
+ n1SeesN2IP: n2ExternalIP,
+ n2SeesN1IP: n1IP,
+ },
+ {
+ name: "both_have_external_ips",
+ pairs: []testcontrol.MasqueradePair{
+ {
+ Node: k1,
+ Peer: k2,
+ NodeMasqueradesAs: n1ExternalIP,
+ },
+ {
+ Node: k2,
+ Peer: k1,
+ NodeMasqueradesAs: n2ExternalIP,
+ },
},
+ n1SeesN2IP: n2ExternalIP,
+ n2SeesN1IP: n1ExternalIP,
},
- n1SeesN2IP: n2ExternalIP,
- n2SeesN1IP: n1ExternalIP,
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- env.Control.SetMasqueradeAddresses(tc.pairs)
-
- s1 := n1.MustStatus()
- n2AsN1Peer := s1.Peer[k2]
- if got := n2AsN1Peer.TailscaleIPs[0]; got != tc.n1SeesN2IP {
- t.Fatalf("n1 sees n2 as %v; want %v", got, tc.n1SeesN2IP)
- }
-
- s2 := n2.MustStatus()
- n1AsN2Peer := s2.Peer[k1]
- if got := n1AsN2Peer.TailscaleIPs[0]; got != tc.n2SeesN1IP {
- t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP)
- }
-
- if err := n1.Tailscale("ping", tc.n1SeesN2IP.String()).Run(); err != nil {
- t.Fatal(err)
- }
-
- if err := n1.Tailscale("ping", "-peerapi", tc.n1SeesN2IP.String()).Run(); err != nil {
- t.Fatal(err)
- }
-
- if err := n2.Tailscale("ping", tc.n2SeesN1IP.String()).Run(); err != nil {
- t.Fatal(err)
- }
+ }
- if err := n2.Tailscale("ping", "-peerapi", tc.n2SeesN1IP.String()).Run(); err != nil {
- t.Fatal(err)
- }
- })
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("v6=%t/%v", v6, tc.name), func(t *testing.T) {
+ env.Control.SetMasqueradeAddresses(tc.pairs)
+
+ ipIdx := 0
+ if v6 {
+ ipIdx = 1
+ }
+
+ s1 := n1.MustStatus()
+ n2AsN1Peer := s1.Peer[k2]
+ if got := n2AsN1Peer.TailscaleIPs[ipIdx]; got != tc.n1SeesN2IP {
+ t.Fatalf("n1 sees n2 as %v; want %v", got, tc.n1SeesN2IP)
+ }
+
+ s2 := n2.MustStatus()
+ n1AsN2Peer := s2.Peer[k1]
+ if got := n1AsN2Peer.TailscaleIPs[ipIdx]; got != tc.n2SeesN1IP {
+ t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP)
+ }
+
+ if err := n1.Tailscale("ping", tc.n1SeesN2IP.String()).Run(); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := n1.Tailscale("ping", "-peerapi", tc.n1SeesN2IP.String()).Run(); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := n2.Tailscale("ping", tc.n2SeesN1IP.String()).Run(); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := n2.Tailscale("ping", "-peerapi", tc.n2SeesN1IP.String()).Run(); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
}
}
@@ -743,7 +760,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) {
nodes[i].StartDaemon()
nodes[i].AwaitResponding()
nodes[i].MustUp()
- nodes[i].AwaitIP()
+ nodes[i].AwaitIP4()
nodes[i].AwaitRunning()
}
expectedPeers := len(nodes) - 1
@@ -758,7 +775,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) {
if err := tstest.WaitFor(20*time.Second, func() error {
return nodes[i].Ping(nodes[j])
}); err != nil {
- t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP(), nodes[j].AwaitIP(), err)
+ t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP4(), nodes[j].AwaitIP4(), err)
}
}
}
@@ -783,7 +800,7 @@ func TestLogoutRemovesAllPeers(t *testing.T) {
nodes[0].MustUp() // This will create a new node
expectedPeers++
- nodes[0].AwaitIP()
+ nodes[0].AwaitIP4()
wantNode0PeerCount(expectedPeers) // all existing peers and the new node
}
@@ -1107,8 +1124,8 @@ func (n *testNode) MustLogOut() {
func (n *testNode) Ping(otherNode *testNode) error {
t := n.env.t
- ip := otherNode.AwaitIP().String()
- t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP())
+ ip := otherNode.AwaitIP4().String()
+ t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP4())
return n.Tailscale("ping", ip).Run()
}
@@ -1162,14 +1179,22 @@ func (n *testNode) AwaitIPs() []netip.Addr {
return addrs
}
-// AwaitIP returns the IP address of n.
-func (n *testNode) AwaitIP() netip.Addr {
+// AwaitIP4 returns the IPv4 address of n.
+func (n *testNode) AwaitIP4() netip.Addr {
t := n.env.t
t.Helper()
ips := n.AwaitIPs()
return ips[0]
}
+// AwaitIP6 returns the IPv6 address of n.
+func (n *testNode) AwaitIP6() netip.Addr {
+ t := n.env.t
+ t.Helper()
+ ips := n.AwaitIPs()
+ return ips[1]
+}
+
// AwaitRunning waits for n to reach the IPN state "Running".
func (n *testNode) AwaitRunning() {
t := n.env.t
diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go
index 08c18bb7bb5cc..cea8e5749fc45 100644
--- a/tstest/integration/tailscaled_deps_test_windows.go
+++ b/tstest/integration/tailscaled_deps_test_windows.go
@@ -53,6 +53,7 @@ import (
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osdiag"
_ "tailscale.com/util/osshare"
+ _ "tailscale.com/util/syspolicy"
_ "tailscale.com/util/winutil"
_ "tailscale.com/version"
_ "tailscale.com/version/distro"
diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go
index 4a2f08921794e..981c5f8c28b6d 100644
--- a/tstest/integration/testcontrol/testcontrol.go
+++ b/tstest/integration/testcontrol/testcontrol.go
@@ -900,8 +900,13 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
peerAddress := s.masquerades[p.Key][node.Key]
s.mu.Unlock()
if peerAddress.IsValid() {
- p.Addresses[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
- p.AllowedIPs[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
+ if peerAddress.Is6() {
+ p.Addresses[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
+ p.AllowedIPs[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
+ } else {
+ p.Addresses[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
+ p.AllowedIPs[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
+ }
}
res.Peers = append(res.Peers, p)
}
@@ -1035,7 +1040,7 @@ func (s *Server) encode(mkey key.MachinePublic, compress bool, v any) (b []byte,
//
// Two types of IPv6 endpoints are considered invalid: link-local
// addresses, and anything with a zone.
-func filterInvalidIPv6Endpoints(eps []string) []string {
+func filterInvalidIPv6Endpoints(eps []netip.AddrPort) []netip.AddrPort {
clean := eps[:0]
for _, ep := range eps {
if keepClientEndpoint(ep) {
@@ -1045,13 +1050,7 @@ func filterInvalidIPv6Endpoints(eps []string) []string {
return clean
}
-func keepClientEndpoint(ep string) bool {
- ipp, err := netip.ParseAddrPort(ep)
- if err != nil {
- // Shouldn't have made it this far if we unmarshalled
- // the incoming JSON response.
- return false
- }
+func keepClientEndpoint(ipp netip.AddrPort) bool {
ip := ipp.Addr()
if ip.Zone() != "" {
return false
diff --git a/tstest/resource.go b/tstest/resource.go
index ebe6af0548cff..4ca9fc9289fd7 100644
--- a/tstest/resource.go
+++ b/tstest/resource.go
@@ -13,8 +13,19 @@ import (
"github.com/google/go-cmp/cmp"
)
+// ResourceCheck takes a snapshot of the current goroutines and registers a
+// cleanup on tb to verify that after the rest, all goroutines created by the
+// test go away. (well, at least that the count matches. Maybe in the future it
+// can look at specific routines).
+//
+// It panics if called from a parallel test.
func ResourceCheck(tb testing.TB) {
tb.Helper()
+
+ // Set an environment variable (anything at all) just for the
+ // side effect of tb.Setenv panicking if we're in a parallel test.
+ tb.Setenv("TS_CHECKING_RESOURCES", "1")
+
startN, startStacks := goroutines()
tb.Cleanup(func() {
if tb.Failed() {
diff --git a/types/ipproto/ipproto.go b/types/ipproto/ipproto.go
index a6b1e0c48a1c6..8c20d51e4cdad 100644
--- a/types/ipproto/ipproto.go
+++ b/types/ipproto/ipproto.go
@@ -6,6 +6,26 @@ package ipproto
import "fmt"
+// IPProtoVersion describes the IP address version.
+type IPProtoVersion uint8
+
+// Valid IPProtoVersion values.
+const (
+ IPProtoVersion4 = 4
+ IPProtoVersion6 = 6
+)
+
+func (p IPProtoVersion) String() string {
+ switch p {
+ case IPProtoVersion4:
+ return "IPv4"
+ case IPProtoVersion6:
+ return "IPv6"
+ default:
+ return fmt.Sprintf("IPProtoVersion-%d", int(p))
+ }
+}
+
// Proto is an IP subprotocol as defined by the IANA protocol
// numbers list
// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),
diff --git a/types/key/node.go b/types/key/node.go
index 4cb72879861aa..6142bf68ac4b5 100644
--- a/types/key/node.go
+++ b/types/key/node.go
@@ -310,7 +310,7 @@ func (k NodePublic) UntypedHexString() string {
return hex.EncodeToString(k.k[:])
}
-// String returns the output of MarshalText as a string.
+// String returns k as a hex-encoded string with a type prefix.
func (k NodePublic) String() string {
bs, err := k.MarshalText()
if err != nil {
@@ -319,17 +319,20 @@ func (k NodePublic) String() string {
return string(bs)
}
-// AppendText implements encoding.TextAppender.
+// AppendText implements encoding.TextAppender. It appends a typed prefix
+// followed by hex encoded represtation of k to b.
func (k NodePublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, nodePublicHexPrefix, k.k[:]), nil
}
-// MarshalText implements encoding.TextMarshaler.
+// MarshalText implements encoding.TextMarshaler. It returns a typed prefix
+// followed by a hex encoded representation of k.
func (k NodePublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
-// MarshalText implements encoding.TextUnmarshaler.
+// UnmarshalText implements encoding.TextUnmarshaler. It expects a typed prefix
+// followed by a hex encoded representation of k.
func (k *NodePublic) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix))
}
diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go
index 9e2c1540666c2..7511544a57cfc 100644
--- a/types/netmap/netmap.go
+++ b/types/netmap/netmap.go
@@ -267,7 +267,7 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
ep := make([]string, p.Endpoints().Len())
for i := range ep {
- e := p.Endpoints().At(i)
+ e := p.Endpoints().At(i).String()
// Align vertically on the ':' between IP and port
colon := strings.IndexByte(e, ':')
spaces := 0
diff --git a/types/netmap/netmap_test.go b/types/netmap/netmap_test.go
index 90f6e26a700a6..e7e2d19575c44 100644
--- a/types/netmap/netmap_test.go
+++ b/types/netmap/netmap_test.go
@@ -42,6 +42,14 @@ func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
return nv
}
+func eps(s ...string) []netip.AddrPort {
+ var eps []netip.AddrPort
+ for _, ep := range s {
+ eps = append(eps, netip.MustParseAddrPort(ep))
+ }
+ return eps
+}
+
func TestNetworkMapConcise(t *testing.T) {
for _, tt := range []struct {
name string
@@ -56,12 +64,12 @@ func TestNetworkMapConcise(t *testing.T) {
{
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
{
Key: testNodeKey(3),
DERP: "127.3.3.40:4",
- Endpoints: []string{"10.2.0.100:12", "10.1.0.100:12345"},
+ Endpoints: eps("10.2.0.100:12", "10.1.0.100:12345"),
},
}),
},
@@ -95,7 +103,7 @@ func TestConciseDiffFrom(t *testing.T) {
{
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -105,7 +113,7 @@ func TestConciseDiffFrom(t *testing.T) {
{
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -119,7 +127,7 @@ func TestConciseDiffFrom(t *testing.T) {
{
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -129,7 +137,7 @@ func TestConciseDiffFrom(t *testing.T) {
{
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -144,7 +152,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -155,19 +163,19 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 1,
Key: testNodeKey(1),
DERP: "127.3.3.40:1",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
{
ID: 3,
Key: testNodeKey(3),
DERP: "127.3.3.40:3",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -182,19 +190,19 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 1,
Key: testNodeKey(1),
DERP: "127.3.3.40:1",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
{
ID: 3,
Key: testNodeKey(3),
DERP: "127.3.3.40:3",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -205,7 +213,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
+ Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
},
}),
},
@@ -220,7 +228,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
+ Endpoints: eps("192.168.0.100:12", "1.1.1.1:1"),
},
}),
},
@@ -231,7 +239,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"},
+ Endpoints: eps("192.168.0.100:12", "1.1.1.1:2"),
},
}),
},
@@ -246,7 +254,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
+ Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"),
DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
},
@@ -259,7 +267,7 @@ func TestConciseDiffFrom(t *testing.T) {
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
- Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
+ Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"),
DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
},
diff --git a/types/netmap/nodemut.go b/types/netmap/nodemut.go
index 919fe0492ba07..31734aa725e9b 100644
--- a/types/netmap/nodemut.go
+++ b/types/netmap/nodemut.go
@@ -45,11 +45,7 @@ type NodeMutationEndpoints struct {
}
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
+ n.Endpoints = slices.Clone(m.Endpoints)
}
// NodeMutationOnline is a NodeMutation that says a node is now online or
@@ -105,15 +101,7 @@ func NodeMutationsFromPatch(p *tailcfg.PeerChange) (_ []NodeMutation, ok bool) {
case "DERPRegion":
ret = append(ret, NodeMutationDERPHome{mutatingNodeID(p.NodeID), p.DERPRegion})
case "Endpoints":
- eps := make([]netip.AddrPort, len(p.Endpoints))
- for i, epStr := range p.Endpoints {
- var err error
- eps[i], err = netip.ParseAddrPort(epStr)
- if err != nil {
- return nil, false
- }
- }
- ret = append(ret, NodeMutationEndpoints{mutatingNodeID(p.NodeID), eps})
+ ret = append(ret, NodeMutationEndpoints{mutatingNodeID(p.NodeID), slices.Clone(p.Endpoints)})
case "Online":
ret = append(ret, NodeMutationOnline{mutatingNodeID(p.NodeID), *p.Online})
case "LastSeen":
diff --git a/types/netmap/nodemut_test.go b/types/netmap/nodemut_test.go
index d126493cc5b60..f11a303af8f99 100644
--- a/types/netmap/nodemut_test.go
+++ b/types/netmap/nodemut_test.go
@@ -91,10 +91,10 @@ func TestMutationsFromMapResponse(t *testing.T) {
name: "patch-ep",
mr: fromChanges(&tailcfg.PeerChange{
NodeID: 1,
- Endpoints: []string{"1.2.3.4:567"},
+ Endpoints: eps("1.2.3.4:567"),
}, &tailcfg.PeerChange{
NodeID: 2,
- Endpoints: []string{"8.9.10.11:1234"},
+ Endpoints: eps("8.9.10.11:1234"),
}),
want: muts(
NodeMutationEndpoints{1, []netip.AddrPort{netip.MustParseAddrPort("1.2.3.4:567")}},
diff --git a/util/httpm/httpm_test.go b/util/httpm/httpm_test.go
new file mode 100644
index 0000000000000..77e6309c8807d
--- /dev/null
+++ b/util/httpm/httpm_test.go
@@ -0,0 +1,29 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package httpm
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestUsedConsistently(t *testing.T) {
+ cmd := exec.Command("git", "grep", "-l", "-F", "http.Method")
+ dir, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd.Dir = filepath.Join(dir, "../..")
+ matches, _ := cmd.Output()
+ for _, fn := range strings.Split(strings.TrimSpace(string(matches)), "\n") {
+ switch fn {
+ case "util/httpm/httpm.go", "util/httpm/httpm_test.go":
+ continue
+ }
+ t.Errorf("http.MethodFoo constant used in %s; use httpm.FOO instead", fn)
+ }
+}
diff --git a/util/linuxfw/detector.go b/util/linuxfw/detector.go
new file mode 100644
index 0000000000000..17b47e2b5e3ab
--- /dev/null
+++ b/util/linuxfw/detector.go
@@ -0,0 +1,110 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package linuxfw
+
+import (
+ "tailscale.com/envknob"
+ "tailscale.com/hostinfo"
+ "tailscale.com/types/logger"
+ "tailscale.com/version/distro"
+)
+
+func detectFirewallMode(logf logger.Logf) FirewallMode {
+ if distro.Get() == distro.Gokrazy {
+ // Reduce startup logging on gokrazy. There's no way to do iptables on
+ // gokrazy anyway.
+ logf("GoKrazy should use nftables.")
+ hostinfo.SetFirewallMode("nft-gokrazy")
+ return FirewallModeNfTables
+ }
+
+ envMode := envknob.String("TS_DEBUG_FIREWALL_MODE")
+ // We now use iptables as default and have "auto" and "nftables" as
+ // options for people to test further.
+ switch envMode {
+ case "auto":
+ return pickFirewallModeFromInstalledRules(logf, linuxFWDetector{})
+ case "nftables":
+ logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
+ hostinfo.SetFirewallMode("nft-forced")
+ return FirewallModeNfTables
+ case "iptables":
+ logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
+ hostinfo.SetFirewallMode("ipt-forced")
+ default:
+ logf("default choosing iptables")
+ hostinfo.SetFirewallMode("ipt-default")
+ }
+ return FirewallModeIPTables
+}
+
+// tableDetector abstracts helpers to detect the firewall mode.
+// It is implemented for testing purposes.
+type tableDetector interface {
+ iptDetect() (int, error)
+ nftDetect() (int, error)
+}
+
+type linuxFWDetector struct{}
+
+// iptDetect returns the number of iptables rules in the current namespace.
+func (l linuxFWDetector) iptDetect() (int, error) {
+ return detectIptables()
+}
+
+// nftDetect returns the number of nftables rules in the current namespace.
+func (l linuxFWDetector) nftDetect() (int, error) {
+ return detectNetfilter()
+}
+
+// pickFirewallModeFromInstalledRules returns the firewall mode to use based on
+// the environment and the system's capabilities.
+func pickFirewallModeFromInstalledRules(logf logger.Logf, det tableDetector) FirewallMode {
+ if distro.Get() == distro.Gokrazy {
+ // Reduce startup logging on gokrazy. There's no way to do iptables on
+ // gokrazy anyway.
+ return FirewallModeNfTables
+ }
+ iptAva, nftAva := true, true
+ iptRuleCount, err := det.iptDetect()
+ if err != nil {
+ logf("detect iptables rule: %v", err)
+ iptAva = false
+ }
+ nftRuleCount, err := det.nftDetect()
+ if err != nil {
+ logf("detect nftables rule: %v", err)
+ nftAva = false
+ }
+ logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
+ switch {
+ case nftRuleCount > 0 && iptRuleCount == 0:
+ logf("nftables is currently in use")
+ hostinfo.SetFirewallMode("nft-inuse")
+ return FirewallModeNfTables
+ case iptRuleCount > 0 && nftRuleCount == 0:
+ logf("iptables is currently in use")
+ hostinfo.SetFirewallMode("ipt-inuse")
+ return FirewallModeIPTables
+ case nftAva:
+ // if both iptables and nftables are available but
+ // neither/both are currently used, use nftables.
+ logf("nftables is available")
+ hostinfo.SetFirewallMode("nft")
+ return FirewallModeNfTables
+ case iptAva:
+ logf("iptables is available")
+ hostinfo.SetFirewallMode("ipt")
+ return FirewallModeIPTables
+ default:
+ // if neither iptables nor nftables are available, use iptablesRunner as a dummy
+ // runner which exists but won't do anything. Creating iptablesRunner errors only
+ // if the iptables command is missing or doesn’t support "--version", as long as it
+ // can determine a version then it’ll carry on.
+ hostinfo.SetFirewallMode("ipt-fb")
+ return FirewallModeIPTables
+ }
+}
diff --git a/util/linuxfw/iptables.go b/util/linuxfw/iptables.go
index 3cc612d0330a0..7231c83fe8283 100644
--- a/util/linuxfw/iptables.go
+++ b/util/linuxfw/iptables.go
@@ -23,13 +23,13 @@ func DebugIptables(logf logger.Logf) error {
return nil
}
-// DetectIptables returns the number of iptables rules that are present in the
+// detectIptables returns the number of iptables rules that are present in the
// system, ignoring the default "ACCEPT" rule present in the standard iptables
// chains.
//
// It only returns an error when there is no iptables binary, or when iptables -S
// fails. In all other cases, it returns the number of non-default rules.
-func DetectIptables() (int, error) {
+func detectIptables() (int, error) {
// run "iptables -S" to get the list of rules using iptables
// exec.Command returns an error if the binary is not found
cmd := exec.Command("iptables", "-S")
diff --git a/util/linuxfw/iptables_runner.go b/util/linuxfw/iptables_runner.go
index 14f2fa53634b8..d703190bcac99 100644
--- a/util/linuxfw/iptables_runner.go
+++ b/util/linuxfw/iptables_runner.go
@@ -45,11 +45,11 @@ func checkIP6TablesExists() error {
return nil
}
-// NewIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
+// newIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
// If the underlying iptables library fails to initialize, that error is
// returned. The runner probes for IPv6 support once at initialization time and
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
-func NewIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
+func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return nil, err
@@ -79,12 +79,12 @@ func NewIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
}
-// HasIPV6 returns true if the system supports IPv6.
+// HasIPV6 reports true if the system supports IPv6.
func (i *iptablesRunner) HasIPV6() bool {
return i.v6Available
}
-// HasIPV6NAT returns true if the system supports IPv6 NAT.
+// HasIPV6NAT reports true if the system supports IPv6 NAT.
func (i *iptablesRunner) HasIPV6NAT() bool {
return i.v6NATAvailable
}
@@ -254,6 +254,12 @@ func (i *iptablesRunner) addBase4(tunname string) error {
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
}
+ // Explicitly allow all other inbound traffic to the tun interface
+ args = []string{"-i", tunname, "-j", "ACCEPT"}
+ if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
+ return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
+ }
+
// Forward all traffic from the Tailscale interface, and drop
// traffic to the tailscale interface by default. We use packet
// marks here so both filter/FORWARD and nat/POSTROUTING can match
@@ -291,7 +297,13 @@ func (i *iptablesRunner) addBase6(tunname string) error {
// TODO: only allow traffic from Tailscale's ULA range to come
// from tailscale0.
- args := []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
+ // Explicitly allow all other inbound traffic to the tun interface
+ args := []string{"-i", tunname, "-j", "ACCEPT"}
+ if err := i.ipt6.Append("filter", "ts-input", args...); err != nil {
+ return fmt.Errorf("adding %v in v6/filter/ts-input: %w", args, err)
+ }
+
+ args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
diff --git a/util/linuxfw/iptables_runner_test.go b/util/linuxfw/iptables_runner_test.go
index e294f064b6631..d4c7c95f4143d 100644
--- a/util/linuxfw/iptables_runner_test.go
+++ b/util/linuxfw/iptables_runner_test.go
@@ -261,6 +261,7 @@ func TestAddAndDeleteBase(t *testing.T) {
}
tsRulesCommon := []fakeRule{ // table/chain/rule
+ {"filter", "ts-input", []string{"-i", tunname, "-j", "ACCEPT"}},
{"filter", "ts-forward", []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}},
{"filter", "ts-forward", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}},
{"filter", "ts-forward", []string{"-o", tunname, "-j", "ACCEPT"}},
diff --git a/util/linuxfw/linuxfw_unsupported.go b/util/linuxfw/linuxfw_unsupported.go
index 4c6029af1a7f8..003d4bdffda56 100644
--- a/util/linuxfw/linuxfw_unsupported.go
+++ b/util/linuxfw/linuxfw_unsupported.go
@@ -25,16 +25,16 @@ func DebugNetfilter(logf logger.Logf) error {
}
// DetectNetfilter is not supported on non-Linux platforms.
-func DetectNetfilter() (int, error) {
+func detectNetfilter() (int, error) {
return 0, ErrUnsupported
}
// DebugIptables is not supported on non-Linux platforms.
-func DebugIptables(logf logger.Logf) error {
+func debugIptables(logf logger.Logf) error {
return ErrUnsupported
}
// DetectIptables is not supported on non-Linux platforms.
-func DetectIptables() (int, error) {
+func detectIptables() (int, error) {
return 0, ErrUnsupported
}
diff --git a/util/linuxfw/nftables.go b/util/linuxfw/nftables.go
index afe6dfa6e3292..8bf99a96341c9 100644
--- a/util/linuxfw/nftables.go
+++ b/util/linuxfw/nftables.go
@@ -103,8 +103,8 @@ func DebugNetfilter(logf logger.Logf) error {
return nil
}
-// DetectNetfilter returns the number of nftables rules present in the system.
-func DetectNetfilter() (int, error) {
+// detectNetfilter returns the number of nftables rules present in the system.
+func detectNetfilter() (int, error) {
conn, err := nftables.New()
if err != nil {
return 0, FWModeNotSupportedError{
diff --git a/util/linuxfw/nftables_runner.go b/util/linuxfw/nftables_runner.go
index 9f56c54230270..d87610ddae0a7 100644
--- a/util/linuxfw/nftables_runner.go
+++ b/util/linuxfw/nftables_runner.go
@@ -175,9 +175,67 @@ func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
return nil
}
-// NewNfTablesRunner creates a new nftablesRunner without guaranteeing
+// NetfilterRunner abstracts helpers to run netfilter commands. It is
+// implemented by linuxfw.IPTablesRunner and linuxfw.NfTablesRunner.
+type NetfilterRunner interface {
+ // AddLoopbackRule adds a rule to permit loopback traffic to addr. This rule
+ // is added only if it does not already exist.
+ AddLoopbackRule(addr netip.Addr) error
+
+ // DelLoopbackRule removes the rule added by AddLoopbackRule.
+ DelLoopbackRule(addr netip.Addr) error
+
+ // AddHooks adds rules to conventional chains like "FORWARD", "INPUT" and
+ // "POSTROUTING" to jump from those chains to tailscale chains.
+ AddHooks() error
+
+ // DelHooks deletes rules added by AddHooks.
+ DelHooks(logf logger.Logf) error
+
+ // AddChains creates custom Tailscale chains.
+ AddChains() error
+
+ // DelChains removes chains added by AddChains.
+ DelChains() error
+
+ // AddBase adds rules reused by different other rules.
+ AddBase(tunname string) error
+
+ // DelBase removes rules added by AddBase.
+ DelBase() error
+
+ // AddSNATRule adds the netfilter rule to SNAT incoming traffic over
+ // the Tailscale interface destined for local subnets. An error is
+ // returned if the rule already exists.
+ AddSNATRule() error
+
+ // DelSNATRule removes the rule added by AddSNATRule.
+ DelSNATRule() error
+
+ // HasIPV6 reports true if the system supports IPv6.
+ HasIPV6() bool
+
+ // HasIPV6NAT reports true if the system supports IPv6 NAT.
+ HasIPV6NAT() bool
+}
+
+// New creates a NetfilterRunner using either nftables or iptables.
+// As nftables is still experimental, iptables will be used unless TS_DEBUG_USE_NETLINK_NFTABLES is set.
+func New(logf logger.Logf) (NetfilterRunner, error) {
+ mode := detectFirewallMode(logf)
+ switch mode {
+ case FirewallModeIPTables:
+ return newIPTablesRunner(logf)
+ case FirewallModeNfTables:
+ return newNfTablesRunner(logf)
+ default:
+ return nil, fmt.Errorf("unknown firewall mode %v", mode)
+ }
+}
+
+// newNfTablesRunner creates a new nftablesRunner without guaranteeing
// the existence of the tables and chains.
-func NewNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
+func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
conn, err := nftables.New()
if err != nil {
return nil, fmt.Errorf("nftables connection: %w", err)
@@ -231,7 +289,7 @@ func newLoadSaddrExpr(proto nftables.TableFamily, destReg uint32) (expr.Any, err
}
}
-// HasIPV6 returns true if the system supports IPv6.
+// HasIPV6 reports true if the system supports IPv6.
func (n *nftablesRunner) HasIPV6() bool {
return n.v6Available
}
@@ -877,6 +935,38 @@ func addAcceptOutgoingPacketRule(conn *nftables.Conn, table *nftables.Table, cha
return nil
}
+// createAcceptIncomingPacketRule creates a rule to accept incoming packets to
+// the given interface.
+func createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {
+ return &nftables.Rule{
+ Table: table,
+ Chain: chain,
+ Exprs: []expr.Any{
+ &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
+ &expr.Cmp{
+ Op: expr.CmpOpEq,
+ Register: 1,
+ Data: []byte(tunname),
+ },
+ &expr.Counter{},
+ &expr.Verdict{
+ Kind: expr.VerdictAccept,
+ },
+ },
+ }
+}
+
+func addAcceptIncomingPacketRule(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
+ rule := createAcceptIncomingPacketRule(table, chain, tunname)
+ _ = conn.AddRule(rule)
+
+ if err := conn.Flush(); err != nil {
+ return fmt.Errorf("flush add rule: %w", err)
+ }
+
+ return nil
+}
+
// AddBase adds some basic processing rules.
func (n *nftablesRunner) AddBase(tunname string) error {
if err := n.addBase4(tunname); err != nil {
@@ -904,6 +994,9 @@ func (n *nftablesRunner) addBase4(tunname string) error {
if err = addDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
return fmt.Errorf("add drop cgnat range rule v4: %w", err)
}
+ if err = addAcceptIncomingPacketRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
+ return fmt.Errorf("add accept incoming packet rule v4: %w", err)
+ }
forwardChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameForward)
if err != nil {
@@ -937,6 +1030,14 @@ func (n *nftablesRunner) addBase4(tunname string) error {
func (n *nftablesRunner) addBase6(tunname string) error {
conn := n.conn
+ inputChain, err := getChainFromTable(conn, n.nft6.Filter, chainNameInput)
+ if err != nil {
+ return fmt.Errorf("get input chain v4: %v", err)
+ }
+ if err = addAcceptIncomingPacketRule(conn, n.nft6.Filter, inputChain, tunname); err != nil {
+ return fmt.Errorf("add accept incoming packet rule v6: %w", err)
+ }
+
forwardChain, err := getChainFromTable(conn, n.nft6.Filter, chainNameForward)
if err != nil {
return fmt.Errorf("get forward chain v6: %w", err)
@@ -1109,7 +1210,9 @@ func (n *nftablesRunner) DelSNATRule() error {
return fmt.Errorf("find SNAT rule v4: %w", err)
}
- _ = conn.DelRule(SNATRule)
+ if SNATRule != nil {
+ _ = conn.DelRule(SNATRule)
+ }
}
if err := conn.Flush(); err != nil {
diff --git a/util/linuxfw/nftables_runner_test.go b/util/linuxfw/nftables_runner_test.go
index ad068957ee9a3..1a451238b948c 100644
--- a/util/linuxfw/nftables_runner_test.go
+++ b/util/linuxfw/nftables_runner_test.go
@@ -7,6 +7,7 @@ package linuxfw
import (
"bytes"
+ "errors"
"fmt"
"net/netip"
"os"
@@ -375,6 +376,38 @@ func TestAddAcceptOutgoingPacketRule(t *testing.T) {
}
}
+func TestAddAcceptIncomingPacketRule(t *testing.T) {
+ proto := nftables.TableFamilyIPv4
+ want := [][]byte{
+ // batch begin
+ []byte("\x00\x00\x00\x0a"),
+ // nft add table ip ts-filter-test
+ []byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
+ // nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0\; }
+ []byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
+ // nft add rule ip ts-filter-test ts-input-test iifname "testTunn" counter accept
+ []byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\xb4\x00\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
+ // batch end
+ []byte("\x00\x00\x00\x0a"),
+ }
+ testConn := newTestConn(t, want)
+ table := testConn.AddTable(&nftables.Table{
+ Family: proto,
+ Name: "ts-filter-test",
+ })
+ chain := testConn.AddChain(&nftables.Chain{
+ Name: "ts-input-test",
+ Table: table,
+ Type: nftables.ChainTypeFilter,
+ Hooknum: nftables.ChainHookInput,
+ Priority: nftables.ChainPriorityFilter,
+ })
+ err := addAcceptIncomingPacketRule(testConn, table, chain, "testTunn")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
func TestAddMatchSubnetRouteMarkRuleMasq(t *testing.T) {
proto := nftables.TableFamilyIPv4
want := [][]byte{
@@ -914,3 +947,63 @@ func TestNFTAddAndDelHookRule(t *testing.T) {
t.Fatalf("expected 0 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
}
}
+
+type testFWDetector struct {
+ iptRuleCount, nftRuleCount int
+ iptErr, nftErr error
+}
+
+func (t *testFWDetector) iptDetect() (int, error) {
+ return t.iptRuleCount, t.iptErr
+}
+
+func (t *testFWDetector) nftDetect() (int, error) {
+ return t.nftRuleCount, t.nftErr
+}
+
+func TestPickFirewallModeFromInstalledRules(t *testing.T) {
+ tests := []struct {
+ name string
+ det *testFWDetector
+ want FirewallMode
+ }{
+ {
+ name: "using iptables legacy",
+ det: &testFWDetector{iptRuleCount: 1},
+ want: FirewallModeIPTables,
+ },
+ {
+ name: "using nftables",
+ det: &testFWDetector{nftRuleCount: 1},
+ want: FirewallModeNfTables,
+ },
+ {
+ name: "using both iptables and nftables",
+ det: &testFWDetector{iptRuleCount: 2, nftRuleCount: 2},
+ want: FirewallModeNfTables,
+ },
+ {
+ name: "not using any firewall, both available",
+ det: &testFWDetector{},
+ want: FirewallModeNfTables,
+ },
+ {
+ name: "not using any firewall, iptables available only",
+ det: &testFWDetector{iptRuleCount: 1, nftErr: errors.New("nft error")},
+ want: FirewallModeIPTables,
+ },
+ {
+ name: "not using any firewall, nftables available only",
+ det: &testFWDetector{iptErr: errors.New("iptables error"), nftRuleCount: 1},
+ want: FirewallModeNfTables,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := pickFirewallModeFromInstalledRules(t.Logf, tt.det)
+ if got != tt.want {
+ t.Errorf("chooseFireWallMode() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/util/race/race.go b/util/race/race.go
new file mode 100644
index 0000000000000..041ce546f3731
--- /dev/null
+++ b/util/race/race.go
@@ -0,0 +1,115 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package race contains a helper to "race" two functions, returning the first
+// successful result. It also allows explicitly triggering the
+// (possibly-waiting) second function when the first function returns an error
+// or indicates that it should be retried.
+package race
+
+import (
+ "context"
+ "errors"
+ "time"
+)
+
+type resultType int
+
+const (
+ first resultType = iota
+ second
+)
+
+// queryResult is an internal type for storing the result of a function call
+type queryResult[T any] struct {
+ ty resultType
+ res T
+ err error
+}
+
+// Func is the signature of a function to be called.
+type Func[T any] func(context.Context) (T, error)
+
+// Race allows running two functions concurrently and returning the first
+// non-error result returned.
+type Race[T any] struct {
+ func1, func2 Func[T]
+ d time.Duration
+ results chan queryResult[T]
+ startFallback chan struct{}
+}
+
+// New creates a new Race that, when Start is called, will immediately call
+// func1 to obtain a result. After the timeout d or if triggered by an error
+// response from func1, func2 will be called.
+func New[T any](d time.Duration, func1, func2 Func[T]) *Race[T] {
+ ret := &Race[T]{
+ func1: func1,
+ func2: func2,
+ d: d,
+ results: make(chan queryResult[T], 2),
+ startFallback: make(chan struct{}),
+ }
+ return ret
+}
+
+// Start will start the "race" process, returning the first non-error result or
+// the errors that occurred when calling func1 and/or func2.
+func (rh *Race[T]) Start(ctx context.Context) (T, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // func1 is started immediately
+ go func() {
+ ret, err := rh.func1(ctx)
+ rh.results <- queryResult[T]{first, ret, err}
+ }()
+
+ // func2 is started after a timeout
+ go func() {
+ wait := time.NewTimer(rh.d)
+ defer wait.Stop()
+
+ // Wait for our timeout, trigger, or context to finish.
+ select {
+ case <-ctx.Done():
+ // Nothing to do; we're done
+ var zero T
+ rh.results <- queryResult[T]{second, zero, ctx.Err()}
+ return
+ case <-rh.startFallback:
+ case <-wait.C:
+ }
+
+ ret, err := rh.func2(ctx)
+ rh.results <- queryResult[T]{second, ret, err}
+ }()
+
+ // For each possible result, get it off the channel.
+ var errs []error
+ for i := 0; i < 2; i++ {
+ res := <-rh.results
+
+ // If this was an error, store it and hope that the other
+ // result gives us something.
+ if res.err != nil {
+ errs = append(errs, res.err)
+
+ // Start the fallback function immediately if this is
+ // the first function's error, to avoid having
+ // to wait.
+ if res.ty == first {
+ close(rh.startFallback)
+ }
+ continue
+ }
+
+ // Got a valid response! Return it.
+ return res.res, nil
+ }
+
+ // If we get here, both raced functions failed. Return whatever errors
+ // we have, joined together.
+ var zero T
+ return zero, errors.Join(errs...)
+}
diff --git a/util/race/race_test.go b/util/race/race_test.go
new file mode 100644
index 0000000000000..d3838271226ac
--- /dev/null
+++ b/util/race/race_test.go
@@ -0,0 +1,99 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package race
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "tailscale.com/tstest"
+)
+
+func TestRaceSuccess1(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "success"
+ rh := New[string](
+ 10*time.Second,
+ func(context.Context) (string, error) {
+ return want, nil
+ }, func(context.Context) (string, error) {
+ t.Fatal("should not be called")
+ return "", nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceRetry(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "fallback"
+ rh := New[string](
+ 10*time.Second,
+ func(context.Context) (string, error) {
+ return "", errors.New("some error")
+ }, func(context.Context) (string, error) {
+ return want, nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceTimeout(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ const want = "fallback"
+ rh := New[string](
+ 100*time.Millisecond,
+ func(ctx context.Context) (string, error) {
+ // Block forever
+ <-ctx.Done()
+ return "", ctx.Err()
+ }, func(context.Context) (string, error) {
+ return want, nil
+ })
+ res, err := rh.Start(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res != want {
+ t.Errorf("got res=%q, want %q", res, want)
+ }
+}
+
+func TestRaceError(t *testing.T) {
+ tstest.ResourceCheck(t)
+
+ err1 := errors.New("error 1")
+ err2 := errors.New("error 2")
+
+ rh := New[string](
+ 100*time.Millisecond,
+ func(ctx context.Context) (string, error) {
+ return "", err1
+ }, func(context.Context) (string, error) {
+ return "", err2
+ })
+
+ _, err := rh.Start(context.Background())
+ if !errors.Is(err, err1) {
+ t.Errorf("wanted err to contain err1; got %v", err)
+ }
+ if !errors.Is(err, err2) {
+ t.Errorf("wanted err to contain err2; got %v", err)
+ }
+}
diff --git a/util/set/handle.go b/util/set/handle.go
new file mode 100644
index 0000000000000..471ceeba2d523
--- /dev/null
+++ b/util/set/handle.go
@@ -0,0 +1,28 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package set
+
+// HandleSet is a set of T.
+//
+// It is not safe for concurrent use.
+type HandleSet[T any] map[Handle]T
+
+// Handle is an opaque comparable value that's used as the map key in a
+// HandleSet. The only way to get one is to call HandleSet.Add.
+type Handle struct {
+ v *byte
+}
+
+// Add adds the element (map value) e to the set.
+//
+// It returns the handle (map key) with which e can be removed, using a map
+// delete.
+func (s *HandleSet[T]) Add(e T) Handle {
+ h := Handle{new(byte)}
+ if *s == nil {
+ *s = make(HandleSet[T])
+ }
+ (*s)[h] = e
+ return h
+}
diff --git a/util/set/set.go b/util/set/set.go
index e6f3ef1f0202d..987747892a371 100644
--- a/util/set/set.go
+++ b/util/set/set.go
@@ -7,9 +7,33 @@ package set
// Set is a set of T.
type Set[T comparable] map[T]struct{}
+// SetOf returns a new set constructed from the elements in slice.
+func SetOf[T comparable](slice []T) Set[T] {
+ s := make(Set[T])
+ s.AddSlice(slice)
+ return s
+}
+
// Add adds e to the set.
func (s Set[T]) Add(e T) { s[e] = struct{}{} }
+// AddSlice adds each element of es to the set.
+func (s Set[T]) AddSlice(es []T) {
+ for _, e := range es {
+ s.Add(e)
+ }
+}
+
+// Slice returns the elements of the set as a slice. The elements will not be
+// in any particular order.
+func (s Set[T]) Slice() []T {
+ es := make([]T, 0, s.Len())
+ for k := range s {
+ es = append(es, k)
+ }
+ return es
+}
+
// Delete removes e from the set.
func (s Set[T]) Delete(e T) { delete(s, e) }
@@ -21,27 +45,3 @@ func (s Set[T]) Contains(e T) bool {
// Len reports the number of items in s.
func (s Set[T]) Len() int { return len(s) }
-
-// HandleSet is a set of T.
-//
-// It is not safe for concurrent use.
-type HandleSet[T any] map[Handle]T
-
-// Handle is a opaque comparable value that's used as the map key
-// in a HandleSet. The only way to get one is to call HandleSet.Add.
-type Handle struct {
- v *byte
-}
-
-// Add adds the element (map value) e to the set.
-//
-// It returns the handle (map key) with which e can be removed, using a map
-// delete.
-func (s *HandleSet[T]) Add(e T) Handle {
- h := Handle{new(byte)}
- if *s == nil {
- *s = make(HandleSet[T])
- }
- (*s)[h] = e
- return h
-}
diff --git a/util/set/set_test.go b/util/set/set_test.go
index 7a920ed882771..e898f4f693de9 100644
--- a/util/set/set_test.go
+++ b/util/set/set_test.go
@@ -3,7 +3,10 @@
package set
-import "testing"
+import (
+ "slices"
+ "testing"
+)
func TestSet(t *testing.T) {
s := Set[int]{}
@@ -21,4 +24,41 @@ func TestSet(t *testing.T) {
if s.Len() != 2 {
t.Errorf("wrong len %d; want 2", s.Len())
}
+
+ more := []int{3, 4}
+ s.AddSlice(more)
+ if !s.Contains(3) {
+ t.Error("missing 3")
+ }
+ if !s.Contains(4) {
+ t.Error("missing 4")
+ }
+ if s.Contains(5) {
+ t.Error("shouldn't have 5")
+ }
+ if s.Len() != 4 {
+ t.Errorf("wrong len %d; want 4", s.Len())
+ }
+
+ es := s.Slice()
+ if len(es) != 4 {
+ t.Errorf("slice has wrong len %d; want 4", len(es))
+ }
+ for _, e := range []int{1, 2, 3, 4} {
+ if !slices.Contains(es, e) {
+ t.Errorf("slice missing %d (%#v)", e, es)
+ }
+ }
+}
+
+func TestSetOf(t *testing.T) {
+ s := SetOf[int]([]int{1, 2, 3, 4, 4, 1})
+ if s.Len() != 4 {
+ t.Errorf("wrong len %d; want 2", s.Len())
+ }
+ for _, n := range []int{1, 2, 3, 4} {
+ if !s.Contains(n) {
+ t.Errorf("should contain %d", n)
+ }
+ }
}
diff --git a/util/set/slice.go b/util/set/slice.go
index fe764b550fe3b..9c300cc28e098 100644
--- a/util/set/slice.go
+++ b/util/set/slice.go
@@ -15,7 +15,7 @@ type Slice[T comparable] struct {
set map[T]bool // nil until/unless slice is large enough
}
-// Slice returns the a view of the underlying slice.
+// Slice returns a view of the underlying slice.
// The elements are in order of insertion.
// The returned value is only valid until ss is modified again.
func (ss *Slice[T]) Slice() views.Slice[T] { return views.SliceOf(ss.slice) }
diff --git a/util/slicesx/slicesx.go b/util/slicesx/slicesx.go
index ba5be72712275..2e4ac956727e0 100644
--- a/util/slicesx/slicesx.go
+++ b/util/slicesx/slicesx.go
@@ -57,3 +57,23 @@ func Partition[S ~[]T, T any](s S, cb func(T) bool) (trues, falses S) {
}
return
}
+
+// EqualSameNil reports whether two slices are equal: the same length, same
+// nilness (notably when length zero), and all elements equal. If the lengths
+// are different or their nilness differs, Equal returns false. Otherwise, the
+// elements are compared in increasing index order, and the comparison stops at
+// the first unequal pair. Floating point NaNs are not considered equal.
+//
+// It is identical to the standard library's slices.Equal but adds the matching
+// nilness check.
+func EqualSameNil[S ~[]E, E comparable](s1, s2 S) bool {
+ if len(s1) != len(s2) || (s1 == nil) != (s2 == nil) {
+ return false
+ }
+ for i := range s1 {
+ if s1[i] != s2[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/util/slicesx/slicesx_test.go b/util/slicesx/slicesx_test.go
index 48efae4fbdc2a..0d206b3649a11 100644
--- a/util/slicesx/slicesx_test.go
+++ b/util/slicesx/slicesx_test.go
@@ -7,6 +7,8 @@ import (
"reflect"
"slices"
"testing"
+
+ qt "github.com/frankban/quicktest"
)
func TestInterleave(t *testing.T) {
@@ -84,3 +86,14 @@ func TestPartition(t *testing.T) {
t.Errorf("odds: got %v, want %v", odds, wantOdds)
}
}
+
+func TestEqualSameNil(t *testing.T) {
+ c := qt.New(t)
+ c.Check(EqualSameNil([]string{"a"}, []string{"a"}), qt.Equals, true)
+ c.Check(EqualSameNil([]string{"a"}, []string{"b"}), qt.Equals, false)
+ c.Check(EqualSameNil([]string{"a"}, []string{}), qt.Equals, false)
+ c.Check(EqualSameNil([]string{}, []string{}), qt.Equals, true)
+ c.Check(EqualSameNil(nil, []string{}), qt.Equals, false)
+ c.Check(EqualSameNil([]string{}, nil), qt.Equals, false)
+ c.Check(EqualSameNil[[]string](nil, nil), qt.Equals, true)
+}
diff --git a/util/syspolicy/handler.go b/util/syspolicy/handler.go
new file mode 100644
index 0000000000000..68ba09176ec0d
--- /dev/null
+++ b/util/syspolicy/handler.go
@@ -0,0 +1,58 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package syspolicy
+
+import (
+ "errors"
+ "sync/atomic"
+)
+
+var (
+ handlerUsed atomic.Bool
+ handler Handler = defaultHandler{}
+)
+
+// Handler reads system policies from OS-specific storage.
+type Handler interface {
+ // ReadString reads the policy settings value string given the key.
+ ReadString(key string) (string, error)
+ // ReadUInt64 reads the policy settings uint64 value given the key.
+ ReadUInt64(key string) (uint64, error)
+ // ReadBool reads the policy setting's boolean value, given the key.
+ ReadBoolean(key string) (bool, error)
+}
+
+// ErrNoSuchKey is returned when the specified key does not have a value set.
+var ErrNoSuchKey = errors.New("no such key")
+
+// defaultHandler is the catch all syspolicy type for anything that isn't windows or apple.
+type defaultHandler struct{}
+
+func (defaultHandler) ReadString(_ string) (string, error) {
+ return "", ErrNoSuchKey
+}
+
+func (defaultHandler) ReadUInt64(_ string) (uint64, error) {
+ return 0, ErrNoSuchKey
+}
+
+func (defaultHandler) ReadBoolean(_ string) (bool, error) {
+ return false, ErrNoSuchKey
+}
+
+// markHandlerInUse is called before handler methods are called.
+func markHandlerInUse() {
+ handlerUsed.Store(true)
+}
+
+// RegisterHandler initializes the policy handler and ensures registration will happen once.
+func RegisterHandler(h Handler) {
+ // Technically this assignment is not concurrency safe, but in the
+ // event that there was any risk of a data race, we will panic due to
+ // the CompareAndSwap failing.
+ handler = h
+ if !handlerUsed.CompareAndSwap(false, true) {
+ panic("handler was already used before registration")
+ }
+}
diff --git a/util/syspolicy/handler_test.go b/util/syspolicy/handler_test.go
new file mode 100644
index 0000000000000..39b18936f176d
--- /dev/null
+++ b/util/syspolicy/handler_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package syspolicy
+
+import "testing"
+
+func TestDefaultHandlerReadValues(t *testing.T) {
+ var h defaultHandler
+
+ got, err := h.ReadString(string(AdminConsoleVisibility))
+ if got != "" || err != ErrNoSuchKey {
+ t.Fatalf("got %v err %v", got, err)
+ }
+ result, err := h.ReadUInt64(string(LogSCMInteractions))
+ if result != 0 || err != ErrNoSuchKey {
+ t.Fatalf("got %v err %v", result, err)
+ }
+}
diff --git a/util/syspolicy/handler_windows.go b/util/syspolicy/handler_windows.go
new file mode 100644
index 0000000000000..c12a21fdfca2a
--- /dev/null
+++ b/util/syspolicy/handler_windows.go
@@ -0,0 +1,40 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package syspolicy
+
+import (
+ "errors"
+
+ "tailscale.com/util/winutil"
+)
+
+type windowsHandler struct{}
+
+func init() {
+ RegisterHandler(windowsHandler{})
+}
+
+func (windowsHandler) ReadString(key string) (string, error) {
+ s, err := winutil.GetPolicyString(key)
+ if errors.Is(err, winutil.ErrNoValue) {
+ err = ErrNoSuchKey
+ }
+ return s, err
+}
+
+func (windowsHandler) ReadUInt64(key string) (uint64, error) {
+ value, err := winutil.GetPolicyInteger(key)
+ if errors.Is(err, winutil.ErrNoValue) {
+ err = ErrNoSuchKey
+ }
+ return value, err
+}
+
+func (windowsHandler) ReadBoolean(key string) (bool, error) {
+ value, err := winutil.GetPolicyInteger(key)
+ if errors.Is(err, winutil.ErrNoValue) {
+ err = ErrNoSuchKey
+ }
+ return value != 0, err
+}
diff --git a/util/syspolicy/policy_keys.go b/util/syspolicy/policy_keys.go
new file mode 100644
index 0000000000000..80b266730bc34
--- /dev/null
+++ b/util/syspolicy/policy_keys.go
@@ -0,0 +1,39 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package syspolicy
+
+type Key string
+
+const (
+ // Keys with a string value
+ ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
+ LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
+
+ // Keys with a string value that specifies an option: "always", "never", "user-decides".
+ // The default is "user-decides" unless otherwise stated.
+ EnableIncomingConnections Key = "AllowIncomingConnections"
+ EnableServerMode Key = "UnattendedMode"
+
+ // Keys with a string value that controls visibility: "show", "hide".
+ // The default is "show" unless otherwise stated.
+ AdminConsoleVisibility Key = "AdminConsole"
+ NetworkDevicesVisibility Key = "NetworkDevices"
+ TestMenuVisibility Key = "TestMenu"
+ UpdateMenuVisibility Key = "UpdateMenu"
+ RunExitNodeVisibility Key = "RunExitNode"
+ PreferencesMenuVisibility Key = "PreferencesMenu"
+
+ // Keys with a string value formatted for use with time.ParseDuration().
+ KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
+
+ // Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
+ // DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
+ // The default is 0 unless otherwise stated.
+ LogSCMInteractions Key = "LogSCMInteractions"
+ FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
+
+ // Boolean key that indicates if posture checking is enabled and the client shall gather
+ // posture data.
+ PostureChecking Key = "PostureChecking"
+)
diff --git a/util/syspolicy/syspolicy.go b/util/syspolicy/syspolicy.go
new file mode 100644
index 0000000000000..2f8cf9dcb0368
--- /dev/null
+++ b/util/syspolicy/syspolicy.go
@@ -0,0 +1,181 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package syspolicy provides functions to retrieve system settings of a device.
+package syspolicy
+
+import (
+ "errors"
+ "time"
+)
+
+func GetString(key Key, defaultValue string) (string, error) {
+ markHandlerInUse()
+ v, err := handler.ReadString(string(key))
+ if errors.Is(err, ErrNoSuchKey) {
+ return defaultValue, nil
+ }
+ return v, err
+}
+
+func GetUint64(key Key, defaultValue uint64) (uint64, error) {
+ markHandlerInUse()
+ v, err := handler.ReadUInt64(string(key))
+ if errors.Is(err, ErrNoSuchKey) {
+ return defaultValue, nil
+ }
+ return v, err
+}
+
+func GetBoolean(key Key, defaultValue bool) (bool, error) {
+ markHandlerInUse()
+ v, err := handler.ReadBoolean(string(key))
+ if errors.Is(err, ErrNoSuchKey) {
+ return defaultValue, nil
+ }
+ return v, err
+}
+
+// PreferenceOption is a policy that governs whether a boolean variable
+// is forcibly assigned an administrator-defined value, or allowed to receive
+// a user-defined value.
+type PreferenceOption int
+
+const (
+ showChoiceByPolicy PreferenceOption = iota
+ neverByPolicy
+ alwaysByPolicy
+)
+
+// Show returns if the UI option that controls the choice administered by this
+// policy should be shown. Currently this is true if and only if the policy is
+// showChoiceByPolicy.
+func (p PreferenceOption) Show() bool {
+ return p == showChoiceByPolicy
+}
+
+// ShouldEnable checks if the choice administered by this policy should be
+// enabled. If the administrator has chosen a setting, the administrator's
+// setting is returned, otherwise userChoice is returned.
+func (p PreferenceOption) ShouldEnable(userChoice bool) bool {
+ switch p {
+ case neverByPolicy:
+ return false
+ case alwaysByPolicy:
+ return true
+ default:
+ return userChoice
+ }
+}
+
+// GetPreferenceOption loads a policy from the registry that can be
+// managed by an enterprise policy management system and allows administrative
+// overrides of users' choices in a way that we do not want tailcontrol to have
+// the authority to set. It describes user-decides/always/never options, where
+// "always" and "never" remove the user's ability to make a selection. If not
+// present or set to a different value, "user-decides" is the default.
+func GetPreferenceOption(name Key) (PreferenceOption, error) {
+ opt, err := GetString(name, "user-decides")
+ if err != nil {
+ return showChoiceByPolicy, err
+ }
+ switch opt {
+ case "always":
+ return alwaysByPolicy, nil
+ case "never":
+ return neverByPolicy, nil
+ default:
+ return showChoiceByPolicy, nil
+ }
+}
+
+// Visibility is a policy that controls whether or not a particular
+// component of a user interface is to be shown.
+type Visibility byte
+
+const (
+ visibleByPolicy Visibility = 'v'
+ hiddenByPolicy Visibility = 'h'
+)
+
+// Show reports whether the UI option administered by this policy should be shown.
+// Currently this is true if and only if the policy is visibleByPolicy.
+func (p Visibility) Show() bool {
+ return p == visibleByPolicy
+}
+
+// GetVisibility loads a policy from the registry that can be managed
+// by an enterprise policy management system and describes show/hide decisions
+// for UI elements. The registry value should be a string set to "show" (return
+// true) or "hide" (return true). If not present or set to a different value,
+// "show" (return false) is the default.
+func GetVisibility(name Key) (Visibility, error) {
+ opt, err := GetString(name, "show")
+ if err != nil {
+ return visibleByPolicy, err
+ }
+ switch opt {
+ case "hide":
+ return hiddenByPolicy, nil
+ default:
+ return visibleByPolicy, nil
+ }
+}
+
+// GetDuration loads a policy from the registry that can be managed
+// by an enterprise policy management system and describes a duration for some
+// action. The registry value should be a string that time.ParseDuration
+// understands. If the registry value is "" or can not be processed,
+// defaultValue is returned instead.
+func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
+ opt, err := GetString(name, "")
+ if opt == "" || err != nil {
+ return defaultValue, err
+ }
+ v, err := time.ParseDuration(opt)
+ if err != nil || v < 0 {
+ return defaultValue, nil
+ }
+ return v, nil
+}
+
+// SelectControlURL returns the ControlURL to use based on a value in
+// the registry (LoginURL) and the one on disk (in the GUI's
+// prefs.conf). If both are empty, it returns a default value. (It
+// always return a non-empty value)
+//
+// See https://github.com/tailscale/tailscale/issues/2798 for some background.
+func SelectControlURL(reg, disk string) string {
+ const def = "https://controlplane.tailscale.com"
+
+ // Prior to Dec 2020's commit 739b02e6, the installer
+ // wrote a LoginURL value of https://login.tailscale.com to the registry.
+ const oldRegDef = "https://login.tailscale.com"
+
+ // If they have an explicit value in the registry, use it,
+ // unless it's an old default value from an old installer.
+ // Then we have to see which is better.
+ if reg != "" {
+ if reg != oldRegDef {
+ // Something explicit in the registry that we didn't
+ // set ourselves by the installer.
+ return reg
+ }
+ if disk == "" {
+ // Something in the registry is better than nothing on disk.
+ return reg
+ }
+ if disk != def && disk != oldRegDef {
+ // The value in the registry is the old
+ // default (login.tailscale.com) but the value
+ // on disk is neither our old nor new default
+ // value, so it must be some custom thing that
+ // the user cares about. Prefer the disk value.
+ return disk
+ }
+ }
+ if disk != "" {
+ return disk
+ }
+ return def
+}
diff --git a/util/syspolicy/syspolicy_test.go b/util/syspolicy/syspolicy_test.go
new file mode 100644
index 0000000000000..859843431bf5c
--- /dev/null
+++ b/util/syspolicy/syspolicy_test.go
@@ -0,0 +1,435 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package syspolicy
+
+import (
+ "errors"
+ "testing"
+ "time"
+)
+
+// testHandler encompasses all data types returned when testing any of the syspolicy
+// methods that involve getting a policy value.
+// For keys and the corresponding values, check policy_keys.go.
+type testHandler struct {
+ t *testing.T
+ key Key
+ s string
+ u64 uint64
+ b bool
+ err error
+}
+
+var someOtherError = errors.New("error other than not found")
+
+func setHandlerForTest(tb testing.TB, h Handler) {
+ tb.Helper()
+ oldHandler := handler
+ handler = h
+ tb.Cleanup(func() { handler = oldHandler })
+}
+
+func (th *testHandler) ReadString(key string) (string, error) {
+ if key != string(th.key) {
+ th.t.Errorf("ReadString(%q) want %q", key, th.key)
+ }
+ return th.s, th.err
+}
+
+func (th *testHandler) ReadUInt64(key string) (uint64, error) {
+ if key != string(th.key) {
+ th.t.Errorf("ReadUint64(%q) want %q", key, th.key)
+ }
+ return th.u64, th.err
+}
+
+func (th *testHandler) ReadBoolean(key string) (bool, error) {
+ if key != string(th.key) {
+ th.t.Errorf("ReadBool(%q) want %q", key, th.key)
+ }
+ return th.b, th.err
+}
+
+func TestGetString(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue string
+ handlerError error
+ defaultValue string
+ wantValue string
+ wantError error
+ }{
+ {
+ name: "read existing value",
+ key: AdminConsoleVisibility,
+ handlerValue: "hide",
+ wantValue: "hide",
+ },
+ {
+ name: "read non-existing value",
+ key: EnableServerMode,
+ handlerError: ErrNoSuchKey,
+ wantError: nil,
+ },
+ {
+ name: "read non-existing value, non-blank default",
+ key: EnableServerMode,
+ handlerError: ErrNoSuchKey,
+ defaultValue: "test",
+ wantValue: "test",
+ wantError: nil,
+ },
+ {
+ name: "reading value returns other error",
+ key: NetworkDevicesVisibility,
+ handlerError: someOtherError,
+ wantError: someOtherError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ s: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ value, err := GetString(tt.key, tt.defaultValue)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if value != tt.wantValue {
+ t.Errorf("value=%v, want %v", value, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestGetUint64(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue uint64
+ handlerError error
+ defaultValue uint64
+ wantValue uint64
+ wantError error
+ }{
+ {
+ name: "read existing value",
+ key: KeyExpirationNoticeTime,
+ handlerValue: 1,
+ wantValue: 1,
+ },
+ {
+ name: "read non-existing value",
+ key: LogSCMInteractions,
+ handlerValue: 0,
+ handlerError: ErrNoSuchKey,
+ wantValue: 0,
+ },
+ {
+ name: "read non-existing value, non-zero default",
+ key: LogSCMInteractions,
+ defaultValue: 2,
+ handlerError: ErrNoSuchKey,
+ wantValue: 2,
+ },
+ {
+ name: "reading value returns other error",
+ key: FlushDNSOnSessionUnlock,
+ handlerError: someOtherError,
+ wantError: someOtherError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ u64: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ value, err := GetUint64(tt.key, tt.defaultValue)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if value != tt.wantValue {
+ t.Errorf("value=%v, want %v", value, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestGetBoolean(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue bool
+ handlerError error
+ defaultValue bool
+ wantValue bool
+ wantError error
+ }{
+ {
+ name: "read existing value",
+ key: FlushDNSOnSessionUnlock,
+ handlerValue: true,
+ wantValue: true,
+ },
+ {
+ name: "read non-existing value",
+ key: LogSCMInteractions,
+ handlerValue: false,
+ handlerError: ErrNoSuchKey,
+ wantValue: false,
+ },
+ {
+ name: "reading value returns other error",
+ key: FlushDNSOnSessionUnlock,
+ handlerError: someOtherError,
+ wantError: someOtherError,
+ defaultValue: true,
+ wantValue: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ b: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ value, err := GetBoolean(tt.key, tt.defaultValue)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if value != tt.wantValue {
+ t.Errorf("value=%v, want %v", value, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestGetPreferenceOption(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue string
+ handlerError error
+ wantValue PreferenceOption
+ wantError error
+ }{
+ {
+ name: "always by policy",
+ key: EnableIncomingConnections,
+ handlerValue: "always",
+ wantValue: alwaysByPolicy,
+ },
+ {
+ name: "never by policy",
+ key: EnableIncomingConnections,
+ handlerValue: "never",
+ wantValue: neverByPolicy,
+ },
+ {
+ name: "use default",
+ key: EnableIncomingConnections,
+ handlerValue: "",
+ wantValue: showChoiceByPolicy,
+ },
+ {
+ name: "read non-existing value",
+ key: EnableIncomingConnections,
+ handlerError: ErrNoSuchKey,
+ wantValue: showChoiceByPolicy,
+ },
+ {
+ name: "other error is returned",
+ key: EnableIncomingConnections,
+ handlerError: someOtherError,
+ wantValue: showChoiceByPolicy,
+ wantError: someOtherError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ s: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ option, err := GetPreferenceOption(tt.key)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if option != tt.wantValue {
+ t.Errorf("option=%v, want %v", option, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestGetVisibility(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue string
+ handlerError error
+ wantValue Visibility
+ wantError error
+ }{
+ {
+ name: "hidden by policy",
+ key: AdminConsoleVisibility,
+ handlerValue: "hide",
+ wantValue: hiddenByPolicy,
+ },
+ {
+ name: "visibility default",
+ key: AdminConsoleVisibility,
+ handlerValue: "show",
+ wantValue: visibleByPolicy,
+ },
+ {
+ name: "read non-existing value",
+ key: AdminConsoleVisibility,
+ handlerValue: "show",
+ handlerError: ErrNoSuchKey,
+ wantValue: visibleByPolicy,
+ },
+ {
+ name: "other error is returned",
+ key: AdminConsoleVisibility,
+ handlerValue: "show",
+ handlerError: someOtherError,
+ wantValue: visibleByPolicy,
+ wantError: someOtherError,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ s: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ visibility, err := GetVisibility(tt.key)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if visibility != tt.wantValue {
+ t.Errorf("visibility=%v, want %v", visibility, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestGetDuration(t *testing.T) {
+ tests := []struct {
+ name string
+ key Key
+ handlerValue string
+ handlerError error
+ defaultValue time.Duration
+ wantValue time.Duration
+ wantError error
+ }{
+ {
+ name: "read existing value",
+ key: KeyExpirationNoticeTime,
+ handlerValue: "2h",
+ wantValue: 2 * time.Hour,
+ defaultValue: 24 * time.Hour,
+ },
+ {
+ name: "invalid duration value",
+ key: KeyExpirationNoticeTime,
+ handlerValue: "-20",
+ wantValue: 24 * time.Hour,
+ defaultValue: 24 * time.Hour,
+ },
+ {
+ name: "read non-existing value",
+ key: KeyExpirationNoticeTime,
+ handlerError: ErrNoSuchKey,
+ wantValue: 24 * time.Hour,
+ defaultValue: 24 * time.Hour,
+ },
+ {
+ name: "read non-existing value different default",
+ key: KeyExpirationNoticeTime,
+ handlerError: ErrNoSuchKey,
+ wantValue: 0 * time.Second,
+ defaultValue: 0 * time.Second,
+ },
+ {
+ name: "other error is returned",
+ key: KeyExpirationNoticeTime,
+ handlerError: someOtherError,
+ wantValue: 24 * time.Hour,
+ wantError: someOtherError,
+ defaultValue: 24 * time.Hour,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setHandlerForTest(t, &testHandler{
+ t: t,
+ key: tt.key,
+ s: tt.handlerValue,
+ err: tt.handlerError,
+ })
+ duration, err := GetDuration(tt.key, tt.defaultValue)
+ if err != tt.wantError {
+ t.Errorf("err=%q, want %q", err, tt.wantError)
+ }
+ if duration != tt.wantValue {
+ t.Errorf("duration=%v, want %v", duration, tt.wantValue)
+ }
+ })
+ }
+}
+
+func TestSelectControlURL(t *testing.T) {
+ tests := []struct {
+ reg, disk, want string
+ }{
+ // Modern default case.
+ {"", "", "https://controlplane.tailscale.com"},
+
+ // For a user who installed prior to Dec 2020, with
+ // stuff in their registry.
+ {"https://login.tailscale.com", "", "https://login.tailscale.com"},
+
+ // Ignore pre-Dec'20 LoginURL from installer if prefs
+ // prefs overridden manually to an on-prem control
+ // server.
+ {"https://login.tailscale.com", "http://on-prem", "http://on-prem"},
+
+ // Something unknown explicitly set in the registry always wins.
+ {"http://explicit-reg", "", "http://explicit-reg"},
+ {"http://explicit-reg", "http://on-prem", "http://explicit-reg"},
+ {"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"},
+ {"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"},
+
+ // If nothing in the registry, disk wins.
+ {"", "http://on-prem", "http://on-prem"},
+ }
+ for _, tt := range tests {
+ if got := SelectControlURL(tt.reg, tt.disk); got != tt.want {
+ t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want)
+ }
+ }
+}
diff --git a/util/winutil/policy/policy_windows.go b/util/winutil/policy/policy_windows.go
index 139cf587623aa..89142951f8bd5 100644
--- a/util/winutil/policy/policy_windows.go
+++ b/util/winutil/policy/policy_windows.go
@@ -49,7 +49,10 @@ func (p PreferenceOptionPolicy) ShouldEnable(userChoice bool) bool {
// "always" and "never" remove the user's ability to make a selection. If not
// present or set to a different value, "user-decides" is the default.
func GetPreferenceOptionPolicy(name string) PreferenceOptionPolicy {
- opt := winutil.GetPolicyString(name, "user-decides")
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
+ return showChoiceByPolicy
+ }
switch opt {
case "always":
return alwaysByPolicy
@@ -81,7 +84,10 @@ func (p VisibilityPolicy) Show() bool {
// true) or "hide" (return true). If not present or set to a different value,
// "show" (return false) is the default.
func GetVisibilityPolicy(name string) VisibilityPolicy {
- opt := winutil.GetPolicyString(name, "show")
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
+ return visibleByPolicy
+ }
switch opt {
case "hide":
return hiddenByPolicy
@@ -96,8 +102,8 @@ func GetVisibilityPolicy(name string) VisibilityPolicy {
// understands. If the registry value is "" or can not be processed,
// defaultValue is returned instead.
func GetDurationPolicy(name string, defaultValue time.Duration) time.Duration {
- opt := winutil.GetPolicyString(name, "")
- if opt == "" {
+ opt, err := winutil.GetPolicyString(name)
+ if opt == "" || err != nil {
return defaultValue
}
v, err := time.ParseDuration(opt)
diff --git a/util/winutil/winutil.go b/util/winutil/winutil.go
index 3ec3f7c990e3b..4b771491ae284 100644
--- a/util/winutil/winutil.go
+++ b/util/winutil/winutil.go
@@ -13,45 +13,49 @@ import (
const RegBase = regBase
// GetPolicyString looks up a registry value in the local machine's path for
-// system policies, or returns the given default if it can't.
+// system policies, or returns empty string and the error.
// Use this function to read values that may be set by sysadmins via the MSI
// installer or via GPO. For registry settings that you do *not* want to be
// visible to sysadmin tools, use GetRegString instead.
//
// This function will only work on GOOS=windows. Trying to run it on any other
-// OS will always return the default value.
-func GetPolicyString(name, defval string) string {
- return getPolicyString(name, defval)
+// OS will always return an empty string and ErrNoValue.
+// If value does not exist or another error happens, returns empty string and error.
+func GetPolicyString(name string) (string, error) {
+ return getPolicyString(name)
}
// GetPolicyInteger looks up a registry value in the local machine's path for
-// system policies, or returns the given default if it can't.
+// system policies, or returns 0 and the associated error.
// Use this function to read values that may be set by sysadmins via the MSI
// installer or via GPO. For registry settings that you do *not* want to be
// visible to sysadmin tools, use GetRegInteger instead.
//
// This function will only work on GOOS=windows. Trying to run it on any other
-// OS will always return the default value.
-func GetPolicyInteger(name string, defval uint64) uint64 {
- return getPolicyInteger(name, defval)
+// OS will always return 0 and ErrNoValue.
+// If value does not exist or another error happens, returns 0 and error.
+func GetPolicyInteger(name string) (uint64, error) {
+ return getPolicyInteger(name)
}
// GetRegString looks up a registry path in the local machine path, or returns
-// the given default if it can't.
+// an empty string and error.
//
// This function will only work on GOOS=windows. Trying to run it on any other
-// OS will always return the default value.
-func GetRegString(name, defval string) string {
- return getRegString(name, defval)
+// OS will always return an empty string and ErrNoValue.
+// If value does not exist or another error happens, returns empty string and error.
+func GetRegString(name string) (string, error) {
+ return getRegString(name)
}
// GetRegInteger looks up a registry path in the local machine path, or returns
-// the given default if it can't.
+// 0 and the error.
//
// This function will only work on GOOS=windows. Trying to run it on any other
-// OS will always return the default value.
-func GetRegInteger(name string, defval uint64) uint64 {
- return getRegInteger(name, defval)
+// OS will always return 0 and ErrNoValue.
+// If value does not exist or another error happens, returns 0 and error.
+func GetRegInteger(name string) (uint64, error) {
+ return getRegInteger(name)
}
// IsSIDValidPrincipal determines whether the SID contained in uid represents a
diff --git a/util/winutil/winutil_notwindows.go b/util/winutil/winutil_notwindows.go
index c9a292aae0ca6..a40712c3f91bf 100644
--- a/util/winutil/winutil_notwindows.go
+++ b/util/winutil/winutil_notwindows.go
@@ -6,6 +6,7 @@
package winutil
import (
+ "errors"
"fmt"
"os/user"
"runtime"
@@ -13,13 +14,15 @@ import (
const regBase = ``
-func getPolicyString(name, defval string) string { return defval }
+var ErrNoValue = errors.New("no value because registry is unavailable on this OS")
-func getPolicyInteger(name string, defval uint64) uint64 { return defval }
+func getPolicyString(name string) (string, error) { return "", ErrNoValue }
-func getRegString(name, defval string) string { return defval }
+func getPolicyInteger(name string) (uint64, error) { return 0, ErrNoValue }
-func getRegInteger(name string, defval uint64) uint64 { return defval }
+func getRegString(name string) (string, error) { return "", ErrNoValue }
+
+func getRegInteger(name string) (uint64, error) { return 0, ErrNoValue }
func isSIDValidPrincipal(uid string) bool { return false }
diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go
index ed516ce6b2365..a686e6335408c 100644
--- a/util/winutil/winutil_windows.go
+++ b/util/winutil/winutil_windows.go
@@ -29,6 +29,9 @@ const (
// ErrNoShell is returned when the shell process is not found.
var ErrNoShell = errors.New("no Shell process is present")
+// ErrNoValue is returned when the value doesn't exist in the registry.
+var ErrNoValue = registry.ErrNotExist
+
// GetDesktopPID searches the PID of the process that's running the
// currently active desktop. Returns ErrNoShell if the shell is not present.
// Usually the PID will be for explorer.exe.
@@ -47,44 +50,44 @@ func GetDesktopPID() (uint32, error) {
return pid, nil
}
-func getPolicyString(name, defval string) string {
+func getPolicyString(name string) (string, error) {
s, err := getRegStringInternal(regPolicyBase, name)
if err != nil {
// Fall back to the legacy path
- return getRegString(name, defval)
+ return getRegString(name)
}
- return s
+ return s, err
}
-func getPolicyInteger(name string, defval uint64) uint64 {
- i, err := getRegIntegerInternal(regPolicyBase, name)
+func getRegString(name string) (string, error) {
+ s, err := getRegStringInternal(regBase, name)
if err != nil {
- // Fall back to the legacy path
- return getRegInteger(name, defval)
+ return "", err
}
- return i
+ return s, err
}
-func getRegString(name, defval string) string {
- s, err := getRegStringInternal(regBase, name)
+func getPolicyInteger(name string) (uint64, error) {
+ i, err := getRegIntegerInternal(regPolicyBase, name)
if err != nil {
- return defval
+ // Fall back to the legacy path
+ return getRegInteger(name)
}
- return s
+ return i, err
}
-func getRegInteger(name string, defval uint64) uint64 {
+func getRegInteger(name string) (uint64, error) {
i, err := getRegIntegerInternal(regBase, name)
if err != nil {
- return defval
+ return 0, err
}
- return i
+ return i, err
}
func getRegStringInternal(subKey, name string) (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.OpenKey(%v): %v", subKey, err)
}
return "", err
@@ -93,7 +96,7 @@ func getRegStringInternal(subKey, name string) (string, error) {
val, _, err := key.GetStringValue(name)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.GetStringValue(%v): %v", name, err)
}
return "", err
@@ -114,7 +117,7 @@ func GetRegStrings(name string, defval []string) []string {
func getRegStringsInternal(subKey, name string) ([]string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.OpenKey(%v): %v", subKey, err)
}
return nil, err
@@ -123,7 +126,7 @@ func getRegStringsInternal(subKey, name string) ([]string, error) {
val, _, err := key.GetStringsValue(name)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.GetStringValue(%v): %v", name, err)
}
return nil, err
@@ -154,7 +157,7 @@ func DeleteRegValue(name string) error {
func deleteRegValueInternal(subKey, name string) error {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE)
- if err == registry.ErrNotExist {
+ if err == ErrNoValue {
return nil
}
if err != nil {
@@ -164,7 +167,7 @@ func deleteRegValueInternal(subKey, name string) error {
defer key.Close()
err = key.DeleteValue(name)
- if err == registry.ErrNotExist {
+ if err == ErrNoValue {
err = nil
}
return err
@@ -173,7 +176,7 @@ func deleteRegValueInternal(subKey, name string) error {
func getRegIntegerInternal(subKey, name string) (uint64, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.OpenKey(%v): %v", subKey, err)
}
return 0, err
@@ -182,7 +185,7 @@ func getRegIntegerInternal(subKey, name string) (uint64, error) {
val, _, err := key.GetIntegerValue(name)
if err != nil {
- if err != registry.ErrNotExist {
+ if err != ErrNoValue {
log.Printf("registry.GetIntegerValue(%v): %v", name, err)
}
return 0, err
diff --git a/wgengine/bench/wg.go b/wgengine/bench/wg.go
index b0a4e7886b70c..883b4a4eba5a2 100644
--- a/wgengine/bench/wg.go
+++ b/wgengine/bench/wg.go
@@ -26,6 +26,13 @@ import (
"tailscale.com/wgengine/wgcfg"
)
+func epFromTyped(eps []tailcfg.Endpoint) (ret []netip.AddrPort) {
+ for _, ep := range eps {
+ ret = append(ret, ep.Addr)
+ }
+ return
+}
+
func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.Prefix) {
l1 := logger.WithPrefix(logf, "e1: ")
k1 := key.NewNode()
@@ -96,17 +103,12 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
}
logf("e1 status: %v", *st)
- var eps []string
- for _, ep := range st.LocalAddrs {
- eps = append(eps, ep.Addr.String())
- }
-
n := &tailcfg.Node{
ID: tailcfg.NodeID(0),
Name: "n1",
Addresses: []netip.Prefix{a1},
AllowedIPs: []netip.Prefix{a1},
- Endpoints: eps,
+ Endpoints: epFromTyped(st.LocalAddrs),
}
e2.SetNetworkMap(&netmap.NetworkMap{
NodeKey: k2.Public(),
@@ -133,17 +135,12 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
}
logf("e2 status: %v", *st)
- var eps []string
- for _, ep := range st.LocalAddrs {
- eps = append(eps, ep.Addr.String())
- }
-
n := &tailcfg.Node{
ID: tailcfg.NodeID(0),
Name: "n2",
Addresses: []netip.Prefix{a2},
AllowedIPs: []netip.Prefix{a2},
- Endpoints: eps,
+ Endpoints: epFromTyped(st.LocalAddrs),
}
e1.SetNetworkMap(&netmap.NetworkMap{
NodeKey: k1.Public(),
diff --git a/wgengine/magicsock/endpoint.go b/wgengine/magicsock/endpoint.go
index f3fea1295a1d4..1e344d1566d2e 100644
--- a/wgengine/magicsock/endpoint.go
+++ b/wgengine/magicsock/endpoint.go
@@ -21,6 +21,8 @@ import (
"golang.org/x/crypto/poly1305"
xmaps "golang.org/x/exp/maps"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
"tailscale.com/disco"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/stun"
@@ -29,11 +31,20 @@ import (
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
"tailscale.com/types/logger"
- "tailscale.com/types/views"
"tailscale.com/util/mak"
"tailscale.com/util/ringbuffer"
)
+var mtuProbePingSizesV4 []int
+var mtuProbePingSizesV6 []int
+
+func init() {
+ for _, m := range tstun.WireMTUsToProbe {
+ mtuProbePingSizesV4 = append(mtuProbePingSizesV4, pktLenToPingSize(m, false))
+ mtuProbePingSizesV6 = append(mtuProbePingSizesV6, pktLenToPingSize(m, true))
+ }
+}
+
// endpoint is a wireguard/conn.Endpoint. In wireguard-go and kernel WireGuard
// there is only one endpoint for a peer, but in Tailscale we distribute a
// number of possible endpoints for a peer which would include the all the
@@ -66,7 +77,7 @@ type endpoint struct {
lastFullPing mono.Time // last time we pinged all disco or wireguard only endpoints
derpAddr netip.AddrPort // fallback/bootstrap path, if non-zero (non-zero for well-behaved clients)
- bestAddr addrLatency // best non-DERP path; zero if none
+ bestAddr addrQuality // best non-DERP path; zero if none
bestAddrAt mono.Time // time best address re-confirmed
trustBestAddrUntil mono.Time // time when bestAddr expires
sentPing map[stun.TxID]sentPing
@@ -94,6 +105,7 @@ type sentPing struct {
at mono.Time
timer *time.Timer // timeout timer
purpose discoPingPurpose
+ size int // size of the disco message
res *ipnstate.PingResult // nil unless CLI ping
cb func(*ipnstate.PingResult) // nil unless CLI ping
}
@@ -208,7 +220,7 @@ func (de *endpoint) deleteEndpointLocked(why string, ep netip.AddrPort) {
What: "deleteEndpointLocked-bestAddr-" + why,
From: de.bestAddr,
})
- de.bestAddr = addrLatency{}
+ de.bestAddr = addrQuality{}
}
}
@@ -274,10 +286,12 @@ func (de *endpoint) DstToBytes() []byte { return packIPPort(de.fakeWGAddr) }
// addrForSendLocked returns the address(es) that should be used for
// sending the next packet. Zero, one, or both of UDP address and DERP
// addr may be non-zero. If the endpoint is WireGuard only and does not have
-// latency information, a bool is returned to indiciate that the
+// latency information, a bool is returned to indicate that the
// WireGuard latency discovery pings should be sent.
//
// de.mu must be held.
+//
+// TODO(val): Rewrite the addrFor*Locked() variations to share code.
func (de *endpoint) addrForSendLocked(now mono.Time) (udpAddr, derpAddr netip.AddrPort, sendWGPing bool) {
udpAddr = de.bestAddr.AddrPort
@@ -354,6 +368,41 @@ func (de *endpoint) addrForWireGuardSendLocked(now mono.Time) (udpAddr netip.Add
return udpAddr, needPing
}
+// addrForPingSizeLocked returns the address(es) that should be used for sending
+// the next ping. It will only return addrs with a large enough path MTU to
+// permit a ping payload of size bytes to be delivered (DERP is always one such
+// addr as it is a TCP connection). If it returns a zero-value udpAddr, then we
+// should continue probing the MTU of all paths to this endpoint. Zero, one, or
+// both of the returned UDP address and DERP address may be non-zero.
+//
+// de.mu must be held.
+func (de *endpoint) addrForPingSizeLocked(now mono.Time, size int) (udpAddr, derpAddr netip.AddrPort) {
+ if size == 0 {
+ udpAddr, derpAddr, _ = de.addrForSendLocked(now)
+ return
+ }
+
+ udpAddr = de.bestAddr.AddrPort
+ pathMTU := de.bestAddr.wireMTU
+ requestedMTU := pingSizeToPktLen(size, udpAddr.Addr().Is6())
+ mtuOk := requestedMTU <= pathMTU
+
+ if udpAddr.IsValid() && mtuOk {
+ if !now.After(de.trustBestAddrUntil) {
+ return udpAddr, netip.AddrPort{}
+ }
+ // We had a bestAddr with large enough MTU but it expired, so
+ // send both to it and DERP.
+ return udpAddr, de.derpAddr
+ }
+
+ // The UDP address isn't valid or it doesn't have a path MTU big enough
+ // for the packet. Return a zero-value udpAddr to signal that we should
+ // keep probing the path MTU to all addresses for this endpoint, and a
+ // valid DERP addr to signal that we should also send via DERP.
+ return netip.AddrPort{}, de.derpAddr
+}
+
// heartbeat is called every heartbeatInterval to keep the best UDP path alive,
// or kick off discovery of other paths.
func (de *endpoint) heartbeat() {
@@ -444,7 +493,7 @@ func (de *endpoint) cliPing(res *ipnstate.PingResult, size int, cb func(*ipnstat
}
now := mono.Now()
- udpAddr, derpAddr, _ := de.addrForSendLocked(now)
+ udpAddr, derpAddr := de.addrForPingSizeLocked(now, size)
if derpAddr.IsValid() {
de.startDiscoPingLocked(derpAddr, now, pingCLI, size, res, cb)
@@ -584,6 +633,12 @@ func (de *endpoint) sendDiscoPing(ep netip.AddrPort, discoKey key.DiscoPublic, t
}, logLevel)
if !sent {
de.forgetDiscoPing(txid)
+ return
+ }
+
+ if size != 0 {
+ metricSentDiscoPeerMTUProbes.Add(1)
+ metricSentDiscoPeerMTUProbeBytes.Add(int64(pingSizeToPktLen(size, ep.Addr().Is6())))
}
}
@@ -627,21 +682,41 @@ func (de *endpoint) startDiscoPingLocked(ep netip.AddrPort, now mono.Time, purpo
st.lastPing = now
}
- txid := stun.NewTxID()
- de.sentPing[txid] = sentPing{
- to: ep,
- at: now,
- timer: time.AfterFunc(pingTimeoutDuration, func() { de.discoPingTimeout(txid) }),
- purpose: purpose,
- res: res,
- cb: cb,
+ // If we are doing a discovery ping or a CLI ping with no specified size
+ // to a non DERP address, then probe the MTU. Otherwise just send the
+ // one specified ping.
+
+ // Default to sending a single ping of the specified size
+ sizes := []int{size}
+ if de.c.PeerMTUEnabled() {
+ isDerp := ep.Addr() == tailcfg.DerpMagicIPAddr
+ if !isDerp && ((purpose == pingDiscovery) || (purpose == pingCLI && size == 0)) {
+ de.c.dlogf("[v1] magicsock: starting MTU probe")
+ sizes = mtuProbePingSizesV4
+ if ep.Addr().Is6() {
+ sizes = mtuProbePingSizesV6
+ }
+ }
}
logLevel := discoLog
if purpose == pingHeartbeat {
logLevel = discoVerboseLog
}
- go de.sendDiscoPing(ep, epDisco.key, txid, size, logLevel)
+ for _, s := range sizes {
+ txid := stun.NewTxID()
+ de.sentPing[txid] = sentPing{
+ to: ep,
+ at: now,
+ timer: time.AfterFunc(pingTimeoutDuration, func() { de.discoPingTimeout(txid) }),
+ purpose: purpose,
+ res: res,
+ cb: cb,
+ size: s,
+ }
+ go de.sendDiscoPing(ep, epDisco.key, txid, s, logLevel)
+ }
+
}
// sendDiscoPingsLocked starts pinging all of ep's endpoints.
@@ -812,19 +887,7 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool) {
de.derpAddr = newDerp
}
- de.setEndpointsLocked(addrPortsFromStringsView{n.Endpoints()})
-}
-
-// addrPortsFromStringsView converts a view of AddrPort strings
-// to a view-like thing of netip.AddrPort.
-// TODO(bradfitz): change the type of tailcfg.Node.Endpoint.
-type addrPortsFromStringsView struct {
- views.Slice[string]
-}
-
-func (a addrPortsFromStringsView) At(i int) netip.AddrPort {
- ap, _ := netip.ParseAddrPort(a.Slice.At(i))
- return ap // or the zero value on error
+ de.setEndpointsLocked(n.Endpoints())
}
func (de *endpoint) setEndpointsLocked(eps interface {
@@ -919,7 +982,7 @@ func (de *endpoint) addCandidateEndpoint(ep netip.AddrPort, forRxPingTxID stun.T
//
// de.mu must be held.
func (de *endpoint) clearBestAddrLocked() {
- de.bestAddr = addrLatency{}
+ de.bestAddr = addrQuality{}
de.bestAddrAt = 0
de.trustBestAddrUntil = 0
}
@@ -952,6 +1015,41 @@ func (de *endpoint) noteConnectivityChange() {
}
}
+// pingSizeToPktLen calculates the minimum path MTU that would permit
+// a disco ping message of length size to reach its target at
+// addr. size is the length of the entire disco message including
+// disco headers. If size is zero, assume it is the safe wire MTU.
+func pingSizeToPktLen(size int, is6 bool) tstun.WireMTU {
+ if size == 0 {
+ return tstun.SafeWireMTU()
+ }
+ headerLen := ipv4.HeaderLen
+ if is6 {
+ headerLen = ipv6.HeaderLen
+ }
+ headerLen += 8 // UDP header length
+ return tstun.WireMTU(size + headerLen)
+}
+
+// pktLenToPingSize calculates the ping payload size that would
+// create a disco ping message whose on-the-wire length is exactly mtu
+// bytes long. If mtu is zero or less than the minimum ping size, then
+// no MTU probe is desired and return zero for an unpadded ping.
+func pktLenToPingSize(mtu tstun.WireMTU, is6 bool) int {
+ if mtu == 0 {
+ return 0
+ }
+ headerLen := ipv4.HeaderLen
+ if is6 {
+ headerLen = ipv6.HeaderLen
+ }
+ headerLen += 8 // UDP header length
+ if mtu < tstun.WireMTU(headerLen) {
+ return 0
+ }
+ return int(mtu) - headerLen
+}
+
// handlePongConnLocked handles a Pong message (a reply to an earlier ping).
// It should be called with the Conn.mu held.
//
@@ -970,6 +1068,15 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
knownTxID = true // for naked returns below
de.removeSentDiscoPingLocked(m.TxID, sp)
+ pktLen := int(pingSizeToPktLen(sp.size, sp.to.Addr().Is6()))
+ if sp.size != 0 {
+ m := getPeerMTUsProbedMetric(tstun.WireMTU(pktLen))
+ m.Add(1)
+ if metricMaxPeerMTUProbed.Value() < int64(pktLen) {
+ metricMaxPeerMTUProbed.Set(int64(pktLen))
+ }
+ }
+
now := mono.Now()
latency := now.Sub(sp.at)
@@ -991,7 +1098,7 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
}
if sp.purpose != pingHeartbeat {
- de.c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got pong tx=%x latency=%v pong.src=%v%v", de.c.discoShort, de.discoShort(), de.publicKey.ShortString(), src, m.TxID[:6], latency.Round(time.Millisecond), m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
+ de.c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got pong tx=%x latency=%v pktlen=%v pong.src=%v%v", de.c.discoShort, de.discoShort(), de.publicKey.ShortString(), src, m.TxID[:6], latency.Round(time.Millisecond), pktLen, m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
if sp.to != src {
fmt.Fprintf(bw, " ping.to=%v", sp.to)
}
@@ -1009,9 +1116,9 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
// Promote this pong response to our current best address if it's lower latency.
// TODO(bradfitz): decide how latency vs. preference order affects decision
if !isDerp {
- thisPong := addrLatency{sp.to, latency}
+ thisPong := addrQuality{sp.to, latency, tstun.WireMTU(pingSizeToPktLen(sp.size, sp.to.Addr().Is6()))}
if betterAddr(thisPong, de.bestAddr) {
- de.c.logf("magicsock: disco: node %v %v now using %v", de.publicKey.ShortString(), de.discoShort(), sp.to)
+ de.c.logf("magicsock: disco: node %v %v now using %v mtu=%v tx=%x", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU, m.TxID[:6])
de.debugUpdates.Add(EndpointChange{
When: time.Now(),
What: "handlePingLocked-bestAddr-update",
@@ -1035,19 +1142,28 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
return
}
-// addrLatency is an IPPort with an associated latency.
-type addrLatency struct {
+// addrQuality is an IPPort with an associated latency and path mtu.
+type addrQuality struct {
netip.AddrPort
latency time.Duration
+ wireMTU tstun.WireMTU
}
-func (a addrLatency) String() string {
- return a.AddrPort.String() + "@" + a.latency.String()
+func (a addrQuality) String() string {
+ return fmt.Sprintf("%v@%v+%v", a.AddrPort, a.latency, a.wireMTU)
}
// betterAddr reports whether a is a better addr to use than b.
-func betterAddr(a, b addrLatency) bool {
+func betterAddr(a, b addrQuality) bool {
if a.AddrPort == b.AddrPort {
+ if a.wireMTU > b.wireMTU {
+ // TODO(val): Think harder about the case of lower
+ // latency and smaller or unknown MTU, and higher
+ // latency but larger MTU. Probably in most cases the
+ // largest MTU will also be the lowest latency but we
+ // can't depend on that.
+ return true
+ }
return false
}
if !b.IsValid() {
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index cdb793e39b195..335ce56562c44 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -42,6 +42,7 @@ import (
"tailscale.com/net/portmapper"
"tailscale.com/net/sockstats"
"tailscale.com/net/stun"
+ "tailscale.com/net/tstun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
@@ -1567,7 +1568,7 @@ func (c *Conn) handlePingLocked(dm *disco.Ping, src netip.AddrPort, di *discoInf
if numNodes > 1 {
pingNodeSrcStr = "[one-of-multi]"
}
- c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got ping tx=%x", c.discoShort, di.discoShort, pingNodeSrcStr, src, dm.TxID[:6])
+ c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got ping tx=%x padding=%v", c.discoShort, di.discoShort, pingNodeSrcStr, src, dm.TxID[:6], dm.Padding)
}
ipDst := src
@@ -1893,7 +1894,7 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
// that differs from the one the NodeID had. But double check.
if ep.nodeID != n.ID() {
// Server error.
- devPanicf("public key moved between nodeIDs")
+ devPanicf("public key moved between nodeIDs (old=%v new=%v, key=%s)", ep.nodeID, n.ID(), n.Key().String())
} else {
// Internal data structures out of sync.
devPanicf("public key found in peerMap but not by nodeID")
@@ -2715,6 +2716,33 @@ func (c *Conn) getPinger() *ping.Pinger {
})
}
+// DebugPickNewDERP picks a new DERP random home temporarily (even if just for
+// seconds) and reports it to control. It exists to test DERP home changes and
+// netmap deltas, etc. It serves no useful user purpose.
+func (c *Conn) DebugPickNewDERP() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ dm := c.derpMap
+ if dm == nil {
+ return errors.New("no derpmap")
+ }
+ if c.netInfoLast == nil {
+ return errors.New("no netinfo")
+ }
+ for _, r := range dm.Regions {
+ if r.RegionID == c.myDerp {
+ continue
+ }
+ c.logf("magicsock: [debug] switching derp home to random %v (%v)", r.RegionID, r.RegionCode)
+ go c.setNearestDERP(r.RegionID)
+ ni2 := c.netInfoLast.Clone()
+ ni2.PreferredDERP = r.RegionID
+ c.callNetInfoCallbackLocked(ni2)
+ return nil
+ }
+ return errors.New("too few regions")
+}
+
// portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize,
// logging an error if it occurs.
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
@@ -2798,16 +2826,18 @@ var (
metricRecvDataIPv6 = clientmetric.NewCounter("magicsock_recv_data_ipv6")
// Disco packets
- metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
- metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
- metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
- metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
- metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
- metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
- metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
- metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
- metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
- metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
+ metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
+ metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
+ metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
+ metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
+ metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
+ metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
+ metricSentDiscoPeerMTUProbes = clientmetric.NewCounter("magicsock_disco_sent_peer_mtu_probes")
+ metricSentDiscoPeerMTUProbeBytes = clientmetric.NewCounter("magicsock_disco_sent_peer_mtu_probe_bytes")
+ metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
+ metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
+ metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
+ metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")
@@ -2825,4 +2855,18 @@ var (
// Disco packets received bpf read path
metricRecvDiscoPacketIPv4 = clientmetric.NewCounter("magicsock_disco_recv_bpf_ipv4")
metricRecvDiscoPacketIPv6 = clientmetric.NewCounter("magicsock_disco_recv_bpf_ipv6")
+
+ // metricMaxPeerMTUProbed is the largest peer path MTU we successfully probed.
+ metricMaxPeerMTUProbed = clientmetric.NewGauge("magicsock_max_peer_mtu_probed")
+
+ // metricRecvDiscoPeerMTUProbesByMTU collects the number of times we
+ // received an peer MTU probe response for a given MTU size.
+ // TODO: add proper support for label maps in clientmetrics
+ metricRecvDiscoPeerMTUProbesByMTU syncs.Map[string, *clientmetric.Metric]
)
+
+func getPeerMTUsProbedMetric(mtu tstun.WireMTU) *clientmetric.Metric {
+ key := fmt.Sprintf("magicsock_recv_disco_peer_mtu_probes_by_mtu_%d", mtu)
+ mm, _ := metricRecvDiscoPeerMTUProbesByMTU.LoadOrInit(key, func() *clientmetric.Metric { return clientmetric.NewCounter(key) })
+ return mm
+}
diff --git a/wgengine/magicsock/magicsock_linux.go b/wgengine/magicsock/magicsock_linux.go
index a4101ccbaa69d..1f7f5528d471a 100644
--- a/wgengine/magicsock/magicsock_linux.go
+++ b/wgengine/magicsock/magicsock_linux.go
@@ -318,13 +318,6 @@ func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
}
}
-const (
- // TODO(jwhited): upstream to unix?
- socketOptionLevelUDP = 17
- socketOptionUDPSegment = 103
- socketOptionUDPGRO = 104
-)
-
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
// and returns two booleans indicating TX and RX UDP offload support.
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
@@ -334,13 +327,13 @@ func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
return
}
err = rc.Control(func(fd uintptr) {
- _, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPSegment)
+ _, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
if errSyscall != nil {
// no point in checking RX, TX support was added first.
return
}
hasTX = true
- errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, socketOptionUDPGRO, 1)
+ errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
hasRX = errSyscall == nil
})
if err != nil {
@@ -367,11 +360,8 @@ func getGSOSizeFromControl(control []byte) (int, error) {
if err != nil {
return 0, fmt.Errorf("error parsing socket control message: %w", err)
}
- if hdr.Level == socketOptionLevelUDP && hdr.Type == socketOptionUDPGRO && len(data) >= 2 {
- var gso uint16
- // TODO(jwhited): replace with encoding/binary.NativeEndian when it's available
- copy(unsafe.Slice((*byte)(unsafe.Pointer(&gso)), 2), data[:2])
- return int(gso), nil
+ if hdr.Level == unix.SOL_UDP && hdr.Type == unix.UDP_GRO && len(data) >= 2 {
+ return int(binary.NativeEndian.Uint16(data[:2])), nil
}
}
return 0, nil
@@ -389,11 +379,10 @@ func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
}
*control = (*control)[:cap(*control)]
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&(*control)[0]))
- hdr.Level = socketOptionLevelUDP
- hdr.Type = socketOptionUDPSegment
+ hdr.Level = unix.SOL_UDP
+ hdr.Type = unix.UDP_SEGMENT
hdr.SetLen(unix.CmsgLen(2))
- // TODO(jwhited): replace with encoding/binary.NativeEndian when it's available
- copy((*control)[unix.SizeofCmsghdr:], unsafe.Slice((*byte)(unsafe.Pointer(&gsoSize)), 2))
+ binary.NativeEndian.PutUint16((*control)[unix.SizeofCmsghdr:], gsoSize)
*control = (*control)[:unix.CmsgSpace(2)]
}
diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go
index 970483f56827d..923eb36eb9c16 100644
--- a/wgengine/magicsock/magicsock_test.go
+++ b/wgengine/magicsock/magicsock_test.go
@@ -290,7 +290,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
DiscoKey: peer.conn.DiscoPublicKey(),
Addresses: addrs,
AllowedIPs: addrs,
- Endpoints: epStrings(eps[i]),
+ Endpoints: epFromTyped(eps[i]),
DERP: "127.3.3.40:1",
}
nm.Peers = append(nm.Peers, peer.View())
@@ -705,6 +705,8 @@ func TestDiscokeyChange(t *testing.T) {
}
func TestActiveDiscovery(t *testing.T) {
+ tstest.ResourceCheck(t)
+
t.Run("simple_internet", func(t *testing.T) {
t.Parallel()
mstun := &natlab.Machine{Name: "stun"}
@@ -900,7 +902,6 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
// get exercised.
func testActiveDiscovery(t *testing.T, d *devices) {
tstest.PanicOnLog()
- tstest.ResourceCheck(t)
tlogf, setT := makeNestable(t)
setT(t)
@@ -1265,7 +1266,7 @@ func addTestEndpoint(tb testing.TB, conn *Conn, sendConn net.PacketConn) (key.No
ID: 1,
Key: nodeKey,
DiscoKey: discoKey,
- Endpoints: []string{sendConn.LocalAddr().String()},
+ Endpoints: eps(sendConn.LocalAddr().String()),
},
}),
})
@@ -1470,7 +1471,7 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) {
ID: 1,
Key: nodeKey1,
DiscoKey: discoKey,
- Endpoints: []string{"192.168.1.2:345"},
+ Endpoints: eps("192.168.1.2:345"),
},
}),
})
@@ -1486,7 +1487,7 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) {
ID: 2,
Key: nodeKey2,
DiscoKey: discoKey,
- Endpoints: []string{"192.168.1.2:345"},
+ Endpoints: eps("192.168.1.2:345"),
},
}),
})
@@ -1648,10 +1649,13 @@ func TestEndpointSetsEqual(t *testing.T) {
func TestBetterAddr(t *testing.T) {
const ms = time.Millisecond
- al := func(ipps string, d time.Duration) addrLatency {
- return addrLatency{netip.MustParseAddrPort(ipps), d}
+ al := func(ipps string, d time.Duration) addrQuality {
+ return addrQuality{AddrPort: netip.MustParseAddrPort(ipps), latency: d}
}
- zero := addrLatency{}
+ almtu := func(ipps string, d time.Duration, mtu tstun.WireMTU) addrQuality {
+ return addrQuality{AddrPort: netip.MustParseAddrPort(ipps), latency: d, wireMTU: mtu}
+ }
+ zero := addrQuality{}
const (
publicV4 = "1.2.3.4:555"
@@ -1662,7 +1666,7 @@ func TestBetterAddr(t *testing.T) {
)
tests := []struct {
- a, b addrLatency
+ a, b addrQuality
want bool // whether a is better than b
}{
{a: zero, b: zero, want: false},
@@ -1724,7 +1728,12 @@ func TestBetterAddr(t *testing.T) {
b: al(publicV6, 100*ms),
want: true,
},
-
+ // If addresses are equal, prefer larger MTU
+ {
+ a: almtu(publicV4, 30*ms, 1500),
+ b: almtu(publicV4, 30*ms, 0),
+ want: true,
+ },
// Private IPs are preferred over public IPs even if the public
// IP is IPv6.
{
@@ -1752,13 +1761,21 @@ func TestBetterAddr(t *testing.T) {
}
-func epStrings(eps []tailcfg.Endpoint) (ret []string) {
+func epFromTyped(eps []tailcfg.Endpoint) (ret []netip.AddrPort) {
for _, ep := range eps {
- ret = append(ret, ep.Addr.String())
+ ret = append(ret, ep.Addr)
}
return
}
+func eps(s ...string) []netip.AddrPort {
+ var eps []netip.AddrPort
+ for _, ep := range s {
+ eps = append(eps, netip.MustParseAddrPort(ep))
+ }
+ return eps
+}
+
func TestStressSetNetworkMap(t *testing.T) {
t.Parallel()
@@ -1778,7 +1795,7 @@ func TestStressSetNetworkMap(t *testing.T) {
ID: tailcfg.NodeID(i) + 1,
DiscoKey: randDiscoKey(),
Key: randNodeKey(),
- Endpoints: []string{fmt.Sprintf("192.168.1.2:%d", i)},
+ Endpoints: eps(fmt.Sprintf("192.168.1.2:%d", i)),
}
}
@@ -2276,7 +2293,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) {
{
ID: 1,
Key: wgkey.Public(),
- Endpoints: []string{wgEp.String()},
+ Endpoints: []netip.AddrPort{wgEp},
IsWireGuardOnly: true,
Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip},
@@ -2337,7 +2354,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
{
ID: 1,
Key: wgkey.Public(),
- Endpoints: []string{wgEp.String()},
+ Endpoints: []netip.AddrPort{wgEp},
IsWireGuardOnly: true,
Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip},
@@ -2465,7 +2482,7 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{
{
Key: wgkey.Public(),
- Endpoints: []string{wgEp.String(), wgEp2.String(), wgEpV6.String()},
+ Endpoints: []netip.AddrPort{wgEp, wgEp2, wgEpV6},
IsWireGuardOnly: true,
Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip},
@@ -2877,3 +2894,112 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
})
}
}
+
+func TestAddrForPingSizeLocked(t *testing.T) {
+ testTime := mono.Now()
+
+ validUdpAddr := netip.MustParseAddrPort("1.1.1.1:111")
+ validDerpAddr := netip.MustParseAddrPort("2.2.2.2:222")
+
+ pingTests := []struct {
+ desc string
+ size int // size of ping payload
+ mtu tstun.WireMTU // The MTU of the path to bestAddr, if any
+ bestAddr bool // If the endpoint should have a valid bestAddr
+ bestAddrTrusted bool // If the bestAddr has not yet expired
+ wantUDP bool // Non-zero UDP addr means send to UDP; zero means start discovery
+ wantDERP bool // Non-zero DERP addr means send to DERP
+ }{
+ {
+ desc: "ping_size_0_and_invalid_UDP_addr_should_start_discovery_and_send_to_DERP",
+ size: 0,
+ bestAddr: false,
+ bestAddrTrusted: false,
+ wantUDP: false,
+ wantDERP: true,
+ },
+ {
+ desc: "ping_size_0_and_valid_trusted_UDP_addr_should_send_to_UDP_and_not_send_to_DERP",
+ size: 0,
+ bestAddr: true,
+ bestAddrTrusted: true,
+ wantUDP: true,
+ wantDERP: false,
+ },
+ {
+ desc: "ping_size_0_and_valid_but_expired_UDP_addr_should_send_to_both_UDP_and_DERP",
+ size: 0,
+ bestAddr: true,
+ bestAddrTrusted: false,
+ wantUDP: true,
+ wantDERP: true,
+ },
+ {
+ desc: "ping_size_too_big_for_trusted_UDP_addr_should_start_discovery_and_send_to_DERP",
+ size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
+ mtu: 1500,
+ bestAddr: true,
+ bestAddrTrusted: true,
+ wantUDP: false,
+ wantDERP: true,
+ },
+ {
+ desc: "ping_size_too_big_for_untrusted_UDP_addr_should_start_discovery_and_send_to_DERP",
+ size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
+ mtu: 1500,
+ bestAddr: true,
+ bestAddrTrusted: false,
+ wantUDP: false,
+ wantDERP: true,
+ },
+ {
+ desc: "ping_size_small_enough_for_trusted_UDP_addr_should_send_to_UDP_and_not_DERP",
+ size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
+ mtu: 1500,
+ bestAddr: true,
+ bestAddrTrusted: true,
+ wantUDP: true,
+ wantDERP: false,
+ },
+ {
+ desc: "ping_size_small_enough_for_untrusted_UDP_addr_should_send_to_UDP_and_DERP",
+ size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
+ mtu: 1500,
+ bestAddr: true,
+ bestAddrTrusted: false,
+ wantUDP: true,
+ wantDERP: true,
+ },
+ }
+
+ for _, test := range pingTests {
+ t.Run(test.desc, func(t *testing.T) {
+ bestAddr := addrQuality{wireMTU: test.mtu}
+ if test.bestAddr {
+ bestAddr.AddrPort = validUdpAddr
+ }
+ ep := &endpoint{
+ derpAddr: validDerpAddr,
+ bestAddr: bestAddr,
+ }
+ if test.bestAddrTrusted {
+ ep.trustBestAddrUntil = testTime.Add(1 * time.Second)
+ }
+
+ udpAddr, derpAddr := ep.addrForPingSizeLocked(testTime, test.size)
+
+ if test.wantUDP && !udpAddr.IsValid() {
+ t.Errorf("%s: udpAddr returned is not valid, won't be sent to UDP address", test.desc)
+ }
+ if !test.wantUDP && udpAddr.IsValid() {
+ t.Errorf("%s: udpAddr returned is valid, discovery will not start", test.desc)
+ }
+ if test.wantDERP && !derpAddr.IsValid() {
+ t.Errorf("%s: derpAddr returned is not valid, won't be sent to DERP", test.desc)
+ }
+ if !test.wantDERP && derpAddr.IsValid() {
+ t.Errorf("%s: derpAddr returned is valid, will be sent to DERP", test.desc)
+ }
+ })
+ }
+}
diff --git a/wgengine/magicsock/peermtu.go b/wgengine/magicsock/peermtu.go
index 8013aa5ea01e0..1995853235f7d 100644
--- a/wgengine/magicsock/peermtu.go
+++ b/wgengine/magicsock/peermtu.go
@@ -5,6 +5,8 @@
package magicsock
+import "tailscale.com/net/tstun"
+
// Peer path MTU routines shared by platforms that implement it.
// DontFragSetting returns true if at least one of the underlying sockets of
@@ -102,6 +104,9 @@ func (c *Conn) UpdatePMTUD() {
_ = c.setDontFragment("udp6", false)
newStatus = false
}
+ if debugPMTUD() {
+ c.logf("magicsock: peermtu: peer MTU probes are %v", tstun.WireMTUsToProbe)
+ }
c.peerMTUEnabled.Store(newStatus)
c.resetEndpointStates()
}
diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go
index 1b4e6d7b66ff1..4e393ce2a2f45 100644
--- a/wgengine/netstack/netstack.go
+++ b/wgengine/netstack/netstack.go
@@ -17,12 +17,11 @@ import (
"os/exec"
"runtime"
"strconv"
- "strings"
"sync"
"sync/atomic"
"time"
- "gvisor.dev/gvisor/pkg/bufferv2"
+ "gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
@@ -197,8 +196,14 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
ipstack.SetPromiscuousMode(nicID, true)
// Add IPv4 and IPv6 default routes, so all incoming packets from the Tailscale side
// are handled by the one fake NIC we use.
- ipv4Subnet, _ := tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 4)), tcpip.AddressMask(strings.Repeat("\x00", 4)))
- ipv6Subnet, _ := tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 16)), tcpip.AddressMask(strings.Repeat("\x00", 16)))
+ ipv4Subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(make([]byte, 4)), tcpip.MaskFromBytes(make([]byte, 4)))
+ if err != nil {
+ return nil, fmt.Errorf("could not create IPv4 subnet: %v", err)
+ }
+ ipv6Subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(make([]byte, 16)), tcpip.MaskFromBytes(make([]byte, 16)))
+ if err != nil {
+ return nil, fmt.Errorf("could not create IPv6 subnet: %v", err)
+ }
ipstack.SetRouteTable([]tcpip.Route{
{
Destination: ipv4Subnet,
@@ -241,7 +246,7 @@ func (ns *Impl) Close() error {
func (ns *Impl) wrapProtoHandler(h func(stack.TransportEndpointID, stack.PacketBufferPtr) bool) func(stack.TransportEndpointID, stack.PacketBufferPtr) bool {
return func(tei stack.TransportEndpointID, pb stack.PacketBufferPtr) bool {
addr := tei.LocalAddress
- ip, ok := netip.AddrFromSlice(net.IP(addr))
+ ip, ok := netip.AddrFromSlice(addr.AsSlice())
if !ok {
ns.logf("netstack: could not parse local address for incoming connection")
return false
@@ -280,10 +285,7 @@ func (ns *Impl) addSubnetAddress(ip netip.Addr) {
// Only register address into netstack for first concurrent connection.
if needAdd {
pa := tcpip.ProtocolAddress{
- AddressWithPrefix: tcpip.AddressWithPrefix{
- Address: tcpip.Address(ip.AsSlice()),
- PrefixLen: int(ip.BitLen()),
- },
+ AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
}
if ip.Is4() {
pa.Protocol = ipv4.ProtocolNumber
@@ -303,14 +305,14 @@ func (ns *Impl) removeSubnetAddress(ip netip.Addr) {
ns.connsOpenBySubnetIP[ip]--
// Only unregister address from netstack after last concurrent connection.
if ns.connsOpenBySubnetIP[ip] == 0 {
- ns.ipstack.RemoveAddress(nicID, tcpip.Address(ip.AsSlice()))
+ ns.ipstack.RemoveAddress(nicID, tcpip.AddrFromSlice(ip.AsSlice()))
delete(ns.connsOpenBySubnetIP, ip)
}
}
func ipPrefixToAddressWithPrefix(ipp netip.Prefix) tcpip.AddressWithPrefix {
return tcpip.AddressWithPrefix{
- Address: tcpip.Address(ipp.Addr().AsSlice()),
+ Address: tcpip.AddrFromSlice(ipp.Addr().AsSlice()),
PrefixLen: int(ipp.Bits()),
}
}
@@ -374,7 +376,7 @@ func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
}
ns.mu.Lock()
for ip := range ns.connsOpenBySubnetIP {
- ipp := tcpip.Address(ip.AsSlice()).WithPrefix()
+ ipp := tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix()
delete(ipsToBeRemoved, ipp)
}
ns.mu.Unlock()
@@ -391,7 +393,7 @@ func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
pa := tcpip.ProtocolAddress{
AddressWithPrefix: ipp,
}
- if ipp.Address.To4() == "" {
+ if ipp.Address.Unspecified() || ipp.Address.Len() == 16 {
pa.Protocol = ipv6.ProtocolNumber
} else {
pa.Protocol = ipv4.ProtocolNumber
@@ -447,7 +449,7 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
- Payload: bufferv2.MakeWithData(bytes.Clone(p.Buffer())),
+ Payload: buffer.MakeWithData(bytes.Clone(p.Buffer())),
})
ns.linkEP.InjectInbound(pn, packetBuf)
packetBuf.DecRef()
@@ -457,7 +459,7 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re
func (ns *Impl) DialContextTCP(ctx context.Context, ipp netip.AddrPort) (*gonet.TCPConn, error) {
remoteAddress := tcpip.FullAddress{
NIC: nicID,
- Addr: tcpip.Address(ipp.Addr().AsSlice()),
+ Addr: tcpip.AddrFromSlice(ipp.Addr().AsSlice()),
Port: ipp.Port(),
}
var ipType tcpip.NetworkProtocolNumber
@@ -473,7 +475,7 @@ func (ns *Impl) DialContextTCP(ctx context.Context, ipp netip.AddrPort) (*gonet.
func (ns *Impl) DialContextUDP(ctx context.Context, ipp netip.AddrPort) (*gonet.UDPConn, error) {
remoteAddress := &tcpip.FullAddress{
NIC: nicID,
- Addr: tcpip.Address(ipp.Addr().AsSlice()),
+ Addr: tcpip.AddrFromSlice(ipp.Addr().AsSlice()),
Port: ipp.Port(),
}
var ipType tcpip.NetworkProtocolNumber
@@ -739,7 +741,7 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
ns.logf("[v2] packet in (from %v): % x", p.Src, p.Buffer())
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
- Payload: bufferv2.MakeWithData(bytes.Clone(p.Buffer())),
+ Payload: buffer.MakeWithData(bytes.Clone(p.Buffer())),
})
ns.linkEP.InjectInbound(pn, packetBuf)
packetBuf.DecRef()
@@ -807,13 +809,13 @@ func (ns *Impl) shouldHandlePing(p *packet.Parsed) (_ netip.Addr, ok bool) {
}
func netaddrIPFromNetstackIP(s tcpip.Address) netip.Addr {
- switch len(s) {
+ switch s.Len() {
case 4:
+ s := s.As4()
return netaddr.IPv4(s[0], s[1], s[2], s[3])
case 16:
- var a [16]byte
- copy(a[:], s)
- return netip.AddrFrom16(a).Unmap()
+ s := s.As16()
+ return netip.AddrFrom16(s).Unmap()
}
return netip.Addr{}
}
@@ -1234,17 +1236,8 @@ func stringifyTEI(tei stack.TransportEndpointID) string {
}
func ipPortOfNetstackAddr(a tcpip.Address, port uint16) (ipp netip.AddrPort, ok bool) {
- var a16 [16]byte
- copy(a16[:], a)
- switch len(a) {
- case 4:
- return netip.AddrPortFrom(
- netip.AddrFrom4(*(*[4]byte)(a16[:4])).Unmap(),
- port,
- ), true
- case 16:
- return netip.AddrPortFrom(netip.AddrFrom16(a16).Unmap(), port), true
- default:
- return ipp, false
+ if addr, ok := netip.AddrFromSlice(a.AsSlice()); ok {
+ return netip.AddrPortFrom(addr, port), true
}
+ return netip.AddrPort{}, false
}
diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go
index 8a7273bd225c4..17bf38693eb8f 100644
--- a/wgengine/router/router_linux.go
+++ b/wgengine/router/router_linux.go
@@ -22,7 +22,6 @@ import (
"golang.org/x/sys/unix"
"golang.org/x/time/rate"
"tailscale.com/envknob"
- "tailscale.com/hostinfo"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
@@ -37,145 +36,6 @@ const (
netfilterOn = preftype.NetfilterOn
)
-// netfilterRunner abstracts helpers to run netfilter commands. It is
-// implemented by linuxfw.IPTablesRunner and linuxfw.NfTablesRunner.
-type netfilterRunner interface {
- AddLoopbackRule(addr netip.Addr) error
- DelLoopbackRule(addr netip.Addr) error
- AddHooks() error
- DelHooks(logf logger.Logf) error
- AddChains() error
- DelChains() error
- AddBase(tunname string) error
- DelBase() error
- AddSNATRule() error
- DelSNATRule() error
-
- HasIPV6() bool
- HasIPV6NAT() bool
-}
-
-// tableDetector abstracts helpers to detect the firewall mode.
-// It is implemented for testing purposes.
-type tableDetector interface {
- iptDetect() (int, error)
- nftDetect() (int, error)
-}
-
-type linuxFWDetector struct{}
-
-// iptDetect returns the number of iptables rules in the current namespace.
-func (l *linuxFWDetector) iptDetect() (int, error) {
- return linuxfw.DetectIptables()
-}
-
-// nftDetect returns the number of nftables rules in the current namespace.
-func (l *linuxFWDetector) nftDetect() (int, error) {
- return linuxfw.DetectNetfilter()
-}
-
-// chooseFireWallMode returns the firewall mode to use based on the
-// environment and the system's capabilities.
-func chooseFireWallMode(logf logger.Logf, det tableDetector) linuxfw.FirewallMode {
- if distro.Get() == distro.Gokrazy {
- // Reduce startup logging on gokrazy. There's no way to do iptables on
- // gokrazy anyway.
- return linuxfw.FirewallModeNfTables
- }
- iptAva, nftAva := true, true
- iptRuleCount, err := det.iptDetect()
- if err != nil {
- logf("detect iptables rule: %v", err)
- iptAva = false
- }
- nftRuleCount, err := det.nftDetect()
- if err != nil {
- logf("detect nftables rule: %v", err)
- nftAva = false
- }
- logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
- switch {
- case nftRuleCount > 0 && iptRuleCount == 0:
- logf("nftables is currently in use")
- hostinfo.SetFirewallMode("nft-inuse")
- return linuxfw.FirewallModeNfTables
- case iptRuleCount > 0 && nftRuleCount == 0:
- logf("iptables is currently in use")
- hostinfo.SetFirewallMode("ipt-inuse")
- return linuxfw.FirewallModeIPTables
- case nftAva:
- // if both iptables and nftables are available but
- // neither/both are currently used, use nftables.
- logf("nftables is available")
- hostinfo.SetFirewallMode("nft")
- return linuxfw.FirewallModeNfTables
- case iptAva:
- logf("iptables is available")
- hostinfo.SetFirewallMode("ipt")
- return linuxfw.FirewallModeIPTables
- default:
- // if neither iptables nor nftables are available, use iptablesRunner as a dummy
- // runner which exists but won't do anything. Creating iptablesRunner errors only
- // if the iptables command is missing or doesn’t support "--version", as long as it
- // can determine a version then it’ll carry on.
- hostinfo.SetFirewallMode("ipt-fb")
- return linuxfw.FirewallModeIPTables
- }
-}
-
-// newNetfilterRunner creates a netfilterRunner using either nftables or iptables.
-// As nftables is still experimental, iptables will be used unless TS_DEBUG_USE_NETLINK_NFTABLES is set.
-func newNetfilterRunner(logf logger.Logf) (netfilterRunner, error) {
- tableDetector := &linuxFWDetector{}
- var mode linuxfw.FirewallMode
-
- // We now use iptables as default and have "auto" and "nftables" as
- // options for people to test further.
- switch {
- case distro.Get() == distro.Gokrazy:
- // Reduce startup logging on gokrazy. There's no way to do iptables on
- // gokrazy anyway.
- logf("GoKrazy should use nftables.")
- hostinfo.SetFirewallMode("nft-gokrazy")
- mode = linuxfw.FirewallModeNfTables
- case envknob.String("TS_DEBUG_FIREWALL_MODE") == "nftables":
- logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
- hostinfo.SetFirewallMode("nft-forced")
- mode = linuxfw.FirewallModeNfTables
- case envknob.String("TS_DEBUG_FIREWALL_MODE") == "auto":
- mode = chooseFireWallMode(logf, tableDetector)
- case envknob.String("TS_DEBUG_FIREWALL_MODE") == "iptables":
- logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
- hostinfo.SetFirewallMode("ipt-forced")
- mode = linuxfw.FirewallModeIPTables
- default:
- logf("default choosing iptables")
- hostinfo.SetFirewallMode("ipt-default")
- mode = linuxfw.FirewallModeIPTables
- }
-
- var nfr netfilterRunner
- var err error
- switch mode {
- case linuxfw.FirewallModeIPTables:
- logf("using iptables")
- nfr, err = linuxfw.NewIPTablesRunner(logf)
- if err != nil {
- return nil, err
- }
- case linuxfw.FirewallModeNfTables:
- logf("using nftables")
- nfr, err = linuxfw.NewNfTablesRunner(logf)
- if err != nil {
- return nil, err
- }
- default:
- return nil, fmt.Errorf("unknown firewall mode: %v", mode)
- }
-
- return nfr, nil
-}
-
type linuxRouter struct {
closed atomic.Bool
logf func(fmt string, args ...any)
@@ -200,7 +60,7 @@ type linuxRouter struct {
// ipPolicyPrefBase is the base priority at which ip rules are installed.
ipPolicyPrefBase int
- nfr netfilterRunner
+ nfr linuxfw.NetfilterRunner
cmd commandRunner
}
@@ -210,7 +70,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
return nil, err
}
- nfr, err := newNetfilterRunner(logf)
+ nfr, err := linuxfw.New(logf)
if err != nil {
return nil, err
}
@@ -222,7 +82,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
return newUserspaceRouterAdvanced(logf, tunname, netMon, nfr, cmd)
}
-func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr netfilterRunner, cmd commandRunner) (Router, error) {
+func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr linuxfw.NetfilterRunner, cmd commandRunner) (Router, error) {
r := &linuxRouter{
logf: logf,
tunname: tunname,
diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go
index 761cdc44b40d1..d77708f51176f 100644
--- a/wgengine/router/router_linux_test.go
+++ b/wgengine/router/router_linux_test.go
@@ -372,7 +372,7 @@ type fakeIPTablesRunner struct {
//we always assume ipv6 and ipv6 nat are enabled when testing
}
-func newIPTablesRunner(t *testing.T) netfilterRunner {
+func newIPTablesRunner(t *testing.T) linuxfw.NetfilterRunner {
return &fakeIPTablesRunner{
t: t,
ipt4: map[string][]string{
@@ -603,7 +603,7 @@ type fakeOS struct {
rules []string
//This test tests on the router level, so we will not bother
//with using iptables or nftables, chose the simpler one.
- nfr netfilterRunner
+ nfr linuxfw.NetfilterRunner
}
func NewFakeOS(t *testing.T) *fakeOS {
@@ -1063,63 +1063,3 @@ func adjustFwmask(t *testing.T, s string) string {
return fwmaskAdjustRe.ReplaceAllString(s, "$1")
}
-
-type testFWDetector struct {
- iptRuleCount, nftRuleCount int
- iptErr, nftErr error
-}
-
-func (t *testFWDetector) iptDetect() (int, error) {
- return t.iptRuleCount, t.iptErr
-}
-
-func (t *testFWDetector) nftDetect() (int, error) {
- return t.nftRuleCount, t.nftErr
-}
-
-func TestChooseFireWallMode(t *testing.T) {
- tests := []struct {
- name string
- det *testFWDetector
- want linuxfw.FirewallMode
- }{
- {
- name: "using iptables legacy",
- det: &testFWDetector{iptRuleCount: 1},
- want: linuxfw.FirewallModeIPTables,
- },
- {
- name: "using nftables",
- det: &testFWDetector{nftRuleCount: 1},
- want: linuxfw.FirewallModeNfTables,
- },
- {
- name: "using both iptables and nftables",
- det: &testFWDetector{iptRuleCount: 2, nftRuleCount: 2},
- want: linuxfw.FirewallModeNfTables,
- },
- {
- name: "not using any firewall, both available",
- det: &testFWDetector{},
- want: linuxfw.FirewallModeNfTables,
- },
- {
- name: "not using any firewall, iptables available only",
- det: &testFWDetector{iptRuleCount: 1, nftErr: errors.New("nft error")},
- want: linuxfw.FirewallModeIPTables,
- },
- {
- name: "not using any firewall, nftables available only",
- det: &testFWDetector{iptErr: errors.New("iptables error"), nftRuleCount: 1},
- want: linuxfw.FirewallModeNfTables,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got := chooseFireWallMode(t.Logf, tt.det)
- if got != tt.want {
- t.Errorf("chooseFireWallMode() = %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go
index 155c29b469821..d51f7a7c62754 100644
--- a/wgengine/router/router_windows.go
+++ b/wgengine/router/router_windows.go
@@ -12,6 +12,7 @@ import (
"net/netip"
"os"
"os/exec"
+ "slices"
"strings"
"sync"
"syscall"
@@ -196,7 +197,7 @@ func (ft *firewallTweaker) doAsyncSet() {
ft.mu.Lock()
for { // invariant: ft.mu must be locked when beginning this block
val := ft.wantLocal
- if ft.known && strsEqual(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch && routesEqual(ft.localRoutes, ft.lastLocalRoutes) {
+ if ft.known && slices.Equal(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch && slices.Equal(ft.localRoutes, ft.lastLocalRoutes) {
ft.running = false
ft.logf("ending netsh goroutine")
ft.mu.Unlock()
@@ -341,28 +342,3 @@ func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, pr
// in via stdin encoded in json.
return ft.fwProcEncoder.Encode(allowedRoutes)
}
-
-func routesEqual(a, b []netip.Prefix) bool {
- if len(a) != len(b) {
- return false
- }
- // Routes are pre-sorted.
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
-}
-
-func strsEqual(a, b []string) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
-}
diff --git a/words/tails.txt b/words/tails.txt
index 19ef24d9c0ef7..a6fe8e9af20ab 100644
--- a/words/tails.txt
+++ b/words/tails.txt
@@ -555,3 +555,24 @@ neurotrichini
uropsilus
desmana
fynbos
+panthera
+onca
+pardus
+leo
+snow
+tigris
+clouded
+neofelis
+nebulosa
+felis
+acinonyx
+eland
+oryx
+gemsbok
+gazella
+nyala
+kudu
+giraffa
+raven
+corvus
+mellori