feat(inbound): scaffold dedicated SOCKS5 inbound protocol

Adds an initial, intentionally minimal scaffold for a dedicated SOCKS5
inbound (Xray 'socks' protocol, RFC 1928). The existing 'mixed' inbound
already supports SOCKS+HTTP on the same port, but several deployments
need a pure SOCKS5 inbound — for example chained outbounds and clients
that don't tolerate HTTP CONNECT on the same listener.

This PR is purposely a scaffold so other contributors can finish the
full integration (sub link generation, routing UI helpers, Xray runtime
AddUser hooks, translations, docs, tests). It does NOT claim feature
completeness.

What's included
---------------
Backend (Go):
  * database/model/model.go: add Socks model.Protocol constant ('socks')
    next to Mixed, with a doc comment explaining the difference vs Mixed.

Frontend (JS/Vue):
  * frontend/src/models/inbound.js
      - Add Protocols.SOCKS ('socks') to the protocol enum, so the
        existing 'Object.values(Protocols)' selector picks it up
        automatically in the inbound create/edit modal.
      - New Inbound.SocksSettings class (auth, accounts, udp, ip)
        that serialises in the exact shape Xray's socks inbound expects
        (https://xtls.github.io/config/inbounds/socks.html).
      - New Inbound.SocksSettings.SocksAccount for password-mode users.
      - Wire SOCKS into Inbound.Settings.getSettings() and fromJson()
        dispatchers so create/edit/restore round-trips work.

  * frontend/src/pages/inbounds/InboundFormModal.vue
      - Extend the existing 'HTTP / Mixed accounts' form so SOCKS shares
        the same accounts editor + auth/UDP/UDP-IP fields. SOCKS reuses
        the MIXED shape because Xray's socks and mixed inbounds accept
        the same settings keys.
      - New addAccountByProtocol() helper dispatches to the correct
        Account class per protocol (Http / Mixed / Socks) so each
        protocol stores accounts in the right namespace.

What's intentionally NOT done (help wanted)
-------------------------------------------
1. sub/subService.go GetLink: SOCKS currently returns '' (same as
   MIXED/HTTP/Tunnel). If we want a 'socks://' subscription link, that
   needs its own gen* function. The existing comment on GetLink already
   lists socks/http/mixed as link-less, so this is consistent for now.
2. Xray runtime AddUser / UpdateInbound hooks in web/service/inbound.go
   — accounts are persisted in settings JSON and Xray picks them up on
   restart, but live add-user without restart is not wired.
3. Routing UI: routing pages already accept any inbound tag, so SOCKS
   inbounds are routable as-is; a dedicated 'protocol == socks' helper
   in routing rule editors would be a nice follow-up.
4. Translations (web/translation/*.json): the protocol name is rendered
   as the raw string 'socks' in the dropdown today; we don't yet have
   a localised label.
5. Tests: xray/process_test.go has no SOCKS-specific case yet.

Why scaffold instead of full feature?
-------------------------------------
The protocol surface area touches db model, settings serialisation,
form UI, sub link generation, routing rules, and translations for 13
locales. Landing a vetted scaffold first lets multiple contributors
parallelise the remaining work without merge churn on a giant PR.

Refs
----
- Xray socks inbound spec: https://xtls.github.io/config/inbounds/socks.html
- RFC 1928 (SOCKS Protocol Version 5)

Co-developed-by: 3x-ui community (call for contributions)
This commit is contained in:
reza 2026-05-18 08:07:17 +00:00
parent f9ae0347c6
commit 4f8b0b90c8
3 changed files with 118 additions and 10 deletions

View file

@ -20,9 +20,14 @@ const (
Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks"
Mixed Protocol = "mixed"
WireGuard Protocol = "wireguard"
Hysteria Protocol = "hysteria"
Hysteria2 Protocol = "hysteria2"
// Socks is a dedicated SOCKS5 inbound (Xray "socks" protocol).
// Unlike Mixed (HTTP+SOCKS), this is a pure SOCKS5 inbound and is
// intended for tunneling clients that only speak SOCKS5.
// See: https://xtls.github.io/config/inbounds/socks.html
Socks Protocol = "socks"
WireGuard Protocol = "wireguard"
Hysteria Protocol = "hysteria"
Hysteria2 Protocol = "hysteria2"
)
// IsHysteria returns true for both "hysteria" and "hysteria2".

View file

@ -10,6 +10,9 @@ export const Protocols = {
WIREGUARD: 'wireguard',
HYSTERIA: 'hysteria',
MIXED: 'mixed',
// Dedicated SOCKS5 inbound. Use MIXED if you also need HTTP support
// on the same port; SOCKS is pure SOCKS5 (RFC 1928) only.
SOCKS: 'socks',
HTTP: 'http',
TUNNEL: 'tunnel',
TUN: 'tun',
@ -2448,6 +2451,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
@ -2464,6 +2468,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
@ -3074,6 +3079,85 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
}
};
// Dedicated SOCKS5 inbound settings.
//
// This mirrors Xray's `socks` inbound (RFC 1928), see:
// https://xtls.github.io/config/inbounds/socks.html
//
// Unlike MixedSettings (which exposes HTTP+SOCKS on the same port),
// SocksSettings configures a pure SOCKS5 inbound. The accepted fields
// are the same shape Xray expects:
// - auth: "noauth" | "password"
// - accounts: [{ user, pass }] (only used when auth === "password")
// - udp: bool (enables UDP ASSOCIATE)
// - ip: string (address sent to client for UDP replies,
// defaults to 127.0.0.1)
//
// NOTE: SOCKS inbounds are tunnel-style and do not produce a subscription
// link (mirrors how MIXED/HTTP are treated in sub/subService.go::GetLink).
Inbound.SocksSettings = class extends Inbound.Settings {
constructor(
protocol,
auth = 'password',
accounts = [new Inbound.SocksSettings.SocksAccount()],
udp = false,
ip = '127.0.0.1',
) {
super(protocol);
this.auth = auth;
this.accounts = accounts;
this.udp = udp;
this.ip = ip;
}
addAccount(account) {
this.accounts.push(account);
}
delAccount(index) {
this.accounts.splice(index, 1);
}
static fromJson(json = {}) {
let accounts;
if (json.auth === 'password') {
accounts = (json.accounts || []).map(
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
);
}
return new Inbound.SocksSettings(
Protocols.SOCKS,
json.auth,
accounts,
json.udp,
json.ip,
);
}
toJson() {
return {
auth: this.auth,
accounts: this.auth === 'password'
? this.accounts.map(account => account.toJson())
: undefined,
udp: this.udp,
ip: this.ip,
};
}
};
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super();
this.user = user;
this.pass = pass;
}
static fromJson(json = {}) {
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
}
};
Inbound.HttpSettings = class extends Inbound.Settings {
constructor(
protocol,

View file

@ -174,6 +174,26 @@ function delFallback(idx) {
inbound.value?.settings?.delFallback?.(idx);
}
// Helper for the shared HTTP / MIXED / SOCKS accounts editor: dispatches
// to the right Account class so each protocol stores accounts in the
// shape Xray expects on the wire.
function addAccountByProtocol() {
const settings = inbound.value?.settings;
if (!settings) return;
switch (inbound.value.protocol) {
case Protocols.HTTP:
settings.addAccount(new Inbound.HttpSettings.HttpAccount());
break;
case Protocols.SOCKS:
settings.addAccount(new Inbound.SocksSettings.SocksAccount());
break;
case Protocols.MIXED:
default:
settings.addAccount(new Inbound.MixedSettings.SocksAccount());
break;
}
}
// Date / GB bridges (legacy used moment via _expiryTime; we go direct).
const expiryDate = computed({
get: () => (dbForm.value?.expiryTime > 0 ? dayjs(dbForm.value.expiryTime) : null),
@ -964,13 +984,12 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
</a-form-item>
</a-form>
<!-- HTTP / Mixed accounts -->
<a-form v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED" :colon="false"
:label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
<!-- HTTP / Mixed / SOCKS accounts -->
<a-form
v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED || protocol === Protocols.SOCKS"
:colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }" class="mt-12">
<a-form-item label="Accounts">
<a-button size="small" @click="protocol === Protocols.HTTP
? inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())
: inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())">
<a-button size="small" @click="addAccountByProtocol()">
<template #icon>
<PlusOutlined />
</template>
@ -993,7 +1012,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
<a-form-item v-if="protocol === Protocols.HTTP" label="Allow transparent">
<a-switch v-model:checked="inbound.settings.allowTransparent" />
</a-form-item>
<template v-if="protocol === Protocols.MIXED">
<template v-if="protocol === Protocols.MIXED || protocol === Protocols.SOCKS">
<a-form-item label="Auth">
<a-select v-model:value="inbound.settings.auth">
<a-select-option value="noauth">noauth</a-select-option>