Firewall Engine¶
The Control Center manages an iptables chain called VM_EGRESS that controls all egress traffic from the AI sandbox VM.
How It Works¶
Chain Architecture¶
FORWARD chain (kernel)
└── Rule: -s 192.168.10.10 -i virbr0 ! -d 192.168.10.0/24 -j VM_EGRESS
└── VM_EGRESS chain (managed by Control Center)
├── ESTABLISHED,RELATED → ACCEPT
├── ICMP → ACCEPT
├── User rule 1 → ACCEPT
├── User rule 2 → ACCEPT
├── ...
├── LOG (VM_BLOCKED: prefix, 10/min rate limit)
└── DROP
Traffic Flow¶
- All traffic from the VM (
192.168.10.10) going to destinations outside the VM subnet (192.168.10.0/24) hits the FORWARD chain - The FORWARD rule jumps to
VM_EGRESSfor this traffic VM_EGRESSprocesses rules top-to-bottom- Already-established connections are always allowed (stateful tracking)
- ICMP (ping) is always allowed for diagnostics
- User-defined rules are checked in order
- Everything else is logged with
VM_BLOCKED:prefix and dropped
Killswitch Mode¶
When the killswitch is activated, the chain is simplified to:
No established connections, no ICMP, no user rules — everything is dropped.
Firewall Disabled Mode¶
When the firewall is toggled off:
All traffic passes through.
Implementation¶
Key Functions (app/services/iptables.py)¶
| Function | Purpose |
|---|---|
ipt(args, table) |
Execute iptables command with --wait 5 |
chain_exists() |
Check if VM_EGRESS chain exists |
ensure_chain() |
Create VM_EGRESS chain if missing |
forward_rule_exists() |
Verify FORWARD → VM_EGRESS jump exists |
ensure_forward_rule() |
Insert FORWARD jump at position 1 |
apply_rules(config) |
Flush and rebuild VM_EGRESS from config |
get_blocked_entries(limit) |
Parse dmesg/log for blocked traffic |
Rule Application Process¶
- Ensure
VM_EGRESSchain exists - Flush all rules in the chain (
-F VM_EGRESS) - Based on mode:
- Killswitch: LOG + DROP
- Disabled: ACCEPT
- Normal: Build rules from config
- Ensure FORWARD jump rule exists
Health Loop¶
A background task runs every 60 seconds to verify the FORWARD jump rule exists. If it was removed (e.g., by Docker network changes), it's automatically re-inserted:
async def _health_loop():
while True:
await asyncio.sleep(60)
try:
if not forward_rule_exists():
cfg = load_config()
apply_rules(cfg)
except Exception:
pass
Configuration Persistence¶
Rules are stored in /app/data/rules.json:
{
"enabled": true,
"killswitch": false,
"rules": [
{
"id": 1,
"description": "DNS to gateway (UDP)",
"protocol": "udp",
"port": 53,
"destination": "192.168.10.1",
"enabled": true,
"locked": true
}
]
}
The locked flag prevents deletion of essential rules (DNS) via the UI.
Legacy Compatibility¶
The config loader handles legacy config files that contain an ssh_forward field from the old Control Center version: