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
- 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)
- Make the NAT hole punch succeed: verify both peers' NAT types and the frps nathole support (bindAddr for nathole on frps)
- Enable keepTunnelOpen with retries so a session gets re-established automatically
- 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
- Copy the pattern at xtcp.go:222 — filter ErrNoTunnelSession out of retry/logging paths
- Enable keepTunnelOpen + sane MaxRetriesAnHour so sessions self-heal
- Expect this error during startup and after NAT punch failures; don't alert on it
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.