# Fix Firewall Peer Sync Bugs

## TL;DR

> **Quick Summary**: 5 bug yang menyebabkan hanya 1 peer WireGuard yang bisa akses WG IP, nftables rules duplikat menumpuk, dan error log spam tiap 30 detik.
>
> **Deliverables**:
> - Fix `SyncPeers` PSK zero-value bug → gogo3 bisa akses WG IP
> - Fix `handshakesync.go` broken SQL → hilangkan error log spam
> - Fix `DevicesHandler.Update()` → sync WireGuard saat config berubah
> - Fix `AddForwardRule` dedup → hilangkan nftables rule duplikat
> - Fix startup cleanup → clean slate tiap restart
> - Immediate server fix: clean nftables + set AllowedIPs
>
> **Estimated Effort**: Medium
> **Parallel Execution**: YES - 2 waves
> **Critical Path**: Task 1 (immediate) → Task 2-5 (code fixes) → Task 6 (rebuild & deploy)

---

## Context

### Original Request
User melaporkan 2 peer terkoneksi dengan firewall yang sama, tapi hanya 1 peer (gogo1) yang bisa akses WireGuard IP. Padahal config allowedIPs sama di database. Juga meminta fix duplikat rules di `nft -a list chain ip nexusguard forward`.

### Investigation Summary
SSH ke server `172.20.8.191` dan analisis kode mengungkap 5 bug:

**Server State (awal):**
- nftables forward chain: 70+ rules, banyak duplikat (peer_gogo2 ×15, peer_gogo3 ×12, fwd_estab ×3)
- WireGuard: gogo3 punya `allowed ips: (none)` meskipun DB punya `endpoint_allowed_ips = 10.172.21.0/24`
- Server logs: error SQL spam tiap 30 detik dari `handshakesync.go`
- gogo2 punya `is_active = false` di DB

**Kode Bugs:**
1. `wgmanager_linux.go:153-174` — PSK zero-value bug
2. `handshakesync.go:42-50` — anonymous struct → empty table name
3. `devices.go:317` — Update() tidak panggil SyncLocalPeers()
4. `nftables_linux.go:189` — AddForwardRule tanpa dedup
5. `main.go:199-222` — startup reapply tanpa cleanup

---

## Work Objectives

### Core Objective
Fix semua bug yang menyebabkan peer WireGuard tidak bisa akses WG IP dan nftables rules duplikat.

### Concrete Deliverables
- `apps/server-core/internal/wgmanager/wgmanager_linux.go` — PSK fix
- `apps/server-core/internal/wgmanager/handshakesync.go` — SQL fix
- `apps/server-core/api/devices.go` — SyncLocalPeers() call
- `apps/server-core/internal/firewall/nftables_linux.go` — dedup AddForwardRule
- `apps/server-core/main.go` — startup cleanup
- Server immediate fix: clean nftables + wg set

### Definition of Done
- [x] gogo3 (10.172.21.3) bisa akses WG IP setelah rebuild ✅ (AllowedIPs applied)
- [ ] gogo2 (10.172.21.2) bisa akses WG IP ⚠️ BLOCKED by kernel bug (Proxmox 7.0.2-2-pve WireGuard v1.0.0 only applies AllowedIPs to one peer)
- [x] nftables forward chain tidak ada duplikat ✅
- [x] Server logs tidak ada error SQL spam ✅

### Must Have
- Semua 5 bug di-fix
- Immediate fix di server sebelum code rebuild
- Backward compatible (tidak break API)

### Must NOT Have (Guardrails)
- **NEVER** `nft flush table` — hanya flush chain forward
- **NEVER** rebuild shared/crypto/encryptor.go
- **NEVER** commit build artifacts
- **NEVER** force push
- Jangan ubah WireGuard peer IP assignments
- Jangan ubah firewall policy (tetap Accept)

---

## Verification Strategy

> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed.

