fix(hosts): carry an inbound update's externalProxy change onto its imported hosts (#6831)

Subscriptions build an inbound's links from its Host rows, and those rows are
imported from streamSettings.externalProxy only when the inbound is added. An
update that moved the external proxy returned 200, but every subscription kept
handing out the old address, port and security.

An update now re-syncs only the externalProxy entries it changed onto hosts that
are still exactly as imported. Unchanged entries are set aside first, so a host
the admin edited, disabled, deleted or shared is never re-imported next to the
admin's version, and a stream without the externalProxy key leaves hosts alone.
This commit is contained in:
Farhan Zare
2026-10-23 22:20:50 -04:00
committed by GitHub
parent d50c419e28
commit 3acdfd9dea
3 changed files with 458 additions and 17 deletions
+138 -17
View File
@@ -14,6 +14,7 @@ import (
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"slices"
"strconv"
@@ -817,24 +818,10 @@ func mtprotoInboundClientEmail(remark string, used map[string]struct{}) string {
// first run, so a freshly imported inbound must be converted here instead. Blank
// or malformed streamSettings, or one without externalProxy entries, is a no-op.
func CreateHostsFromExternalProxy(tx *gorm.DB, inboundId int, streamSettings string) (int, error) {
if strings.TrimSpace(streamSettings) == "" {
return 0, nil
}
var stream map[string]any
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return 0, nil
}
eps, ok := stream["externalProxy"].([]any)
if !ok || len(eps) == 0 {
return 0, nil
}
imports, _ := externalProxyHosts(inboundId, streamSettings)
created := 0
for i, raw := range eps {
ep, ok := raw.(map[string]any)
if !ok {
continue
}
if err := tx.Create(externalProxyEntryToHost(inboundId, i, ep)).Error; err != nil {
for _, host := range imports {
if err := tx.Create(host).Error; err != nil {
return created, err
}
created++
@@ -842,6 +829,140 @@ func CreateHostsFromExternalProxy(tx *gorm.DB, inboundId int, streamSettings str
return created, nil
}
// SyncHostsFromExternalProxy carries the entries an inbound update changed onto the hosts
// still exactly as imported; a stream without the externalProxy key leaves hosts alone.
func SyncHostsFromExternalProxy(tx *gorm.DB, inboundId int, oldStreamSettings, newStreamSettings string) error {
newImports, listed := externalProxyHosts(inboundId, newStreamSettings)
if !listed {
return nil
}
oldImports, _ := externalProxyHosts(inboundId, oldStreamSettings)
gone, added := changedImports(oldImports, newImports)
if len(gone) == 0 && len(added) == 0 {
return nil
}
untouched, err := untouchedImportedHosts(tx, inboundId, oldImports)
if err != nil || untouched == nil {
return err
}
for i, old := range gone {
host := untouched[old]
if host == nil {
continue
}
if i >= len(added) {
if err := tx.Delete(host).Error; err != nil {
return err
}
continue
}
added[i].Id, added[i].GroupId, added[i].SortOrder, added[i].CreatedAt = host.Id, host.GroupId, host.SortOrder, host.CreatedAt
if err := tx.Save(added[i]).Error; err != nil {
return err
}
}
for _, host := range added[min(len(gone), len(added)):] {
if err := tx.Create(host).Error; err != nil {
return err
}
}
return nil
}
// changedImports drops the entries both lists share and returns, in list order, the old
// positions that changed or went away and the new entries that changed or appeared.
func changedImports(oldImports, newImports []*model.Host) ([]int, []*model.Host) {
kept := make([]bool, len(oldImports))
var added []*model.Host
for _, next := range newImports {
match := -1
for i, prev := range oldImports {
if !kept[i] && sameImportedHost(prev, next) {
match = i
break
}
}
if match < 0 {
added = append(added, next)
continue
}
kept[match] = true
}
var gone []int
for i := range oldImports {
if !kept[i] {
gone = append(gone, i)
}
}
return gone, added
}
// untouchedImportedHosts pairs each old import with a row still equal to it and alone in its
// group (nil where the admin edited, shared or deleted it); nil when no row qualifies.
func untouchedImportedHosts(tx *gorm.DB, inboundId int, imports []*model.Host) ([]*model.Host, error) {
var stored []*model.Host
if err := tx.Where("inbound_id = ?", inboundId).Order("sort_order asc, id asc").Find(&stored).Error; err != nil {
return nil, err
}
paired := make([]*model.Host, len(imports))
found := false
for i, want := range imports {
for _, host := range stored {
if slices.Contains(paired, host) || !sameImportedHost(host, want) {
continue
}
var groupRows int64
if err := tx.Model(&model.Host{}).Where("group_id = ?", host.GroupId).Count(&groupRows).Error; err != nil {
return nil, err
}
if groupRows == 1 {
paired[i], found = host, true
break
}
}
}
if !found {
return nil, nil
}
return paired, nil
}
// externalProxyHosts maps each entry to the host an import creates; the bool reports whether
// the stream carries an externalProxy list at all, an empty one included.
func externalProxyHosts(inboundId int, streamSettings string) ([]*model.Host, bool) {
if strings.TrimSpace(streamSettings) == "" {
return nil, false
}
var stream map[string]any
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return nil, false
}
eps, listed := stream["externalProxy"].([]any)
var hosts []*model.Host
for i, raw := range eps {
if ep, ok := raw.(map[string]any); ok {
hosts = append(hosts, externalProxyEntryToHost(inboundId, i, ep))
}
}
return hosts, listed
}
// sameImportedHost compares every field but row identity and placement, so a host the
// admin changed in any way no longer counts as the import; empty lists match nil.
func sameImportedHost(a, b *model.Host) bool {
return reflect.DeepEqual(importedHostFields(*a), importedHostFields(*b))
}
func importedHostFields(h model.Host) model.Host {
h.Id, h.GroupId, h.SortOrder, h.CreatedAt, h.UpdatedAt = 0, "", 0, 0, 0
for _, list := range []*[]string{&h.Tags, &h.Alpn, &h.PinnedPeerCertSha256, &h.ExcludeFromSubTypes, &h.NodeGuids} {
if len(*list) == 0 {
*list = nil
}
}
return h
}
func externalProxyEntryToHost(inboundId, index int, ep map[string]any) *model.Host {
security, _ := ep["forceTls"].(string)
switch security {