From b0d9fe156bf7f6e01dbfc3e15aefcbe9b38eede0 Mon Sep 17 00:00:00 2001 From: reza Date: Mon, 18 May 2026 14:56:41 +0000 Subject: [PATCH] feat(socks): add IsSocksLike helper, info-modal display, and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second-pass on the SOCKS5 inbound scaffold (PR #4452). This commit ticks off two of the 'help wanted' items from the scaffold's TODO list and tightens the existing dispatcher so that adding a new protocol-without-link in the future is a one-line change instead of an audit through every switch. Backend (Go) ------------ * database/model/model.go: new IsSocksLike(p) helper that returns true for both 'socks' and 'mixed'. Mirrors the existing IsHysteria pattern (one helper, two underlying constants) so call sites don't have to re-list both protocols every time they need to treat 'this inbound speaks SOCKS5' uniformly. * database/model/model_test.go: - TestSocksProtocolConstant pins the wire value 'socks' so a future refactor can't silently rename it (which would orphan every stored inbound row). - TestIsSocksLike covers Socks, Mixed, every other declared protocol, the empty Protocol, and a wrong-case input. * sub/subService.go GetLink: - Replace bare string literals ('vmess', 'vless', …) with the typed model.* constants so a typo can't silently fall through. - Add an explicit link-less case for Socks/Mixed/HTTP/Tunnel/WireGuard with a comment explaining why we don't emit 'socks://…' URLs (follow-up #1 in the scaffold PR description). Frontend (JS/Vue) ----------------- * frontend/src/models/dbinbound.js: add 'isSocks' getter (pure SOCKS5 inbound) and 'isSocksLike' getter (Socks OR Mixed), matching the pattern used by isMixed/isHTTP/isWireguard above. * frontend/src/pages/inbounds/InboundInfoModal.vue: the existing 'Mixed' info block (auth/UDP/IP/accounts) now also renders for the new SOCKS protocol via isSocksLike, since Xray's mixed and socks inbounds accept the exact same settings keys. Without this, opening the info modal for a SOCKS inbound would show an empty body. Header comment updated to list SOCKS alongside Mixed/HTTP/Tunnel. Still outstanding from the scaffold's TODO list: - Xray runtime AddUser hooks (web/service/inbound.go) - Translations for the 'socks' label across all 13 locales - Routing UI protocol == socks helper --- database/model/model.go | 12 ++++++ database/model/model_test.go | 39 +++++++++++++++++++ frontend/src/models/dbinbound.js | 14 +++++++ .../src/pages/inbounds/InboundInfoModal.vue | 7 ++-- sub/subService.go | 19 ++++++--- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 88d1548c..0e0eb714 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -37,6 +37,18 @@ func IsHysteria(p Protocol) bool { return p == Hysteria || p == Hysteria2 } +// IsSocksLike returns true for both the dedicated "socks" inbound and +// the combined "mixed" inbound, since Mixed exposes SOCKS5 alongside +// HTTP on the same port and accepts the exact same settings shape +// (auth/accounts/udp/ip) that the pure Socks inbound does. +// +// Use this helper anywhere routing, sub-link generation, or UI code +// needs to treat "this inbound speaks SOCKS5" uniformly without +// re-listing both constants at every call site. +func IsSocksLike(p Protocol) bool { + return p == Socks || p == Mixed +} + // User represents a user account in the 3x-ui panel. type User struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` diff --git a/database/model/model_test.go b/database/model/model_test.go index d98c5157..aa7b6b40 100644 --- a/database/model/model_test.go +++ b/database/model/model_test.go @@ -20,3 +20,42 @@ func TestIsHysteria(t *testing.T) { } } } + +// TestSocksProtocolConstant pins the wire value of the SOCKS5 protocol +// constant. It must stay "socks" because that's the literal Xray expects +// in inbound.protocol JSON (see https://xtls.github.io/config/inbounds/socks.html); +// changing it would silently break every stored inbound row. +func TestSocksProtocolConstant(t *testing.T) { + if got, want := string(Socks), "socks"; got != want { + t.Errorf("Socks protocol constant = %q, want %q", got, want) + } + if Socks == Mixed { + t.Error("Socks and Mixed must be distinct protocols") + } +} + +func TestIsSocksLike(t *testing.T) { + cases := []struct { + in Protocol + want bool + }{ + {Socks, true}, + {Mixed, true}, + {HTTP, false}, + {VLESS, false}, + {VMESS, false}, + {Trojan, false}, + {Shadowsocks, false}, + {WireGuard, false}, + {Hysteria, false}, + {Hysteria2, false}, + {Tunnel, false}, + {Protocol(""), false}, + {Protocol("SOCKS"), false}, // case-sensitive: must match the stored lowercase value + } + for _, c := range cases { + if got := IsSocksLike(c.in); got != c.want { + t.Errorf("IsSocksLike(%q) = %v, want %v", c.in, got, c.want) + } + } +} diff --git a/frontend/src/models/dbinbound.js b/frontend/src/models/dbinbound.js index d7a9483e..71f2c197 100644 --- a/frontend/src/models/dbinbound.js +++ b/frontend/src/models/dbinbound.js @@ -62,6 +62,20 @@ export class DBInbound { return this.protocol === Protocols.MIXED; } + // Pure SOCKS5 inbound (Xray "socks" protocol, RFC 1928). Distinct + // from `isMixed`, which is HTTP+SOCKS on the same port. Use + // `isSocksLike` when you want either one. + get isSocks() { + return this.protocol === Protocols.SOCKS; + } + + // True for both the dedicated SOCKS5 inbound and the combined + // Mixed inbound, since both speak SOCKS5 and share the same + // settings shape (auth/accounts/udp/ip). + get isSocksLike() { + return this.protocol === Protocols.SOCKS || this.protocol === Protocols.MIXED; + } + get isHTTP() { return this.protocol === Protocols.HTTP; } diff --git a/frontend/src/pages/inbounds/InboundInfoModal.vue b/frontend/src/pages/inbounds/InboundInfoModal.vue index 61ce2fcf..4a2b4bd6 100644 --- a/frontend/src/pages/inbounds/InboundInfoModal.vue +++ b/frontend/src/pages/inbounds/InboundInfoModal.vue @@ -26,7 +26,7 @@ const { datepicker } = useDatepicker(); // client row + share links // • SS single-user → connection details + share link // • WireGuard → secret/peers + per-peer config download -// • Mixed/HTTP/Tunnel → connection details only +// • Mixed/SOCKS/HTTP/Tunnel → connection details only // // We display links via QrPanel — each link gets its own QR + copy + // (for WireGuard configs) download button. @@ -640,8 +640,9 @@ const showSubscriptionTab = computed( - -
+ +
Auth
diff --git a/sub/subService.go b/sub/subService.go index d769bf5a..d6db594c 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -209,18 +209,27 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri // pair. Returns "" when the inbound's protocol doesn't produce a subscription URL // (socks, http, mixed, wireguard, dokodemo, tunnel). The returned string may // contain multiple `\n`-separated URLs when the inbound has externalProxy set. +// +// SOCKS5 and Mixed are deliberately link-less here. A `socks://user:pass@host:port` +// form exists in some client ecosystems but is not standardised across the +// xray/v2ray clients we target, and emitting one would mislead users into +// pasting it into clients that silently ignore it. Tracked as follow-up #1 +// in the SOCKS5 scaffold PR description. func (s *SubService) GetLink(inbound *model.Inbound, email string) string { switch inbound.Protocol { - case "vmess": + case model.VMESS: return s.genVmessLink(inbound, email) - case "vless": + case model.VLESS: return s.genVlessLink(inbound, email) - case "trojan": + case model.Trojan: return s.genTrojanLink(inbound, email) - case "shadowsocks": + case model.Shadowsocks: return s.genShadowsocksLink(inbound, email) - case "hysteria", "hysteria2": + case model.Hysteria, model.Hysteria2: return s.genHysteriaLink(inbound, email) + case model.Socks, model.Mixed, model.HTTP, model.Tunnel, model.WireGuard: + // Tunnel-style or proxy-listener protocols: no per-client URL. + return "" } return "" }