{"record":{"id":"c652ae3bac073301","repo":"XTLS/Xray-core","slug":"pattern-string-does-not-conform-to-letter-digit-hy","errorCode":null,"errorMessage":"pattern string does not conform to Letter-Digit-Hyphen (LDH) subset","messagePattern":"pattern string does not conform to Letter-Digit-Hyphen \\(LDH\\) subset","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"common/geodata/strmatcher/matchers.go","lineNumber":171,"sourceCode":"//  2. If any non-ASCII characters, domain are converted from Internationalized domain name to Punycode.\nfunc ToDomain(pattern string) (string, error) {\n\tfor {\n\t\tisASCII, hasUpper := true, false\n\t\tfor i := 0; i < len(pattern); i++ {\n\t\t\tc := pattern[i]\n\t\t\tif c >= utf8.RuneSelf {\n\t\t\t\tisASCII = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase 'A' <= c && c <= 'Z':\n\t\t\t\thasUpper = true\n\t\t\tcase 'a' <= c && c <= 'z':\n\t\t\tcase '0' <= c && c <= '9':\n\t\t\tcase c == '-':\n\t\t\tcase c == '.':\n\t\t\tdefault:\n\t\t\t\treturn \"\", errors.New(\"pattern string does not conform to Letter-Digit-Hyphen (LDH) subset\")\n\t\t\t}\n\t\t}\n\t\tif !isASCII {\n\t\t\tvar err error\n\t\t\tpattern, err = idna.Punycode.ToASCII(pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif hasUpper {\n\t\t\tpattern = strings.ToLower(pattern)\n\t\t}\n\t\tbreak\n\t}\n\treturn pattern, nil\n}\n","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/common/geodata/strmatcher/matchers.go#L153-L189","documentation":"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.","triggerScenarios":"Patterns like \"my_host.example.com\" or \"*.example.com\" routed into the substring/domain matcher path that enforces LDH; underscore is the classic offender.","commonSituations":"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.","solutions":["Replace '_' with '-' in the pattern, or route such names to a matcher that accepts them (e.g. keyword:/regexp: rules instead of domain matching).","If a wildcard was intended, ensure the rule type supports it rather than passing '*' into this normalizer."],"exampleFix":"// before\n\"domain\": [\"domain:my_host.example.com\"]\n\n// after\n\"domain\": [\"keyword:my_host.example.com\"]","handlingStrategy":"validation","validationCode":"var ldhRe = regexp.MustCompile(`^[A-Za-z0-9.-]+$`)\nfunc isLDH(pattern string) bool {\n    if !ldhRe.MatchString(pattern) { return false }\n    for _, r := range pattern { if r >= utf8.RuneSelf { return true /* punycode path */ } }\n    return true\n}","typeGuard":"func isLDHPattern(p string) bool {\n    for _, r := range p {\n        if r >= utf8.RuneSelf { return true }\n        if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '.') { return false }\n    }\n    return true\n}","tryCatchPattern":null,"preventionTips":["Route underscore/wildcard-containing names through keyword: or regexp: matchers instead of domain matchers.","Sanitize hostnames to LDH before feeding strmatcher builders."],"tags":["strmatcher","domain","ldh","validation","xray"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}