fatedier/frp · error

ErrNoTunnelSession

ErrNoTunnelSession

Error message

no tunnel session

What it means

ErrNoTunnelSession is returned by KCPTunnelSession/QUICTunnelSession.OpenConn (xtcp.go:371,430) when the underlying session field is nil — i.e. OpenConn was called before Init() successfully completed or after Close() nulled the session. It signals the XTCP visitor has no NAT-hole tunnel established, not a network failure.

Source

Thrown at client/visitor/xtcp.go:42

	"sync"
	"time"

	libio "github.com/fatedier/golib/io"
	fmux "github.com/hashicorp/yamux"
	quic "github.com/quic-go/quic-go"
	"golang.org/x/time/rate"

	v1 "github.com/fatedier/frp/pkg/config/v1"
	"github.com/fatedier/frp/pkg/msg"
	"github.com/fatedier/frp/pkg/naming"
	"github.com/fatedier/frp/pkg/nathole"
	"github.com/fatedier/frp/pkg/transport"
	netpkg "github.com/fatedier/frp/pkg/util/net"
	"github.com/fatedier/frp/pkg/util/util"
	"github.com/fatedier/frp/pkg/util/xlog"
)

var ErrNoTunnelSession = errors.New("no tunnel session")

type XTCPVisitor struct {
	*BaseVisitor
	session       TunnelSession
	startTunnelCh chan struct{}
	retryLimiter  *rate.Limiter
	cancel        context.CancelFunc

	cfg *v1.XTCPVisitorConfig
}

func (sv *XTCPVisitor) Run() (err error) {
	sv.ctx, sv.cancel = context.WithCancel(sv.ctx)

	if sv.cfg.Protocol == "kcp" {
		sv.session = NewKCPTunnelSession()
	} else {
		sv.session = NewQUICTunnelSession(sv.clientCfg)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. In custom visitors, ignore/reject connections during the pre-Init window instead of erroring loudly — the built-in keepTunnelOpen loop already skips ErrNoTunnelSession (xtcp.go:222)
  2. Make the NAT hole punch succeed: verify both peers' NAT types and the frps nathole support (bindAddr for nathole on frps)
  3. Enable keepTunnelOpen with retries so a session gets re-established automatically
  4. Check that the visitor dialed the correct serverUDPPort for nathole discovery

Example fix

// before
conn, err := sv.session.OpenConn(ctx)
if err != nil { log.Error(err) } // no tunnel session spam

// after
conn, err := sv.session.OpenConn(ctx)
if errors.Is(err, visitor.ErrNoTunnelSession) {
    // tunnel not established yet; drop the client conn, retry will occur
    clientConn.Close()
    return
}
Defensive patterns

Strategy: validation

Validate before calling

// Mirror the built-in guard: only open conns when a session exists
type sessionReady interface{ Ready() bool }

if s, ok := sv.session.(sessionReady); ok && !s.Ready() {
    clientConn.Close() // no tunnel yet
    return
}

Try / catch

conn, err := sv.session.OpenConn(ctx)
if errors.Is(err, visitor.ErrNoTunnelSession) {
    clientConn.Close()
    return // hole punch pending/failed; keepTunnelOpen will retry
}

Prevention

When it happens

Trigger: An incoming connection arrives on the XTCP visitor's local listener while makeNatHole/Init has not finished (or failed); the session was closed concurrently (visitor shutdown, keepTunnelOpen retry cycle) and a late connection tries to open a stream.

Common situations: Connections hitting the visitor port immediately after startup before the first NAT hole punch completes; NAT hole punch failing (symmetric NAT) so the session never initializes while traffic still arrives; shutdown races where Close() and OpenConn contend.

Related errors


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