{"record":{"id":"1dffbc3cddf834a8","repo":"fatedier/frp","slug":"not-enough-addresses","errorCode":null,"errorMessage":"not enough addresses","messagePattern":"not enough addresses","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/nathole/classify.go","lineNumber":44,"sourceCode":"\tHardNAT = \"HardNAT\"\n\n\tBehaviorNoChange    = \"BehaviorNoChange\"\n\tBehaviorIPChanged   = \"BehaviorIPChanged\"\n\tBehaviorPortChanged = \"BehaviorPortChanged\"\n\tBehaviorBothChanged = \"BehaviorBothChanged\"\n)\n\ntype NatFeature struct {\n\tNatType            string\n\tBehavior           string\n\tPortsDifference    int\n\tRegularPortsChange bool\n\tPublicNetwork      bool\n}\n\nfunc ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, error) {\n\tif len(addresses) <= 1 {\n\t\treturn nil, fmt.Errorf(\"not enough addresses\")\n\t}\n\tnatFeature := &NatFeature{}\n\tipChanged := false\n\tportChanged := false\n\n\tvar baseIP, basePort string\n\tvar portMax, portMin int\n\tfor _, addr := range addresses {\n\t\tip, port, err := net.SplitHostPort(addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tportNum, err := strconv.Atoi(port)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif slices.Contains(localIPs, ip) {\n\t\t\tnatFeature.PublicNetwork = true","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/nathole/classify.go#L26-L62","documentation":"Thrown by ClassifyNATFeature in pkg/nathole/classify.go when the addresses slice has one or zero entries. NAT classification compares the IP and port observed from at least two different STUN observations to infer endpoint-independent vs address-dependent mapping behavior; with a single observation there is nothing to compare, so classification is impossible.","triggerScenarios":"Calling ClassifyNATFeature(addresses, localIPs) with len(addresses) <= 1. In frp this happens when Discover() returned only one mapped address because only one STUN server responded, or when a caller passes a single hard-coded address instead of the multi-server discovery result.","commonSituations":"STUN servers misconfigured or unreachable so discovery yields one address; firewalls blocking UDP to all but one STUN endpoint; custom code that calls ClassifyNATFeature directly without first checking the discovery result length; symmetric NAT environments where the second STUN query fails.","solutions":["Ensure Discover(stunServers, ...) is given at least two reachable STUN servers and returns >= 2 addresses before classifying","Check natFeature/stun server reachability: verify natHoleSTUNServer in frps.toml points to working STUN endpoints","Retry discovery — transient UDP loss can drop one observation","If calling ClassifyNATFeature yourself, guard with len(addresses) < 2 and skip classification"],"exampleFix":"// before\nfeature, err := ClassifyNATFeature(addrs, localIPs)\n\n// after\nif len(addrs) < 2 {\n    return nil, fmt.Errorf(\"need at least 2 discovered addresses, got %d\", len(addrs))\n}\nfeature, err := ClassifyNATFeature(addrs, localIPs)","handlingStrategy":"validation","validationCode":"if len(addresses) < 2 {\n    return nil, fmt.Errorf(\"classification needs >=2 addresses, got %d\", len(addresses))\n}","typeGuard":null,"tryCatchPattern":"feature, err := nathole.ClassifyNATFeature(addrs, localIPs)\nif err != nil {\n    if strings.Contains(err.Error(), \"not enough addresses\") {\n        // re-run discovery with more STUN servers before retrying\n    }\n    return err\n}","preventionTips":["Ensure discovery returns >=2 addresses before classifying","Configure multiple reachable STUN servers","Validate each address parses as host:port before passing in"],"tags":["go","frp","nathole","stun","nat","validation"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}