Support for VLESS Reverse

Summary

Xray v25.10.15 (PR #5101) introduces a VLESS simplified reverse proxy feature where a specific user entry in a VLESS inbound includes a reverse field:

{
  "tag": "r-inbound",
  "port": 8443,
  "protocol": "vless",
  "settings": {
    "users": [
      {
        "id": "ac04551d-6ebf-4685-86e2-17c12491f7f4",
        "flow": "xtls-rprx-vision",
        "reverse": {
          "tag": "r-outbound"
        }
      }
    ],
    "decryption": "mlkem768x25519plus.native.600s.aCF82eKiK6g0DIbv0_nsjbHC4RyKCc9NRjl-X9lyi0k"
  }
}

This allows a device behind NAT/firewall (no inbound ports open) to establish a reverse tunnel to a public server without any overlay network such as Tailscale.

References:

Problem

Remnawave’s config processing pipeline makes this impossible for three reasons:

Issue 1: cleanInboundClients() unconditionally wipes all VLESS users/clients

In src/common/helpers/xray-config/xray-config.validator.ts:

public cleanInboundClients(injectFlow: boolean): void {
    for (const inbound of this.config.inbounds) {
        if (!this.hasManagedClients(inbound)) continue;
        this.ensureSettings(inbound);
        inbound.settings!.clients = [];  // always wiped
    }
}

Any reverse-annotated user entry written in the config profile is destroyed before xray receives the config.

Issue 2: addUsersToInbound() only injects { id, email } — no reverse field

case 'vless':
    inbound.settings.clients ??= [];
    for (const user of users) {
        inbound.settings.clients.push({
            id: user.vlessUuid,
            email: user.tId.toString(),
            // 'reverse' field is never injected
        });
    }

There is no mechanism in the user data model or squad system to carry a reverse field through to the injected config.

Issue 3: leaveInbounds() excludes unregistered VLESS inbounds

public leaveInbounds(tags: Set): void {
    this.config.inbounds = this.config.inbounds!.filter(
        (inbound) => tags.has(inbound.tag!) || !this.hasManagedClients(inbound),
    );
}

Since vless is a managed protocol, a reverse-only inbound not present in activeInbounds is stripped from the config. But if it is added to activeInbounds, its reverse-annotated users get wiped by Issue 1.

Use Case

A device sits behind a home router where the firewall cannot be modified (no open inbound ports). The VLESS simplified reverse proxy in Xray v26 is exactly designed for this NAT traversal scenario — the internal device initiates an outbound connection to the public server and establishes a persistent reverse tunnel. This feature cannot be used with Remnawave as the panel.

Expected Behavior

One of the following would resolve this:

  1. Preferred: Add a “reverse proxy user” concept to the squad/inbound system, where a special non-subscribing user entry carries a reverse: { tag: "..." } field that is injected as-is into the target VLESS inbound’s users array, bypassing cleanInboundClients for that entry.

  2. Alternative: Introduce a flag on an inbound registration (e.g. isReverseProxy: true) that causes Remnawave to skip cleanInboundClients for that inbound and pass the raw users array from the config profile directly to xray unchanged.

Environment

  • Remnawave backend: v2.7.4
  • Xray-core: v26.3.27
  • xray-typed: v1.1.1

Alternative suggested workaround has drawbacks in my pov also preferred one is somehow not very optimal

It can be done in similar way but with a better execution and customisation options:

  1. User Edit/Create
  2. Add a new toggle as isReverse
  3. Condition check == If TRUE > then >
    4. A new field for Reverse Tag to be typed by Admin (or with a button to randomly generated?)
    5. A new Inbound selection dropdown (like internal squads) in user’s access scope to be used as reverse proxy inbound/outbound
  4. Remnawave injects the related code at runtime to that inbounds related nodes

I think it can be done more easily than other options.

There will never be any additional fields for “User.”

I don’t want to make the same mistake other panels made when they linked “flow” to a specific user. In my opinion, such an architecture will cause a huge number of problems in the future. I believe a more correct approach is the same as was implemented for “flow,” which was moved to the same level as “clients.”

It might look something like this, but I haven’t yet decided whether to make a PR to Xray repo: Comparing XTLS:main...kastov:reverse · XTLS/Xray-core · GitHub

I haven’t seen that you took initiative to change related matters of it in the core itself.

If it got merged, so yeah otherwise what are the options?

Unfortunately, the PR was rejected.

So, need to do it with a workaround unfortunately…