fix(amneziawg): let an emptied forwardedPorts withdraw a client's forwards

UpdateInboundClient carried the stored forwardedPorts forward whenever
the incoming value was empty, and Client.ForwardedPorts was a plain
omitempty string, so clearing the field in the client editor, as its
hint says to, reported success while the settings kept the old ports
and the host kept relaying them to the client's device.

The field is now a pointer, as KeepAlive is: nil (not sent) keeps the
stored spec and "" withdraws it. The client record follows the live
spec on every update, and a one-time seeder fills records that still
hold their creation value from the inbound settings, so the editor no
longer posts an empty field that would drop forwards added later.
This commit is contained in:
MHSanaei
2026-11-01 08:55:38 +01:00
parent 0972ac31eb
commit a9191cb468
19 changed files with 304 additions and 31 deletions
+41 -1
View File
@@ -1530,7 +1530,7 @@ func runSeeders(isUsersEmpty bool) error {
}
if empty && isUsersEmpty {
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "SessionSecretRotation", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban", "ClientGroupNameTrim", "OwnerlessClientHwidPurge"}
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "SessionSecretRotation", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban", "ClientGroupNameTrim", "OwnerlessClientHwidPurge", "ClientForwardedPortsBackfill"}
for _, name := range seeders {
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
return err
@@ -1725,6 +1725,12 @@ func runSeeders(isUsersEmpty bool) error {
}
}
if !slices.Contains(seedersHistory, "ClientForwardedPortsBackfill") {
if err := backfillClientForwardedPorts(); err != nil {
return err
}
}
if err := seedHostsFromExternalProxy(); err != nil {
return err
}
@@ -1920,6 +1926,40 @@ func clearLegacyProxySettings() error {
})
}
// The client editor fills forwardedPorts from the record, which kept the value it was created
// with; an emptied field now withdraws the forwards, so the record must hold the live spec.
func backfillClientForwardedPorts() error {
return db.Transaction(func(tx *gorm.DB) error {
var inbounds []model.Inbound
if err := tx.Select("id", "settings").Where("protocol = ?", model.AmneziaWG).Find(&inbounds).Error; err != nil {
return err
}
for _, ib := range inbounds {
var parsed struct {
Clients []struct {
Email string `json:"email"`
ForwardedPorts string `json:"forwardedPorts"`
} `json:"clients"`
}
if json.Unmarshal([]byte(ib.Settings), &parsed) != nil {
continue
}
for _, c := range parsed.Clients {
spec := strings.TrimSpace(c.ForwardedPorts)
if c.Email == "" || spec == "" {
continue
}
if err := tx.Model(&model.ClientRecord{}).
Where("email = ? AND (wg_forwarded_ports IS NULL OR wg_forwarded_ports = '')", c.Email).
UpdateColumn("wg_forwarded_ports", spec).Error; err != nil {
return err
}
}
}
return tx.Create(&model.HistoryOfSeeders{SeederName: "ClientForwardedPortsBackfill"}).Error
})
}
// The HWID gate stored a row for any subId a request named, owned or not; those rows
// belong to no subscription and only grow the database.
func purgeOwnerlessClientHwids() error {