Skip to content

Commit fcfd735

Browse files
committed
fix: WebSocket ban handling and unban support
Major improvements to WebSocket ban synchronization: 1. Real ban application via WebSocket: - handleWSBan now actually applies bans via firewall manager - Was only logging before, not actually banning 2. Ban sync with unban support: - Added handleWSBanSync for full ban list synchronization - Removes local bans that no longer exist in Core (deleted in UI) - Prevents spam logs by only logging changes (new/removed bans) 3. Reduced log spam: - Individual 'New ban received' logs removed from ban_sync - Now logs summary: 'Ban sync complete: X new bans, Y removed' - Individual ban logs only for real-time 'ban' messages 4. Added GetAllBannedIPs() to IPTablesManager: - Returns all currently banned IPs from in-memory cache - Used for detecting which bans to remove during sync Technical changes: - WebSocketClient now accepts onBanSync callback - Ban sync compares Core ban list with local bans - Missing bans in Core list are automatically unbanned locally - All WebSocket bans use reportToSync=false to prevent loops
1 parent d65d6e3 commit fcfd735

3 files changed

Lines changed: 130 additions & 9 deletions

File tree

config/manager.go

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strings"
1313
"sync"
1414
"time"
15+
16+
"github.com/defenra/agent/firewall"
1517
)
1618

1719
type ConfigManager struct {
@@ -96,8 +98,9 @@ func (cm *ConfigManager) initWebSocket() {
9698
cm.agentID,
9799
cm.agentKey,
98100
cm.coreURL,
99-
cm.handleWSConfig, // callback for config updates
100-
cm.handleWSBan, // callback for ban updates
101+
cm.handleWSConfig, // callback for config updates
102+
cm.handleWSBan, // callback for individual ban
103+
cm.handleWSBanSync, // callback for ban sync (with unban support)
101104
)
102105
}
103106

@@ -109,8 +112,104 @@ func (cm *ConfigManager) handleWSConfig(config *Config) {
109112
}
110113

111114
func (cm *ConfigManager) handleWSBan(ban BanInfo) {
112-
// Forward to firewall manager
113-
log.Printf("[WebSocket] New ban received: %s", ban.IP)
115+
// Apply ban via firewall manager
116+
firewallMgr := firewall.GetIPTablesManager()
117+
if firewallMgr == nil {
118+
return
119+
}
120+
121+
// Check if already banned to avoid spam logs
122+
if firewallMgr.IsBanned(ban.IP) {
123+
return
124+
}
125+
126+
// Calculate duration
127+
now := time.Now()
128+
if ban.ExpiresAt.Before(now) {
129+
// Ban already expired, skip
130+
return
131+
}
132+
duration := ban.ExpiresAt.Sub(now)
133+
134+
// Apply ban without reporting back to Core (came from Core via WebSocket)
135+
var err error
136+
if ban.IsCIDR {
137+
err = firewallMgr.BanIPRangeWithSync(ban.IP, duration, ban.Reason+" (global)", false)
138+
} else if ban.IsPermanent {
139+
err = firewallMgr.AddToPermanentBlacklistWithSync(ban.IP, ban.Reason+" (global)", false)
140+
} else {
141+
err = firewallMgr.BanIPWithSync(ban.IP, duration, ban.Reason+" (global)", false)
142+
}
143+
144+
if err != nil {
145+
log.Printf("[WebSocket] Failed to apply ban for %s: %v", ban.IP, err)
146+
} else {
147+
log.Printf("[WebSocket] Applied ban: %s (%s, expires: %v)", ban.IP, ban.Reason, duration.Round(time.Minute))
148+
}
149+
}
150+
151+
func (cm *ConfigManager) handleWSBanSync(bans []BanInfo) {
152+
// Apply all bans from sync and remove local bans that are not in the list
153+
firewallMgr := firewall.GetIPTablesManager()
154+
if firewallMgr == nil {
155+
return
156+
}
157+
158+
// Track which bans were applied in this sync
159+
appliedBans := make(map[string]bool)
160+
newBansCount := 0
161+
162+
for _, ban := range bans {
163+
appliedBans[ban.IP] = true
164+
165+
// Skip if already banned
166+
if firewallMgr.IsBanned(ban.IP) {
167+
continue
168+
}
169+
170+
// Check if expired
171+
now := time.Now()
172+
if ban.ExpiresAt.Before(now) {
173+
continue
174+
}
175+
duration := ban.ExpiresAt.Sub(now)
176+
177+
// Apply ban without reporting back to Core
178+
var err error
179+
if ban.IsCIDR {
180+
err = firewallMgr.BanIPRangeWithSync(ban.IP, duration, ban.Reason+" (global)", false)
181+
} else if ban.IsPermanent {
182+
err = firewallMgr.AddToPermanentBlacklistWithSync(ban.IP, ban.Reason+" (global)", false)
183+
} else {
184+
err = firewallMgr.BanIPWithSync(ban.IP, duration, ban.Reason+" (global)", false)
185+
}
186+
187+
if err != nil {
188+
log.Printf("[WebSocket] Failed to apply ban for %s: %v", ban.IP, err)
189+
} else {
190+
newBansCount++
191+
}
192+
}
193+
194+
// Remove local bans that are not in the Core list (unban)
195+
// This handles case when ban was removed in UI
196+
locallyBannedIPs := firewallMgr.GetAllBannedIPs()
197+
removedCount := 0
198+
for _, ip := range locallyBannedIPs {
199+
if !appliedBans[ip] {
200+
// This ban exists locally but not in Core - remove it
201+
if err := firewallMgr.UnbanIP(ip); err != nil {
202+
log.Printf("[WebSocket] Failed to unban %s: %v", ip, err)
203+
} else {
204+
removedCount++
205+
log.Printf("[WebSocket] Removed ban (deleted in UI): %s", ip)
206+
}
207+
}
208+
}
209+
210+
if newBansCount > 0 || removedCount > 0 {
211+
log.Printf("[WebSocket] Ban sync complete: %d new bans, %d removed", newBansCount, removedCount)
212+
}
114213
}
115214

