Skip to content

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

  1. 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
  2. The FORWARD rule jumps to VM_EGRESS for this traffic
  3. VM_EGRESS processes rules top-to-bottom
  4. Already-established connections are always allowed (stateful tracking)
  5. ICMP (ping) is always allowed for diagnostics
  6. User-defined rules are checked in order
  7. Everything else is logged with VM_BLOCKED: prefix and dropped

Killswitch Mode

When the killswitch is activated, the chain is simplified to:

VM_EGRESS chain
  ├── LOG (VM_KILLED: prefix, 10/min rate limit)
  └── DROP

No established connections, no ICMP, no user rules — everything is dropped.

Firewall Disabled Mode

When the firewall is toggled off:

VM_EGRESS chain
  └── ACCEPT

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

  1. Ensure VM_EGRESS chain exists
  2. Flush all rules in the chain (-F VM_EGRESS)
  3. Based on mode:
    • Killswitch: LOG + DROP
    • Disabled: ACCEPT
    • Normal: Build rules from config
  4. 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:

data.pop("ssh_forward", None)  # Strip legacy field