[PR #2453] [CLOSED] Require SOCKS5 auth on local Xray inbound (fixes #2452) #2813

Closed
opened 2026-10-14 21:05:28 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amnezia-client/pull/2453
Author: @Gerk17
Created: 4/8/2026
Status: ❌ Closed

Base: dev ← Head: fix/issue-2452-socks-auth


📝 Commits (1)

  • 9436a8b Require SOCKS5 auth on local Xray inbound (fixes #2452)

📊 Changes

6 files changed (+130 additions, -7 deletions)

View changed files

📝 client/android/xray/src/main/kotlin/Xray.kt (+11 -1)
📝 client/android/xray/src/main/kotlin/XrayConfig.kt (+10 -0)
📝 client/core/serialization/inbound.cpp (+60 -4)
📝 client/core/serialization/serialization.h (+17 -0)
📝 client/platforms/ios/PacketTunnelProvider+Xray.swift (+26 -0)
📝 client/protocols/xrayprotocol.cpp (+6 -2)

📄 Description

Summary

Closes #2452.

The local SOCKS5 listener that bridges tun2socks and the Xray core was
unauthenticated and bound to 127.0.0.1:10808. On Android the loopback
interface is shared across UIDs, so any third-party app with INTERNET
permission could connect to it and use it as an open proxy, bypassing
per-app split tunneling and sending traffic through the VPN tunnel even
when the user had explicitly excluded the app.

This PR requires SOCKS5 username/password authentication on that
inbound. Credentials are generated per process via
QRandomGenerator::system(), kept only in memory, and re-rolled on
every process start. The tun2socks consumer on each platform reads the
credentials and authenticates, so the VPN keeps working end-to-end
while external apps can no longer use the proxy.

Changes

Serialization (client/core/serialization/)

  • inbound.cpp: GenerateInboundEntry() now emits
    settings.auth = "password" and settings.accounts = [{user, pass}]
    with random 24-char tokens generated by QRandomGenerator::system().
    Adds GetInboundCredentials() returning the same pair for in-process
    consumers via a Meyers singleton.
  • serialization.h: declares InboundCredentials and
    GetInboundCredentials() next to the existing GenerateInboundEntry().

Desktop (client/protocols/xrayprotocol.cpp)

  • The tun2socks process is now launched with
    socks5://user:pass@127.0.0.1:10808 instead of the unauthenticated
    URL. The credentials are read from GetInboundCredentials() so they
    always match what GenerateInboundEntry() put into the Xray config.

Android (client/android/xray/src/main/kotlin/)

  • XrayConfig.kt: adds socksUser / socksPass fields, builder
    setters and constructor wiring, mirroring the existing socksPort.
  • Xray.kt: parses
    inbounds[0].settings.accounts[0] from the Xray JSON config and
    populates the new fields. runTun2Socks() builds a
    socks5://user:pass@127.0.0.1:port URL for Tun2SocksConfig.proxy.
    Uses optJSONObject / optJSONArray so older configs without an
    accounts block degrade gracefully (no auth → behaves as before).

iOS (client/platforms/ios/PacketTunnelProvider+Xray.swift)

  • Reads the same inbounds[0].settings.accounts[0] from the JSON
    config and threads username / password into
    setupAndRunTun2socks(...). The hev-socks5-tunnel YAML config now
    includes username: / password: lines under socks5: when
    credentials are present.

Security properties

  • Credentials are generated by a CSPRNG (QRandomGenerator::system(),
    which maps to /dev/urandom on Linux/Android, SecRandomCopyBytes
    on Apple, BCryptGenRandom on Windows).
  • They live only in process memory, are never written to disk, logs,
    or notifications, and are never serialized into shareable Xray
    config exports.
  • They are re-rolled on every process start, so a VPN reconnect after
    an app kill yields a fresh pair.
  • After the patch, an external app connecting to 127.0.0.1:10808
    receives a SOCKS5 handshake that requires method 0x02
    (username/password) and cannot proceed without the secret. The
    per-app split-tunneling bypass from #2452 is closed.

What this PR does not do

It does not hide the fact that something is listening on
127.0.0.1:10808. An attacker app can still probe the port and infer
"AmneziaVPN with Xray is active". Fully addressing the
detectability concern from the second half of #2452 would require
moving the IPC off loopback TCP entirely (e.g. to a Unix domain
socket inside the app's private files dir), which in turn requires
adding Unix socket support upstream in hev-socks5-tunnel. That
should be tracked as a follow-up; this PR is the minimal fix that
closes the bypass without new dependencies.

Testing

Locally on Linux desktop with Qt 6.6.2:

  • Full client builds cleanly (235/235, both AmneziaVPN and
    AmneziaVPN-service link successfully).
  • A small standalone test calling GenerateInboundEntry() and
    GetInboundCredentials() confirms that the JSON contains
    auth: "password" and a populated accounts array, that the
    credentials returned by GetInboundCredentials() match the ones
    inside the JSON, and that they differ between process invocations.

The Android, iOS, Windows and macOS changes are mechanical and rely
on this repository's CI for build verification. Reviewers familiar
with each platform should sanity-check the wiring; I'm happy to
iterate on anything that looks off.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/amnezia-vpn/amnezia-client/pull/2453 **Author:** [@Gerk17](https://github.com/Gerk17) **Created:** 4/8/2026 **Status:** ❌ Closed **Base:** `dev` ← **Head:** `fix/issue-2452-socks-auth` --- ### 📝 Commits (1) - [`9436a8b`](https://github.com/amnezia-vpn/amnezia-client/commit/9436a8b8997bef6c8fc9945364cc56688bcdd2d3) Require SOCKS5 auth on local Xray inbound (fixes #2452) ### 📊 Changes **6 files changed** (+130 additions, -7 deletions) <details> <summary>View changed files</summary> 📝 `client/android/xray/src/main/kotlin/Xray.kt` (+11 -1) 📝 `client/android/xray/src/main/kotlin/XrayConfig.kt` (+10 -0) 📝 `client/core/serialization/inbound.cpp` (+60 -4) 📝 `client/core/serialization/serialization.h` (+17 -0) 📝 `client/platforms/ios/PacketTunnelProvider+Xray.swift` (+26 -0) 📝 `client/protocols/xrayprotocol.cpp` (+6 -2) </details> ### 📄 Description ## Summary Closes #2452. The local SOCKS5 listener that bridges `tun2socks` and the Xray core was unauthenticated and bound to `127.0.0.1:10808`. On Android the loopback interface is shared across UIDs, so any third-party app with `INTERNET` permission could connect to it and use it as an open proxy, bypassing per-app split tunneling and sending traffic through the VPN tunnel even when the user had explicitly excluded the app. This PR requires SOCKS5 username/password authentication on that inbound. Credentials are generated per process via `QRandomGenerator::system()`, kept only in memory, and re-rolled on every process start. The tun2socks consumer on each platform reads the credentials and authenticates, so the VPN keeps working end-to-end while external apps can no longer use the proxy. ## Changes **Serialization (`client/core/serialization/`)** - `inbound.cpp`: `GenerateInboundEntry()` now emits `settings.auth = "password"` and `settings.accounts = [{user, pass}]` with random 24-char tokens generated by `QRandomGenerator::system()`. Adds `GetInboundCredentials()` returning the same pair for in-process consumers via a Meyers singleton. - `serialization.h`: declares `InboundCredentials` and `GetInboundCredentials()` next to the existing `GenerateInboundEntry()`. **Desktop (`client/protocols/xrayprotocol.cpp`)** - The tun2socks process is now launched with `socks5://user:pass@127.0.0.1:10808` instead of the unauthenticated URL. The credentials are read from `GetInboundCredentials()` so they always match what `GenerateInboundEntry()` put into the Xray config. **Android (`client/android/xray/src/main/kotlin/`)** - `XrayConfig.kt`: adds `socksUser` / `socksPass` fields, builder setters and constructor wiring, mirroring the existing `socksPort`. - `Xray.kt`: parses `inbounds[0].settings.accounts[0]` from the Xray JSON config and populates the new fields. `runTun2Socks()` builds a `socks5://user:pass@127.0.0.1:port` URL for `Tun2SocksConfig.proxy`. Uses `optJSONObject` / `optJSONArray` so older configs without an `accounts` block degrade gracefully (no auth → behaves as before). **iOS (`client/platforms/ios/PacketTunnelProvider+Xray.swift`)** - Reads the same `inbounds[0].settings.accounts[0]` from the JSON config and threads `username` / `password` into `setupAndRunTun2socks(...)`. The hev-socks5-tunnel YAML config now includes `username:` / `password:` lines under `socks5:` when credentials are present. ## Security properties - Credentials are generated by a CSPRNG (`QRandomGenerator::system()`, which maps to `/dev/urandom` on Linux/Android, `SecRandomCopyBytes` on Apple, `BCryptGenRandom` on Windows). - They live only in process memory, are never written to disk, logs, or notifications, and are never serialized into shareable Xray config exports. - They are re-rolled on every process start, so a VPN reconnect after an app kill yields a fresh pair. - After the patch, an external app connecting to `127.0.0.1:10808` receives a SOCKS5 handshake that requires method `0x02` (username/password) and cannot proceed without the secret. The per-app split-tunneling bypass from #2452 is closed. ## What this PR does *not* do It does not hide the fact that *something* is listening on `127.0.0.1:10808`. An attacker app can still probe the port and infer "AmneziaVPN with Xray is active". Fully addressing the detectability concern from the second half of #2452 would require moving the IPC off loopback TCP entirely (e.g. to a Unix domain socket inside the app's private files dir), which in turn requires adding Unix socket support upstream in `hev-socks5-tunnel`. That should be tracked as a follow-up; this PR is the minimal fix that closes the bypass without new dependencies. ## Testing Locally on Linux desktop with Qt 6.6.2: - Full client builds cleanly (`235/235`, both `AmneziaVPN` and `AmneziaVPN-service` link successfully). - A small standalone test calling `GenerateInboundEntry()` and `GetInboundCredentials()` confirms that the JSON contains `auth: "password"` and a populated `accounts` array, that the credentials returned by `GetInboundCredentials()` match the ones inside the JSON, and that they differ between process invocations. The Android, iOS, Windows and macOS changes are mechanical and rely on this repository's CI for build verification. Reviewers familiar with each platform should sanity-check the wiring; I'm happy to iterate on anything that looks off. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-14 21:05:28 +00:00
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#2813
No description provided.