XTLS/Xray-core · error

pattern string does not conform to Letter-Digit-Hyphen (LDH)

Error message

pattern string does not conform to Letter-Digit-Hyphen (LDH) subset

What it means

While normalizing a domain-matcher pattern to lowercase/ASCII, every byte must belong to the LDH subset (letters, digits, hyphen, dot). Any other ASCII punctuation (underscore, wildcard '*', slash, etc.) in a pattern that this normalizer processes is rejected; non-ASCII is converted via punycode instead.

Source

Thrown at common/geodata/strmatcher/matchers.go:171

//  2. If any non-ASCII characters, domain are converted from Internationalized domain name to Punycode.
func ToDomain(pattern string) (string, error) {
	for {
		isASCII, hasUpper := true, false
		for i := 0; i < len(pattern); i++ {
			c := pattern[i]
			if c >= utf8.RuneSelf {
				isASCII = false
				break
			}
			switch {
			case 'A' <= c && c <= 'Z':
				hasUpper = true
			case 'a' <= c && c <= 'z':
			case '0' <= c && c <= '9':
			case c == '-':
			case c == '.':
			default:
				return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset")
			}
		}
		if !isASCII {
			var err error
			pattern, err = idna.Punycode.ToASCII(pattern)
			if err != nil {
				return "", err
			}
			continue
		}
		if hasUpper {
			pattern = strings.ToLower(pattern)
		}
		break
	}
	return pattern, nil
}

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Replace '_' with '-' in the pattern, or route such names to a matcher that accepts them (e.g. keyword:/regexp: rules instead of domain matching).
  2. If a wildcard was intended, ensure the rule type supports it rather than passing '*' into this normalizer.

Example fix

// before
"domain": ["domain:my_host.example.com"]

// after
"domain": ["keyword:my_host.example.com"]
Defensive patterns

Strategy: validation

Validate before calling

var ldhRe = regexp.MustCompile(`^[A-Za-z0-9.-]+$`)
func isLDH(pattern string) bool {
    if !ldhRe.MatchString(pattern) { return false }
    for _, r := range pattern { if r >= utf8.RuneSelf { return true /* punycode path */ } }
    return true
}

Type guard

func isLDHPattern(p string) bool {
    for _, r := range p {
        if r >= utf8.RuneSelf { return true }
        if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '.') { return false }
    }
    return true
}

Prevention

When it happens

Trigger: Patterns like "my_host.example.com" or "*.example.com" routed into the substring/domain matcher path that enforces LDH; underscore is the classic offender.

Common situations: Using underscore subdomains (valid in DNS but not LDH) with matchers that assume hostnames; feeding URL paths or wildcard strings where a plain domain is expected.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/c652ae3bac073301. Report an issue: GitHub.