Files
3x-ui/internal/web/service/inbound_protocol.go
T
MHSanaei c5e1e54c75 fix(xray): keep XDRIVE storage and Vision flow within what xray-core runs
xray-core opens an XDRIVE stream's storage only when the inbound starts
listening, so a Google Drive inbound saved with the form's defaults (no
folder, no secrets), a local one without a folder, or a template one
whose template xray cannot use stopped xray and every other inbound with
it. validateXDriveStorage applies the core's storage rules at save, on
inbound add/update and on the xray template's inbounds and outbounds:
the service must be local, Google Drive or template; local and Google
Drive need a remoteFolder; Google Drive needs its three non-empty
secrets; a template must pass newTemplateStorage's checks (put, get,
list and delete urls, and a list namesRegex with one capture group).

A VLESS client kept its xtls-rprx-vision flow when its inbound moved to
a transport that cannot carry it, and GetXrayConfig only honoured
DisableFlow, so the server demanded Vision there and refused every
client. GetXrayConfig and the outbound form now apply the Vision gate,
and that gate now follows the core in the backend, the subscription
service and the frontend: TLS/REALITY on raw TCP, or VLESS encryption on
any transport (xray runs Vision inside encryption.CommonConn whatever
the transport, not only on XHTTP). The outbound modal passes the VLESS
encryption into the gate and gates Mux on the effective flow.
2026-10-12 00:49:11 +02:00

161 lines
5.6 KiB
Go

package service
import (
"encoding/json"
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/version"
"gorm.io/gorm"
)
// inboundShadowsocksMethod extracts settings.method for Shadowsocks inbounds so
// the client UI can generate a valid PSK (base64 of the method's key length)
// for Shadowsocks 2022 ciphers. Returns "" for non-Shadowsocks inbounds.
func inboundShadowsocksMethod(protocol, settings string) string {
if protocol != string(model.Shadowsocks) || settings == "" {
return ""
}
var s struct {
Method string `json:"method"`
}
if err := json.Unmarshal([]byte(settings), &s); err != nil {
return ""
}
return s.Method
}
// inboundCanEnableTlsFlow mirrors the frontend's canEnableTlsFlow: xray-core runs Vision over
// TLS/REALITY on raw TCP, or inside VLESS encryption (encryption.CommonConn) on any transport.
func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool {
if protocol != string(model.VLESS) {
return false
}
var stream struct {
Network string `json:"network"`
Security string `json:"security"`
}
if json.Unmarshal([]byte(streamSettings), &stream) == nil && stream.Network == "tcp" &&
(stream.Security == "tls" || stream.Security == "reality") {
return true
}
return vlessEncryptionEnabled(settings)
}
// nodeEligibleProtocols mirrors the frontend's NODE_ELIGIBLE_PROTOCOLS. A sidecar
// protocol's row is local on the node it is pushed to, so that panel runs it.
var nodeEligibleProtocols = map[model.Protocol]bool{
model.VLESS: true,
model.VMESS: true,
model.Trojan: true,
model.Shadowsocks: true,
model.Hysteria: true,
model.WireGuard: true,
model.MTProto: true,
model.AmneziaWG: true,
model.TUIC: true,
model.MASQUE: true,
}
// nodeProtocolFirstRelease is the panel release that introduced each protocol
// newer than node support itself; an older node would hand it to Xray as-is.
var nodeProtocolFirstRelease = map[model.Protocol]string{
model.MTProto: "v3.5.0",
model.AmneziaWG: "v3.7.0",
model.TUIC: "v3.8.0",
model.MASQUE: "v3.9.1",
}
// checkNodeCanHostProtocol refuses assigning protocol to nodeID unless the
// protocol may live on a node and that node's panel is new enough to run it.
func checkNodeCanHostProtocol(db *gorm.DB, nodeID int, protocol model.Protocol) error {
if !nodeEligibleProtocols[protocol] {
return common.NewErrorf("%s inbounds cannot be assigned to a node", protocol)
}
firstRelease, ok := nodeProtocolFirstRelease[protocol]
if !ok {
return nil
}
var node model.Node
if err := db.Select("id", "name", "panel_version").First(&node, nodeID).Error; err != nil {
return err
}
if strings.TrimSpace(node.PanelVersion) == "" {
return common.NewErrorf("node %q has not reported its panel version yet; %s inbounds need %s or newer",
node.Name, protocol, firstRelease)
}
// A dev build reports "dev+<sha>": it tracks main, which carries every protocol.
if cmp, ok := version.Compare(node.PanelVersion, firstRelease); ok && cmp < 0 {
return common.NewErrorf("node %q runs panel %s; %s inbounds need %s or newer",
node.Name, node.PanelVersion, protocol, firstRelease)
}
return nil
}
// vlessEncryptionEnabled reports whether a VLESS inbound has VLESS-level
// encryption (vlessenc / ML-KEM) configured. When enabled these fields hold a
// generated dotted string (e.g. "mlkem768x25519plus.native.0rtt.<key>"); "none"
// or empty means off. The value is never the literal "vlessenc" — that is the
// name of the `xray vlessenc` CLI subcommand, not a stored value.
//
// Both fields are checked: decryption is the authoritative server-side value
// xray-core reads, while encryption is stored by the panel for link generation.
// The ML-KEM/X25519 buttons set both, but accepting either keeps the gate
// working for inbounds configured via the API or raw JSON.
func vlessEncryptionEnabled(settings string) bool {
if settings == "" {
return false
}
var s struct {
Encryption string `json:"encryption"`
Decryption string `json:"decryption"`
}
if err := json.Unmarshal([]byte(settings), &s); err != nil {
return false
}
return vlessEncValueSet(s.Encryption) || vlessEncValueSet(s.Decryption)
}
// vlessEncValueSet reports whether a VLESS encryption/decryption field holds a
// real (generated) value rather than the "none"/empty sentinel.
func vlessEncValueSet(v string) bool {
return v != "" && v != "none"
}
// inboundCanHostFallbacks gates the settings.fallbacks injection.
// Xray only honors fallbacks on VLESS and Trojan inbounds carried over
// TCP transport with TLS or Reality security. This is intentionally stricter
// than inboundCanEnableTlsFlow (which also accepts any vlessenc stream): fallbacks
// are a raw-TCP-only feature.
func inboundCanHostFallbacks(ib *model.Inbound) bool {
if ib == nil {
return false
}
if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan {
return false
}
return streamSupportsFallbacks(ib.StreamSettings)
}
// streamSupportsFallbacks reports whether the stream is raw TCP carried over
// TLS or REALITY — the only transport Xray honors inbound fallbacks on (and the
// classic requirement for XTLS Vision before vlessenc).
func streamSupportsFallbacks(streamSettings string) bool {
if streamSettings == "" {
return false
}
var stream struct {
Network string `json:"network"`
Security string `json:"security"`
}
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return false
}
if stream.Network != "tcp" {
return false
}
return stream.Security == "tls" || stream.Security == "reality"
}