all: fix golangci-lint issues (#3064)
This commit is contained in:
parent
bfb6fd80df
commit
ce580f8245
131 changed files with 3131 additions and 1560 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue