mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03:00
fix(fallback): derive a linked fallback's PROXY protocol header from its child (#6880)
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -123,6 +124,7 @@ func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[s
|
||||
out := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
dest := strings.TrimSpace(r.Dest)
|
||||
xver := r.Xver
|
||||
if dest == "" {
|
||||
child, ok := byId[r.ChildId]
|
||||
if !ok {
|
||||
@@ -133,6 +135,11 @@ func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[s
|
||||
listen = "127.0.0.1"
|
||||
}
|
||||
dest = fmt.Sprintf("%s:%d", listen, child.Port)
|
||||
// A child that accepts PROXY protocol rejects a fallback without the header (#6835);
|
||||
// deriving it here keeps the row right through either order of toggling.
|
||||
if xver == 0 && acceptsProxyProtocol(child.StreamSettings) {
|
||||
xver = 1
|
||||
}
|
||||
}
|
||||
entry := map[string]any{
|
||||
"dest": dest,
|
||||
@@ -146,10 +153,51 @@ func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[s
|
||||
if r.Path != "" {
|
||||
entry["path"] = r.Path
|
||||
}
|
||||
if r.Xver > 0 {
|
||||
entry["xver"] = r.Xver
|
||||
if xver > 0 {
|
||||
entry["xver"] = xver
|
||||
}
|
||||
out = append(out, entry)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HasDerivedFallbackTo reports whether a master's fallback takes its dest and xver from childId.
|
||||
func (s *FallbackService) HasDerivedFallbackTo(tx *gorm.DB, childId int) (bool, error) {
|
||||
var n int64
|
||||
err := tx.Model(&model.InboundFallback{}).Where("child_id = ? AND TRIM(dest) = ''", childId).Count(&n).Error
|
||||
return n > 0, err
|
||||
}
|
||||
|
||||
// fallbackTargetChanged reports whether an edit changed what a derived fallback is built from.
|
||||
func fallbackTargetChanged(before, after *model.Inbound) bool {
|
||||
return before.Listen != after.Listen || before.Port != after.Port ||
|
||||
acceptsProxyProtocol(before.StreamSettings) != acceptsProxyProtocol(after.StreamSettings)
|
||||
}
|
||||
|
||||
// proxyProtocolTransportKey maps the transports whose settings carry acceptProxyProtocol;
|
||||
// RAW is stored as tcp, older rows say raw.
|
||||
var proxyProtocolTransportKey = map[string]string{
|
||||
"tcp": "tcpSettings",
|
||||
"raw": "tcpSettings",
|
||||
"ws": "wsSettings",
|
||||
"httpupgrade": "httpupgradeSettings",
|
||||
}
|
||||
|
||||
// acceptsProxyProtocol reports whether an inbound's listener expects a PROXY header, via
|
||||
// sockopt or its transport, the two places xray-core wraps the listener.
|
||||
func acceptsProxyProtocol(streamSettings string) bool {
|
||||
var stream map[string]any
|
||||
if json.Unmarshal([]byte(streamSettings), &stream) != nil {
|
||||
return false
|
||||
}
|
||||
if sockopt, _ := stream["sockopt"].(map[string]any); sockopt["acceptProxyProtocol"] == true {
|
||||
return true
|
||||
}
|
||||
network, _ := stream["network"].(string)
|
||||
key, ok := proxyProtocolTransportKey[strings.ToLower(network)]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
transport, _ := stream[key].(map[string]any)
|
||||
return transport["acceptProxyProtocol"] == true
|
||||
}
|
||||
|
||||
@@ -2267,6 +2267,17 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
if err := tx.Save(oldInbound).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// A master's running fallback was built from this child's listen, port and PROXY flag;
|
||||
// re-adding only the child would leave the master pointing at the old ones.
|
||||
if fallbackTargetChanged(&running, oldInbound) {
|
||||
linked, err := s.fallbackService.HasDerivedFallbackTo(tx, oldInbound.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if linked {
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
newClients, gcErr := s.GetClients(oldInbound)
|
||||
if gcErr != nil {
|
||||
return gcErr
|
||||
|
||||
@@ -76,3 +76,82 @@ func TestBuildRuntimeInboundForAPI_NoFallbacksOnWsInbound(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user