[GH-ISSUE #2958] XRay/VLESS server config regenerated inconsistently on every settings interaction — Reality silently dropped, keys rotate, flow+security mismatch causes silent connection failure #1353

Open
opened 2026-10-13 20:40:49 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @K0T1KMEW on GitHub (Aug 7, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2958

Summary
On a self-hosted AmneziaVPN server, interacting with XRay protocol settings in the client (even without intentionally changing anything) causes the server-side XRay config to be regenerated from scratch rather than edited in place. Across repeated tests on the same server, this produced at least three different, inconsistent outcomes — including one combination that connects but never passes traffic, and one that fails to connect immediately. This makes XRay effectively unusable for any setup with more than one client device, since managing multiple devices requires touching the server config, which is precisely what triggers the regression.

Environment

  • Server: self-hosted, Ubuntu 24.04, official Amnezia install, XRay running in the amnezia-xray Docker container (alongside separate amnezia-awg / amnezia-awg2 containers for AmneziaWG, unaffected by this bug)
  • Clients tested: AmneziaVPN 5.0.0.5 on Windows 11, and AmneziaVPN 5.0.0.5 on Android — both linked to the same server, with client sync/admin enabled between the two devices
  • Also tested AmneziaVPN 4.8.15.4 on Windows 11 for comparison (see "Related" below)

What we observed, condensed from a full day of testing

  1. Config regeneration on every touch. Every time the XRay protocol settings were opened/saved on either device (Windows or Android) for the same server, /opt/amnezia/xray/server.json inside the container was rewritten with a brand-new privateKey. This happened even when no field was intentionally changed. This is the same root cause as #1225 (editing an existing XRay connection is not supported — any change effectively recreates it).

  2. security field is inconsistent across regenerations. Checked directly on the server after each client interaction:

    • Sometimes: "security": "reality" with a full, valid realitySettings block (privateKey, dest, serverNames, shortIds) — Reality correctly configured.
    • Sometimes: "security": "" — Reality silently absent, while the client UI still displayed "Reality" as the active/selected setting for that server, with no error or warning shown to the user.
    • We captured this transition at least 4 times across the session, purely by re-opening settings / toggling unrelated fields (e.g., flow) and saving again — no deliberate attempt to disable Reality was made.
  3. flow + security: none is a silently broken combination. When testing with an independent client (v2rayNG on Android, using a hand-built vless:// link derived directly from the server's actual server.json) to rule out client-side bugs:

    • flow=xtls-rprx-vision + security=none → connection fails immediately ("no internet" — fast failure, not a hang).
    • Same UUID's sibling client entry (no flow set) + security=none → connects and works normally, full speed.

    This suggests the server is willing to accept and serve a clients[] entry combining flow: xtls-rprx-vision with no security/Reality at all, which is a nonsensical combination (XTLS-Vision expects a TLS layer underneath), but the client silently generates such configs without validation.

  4. UI shows "connected but hangs forever" vs "connection fails fast" as different symptoms of different broken states, which made this hard to diagnose without directly inspecting the server-side JSON — the client gives no indication of which underlying state it is in.

Example configs captured (keys redacted)

Working (independent client, security: none, no flow):

{
    "inbounds": [{
        "port": 443,
        "protocol": "vless",
        "settings": {
            "clients": [
                { "flow": "xtls-rprx-vision", "id": "<uuid-1>" },
                { "id": "<uuid-2>" }
            ],
            "decryption": "none"
        },
        "streamSettings": { "network": "tcp", "security": "" }
    }],
    "outbounds": [{ "protocol": "freedom" }]
}

<uuid-2> (no flow) connects and works. <uuid-1> (flow set) fails immediately with this same security: "".

Also observed on the same server, a few interactions earlier (Reality present):

{
    "inbounds": [{
        "port": 443,
        "protocol": "vless",
        "settings": {
            "clients": [
                { "flow": "xtls-rprx-vision", "id": "<uuid-3>" },
                { "id": "<uuid-4>" },
                { "id": "<uuid-5>" }
            ],
            "decryption": "none"
        },
        "streamSettings": {
            "network": "tcp",
            "security": "reality",
            "realitySettings": {
                "dest": "www.googletagmanager.com:443",
                "fingerprint": "chrome",
                "privateKey": "<redacted>",
                "serverNames": ["www.googletagmanager.com"],
                "shortIds": ["<redacted>"]
            }
        }
    }],
    "outbounds": [{ "protocol": "freedom" }]
}

This state also worked when tested immediately after being generated — but was silently replaced by a security: "" state after the next unrelated settings interaction, with no user action targeting Reality specifically.

Separately observed on a different self-hosted server (same account, same 5.0.0.5 client): Windows tun2socks routing/DNS issue. Not certain if related, noting for completeness — on Windows, after connecting via XRay, tun2 interface comes up but occasionally the default route isn't installed (empty gateway in ipconfig), and separately, DNS (UDP/53) does not appear to be proxied through tun2socks even when TCP works (confirmed via netstat/nslookup — TCP connections established fine, but nslookup through the tunnel times out). This may be a separate bug from the config-regeneration issue above.

Expected behavior

  • Editing/re-saving XRay settings for an existing server should not regenerate privateKey or silently change security/Reality state unless the user explicitly changes that setting (same scope as #1225).
  • The client should never display "Reality" as active in the UI when the server-side config it manages has security: "" — this is a UI/state desync that hides the real problem from the user.
  • Server should not accept/generate a clients[] entry with flow: xtls-rprx-vision when security is not reality/tls, since this combination cannot work — client-side validation should prevent this, or clearly warn.
  • Multi-client (multi-device) management for XRay should be possible without triggering a full server reconfiguration — related to #902 (XRay user list not visible/manageable in the client), which made it impossible to see what the server currently held without SSH access.

Current workaround
Manually editing /opt/amnezia/xray/server.json inside the amnezia-xray container via SSH (adding/removing UUIDs in clients[] directly, fixing realitySettings by hand when needed), and afterward never opening the XRay protocol settings UI again on any linked device, since doing so risks silently reverting the manual configuration. Each client device is given a hand-built vless:// link matching the current server state, imported via a third-party client (v2rayNG/v2rayN) rather than relying on the Amnezia app's own config generation/sync for XRay specifically.

Related issues

  • #1225 — editing existing XRay connection settings not supported (same root cause as the regeneration behavior described here)
  • #902 — XRay user list not visible/manageable in the client (makes diagnosing server state impossible without SSH)
Originally created by @K0T1KMEW on GitHub (Aug 7, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2958 **Summary** On a self-hosted AmneziaVPN server, interacting with XRay protocol settings in the client (even without intentionally changing anything) causes the server-side XRay config to be regenerated from scratch rather than edited in place. Across repeated tests on the same server, this produced at least three different, inconsistent outcomes — including one combination that connects but never passes traffic, and one that fails to connect immediately. This makes XRay effectively unusable for any setup with more than one client device, since managing multiple devices requires touching the server config, which is precisely what triggers the regression. **Environment** - Server: self-hosted, Ubuntu 24.04, official Amnezia install, XRay running in the `amnezia-xray` Docker container (alongside separate `amnezia-awg` / `amnezia-awg2` containers for AmneziaWG, unaffected by this bug) - Clients tested: AmneziaVPN 5.0.0.5 on Windows 11, and AmneziaVPN 5.0.0.5 on Android — both linked to the same server, with client sync/admin enabled between the two devices - Also tested AmneziaVPN 4.8.15.4 on Windows 11 for comparison (see "Related" below) **What we observed, condensed from a full day of testing** 1. **Config regeneration on every touch.** Every time the XRay protocol settings were opened/saved on either device (Windows or Android) for the same server, `/opt/amnezia/xray/server.json` inside the container was rewritten with a brand-new `privateKey`. This happened even when no field was intentionally changed. This is the same root cause as #1225 (editing an existing XRay connection is not supported — any change effectively recreates it). 2. **`security` field is inconsistent across regenerations.** Checked directly on the server after each client interaction: - Sometimes: `"security": "reality"` with a full, valid `realitySettings` block (privateKey, dest, serverNames, shortIds) — Reality correctly configured. - Sometimes: `"security": ""` — Reality silently absent, **while the client UI still displayed "Reality" as the active/selected setting for that server**, with no error or warning shown to the user. - We captured this transition at least 4 times across the session, purely by re-opening settings / toggling unrelated fields (e.g., `flow`) and saving again — no deliberate attempt to disable Reality was made. 3. **`flow` + `security: none` is a silently broken combination.** When testing with an independent client (v2rayNG on Android, using a hand-built `vless://` link derived directly from the server's actual `server.json`) to rule out client-side bugs: - `flow=xtls-rprx-vision` + `security=none` → connection fails immediately ("no internet" — fast failure, not a hang). - Same UUID's sibling client entry (no `flow` set) + `security=none` → connects and works normally, full speed. This suggests the server is willing to accept and serve a `clients[]` entry combining `flow: xtls-rprx-vision` with no `security`/Reality at all, which is a nonsensical combination (XTLS-Vision expects a TLS layer underneath), but the client silently generates such configs without validation. 4. **UI shows "connected but hangs forever" vs "connection fails fast" as different symptoms of different broken states**, which made this hard to diagnose without directly inspecting the server-side JSON — the client gives no indication of which underlying state it is in. **Example configs captured (keys redacted)** Working (independent client, `security: none`, no flow): ```json { "inbounds": [{ "port": 443, "protocol": "vless", "settings": { "clients": [ { "flow": "xtls-rprx-vision", "id": "<uuid-1>" }, { "id": "<uuid-2>" } ], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "" } }], "outbounds": [{ "protocol": "freedom" }] } ``` `<uuid-2>` (no flow) connects and works. `<uuid-1>` (flow set) fails immediately with this same `security: ""`. Also observed on the same server, a few interactions earlier (Reality present): ```json { "inbounds": [{ "port": 443, "protocol": "vless", "settings": { "clients": [ { "flow": "xtls-rprx-vision", "id": "<uuid-3>" }, { "id": "<uuid-4>" }, { "id": "<uuid-5>" } ], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "dest": "www.googletagmanager.com:443", "fingerprint": "chrome", "privateKey": "<redacted>", "serverNames": ["www.googletagmanager.com"], "shortIds": ["<redacted>"] } } }], "outbounds": [{ "protocol": "freedom" }] } ``` This state also worked when tested immediately after being generated — but was silently replaced by a `security: ""` state after the next unrelated settings interaction, with no user action targeting Reality specifically. **Separately observed on a different self-hosted server (same account, same 5.0.0.5 client): Windows tun2socks routing/DNS issue.** Not certain if related, noting for completeness — on Windows, after connecting via XRay, `tun2` interface comes up but occasionally the default route isn't installed (empty gateway in `ipconfig`), and separately, DNS (UDP/53) does not appear to be proxied through tun2socks even when TCP works (confirmed via `netstat`/`nslookup` — TCP connections established fine, but `nslookup` through the tunnel times out). This may be a separate bug from the config-regeneration issue above. **Expected behavior** - Editing/re-saving XRay settings for an existing server should not regenerate `privateKey` or silently change `security`/Reality state unless the user explicitly changes that setting (same scope as #1225). - The client should never display "Reality" as active in the UI when the server-side config it manages has `security: ""` — this is a UI/state desync that hides the real problem from the user. - Server should not accept/generate a `clients[]` entry with `flow: xtls-rprx-vision` when `security` is not `reality`/tls, since this combination cannot work — client-side validation should prevent this, or clearly warn. - Multi-client (multi-device) management for XRay should be possible without triggering a full server reconfiguration — related to #902 (XRay user list not visible/manageable in the client), which made it impossible to see what the server currently held without SSH access. **Current workaround** Manually editing `/opt/amnezia/xray/server.json` inside the `amnezia-xray` container via SSH (adding/removing UUIDs in `clients[]` directly, fixing `realitySettings` by hand when needed), and afterward never opening the XRay protocol settings UI again on any linked device, since doing so risks silently reverting the manual configuration. Each client device is given a hand-built `vless://` link matching the current server state, imported via a third-party client (v2rayNG/v2rayN) rather than relying on the Amnezia app's own config generation/sync for XRay specifically. **Related issues** - #1225 — editing existing XRay connection settings not supported (same root cause as the regeneration behavior described here) - #902 — XRay user list not visible/manageable in the client (makes diagnosing server state impossible without SSH)
Author
Owner

@vkamn commented on GitHub (Aug 14, 2026):

Is this issue still present in the current dev branch?

<!-- gh-comment-id:5294467333 --> @vkamn commented on GitHub (Aug 14, 2026): Is this issue still present in the current dev branch?
Author
Owner

@Ingwar77 commented on GitHub (Sep 2, 2026):

Adding a corroborating report from a separate self-hosted deployment — same environment class as the original report (self-hosted, Ubuntu 24.04, amnezia-xray in its own Docker container, separate from amnezia-awg2).

I haven't captured a direct before/after diff of the privateKey (didn't want to risk destabilizing a working production setup to reproduce it), but the observed symptoms line up with this issue:

  • The exported VLESS/XRay config from this server connects and works correctly in third-party clients (v2RayN, Shadowrocket) on the same machine, but fails to establish a connection when used from within AmneziaVPN itself, using the same server profile.
  • Separately, the amnezia-xray container's ID has changed between sessions without any manual container-level action on our end — consistent with the client silently redeploying backend state.
  • A related instability pattern (not the same mechanism, but same class of bug) was also observed with AWG/MTProto settings: editing those via the client shifted the actual listening ports and even spun up an entirely new, separate MTProxy container, without any clear indication in the UI that this had happened.
  • Upgrading to 5.0.1.5 also required a full deletion/reinstall of the existing AWG2.0 and XRay configs rather than an in-place update, which triggered multiple server reboots.

None of this pinpoints the exact internal cause, but it does suggest the config-regeneration/state-desync behavior described here isn't isolated to one deployment.

<!-- gh-comment-id:5515597148 --> @Ingwar77 commented on GitHub (Sep 2, 2026): Adding a corroborating report from a separate self-hosted deployment — same environment class as the original report (self-hosted, Ubuntu 24.04, `amnezia-xray` in its own Docker container, separate from `amnezia-awg2`). I haven't captured a direct before/after diff of the privateKey (didn't want to risk destabilizing a working production setup to reproduce it), but the observed symptoms line up with this issue: - The exported VLESS/XRay config from this server connects and works correctly in third-party clients (v2RayN, Shadowrocket) on the same machine, but fails to establish a connection when used from within AmneziaVPN itself, using the same server profile. - Separately, the `amnezia-xray` container's ID has changed between sessions without any manual container-level action on our end — consistent with the client silently redeploying backend state. - A related instability pattern (not the same mechanism, but same class of bug) was also observed with AWG/MTProto settings: editing those via the client shifted the actual listening ports and even spun up an entirely new, separate MTProxy container, without any clear indication in the UI that this had happened. - Upgrading to 5.0.1.5 also required a full deletion/reinstall of the existing AWG2.0 and XRay configs rather than an in-place update, which triggered multiple server reboots. None of this pinpoints the exact internal cause, but it does suggest the config-regeneration/state-desync behavior described here isn't isolated to one deployment.
Author
Owner

@HeisLuka commented on GitHub (Sep 9, 2026):

I checked this against current dev.

The answer is: the original issue is partially fixed, but the underlying destructive-update problem is not completely gone.

There have been several meaningful changes since this report was opened.

What appears fixed

1. Simply opening/saving unchanged settings should no longer trigger an update

Current PageProtocolXraySettings.qml only shows the Save button when:

XrayConfigModel.hasUnsavedChanges || portDirty

and XrayConfigModel::hasUnsavedChanges() compares the current server settings with the original configuration.

So the original:

open settings
-> save without changing anything
-> regenerate server

path no longer appears to be intentionally reachable.

That is an important improvement.

2. flow=xtls-rprx-vision without a secure transport is now sanitized

Current XRay configurator has:

effectiveClientFlow(...)

which only keeps flow when the transport is raw and security is either:

tls
reality

Otherwise the effective flow is empty.

So the specifically reported state:

flow = xtls-rprx-vision
security = none

is no longer generated through this path.

3. The old XRay update implementation has changed substantially

Current applyServerSettingsToRemote() reads the existing server.json first and works from that object instead of blindly generating a new server document.

It preserves the existing clients[] list and either updates their effective flow or appends a new client.

Also, commit 667d98f7 (#2975) explicitly removed:

  • the old mergeStreamSettingsForServerInbound() update;
  • direct port rewriting in that path;
  • the unconditional:
if (container == DockerContainer::Xray ...)
    reinstallRequired = true;

So I would not describe current dev as “XRay is regenerated on every settings interaction” anymore.

That part of this issue is stale.

What still concerns me

The remaining problem is the boundary between a normal configuration edit and an identity-destructive reinstall.

The current XRay setup script still generates fresh state:

new XRay client UUID
new Reality shortId
new X25519 private/public key pair

every time that setup path runs.

And InstallController::setupContainer() is still a container setup/rebuild path which eventually calls the XRay setup/config generation logic.

Therefore, for any server-settings change which InstallController classifies as requiring a reinstall, the important question becomes:

does an ordinary settings change preserve the existing
XRay server identity,
or does it create a new identity?

If it creates a new identity, then the multi-client failure mode from this issue still exists, just with a narrower trigger than originally reported.

The UI itself still warns before saving XRay settings:

"All users with whom you shared a connection with
will no longer be able to connect to it."

which also suggests that at least some current update operations are intentionally destructive.

I think the invariant should be stronger

Routine server configuration changes should preserve:

Reality private/public key pair
Reality shortId
existing clients[] UUIDs
unknown/unmanaged server.json fields
other persistent server identity

unless the user explicitly requests identity/key rotation.

Conceptually:

ordinary edit
    -> read current effective server.json
    -> patch only requested fields
    -> validate candidate
    -> restart/reload
    -> read back effective state
    -> commit local model

explicit Rotate identity
    -> generate new keys / IDs
    -> invalidate old clients intentionally

A container rebuild should not automatically imply cryptographic identity rotation.

If rebuilding the image is necessary for some server-setting changes, the existing identity could be read before replacement and migrated into the new generation.

Failure handling is also important

The current model should ideally behave transactionally:

old working generation
        ↓
stage candidate
        ↓
validate XRay config
        ↓
restart/reload
        ↓
verify runtime/readback
        ↓
commit candidate

If validation/restart/readback fails, the old working configuration should remain recoverable rather than leaving the stored client state ahead of the actual server state.

Regression cases

I would use at least:

  1. Open/close XRay settings without changes:

    • no remote write;
    • no restart;
    • no key/UUID changes.
  2. Change only flow:

    • existing Reality keys preserved;
    • existing client UUIDs preserved.
  3. Change ordinary transport/security parameters:

    • define explicitly which changes require container rebuild;
    • rebuild alone must not rotate server identity.
  4. Existing multi-client server:

    • settings update preserves every unrelated clients[] entry.
  5. Invalid flow/security/transport combination:

    • rejected or normalized before remote mutation.
  6. Failed candidate/restart:

    • previous working server configuration remains usable.
  7. Explicit “rotate/regenerate credentials” operation:

    • keys and client identity change intentionally.

So for the maintainer question “is this still present on current dev?” I would classify it as:

partially fixed.

The unconditional/no-op regeneration and the reported invalid flow/security combination appear addressed, but XRay server identity still needs an explicit preservation contract across any update path that performs a container reinstall.

<!-- gh-comment-id:5607995456 --> @HeisLuka commented on GitHub (Sep 9, 2026): I checked this against current `dev`. The answer is: **the original issue is partially fixed, but the underlying destructive-update problem is not completely gone.** There have been several meaningful changes since this report was opened. ### What appears fixed #### 1. Simply opening/saving unchanged settings should no longer trigger an update Current `PageProtocolXraySettings.qml` only shows the Save button when: ```text XrayConfigModel.hasUnsavedChanges || portDirty ``` and `XrayConfigModel::hasUnsavedChanges()` compares the current server settings with the original configuration. So the original: ```text open settings -> save without changing anything -> regenerate server ``` path no longer appears to be intentionally reachable. That is an important improvement. #### 2. `flow=xtls-rprx-vision` without a secure transport is now sanitized Current XRay configurator has: ```cpp effectiveClientFlow(...) ``` which only keeps `flow` when the transport is raw and security is either: ```text tls reality ``` Otherwise the effective flow is empty. So the specifically reported state: ```text flow = xtls-rprx-vision security = none ``` is no longer generated through this path. #### 3. The old XRay update implementation has changed substantially Current `applyServerSettingsToRemote()` reads the existing `server.json` first and works from that object instead of blindly generating a new server document. It preserves the existing `clients[]` list and either updates their effective flow or appends a new client. Also, commit `667d98f7` (`#2975`) explicitly removed: * the old `mergeStreamSettingsForServerInbound()` update; * direct port rewriting in that path; * the unconditional: ```cpp if (container == DockerContainer::Xray ...) reinstallRequired = true; ``` So I would **not** describe current `dev` as “XRay is regenerated on every settings interaction” anymore. That part of this issue is stale. ### What still concerns me The remaining problem is the boundary between a normal configuration edit and an identity-destructive reinstall. The current XRay setup script still generates fresh state: ```text new XRay client UUID new Reality shortId new X25519 private/public key pair ``` every time that setup path runs. And `InstallController::setupContainer()` is still a container setup/rebuild path which eventually calls the XRay setup/config generation logic. Therefore, for any server-settings change which `InstallController` classifies as requiring a reinstall, the important question becomes: ```text does an ordinary settings change preserve the existing XRay server identity, or does it create a new identity? ``` If it creates a new identity, then the multi-client failure mode from this issue still exists, just with a narrower trigger than originally reported. The UI itself still warns before saving XRay settings: ```text "All users with whom you shared a connection with will no longer be able to connect to it." ``` which also suggests that at least some current update operations are intentionally destructive. ### I think the invariant should be stronger Routine server configuration changes should preserve: ```text Reality private/public key pair Reality shortId existing clients[] UUIDs unknown/unmanaged server.json fields other persistent server identity ``` unless the user explicitly requests identity/key rotation. Conceptually: ```text ordinary edit -> read current effective server.json -> patch only requested fields -> validate candidate -> restart/reload -> read back effective state -> commit local model explicit Rotate identity -> generate new keys / IDs -> invalidate old clients intentionally ``` A container rebuild should not automatically imply cryptographic identity rotation. If rebuilding the image is necessary for some server-setting changes, the existing identity could be read before replacement and migrated into the new generation. ### Failure handling is also important The current model should ideally behave transactionally: ```text old working generation ↓ stage candidate ↓ validate XRay config ↓ restart/reload ↓ verify runtime/readback ↓ commit candidate ``` If validation/restart/readback fails, the old working configuration should remain recoverable rather than leaving the stored client state ahead of the actual server state. ### Regression cases I would use at least: 1. Open/close XRay settings without changes: * no remote write; * no restart; * no key/UUID changes. 2. Change only `flow`: * existing Reality keys preserved; * existing client UUIDs preserved. 3. Change ordinary transport/security parameters: * define explicitly which changes require container rebuild; * rebuild alone must not rotate server identity. 4. Existing multi-client server: * settings update preserves every unrelated `clients[]` entry. 5. Invalid `flow/security/transport` combination: * rejected or normalized before remote mutation. 6. Failed candidate/restart: * previous working server configuration remains usable. 7. Explicit “rotate/regenerate credentials” operation: * keys and client identity change intentionally. So for the maintainer question “is this still present on current dev?” I would classify it as: **partially fixed.** The unconditional/no-op regeneration and the reported invalid flow/security combination appear addressed, but XRay server identity still needs an explicit preservation contract across any update path that performs a container reinstall.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#1353
No description provided.