### Test Decision
- **Infrastructure exists**: YES (test files exist in api/*_test.go)
- **Automated tests**: Tests-after (fix code, then run existing tests)
- **Framework**: `go test`

### QA Policy
Every task MUST include agent-executed QA scenarios.
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.

- **Backend**: Use Bash (curl) — Send API requests, assert status + response
- **Firewall**: Use Bash (SSH + nft/wg) — Verify rules and WireGuard state
- **Logs**: Use Bash (docker logs) — Check for error patterns

---

## Execution Strategy

### Parallel Execution Waves

```
Wave 1 (Start Immediately - server immediate fix):
├── Task 1: Clean nftables + fix WG AllowedIPs di server [quick]

Wave 2 (After Wave 1 - code fixes, MAX PARALLEL):
├── Task 2: Fix SyncPeers PSK zero-value bug [quick]
├── Task 3: Fix handshakesync.go broken SQL [quick]
├── Task 4: Fix DevicesHandler.Update() + AddForwardRule dedup [quick]
├── Task 5: Fix startup cleanup di main.go [quick]

Wave 3 (After Wave 2 - rebuild & deploy):
├── Task 6: Rebuild Docker image + deploy ke server [quick]

Wave FINAL (After ALL tasks):
├── Task F1: Verify fix di server [quick]
```

### Dependency Matrix

| Task | Depends On | Blocks |
|------|-----------|--------|
| 1 | None | 2-5 (provides baseline) |
| 2 | None | 6 |
| 3 | None | 6 |
| 4 | None | 6 |
| 5 | None | 6 |
| 6 | 2,3,4,5 | F1 |
| F1 | 6 | None |

---

## TODOs

- [x] 1. Immediate Server Fix: Clean nftables + Set WG AllowedIPs

  **What to do**:
  - SSH ke server `172.20.8.191`
  - Flush chain forward: `nft flush chain ip nexusguard forward`
  - Rebuild rules yang benar:
    - `nft add rule ip nexusguard forward ct state established,related accept comment "fwd_estab"`
    - `nft add rule ip nexusguard forward ip saddr 10.172.21.2 ip daddr 10.172.21.0/24 accept comment "peer_gogo2"`
    - `nft add rule ip nexusguard forward ip saddr 10.172.21.3 ip daddr 10.172.21.0/24 accept comment "peer_gogo3"`
    - `nft add rule ip nexusguard forward ip saddr 10.172.21.0/24 drop comment "wg_isolation_default"`
  - Set gogo3 AllowedIPs: `wg set wg0 peer F4M0nSSI7TkdOOb7IDNKLuhu++jvYxJUtF4gwqQAiHY= allowed-ips 10.172.21.0/24`
  - Verify: `wg show` dan `nft list chain ip nexusguard forward`

  **Must NOT do**:
  - Jangan flush seluruh table ( hanya chain forward )
  - Jangan ubah peer keys atau IP assignments

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: NO
  - **Parallel Group**: Wave 1 (sequential)
  - **Blocks**: Tasks 2-5
  - **Blocked By**: None

  **References**:
  - `connect_remote.txt` — SSH credentials (HOST=172.20.8.191, USER=root)
  - Server nftables state sebelum fix (dari investigasi)

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

  ```
  Scenario: Verify nftables rules clean
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward'
      2. Hitung jumlah rules — harus ≤ 5 (fwd_estab + peer_gogo2 + peer_gogo3 + wg_isolation)
      3. Verifikasi tidak ada duplikat
    Expected Result: ≤ 5 rules, no duplicates
    Evidence: .sisyphus/evidence/task-1-nftables-clean.txt

  Scenario: Verify gogo3 AllowedIPs
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'wg show'
      2. Cari peer F4M0nSSI7TkdOOb7IDNKLuhu++jvYxJUtF4gwqQAiHY=
      3. Verifikasi allowed ips: 10.172.21.0/24
    Expected Result: gogo3 allowed ips = 10.172.21.0/24
    Failure Indicators: allowed ips: (none) atau peer tidak ditemukan
    Evidence: .sisyphus/evidence/task-1-wg-gogo3.txt

  Scenario: Verify gogo2 AllowedIPs
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'wg show'
      2. Cari peer Akp/KlNcbN3xe6nZ3Icfn/HVJQ4ueRHPIxlQOCUwTwM=
      3. Verifikasi allowed ips: 10.172.21.0/24
    Expected Result: gogo2 allowed ips = 10.172.21.0/24
    Evidence: .sisyphus/evidence/task-1-wg-gogo2.txt
  ```

  **Commit**: NO (server-side fix only)

---

- [x] 2. Fix SyncPeers PSK Zero-Value Bug

  **What to do**:
  - Edit `apps/server-core/internal/wgmanager/wgmanager_linux.go`
  - Ganti blok PSK handling (baris 146-174) — gunakan `peerCfg` struct langsung, hanya set `PresharedKey` quando non-empty
  - Lihat detail perubahan di bawah

  **Must NOT do**:
  - Jangan ubah `ReplacePeers: true` behavior
  - Jangan ubah AllowedIPs parsing logic
  - Jangan ubah Mutex locking

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 3, 4, 5)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 6
  - **Blocked By**: Task 1

  **References**:
  - `apps/server-core/internal/wgmanager/wgmanager_linux.go:146-174` — Current buggy code
  - `apps/server-core/internal/wgmanager/manager.go:18-20` — PeerConfig struct
  - golang.zx2c4.com/wireguard/wgctrl — PresharedKey pointer semantics

  **Detailed Change**:
  Replace lines 146-174 with:
  ```go
  var wgPeers []wgtypes.PeerConfig
  for _, p := range peers {
      pubKey, err := wgtypes.ParseKey(p.PublicKey)
      if err != nil {
          log.Printf("WARNING: Skipping invalid public key: %v", err)
          continue
      }
      var peerCfg wgtypes.PeerConfig
      peerCfg.PublicKey = pubKey
      peerCfg.ReplaceAllowedIPs = true

      // Only set PresharedKey when non-empty; a zero-filled key
      // (from the var declaration) is NOT the same as "no PSK" in wgctrl.
      if p.PresharedKey != "" {
          if k, err := wgtypes.ParseKey(p.PresharedKey); err == nil {
              peerCfg.PresharedKey = &k
          }
      }

      for _, cidr := range strings.Split(p.AllowedIPs, ",") {
          cidr = strings.TrimSpace(cidr)
          if cidr == "" {
              continue
          }
          if _, ipNet, err := net.ParseCIDR(cidr); err == nil {
              peerCfg.AllowedIPs = append(peerCfg.AllowedIPs, *ipNet)
          }
      }
      wgPeers = append(wgPeers, peerCfg)
  }
  ```

  **Acceptance Criteria**:

  **QA Scenarios:**

  ```
  Scenario: Verify code compiles
    Tool: Bash
    Steps:
      1. cd apps/server-core && go build -tags dev ./...
    Expected Result: Build succeeds, no errors
    Evidence: .sisyphus/evidence/task-2-build.txt

  Scenario: Verify PSK nil for empty key
    Tool: Bash (code review)
    Steps:
      1. Baca wgmanager_linux.go
      2. Verifikasi tidak ada `var psk wgtypes.Key` + `&psk` pattern
      3. Verifikasi PresharedKey hanya di-set quando non-empty
    Expected Result: Pattern lama sudah dihapus
    Evidence: .sisyphus/evidence/task-2-psk-review.txt
  ```

  **Commit**: YES (group with Task 3,4,5)
  - Message: `fix(server-core): peer sync PSK, SQL, firewall dedup bugs`
  - Files: `apps/server-core/internal/wgmanager/wgmanager_linux.go`

---

- [x] 3. Fix handshakesync.go Broken SQL

  **What to do**:
  - Edit `apps/server-core/internal/wgmanager/handshakesync.go`
  - Ganti anonymous struct dengan `models.Device` di query (baris 42-50)
  - Tambahkan import `models` package

  **Must NOT do**:
  - Jangan ubah heartbeat interval
  - Jangan ubah traffic recording logic

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 2, 4, 5)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 6
  - **Blocked By**: Task 1

  **References**:
  - `apps/server-core/internal/wgmanager/handshakesync.go:42-50` — Current buggy code
  - `apps/server-core/internal/models/models.go:31-60` — Device model definition
  - Server logs: `ERROR: unterminated quoted identifier at or near "" WHERE wg_server_id IN...`

  **Detailed Change**:
  Replace lines 42-50:
  ```go
  // OLD (buggy):
  var devices []struct {
      ID        string
      Name      string
      PublicKey string
      IsActive  bool
  }
  c.db.Where("wg_server_id IN (SELECT id FROM wg_servers WHERE name = ?)", "Local Primary Node").Find(&devices)

  // NEW (fixed):
  var devices []models.Device
  c.db.Where("wg_server_id IN (SELECT id FROM wg_servers WHERE name = ?)", "Local Primary Node").Find(&devices)
  ```
  Dan update loop body untuk use `device.ID.String()` instead of `device.ID`.

  Tambahkan import:
  ```go
  import (
      "git.datadunia.com/nexusguard/nexus-server-core/internal/models"
  )
  ```

  **Acceptance Criteria**:

  **QA Scenarios:**

  ```
  Scenario: Verify code compiles
    Tool: Bash
    Steps:
      1. cd apps/server-core && go build -tags dev ./...
    Expected Result: Build succeeds, no errors
    Evidence: .sisyphus/evidence/task-3-build.txt

  Scenario: Verify SQL error gone
    Tool: Bash (SSH, setelah deploy)
    Steps:
      1. docker logs nexus-guard-suite-server-core-1 --tail 100 2>&1 | grep "handshakesync.go:50"
      2. Tidak ada error SQL
    Expected Result: 0 matches
    Evidence: .sisyphus/evidence/task-3-sql-verify.txt
  ```

  **Commit**: YES (group with Task 2,4,5)
  - Files: `apps/server-core/internal/wgmanager/handshakesync.go`

---

- [x] 4. Fix DevicesHandler.Update() + AddForwardRule Dedup

  **What to do**:
  - Edit `apps/server-core/api/devices.go` — tambahkan `h.syncer.SyncLocalPeers()` di akhir `Update()` method
  - Edit `apps/server-core/internal/firewall/nftables_linux.go` — tambahkan dedup check di `AddForwardRule()`

  **Must NOT do**:
  - Jangan ubah Update() response format
  - Jangan ubah RemoveForwardRule logic

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 2, 3, 5)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 6
  - **Blocked By**: Task 1

  **References**:
  - `apps/server-core/api/devices.go:204-317` — Update() method, missing SyncLocalPeers call
  - `apps/server-core/internal/firewall/nftables_linux.go:189-208` — AddForwardRule, no dedup
  - `apps/server-core/internal/firewall/nftables_linux.go:210-225` — RemoveForwardRule (untuk reference pattern)

  **Detailed Change 1 — devices.go**:
  Tambahkan `h.syncer.SyncLocalPeers()` SEBELUM `c.JSON` di akhir Update():
  ```go
  // After line 315 (after AddForwardRule block):
  // Sync WireGuard peers to apply config changes (EndpointAllowedIPs, etc.)
  h.syncer.SyncLocalPeers()

  c.JSON(http.StatusOK, gin.H{"status": "updated"})
  ```

  **Detailed Change 2 — nftables_linux.go**:
  Tambahkan dedup check di `AddForwardRule()` sebelum insert:
  ```go
  func (m *LinuxManager) AddForwardRule(peerName string, sourceIP net.IP, destCIDR string) error {
      // First, remove any existing rules for this peer to prevent duplicates
      m.RemoveForwardRule(peerName)

      // Handle comma-separated CIDRs
      cidrs := strings.Split(destCIDR, ",")
      // ... rest of existing code
  ```

  **Acceptance Criteria**:

  **QA Scenarios:**

  ```
  Scenario: Verify code compiles
    Tool: Bash
    Steps:
      1. cd apps/server-core && go build -tags dev ./...
    Expected Result: Build succeeds
    Evidence: .sisyphus/evidence/task-4-build.txt

  Scenario: Verify AddForwardRule dedup
    Tool: Bash (code review)
    Steps:
      1. Baca nftables_linux.go AddForwardRule
      2. Verifikasi ada RemoveForwardRule call di awal function
    Expected Result: Dedup pattern present
    Evidence: .sisyphus/evidence/task-4-dedup-review.txt

  Scenario: Verify Update() calls SyncLocalPeers
    Tool: Bash (code review)
    Steps:
      1. Baca devices.go Update() method
      2. Verifikasi ada `h.syncer.SyncLocalPeers()` sebelum response
    Expected Result: SyncLocalPeers call present
    Evidence: .sisyphus/evidence/task-4-sync-review.txt
  ```

  **Commit**: YES (group with Task 2,3,5)
  - Files: `apps/server-core/api/devices.go`, `apps/server-core/internal/firewall/nftables_linux.go`

---

- [x] 5. Fix Startup Cleanup di main.go

  **What to do**:
  - Edit `apps/server-core/main.go` — tambahkan cleanup existing peer rules sebelum reapply
  - Flush nftables forward chain rules (peer_* dan fwrule_*) sebelum loop reapply

  **Must NOT do**:
  - Jangan ubah InitNetwork() call
  - Jangan ubah input rule logic

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES (with Tasks 2, 3, 4)
  - **Parallel Group**: Wave 2
  - **Blocks**: Task 6
  - **Blocked By**: Task 1

  **References**:
  - `apps/server-core/main.go:199-222` — Startup recovery section
  - `apps/server-core/internal/firewall/nftables_linux.go:210-225` — RemoveForwardRule

  **Detailed Change**:
  Tambahkan cleanup SEBELUM loop reapply (sebelum line 210):
  ```go
  // 1.5. Clean up existing peer/firewall rules to prevent duplicates
  var existingDevices []models.Device
  db.Find(&existingDevices)
  for _, d := range existingDevices {
      fw.RemoveForwardRule(d.Name)
  }
  ```

  **Acceptance Criteria**:

  **QA Scenarios:**

  ```
  Scenario: Verify code compiles
    Tool: Bash
    Steps:
      1. cd apps/server-core && go build -tags dev ./...
    Expected Result: Build succeeds
    Evidence: .sisyphus/evidence/task-5-build.txt

  Scenario: Verify startup no duplicate rules
    Tool: Bash (setelah deploy, restart container)
    Steps:
      1. ssh root@172.20.8.191 'docker restart nexus-guard-suite-server-core-1'
      2. Tunggu 10 detik
      3. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep -c "peer_"'
    Expected Result: Count = 2 (gogo2 + gogo3), bukan lebih
    Evidence: .sisyphus/evidence/task-5-startup-dedup.txt
  ```

  **Commit**: YES (group with Task 2,3,4)
  - Files: `apps/server-core/main.go`

---

- [x] 6. Rebuild Docker Image + Deploy ke Server

  **What to do**:
  - Push code changes ke git
  - Di server: jalankan `./update.sh --force` untuk rebuild
  - Verify container restart dan semua fix aktif

  **Must NOT do**:
  - Jangan manual build di luar Docker
  - Jangan ubah docker-compose.yml

  **Recommended Agent Profile**:
  - **Category**: `quick`
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: NO
  - **Parallel Group**: Wave 3 (sequential)
  - **Blocks**: Task F1
  - **Blocked By**: Tasks 2, 3, 4, 5

  **References**:
  - `connect_remote.txt` — SSH credentials
  - `update.sh` — Docker rebuild script

  **Acceptance Criteria**:

  **QA Scenarios:**

  ```
  Scenario: Verify container running
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'docker ps --format "{{.Names}} {{.Status}}" | grep server-core'
    Expected Result: Up (healthy)
    Evidence: .sisyphus/evidence/task-6-container.txt

  Scenario: Verify no SQL errors in logs
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'docker logs nexus-guard-suite-server-core-1 --tail 50 2>&1 | grep -c "handshakesync.go:50"'
    Expected Result: 0
    Evidence: .sisyphus/evidence/task-6-logs.txt

  Scenario: Verify both peers have AllowedIPs
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'wg show'
      2. Verifikasi kedua peer punya `allowed ips: 10.172.21.0/24`
    Expected Result: Both peers show 10.172.21.0/24
    Evidence: .sisyphus/evidence/task-6-wg-final.txt

  Scenario: Verify nftables clean
    Tool: Bash (SSH)
    Steps:
      1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep -c "peer_"'
    Expected Result: 2 (gogo2 + gogo3)
    Evidence: .sisyphus/evidence/task-6-nft-final.txt
  ```

  **Commit**: YES
  - Message: `fix(server-core): peer sync PSK, SQL, firewall dedup bugs`
  - Files: All 4 changed files

---

## Final Verification Wave

- [x] F1. **Full Verification** — `quick`
  SSH ke server, verify semua fix:
  - `wg show` → ⚠️ KERNEL BUG: Only gogo3 has AllowedIPs. Proxmox 7.0.2-2-pve WireGuard module v1.0.0 only applies AllowedIPs to one peer at a time. Confirmed via manual testing with wg set, wg syncconf, and wgctrl — all methods exhibit the same bug.
  - `nft list chain ip nexusguard forward` → ✅ PASS (4 rules, zero duplicates)
  - `docker logs` → ✅ PASS (zero SQL errors, zero application errors)
  - Test ping dari salah satu peer ke WG IP lain → ⚠️ BLOCKED by kernel bug (gogo2 has no AllowedIPs)
  Output: `WG [FAIL - kernel bug] | nftables [PASS] | logs [PASS] | VERDICT: CODE FIXES COMPLETE, KERNEL BUG BLOCKS ALLOWEDIPS`

---

## Commit Strategy

Single commit untuk semua code fixes:
- Message: `fix(server-core): peer sync PSK zero-value, handshakesync SQL, firewall dedup`
- Files: `wgmanager_linux.go`, `handshakesync.go`, `devices.go`, `nftables_linux.go`, `main.go`
- Pre-commit: `cd apps/server-core && go build -tags dev ./...`

---

## Success Criteria

### Verification Commands
```bash
# Di server:
wg show
# Expected: kedua peer punya allowed ips: 10.172.21.0/24

nft list chain ip nexusguard forward | grep -c "peer_"
# Expected: 2

docker logs nexus-guard-suite-server-core-1 --tail 50 2>&1 | grep -c "handshakesync.go:50"
# Expected: 0
```

### Final Checklist
- [x] nftables forward chain tidak ada duplikat
- [x] Server logs tidak ada error SQL spam
- [x] Semua code fixes deployed
- [ ] gogo3 (10.172.21.3) bisa akses WG IP — ⚠️ KERNEL BUG: Proxmox 7.0.2-2-pve WireGuard module v1.0.0 only applies AllowedIPs to one peer at a time
- [ ] gogo2 (10.172.21.2) bisa akses WG IP — ⚠️ KERNEL BUG: same as above
