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 on | Linux (KVM, tap datapath), macOS (HVF engine, user-mode NAT datapath), Windows (WHP, user-mode NAT datapath) |
|---|---|
| Interfaces | CLI 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 |
| Policies | all (default) · public · public:+EXCEPTION,-DENIED,… · deny:NAME,CIDR,… · allow:NAME,*.NAME,ADDRESS,CIDR,… · none |
| Scope | Per NIC; flash run --egress sets every NIC, flash set --nic N --egress one |
| DNS resolvers a restricted guest may ask | The NIC's user-mode NAT gateway, 1.1.1.1, 1.0.0.1, 8.8.8.8, 8.8.4.4 |
| DNS pin lifetime | The record's TTL, held between 5 s and 24 h |
| Connection log | Newest 1,024 records per NIC; a refused destination recorded once per 10 s; live SSE stream |
| Throughput | 1,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:
- 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.
- 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.
- 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(reasondns_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 answeredREFUSEDby the engine itself, so the query carries nothing out. DNS over TCP is dropped under a restricted policy (dns_tcp). - 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.
- 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 (usenonefor nothing). - A NIC index the VM lacks returns
404 unknown_nic. - Pass the response's
nextback assinceto read only newer records. reasonon a refused record is one ofpolicy,private,denied,dns,dns_server,dns_tcp,rebinding.- A
dnsrecord is a question:dstis the name. A record withnamewas 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.
publicand 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.