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, err

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Verify both peers' NAT types with `frpc nathole discover`; symmetric NAT on either side defeats xtcp, so switch to stcp.
  2. Review transport.quic.keepalivePeriod and maxIdleTimeout so fragile paths stay alive.
  3. Ensure the path MTU allows ~1200-byte UDP datagrams end-to-end.
  4. Use identical frp versions on both ends.
  5. 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

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


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/4171e320b48280b6. Report an issue: GitHub.