v2fly/v2ray-core · error

no inbound metadata

Error message

no inbound metadata

What it means

This is a hard Go panic raised in the Trojan inbound's Process() after the client request has been successfully decoded. The handler expects the context to already carry session.Inbound metadata (set by the inbound worker in app/proxyman/inbound/worker.go via session.ContextWithInbound); if session.InboundFromContext(ctx) returns nil, the invariant 'this proxy is only driven by a registered inbound handler' was violated, so the process panics instead of continuing without inbound metadata.

Source

Thrown at proxy/trojan/server.go:188

	clientReader := &ConnReader{Reader: bufferedReader}
	if err := clientReader.ParseHeader(); err != nil {
		log.Record(&log.AccessMessage{
			From:   conn.RemoteAddr(),
			To:     "",
			Status: log.AccessRejected,
			Reason: err,
		})
		return newError("failed to create request from: ", conn.RemoteAddr()).Base(err)
	}

	destination := clientReader.Target
	if err := conn.SetReadDeadline(time.Time{}); err != nil {
		return newError("unable to set read deadline").Base(err).AtWarning()
	}

	inbound := session.InboundFromContext(ctx)
	if inbound == nil {
		panic("no inbound metadata")
	}
	inbound.User = user
	sessionPolicy = s.policyManager.ForLevel(user.Level)

	if destination.Network == net.Network_UDP { // handle udp request
		return s.handleUDPPayload(ctx, &PacketReader{Reader: clientReader}, &PacketWriter{Writer: conn}, dispatcher)
	}

	ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
		From:   conn.RemoteAddr(),
		To:     destination,
		Status: log.AccessAccepted,
		Reason: "",
		Email:  user.Email,
	})

	newError("received request for ", destination).WriteToLog(sid)
	return s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher)

View on GitHub (pinned to db12914161)

Solutions

  1. Trace the caller: ensure the connection is accepted through a registered inbound handler (proxyman worker), which stamps ctx with session.ContextWithInbound before invoking the proxy.
  2. If you invoke Process() yourself (tests/embedding), wrap the context first: ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: "your-inbound-tag"}).
  3. Audit any intermediate code between accept and Process for context reassignment that drops values (search for 'ctx =' between the listener callback and the proxy call).
  4. As a defensive measure in custom hosts, recover() the panic and log the offending goroutine path to identify the missing wiring.

Example fix

// before (test / custom accept loop)
func handle(conn net.Conn) {
    ctx := context.Background()
    go trojanServer.Process(ctx, link, conn) // panics: no inbound metadata
}

// after
func handle(conn net.Conn) {
    ctx := context.Background()
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        Tag: "my-trojan-inbound",
        // normally filled by proxyman: Source/Gateway/Receiver as available
    })
    go trojanServer.Process(ctx, link, conn)
}
Defensive patterns

Strategy: validation

Validate before calling

// before driving a trojan inbound handler, verify the pipeline context:
if session.InboundFromContext(ctx) == nil {
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        Tag:    "my-inbound",
        Source: net.DestinationFromAddr(conn.RemoteAddr()),
    })
}
err := trojanInbound.Process(ctx, link, conn)

Type guard

func hasInboundMetadata(ctx context.Context) bool {
    return session.InboundFromContext(ctx) != nil
}

Try / catch

// last-resort containment around a nonstandard invocation:
func safeProcess(ctx context.Context, p proxy.Inbound, r io.Reader, w io.Writer) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = newError("inbound handler panicked: ", r)
        }
    }()
    return p.Process(ctx, r, w)
}

Prevention

When it happens

Trigger: Calling trojan.Server.Process(ctx, ...) directly (e.g. from a test or a custom dispatcher) with a plain context.Background()/context.TODO() that never went through proxyman's worker; wiring the Trojan inbound into a custom transport or embedding that invokes the proxy without first doing ctx = session.ContextWithInbound(ctx, &session.Inbound{...}); any code path that rebuilds or copies the context and drops the inbound value before dispatch.

Common situations: Writing unit tests for proxy handlers and passing a bare context; forking the inbound pipeline or adding a new transport that bypasses app/proxyman/inbound/worker.go; refactoring that accidentally replaces ctx (e.g. ctx = context.Background()) before calling Process; using community forks that drive proxies through custom accept loops (tun, reverse bridge set inbound explicitly, custom code often forgets).

Related errors


AI-assisted analysis of v2fly/v2ray-core@db12914161 (2026-08-15). Data as JSON: /api/errors/83cb1f6a14c5d1fc. Report an issue: GitHub.