{"record":{"id":"4171e320b48280b6","repo":"fatedier/frp","slug":"dial-quic-error-v","errorCode":null,"errorMessage":"dial quic error: %v","messagePattern":"dial quic error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/visitor/xtcp.go","lineNumber":416,"sourceCode":"\treturn &QUICTunnelSession{\n\t\tclientCfg: clientCfg,\n\t}\n}\n\nfunc (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {\n\ttlsConfig, err := transport.NewClientTLSConfig(\"\", \"\", \"\", raddr.String())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create tls config error: %v\", err)\n\t}\n\ttlsConfig.NextProtos = []string{\"frp\"}\n\tquicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,\n\t\t&quic.Config{\n\t\t\tMaxIdleTimeout:     time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,\n\t\t\tMaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),\n\t\t\tKeepAlivePeriod:    time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,\n\t\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dial quic error: %v\", err)\n\t}\n\tqs.mu.Lock()\n\tqs.session = quicConn\n\tqs.listenConn = listenConn\n\tqs.mu.Unlock()\n\treturn nil\n}\n\nfunc (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {\n\tqs.mu.RLock()\n\tdefer qs.mu.RUnlock()\n\tsession := qs.session\n\tif session == nil {\n\t\treturn nil, ErrNoTunnelSession\n\t}\n\tstream, err := session.OpenStreamSync(ctx)\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/client/visitor/xtcp.go#L398-L434","documentation":"QUICTunnelSession.Init failed at quic.Dial: the QUIC handshake toward the xtcp peer over the punched UDP path did not complete. The dial uses client-configured MaxIdleTimeout, MaxIncomingStreams, and KeepalivePeriod, and ALPN 'frp'; failure means no compatible QUIC endpoint answered within the timeout.","triggerScenarios":"quic.Dial timeout/handshake failure: the NAT hole is not actually open (peer NAT symmetric), UDP fragments dropped (QUIC initial packets are larger than STUN probes), peer frpc not running the QUIC session, or ALPN/TLS mismatch (peer is not frp-QUIC).","commonSituations":"NAT or firewall dropping the ~1200-byte QUIC initial datagrams while small STUN probes passed; peer behind symmetric NAT so the punched mapping is wrong for data; QUIC keepalive/maxIdleTimeout misconfigured so half-open paths expire; frpc/frps version mismatch in QUIC parameters.","solutions":["Verify both peers' NAT types with `frpc nathole discover`; symmetric NAT on either side defeats xtcp, so switch to stcp.","Review transport.quic.keepalivePeriod and maxIdleTimeout so fragile paths stay alive.","Ensure the path MTU allows ~1200-byte UDP datagrams end-to-end.","Use identical frp versions on both ends.","Retry immediately after discovery; punched mappings expire in seconds and a second attempt often succeeds."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-check UDP reachability of the punched address before QUIC dial\nif c, err := net.Dial(\"udp\", raddr.String()); err != nil {\n    // abort early with a clearer error\n} else {\n    c.Close()\n}","typeGuard":null,"tryCatchPattern":"if err := qs.Init(listenConn, raddr); err != nil {\n    if isTimeout(err) { timer.Reset(500 * time.Millisecond); continue } // outer loop retries\n}","preventionTips":["Verify both peers' NAT types before relying on xtcp/QUIC","Tune transport.quic.keepalivePeriod for lossy paths","Ensure MTU >= 1200 for QUIC initials on the whole path"],"tags":["xtcp","quic","nat","udp","timeout"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}