Network

Egress engine and connection log

Every frame a VM sends is checked against its policy outside the guest, and every connection is logged.

The FlashVMM egress engine is a per-NIC network policy enforcer that sits in the VMM's own frame path, outside the guest, and decides every Ethernet frame a FlashVM sends against an all, none, public or allow-list policy, with DNS answers pinned to their record lifetime and every connection logged.

Every FlashVMM engine moves its guest's frames itself: the virtio-net device on KVM reads and writes the tap, and the HVF and Windows Hypervisor Platform engines carry frames over the built-in user-mode NAT pipe. One shared engine, flash-egress, sits on that path on every OS, so the policy is the same code wherever the VM runs. The guest has no setting, process or privilege that reaches it. Policies change on a running VM through the CLI, MCP or REST, and each decision lands in a connection log an agent can read or stream. The secret relay rides the same path (see secret relay).

Key facts

Runs onLinux (KVM, tap datapath), macOS (HVF engine, user-mode NAT datapath), Windows (WHP, user-mode NAT datapath)
InterfacesCLI flash run --egress, flash set --egress, flash connections · MCP flash_connections, flash_cli · REST PATCH /v1/flashvm/nics/{nic_index}, GET /v1/flashvm/nics/{nic_index}/connections
Policiesall (default) · public · public:+EXCEPTION,-DENIED,… · deny:NAME,CIDR,… · allow:NAME,*.NAME,ADDRESS,CIDR,… · none
ScopePer NIC; flash run --egress sets every NIC, flash set --nic N --egress one
DNS resolvers a restricted guest may askThe NIC's user-mode NAT gateway, 1.1.1.1, 1.0.0.1, 8.8.8.8, 8.8.4.4
DNS pin lifetimeThe record's TTL, held between 5 s and 24 h
Connection logNewest 1,024 records per NIC; a refused destination recorded once per 10 s; live SSE stream
Throughput1,751 vs 1,679 Mbit/s with per-VM rules on vs off (Linux x86 cloud bare metal)

How it works

One decision per outbound frame

