From 4f8b0b90c83eab4af64c837166cdc49bfc38e8ea Mon Sep 17 00:00:00 2001 From: reza Date: Mon, 18 May 2026 08:07:17 +0000 Subject: [PATCH] feat(inbound): scaffold dedicated SOCKS5 inbound protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- database/model/model.go | 11 ++- frontend/src/models/inbound.js | 84 +++++++++++++++++++ .../src/pages/inbounds/InboundFormModal.vue | 33 ++++++-- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index d71e0589..88d1548c 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -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". diff --git a/frontend/src/models/inbound.js b/frontend/src/models/inbound.js index 830dc9a9..a205bd1c 100644 --- a/frontend/src/models/inbound.js +++ b/frontend/src/models/inbound.js @@ -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, diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index 8d2019de..a68b38eb 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -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')); - - + + - + @@ -993,7 +1012,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream')); -