Network Isolation
The guest VM has no real network interface. DNS and HTTPS are redirected to guest-side proxy binaries, forwarded to host handlers over vsock, lifted into typed Security Events, checked by the Security Engine, and logged through the resolved-event path.
Air-gapped architecture
Section titled “Air-gapped architecture”graph LR
subgraph "Guest VM"
APP["Application (curl, pip, npm)"]
DNS["capsem-dns-proxy<br/>UDP/TCP :1053"]
IPT["iptables REDIRECT<br/>:443 -> :10443"]
NP["capsem-net-proxy<br/>TCP:10443"]
end
subgraph "Host"
HDNS["DNS Proxy<br/>SecurityEvent + upstream resolver"]
MITM["MITM Proxy<br/>TLS termination + SecurityEvent"]
UP["Upstream server"]
end
APP -->|DNS :53| DNS
DNS -->|vsock:5007| HDNS
HDNS -->|allowed query| UP
APP -->|HTTPS :443| IPT
IPT -->|TCP :10443| NP
NP -->|vsock:5002| MITM
MITM -->|TLS| UP
No packets leave the VM through a NIC. DNS reaches the host only through vsock port 5007, and HTTPS reaches the host only through vsock port 5002.
Guest network setup
Section titled “Guest network setup”capsem-init builds the air-gapped network stack during boot:
| Step | Command | Purpose |
|---|---|---|
| 1. Loopback | ip link set lo up | Enable localhost |
| 2. Dummy NIC | ip link add dummy0 type dummy | Create fake interface |
| 3. Assign IP | ip addr add 10.0.0.1/24 dev dummy0 | Give it a local address |
| 4. Default route | ip route add default dev dummy0 | All traffic routes to dummy0 |
| 5. DNS redirect | iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-port 1053 plus TCP | Send DNS to capsem-dns-proxy |
| 6. HTTPS redirect | iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 10443 | Redirect HTTPS to proxy |
| 7. Net proxy | capsem-net-proxy | TCP:10443 to vsock:5002 bridge |
| 8. DNS proxy | capsem-dns-proxy | UDP/TCP :1053 to vsock:5007 bridge |
The result: when an application resolves github.com, the query is captured on port 53, handled by capsem-dns-proxy, and resolved or denied by the host DNS handler. When an application connects to github.com:443, iptables redirects the socket to 127.0.0.1:10443; capsem-net-proxy bridges the TCP connection to the host over vsock port 5002.
MITM proxy overview
Section titled “MITM proxy overview”The host MITM proxy receives each connection on vsock:5002 and runs a full inspection pipeline:
graph TD
A["vsock:5002 connection"] --> B["TLS ClientHello<br/>extract SNI domain"]
B --> C["Complete TLS handshake<br/>mint leaf cert for domain"]
C --> D["Parse HTTP request<br/>method + path + headers"]
D --> E["Build http.request SecurityEvent"]
E --> F{"Security Engine decision"}
F -->|block/ask| G["Return denial<br/>emit resolved event"]
F -->|rewrite| H["Validate/apply mutation"]
F -->|allow| I["Forward to upstream<br/>real TLS connection"]
H --> I
I --> J["Stream response<br/>to guest"]
J --> K["Emit resolved event<br/>and telemetry projections"]
The proxy mints per-domain TLS certificates signed by a static Capsem CA (ECDSA P-256, 24-hour validity). The CA is baked into the guest rootfs and trusted by the system certificate store, Python certifi, and Node.js. See MITM Proxy Architecture for implementation details.
CA trust chain
Section titled “CA trust chain”| Component | How it trusts the CA |
|---|---|
| System store | /usr/local/share/ca-certificates/capsem-ca.crt + update-ca-certificates |
| Python certifi | Patched bundle includes Capsem CA |
| Node.js | NODE_EXTRA_CA_CERTS env var |
| curl/wget | SSL_CERT_FILE env var |
| pip/requests | REQUESTS_CA_BUNDLE env var |
Profile-Owned Enforcement
Section titled “Profile-Owned Enforcement”Users customize network behavior through Profile V2 capabilities and profile-owned enforcement rules, not standalone network allow/block files:
[security.rules.http.allow_internal]on = "http.request"if = 'http.request.host.endsWith(".internal.corp.com")'decision = "allow"priority = 10
[security.rules.http.block_bad]on = "http.request"if = 'http.request.host == "malware.bad.com"'decision = "block"priority = 10Corporate profiles can lock the relevant profile sections so user profile forks cannot weaken network enforcement.
There is no migrated default allow/block list. Hosts that should be reachable must be represented by explicit profile rules, generated package/provider rules, or system catch-alls derived from profile capabilities.
HTTP and DNS Enforcement
Section titled “HTTP and DNS Enforcement”The Network Engine lifts HTTP and DNS activity into Security Events. The Security Engine evaluates profile-owned enforcement rules over canonical roots.
[security.rules.http.block_repo_writes]on = "http.request"if = 'http.request.host == "github.com" && http.request.method == "POST" && http.request.path.startsWith("/openai/")'decision = "block"priority = 10
[security.rules.dns.block_ai_provider]on = "dns.request"if = 'dns.request.qname == "api.openai.com" && dns.request.qtype == "A"'decision = "block"priority = 10HTTP rewrite rules can strip request or response headers before they leave
the boundary or appear in telemetry. DNS rewrite rules synthesize configured
answers without upstream resolution.
See Rule Authoring for the full rule reference.
Telemetry
Section titled “Telemetry”Every proxied request is logged to the per-VM session.db:
| Column | Content |
|---|---|
domain | Target domain |
method | HTTP method |
path | Request path |
status_code | Upstream response status |
decision | allowed, denied, or error |
bytes_sent | Request body size |
bytes_received | Response body size |
duration_ms | End-to-end latency |
request_body_preview | First 4 KB of request body |
response_body_preview | First 4 KB of response body |
matched_rule | Which Security Engine rule matched |
For AI provider traffic (Anthropic, OpenAI, Google), the proxy also parses SSE streams to extract model calls, token usage, tool calls, and estimated cost. See Session Telemetry for the full schema.
DNS queries are logged separately in dns_events with qname, qtype,
rcode, decision, matched_rule, process_name, and trace_id.
What gets blocked
Section titled “What gets blocked”| Scenario | Outcome | Why |
|---|---|---|
HTTPS to a domain with no allowing rule (example.com) | 403 Forbidden | Profile catch-all denies the event |
HTTPS to blocked domain (api.openai.com) | 403 Forbidden | Profile enforcement rule blocks |
HTTP port 80 (http://google.com) | Connection refused | Only port 443 is redirected |
Non-standard port (https://google.com:8443) | Connection refused | Only port 443 is redirected |
Direct IP (https://1.1.1.1) | Connection refused | No real NIC; dummy0 has no real route |
| POST to allowed domain with block rule | 403 Forbidden | Security Engine rule blocks the method |
capsem-doctor validation
Section titled “capsem-doctor validation”Network isolation is validated by test_network.py across 7 layers. Tests are ordered low-to-high so failures pinpoint the exact broken layer.
| Layer | Tests | What it validates |
|---|---|---|
| L1: Guest plumbing | test_dummy0_has_ip, test_dns_proxy_listening_udp, test_dns_proxy_listening_tcp, test_iptables_redirect_dns_udp_to_1053, test_iptables_redirect_dns_tcp_to_1053, test_dns_resolves_via_capsem_proxy, test_dns_nxdomain_propagates_from_upstream, test_iptables_redirect_443_to_10443 | dummy0 has 10.0.0.1, DNS is captured by capsem-dns-proxy, real upstream answers and NXDOMAIN propagate, HTTPS redirect rule is present |
| L2: Net proxy | test_net_proxy_listening, test_tcp_443_reaches_proxy, test_vsock_bridge_delivers_bytes | capsem-net-proxy accepts TCP on :10443, iptables redirect works, bytes flow through vsock bridge |
| L3: TLS handshake | test_tls_handshake_completes, test_tls_cert_from_capsem_ca | Full TLS to allowed domain succeeds, MITM proxy presents Capsem CA cert |
| L4: HTTP over MITM | test_curl_https_with_skip_verify, test_curl_verbose_diagnostics | curl -k gets HTTP response, full handshake trace captured |
| L5: CA trust | test_mitm_ca_cert_file_exists, test_mitm_ca_in_system_bundle, test_certifi_includes_capsem_ca, test_curl_allowed_domain_ca_trusted, test_python_urllib_https_trusted, test_ca_env_var_set | CA cert file exists, in system bundle, in Python certifi, curl works without -k, Python TLS works, SSL_CERT_FILE/REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS set |
| L6: Enforcement | test_denied_domain_rejected, test_post_to_random_domain_denied, test_ai_provider_domain_blocked, test_http_port_80_not_proxied, test_non_standard_port_fails, test_direct_ip_no_route | Denied domains get 403, port 80 fails, non-443 ports fail, direct IP fails |
| L7: Throughput | test_proxy_download_throughput | 100 MB download through MITM meets minimum speed threshold |
Additional network tests in test_sandbox.py:
| Test | Property |
|---|---|
test_dummy_interface_exists | dummy0 interface present |
test_dns_resolves_via_capsem_proxy | DNS resolves through the host proxy, not the old local sentinel |
test_iptables_redirect | REDIRECT rule active |
test_net_proxy_running | capsem-net-proxy process alive |
test_dns_proxy_running | capsem-dns-proxy process alive |
| legacy DNS daemon check | Retired DNS service is absent |
test_no_real_nics | Only lo and dummy0 in /sys/class/net/ |
test_allowed_domain | End-to-end HTTPS to allowed domain (5-step diagnostic) |
test_denied_domain | HTTPS to denied domain returns 403 or refused |