# Gaol Networking Guide

## Network Archetypes — Pick One

When creating a container, choose the archetype that matches your use case.
Don't configure low-level networking manually — use an archetype.

| Archetype | Description | Isolation | Internet | Use case |
|---|---|---|---|---|
| `host` | Shares host network | None | Direct | Dev containers, local services |
| `shared` | NAT through host veth | Basic | Via host NAT | Default, general purpose |
| `bridged` | Own IP on LAN bridge | Network-level | Direct (own IP) | Services addressable on LAN |
| `tailscale` | Mesh via Tailscale | Overlay | Via tailnet | Remote access, cross-host |
| `vpn-exit` | Private veth → VPN | Strong | Via VPN only | Privacy-sensitive services |
| `tor-vpn-exit` | Private veth → Tor → VPN | Maximum | Via Tor+VPN | High-privacy workloads |

## How Each Archetype Works

### host

Container shares the host's network namespace. No network isolation.
The container sees all host interfaces and can bind to any port.

```yaml
network:
  mode: host
```

nspawn config: `VirtualEthernet=no` or `Private=no`

### shared (default)

Container gets a virtual ethernet pair (veth). Traffic is NAT'd through the host.
This is the default nspawn behavior.

```yaml
network:
  mode: shared
```

nspawn config: `VirtualEthernet=yes`

Inside the container, the interface is `host0`. systemd-networkd assigns an address
via DHCP from the host's systemd-networkd.

### bridged

Container's veth is attached to a host bridge (typically `br0`). The container
gets its own IP on the LAN via DHCP or static assignment.

```yaml
network:
  mode: bridged
  bridge: br0
```

nspawn config: `VirtualEthernet=yes`, `Bridge=br0`

### vpn-exit

Container uses a private veth pair with a dedicated /30 subnet. All traffic routes
through a companion VPN container. A kill-switch drops traffic if the VPN goes down.

**Topology:**
```
container (10.200.x.2) ──veth──> host (10.200.x.1) ──nftables forward──> vpn-container ──> internet
```

**What gets created:**
- Private veth pair with /30 subnet (auto-allocated from 10.200.0.0/16 pool)
- Host-side networkd `.network` file for the veth
- Container-side networkd config (static IP, gateway = host veth)
- nftables table with forwarding + masquerade rules
- Kill-switch (nftables OUTPUT DROP default policy inside container)
- Policy routing (ip rule + ip route in separate routing table)
- DNS pointing to VPN provider's resolver

### tor-vpn-exit

Same as vpn-exit but adds a Tor hop between the container and the VPN.

**Topology:**
```
container (10.200.x.2) ──veth──> host ──> tor-container (SOCKS+DNS) ──> vpn-container ──> internet
```

**Additional components:**
- Tor companion container running SOCKS proxy and DNSPort on port 53
- Container DNS points to Tor container IP (direct, no DNAT needed)
- Second private veth pair for tor → vpn hop
- Kill-switch on both the main container and the tor container

---

## DNS Configuration

### The #1 agent mistake: broken DNS

Most DNS problems come from containers having `nameserver 127.0.0.53` in
`/etc/resolv.conf` (systemd-resolved stub) when resolved isn't configured.

**Quick fix — bypass resolved entirely:**
```bash
rm /etc/resolv.conf    # remove symlink
echo 'nameserver 1.1.1.1' > /etc/resolv.conf
```

**Proper fix — configure resolved:**
```ini
# /etc/systemd/resolved.conf
[Resolve]
DNS=1.1.1.1
```
Then `systemctl restart systemd-resolved`.

### DNS for each archetype

| Archetype | DNS target | Configuration |
|---|---|---|
| `host` | Host's DNS | Inherited, nothing to do |
| `shared` | Host resolver or public | `nameserver 1.1.1.1` in resolv.conf |
| `bridged` | LAN DNS or public | Via DHCP or static resolv.conf |
| `vpn-exit` | VPN provider DNS | resolv.conf → VPN DNS server IP |
| `tor-vpn-exit` | Tor DNSPort | resolv.conf → tor container IP (port 53 directly) |

**Critical:** For chain archetypes, the DNS server must be reachable through
the chain. Don't use `1.1.1.1` when traffic only routes through Tor — use
the Tor container's DNSPort instead.

---

## nftables (Not iptables)

Gaol uses nftables for all firewall rules. Do NOT use `iptables` commands.

### Key concepts

- **Atomic loading**: `nft -f ruleset.nft` loads all rules at once (no partial state)
- **Named tables**: Each archetype gets its own table (e.g., `ip gaol_mychain`)
- **Clean teardown**: `nft delete table ip gaol_mychain` removes everything instantly
- **No conflicts**: Named tables are isolated from each other

### Kill-switch pattern

```nft
table ip gaol_mycontainer {
    chain output {
        type filter hook output priority 0; policy drop;
        oif "lo" accept
        ip daddr 10.200.7.1 accept          # allow gateway
        ct state established,related accept  # allow responses
        # everything else dropped
    }
}
```

### Chain forwarding pattern

