How to Implement Multi-Port Inbound with Different Protocols on Xray-Core

You can run multiple inbound handlers on a single Xray-core instance by defining separate inbound entries in your configuration, each with unique tags, distinct port ranges in receiver_settings, and protocol-specific proxy_settings blocks.

Xray-core supports running multiple proxy protocols simultaneously on different ports through its modular inbound handler architecture. Each listening endpoint consists of ReceiverSettings (defining where to listen) and ProxySettings (defining what protocol to run), managed independently by the inbound manager in app/proxyman/inbound/inbound.go. This allows a single Xray-core binary to handle VMess on port 443, VLESS on port 8443, and Shadowsocks on port 8388 simultaneously.

Understanding the Inbound Architecture

Xray-core treats every listening endpoint as a discrete inbound handler. Understanding the relationship between receiver configuration and proxy configuration is essential for implementing multi-port setups.

ReceiverConfig and Port Allocation

The ReceiverConfig (defined in app/proxyman/config.proto) specifies the network binding parameters:

  • listen: The IP address to bind (e.g., "0.0.0.0" for all interfaces)
  • port_list: A PortList message supporting single ports or ranges
  • stream_settings: TLS, XTLS, or transport layer configuration

According to the source code in app/proxyman/inbound/always.go, the AlwaysOnInboundHandler iterates over every port in the PortList and creates independent workers (implementations of tcpWorker, udpWorker, or dsWorker) for each.

ProxySettings and Protocol Selection

The ProxySettings (e.g., vmess.inbound.Config, vless.inbound.Config) define the protocol implementation and authentication:

  • type: Protocol identifier ("vmess", "vless", "trojan", "shadowsocks", etc.)
  • users: Authentication credentials specific to the protocol

Each inbound handler combines one ReceiverConfig with one ProxySettings message. Because these are paired one-to-one, you must define separate inbound entries to run different protocols on different ports.

Configuration Examples

Xray-core supports JSON, YAML, TOML, and Protobuf configurations. Below are practical examples showing VMess on port 443 and VLESS on port 8443.

JSON Configuration

{
  "inbound": [
    {
      "tag": "vmess-443",
      "receiver_settings": {
        "@type": "pipe",
        "listen": "0.0.0.0",
        "port_list": {
          "range": [
            {
              "from": 443,
              "to": 443
            }
          ]
        },
        "sniffing_settings": {
          "enabled": true,
          "dest_override": ["http", "tls"]
        }
      },
      "proxy_settings": {
        "@type": "vmess",
        "users": [
          {
            "id": "11111111-1111-1111-1111-111111111111",
            "alterId": 64,
            "level": 0,
            "email": "user@example.com"
          }
        ]
      }
    },
    {
      "tag": "vless-8443",
      "receiver_settings": {
        "@type": "pipe",
        "listen": "0.0.0.0",
        "port_list": {
          "range": [
            {
              "from": 8443,
              "to": 8443
            }
          ]
        }
      },
      "proxy_settings": {
        "@type": "vless",
        "users": [
          {
            "id": "22222222-2222-2222-2222-222222222222",
            "encryption": "none",
            "level": 0,
            "email": "vless@example.com"
          }
        ]
      }
    }
  ],
  "outbound": [
    {
      "tag": "direct",
      "protocol": "freedom",
      "settings": {}
    }
  ]
}

YAML Configuration

inbound:
  - tag: vmess-443
    receiver_settings:
      type: pipe
      listen: 0.0.0.0
      port_list:
        range:
          - from: 443
            to: 443
      sniffing_settings:
        enabled: true
        dest_override:
          - http
          - tls
    proxy_settings:
      type: vmess
      users:
        - id: 11111111-1111-1111-1111-111111111111
          alterId: 64
          level: 0
          email: user@example.com

  - tag: vless-8443
    receiver_settings:
      type: pipe
      listen: 0.0.0.0
      port_list:
        range:
          - from: 8443
            to: 8443
    proxy_settings:
      type: vless
      users:
        - id: 22222222-2222-2222-2222-222222222222
          encryption: none
          level: 0
          email: vless@example.com

outbound:
  - tag: direct
    protocol: freedom
    settings: {}

