{"record":{"id":"eb5c4c5c9ece7a0b","repo":"fatedier/frp","slug":"open-tunnel-timeout","errorCode":null,"errorMessage":"open tunnel timeout","messagePattern":"open tunnel timeout","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/visitor/xtcp.go","lineNumber":216,"sourceCode":"\t}\n}\n\n// openTunnel will open a tunnel connection to the target server.\nfunc (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {\n\txl := xlog.FromContextSafe(sv.ctx)\n\tctx, cancel := context.WithTimeout(ctx, 20*time.Second)\n\tdefer cancel()\n\n\ttimer := time.NewTimer(0)\n\tdefer timer.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-sv.ctx.Done():\n\t\t\treturn nil, sv.ctx.Err()\n\t\tcase <-ctx.Done():\n\t\t\tif errors.Is(ctx.Err(), context.DeadlineExceeded) {\n\t\t\t\treturn nil, fmt.Errorf(\"open tunnel timeout\")\n\t\t\t}\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-timer.C:\n\t\t\tconn, err = sv.getTunnelConn(ctx)\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, ErrNoTunnelSession) {\n\t\t\t\t\txl.Warnf(\"get tunnel connection error: %v\", err)\n\t\t\t\t}\n\t\t\t\ttimer.Reset(500 * time.Millisecond)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn conn, nil\n\t\t}\n\t}\n}\n\nfunc (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {\n\tconn, err := sv.session.OpenConn(ctx)","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/client/visitor/xtcp.go#L198-L234","documentation":"An xtcp visitor gave up establishing the NAT-hole tunnel because the 20-second context deadline expired before getTunnelConn succeeded. The loop retries every 500 ms and silently tolerates ErrNoTunnelSession (discovery still running), so this timeout means no usable tunnel session was obtained within 20 s.","triggerScenarios":"Each getTunnelConn attempt fails (or returns ErrNoTunnelSession because the KCP/QUIC session never initialized) for the full 20 s window; the explicit 'open tunnel timeout' string marks the outer deadline firing.","commonSituations":"Both peers behind symmetric or hard NATs where hole punching cannot succeed; natHoleSTUNServer unreachable so coordination never completes; the frpc that owns the xtcp proxy is offline; UDP blocked between the peers so session Init keeps failing.","solutions":["Run `frpc nathole discover` on both ends to confirm the NAT types are punchable (not both symmetric).","Check that natHoleSTUNServer is set and reachable from both clients, and that the xtcp proxy owner frpc is online.","Verify UDP is allowed end-to-end; xtcp cannot work if either side blocks outbound UDP.","If NATs are not punchable, switch the proxy type to stcp relayed through frps.","Keep the retry loop but pre-test connectivity (see validation snippet) to fail fast with a clearer reason."],"exampleFix":"# before: xtcp that times out behind hard NAT\n[[proxies]]\nname = \"p2p\"\ntype = \"xtcp\"\nsecretKey = \"abc\"\n\n# after: fall back to relayed stcp when punching fails\n[[proxies]]\nname = \"p2p\"\ntype = \"stcp\"\nsecretKey = \"abc\"","handlingStrategy":"fallback","validationCode":"// Before attempting xtcp, confirm NAT types are punchable\nfeature, err := nathole.Discover(ctx, cfg.NatHoleSTUNServer, \"\")\nif err == nil && feature.NatType == nathole.NatTypeSymmetric {\n    // skip xtcp, provision stcp instead\n}","typeGuard":null,"tryCatchPattern":"conn, err := sv.getTunnelConn(ctx)\nif err != nil {\n    if strings.Contains(err.Error(), \"open tunnel timeout\") {\n        // fall back to stcp relayed through frps\n    }\n}","preventionTips":["Pre-check NAT types with `frpc nathole discover` on both ends","Keep UDP open end-to-end","Design visitors to fall back from xtcp to stcp when punching fails"],"tags":["xtcp","nat","timeout","p2p","udp"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}