fix: chunk large IN ? queries and fix IPv6 same-origin check

This commit is contained in:
lolka1333 2026-04-28 20:57:23 +02:00
parent 2bb3170ab0
commit f0d6966724
2 changed files with 119 additions and 25 deletions

View file

@ -46,7 +46,13 @@ func checkSameOrigin(r *http.Request) bool {
} }
host, _, err := net.SplitHostPort(r.Host) host, _, err := net.SplitHostPort(r.Host)
if err != nil { if err != nil {
// IPv6 literal без порта приходит как "[::1]" — net.SplitHostPort
// в этом случае ошибается, а url.Hostname() возвращает адрес без
// скобок. Снимаем их вручную, чтобы same-origin не отказывал на IPv6.
host = r.Host host = r.Host
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
host = host[1 : len(host)-1]
}
} }
return strings.EqualFold(u.Hostname(), host) return strings.EqualFold(u.Hostname(), host)
} }

View file

@ -369,6 +369,7 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
// Bulk-delete client IPs for every email in this inbound. The previous // Bulk-delete client IPs for every email in this inbound. The previous
// per-client loop fired one DELETE per row — at 7k+ clients that meant // per-client loop fired one DELETE per row — at 7k+ clients that meant
// thousands of synchronous SQL roundtrips and a multi-second freeze. // thousands of synchronous SQL roundtrips and a multi-second freeze.
// Chunked to stay under SQLite's bind-variable limit on huge inbounds.
if len(clients) > 0 { if len(clients) > 0 {
emails := make([]string, 0, len(clients)) emails := make([]string, 0, len(clients))
for i := range clients { for i := range clients {
@ -376,8 +377,8 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
emails = append(emails, clients[i].Email) emails = append(emails, clients[i].Email)
} }
} }
if len(emails) > 0 { for _, batch := range chunkStrings(uniqueNonEmptyStrings(emails), sqliteMaxVars) {
if err := db.Where("client_email IN ?", emails). if err := db.Where("client_email IN ?", batch).
Delete(model.InboundClientIps{}).Error; err != nil { Delete(model.InboundClientIps{}).Error; err != nil {
return false, err return false, err
} }
@ -1528,10 +1529,15 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
for _, traffic := range traffics { for _, traffic := range traffics {
inbound_ids = append(inbound_ids, traffic.InboundId) inbound_ids = append(inbound_ids, traffic.InboundId)
} }
err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error // Chunked to stay under SQLite's bind-variable limit when many inbounds
if err != nil { // are touched in a single tick.
for _, batch := range chunkInts(inbound_ids, sqliteMaxVars) {
var page []*model.Inbound
if err = tx.Model(model.Inbound{}).Where("id IN ?", batch).Find(&page).Error; err != nil {
return false, 0, err return false, 0, err
} }
inbounds = append(inbounds, page...)
}
for inbound_index := range inbounds { for inbound_index := range inbounds {
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
@ -2390,16 +2396,25 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
} }
} }
var traffics []*xray.ClientTraffic // Chunked to stay under SQLite's bind-variable limit when a single Telegram
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error // account owns thousands of clients across inbounds.
if err != nil { uniqEmails := uniqueNonEmptyStrings(emails)
traffics := make([]*xray.ClientTraffic, 0, len(uniqEmails))
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
var page []*xray.ClientTraffic
if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
continue
}
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err)
return nil, err
}
traffics = append(traffics, page...)
}
if len(traffics) == 0 {
logger.Warning("No ClientTraffic records found for emails:", emails) logger.Warning("No ClientTraffic records found for emails:", emails)
return nil, nil return nil, nil
} }
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", emails, err)
return nil, err
}
// Populate UUID and other client data for each traffic record // Populate UUID and other client data for each traffic record
for i := range traffics { for i := range traffics {
@ -2413,19 +2428,87 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
return traffics, nil return traffics, nil
} }
// sqliteMaxVars is a safe ceiling for the number of bind parameters in a
// single SQL statement. SQLite's SQLITE_MAX_VARIABLE_NUMBER is 999 on builds
// before 3.32 and 32766 after; staying under 999 keeps queries portable
// across forks/old binaries and also bounds per-query memory on truly large
// installs (>32k clients) where even modern SQLite would refuse a single IN.
const sqliteMaxVars = 900
// uniqueNonEmptyStrings returns a deduplicated copy of in with empty strings
// removed, preserving the order of first occurrence.
func uniqueNonEmptyStrings(in []string) []string {
if len(in) == 0 {
return nil
}
seen := make(map[string]struct{}, len(in))
out := make([]string, 0, len(in))
for _, v := range in {
if v == "" {
continue
}
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}
// chunkStrings splits s into consecutive sub-slices of at most size elements.
// Returns nil for an empty input or non-positive size.
func chunkStrings(s []string, size int) [][]string {
if size <= 0 || len(s) == 0 {
return nil
}
out := make([][]string, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
out = append(out, s[i:end])
}
return out
}
// chunkInts splits s into consecutive sub-slices of at most size elements.
// Returns nil for an empty input or non-positive size.
func chunkInts(s []int, size int) [][]int {
if size <= 0 || len(s) == 0 {
return nil
}
out := make([][]int, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
out = append(out, s[i:end])
}
return out
}
// GetActiveClientTraffics returns the absolute ClientTraffic rows for the given // GetActiveClientTraffics returns the absolute ClientTraffic rows for the given
// emails in a single batched query. Used by the WebSocket delta path to push // emails. Used by the WebSocket delta path to push per-client absolute
// per-client absolute counters without re-serializing the full inbound list. // counters without re-serializing the full inbound list. The query is chunked
// Empty input or a "record not found" result returns an empty slice. // to stay under SQLite's bind-variable limit on very large active sets.
// Empty input returns (nil, nil).
func (s *InboundService) GetActiveClientTraffics(emails []string) ([]*xray.ClientTraffic, error) { func (s *InboundService) GetActiveClientTraffics(emails []string) ([]*xray.ClientTraffic, error) {
if len(emails) == 0 { uniq := uniqueNonEmptyStrings(emails)
if len(uniq) == 0 {
return nil, nil return nil, nil
} }
db := database.GetDB() db := database.GetDB()
var traffics []*xray.ClientTraffic traffics := make([]*xray.ClientTraffic, 0, len(uniq))
if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error; err != nil { for _, batch := range chunkStrings(uniq, sqliteMaxVars) {
var page []*xray.ClientTraffic
if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
return nil, err return nil, err
} }
traffics = append(traffics, page...)
}
return traffics, nil return traffics, nil
} }
@ -2824,12 +2907,17 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
db := database.GetDB() db := database.GetDB()
// Step 1: Get ClientTraffic records for emails in the input list // Step 1: Get ClientTraffic records for emails in the input list.
var clients []xray.ClientTraffic // Chunked to stay under SQLite's bind-variable limit on huge inputs.
err := db.Where("email IN ?", emails).Find(&clients).Error uniqEmails := uniqueNonEmptyStrings(emails)
if err != nil && err != gorm.ErrRecordNotFound { clients := make([]xray.ClientTraffic, 0, len(uniqEmails))
for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
var page []xray.ClientTraffic
if err := db.Where("email IN ?", batch).Find(&page).Error; err != nil && err != gorm.ErrRecordNotFound {
return nil, nil, err return nil, nil, err
} }
clients = append(clients, page...)
}
// Step 2: Sort clients by (Up + Down) descending // Step 2: Sort clients by (Up + Down) descending
sort.Slice(clients, func(i, j int) bool { sort.Slice(clients, func(i, j int) bool {