XTLS/Xray-core · error

connection closed

Error message

connection closed

What it means

The tun inbound handler logs this when dispatcher.DispatchLink fails for a connection extracted from the TUN/gVisor stack. 'connection closed' is the wrapper; the real cause (Base err) is the dispatch failure — invalid destination, routing block, or outbound dial error. The client conn is then closed by the deferred Close.

Source

Thrown at proxy/tun/handler.go:211

	ctx = session.ContextWithContent(ctx, &session.Content{
		SniffingRequest: t.sniffingRequest,
	})
	ctx = session.SubContextFromMuxInbound(ctx)

	ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
		From:   inbound.Source,
		To:     destination,
		Status: log.AccessAccepted,
		Reason: "",
	})
	errors.LogInfo(ctx, "processing from ", source, " to ", destination)

	link := &transport.Link{
		Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
		Writer: buf.NewWriter(conn),
	}
	if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil {
		errors.LogError(ctx, errors.New("connection closed").Base(err))
	}
}

// Close implements common.Closable.
func (t *Handler) Close() error {
	return errors.Combine(common.CloseIfExists(t.stack), common.CloseIfExists(t.tun))
}

// Network implements proxy.Inbound
// and exists only to comply to proxy interface, declaring it doesn't listen on any network,
// making the process not open any port for this inbound (input will be network interface)
func (t *Handler) Network() []net.Network {
	return []net.Network{}
}

// Process implements proxy.Inbound
// and exists only to comply to proxy interface, which should never get any inputs due to no listening ports
func (t *Handler) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Read the Base error in the same log line to get the dispatch failure reason
  2. Check routing rules for unintended matches (geoip:private, broadcast/multicast ranges) and route them to direct/block explicitly
  3. Verify the outbound referenced by the matched rule is alive and can resolve/dial
  4. Enable routing debug logs to see which rule matched the failing destination

Example fix

// json: keep local/multicast traffic off the proxy while using tun
"routing": {"rules": [
  {"type": "field", "ip": ["geoip:private", "224.0.0.0/3", "ff00::/8"], "outboundTag": "direct"}
]}
Defensive patterns

Strategy: try-catch

Validate before calling

// guard dispatch of tun connections
if !validDest(destination) {
    errors.LogInfo(ctx, "dropping invalid destination ", destination)
    return
}

Try / catch

if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil {
    // per-flow failure: log with cause and let deferred conn.Close() clean up
    errors.LogError(ctx, errors.New("connection closed").Base(err))
}

Prevention

When it happens

Trigger: An app on the tun interface connects to a destination that routing blocks or an outbound cannot dial; DNS resolution inside dispatch fails; the destination derived from the packet is invalid (e.g. unroutable literal).

Common situations: tun-mode (TUN mode) with routing rules sending everything to a dead outbound; system apps probing link-local or multicast addresses that reach the tun stack; misconfigured sockopt/interface rules after enabling tun.

Understand the failure class

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/17538df60018649a. Report an issue: GitHub.