116215
func (cm *ConfigManager) poll() {

config/websocket.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,13 @@ type WebSocketClient struct {
5151
cancel context.CancelFunc
5252
onConfig func(*Config)
5353
onBan func(BanInfo)
54+
onBanSync func([]BanInfo)
5455
isRunning bool
5556
reconnectInterval time.Duration
5657
}
5758

5859
// NewWebSocketClient creates a new WebSocket client
59-
func NewWebSocketClient(agentId, agentKey, coreURL string, onConfig func(*Config), onBan func(BanInfo)) *WebSocketClient {
60+
func NewWebSocketClient(agentId, agentKey, coreURL string, onConfig func(*Config), onBan func(BanInfo), onBanSync func([]BanInfo)) *WebSocketClient {
6061
ctx, cancel := context.WithCancel(context.Background())
6162
return &WebSocketClient{
6263
agentId: agentId,
@@ -66,6 +67,7 @@ func NewWebSocketClient(agentId, agentKey, coreURL string, onConfig func(*Config
6667
cancel: cancel,
6768
onConfig: onConfig,
6869
onBan: onBan,
70+
onBanSync: onBanSync,
6971
reconnectInterval: 5 * time.Second,
7072
}
7173
}
@@ -216,10 +218,11 @@ func (w *WebSocketClient) processMessage(data []byte) error {
216218
return fmt.Errorf("unmarshal ban_sync error: %w", err)
217219
}
218220

219-
log.Printf("[WebSocket] Received ban sync with %d bans", banSync.Total)
220-
221-
// Forward to ban handler if set
222-
if w.onBan != nil {
221+
// Use onBanSync callback for full sync (handles new bans and unbans)
222+
if w.onBanSync != nil {
223+
w.onBanSync(banSync.Bans)
224+
} else if w.onBan != nil {
225+
// Fallback: forward to individual ban handler
223226
for _, ban := range banSync.Bans {
224227
w.onBan(ban)
225228
}

firewall/iptables.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,25 @@ func (m *IPTablesManager) IsBanned(ip string) bool {
242242
return exists && time.Now().Before(expiresAt)
243243
}
244244

245+
// GetAllBannedIPs returns all currently banned IPs from in-memory cache
246+
// Note: This only works for fallback mode (no ipset). For ipset mode,
247+
// this returns the in-memory cache which may not include all IPs from ipset.
248+
func (m *IPTablesManager) GetAllBannedIPs() []string {
249+
m.mu.RLock()
250+
defer m.mu.RUnlock()
251+
252+
ips := make([]string, 0, len(m.bannedIPs))
253+
now := time.Now()
254+
255+
for ip, expiresAt := range m.bannedIPs {
256+
if now.Before(expiresAt) {
257+
ips = append(ips, ip)
258+
}
259+
}
260+
261+
return ips
262+
}
263+
245264
func (m *IPTablesManager) cleanupExpired() {
246265
ticker := time.NewTicker(m.checkInterval)
247266
defer ticker.Stop()

0 commit comments

Comments
 (0)