diff --git a/hscontrol/app.go b/hscontrol/app.go index 1dbf66b6..c93a6a2e 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -274,7 +274,7 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) { // Redirect to our TLS url. func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) { target := h.cfg.ServerURL + req.URL.RequestURI() - http.Redirect(w, req, target, http.StatusFound) + http.Redirect(w, req, target, http.StatusFound) //nolint:gosec // G710: target prefixed by trusted ServerURL } func (h *Headscale) scheduledTasks(ctx context.Context) { @@ -580,7 +580,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.Mux { r.HandleFunc("/v1/*", grpcMux.ServeHTTP) }) // Ping response endpoint: receives HEAD from clients responding - // to a PingRequest. The unguessable ping ID serves as authentication. + // to a [tailcfg.PingRequest]. The unguessable ping ID serves as authentication. r.Head("/machine/ping-response", h.PingResponseHandler) r.Get("/favicon.ico", FaviconHandler) @@ -1144,7 +1144,7 @@ func (h *Headscale) Change(cs ...change.Change) { h.mapBatcher.AddWork(cs...) } -// HTTPHandler returns an http.Handler for the Headscale control server. +// HTTPHandler returns an [http.Handler] for the [Headscale] control server. // The handler serves the Tailscale control protocol including the /key // endpoint and /ts2021 Noise upgrade path. func (h *Headscale) HTTPHandler() http.Handler { @@ -1224,7 +1224,7 @@ func (l *acmeLogger) RoundTrip(req *http.Request) (*http.Response, error) { return resp, nil } -// zerologRequestLogger implements chi's middleware.LogFormatter +// [zerologRequestLogger] implements chi's [middleware.LogFormatter] // to route HTTP request logs through zerolog. type zerologRequestLogger struct{} diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 5b595333..db89539d 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -27,15 +27,15 @@ const ( defaultOAuthOptionsCount = 3 authCacheExpiration = time.Minute * 15 - // authCacheMaxEntries bounds the OIDC state→AuthInfo cache to prevent + // authCacheMaxEntries bounds the OIDC state→[AuthInfo] cache to prevent // unauthenticated cache-fill DoS via repeated /register/{auth_id} or // /auth/{auth_id} GETs that mint OIDC state cookies. authCacheMaxEntries = 1024 // cookieNamePrefixLen is the number of leading characters from a - // state/nonce value that getCookieName splices into the cookie name. + // state/nonce value that [getCookieName] splices into the cookie name. // State and nonce values that are shorter than this are rejected at - // the callback boundary so getCookieName cannot panic on a slice + // the callback boundary so [getCookieName] cannot panic on a slice // out-of-range. cookieNamePrefixLen = 6 ) @@ -69,7 +69,7 @@ type AuthProviderOIDC struct { cfg *types.OIDCConfig // authCache holds auth information between the auth and the callback - // steps. It is a bounded LRU keyed by OIDC state, evicting oldest + // steps. It is a bounded [expirable.LRU] keyed by OIDC state, evicting oldest // entries to keep the cache footprint constant under attack. authCache *expirable.LRU[string, AuthInfo] @@ -286,9 +286,9 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler( util.LogErr(err, "could not get userinfo; only using claims from id token") } - // The oidc.UserInfo type only decodes some fields (Subject, Profile, Email, EmailVerified). + // The [oidc.UserInfo] type only decodes some fields (Subject, Profile, Email, EmailVerified). // We are interested in other fields too (e.g. groups are required for allowedGroups) so we - // decode into our own OIDCUserInfo type using the underlying claims struct. + // decode into our own [types.OIDCUserInfo] type using the underlying claims struct. var userinfo2 types.OIDCUserInfo if userinfo != nil && userinfo.Claims(&userinfo2) == nil && userinfo2.Sub == claims.Sub { // Update the user with the userinfo claims (with id token claims as fallback). @@ -444,10 +444,10 @@ func extractCodeAndStateParamFromRequest( return "", "", NewHTTPError(http.StatusBadRequest, "missing code or state parameter", errEmptyOIDCCallbackParams) } - // Reject states that are too short for getCookieName to splice + // Reject states that are too short for [getCookieName] to splice // into a cookie name. Without this guard a request with // ?state=abc panics on the slice out-of-range and is recovered by - // chi's middleware.Recoverer, amplifying small-DoS log noise. + // chi's [middleware.Recoverer], amplifying small-DoS log noise. if len(state) < cookieNamePrefixLen { return "", "", NewHTTPError(http.StatusBadRequest, "invalid state parameter", errOIDCStateTooShort) } @@ -552,15 +552,15 @@ func validateOIDCAllowedUsers( // // The following tests are always applied: // -// - validateOIDCAllowedGroups +// - [validateOIDCAllowedGroups] // // The following tests are applied if cfg.EmailVerifiedRequired=false // or claims.email_verified=true: // -// - validateOIDCAllowedDomains -// - validateOIDCAllowedUsers +// - [validateOIDCAllowedDomains] +// - [validateOIDCAllowedUsers] // -// NOTE that, contrary to the function name, validateOIDCAllowedUsers +// NOTE that, contrary to the function name, [validateOIDCAllowedUsers] // only checks the email address -- not the username. func doOIDCAuthorization( cfg *types.OIDCConfig, @@ -658,7 +658,7 @@ func (a *AuthProviderOIDC) createOrUpdateUserFromClaim( const registerConfirmCSRFCookie = "headscale_register_confirm" // renderRegistrationConfirmInterstitial captures the resolved OIDC -// identity and node expiry into the cached AuthRequest, sets the CSRF +// identity and node expiry into the cached [types.AuthRequest], sets the CSRF // cookie, and renders the confirmation page that the user must // explicitly submit before the registration is finalised. func (a *AuthProviderOIDC) renderRegistrationConfirmInterstitial( @@ -698,6 +698,7 @@ func (a *AuthProviderOIDC) renderRegistrationConfirmInterstitial( CSRF: csrf, }) + //nolint:gosec // G124: Secure set conditionally via req.TLS; HttpOnly + SameSite already set http.SetCookie(writer, &http.Cookie{ Name: registerConfirmCSRFCookie, Value: csrf, @@ -733,7 +734,7 @@ func (a *AuthProviderOIDC) renderRegistrationConfirmInterstitial( // RegisterConfirmHandler is the POST endpoint behind the OIDC // registration confirmation interstitial. It validates the CSRF cookie // against the form-submitted token, finalises the registration via -// handleRegistration, and renders the success page. +// [AuthProviderOIDC.handleRegistration], and renders the success page. func (a *AuthProviderOIDC) RegisterConfirmHandler( writer http.ResponseWriter, req *http.Request, @@ -823,6 +824,7 @@ func (a *AuthProviderOIDC) RegisterConfirmHandler( } // Clear the CSRF cookie now that the registration is final. + //nolint:gosec // G124: Secure set conditionally via req.TLS; HttpOnly + SameSite already set http.SetCookie(writer, &http.Cookie{ Name: registerConfirmCSRFCookie, Value: "", @@ -838,7 +840,7 @@ func (a *AuthProviderOIDC) RegisterConfirmHandler( writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.WriteHeader(http.StatusOK) - // renderRegistrationSuccessTemplate's output only embeds + // [renderRegistrationSuccessTemplate]'s output only embeds // HTML-escaped values from a server-side template, so the gosec // XSS warning is a false positive here. if _, err := writer.Write(content.Bytes()); err != nil { //nolint:noinlineerr,gosec @@ -918,9 +920,9 @@ func renderAuthSuccessTemplate( } // getCookieName generates a unique cookie name based on a cookie value. -// Callers must ensure value has at least cookieNamePrefixLen bytes; -// extractCodeAndStateParamFromRequest enforces this for the state -// parameter, and setCSRFCookie always supplies a 64-byte random value. +// Callers must ensure value has at least [cookieNamePrefixLen] bytes; +// [extractCodeAndStateParamFromRequest] enforces this for the state +// parameter, and [setCSRFCookie] always supplies a 64-byte random value. func getCookieName(baseName, value string) string { return fmt.Sprintf("%s_%s", baseName, value[:cookieNamePrefixLen]) } @@ -931,6 +933,7 @@ func setCSRFCookie(w http.ResponseWriter, r *http.Request, name string) (string, return val, err } + //nolint:gosec // G124: Secure set conditionally via r.TLS; HttpOnly + SameSite already set c := &http.Cookie{ Path: "/oidc/callback", Name: getCookieName(name, val), diff --git a/hscontrol/oidc_confirm_test.go b/hscontrol/oidc_confirm_test.go index 74af90ec..bb4e4ee5 100644 --- a/hscontrol/oidc_confirm_test.go +++ b/hscontrol/oidc_confirm_test.go @@ -24,6 +24,7 @@ func newConfirmRequest(t *testing.T, authID types.AuthID, formCSRF, cookieCSRF s form, ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + //nolint:gosec // G124: test fixture req.AddCookie(&http.Cookie{ Name: registerConfirmCSRFCookie, Value: cookieCSRF, @@ -67,7 +68,7 @@ func TestRegisterConfirmHandler_RejectsCSRFMismatch(t *testing.T) { "CSRF cookie/form mismatch must be rejected with 403") // And the registration must still be pending — the rejected POST - // must not have called handleRegistration. + // must not have called [AuthProviderOIDC.handleRegistration]. cached, ok := app.state.GetAuthCacheEntry(authID) require.True(t, ok, "rejected POST must not evict the cached registration") require.NotNil(t, cached.PendingConfirmation(), diff --git a/hscontrol/tailsql.go b/hscontrol/tailsql.go index d6ef380e..7f2a52ba 100644 --- a/hscontrol/tailsql.go +++ b/hscontrol/tailsql.go @@ -84,7 +84,7 @@ func runTailSQLService(ctx context.Context, logf logger.Logf, stateDir, dbPath s go func() { _ = http.Serve(lst, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //nolint:gosec target := base + r.RequestURI - http.Redirect(w, r, target, http.StatusPermanentRedirect) + http.Redirect(w, r, target, http.StatusPermanentRedirect) //nolint:gosec // G710: target prefixed by trusted base URL })) }() // log.Printf("Redirecting HTTP to HTTPS at %q", base) diff --git a/integration/scenario.go b/integration/scenario.go index eca239b9..2233cbdf 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -56,7 +56,7 @@ var ( errNoClientFound = errors.New("client not found") // AllVersions represents a list of Tailscale versions the suite - // uses to test compatibility with the ControlServer. + // uses to test compatibility with the [ControlServer]. // // The list contains two special cases, "head" and "unstable" which // points to the current tip of Tailscale's main branch and the latest @@ -78,7 +78,7 @@ var ( ) ) -// User represents a User in the ControlServer and a map of TailscaleClient's +// User represents a User in the [ControlServer] and a map of [TailscaleClient]'s // associated with the User. type User struct { Clients map[string]TailscaleClient @@ -88,10 +88,10 @@ type User struct { syncWaitGroup errgroup.Group } -// Scenario is a representation of an environment with one ControlServer and -// one or more User's and its associated TailscaleClients. +// Scenario is a representation of an environment with one [ControlServer] and +// one or more [User]'s and its associated [TailscaleClient]s. // A Scenario is intended to simplify setting up a new testcase for testing -// a ControlServer with TailscaleClients. +// a [ControlServer] with [TailscaleClient]s. // TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS. type Scenario struct { // TODO(kradalby): support multiple headcales for later, currently only @@ -177,8 +177,8 @@ func (s *Scenario) prefixedNetworkName(name string) string { return s.testHashPrefix + "-" + name } -// NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with -// a set of Users and TailscaleClients. +// NewScenario creates a test [Scenario] which can be used to bootstraps a [ControlServer] with +// a set of [User]s and [TailscaleClient]s. func NewScenario(spec ScenarioSpec) (*Scenario, error) { pool, err := dockertest.NewPool("") if err != nil { @@ -422,15 +422,15 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) { } } -// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) +// Shutdown shuts down and cleans up all the containers ([ControlServer], [TailscaleClient]) // and networks associated with it. -// In addition, it will save the logs of the ControlServer to `/tmp/control` in the +// In addition, it will save the logs of the [ControlServer] to `/tmp/control` in the // environment running the tests. func (s *Scenario) Shutdown() { s.ShutdownAssertNoPanics(nil) } -// Users returns the name of all users associated with the Scenario. +// Users returns the name of all users associated with the [Scenario]. func (s *Scenario) Users() []string { users := make([]string, 0, len(s.users)) for user := range s.users { @@ -443,8 +443,8 @@ func (s *Scenario) Users() []string { /// Headscale related stuff // Note: These functions assume that there is a _single_ headscale instance for now -// Headscale returns a ControlServer instance based on hsic (HeadscaleInContainer) -// If the Scenario already has an instance, the pointer to the running container +// Headscale returns a [ControlServer] instance based on hsic ([hsic.HeadscaleInContainer]). +// If the [Scenario] already has an instance, the pointer to the running container // will be return, otherwise a new instance will be created. // TODO(kradalby): make port and headscale configurable, multiple instances support? func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { @@ -474,12 +474,12 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { return headscale, nil } -// Pool returns the dockertest pool for the scenario. +// Pool returns the [dockertest.Pool] for the scenario. func (s *Scenario) Pool() *dockertest.Pool { return s.pool } -// GetOrCreateUser gets or creates a user in the scenario. +// GetOrCreateUser gets or creates a user in the [Scenario]. func (s *Scenario) GetOrCreateUser(userStr string) *User { s.mu.Lock() defer s.mu.Unlock() @@ -497,7 +497,7 @@ func (s *Scenario) GetOrCreateUser(userStr string) *User { } // CreatePreAuthKey creates a "pre authentorised key" to be created in the -// Headscale instance on behalf of the Scenario. +// Headscale instance on behalf of the [Scenario]. func (s *Scenario) CreatePreAuthKey( user uint64, reusable bool, @@ -516,7 +516,7 @@ func (s *Scenario) CreatePreAuthKey( } // CreatePreAuthKeyWithOptions creates a "pre authorised key" with the specified options -// to be created in the Headscale instance on behalf of the Scenario. +// to be created in the Headscale instance on behalf of the [Scenario]. func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error) { headscale, err := s.Headscale() if err != nil { @@ -532,7 +532,7 @@ func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.Pr } // CreatePreAuthKeyWithTags creates a "pre authorised key" with the specified tags -// to be created in the Headscale instance on behalf of the Scenario. +// to be created in the Headscale instance on behalf of the [Scenario]. func (s *Scenario) CreatePreAuthKeyWithTags( user uint64, reusable bool, @@ -552,8 +552,8 @@ func (s *Scenario) CreatePreAuthKeyWithTags( return key, nil } -// CreateUser creates a User to be created in the -// Headscale instance on behalf of the Scenario. +// CreateUser creates a [User] to be created in the +// Headscale instance on behalf of the [Scenario]. func (s *Scenario) CreateUser(user string) (*v1.User, error) { if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr u, err := headscale.CreateUser(user) @@ -619,8 +619,8 @@ func (s *Scenario) CreateTailscaleNode( return tsClient, nil } -// CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a -// User in the Scenario. +// CreateTailscaleNodesInUser creates and adds a new [TailscaleClient] to a +// [User] in the [Scenario]. func (s *Scenario) CreateTailscaleNodesInUser( userStr string, requestedVersion string, @@ -720,8 +720,8 @@ func (s *Scenario) CreateTailscaleNodesInUser( return fmt.Errorf("adding tailscale node: %w", errNoUserAvailable) } -// RunTailscaleUp will log in all of the TailscaleClients associated with a -// User to the given ControlServer (by URL). +// RunTailscaleUp will log in all of the [TailscaleClient]s associated with a +// [User] to the given [ControlServer] (by URL). func (s *Scenario) RunTailscaleUp( userStr, loginServer, authKey string, ) error { @@ -752,7 +752,7 @@ func (s *Scenario) RunTailscaleUp( return fmt.Errorf("bringing up tailscale node: %w", errNoUserAvailable) } -// CountTailscale returns the total number of TailscaleClients in a Scenario. +// CountTailscale returns the total number of [TailscaleClient]s in a [Scenario]. // This is the sum of Users x TailscaleClients. func (s *Scenario) CountTailscale() int { count := 0 @@ -764,8 +764,8 @@ func (s *Scenario) CountTailscale() int { return count } -// WaitForTailscaleSync blocks execution until all the TailscaleClient reports -// to have all other TailscaleClients present in their netmap.NetworkMap. +// WaitForTailscaleSync blocks execution until all the [TailscaleClient] reports +// to have all other [TailscaleClient]s present in their [netmap.NetworkMap]. func (s *Scenario) WaitForTailscaleSync() error { tsCount := s.CountTailscale() @@ -784,7 +784,7 @@ func (s *Scenario) WaitForTailscaleSync() error { return err } -// SyncOption configures WaitForTailscaleSyncPerUser. +// SyncOption configures [Scenario.WaitForTailscaleSyncPerUser]. type SyncOption func(*syncOptions) type syncOptions struct { @@ -799,7 +799,7 @@ func WithPreBarrier(barrier func(context.Context) error) SyncOption { return func(o *syncOptions) { o.preBarrier = barrier } } -// WaitForTailscaleSyncPerUser blocks until each TailscaleClient has +// WaitForTailscaleSyncPerUser blocks until each [TailscaleClient] has // the expected per-user peer count (necessary for policies like // autogroup:self where cross-user peers are invisible). func (s *Scenario) WaitForTailscaleSyncPerUser(timeout, retryInterval time.Duration, opts ...SyncOption) error { @@ -848,8 +848,8 @@ func (s *Scenario) WaitForTailscaleSyncPerUser(timeout, retryInterval time.Durat return nil } -// WaitForTailscaleSyncWithPeerCount blocks execution until all the TailscaleClient reports -// to have all other TailscaleClients present in their netmap.NetworkMap. +// WaitForTailscaleSyncWithPeerCount blocks execution until all the [TailscaleClient] reports +// to have all other [TailscaleClient]s present in their [netmap.NetworkMap]. func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int, timeout, retryInterval time.Duration) error { var allErrors []error @@ -890,7 +890,7 @@ func (s *Scenario) CreateHeadscaleEnv( } // CreateHeadscaleEnv starts the headscale environment and the clients -// according to the ScenarioSpec passed to the Scenario. +// according to the [ScenarioSpec] passed to the [Scenario]. func (s *Scenario) createHeadscaleEnv( withURL bool, tsOpts []tsic.Option, @@ -900,7 +900,7 @@ func (s *Scenario) createHeadscaleEnv( } // createHeadscaleEnvWithTags starts the headscale environment and the clients -// according to the ScenarioSpec passed to the Scenario. If preAuthKeyTags is +// according to the [ScenarioSpec] passed to the [Scenario]. If preAuthKeyTags is // non-empty and withURL is false, the tags will be applied to the PreAuthKey // (tags-as-identity model). // @@ -1381,6 +1381,7 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return nil, err } + //nolint:gosec // G706: integration-only log of trusted scenario state log.Printf(` --- %s - method: %s | url: %s @@ -1391,8 +1392,8 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return resp, nil } -// GetIPs returns all netip.Addr of TailscaleClients associated with a User -// in a Scenario. +// GetIPs returns all [netip.Addr] of [TailscaleClient]s associated with a [User] +// in a [Scenario]. func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) { var ips []netip.Addr @@ -1412,7 +1413,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) { return ips, fmt.Errorf("getting IPs: %w", errNoUserAvailable) } -// GetClients returns all TailscaleClients associated with a User in a Scenario. +// GetClients returns all [TailscaleClient]s associated with a [User] in a [Scenario]. func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) { var clients []TailscaleClient @@ -1427,7 +1428,7 @@ func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) { return clients, fmt.Errorf("getting clients: %w", errNoUserAvailable) } -// ListTailscaleClients returns a list of TailscaleClients given the Users +// ListTailscaleClients returns a list of [TailscaleClient]s given the [User]s // passed as parameters. func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) { var allClients []TailscaleClient @@ -1448,7 +1449,7 @@ func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, err return allClients, nil } -// FindTailscaleClientByIP returns a TailscaleClient associated with an IP address +// FindTailscaleClientByIP returns a [TailscaleClient] associated with an IP address // if it exists. func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) { clients, err := s.ListTailscaleClients() @@ -1466,7 +1467,7 @@ func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, erro return nil, errNoClientFound } -// ListTailscaleClientsIPs returns a list of netip.Addr based on Users +// ListTailscaleClientsIPs returns a list of [netip.Addr] based on [User]s // passed as parameters. func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) { var allIps []netip.Addr @@ -1487,7 +1488,7 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error return allIps, nil } -// ListTailscaleClientsFQDNs returns a list of FQDN based on Users +// ListTailscaleClientsFQDNs returns a list of FQDN based on [User]s // passed as parameters. func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) { allFQDNs := make([]string, 0) @@ -1509,8 +1510,8 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) return allFQDNs, nil } -// WaitForTailscaleLogout blocks execution until all TailscaleClients have -// logged out of the ControlServer. +// WaitForTailscaleLogout blocks execution until all [TailscaleClient]s have +// logged out of the [ControlServer]. func (s *Scenario) WaitForTailscaleLogout() error { for _, user := range s.users { for _, client := range user.Clients {