fatedier/frp · error

open tunnel timeout

Error message

open tunnel timeout

What it means

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.

Source

Thrown at client/visitor/xtcp.go:216

	}
}

// openTunnel will open a tunnel connection to the target server.
func (sv *XTCPVisitor) openTunnel(ctx context.Context) (conn net.Conn, err error) {
	xl := xlog.FromContextSafe(sv.ctx)
	ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
	defer cancel()

	timer := time.NewTimer(0)
	defer timer.Stop()

	for {
		select {
		case <-sv.ctx.Done():
			return nil, sv.ctx.Err()
		case <-ctx.Done():
			if errors.Is(ctx.Err(), context.DeadlineExceeded) {
				return nil, fmt.Errorf("open tunnel timeout")
			}
			return nil, ctx.Err()
		case <-timer.C:
			conn, err = sv.getTunnelConn(ctx)
			if err != nil {
				if !errors.Is(err, ErrNoTunnelSession) {
					xl.Warnf("get tunnel connection error: %v", err)
				}
				timer.Reset(500 * time.Millisecond)
				continue
			}
			return conn, nil
		}
	}
}

func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
	conn, err := sv.session.OpenConn(ctx)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Run `frpc nathole discover` on both ends to confirm the NAT types are punchable (not both symmetric).
  2. Check that natHoleSTUNServer is set and reachable from both clients, and that the xtcp proxy owner frpc is online.
  3. Verify UDP is allowed end-to-end; xtcp cannot work if either side blocks outbound UDP.
  4. If NATs are not punchable, switch the proxy type to stcp relayed through frps.
  5. Keep the retry loop but pre-test connectivity (see validation snippet) to fail fast with a clearer reason.

Example fix

# before: xtcp that times out behind hard NAT
[[proxies]]
name = "p2p"
type = "xtcp"
secretKey = "abc"

# after: fall back to relayed stcp when punching fails
[[proxies]]
name = "p2p"
type = "stcp"
secretKey = "abc"
Defensive patterns

Strategy: fallback

Validate before calling

// Before attempting xtcp, confirm NAT types are punchable
feature, err := nathole.Discover(ctx, cfg.NatHoleSTUNServer, "")
if err == nil && feature.NatType == nathole.NatTypeSymmetric {
    // skip xtcp, provision stcp instead
}

Try / catch

conn, err := sv.getTunnelConn(ctx)
if err != nil {
    if strings.Contains(err.Error(), "open tunnel timeout") {
        // fall back to stcp relayed through frps
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


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