3x-ui/database/model/model.go
reza 4f8b0b90c8 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)
2026-05-18 08:07:17 +00:00

198 lines
10 KiB
Go

// Package model defines the database models and data structures used by the 3x-ui panel.
package model
import (
"fmt"
"github.com/mhsanaei/3x-ui/v3/util/json_util"
"github.com/mhsanaei/3x-ui/v3/xray"
)
// Protocol represents the protocol type for Xray inbounds.
type Protocol string
// Protocol constants for different Xray inbound protocols
const (
VMESS Protocol = "vmess"
VLESS Protocol = "vless"
Tunnel Protocol = "tunnel"
HTTP Protocol = "http"
Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks"
Mixed Protocol = "mixed"
// 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".
// Use instead of a bare ==model.Hysteria check: a v2 inbound stored
// with the literal v2 string would otherwise fall through (#4081).
func IsHysteria(p Protocol) bool {
return p == Hysteria || p == Hysteria2
}
// User represents a user account in the 3x-ui panel.
type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
LoginEpoch int64 `json:"-" gorm:"default:0"`
}
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
UserId int `json:"-"` // Associated user ID
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
Down int64 `json:"down" form:"down"` // Download traffic in bytes
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
Remark string `json:"remark" form:"remark"` // Human-readable remark
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
// Xray configuration fields
Listen string `json:"listen" form:"listen"`
Port int `json:"port" form:"port"`
Protocol Protocol `json:"protocol" form:"protocol"`
Settings string `json:"settings" form:"settings"`
StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
}
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Up int64 `json:"up" form:"up" gorm:"default:0"`
Down int64 `json:"down" form:"down" gorm:"default:0"`
Total int64 `json:"total" form:"total" gorm:"default:0"`
}
// InboundClientIps stores IP addresses associated with inbound clients for access control.
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"`
}
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
type HistoryOfSeeders struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
SeederName string `json:"seederName"`
}
type ApiToken struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"uniqueIndex;not null"`
Token string `json:"token" gorm:"not null"`
Enabled bool `json:"enabled" gorm:"default:true"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
}
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
// Default to 0.0.0.0 (all interfaces) when listen is empty
// This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0
if listen == "" {
listen = "0.0.0.0"
}
listen = fmt.Sprintf("\"%v\"", listen)
return &xray.InboundConfig{
Listen: json_util.RawMessage(listen),
Port: i.Port,
Protocol: string(i.Protocol),
Settings: json_util.RawMessage(i.Settings),
StreamSettings: json_util.RawMessage(i.StreamSettings),
Tag: i.Tag,
Sniffing: json_util.RawMessage(i.Sniffing),
}
}
// Setting stores key-value configuration settings for the 3x-ui panel.
type Setting struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
}
// Node represents a remote 3x-ui panel registered with the central panel.
// The central panel polls each node's existing /panel/api/server/status
// endpoint over HTTP using the per-node ApiToken to populate the runtime
// status fields below.
type Node struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" form:"name" gorm:"uniqueIndex"`
Remark string `json:"remark" form:"remark"`
Scheme string `json:"scheme" form:"scheme"`
Address string `json:"address" form:"address"`
Port int `json:"port" form:"port"`
BasePath string `json:"basePath" form:"basePath"`
ApiToken string `json:"apiToken" form:"apiToken"`
Enable bool `json:"enable" form:"enable" gorm:"default:true"`
AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
// Heartbeat-updated fields. UpdatedAt advances on every probe even when
// the row is otherwise unchanged so the UI's "last seen" tooltip is
// truthful without us having to read LastHeartbeat separately.
Status string `json:"status" gorm:"default:unknown"` // online|offline|unknown
LastHeartbeat int64 `json:"lastHeartbeat"` // unix seconds, 0 = never
LatencyMs int `json:"latencyMs"`
XrayVersion string `json:"xrayVersion"`
CpuPct float64 `json:"cpuPct"`
MemPct float64 `json:"memPct"`
UptimeSecs uint64 `json:"uptimeSecs"`
LastError string `json:"lastError"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime"`
}
type CustomGeoResource struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias;column:geo_type"`
Alias string `json:"alias" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias"`
Url string `json:"url" gorm:"not null"`
LocalPath string `json:"localPath" gorm:"column:local_path"`
LastUpdatedAt int64 `json:"lastUpdatedAt" gorm:"default:0;column:last_updated_at"`
LastModified string `json:"lastModified" gorm:"column:last_modified"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime;column:created_at"`
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime;column:updated_at"`
}
type ClientReverse struct {
Tag string `json:"tag"`
}
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
type Client struct {
ID string `json:"id,omitempty"` // Unique client identifier
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
Password string `json:"password,omitempty"` // Client password
Flow string `json:"flow,omitempty"` // Flow control (XTLS)
Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
Email string `json:"email"` // Client email identifier
LimitIP int `json:"limitIp"` // IP limit for this client
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
SubID string `json:"subId" form:"subId"` // Subscription identifier
Comment string `json:"comment" form:"comment"` // Client comment
Reset int `json:"reset" form:"reset"` // Reset period in days
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
}