Critical configuration notes:

  • The tag field must be unique for each inbound handler; it serves as the key in inbound.Manager's handler map.
  • The type: pipe in receiver_settings resolves to proxyman.ReceiverConfig.
  • A single ReceiverConfig cannot host multiple protocols; each protocol requires its own inbound entry.

Internal Handler Wiring

Understanding how Xray-core processes your configuration helps debug multi-port setups.

Handler Creation Flow

When Xray starts, the following sequence occurs according to app/proxyman/inbound/inbound.go:

  1. Config parsing: core.LoadConfig unmarshals the configuration into core.Config (defined in core/config.pb.go), which contains a slice of InboundHandlerConfig structs.
  2. Handler instantiation: proxyman.NewHandler(ctx, cfg) (line 55) extracts:
    • receiverSettings as *proxyman.ReceiverConfig
    • proxySettings as *serial.TypedMessage
  3. Object creation: The function calls common.CreateObject to instantiate the protocol-specific inbound handler (e.g., VMess or VLESS).
  4. Registration: The handler is registered in inbound.Manager using its tag.

Worker Generation and Port Binding

The NewAlwaysOnInboundHandler function in app/proxyman/inbound/always.go (lines 27-66) manages the actual network listeners:

  • Iterates through the PortList in the ReceiverConfig
  • For each port, creates protocol-specific workers (tcpWorker, udpWorker, or dsWorker for domain sockets)
  • Each worker calls net.ListenTCP or net.ListenUDP independently
  • All workers share the same proxy.Inbound instance but maintain separate socket references

When Manager.Start() executes (lines 8-25 of inbound.go), every registered handler starts its workers, resulting in independent listeners on the configured ports.

Practical Implementation Tips

  • Port ranges for single protocols: If you need a wide port range for the same protocol, use a single ReceiverConfig with multiple ranges in port_list. This is more efficient than creating separate inbound entries.

  • Protocol-specific stream settings: When different protocols require different TLS certificates or transport settings (WebSocket, gRPC, etc.), define separate inbound entries each with their own stream_settings under receiver_settings.

  • Debugging listener startup: Enable debug logging (loglevel: debug) to verify worker creation. The errors.LogDebug calls in always.go (lines 31-33) output the specific address and port each worker binds to.

  • Tag uniqueness: Duplicate tags cause registration failures in inbound.Manager. Always use descriptive, unique tags like "vmess-tcp-443" or "trojan-ws-8443".

Summary

  • Multi-port inbound requires separate inbound entries in your Xray-core configuration, each with unique tags.
  • ReceiverSettings (type: pipe) control where the server listens, supporting single ports or ranges via port_list.
  • ProxySettings determine the protocol (VMess, VLESS, Trojan, etc.) and authentication for that specific listener.
  • The inbound manager in app/proxyman/inbound/inbound.go registers each handler, while NewAlwaysOnInboundHandler in always.go creates independent workers for every specified port.
  • A single inbound entry cannot host multiple protocols; each protocol requires its own handler configuration.

Frequently Asked Questions

Can I run multiple protocols on the same port?

No. A single ReceiverConfig can only bind one protocol implementation. To handle multiple protocols on the same port (e.g., multiplexing TLS and non-TLS on 443), you must use a multiplexing transport like splithttp or place a reverse proxy (such as Nginx or HAProxy) in front of Xray-core to route traffic based on SNI or ALPN before it reaches the Xray listeners.

How do I configure a port range for one protocol?

Specify a range in the port_list field under receiver_settings. For example, setting from: 10000 and to: 10100 creates 101 separate workers listening on ports 10000 through 10100, all using the same protocol configuration. This is handled efficiently by the AlwaysOnInboundHandler without requiring 101 separate inbound entries.

Why does my multi-port configuration fail to start?

The most common causes are duplicate tag values (each inbound must have a unique tag) or port conflicts where another process is already bound to the specified port. Check the debug logs for errors from inbound.Manager or NewAlwaysOnInboundHandler regarding handler registration or net.Listen failures.

Can different inbound handlers share the same outbound routing?

Yes. The tag in inbound configuration is used for routing decisions, but multiple inbounds can route to the same outbound tag. Define your outbound handlers (e.g., "direct", "blocked", or a remote server) once, then reference the appropriate outbound tag in your routing rules based on the inbound tag or domain matching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →