mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
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:
+41
-1
@@ -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 {
|
||||
|
||||
@@ -452,3 +452,43 @@ func TestOwnerlessClientHwidsPurgedOnUpgrade(t *testing.T) {
|
||||
t.Fatalf("hwid rows after upgrade = %q, want [owned]", subs)
|
||||
}
|
||||
}
|
||||
|
||||
// The editor fills forwardedPorts from the record, and an emptied field now withdraws the
|
||||
// forwards: a record left at its creation value would drop live forwards on the next save.
|
||||
func TestClientForwardedPortsBackfilledOnUpgrade(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "x-ui.db")
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = CloseDB() })
|
||||
if err := db.Create(&model.ClientRecord{Email: "fwd@x", Enable: true}).Error; err != nil {
|
||||
t.Fatalf("seed client: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.ClientRecord{Email: "kept@x", Enable: true, ForwardedPorts: "9000"}).Error; err != nil {
|
||||
t.Fatalf("seed client: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Inbound{
|
||||
Tag: "awg", Port: 51820, Protocol: model.AmneziaWG, Enable: true,
|
||||
Settings: `{"clients":[{"email":"fwd@x","forwardedPorts":"8022"},{"email":"kept@x","forwardedPorts":"9001"}]}`,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
if err := db.Where("seeder_name = ?", "ClientForwardedPortsBackfill").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
|
||||
t.Fatalf("clear seeder history: %v", err)
|
||||
}
|
||||
if err := CloseDB(); err != nil {
|
||||
t.Fatalf("CloseDB: %v", err)
|
||||
}
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("reopen InitDB: %v", err)
|
||||
}
|
||||
for email, want := range map[string]string{"fwd@x": "8022", "kept@x": "9000"} {
|
||||
var rec model.ClientRecord
|
||||
if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
|
||||
t.Fatalf("read %s: %v", email, err)
|
||||
}
|
||||
if rec.ForwardedPorts != want {
|
||||
t.Errorf("%s forwardedPorts = %q after upgrade, want %q", email, rec.ForwardedPorts, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +955,7 @@ type Client struct {
|
||||
AllowedIPsByInbound map[int][]string `json:"allowedIPsByInbound,omitempty"`
|
||||
PreSharedKey string `json:"preSharedKey,omitempty"`
|
||||
KeepAlive *KeepAliveValue `json:"keepAlive,omitempty"` // Numeric values or AmneziaWG ranges; 0 disables, omission preserves.
|
||||
ForwardedPorts string `json:"forwardedPorts,omitempty"` // AmneziaWG per-client port-forwarding spec, e.g. "80,443,8000-8100"
|
||||
ForwardedPorts *string `json:"forwardedPorts,omitempty"` // AmneziaWG port-forwarding spec, e.g. "80,443,8000-8100"; "" withdraws the forwards, omit to keep them
|
||||
Secret string `json:"secret,omitempty" example:"ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d"`
|
||||
AdTag string `json:"adTag,omitempty" example:"0123456789abcdef0123456789abcdef"`
|
||||
Email string `json:"email"` // Client email identifier
|
||||
@@ -1273,6 +1273,27 @@ func keepAliveValue(value *KeepAliveValue) KeepAliveValue {
|
||||
return *value
|
||||
}
|
||||
|
||||
// ForwardedPortsSpec is the client's port-forwarding spec, "" when unset.
|
||||
func (c Client) ForwardedPortsSpec() string {
|
||||
if c.ForwardedPorts == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*c.ForwardedPorts)
|
||||
}
|
||||
|
||||
// ForwardedPortsPtr wraps an explicit spec, "" included: distinct from a nil
|
||||
// ForwardedPorts, which means the field was never sent.
|
||||
func ForwardedPortsPtr(spec string) *string { return &spec }
|
||||
|
||||
// nonEmptyForwardedPorts keeps a stored "" nil: a record re-saved as a client must not
|
||||
// read as a request to withdraw the forwards.
|
||||
func nonEmptyForwardedPorts(spec string) *string {
|
||||
if spec == "" {
|
||||
return nil
|
||||
}
|
||||
return ForwardedPortsPtr(spec)
|
||||
}
|
||||
|
||||
func (c *Client) ToRecord() *ClientRecord {
|
||||
rec := &ClientRecord{
|
||||
Email: c.Email,
|
||||
@@ -1304,7 +1325,7 @@ func (c *Client) ToRecord() *ClientRecord {
|
||||
AllowedIPs: strings.Join(c.AllowedIPs, ","),
|
||||
PreSharedKey: c.PreSharedKey,
|
||||
KeepAlive: keepAliveValue(c.KeepAlive),
|
||||
ForwardedPorts: c.ForwardedPorts,
|
||||
ForwardedPorts: c.ForwardedPortsSpec(),
|
||||
Secret: c.Secret,
|
||||
AdTag: c.AdTag,
|
||||
}
|
||||
@@ -1364,7 +1385,7 @@ func (r *ClientRecord) ToClient() *Client {
|
||||
AllowedIPs: splitWireguardAllowedIPs(r.AllowedIPs),
|
||||
PreSharedKey: r.PreSharedKey,
|
||||
KeepAlive: keepAlivePtr(r.KeepAlive),
|
||||
ForwardedPorts: r.ForwardedPorts,
|
||||
ForwardedPorts: nonEmptyForwardedPorts(r.ForwardedPorts),
|
||||
Secret: r.Secret,
|
||||
AdTag: r.AdTag,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ func TestClientToRecordRoundTripWireGuard(t *testing.T) {
|
||||
AllowedIPs: []string{"10.0.0.2/32", "fd00::2/128"},
|
||||
PreSharedKey: "cHNrLWJhc2U2NC0zMmJ5dGVzLXBsYWNlaG9sZGVyISE=",
|
||||
KeepAlive: KeepAlivePtr(25),
|
||||
|
||||
ForwardedPorts: ForwardedPortsPtr("80,8000-8100"),
|
||||
}
|
||||
|
||||
rec := c.ToRecord()
|
||||
@@ -32,6 +34,7 @@ func TestClientToRecordRoundTripWireGuard(t *testing.T) {
|
||||
{"PublicKey", c.PublicKey, got.PublicKey},
|
||||
{"PreSharedKey", c.PreSharedKey, got.PreSharedKey},
|
||||
{"KeepAlive", c.KeepAliveSeconds(), got.KeepAliveSeconds()},
|
||||
{"ForwardedPorts", c.ForwardedPortsSpec(), got.ForwardedPortsSpec()},
|
||||
} {
|
||||
if f.a != f.b {
|
||||
t.Errorf("%s round-trip = %v, want %v", f.name, f.b, f.a)
|
||||
@@ -59,6 +62,15 @@ func TestClientToClientOmitsUnsetKeepAlive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit "" withdraws the forwards, which a re-saved record must never ask for.
|
||||
func TestClientToClientOmitsUnsetForwardedPorts(t *testing.T) {
|
||||
rec := &ClientRecord{Email: "awg@example.test", Enable: true}
|
||||
|
||||
if got := rec.ToClient().ForwardedPorts; got != nil {
|
||||
t.Fatalf("ForwardedPorts for a record that never set any = %q, want nil", *got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRecordEmptyAllowedIPs(t *testing.T) {
|
||||
rec := &ClientRecord{Email: "bob@example.test", AllowedIPs: ""}
|
||||
if got := rec.ToClient().AllowedIPs; got != nil {
|
||||
|
||||
Reference in New Issue
Block a user