{"record":{"id":"57c55eaf901b1250","repo":"fatedier/frp","slug":"classify-visitor-nat-feature-error-v","errorCode":null,"errorMessage":"classify visitor nat feature error: %v","messagePattern":"classify visitor nat feature error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/nathole/controller.go","lineNumber":311,"sourceCode":"\t\tSid:           sid,\n\t\tError:         errInfo,\n\t}\n}\n\n// analysis analyzes the NAT type and behavior of the visitor and client, then makes hole-punching decisions.\n// return the response to the visitor and client.\nfunc (c *Controller) analysis(session *Session) (*msg.NatHoleResp, *msg.NatHoleResp, error) {\n\tcm := session.clientMsg\n\tvm := session.visitorMsg\n\n\tcNatFeature, err := ClassifyNATFeature(cm.MappedAddrs, parseIPs(cm.AssistedAddrs))\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"classify client nat feature error: %v\", err)\n\t}\n\n\tvNatFeature, err := ClassifyNATFeature(vm.MappedAddrs, parseIPs(vm.AssistedAddrs))\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"classify visitor nat feature error: %v\", err)\n\t}\n\tsession.cNatFeature = cNatFeature\n\tsession.vNatFeature = vNatFeature\n\tsession.genAnalysisKey()\n\n\tmode, index, cBehavior, vBehavior := c.analyzer.GetRecommandBehaviors(session.analysisKey, cNatFeature, vNatFeature)\n\tsession.recommandMode = mode\n\tsession.recommandIndex = index\n\tsession.cBehavior = cBehavior\n\tsession.vBehavior = vBehavior\n\n\ttimeoutMs := max(cBehavior.SendDelayMs, vBehavior.SendDelayMs) + 5000\n\tif cBehavior.ListenRandomPorts > 0 || vBehavior.ListenRandomPorts > 0 {\n\t\ttimeoutMs += 30000\n\t}\n\n\tprotocol := vm.Protocol\n\tvResp := newNatHoleResponse(","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/nathole/controller.go#L293-L329","documentation":"Wraps a ClassifyNATFeature failure for the visitor side inside Controller.analysis (pkg/nathole/controller.go). After classifying the client's NAT successfully, analysis does the same for vm.MappedAddrs (the xtcp visitor's STUN-observed addresses); failure here — typically fewer than two mapped addresses or a malformed address — aborts hole-punch analysis with this error.","triggerScenarios":"Controller.analysis(session) where vm.MappedAddrs has <= 1 entry (only one STUN server responded for the visitor) or contains a string that fails host:port parsing. The visitor's discovery quality depends on its own network's UDP egress and the configured STUN servers.","commonSituations":"Visitor behind a restrictive firewall that blocks UDP to all but one STUN endpoint; visitor's frpc version reporting addresses differently; symmetric NAT dropping the second STUN exchange.","solutions":["From the visitor machine, verify UDP reachability to at least two STUN servers (e.g. with a STUN test client)","Configure healthy STUN servers on frps (natHoleAnalysisSTUNServer) so both sides gather multiple observations","Retry the visit; discovery is best-effort over UDP","If it persists, xtcp hole punching may be impossible from that network — fall back to relay (stcp or a tcp proxy)"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := controller.Analysis(session)\nif err != nil && strings.Contains(err.Error(), \"classify visitor nat feature error\") {\n    // visitor's discovery was thin: retry the visit once, then check visitor-side UDP egress\n}","preventionTips":["Verify visitor host can reach >=2 STUN servers over UDP","Retry visits before treating failure as permanent","Fall back to stcp relay when the visitor network starves discovery"],"tags":["go","frp","nathole","stun","nat","visitor"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}