{"record":{"id":"481cfb279c002685","repo":"XTLS/Xray-core","slug":"empty-stunservers","errorCode":null,"errorMessage":"empty stunServers","messagePattern":"empty stunServers","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"infra/conf/transport_finalmask.go","lineNumber":873,"sourceCode":"\n\ttoken, err = url.PathUnescape(u.User.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"invalid token\", token)\n\t}\n\n\tid, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), \"/\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif id == \"\" {\n\t\treturn nil, errors.New(\"invalid id\", id)\n\t}\n\n\tif len(c.StunServers) == 0 {\n\t\treturn nil, errors.New(\"empty stunServers\")\n\t}\n\n\tfor _, s := range c.StunServers {\n\t\t_, _, err = net.SplitHostPort(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tstunServers = c.StunServers\n\n\tif c.TlsConfig != nil {\n\t\ttc, err := c.TlsConfig.Build()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttlsConfig = tc.(*tls.Config)\n\t}","sourceCodeStart":855,"sourceCodeEnd":891,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/infra/conf/transport_finalmask.go#L855-L891","documentation":"Thrown by Realm.Build() when the 'stunServers' array on the Realm transport config is empty. The Realm transport depends on STUN for NAT discovery, so at least one server is mandatory even though the URL itself parsed fine. Each entry must then also pass net.SplitHostPort (host:port form).","triggerScenarios":"Declaring a Realm transport with a valid url but omitting stunServers, or providing stunServers: [] in JSON. Entries like \"stun.l.google.com\" without a port fail the subsequent SplitHostPort check with a wrapped error.","commonSituations":"Copy-pasting an example that omits STUN; assuming STUN is optional; providing hostnames without ports.","solutions":["Add at least one STUN server in 'host:port' form, e.g. \"stun.l.google.com:19302\".","Verify every entry contains an explicit port.","If behind no NAT, still supply a reachable public STUN server — there is no opt-out."],"exampleFix":"// before\n\"realm\": { \"url\": \"realm://t@h:443/id\", \"stunServers\": [] }\n// after\n\"realm\": { \"url\": \"realm://t@h:443/id\", \"stunServers\": [\"stun.l.google.com:19302\"] }","handlingStrategy":"validation","validationCode":"if len(stunServers) == 0 {\n    return fmt.Errorf(\"realm transport requires at least one stun server\")\n}\nfor _, s := range stunServers {\n    if _, _, err := net.SplitHostPort(s); err != nil {\n        return fmt.Errorf(\"stun server %q must be host:port\", s)\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Ship a default STUN entry (e.g. stun.l.google.com:19302) with every Realm config.","Enforce host:port form in generators.","Remember STUN is mandatory for Realm — there is no opt-out flag."],"tags":["go","xray","config","validation","stun","transport"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}