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
+24 -3
View File
@@ -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 {