mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-04 13:12:07 +03:00
ed31ee432c
The node gate assumed a node-assigned sidecar row would never converge,
because the master's reconcile loops only read node_id IS NULL rows. But
a pushed row is local on the node's own panel, whose AmneziaWG, TUIC and
mtg loops run it like any other; node-adopted rows of these protocols
already worked. Only creating or cloning them from the master was blocked.
Open the three protocols on both lists and fix what assumed the master's
host for a node row:
- A node older than the release that introduced the protocol (MTProto
v3.5.0, AmneziaWG v3.7.0, TUIC v3.8.0) would hand it to Xray as-is, so
add and protocol-changing update refuse it, and a node that has not
reported its version yet. Dev builds ("dev+<sha>") track main and pass.
- MTProto's routeXrayPort is a loopback port on the host running mtg.
The master no longer allocates one, nor forwards a cloned source's, for
a node row; the node allocates its own and node sync adopts it back.
- AmneziaWG forwardedPorts were checked against the master's inbounds,
web port and id-derived relay ports. A node row is now checked against
its own node's inbounds; the node re-checks what only it knows.
UpdateInbound restores the stored nodeId before that check.
Verified on a docker master+node pair: AWG, routed MTProto and TUIC
created and cloned from the master start on the node (interface, mtg,
tuic-server); an AWG client added and an MTProto client edited on the
master apply on the node; the node-chosen egress port survives edits.
Closes #6306
91 lines
4.7 KiB
Go
91 lines
4.7 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// A representative vlessenc/ML-KEM encryption value as produced by `xray
|
|
// vlessenc` — a dotted string, never the literal "vlessenc".
|
|
const vlessEncValue = "mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y"
|
|
|
|
func TestInboundCanEnableTlsFlow(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
protocol string
|
|
streamSettings string
|
|
settings string
|
|
want bool
|
|
}{
|
|
{"vless tcp tls", string(model.VLESS), `{"network":"tcp","security":"tls"}`, "", true},
|
|
{"vless tcp reality", string(model.VLESS), `{"network":"tcp","security":"reality"}`, "", true},
|
|
{"vless tcp none no enc", string(model.VLESS), `{"network":"tcp","security":"none"}`, "", false},
|
|
{"vless ws tls", string(model.VLESS), `{"network":"ws","security":"tls"}`, "", false},
|
|
{"vless grpc reality", string(model.VLESS), `{"network":"grpc","security":"reality"}`, "", false},
|
|
{"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false},
|
|
{"empty stream", string(model.VLESS), "", "", false},
|
|
|
|
// vlessenc is gated to XHTTP only. TCP without tls/reality is NOT
|
|
// Vision-capable even with vlessenc set — the combination only works on
|
|
// XHTTP in practice.
|
|
{"vless tcp vlessenc not capable", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, false},
|
|
// ws is a framed transport — vlessenc never enables Vision there.
|
|
{"vless ws vlessenc still off", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false},
|
|
|
|
// XHTTP + VLESS encryption (the #5157 case).
|
|
{"vless xhttp vlessenc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true},
|
|
{"vless xhttp encryption none", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, false},
|
|
{"vless xhttp no settings", string(model.VLESS), `{"network":"xhttp","security":"none"}`, "", false},
|
|
// Regression for PR #5185: the gate is "any non-none encryption", NOT an
|
|
// equality check against the literal "vlessenc" (which the buggy PR used
|
|
// and which never matches a real, generated encryption value). An x25519
|
|
// auth value must enable it just like the ML-KEM value above.
|
|
{"vless xhttp x25519 enc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"native.0rtt.121s-180s.xRMUYYjQctqYO1pSyffM-w"}`, true},
|
|
// Server-side configs (API/JSON) may carry only decryption; that alone
|
|
// must also enable the flow gate.
|
|
{"vless xhttp decryption only", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"decryption":"` + vlessEncValue + `","encryption":"none"}`, true},
|
|
// XHTTP without encryption stays off even with tls (Vision over XHTTP is
|
|
// gated on vlessenc, not transport security).
|
|
{"vless xhttp tls no encryption", string(model.VLESS), `{"network":"xhttp","security":"tls"}`, `{"encryption":"none"}`, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := inboundCanEnableTlsFlow(tc.protocol, tc.streamSettings, tc.settings)
|
|
if got != tc.want {
|
|
t.Errorf("inboundCanEnableTlsFlow(%q, %q, %q) = %v, want %v",
|
|
tc.protocol, tc.streamSettings, tc.settings, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Fallbacks must remain raw-TCP-only and must NOT follow the broadened flow gate
|
|
// onto XHTTP+vlessenc.
|
|
func TestInboundCanHostFallbacks_StaysTcpOnly(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
protocol model.Protocol
|
|
streamSettings string
|
|
settings string
|
|
want bool
|
|
}{
|
|
{"vless tcp tls", model.VLESS, `{"network":"tcp","security":"tls"}`, "", true},
|
|
{"trojan tcp reality", model.Trojan, `{"network":"tcp","security":"reality"}`, "", true},
|
|
{"vless xhttp vlessenc not fallback-capable", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false},
|
|
{"vmess tcp tls not fallback-capable", model.VMESS, `{"network":"tcp","security":"tls"}`, "", false},
|
|
{"nil-ish empty stream", model.VLESS, "", "", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings, Settings: tc.settings}
|
|
if got := inboundCanHostFallbacks(ib); got != tc.want {
|
|
t.Errorf("inboundCanHostFallbacks = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
if inboundCanHostFallbacks(nil) {
|
|
t.Errorf("inboundCanHostFallbacks(nil) = true, want false")
|
|
}
|
|
}
|