Skip to content

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.

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.

capsem-init builds the air-gapped network stack during boot:

StepCommandPurpose
1. Loopbackip link set lo upEnable localhost
2. Dummy NICip link add dummy0 type dummyCreate fake interface
3. Assign IPip addr add 10.0.0.1/24 dev dummy0Give it a local address
4. Default routeip route add default dev dummy0All traffic routes to dummy0
5. DNS redirectiptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-port 1053 plus TCPSend DNS to capsem-dns-proxy
6. HTTPS redirectiptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 10443Redirect HTTPS to proxy
7. Net proxycapsem-net-proxyTCP:10443 to vsock:5002 bridge
8. DNS proxycapsem-dns-proxyUDP/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.

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.

ComponentHow it trusts the CA
System store/usr/local/share/ca-certificates/capsem-ca.crt + update-ca-certificates
Python certifiPatched bundle includes Capsem CA
Node.jsNODE_EXTRA_CA_CERTS env var
curl/wgetSSL_CERT_FILE env var
pip/requestsREQUESTS_CA_BUNDLE env var

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 = 10

Corporate 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.

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 = 10

HTTP 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.

Every proxied request is logged to the per-VM session.db:

ColumnContent
domainTarget domain
methodHTTP method
pathRequest path
status_codeUpstream response status
decisionallowed, denied, or error
bytes_sentRequest body size
bytes_receivedResponse body size
duration_msEnd-to-end latency
request_body_previewFirst 4 KB of request body
response_body_previewFirst 4 KB of response body
matched_ruleWhich 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.

ScenarioOutcomeWhy
HTTPS to a domain with no allowing rule (example.com)403 ForbiddenProfile catch-all denies the event
HTTPS to blocked domain (api.openai.com)403 ForbiddenProfile enforcement rule blocks
HTTP port 80 (http://google.com)Connection refusedOnly port 443 is redirected
Non-standard port (https://google.com:8443)Connection refusedOnly port 443 is redirected
Direct IP (https://1.1.1.1)Connection refusedNo real NIC; dummy0 has no real route
POST to allowed domain with block rule403 ForbiddenSecurity Engine rule blocks the method

Network isolation is validated by test_network.py across 7 layers. Tests are ordered low-to-high so failures pinpoint the exact broken layer.

LayerTestsWhat it validates
L1: Guest plumbingtest_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_10443dummy0 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 proxytest_net_proxy_listening, test_tcp_443_reaches_proxy, test_vsock_bridge_delivers_bytescapsem-net-proxy accepts TCP on :10443, iptables redirect works, bytes flow through vsock bridge
L3: TLS handshaketest_tls_handshake_completes, test_tls_cert_from_capsem_caFull TLS to allowed domain succeeds, MITM proxy presents Capsem CA cert
L4: HTTP over MITMtest_curl_https_with_skip_verify, test_curl_verbose_diagnosticscurl -k gets HTTP response, full handshake trace captured
L5: CA trusttest_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_setCA 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: Enforcementtest_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_routeDenied domains get 403, port 80 fails, non-443 ports fail, direct IP fails
L7: Throughputtest_proxy_download_throughput100 MB download through MITM meets minimum speed threshold

Additional network tests in test_sandbox.py:

TestProperty
test_dummy_interface_existsdummy0 interface present
test_dns_resolves_via_capsem_proxyDNS resolves through the host proxy, not the old local sentinel
test_iptables_redirectREDIRECT rule active
test_net_proxy_runningcapsem-net-proxy process alive
test_dns_proxy_runningcapsem-dns-proxy process alive
legacy DNS daemon checkRetired DNS service is absent
test_no_real_nicsOnly lo and dummy0 in /sys/class/net/
test_allowed_domainEnd-to-end HTTPS to allowed domain (5-step diagnostic)
test_denied_domainHTTPS to denied domain returns 403 or refused