For each frame the guest sends, the engine returns one of four verdicts: Pass, Drop, Reply (drop it and hand the guest a synthesized frame instead, a DNS REFUSED) or Forward (a call to the secret relay, rewritten to the relay's listener). The steps, in order:

  1. Relay first. TCP to the guest's secret-proxy endpoint (port 7730 on its host address) is rewritten to the relay and forwarded; it stays on the host and sits outside the policy.
  2. Link control. ARP, IPv6 neighbor discovery and DHCP (a BOOTP request, broadcast or to the gateway) pass under every policy, so the guest keeps its address.
  3. DNS questions. A UDP query on port 53 is parsed. Under a restricted policy, a question to any server other than the allowed resolvers is answered REFUSED (reason dns_server). A question for a name the policy allows passes and is remembered by resolver, source port, query id and name for 15 s; any other name is answered REFUSED by the engine itself, so the query carries nothing out. DNS over TCP is dropped under a restricted policy (dns_tcp).
  4. Established flows. A frame on a flow already admitted (protocol, source, source port, destination, destination port) passes and adds its bytes and packets to that flow's record. Idle flows age out after 3,600 s for TCP, 180 s for UDP and 60 s for other protocols; up to 8,192 live flows are tracked per NIC.
  5. New destinations. The destination address is checked against the policy, together with the allowed name it was pinned from, if any. Admitted: a new flow and a log record. Refused: dropped and logged with its reason.

DNS pinning

Names are enforced by watching the guest's own lookups. When an answer arrives from an allowed resolver, matches a question this guest asked within 15 s, and names an allowed host, each address in it is pinned to that name for the record's TTL (at least 5 s, at most 24 h). A connection to a pinned address is judged as that name. A flow admitted while its pin was live keeps running after the pin expires. Pins and flows are per NIC and are cleared when the NIC is reset or restored.

Rebinding and CNAME defense

An allowed name whose answer is a private, loopback or link-local address is left unpinned and logged as rebinding, unless an explicit CIDR (or, under public, the name itself as an exception) covers it. Under public, a CNAME chain that passes through a denied name is refused as denied.

What public means

public reaches the internet and keeps every internal range out: RFC 1918, CGNAT (100.64.0.0/10), loopback, link-local and metadata addresses, unspecified, broadcast, multicast, the documentation and benchmarking ranges, ULA and IPv6 link-local and site-local, with IPv4 addresses embedded in IPv6 (NAT64, 6to4, Teredo) judged as the IPv4 they carry. +EXCEPTION entries open specific private CIDRs or names (an internal registry, a task server); -DENIED entries are refused at DNS and by address. deny:a.com,b.com is public minus those.

Live policy changes

Replacing a policy on a running NIC takes the policy write lock and clears every live flow under it, so no frame is ever decided by the new policy against a flow the old one admitted. Learned pins stay and are re-checked against the new policy at their next use. The connection log and its sequence numbers carry on.

Placement per platform

Each OS uses its native datapath: Linux runs the engine inside the virtio-net device, on the frames it moves between the guest's queues and the tap; macOS runs it inside the HVF engine's NIC, on the frames exchanged with the built-in user-mode NAT; Windows runs it in the WHP network bridge on the same user-mode NAT frames. On the user-mode NAT the gateway also answers DNS, so it joins the allowed resolvers. Snapshots record each NIC's policy, so a restore keeps it unless the restore names a new one, and a forked child inherits its parent's. A NIC pool can carry a default policy that every VM claiming from it receives.

Interfaces

CLI

# An agent sandbox: the public internet, nothing private
flash run --bundle ./bundle --vcpus 2 --mem-mib 1024 --id crawler \
  --nic tap=t0 --egress public --detach

# Only a package index and a code host
flash run --bundle ./bundle --vcpus 2 --mem-mib 1024 --id builder \
  --nic tap=t0 --egress 'allow:pypi.org,*.pythonhosted.org,*.github.com' --detach

# A long list from a file: {"policy": "public", "allow": [], "deny": ["pastebin.com"]}
flash run --bundle ./bundle --vcpus 2 --mem-mib 1024 --nic tap=t0 --egress-file policy.json

# Narrow a running VM (every NIC, or one with --nic); live flows end at once
flash set crawler --egress 'deny:pastebin.com'

# Read the log, or follow it
flash connections crawler --nic 0
flash connections crawler --follow --json

MCP

flash_connections(vm_id: "crawler", nic: 0, since: 0)
flash_cli(args: ["set", "crawler", "--egress", "allow:pypi.org,*.github.com"])
flash_api(vm_id: "crawler", method: "PATCH", path: "/v1/flashvm/nics/0", body: {"egress": "public"})

REST

PATCH /v1/flashvm/nics/0
{"egress": {"policy": "public", "allow": ["192.168.127.254"], "deny": ["pastebin.com"]}}

GET /v1/flashvm/nics/0/connections?since=41
{"connections": [
  {"seq": 42, "t": 1793750400123, "proto": "dns", "dst": "pypi.org", "port": 53,
   "bytes": 0, "packets": 0, "allowed": true},
  {"seq": 43, "t": 1793750400131, "proto": "tcp", "dst": "151.101.0.223", "port": 443,
   "name": "pypi.org", "bytes": 5120, "packets": 9, "allowed": true},
  {"seq": 44, "t": 1793750400140, "proto": "tcp", "dst": "169.254.169.254", "port": 80,
   "bytes": 0, "packets": 0, "allowed": false, "reason": "policy"}],
 "next": 44}

GET /v1/flashvm/nics/0/connections/events      (SSE: one `connection` event per record)

The create body takes the policy per NIC as the flag form ("egress": "public"); PATCH takes either the flag form or the Egress object {policy, allow, deny}. Contract an agent can rely on:

  • A malformed policy returns 422 invalid_egress; allow: needs at least one entry (use none for nothing).
  • A NIC index the VM lacks returns 404 unknown_nic.
  • Pass the response's next back as since to read only newer records.
  • reason on a refused record is one of policy, private, denied, dns, dns_server, dns_tcp, rebinding.
  • A dns record is a question: dst is the name. A record with name was reached through that allowed name's pin.

Guarantees

  • Enforced outside the guest. The decision is made in the VMM's frame path, beyond the reach of anything running as root inside the VM.
  • Names leak nothing. Under an allow-list, a question for an unlisted name is answered on the host and never leaves; only answers from the allowed resolvers to questions this guest asked can open an address.
  • Private stays private. public and allow-lists keep LAN, host, link-local and metadata addresses out, including through DNS rebinding, unless a rule names them.
  • Narrowing is immediate. A new policy ends every live flow at the moment it lands.
  • Every connection is on the record. Allowed and refused flows and DNS questions are logged under every policy, including all, with bytes and packets per flow; a slow log reader drops records rather than slowing the NIC.

See egress control and secret proxies.

What it does

Related