3x-ui/database/model/model_test.go

104 lines
2.9 KiB
Go
Raw Normal View History

package model
import "testing"
func TestIsHysteria(t *testing.T) {
cases := []struct {
in Protocol
want bool
}{
{Hysteria, true},
{Hysteria2, true},
{VLESS, false},
{Shadowsocks, false},
{Protocol(""), false},
{Protocol("hysteria3"), false},
}
for _, c := range cases {
if got := IsHysteria(c.in); got != c.want {
t.Errorf("IsHysteria(%q) = %v, want %v", c.in, got, c.want)
}
}
}
feat(socks): add IsSocksLike helper, info-modal display, and tests 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
2026-05-18 14:56:41 +00:00
// 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)
}
}
}
feat(socks): guard client-lifecycle paths against account-based inbounds 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.
2026-05-25 15:55:34 +00:00
// TestIsAccountBased pins the set of inbounds that route credentials
// through settings.accounts[] rather than settings.clients[]. Anything
// that returns true here is OFF-LIMITS for the client-lifecycle code
// paths (AddInboundClient / UpdateInboundClient / DelInboundClient,
// depletion, traffic reset, telegram 'add client' keyboard).
//
// HTTP is intentionally included even though the panel UI doesn't
// currently surface it as a standalone protocol — the runtime API
// AddUser branch in xray/api.go handles it symmetrically with socks,
// and any future UI work plugging HTTP back in inherits the same
// safety net for free.
//
// WireGuard is intentionally EXCLUDED: its peers live under
// settings.peers[] and are managed through a separate path; treating
// it as account-based would lock out the existing wireguard UI.
func TestIsAccountBased(t *testing.T) {
cases := []struct {
in Protocol
want bool
}{
{Socks, true},
{Mixed, true},
{HTTP, true},
{VLESS, false},
{VMESS, false},
{Trojan, false},
{Shadowsocks, false},
{WireGuard, false}, // peers, not accounts; managed separately
{Hysteria, false},
{Hysteria2, false},
{Tunnel, false},
{Protocol(""), false},
{Protocol("SOCKS"), false}, // case-sensitive lowercase invariant
}
for _, c := range cases {
if got := IsAccountBased(c.in); got != c.want {
t.Errorf("IsAccountBased(%q) = %v, want %v", c.in, got, c.want)
}
}
}