Account-based inbounds (Socks/Mixed/HTTP) keep their credentials in
`settings.accounts[]` — an array of plain {user, pass} objects — while
every other inbound (vless/vmess/trojan/shadowsocks/hysteria/…) keeps
them in `settings.clients[]`, the rich Client struct with id, email,
sub-id, totalGB, expiry, traffic-reset cadence, etc.
The whole client lifecycle on InboundService (AddInboundClient,
UpdateInboundClient, DelInboundClient, CopyInboundClients) was written
against the latter shape, and several of those methods do an unchecked
`settings["clients"].([]any)` cast on the way in. If anything ever
managed to call them against a SOCKS5 inbound the panel would panic
straight out of the goroutine.
In practice the UI itself can't get there — `dbinbound.isMultiUser()`
returns false for SOCKS, which already gates the ClientRowTable,
"add client" menu, copy-clients menu, etc. — but the HTTP API is
addressable directly, the Telegram bot path is independent, and a
future feature could easily plug into one of those entry points and
hit the cast. Defense in depth is cheap here.
Backend
-------
* Add `model.IsAccountBased(p Protocol) bool` covering Socks, Mixed
and HTTP. WireGuard is *not* in the set — its peers live under
`settings.peers[]` and are managed through a separate code path
that already knows about them.
* AddInboundClient / UpdateInboundClient / DelInboundClient now load
the target inbound up front and bail out with a clear, actionable
error when the protocol is account-based, instead of falling into
the unchecked clients cast. The error message points the caller at
the right escape hatch ("update the inbound directly with
settings.accounts[] instead").
* CopyInboundClients refuses account-based inbounds on either side
of the copy — neither direction has well-defined semantics
(downcasting a rich client to {user, pass} silently drops
sub-id/totalGB/expiry; upcasting the other way invents fields the
runtime can't honor).
Tests
-----
* TestIsAccountBased pins the protocol set, including the explicit
WireGuard-excluded and lowercase-invariant cases.
* TestAddInboundClient_RejectsSocks, TestUpdateInboundClient_RejectsSocks,
TestDelInboundClient_RejectsSocks: the three guards must fire on a
SOCKS inbound seeded with a realistic settings.accounts[] payload.
* TestCopyInboundClients_RejectsSocksSource and ...Target: both
directions are refused.
* TestAddInboundClient_AllowsVless: sanity check that the guard does
not fire on a client-based protocol — if this ever flipped the
feature would be broken for everyone, not just SOCKS users.
Other scenarios reviewed (no code changes needed):
* Routing rules — keyed off inbound tag, protocol-agnostic.
* Balancers — outbound-tag based, untouched by inbound protocol.
* Outbound side — frontend already exposes SOCKS as an outbound
with user/pass through the existing OutboundFormModal.
* Depletion / traffic reset / disable-invalid-clients — driven by
SQL queries on the client_traffics table, which is naturally empty
for account-based inbounds (they never create rows there).
* SetInboundEnable — operates on the inbound table directly, no
per-client surgery, safe for SOCKS.
* Sub-link generators (sub/subService, subJsonService, subClashService)
— already return empty for SOCKS/Mixed/HTTP/Tunnel/WireGuard.
* Frontend client modals (ClientFormModal, ClientRowTable,
ClientBulkModal, CopyClientsModal) — gated upstream by
`dbInbound.isMultiUser()`, which is false for SOCKS.
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
UI stores v1 and v2 both as "hysteria" with settings.version, but
inbounds that came in from imports / manual SQL can carry the
literal "hysteria2" string and get silently dropped everywhere we
switch on protocol.
Add Hysteria2 constant + IsHysteria helper, use it in the places
that gate on protocol (sub SQL, getLink, genHysteriaLink, clash
buildProxy, json gen, inbound.go validation, xray AddUser).
Existing "hysteria" inbounds are untouched.
closes#4081