mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 00:55:01 +03:00
fix(node): let a node change only the clients it alone hosts
A node snapshot was merged into the master's client records by email, and the only ownership check filtered one derived copy of it: the node's settings were still stored and re-synced later, its stats still merged, and a record linked to the master as well as the node took every value the node reported, including credentials rendered into the master's own Xray config. Each snapshot now passes one gate before anything is written. A record also linked to the master or another node keeps the master's values apart from per-link fields and the lifecycle the merge already settles; a record linked only elsewhere, a detached record, a padded or case-variant email and a new client holding another client's subId are dropped from the stored settings as well. A soft-orphaned record can be taken back only by the node whose merge orphaned it, which the new clients.sync_orphaned_by column records. A node tunnel no longer owns the peer keys of a client a master-local tunnel also serves.
This commit is contained in:
@@ -113,7 +113,7 @@ func (s *ClientService) syncInboundClients(tx *gorm.DB, inboundId int, clients [
|
||||
}
|
||||
|
||||
var inbound model.Inbound
|
||||
if err := tx.Select("protocol").Where("id = ?", inboundId).Take(&inbound).Error; err != nil {
|
||||
if err := tx.Select("protocol", "node_id").Where("id = ?", inboundId).Take(&inbound).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.validateProtocolClients(tx, inbound.Protocol, inboundId, clients, detachEmails, prune); err != nil {
|
||||
@@ -156,7 +156,7 @@ func (s *ClientService) syncInboundClients(tx *gorm.DB, inboundId int, clients [
|
||||
ids = append(ids, row.Id)
|
||||
}
|
||||
var err error
|
||||
if onLowerTunnel, err = clientsOnLowerTunnel(tx, inboundId, ids); err != nil {
|
||||
if onLowerTunnel, err = clientsOnLowerTunnel(tx, inboundId, inbound.NodeID, ids); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -408,16 +408,21 @@ func keepLinkOwnedFields(row, before *model.ClientRecord, ownsTunnelPeer bool) {
|
||||
row.KeepAlive = before.KeepAlive
|
||||
}
|
||||
|
||||
// clientsOnLowerTunnel returns the clients also attached to a WireGuard/AmneziaWG
|
||||
// inbound with a lower id than inboundId: that tunnel owns their record's peer fields.
|
||||
func clientsOnLowerTunnel(tx *gorm.DB, inboundId int, clientIds []int) (map[int]struct{}, error) {
|
||||
// clientsOnLowerTunnel returns the clients also attached to a higher-ranked WireGuard/AmneziaWG
|
||||
// inbound, which owns their record's peer fields: a master-local one first, then the lowest id.
|
||||
func clientsOnLowerTunnel(tx *gorm.DB, inboundId int, nodeID *int, clientIds []int) (map[int]struct{}, error) {
|
||||
outranks := "inbounds.node_id IS NULL AND client_inbounds.inbound_id < ?"
|
||||
if nodeID != nil {
|
||||
outranks = "(inbounds.node_id IS NULL OR client_inbounds.inbound_id < ?)"
|
||||
}
|
||||
out := make(map[int]struct{})
|
||||
for _, batch := range chunkInts(clientIds, sqlInChunk) {
|
||||
var ids []int
|
||||
if err := tx.Table("client_inbounds").
|
||||
Joins("JOIN inbounds ON inbounds.id = client_inbounds.inbound_id").
|
||||
Where("client_inbounds.client_id IN ? AND client_inbounds.inbound_id < ? AND inbounds.protocol IN ?",
|
||||
batch, inboundId, []model.Protocol{model.WireGuard, model.AmneziaWG}).
|
||||
Where("client_inbounds.client_id IN ? AND inbounds.protocol IN ?",
|
||||
batch, []model.Protocol{model.WireGuard, model.AmneziaWG}).
|
||||
Where(outranks, inboundId).
|
||||
Distinct().Pluck("client_inbounds.client_id", &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user