```nft
table ip gaol_mychain {
    chain forward {
        type filter hook forward priority 0; policy drop;
        iif "ve-mycontainer" accept                         # container → upstream
        oif "ve-mycontainer" ct state established,related accept  # responses back
    }
    chain postrouting {
        type nat hook postrouting priority 100;
        ip saddr 10.200.7.0/30 masquerade                  # NAT container traffic
    }
}
```

---

## nspawn Networking Gotchas

### 1. networkd must be explicitly enabled

Debootstrapped containers don't have systemd-networkd enabled:
```bash
systemctl enable --now systemd-networkd
```
Without this, the container gets no IP address on its veth.

### 2. Override filenames must match exactly

The builtin `/usr/lib/systemd/network/80-container-host0.network` configures DHCP.
To override with static config, use the **exact same filename**:
```
/etc/systemd/network/80-container-host0.network  ← CORRECT (overrides)
/etc/systemd/network/80-host0.network            ← WRONG (sorts differently, loses)
```

### 3. Container names ≤ 12 chars for predictable veth names

systemd derives veth names from container names. Names > 12 chars get hashed,
making them unpredictable in nftables rules and networkd configs. Keep container
names short.

### 4. systemd-networkd re-adds masquerade entries

When networkd reloads, it re-adds `io.systemd.nat` masquerade entries in nftables
that conflict with chain routing. Chain setup scripts must remove these after
every route setup.

### 5. Missing packages in minimal containers

`debootstrap` doesn't include:
- `ca-certificates` — needed for HTTPS (curl, apt over https)
- `openresolv` — needed for `wg-quick` DNS management
- `systemd-resolved` — may not be installed; manage resolv.conf directly

### 6. VPN containers on br0 still need networkd

Even bridged VPN containers need systemd-networkd enabled with a DHCP config
for host0, or they won't get an IP.

---

## Policy Routing (Chain Networking)

Chain archetypes use policy routing to direct traffic through specific paths
without affecting the host's default routing:

```bash
# Rule: traffic from container subnet uses routing table 200
ip rule add from 10.200.1.2 lookup 200 priority 100

# Rule: traffic arriving on container's veth uses table 200
ip rule add iif ve-mycontainer lookup 200 priority 100

# Table 200 has a single default route through the next chain hop
ip route add default via 10.200.2.1 table 200
```

**Important:** Use `iif` (incoming interface) in ip rules to only match
forwarded traffic from the container, not traffic originating on the host.

Each chain hop gets its own routing table number (200, 201, 202, etc.).
These are tracked in the subnet allocator to avoid conflicts.

---

## Diagnosing Network Issues

Use the `gaol_network_check` MCP tool:
```
gaol_network_check(name="mycontainer", runtime="nspawn")
```

### What it checks
- Interface addresses and default route
- DNS configuration (resolv.conf contents)
- DNS resolution test (resolves example.com)
- Gateway reachability (ping)
- Internet connectivity and exit IP (optional)

### Common diagnostic patterns

| Symptom | Likely cause | Fix |
|---|---|---|
| No IP address | networkd not running | `systemctl enable --now systemd-networkd` |
| No default route | networkd misconfigured | Check /etc/systemd/network/*.network files |
| DNS timeout | resolv.conf → 127.0.0.53, resolved not running | Write `nameserver 1.1.1.1` to resolv.conf |
| Can ping gateway, no internet | ip_forward disabled or nftables blocking | `sysctl net.ipv4.ip_forward=1`, check nft rules |
| Wrong exit IP | VPN not connected or routing bypassed | Check VPN status, verify policy routing table |
| Connection refused | Kill-switch active, service not started | Check `nft list ruleset`, verify service is running |
| HTTPS fails but HTTP works | Missing ca-certificates | `apt install ca-certificates` |

---

## API Reference

### Network Modes

Defined in `src/gaol/models/container.py`:
- `shared` (default): NAT through host (`VirtualEthernet=yes`)
- `bridged`: Direct network access with own IP (`Bridge=br0`)
- `host`: Share host network namespace (`Private=no`)
- `none`: No networking (`Private=yes`)

### Port Mapping

Format: `["8080:80", "53:53/udp", "0.0.0.0:8443:443/tcp"]`

### VPN Configuration

```yaml
privacy:
  vpn:
    enabled: true
    provider: wireguard    # or openvpn
    config_file: ~/mullvad-wg.conf
    kill_switch: true
    dns_servers: [10.64.0.1]
```

### Tor Configuration

```yaml
privacy:
  tor:
    enabled: true
    transparent: true        # route ALL traffic through Tor
    exit_nodes: [SE, DE]
    exclude_nodes: [RU, CN]
    socks_port: 9050
    dns_port: 5353
```

### Hidden Services

```yaml
privacy:
  onion:
    enabled: true
    port: 80
    onion_port: 80
    version: 3
```

### Firewall Rules

nftables backend. Supports forwarding rules:

| Field | Description |
|---|---|
| `host_port` | Port on host |
| `guest_ip` | Container IP |
| `guest_port` | Port in container |
| `protocol` | TCP or UDP |
| `host_ip` | Bind address (default: `0.0.0.0`) |
