mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
e0d2a0c58c
Turning on Proxy Protocol for a local fallback target behind a VLESS/Trojan TLS master broke the fallback: the row was saved with xver 0, so the master sent no PROXY header and the child, now expecting one, rejected every connection (#6835). A fallback without an explicit dest already takes its address from the child at build time; it now also sends header v1 when the child accepts PROXY protocol (sockopt or the raw/ws/httpupgrade transport flag) and the row sets no xver of its own, so either order of enabling it works, from the panel or the API. Editing a child's listen, port or PROXY flag rebuilds the masters that derive from it, since re-adding only the child left them stale. Fixes #6835.
158 lines
5.9 KiB
Go
158 lines
5.9 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// TestBuildRuntimeInboundForAPI_InjectsFallbacks is the #5963 regression: the
|
|
// runtime inbound sent to nodes never carried the inbound_fallbacks rows, so
|
|
// fallbacks configured on the master silently vanished from every node.
|
|
func TestBuildRuntimeInboundForAPI_InjectsFallbacks(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &InboundService{}
|
|
db := database.GetDB()
|
|
|
|
clients := []model.Client{
|
|
{Email: "fb@x", ID: "11111111-1111-1111-1111-111111111111", Enable: true},
|
|
}
|
|
master := mkInbound(t, 30201, model.VLESS, clientsSettings(t, clients))
|
|
master.StreamSettings = `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"s"}}`
|
|
if err := db.Save(master).Error; err != nil {
|
|
t.Fatalf("save master stream: %v", err)
|
|
}
|
|
|
|
fb := model.InboundFallback{MasterId: master.Id, ChildId: 0, Dest: "8081", Alpn: "h2", Path: "/fb", Xver: 1}
|
|
if err := db.Create(&fb).Error; err != nil {
|
|
t.Fatalf("seed fallback: %v", err)
|
|
}
|
|
|
|
runtimeIb, err := svc.buildInboundForLocalRuntime(db, master)
|
|
if err != nil {
|
|
t.Fatalf("buildInboundForLocalRuntime: %v", err)
|
|
}
|
|
|
|
var settings map[string]any
|
|
if err := json.Unmarshal([]byte(runtimeIb.Settings), &settings); err != nil {
|
|
t.Fatalf("runtime settings not valid json: %v", err)
|
|
}
|
|
fallbacks, ok := settings["fallbacks"].([]any)
|
|
if !ok || len(fallbacks) != 1 {
|
|
t.Fatalf("runtime settings must carry the fallback, got: %s", runtimeIb.Settings)
|
|
}
|
|
if !strings.Contains(runtimeIb.Settings, `"dest"`) || !strings.Contains(runtimeIb.Settings, "8081") {
|
|
t.Fatalf("fallback dest missing from runtime settings: %s", runtimeIb.Settings)
|
|
}
|
|
}
|
|
|
|
// A non-fallback-capable inbound (ws transport) must stay untouched.
|
|
func TestBuildRuntimeInboundForAPI_NoFallbacksOnWsInbound(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &InboundService{}
|
|
db := database.GetDB()
|
|
|
|
clients := []model.Client{
|
|
{Email: "ws@x", ID: "22222222-2222-2222-2222-222222222222", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 30202, model.VLESS, clientsSettings(t, clients))
|
|
ib.StreamSettings = `{"network":"ws","security":"none"}`
|
|
if err := db.Save(ib).Error; err != nil {
|
|
t.Fatalf("save stream: %v", err)
|
|
}
|
|
fb := model.InboundFallback{MasterId: ib.Id, Dest: "8082"}
|
|
if err := db.Create(&fb).Error; err != nil {
|
|
t.Fatalf("seed fallback: %v", err)
|
|
}
|
|
|
|
runtimeIb, err := svc.buildInboundForLocalRuntime(db, ib)
|
|
if err != nil {
|
|
t.Fatalf("buildInboundForLocalRuntime: %v", err)
|
|
}
|
|
if strings.Contains(runtimeIb.Settings, "fallbacks") {
|
|
t.Fatalf("ws inbound must not receive fallbacks: %s", runtimeIb.Settings)
|
|
}
|
|
}
|
|
|
|
// #6835: a child that accepts PROXY protocol must get a header from its fallback, whichever
|
|
// was saved first; an explicit dest or a chosen xver stays the operator's.
|
|
func TestBuildFallbacksJSON_DerivesXverFromTheChild(t *testing.T) {
|
|
setupBulkDB(t)
|
|
db := database.GetDB()
|
|
master := mkInbound(t, 30301, model.VLESS, `{"clients":[]}`)
|
|
|
|
cases := []struct {
|
|
name string
|
|
stream string
|
|
dest string
|
|
xver int
|
|
want any
|
|
}{
|
|
{"raw transport flag", `{"network":"tcp","tcpSettings":{"acceptProxyProtocol":true}}`, "", 0, float64(1)},
|
|
{"sockopt flag", `{"network":"grpc","sockopt":{"acceptProxyProtocol":true}}`, "", 0, float64(1)},
|
|
{"websocket flag", `{"network":"ws","wsSettings":{"acceptProxyProtocol":true}}`, "", 0, float64(1)},
|
|
{"stale flag of another transport", `{"network":"grpc","tcpSettings":{"acceptProxyProtocol":true}}`, "", 0, nil},
|
|
{"child without PROXY protocol", `{"network":"tcp"}`, "", 0, nil},
|
|
{"explicit dest keeps the stored xver", `{"network":"tcp","tcpSettings":{"acceptProxyProtocol":true}}`, "127.0.0.1:9000", 0, nil},
|
|
{"chosen xver 2 stays", `{"network":"tcp","tcpSettings":{"acceptProxyProtocol":true}}`, "", 2, float64(2)},
|
|
}
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := db.Where("master_id = ?", master.Id).Delete(&model.InboundFallback{}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
child := mkInboundStream(t, 30310+i, model.VLESS, `{"clients":[]}`, tc.stream)
|
|
fb := model.InboundFallback{MasterId: master.Id, ChildId: child.Id, Dest: tc.dest, Xver: tc.xver}
|
|
if err := db.Create(&fb).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := (&FallbackService{}).BuildFallbacksJSON(db, master.Id)
|
|
if err != nil || len(got) != 1 {
|
|
t.Fatalf("BuildFallbacksJSON = %v, %v", got, err)
|
|
}
|
|
raw, _ := json.Marshal(got[0])
|
|
var entry map[string]any
|
|
_ = json.Unmarshal(raw, &entry)
|
|
if entry["xver"] != tc.want {
|
|
t.Fatalf("xver = %v, want %v (entry %s)", entry["xver"], tc.want, raw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The running master keeps the fallback it was built with, so turning PROXY protocol on in a
|
|
// linked child must rebuild the master too; an inbound no fallback derives from needs nothing.
|
|
func TestUpdateInbound_FallbackChildChangeRebuildsItsMaster(t *testing.T) {
|
|
setupBulkDB(t)
|
|
mgr := useTestRuntimeManager(t)
|
|
mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{})
|
|
db := database.GetDB()
|
|
master := mkInboundStream(t, 30401, model.VLESS, `{"clients":[]}`, `{"network":"tcp","security":"tls"}`)
|
|
linked := mkInboundStream(t, 30402, model.VLESS, `{"clients":[]}`, `{"network":"tcp"}`)
|
|
unlinked := mkInboundStream(t, 30403, model.VLESS, `{"clients":[]}`, `{"network":"tcp"}`)
|
|
if err := db.Create(&model.InboundFallback{MasterId: master.Id, ChildId: linked.Id}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
ib *model.Inbound
|
|
want bool
|
|
}{{"linked child", linked, true}, {"unlinked inbound", unlinked, false}} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
update := *tc.ib
|
|
update.StreamSettings = `{"network":"tcp","tcpSettings":{"acceptProxyProtocol":true}}`
|
|
_, needRestart, err := (&InboundService{}).UpdateInbound(&update)
|
|
if err != nil {
|
|
t.Fatalf("UpdateInbound: %v", err)
|
|
}
|
|
if needRestart != tc.want {
|
|
t.Fatalf("needRestart = %v, want %v", needRestart, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|