fix(node): bound the host groups a node's inbounds may adopt

A node-reported host group is stored once per address and per adopted
inbound, so the rows and bytes a merge wrote were the product of three
values the node chooses, unbounded by the response size cap: a small
hosts/list answer could fill the master's disk.

A group whose serialized form exceeds 16 KiB is no longer adopted, and
a node's inbounds hold at most 2000 host rows, counted over what they
already hold, so repeated merges do not add more; the merge logs how
many groups it left out.
This commit is contained in:
MHSanaei
2026-11-01 01:05:48 +01:00
parent 3526a65f5d
commit f9f21fd8a4
3 changed files with 128 additions and 4 deletions
+36 -3
View File
@@ -1,6 +1,7 @@
package service
import (
"encoding/json"
"slices"
"sort"
"strconv"
@@ -182,19 +183,51 @@ func buildHostRows(groupId string, req *entity.HostGroup) []*model.Host {
return rows
}
// A node's group is stored once per address and per adopted inbound, so the
// response cap does not bound the write: the group's size and a per-node row budget do.
const (
maxAdoptedHostGroupBytes = 16 << 10
maxAdoptedHostRows = 2000
)
// adoptableHostGroups drops the node-reported groups too large to adopt.
func adoptableHostGroups(groups []*entity.HostGroup) []*entity.HostGroup {
return slices.DeleteFunc(slices.Clone(groups), func(g *entity.HostGroup) bool {
if g == nil {
return true
}
wire, err := json.Marshal(g)
return err != nil || len(wire) > maxAdoptedHostGroupBytes
})
}
// adoptedHostRowBudget is how many more host rows a node's inbounds may adopt.
func adoptedHostRowBudget(db *gorm.DB, nodeID int) (int, error) {
var held int64
err := db.Model(&model.Host{}).
Where("inbound_id IN (?)", db.Model(&model.Inbound{}).Select("id").Where("node_id = ?", nodeID)).
Count(&held).Error
return max(maxAdoptedHostRows-int(held), 0), err
}
// adoptedHostRows projects a node's host groups onto a freshly adopted central
// inbound so TLS/SNI/fingerprint overrides survive the node-to-master import.
func adoptedHostRows(groups []*entity.HostGroup, nodeInboundId, centralInboundId int) []*model.Host {
// inbound, building at most budget rows; it also returns how many groups that left out.
func adoptedHostRows(groups []*entity.HostGroup, nodeInboundId, centralInboundId, budget int) ([]*model.Host, int) {
var rows []*model.Host
skipped := 0
for _, g := range groups {
if g == nil || !slices.Contains(g.InboundIds, nodeInboundId) {
continue
}
if len(rows)+max(len(g.Hosts), 1) > budget {
skipped++
continue
}
scoped := *g
scoped.InboundIds = []int{centralInboundId}
rows = append(rows, buildHostRows(g.GroupId, &scoped)...)
}
return rows
return rows, skipped
}
func validateInboundsExist(tx *gorm.DB, inboundIds []int) error {