{"record":{"id":"b4d236abdf45237a","repo":"fatedier/frp","slug":"classify-nat-feature-error-v","errorCode":null,"errorMessage":"classify nat feature error: %v","messagePattern":"classify nat feature error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/nathole/nathole.go","lineNumber":131,"sourceCode":"\t}\n\treturn nil\n}\n\n// Prepare is used to do some preparation work before penetration.\nfunc Prepare(stunServers []string, opts PrepareOptions) (*PrepareResult, error) {\n\t// discover for Nat type\n\taddrs, localAddr, err := Discover(stunServers, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"discover error: %v\", err)\n\t}\n\tif len(addrs) < 2 {\n\t\treturn nil, fmt.Errorf(\"discover error: not enough addresses\")\n\t}\n\n\tlocalIPs, _ := ListLocalIPsForNatHole(10)\n\tnatFeature, err := ClassifyNATFeature(addrs, localIPs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"classify nat feature error: %v\", err)\n\t}\n\n\tladdr, err := net.ResolveUDPAddr(\"udp4\", localAddr.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"resolve local udp addr error: %v\", err)\n\t}\n\tlistenConn, err := net.ListenUDP(\"udp4\", laddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"listen local udp addr error: %v\", err)\n\t}\n\n\t// Apply NAT traversal options\n\tvar assistedAddrs []string\n\tif !opts.DisableAssistedAddrs {\n\t\tassistedAddrs = make([]string, 0, len(localIPs))\n\t\tfor _, ip := range localIPs {\n\t\t\tassistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port)))\n\t\t}","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/nathole/nathole.go#L113-L149","documentation":"Returned by nathole.Prepare (pkg/nathole/nathole.go) when ClassifyNATFeature(addrs, localIPs) fails after discovery succeeded with >= 2 addresses. The wrapped error is either 'not enough addresses' (defensive; normally caught by the earlier length check) or a parsing failure — an address in the discovery result that does not survive net.SplitHostPort / strconv.Atoi.","triggerScenarios":"nathole.Prepare where a discovered address string is malformed (missing port, non-numeric port, bracket issues with IPv6 literals). Reachable only when discovery produced >= 2 entries but one is not a clean host:port string.","commonSituations":"A misbehaving STUN server returning odd MAPPED-ADDRESS formatting; IPv6 handling edge cases where the address lacks brackets; version-skewed components producing different address formats.","solutions":["Read the wrapped error: a SplitHostPort/Atoi failure means one address is malformed — identify which STUN server produced it and replace it","Prefer IPv4 STUN endpoints for xtcp discovery","Align frpc/frps versions if formats may differ","Retry discovery; a single malformed response can be transient"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"for _, a := range addrs {\n    if _, _, err := net.SplitHostPort(a); err != nil {\n        return fmt.Errorf(\"malformed address %q from STUN discovery\", a)\n    }\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"classify nat feature error\") {\n    // wrapped error tells whether it is a count problem or a parse problem; fix the offending STUN source\n}","preventionTips":["Sanity-parse discovered addresses before Prepare","Replace STUN servers that emit malformed mappings","Keep frp versions consistent across components"],"tags":["go","frp","nathole","stun","parsing"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}