mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-06 05:05:01 +03:00
perf(sub): resolve legacy socket fallback fronts once per request
projectThroughFallbackMaster found the front of each unix-socket child by scanning every inbound's settings JSON, once per child, so a subscription cost the number of socket inbounds times the size of all settings in the table. A request now collects every host's socket fronts in one statement, the lowest-id front naming a socket on that host still wins, and an inbound with unreadable settings no longer fails the lookup.
This commit is contained in:
@@ -17,6 +17,8 @@ import (
|
|||||||
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func seedSubDB(t *testing.T) {
|
func seedSubDB(t *testing.T) {
|
||||||
@@ -2011,3 +2013,67 @@ func TestSub_HostMultiSNI_Overrides(t *testing.T) {
|
|||||||
t.Errorf("expected blank sni: %s", parts[1])
|
t.Errorf("expected blank sni: %s", parts[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "@socket" front lookup parses every inbound's settings, so a request runs it
|
||||||
|
// once, not per child; the lower-id front still wins and unreadable settings are skipped.
|
||||||
|
func TestSub_LegacyFallbackMasterLookedUpOncePerRequest(t *testing.T) {
|
||||||
|
seedSubDB(t)
|
||||||
|
db := database.GetDB()
|
||||||
|
fronts := []*model.Inbound{
|
||||||
|
{
|
||||||
|
UserId: 1, Tag: "front", Enable: true, Port: 9443, Protocol: model.VLESS,
|
||||||
|
Settings: `{"clients":[],"decryption":"none","fallbacks":[{"dest":80},{"dest":"@child-1"},"not-an-object"]}`,
|
||||||
|
StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"front.sni"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserId: 1, Tag: "newer-front", Enable: true, Port: 9444, Protocol: model.VLESS,
|
||||||
|
Settings: `{"clients":[],"decryption":"none","fallbacks":[{"dest":"@child-1"},{"dest":"@child-2"}]}`,
|
||||||
|
StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"newer.sni"}}`,
|
||||||
|
},
|
||||||
|
{UserId: 1, Tag: "broken", Enable: true, Port: 9445, Protocol: model.VLESS, Settings: `{not json`, StreamSettings: `{}`},
|
||||||
|
}
|
||||||
|
for _, ib := range fronts {
|
||||||
|
if err := db.Create(ib).Error; err != nil {
|
||||||
|
t.Fatalf("seed %s: %v", ib.Tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 1; i <= 3; i++ {
|
||||||
|
child := seedSubInbound(t, "s1", fmt.Sprintf("child-%d", i), 4450+i, i, `{"network":"ws","security":"none"}`)
|
||||||
|
child.Listen = fmt.Sprintf("@child-%d", i)
|
||||||
|
if err := db.Model(&model.Inbound{}).Where("id = ?", child.Id).Update("listen", child.Listen).Error; err != nil {
|
||||||
|
t.Fatalf("set child listen: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scans := 0
|
||||||
|
count := func(tx *gorm.DB) {
|
||||||
|
if strings.Contains(tx.Statement.SQL.String(), "$.fallbacks") {
|
||||||
|
scans++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = db.Callback().Query().After("gorm:query").Register("test:count_front_scans", count)
|
||||||
|
_ = db.Callback().Row().After("gorm:row").Register("test:count_front_scans", count)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = db.Callback().Query().Remove("test:count_front_scans")
|
||||||
|
_ = db.Callback().Row().Remove("test:count_front_scans")
|
||||||
|
})
|
||||||
|
|
||||||
|
links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSubs: %v", err)
|
||||||
|
}
|
||||||
|
if scans != 1 {
|
||||||
|
t.Fatalf("inbound settings were scanned %d times in one request, want 1", scans)
|
||||||
|
}
|
||||||
|
if len(links) != 3 {
|
||||||
|
t.Fatalf("links = %d, want 3: %v", len(links), links)
|
||||||
|
}
|
||||||
|
for i, want := range []string{"req.example.com:9443?", "req.example.com:9444?", "req.example.com:4453?"} {
|
||||||
|
if !strings.Contains(links[i], want) {
|
||||||
|
t.Fatalf("child-%d link should advertise %s: %s", i+1, want, links[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(links[0], "sni=front.sni") {
|
||||||
|
t.Fatalf("child-1 must carry its front's TLS state: %s", links[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+55
-13
@@ -79,6 +79,9 @@ type SubService struct {
|
|||||||
// with the clients array left out; generators read only inbound-level
|
// with the clients array left out; generators read only inbound-level
|
||||||
// fields (encryption, method, version, …) from it.
|
// fields (encryption, method, version, …) from it.
|
||||||
settingsByInbound map[int]map[string]any
|
settingsByInbound map[int]map[string]any
|
||||||
|
// socketFronts maps a host and a unix-socket dest named by a settings.fallbacks to the
|
||||||
|
// lowest-id inbound of that host naming it. Filled by socketFront; reset per request.
|
||||||
|
socketFronts map[socketFrontKey]*model.Inbound
|
||||||
// rendered is what this request has put into its response so far.
|
// rendered is what this request has put into its response so far.
|
||||||
rendered renderBudget
|
rendered renderBudget
|
||||||
// settingsCache is shared by this service's requests (ForRequest copies the pointer); only
|
// settingsCache is shared by this service's requests (ForRequest copies the pointer); only
|
||||||
@@ -130,6 +133,7 @@ func (s *SubService) PrepareForRequest(host string) {
|
|||||||
s.clientsByInbound = map[int]map[string]model.Client{}
|
s.clientsByInbound = map[int]map[string]model.Client{}
|
||||||
s.fullyPrimedInbounds = map[int]bool{}
|
s.fullyPrimedInbounds = map[int]bool{}
|
||||||
s.settingsByInbound = map[int]map[string]any{}
|
s.settingsByInbound = map[int]map[string]any{}
|
||||||
|
s.socketFronts = nil
|
||||||
s.hostsByInbound = nil
|
s.hostsByInbound = nil
|
||||||
s.hostGroupOrders = nil
|
s.hostGroupOrders = nil
|
||||||
s.sharedGroupSni = nil
|
s.sharedGroupSni = nil
|
||||||
@@ -878,21 +882,9 @@ func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if master == nil && len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
if master == nil && len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||||
var m model.Inbound
|
|
||||||
// A unix socket exists only on the host that binds it, so only an inbound
|
// A unix socket exists only on the host that binds it, so only an inbound
|
||||||
// hosted where the child runs (this panel, or the same node) can front it.
|
// hosted where the child runs (this panel, or the same node) can front it.
|
||||||
q := db.Model(model.Inbound{})
|
master = s.socketFront(socketFrontKey{node: nodeKeyOf(inbound.NodeID), dest: inbound.Listen})
|
||||||
if inbound.NodeID != nil {
|
|
||||||
q = q.Where("node_id = ?", *inbound.NodeID)
|
|
||||||
} else {
|
|
||||||
q = q.Where("node_id IS NULL")
|
|
||||||
}
|
|
||||||
if err := q.
|
|
||||||
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
|
|
||||||
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", inbound.Listen).
|
|
||||||
First(&m).Error; err == nil {
|
|
||||||
master = &m
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if master == nil {
|
if master == nil {
|
||||||
@@ -3521,3 +3513,53 @@ func extraSalamanderKeys(settings map[string]any, expressedPacketSize bool) []st
|
|||||||
sort.Strings(extra)
|
sort.Strings(extra)
|
||||||
return extra
|
return extra
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// socketFrontKey names a unix socket on one host: 0 is this panel, else the node id.
|
||||||
|
type socketFrontKey struct {
|
||||||
|
node int
|
||||||
|
dest string
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeKeyOf(nodeID *int) int {
|
||||||
|
if nodeID == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *nodeID
|
||||||
|
}
|
||||||
|
|
||||||
|
// socketFront resolves a legacy unix-socket child to its front. That means parsing the
|
||||||
|
// settings JSON of every inbound, so the scan runs once per request, not once per child.
|
||||||
|
func (s *SubService) socketFront(key socketFrontKey) *model.Inbound {
|
||||||
|
if s.socketFronts == nil {
|
||||||
|
s.socketFronts = map[socketFrontKey]*model.Inbound{}
|
||||||
|
var fronts []struct {
|
||||||
|
NodeID *int
|
||||||
|
Listen string
|
||||||
|
Port int
|
||||||
|
StreamSettings string
|
||||||
|
Fallbacks string
|
||||||
|
}
|
||||||
|
// json_valid keeps one inbound with unreadable settings from failing the
|
||||||
|
// whole statement and hiding every front.
|
||||||
|
if err := database.GetDB().Model(model.Inbound{}).
|
||||||
|
Select("node_id, listen, port, stream_settings, json_extract(settings, '$.fallbacks') AS fallbacks").
|
||||||
|
Where("CASE WHEN json_valid(settings) THEN JSON_TYPE(settings, '$.fallbacks') END = 'array'").
|
||||||
|
Order("id ASC").Scan(&fronts).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, front := range fronts {
|
||||||
|
var fallbacks []any
|
||||||
|
_ = json.Unmarshal([]byte(front.Fallbacks), &fallbacks)
|
||||||
|
for _, fallback := range fallbacks {
|
||||||
|
rule, _ := fallback.(map[string]any)
|
||||||
|
dest, _ := rule["dest"].(string)
|
||||||
|
k := socketFrontKey{node: nodeKeyOf(front.NodeID), dest: dest}
|
||||||
|
if _, taken := s.socketFronts[k]; taken || !strings.HasPrefix(dest, "@") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.socketFronts[k] = &model.Inbound{Listen: front.Listen, Port: front.Port, StreamSettings: front.StreamSettings}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.socketFronts[key]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user