feat: synchronize access.log client IPs across nodes (#5098)

* feat: synchronize access.log client IPs across nodes for global fail2ban limits

* fix(nodes): harden cross-node client-IP merge for cluster fail2ban

MergeInboundClientIps inserted new rows with the remote node's primary key,
which collides with the independently auto-incremented local id and rolled
back the whole sync batch — breaking exactly the node-only clients the
feature targets. It also never evicted stale IPs, so the 30-minute cutoff
was defeated cluster-wide (the master pushed its unpruned table back to
nodes, which re-added IPs they had just pruned) and the blobs grew unbounded.

- drop the remote id on create (Id=0) and guard the email-unique race with
  ON CONFLICT DO NOTHING; also fixes a latent Postgres sequence collision
- apply the same 30-minute stale cutoff inside the merge and skip creating
  node-only rows whose IPs are all stale
- throttle the IP fetch/merge/push to ~10s (data only refreshes every 10s)
  instead of running on every 5s traffic tick, cutting SQLite write churn
- log the load error on the push path and tidy the merge response message
- add unit tests for the merge (remote-id, dedup, stale-drop, skips)

---------

Co-authored-by: Rqzbeh <Rqzbeh@example.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Rouzbeh†
2026-06-09 00:59:50 +02:00
committed by GitHub
parent 0d7b6872f7
commit 9f31d7d056
9 changed files with 532 additions and 3 deletions
+19
View File
@@ -533,3 +533,22 @@ func isNonEmptySlice(v any) bool {
s, ok := v.([]any)
return ok && len(s) > 0
}
func (r *Remote) FetchAllClientIps(ctx context.Context) ([]model.InboundClientIps, error) {
env, err := r.do(ctx, http.MethodGet, "panel/api/server/clientIps", nil)
if err != nil {
return nil, err
}
var ips []model.InboundClientIps
if len(env.Obj) > 0 {
if err := json.Unmarshal(env.Obj, &ips); err != nil {
return nil, fmt.Errorf("decode client ips: %w", err)
}
}
return ips, nil
}
func (r *Remote) PushAllClientIps(ctx context.Context, ips []model.InboundClientIps) error {
_, err := r.do(ctx, http.MethodPost, "panel/api/server/clientIps", ips)
return err
}