all: fix golangci-lint issues (#3064)

This commit is contained in:
Kristoffer Dalby 2026-02-06 21:45:32 +01:00 committed by GitHub
parent bfb6fd80df
commit ce580f8245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
131 changed files with 3131 additions and 1560 deletions

View file

@ -20,7 +20,11 @@ const (
DatabaseSqlite = "sqlite3"
)
var ErrCannotParsePrefix = errors.New("cannot parse prefix")
// Common errors.
var (
ErrCannotParsePrefix = errors.New("cannot parse prefix")
ErrInvalidRegistrationIDLength = errors.New("registration ID has invalid length")
)
type StateUpdateType int
@ -100,6 +104,10 @@ func (su *StateUpdate) Empty() bool {
return len(su.ChangePatches) == 0
case StatePeerRemoved:
return len(su.Removed) == 0
case StateFullUpdate, StateSelfUpdate, StateDERPUpdated:
// These update types don't have associated data to check,
// so they are never considered empty.
return false
}
return false
@ -175,8 +183,9 @@ func MustRegistrationID() RegistrationID {
func RegistrationIDFromString(str string) (RegistrationID, error) {
if len(str) != RegistrationIDLength {
return "", fmt.Errorf("registration ID must be %d characters long", RegistrationIDLength)
return "", fmt.Errorf("%w: expected %d, got %d", ErrInvalidRegistrationIDLength, RegistrationIDLength, len(str))
}
return RegistrationID(str), nil
}

View file

@ -33,10 +33,12 @@ const (
)
var (
errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
errServerURLSuffix = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
errServerURLSame = errors.New("server_url cannot use the same domain as base_domain in a way that could make the DERP and headscale server unreachable")
errInvalidPKCEMethod = errors.New("pkce.method must be either 'plain' or 'S256'")
errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
errServerURLSuffix = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
errServerURLSame = errors.New("server_url cannot use the same domain as base_domain in a way that could make the DERP and headscale server unreachable")
errInvalidPKCEMethod = errors.New("pkce.method must be either 'plain' or 'S256'")
ErrNoPrefixConfigured = errors.New("no IPv4 or IPv6 prefix configured, minimum one prefix is required")
ErrInvalidAllocationStrategy = errors.New("invalid prefix allocation strategy")
)
type IPAllocationStrategy string
@ -301,6 +303,7 @@ func validatePKCEMethod(method string) error {
if method != PKCEMethodPlain && method != PKCEMethodS256 {
return errInvalidPKCEMethod
}
return nil
}
@ -326,6 +329,7 @@ func LoadConfig(path string, isFile bool) error {
viper.SetConfigFile(path)
} else {
viper.SetConfigName("config")
if path == "" {
viper.AddConfigPath("/etc/headscale/")
viper.AddConfigPath("$HOME/.headscale")
@ -401,8 +405,10 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential))
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err := viper.ReadInConfig()
if err != nil {
var configFileNotFoundError viper.ConfigFileNotFoundError
if errors.As(err, &configFileNotFoundError) {
log.Warn().Msg("no config file found, using defaults")
return nil
}
@ -442,7 +448,8 @@ func validateServerConfig() error {
depr.fatal("oidc.map_legacy_users")
if viper.GetBool("oidc.enabled") {
if err := validatePKCEMethod(viper.GetString("oidc.pkce.method")); err != nil {
err := validatePKCEMethod(viper.GetString("oidc.pkce.method"))
if err != nil {
return err
}
}
@ -556,6 +563,7 @@ func derpConfig() DERPConfig {
automaticallyAddEmbeddedDerpRegion := viper.GetBool(
"derp.server.automatically_add_embedded_derp_region",
)
if serverEnabled && stunAddr == "" {
log.Fatal().
Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
@ -625,13 +633,16 @@ func policyConfig() PolicyConfig {
func logConfig() LogConfig {
logLevelStr := viper.GetString("log.level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}
logFormatOpt := viper.GetString("log.format")
var logFormat string
switch logFormatOpt {
case JSONLogFormat:
logFormat = JSONLogFormat
@ -658,7 +669,7 @@ func databaseConfig() DatabaseConfig {
type_ := viper.GetString("database.type")
skipErrRecordNotFound := viper.GetBool("database.gorm.skip_err_record_not_found")
slowThreshold := viper.GetDuration("database.gorm.slow_threshold") * time.Millisecond
slowThreshold := time.Duration(viper.GetInt64("database.gorm.slow_threshold")) * time.Millisecond
parameterizedQueries := viper.GetBool("database.gorm.parameterized_queries")
prepareStmt := viper.GetBool("database.gorm.prepare_stmt")
@ -730,6 +741,7 @@ func dns() (DNSConfig, error) {
if err != nil {
return DNSConfig{}, fmt.Errorf("unmarshalling dns extra records: %w", err)
}
dns.ExtraRecords = extraRecords
}
@ -745,30 +757,23 @@ func (d *DNSConfig) globalResolvers() []*dnstype.Resolver {
var resolvers []*dnstype.Resolver
for _, nsStr := range d.Nameservers.Global {
warn := ""
if _, err := netip.ParseAddr(nsStr); err == nil {
if _, err := netip.ParseAddr(nsStr); err == nil { //nolint:noinlineerr
resolvers = append(resolvers, &dnstype.Resolver{
Addr: nsStr,
})
continue
} else {
warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err)
}
if _, err := url.Parse(nsStr); err == nil {
if _, err := url.Parse(nsStr); err == nil { //nolint:noinlineerr
resolvers = append(resolvers, &dnstype.Resolver{
Addr: nsStr,
})
continue
} else {
warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err)
}
if warn != "" {
log.Warn().Msg(warn)
}
log.Warn().Str("nameserver", nsStr).Msg("invalid global nameserver, ignoring")
}
return resolvers
@ -780,34 +785,30 @@ func (d *DNSConfig) globalResolvers() []*dnstype.Resolver {
// If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
func (d *DNSConfig) splitResolvers() map[string][]*dnstype.Resolver {
routes := make(map[string][]*dnstype.Resolver)
for domain, nameservers := range d.Nameservers.Split {
var resolvers []*dnstype.Resolver
for _, nsStr := range nameservers {
warn := ""
if _, err := netip.ParseAddr(nsStr); err == nil {
if _, err := netip.ParseAddr(nsStr); err == nil { //nolint:noinlineerr
resolvers = append(resolvers, &dnstype.Resolver{
Addr: nsStr,
})
continue
} else {
warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err)
}
if _, err := url.Parse(nsStr); err == nil {
if _, err := url.Parse(nsStr); err == nil { //nolint:noinlineerr
resolvers = append(resolvers, &dnstype.Resolver{
Addr: nsStr,
})
continue
} else {
warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err)
}
if warn != "" {
log.Warn().Msg(warn)
}
log.Warn().Str("nameserver", nsStr).Str("domain", domain).Msg("invalid split dns nameserver, ignoring")
}
routes[domain] = resolvers
}
@ -822,6 +823,7 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
}
cfg.Proxied = dns.MagicDNS
cfg.ExtraRecords = dns.ExtraRecords
if dns.OverrideLocalDNS {
cfg.Resolvers = dns.globalResolvers()
@ -830,10 +832,12 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
}
routes := dns.splitResolvers()
cfg.Routes = routes
if dns.BaseDomain != "" {
cfg.Domains = []string{dns.BaseDomain}
}
cfg.Domains = append(cfg.Domains, dns.SearchDomains...)
return &cfg
@ -843,7 +847,7 @@ func prefixV4() (*netip.Prefix, error) {
prefixV4Str := viper.GetString("prefixes.v4")
if prefixV4Str == "" {
return nil, nil
return nil, nil //nolint:nilnil // empty prefix is valid, not an error
}
prefixV4, err := netip.ParsePrefix(prefixV4Str)
@ -853,6 +857,7 @@ func prefixV4() (*netip.Prefix, error) {
builder := netipx.IPSetBuilder{}
builder.AddPrefix(tsaddr.CGNATRange())
ipSet, _ := builder.IPSet()
if !ipSet.ContainsPrefix(prefixV4) {
log.Warn().
@ -867,7 +872,7 @@ func prefixV6() (*netip.Prefix, error) {
prefixV6Str := viper.GetString("prefixes.v6")
if prefixV6Str == "" {
return nil, nil
return nil, nil //nolint:nilnil // empty prefix is valid, not an error
}
prefixV6, err := netip.ParsePrefix(prefixV6Str)
@ -910,7 +915,7 @@ func LoadCLIConfig() (*Config, error) {
// LoadServerConfig returns the full Headscale configuration to
// host a Headscale server. This is called as part of `headscale serve`.
func LoadServerConfig() (*Config, error) {
if err := validateServerConfig(); err != nil {
if err := validateServerConfig(); err != nil { //nolint:noinlineerr
return nil, err
}
@ -928,11 +933,13 @@ func LoadServerConfig() (*Config, error) {
}
if prefix4 == nil && prefix6 == nil {
return nil, errors.New("no IPv4 or IPv6 prefix configured, minimum one prefix is required")
return nil, ErrNoPrefixConfigured
}
allocStr := viper.GetString("prefixes.allocation")
var alloc IPAllocationStrategy
switch allocStr {
case string(IPAllocationStrategySequential):
alloc = IPAllocationStrategySequential
@ -940,7 +947,8 @@ func LoadServerConfig() (*Config, error) {
alloc = IPAllocationStrategyRandom
default:
return nil, fmt.Errorf(
"config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s",
"%w: %q, allowed options: %s, %s",
ErrInvalidAllocationStrategy,
allocStr,
IPAllocationStrategySequential,
IPAllocationStrategyRandom,
@ -957,15 +965,18 @@ func LoadServerConfig() (*Config, error) {
randomizeClientPort := viper.GetBool("randomize_client_port")
oidcClientSecret := viper.GetString("oidc.client_secret")
oidcClientSecretPath := viper.GetString("oidc.client_secret_path")
if oidcClientSecretPath != "" && oidcClientSecret != "" {
return nil, errOidcMutuallyExclusive
}
if oidcClientSecretPath != "" {
secretBytes, err := os.ReadFile(os.ExpandEnv(oidcClientSecretPath))
if err != nil {
return nil, err
}
oidcClientSecret = strings.TrimSpace(string(secretBytes))
}
@ -979,7 +990,8 @@ func LoadServerConfig() (*Config, error) {
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
if dnsConfig.BaseDomain != "" {
if err := isSafeServerURL(serverURL, dnsConfig.BaseDomain); err != nil {
err := isSafeServerURL(serverURL, dnsConfig.BaseDomain)
if err != nil {
return nil, err
}
}
@ -994,7 +1006,7 @@ func LoadServerConfig() (*Config, error) {
PrefixV4: prefix4,
PrefixV6: prefix6,
IPAllocation: IPAllocationStrategy(alloc),
IPAllocation: alloc,
NoisePrivateKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("noise.private_key_path"),
@ -1082,6 +1094,7 @@ func LoadServerConfig() (*Config, error) {
if workers := viper.GetInt("tuning.batcher_workers"); workers > 0 {
return workers
}
return DefaultBatcherWorkers()
}(),
RegisterCacheCleanup: viper.GetDuration("tuning.register_cache_cleanup"),
@ -1117,6 +1130,7 @@ func isSafeServerURL(serverURL, baseDomain string) error {
}
s := len(serverDomainParts)
b := len(baseDomainParts)
for i := range baseDomainParts {
if serverDomainParts[s-i-1] != baseDomainParts[b-i-1] {
@ -1134,9 +1148,12 @@ type deprecator struct {
// warnWithAlias will register an alias between the newKey and the oldKey,
// and log a deprecation warning if the oldKey is set.
//
//nolint:unused
func (d *deprecator) warnWithAlias(newKey, oldKey string) {
// NOTE: RegisterAlias is called with NEW KEY -> OLD KEY
viper.RegisterAlias(newKey, oldKey)
if viper.IsSet(oldKey) {
d.warns.Add(
fmt.Sprintf(
@ -1179,6 +1196,8 @@ func (d *deprecator) fatalIfNewKeyIsNotUsed(newKey, oldKey string) {
}
// warn deprecates and adds an option to log a warning if the oldKey is set.
//
//nolint:unused
func (d *deprecator) warnNoAlias(newKey, oldKey string) {
if viper.IsSet(oldKey) {
d.warns.Add(
@ -1193,6 +1212,8 @@ func (d *deprecator) warnNoAlias(newKey, oldKey string) {
}
// warn deprecates and adds an entry to the warn list of options if the oldKey is set.
//
//nolint:unused
func (d *deprecator) warn(oldKey string) {
if viper.IsSet(oldKey) {
d.warns.Add(

View file

@ -26,7 +26,7 @@ func TestReadConfig(t *testing.T) {
{
name: "unmarshal-dns-full-config",
configPath: "testdata/dns_full.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
dns, err := dns()
if err != nil {
return nil, err
@ -61,7 +61,7 @@ func TestReadConfig(t *testing.T) {
{
name: "dns-to-tailcfg.DNSConfig",
configPath: "testdata/dns_full.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
dns, err := dns()
if err != nil {
return nil, err
@ -92,7 +92,7 @@ func TestReadConfig(t *testing.T) {
{
name: "unmarshal-dns-full-no-magic",
configPath: "testdata/dns_full_no_magic.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
dns, err := dns()
if err != nil {
return nil, err
@ -127,7 +127,7 @@ func TestReadConfig(t *testing.T) {
{
name: "dns-to-tailcfg.DNSConfig",
configPath: "testdata/dns_full_no_magic.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
dns, err := dns()
if err != nil {
return nil, err
@ -158,7 +158,7 @@ func TestReadConfig(t *testing.T) {
{
name: "base-domain-in-server-url-err",
configPath: "testdata/base-domain-in-server-url.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
return LoadServerConfig()
},
want: nil,
@ -167,7 +167,7 @@ func TestReadConfig(t *testing.T) {
{
name: "base-domain-not-in-server-url",
configPath: "testdata/base-domain-not-in-server-url.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
cfg, err := LoadServerConfig()
if err != nil {
return nil, err
@ -187,7 +187,7 @@ func TestReadConfig(t *testing.T) {
{
name: "dns-override-true-errors",
configPath: "testdata/dns-override-true-error.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
return LoadServerConfig()
},
wantErr: "Fatal config error: dns.nameservers.global must be set when dns.override_local_dns is true",
@ -195,7 +195,7 @@ func TestReadConfig(t *testing.T) {
{
name: "dns-override-true",
configPath: "testdata/dns-override-true.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper
_, err := LoadServerConfig()
if err != nil {
return nil, err
@ -221,7 +221,7 @@ func TestReadConfig(t *testing.T) {
{
name: "policy-path-is-loaded",
configPath: "testdata/policy-path-is-loaded.yaml",
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper // inline test closure
cfg, err := LoadServerConfig()
if err != nil {
return nil, err
@ -242,6 +242,7 @@ func TestReadConfig(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
viper.Reset()
err := LoadConfig(tt.configPath, true)
require.NoError(t, err)
@ -276,14 +277,14 @@ func TestReadConfigFromEnv(t *testing.T) {
"HEADSCALE_DATABASE_SQLITE_WRITE_AHEAD_LOG": "false",
"HEADSCALE_PREFIXES_V4": "100.64.0.0/10",
},
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper // inline test closure
t.Logf("all settings: %#v", viper.AllSettings())
assert.Equal(t, "trace", viper.GetString("log.level"))
assert.Equal(t, "100.64.0.0/10", viper.GetString("prefixes.v4"))
assert.False(t, viper.GetBool("database.sqlite.write_ahead_log"))
return nil, nil
return nil, nil //nolint:nilnil // test setup returns nil to indicate no expected value
},
want: nil,
},
@ -300,7 +301,7 @@ func TestReadConfigFromEnv(t *testing.T) {
// "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
// "HEADSCALE_DNS_EXTRA_RECORDS": `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`,
},
setup: func(t *testing.T) (any, error) {
setup: func(t *testing.T) (any, error) { //nolint:thelper // inline test closure
t.Logf("all settings: %#v", viper.AllSettings())
dns, err := dns()
@ -335,6 +336,7 @@ func TestReadConfigFromEnv(t *testing.T) {
}
viper.Reset()
err := LoadConfig("testdata/minimal.yaml", true)
require.NoError(t, err)
@ -349,11 +351,10 @@ func TestReadConfigFromEnv(t *testing.T) {
}
func TestTLSConfigValidation(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "headscale")
if err != nil {
t.Fatal(err)
}
// defer os.RemoveAll(tmpDir)
tmpDir := t.TempDir()
var err error
configYaml := []byte(`---
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: ""
@ -363,6 +364,7 @@ noise:
// Populate a custom config file
configFilePath := filepath.Join(tmpDir, "config.yaml")
err = os.WriteFile(configFilePath, configYaml, 0o600)
if err != nil {
t.Fatalf("Couldn't write file %s", configFilePath)
@ -398,10 +400,12 @@ server_url: http://127.0.0.1:8080
tls_letsencrypt_hostname: example.com
tls_letsencrypt_challenge_type: TLS-ALPN-01
`)
err = os.WriteFile(configFilePath, configYaml, 0o600)
if err != nil {
t.Fatalf("Couldn't write file %s", configFilePath)
}
err = LoadConfig(tmpDir, false)
require.NoError(t, err)
}
@ -463,6 +467,7 @@ func TestSafeServerURL(t *testing.T) {
return
}
assert.NoError(t, err)
})
}

View file

@ -53,7 +53,7 @@ func (id NodeID) StableID() tailcfg.StableNodeID {
}
func (id NodeID) NodeID() tailcfg.NodeID {
return tailcfg.NodeID(id)
return tailcfg.NodeID(id) //nolint:gosec // NodeID is bounded
}
func (id NodeID) Uint64() uint64 {
@ -162,11 +162,12 @@ func (node *Node) GivenNameHasBeenChanged() bool {
// Strip invalid DNS characters for givenName comparison
normalised := strings.ToLower(node.Hostname)
normalised = invalidDNSRegex.ReplaceAllString(normalised, "")
return node.GivenName == normalised
}
// IsExpired returns whether the node registration has expired.
func (node Node) IsExpired() bool {
func (node *Node) IsExpired() bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefore considered
// to mean "not expired"
@ -245,8 +246,14 @@ func (node *Node) RequestTags() []string {
}
func (node *Node) Prefixes() []netip.Prefix {
var addrs []netip.Prefix
for _, nodeAddress := range node.IPs() {
ips := node.IPs()
if len(ips) == 0 {
return nil
}
addrs := make([]netip.Prefix, 0, len(ips))
for _, nodeAddress := range ips {
ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen())
addrs = append(addrs, ip)
}
@ -274,9 +281,14 @@ func (node *Node) IsExitNode() bool {
}
func (node *Node) IPsAsString() []string {
var ret []string
ips := node.IPs()
if len(ips) == 0 {
return nil
}
for _, ip := range node.IPs() {
ret := make([]string, 0, len(ips))
for _, ip := range ips {
ret = append(ret, ip.String())
}
@ -480,7 +492,7 @@ func (node *Node) IsSubnetRouter() bool {
return len(node.SubnetRoutes()) > 0
}
// AllApprovedRoutes returns the combination of SubnetRoutes and ExitRoutes
// AllApprovedRoutes returns the combination of SubnetRoutes and ExitRoutes.
func (node *Node) AllApprovedRoutes() []netip.Prefix {
return append(node.SubnetRoutes(), node.ExitRoutes()...)
}
@ -527,7 +539,7 @@ func (node *Node) MarshalZerologObject(e *zerolog.Event) {
// - logTracePeerChange in poll.go.
func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
ret := tailcfg.PeerChange{
NodeID: tailcfg.NodeID(node.ID),
NodeID: tailcfg.NodeID(node.ID), //nolint:gosec // NodeID is bounded
}
if node.NodeKey.String() != req.NodeKey.String() {
@ -553,11 +565,9 @@ func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerC
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
} else if node.Hostinfo.NetInfo == nil {
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
} else {
} else if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
// If there is a PreferredDERP check if it has changed.
if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
}
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
}
}
@ -618,13 +628,16 @@ func (node *Node) ApplyHostnameFromHostInfo(hostInfo *tailcfg.Hostinfo) {
}
newHostname := strings.ToLower(hostInfo.Hostname)
if err := util.ValidateHostname(newHostname); err != nil {
err := util.ValidateHostname(newHostname)
if err != nil {
log.Warn().
Str("node.id", node.ID.String()).
Str("current_hostname", node.Hostname).
Str("rejected_hostname", hostInfo.Hostname).
Err(err).
Msg("Rejecting invalid hostname update from hostinfo")
return
}
@ -716,6 +729,7 @@ func (nodes Nodes) IDMap() map[NodeID]*Node {
func (nodes Nodes) DebugString() string {
var sb strings.Builder
sb.WriteString("Nodes:\n")
for _, node := range nodes {
sb.WriteString(node.DebugString())
sb.WriteString("\n")
@ -724,7 +738,7 @@ func (nodes Nodes) DebugString() string {
return sb.String()
}
func (node Node) DebugString() string {
func (node *Node) DebugString() string {
var sb strings.Builder
fmt.Fprintf(&sb, "%s(%s):\n", node.Hostname, node.ID)
@ -897,7 +911,7 @@ func (nv NodeView) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.Peer
// GetFQDN returns the fully qualified domain name for the node.
func (nv NodeView) GetFQDN(baseDomain string) (string, error) {
if !nv.Valid() {
return "", errors.New("creating valid FQDN: node view is invalid")
return "", fmt.Errorf("creating valid FQDN: %w", ErrInvalidNodeView)
}
return nv.ж.GetFQDN(baseDomain)

View file

@ -407,7 +407,7 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "我的电脑",
Hostname: "我的电脑", //nolint:gosmopolitan // intentional i18n test data
},
want: Node{
GivenName: "valid-hostname",
@ -491,7 +491,7 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "server-北京-01",
Hostname: "server-北京-01", //nolint:gosmopolitan // intentional i18n test data
},
want: Node{
GivenName: "valid-hostname",
@ -505,7 +505,7 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "我的电脑",
Hostname: "我的电脑", //nolint:gosmopolitan // intentional i18n test data
},
want: Node{
GivenName: "valid-hostname",
@ -533,7 +533,7 @@ func TestApplyHostnameFromHostInfo(t *testing.T) {
Hostname: "valid-hostname",
},
change: &tailcfg.Hostinfo{
Hostname: "测试💻机器",
Hostname: "测试💻机器", //nolint:gosmopolitan // intentional i18n test data
},
want: Node{
GivenName: "valid-hostname",

View file

@ -116,7 +116,7 @@ func (key *PreAuthKey) Proto() *v1.PreAuthKey {
return &protoKey
}
// canUsePreAuthKey checks if a pre auth key can be used.
// Validate checks if a pre auth key can be used.
func (pak *PreAuthKey) Validate() error {
if pak == nil {
return PAKError("invalid authkey")

View file

@ -111,6 +111,7 @@ func TestCanUsePreAuthKey(t *testing.T) {
t.Errorf("expected error but got none")
} else {
var httpErr PAKError
ok := errors.As(err, &httpErr)
if !ok {
t.Errorf("expected HTTPError but got %T", err)

View file

@ -4,6 +4,7 @@ import (
"cmp"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/mail"
"net/url"
@ -20,6 +21,9 @@ import (
"tailscale.com/tailcfg"
)
// ErrCannotParseBoolean is returned when a value cannot be parsed as boolean.
var ErrCannotParseBoolean = errors.New("cannot parse value as boolean")
type UserID uint64
type Users []User
@ -42,9 +46,11 @@ var TaggedDevices = User{
func (u Users) String() string {
var sb strings.Builder
sb.WriteString("[ ")
for _, user := range u {
fmt.Fprintf(&sb, "%d: %s, ", user.ID, user.Name)
}
sb.WriteString(" ]")
return sb.String()
@ -55,7 +61,8 @@ func (u Users) String() string {
// At the end of the day, users in Tailscale are some kind of 'bubbles' or users
// that contain our machines.
type User struct {
gorm.Model
gorm.Model //nolint:embeddedstructfieldcheck
// The index `idx_name_provider_identifier` is to enforce uniqueness
// between Name and ProviderIdentifier. This ensures that
// you can have multiple users with the same name in OIDC,
@ -91,6 +98,7 @@ func (u *User) StringID() string {
if u == nil {
return ""
}
return strconv.FormatUint(uint64(u.ID), 10)
}
@ -130,7 +138,7 @@ func (u *User) profilePicURL() string {
func (u *User) TailscaleUser() tailcfg.User {
return tailcfg.User{
ID: tailcfg.UserID(u.ID),
ID: tailcfg.UserID(u.ID), //nolint:gosec // UserID is bounded
DisplayName: u.Display(),
ProfilePicURL: u.profilePicURL(),
Created: u.CreatedAt,
@ -150,7 +158,7 @@ func (u UserView) ID() uint {
func (u *User) TailscaleLogin() tailcfg.Login {
return tailcfg.Login{
ID: tailcfg.LoginID(u.ID),
ID: tailcfg.LoginID(u.ID), //nolint:gosec // safe conversion for user ID
Provider: u.Provider,
LoginName: u.Username(),
DisplayName: u.Display(),
@ -164,7 +172,7 @@ func (u UserView) TailscaleLogin() tailcfg.Login {
func (u *User) TailscaleUserProfile() tailcfg.UserProfile {
return tailcfg.UserProfile{
ID: tailcfg.UserID(u.ID),
ID: tailcfg.UserID(u.ID), //nolint:gosec // UserID is bounded
LoginName: u.Username(),
DisplayName: u.Display(),
ProfilePicURL: u.profilePicURL(),
@ -184,6 +192,7 @@ func (u *User) Proto() *v1.User {
if name == "" {
name = u.Username()
}
return &v1.User{
Id: uint64(u.ID),
Name: name,
@ -220,7 +229,7 @@ func (u UserView) MarshalZerologObject(e *zerolog.Event) {
u.ж.MarshalZerologObject(e)
}
// JumpCloud returns a JSON where email_verified is returned as a
// FlexibleBoolean handles JumpCloud's JSON where email_verified is returned as a
// string "true" or "false" instead of a boolean.
// This maps bool to a specific type with a custom unmarshaler to
// ensure we can decode it from a string.
@ -229,6 +238,7 @@ type FlexibleBoolean bool
func (bit *FlexibleBoolean) UnmarshalJSON(data []byte) error {
var val any
err := json.Unmarshal(data, &val)
if err != nil {
return fmt.Errorf("unmarshalling data: %w", err)
@ -242,10 +252,11 @@ func (bit *FlexibleBoolean) UnmarshalJSON(data []byte) error {
if err != nil {
return fmt.Errorf("parsing %s as boolean: %w", v, err)
}
*bit = FlexibleBoolean(pv)
default:
return fmt.Errorf("parsing %v as boolean", v)
return fmt.Errorf("%w: %v", ErrCannotParseBoolean, v)
}
return nil
@ -279,9 +290,11 @@ func (c *OIDCClaims) Identifier() string {
if c.Iss == "" && c.Sub == "" {
return ""
}
if c.Iss == "" {
return CleanIdentifier(c.Sub)
}
if c.Sub == "" {
return CleanIdentifier(c.Iss)
}
@ -292,9 +305,9 @@ func (c *OIDCClaims) Identifier() string {
var result string
// Try to parse as URL to handle URL joining correctly
if u, err := url.Parse(issuer); err == nil && u.Scheme != "" {
if u, err := url.Parse(issuer); err == nil && u.Scheme != "" { //nolint:noinlineerr
// For URLs, use proper URL path joining
if joined, err := url.JoinPath(issuer, subject); err == nil {
if joined, err := url.JoinPath(issuer, subject); err == nil { //nolint:noinlineerr
result = joined
}
}
@ -366,6 +379,7 @@ func CleanIdentifier(identifier string) string {
cleanParts = append(cleanParts, trimmed)
}
}
if len(cleanParts) == 0 {
return ""
}
@ -408,6 +422,7 @@ func (u *User) FromClaim(claims *OIDCClaims, emailVerifiedRequired bool) {
if claims.Iss == "" && !strings.HasPrefix(identifier, "/") {
identifier = "/" + identifier
}
u.ProviderIdentifier = sql.NullString{String: identifier, Valid: true}
u.DisplayName = claims.Name
u.ProfilePicURL = claims.ProfilePictureURL

View file

@ -66,10 +66,13 @@ func TestUnmarshallOIDCClaims(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got OIDCClaims
if err := json.Unmarshal([]byte(tt.jsonstr), &got); err != nil {
err := json.Unmarshal([]byte(tt.jsonstr), &got)
if err != nil {
t.Errorf("UnmarshallOIDCClaims() error = %v", err)
return
}
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("UnmarshallOIDCClaims() mismatch (-want +got):\n%s", diff)
}
@ -190,6 +193,7 @@ func TestOIDCClaimsIdentifier(t *testing.T) {
}
result := claims.Identifier()
assert.Equal(t, tt.expected, result)
if diff := cmp.Diff(tt.expected, result); diff != "" {
t.Errorf("Identifier() mismatch (-want +got):\n%s", diff)
}
@ -282,6 +286,7 @@ func TestCleanIdentifier(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := CleanIdentifier(tt.identifier)
assert.Equal(t, tt.expected, result)
if diff := cmp.Diff(tt.expected, result); diff != "" {
t.Errorf("CleanIdentifier() mismatch (-want +got):\n%s", diff)
}
@ -479,7 +484,9 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got OIDCClaims
if err := json.Unmarshal([]byte(tt.jsonstr), &got); err != nil {
err := json.Unmarshal([]byte(tt.jsonstr), &got)
if err != nil {
t.Errorf("TestOIDCClaimsJSONToUser() error = %v", err)
return
}
@ -487,6 +494,7 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
var user User
user.FromClaim(&got, tt.emailVerifiedRequired)
if diff := cmp.Diff(user, tt.want); diff != "" {
t.Errorf("TestOIDCClaimsJSONToUser() mismatch (-want +got):\n%s", diff)
}

View file

@ -38,9 +38,7 @@ func (v *VersionInfo) String() string {
return sb.String()
}
var buildInfo = sync.OnceValues(func() (*debug.BuildInfo, bool) {
return debug.ReadBuildInfo()
})
var buildInfo = sync.OnceValues(debug.ReadBuildInfo)
var GetVersionInfo = sync.OnceValue(func() *VersionInfo {
info := &VersionInfo{