fatedier/frp · error
dial quic error: %v
Error message
dial quic error: %v
What it means
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.
Source
Thrown at client/visitor/xtcp.go:416
return &QUICTunnelSession{
clientCfg: clientCfg,
}
}
func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) error {
tlsConfig, err := transport.NewClientTLSConfig("", "", "", raddr.String())
if err != nil {
return fmt.Errorf("create tls config error: %v", err)
}
tlsConfig.NextProtos = []string{"frp"}
quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig,
&quic.Config{
MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams),
KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second,
})
if err != nil {
return fmt.Errorf("dial quic error: %v", err)
}
qs.mu.Lock()
qs.session = quicConn
qs.listenConn = listenConn
qs.mu.Unlock()
return nil
}
func (qs *QUICTunnelSession) OpenConn(ctx context.Context) (net.Conn, error) {
qs.mu.RLock()
defer qs.mu.RUnlock()
session := qs.session
if session == nil {
return nil, ErrNoTunnelSession
}
stream, err := session.OpenStreamSync(ctx)
if err != nil {
return nil, errView on GitHub (pinned to 6c8a8d0a97)
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.
Defensive patterns
Strategy: retry
Validate before calling
// Pre-check UDP reachability of the punched address before QUIC dial
if c, err := net.Dial("udp", raddr.String()); err != nil {
// abort early with a clearer error
} else {
c.Close()
} Try / catch
if err := qs.Init(listenConn, raddr); err != nil {
if isTimeout(err) { timer.Reset(500 * time.Millisecond); continue } // outer loop retries
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- open tunnel timeout
- create kcp connection from udp connection error: %v
- wait detect message error: %v
- wait detect message timeout
- dial udp error: %v
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/4171e320b48280b6.
Report an issue: GitHub.