juanfont/headscale · error · ErrUsernameInvalidChar

username contains invalid character: '%c'

Error message

username contains invalid character: '%c'

What it means

Returned by the username validator in hscontrol/util when a character in a username is not a letter, digit, '-', '.', '_', or a single '@'. It wraps ErrUsernameInvalidChar. This constrains what can safely become part of MagicDNS names and DNS records derived from usernames.

Source

Thrown at hscontrol/util/dns.go:68

	}

	atCount := 0

	for _, char := range username {
		switch {
		case unicode.IsLetter(char),
			unicode.IsDigit(char),
			char == '-',
			char == '.',
			char == '_':
			// Valid characters
		case char == '@':
			atCount++
			if atCount > 1 {
				return ErrUsernameTooManyAt
			}
		default:
			return fmt.Errorf("%w: '%c'", ErrUsernameInvalidChar, char)
		}
	}

	return nil
}

// generateMagicDNSRootDomains generates a list of DNS entries to be included in [tailcfg.DNSConfig.Routes] in [tailcfg.MapResponse].
// This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS
// server (listening in 100.100.100.100 udp/53) should be used for.
//
// Tailscale.com includes in the list:
// - the [types.DNSConfig.BaseDomain] of the user
// - the reverse DNS entry for IPv6 (0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa., see below more on IPv6)
// - the reverse DNS entries for the IPv4 subnets covered by the user's `IPPrefix`.
//   In the public SaaS this is [64-127].100.in-addr.arpa.
//
// The main purpose of this function is then generating the list of IPv4 entries. For the 100.64.0.0/10, this
// is clear, and could be hardcoded. But we are allowing any range as `IPPrefix`, so we need to find out the

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Pre-sanitize usernames before user creation: map disallowed characters to '-' or drop them
  2. If using OIDC, configure the provider to supply a compliant preferred_username claim
  3. Check the exact offending character printed by '%c' and adjust the source-of-truth naming scheme

Example fix

// before
username := "ad\\bob"
err := util.ValidateUsername(username) // invalid char '\\'
// after
username := "ad-bob"
err := util.ValidateUsername(username)
Defensive patterns

Strategy: validation

Validate before calling

import (
    "strings"
    "unicode"
)

func normalizeUsername(name string) string {
    var b strings.Builder
    for _, r := range strings.ToLower(name) {
        switch {
        case unicode.IsLetter(r) || unicode.IsDigit(r), r == '-', r == '.', r == '_':
            b.WriteRune(r)
        case r == '@':
            b.WriteRune('-') // or keep single @ if your flow allows
        default:
            b.WriteRune('-')
        }
    }
    return b.String()
}

Try / catch

if err := util.ValidateUsername(u); err != nil {
    if errors.Is(err, util.ErrUsernameInvalidChar) {
        u = normalizeUsername(u) // then re-validate
    }
    if errors.Is(err, util.ErrUsernameTooManyAt) {
        u = strings.Replace(u, "@", "-", -1)
    }
}

Prevention

When it happens

Trigger: Validating a username containing characters such as ':', '$', spaces, or a second '@' (the second '@' yields ErrUsernameTooManyAt instead). Happens on OIDC login with an identity-provider profile name containing unusual characters, or when creating users via the API.

Common situations: OIDC providers that emit usernames with characters from email display names or external schemas (e.g. 'DOMAIN\\user' or 'user+tag'); migrating user databases with legacy character sets.

Understand the failure class

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/501676d699e9c397. Report an issue: GitHub.