The microVM Metadata Service (MMDS)
MMDS is a small metadata service that lives inside the VMM and answers HTTP requests from the
guest at the link-local address 169.254.169.254 — exactly like the AWS EC2 Instance Metadata
Service (IMDS) that cloud users already know. The orchestrator pushes a JSON document into the VMM
over the Firecracker REST API; the guest reads it back over its network interface as if it were
talking to a real metadata endpoint. No host networking is involved, no real server is listening:
Firecracker fabricates the HTTP responses internally using a tiny in-VMM TCP/IP stack.
This chapter covers what MMDS is and where it sits, the V1 (deprecated) vs V2 (token/session) data
plane, how it's configured (PUT /mmds for contents, PUT /mmds/config for settings), the dumbo
TCP/IP stack that answers from 169.254.169.254, the net-device interception hook that diverts those
packets, and why MMDS is deliberately not part of a snapshot.
Note: MMDS is the one place where Firecracker originates network traffic to the guest without a host backend. It is an intentional, audited exception to "the VMM does almost nothing." Because it parses guest-supplied packets,
dumbois part of the attack surface — which is exactly why it is minimal, and why production hosts are told to block guest egress to169.254.169.254from leaking out (docs/prod-host-setup.md).
Where MMDS lives
rg -n "mod mmds|struct Mmds|MmdsVersion|MMDS_IPV4_ADDR|169.254.169.254|fn get|fn put" src/vmm/src/mmds/
find src/vmm/src/mmds -name "*.rs"
find src/vmm/src/dumbo -name "*.rs"
The metadata store and HTTP logic are in src/vmm/src/mmds/; the network stack that carries the
responses is in src/vmm/src/dumbo/. Both are now modules inside the vmm crate (historically they
were separate crates — rg/find to confirm on your branch). The store is a JSON document; the
MMDS module exposes get/put operations on it and renders HTTP responses for guest requests.
┌──────────────────────────────────────────────────────────┐
│ guest │
│ curl http://169.254.169.254/latest/meta-data/... │
└───────────────┬──────────────────────────────────────────┘
│ packets to 169.254.169.254 on the guest's NIC
▼
┌──────────────────────────────────────────────────────────┐
│ virtio-net device (TX queue) │
│ hook: is dst == MMDS address? ── yes ─► divert │
└───────────────┬──────────────────────────────────────────┘
│ diverted frames
▼
┌──────────────────────────────────────────────────────────┐
│ dumbo: tiny in-VMM TCP/IP → MMDS HTTP handler │
│ builds the HTTP response from the JSON store │
└───────────────┬──────────────────────────────────────────┘
│ synthesized reply frames
▼ injected back into the guest's RX queue
The host network (the TAP device) never sees these packets — they are intercepted before they leave the VMM.
V1 vs V2
rg -n "MmdsVersion|V1|V2|token|session|X-metadata-token|imds_compat|fn generate_token" src/vmm/src/mmds/
sed -n '1,80p' docs/mmds/mmds-user-guide.md
| Version | Auth model | Status |
|---|---|---|
| V1 | None — any GET to 169.254.169.254 returns metadata | Deprecated |
| V2 | Session token: PUT /latest/api/token to get a token, then pass it in X-metadata-token on each GET | Recommended (mirrors IMDSv2) |
V2 mirrors AWS IMDSv2: the guest first does a PUT to obtain a short-lived session token, then must
present that token on every metadata GET. This defends against confused-deputy attacks (e.g. an
SSRF in a guest workload tricking the workload into fetching credentials), the same reason AWS pushed
IMDSv2. You select the version with PUT /mmds/config.
Configuring MMDS
rg -n "mmds-config|MmdsConfig|version|network_interfaces|ipv4_address|imds_compat|put_config" src/vmm/src/
There are two distinct configuration endpoints:
| Endpoint | Sets |
|---|---|
PUT /mmds (also PATCH, GET) | the contents — the JSON document the guest will read |
PUT /mmds/config | the settings — version (V1/V2), which network_interfaces may reach MMDS, the MMDS IPv4 address |
A typical setup over the API socket:
API=/tmp/fc.sock
# 1. Decide which interface(s) can reach MMDS and which version.
curl -X PUT --unix-socket $API --data \
'{"version":"V2","network_interfaces":["net1"],"ipv4_address":"169.254.169.254"}' \
http://localhost/mmds/config
# 2. Push the metadata document the guest will see.
curl -X PUT --unix-socket $API --data \
'{"latest":{"meta-data":{"instance-id":"i-abc","local-hostname":"fc"}}}' \
http://localhost/mmds
/mmds/config is a pre-boot configuration (set it before InstanceStart); /mmds contents can be
updated while the VM runs, which is how an orchestrator pushes fresh metadata to a live guest.
dumbo: the tiny in-VMM TCP/IP stack
rg -n "struct .*Tcp|Connection|fn rx|fn tx|parse|Ipv4|EthernetFrame|checksum|arp|window" src/vmm/src/dumbo/
dumbo is a deliberately minimal TCP/IP implementation — just enough to terminate the handful of
short HTTP request/response exchanges MMDS needs and nothing more. It parses Ethernet frames, IPv4
packets, and a constrained subset of TCP (enough for a single short connection per request), handles
ARP for the MMDS address, and hands the HTTP bytes to the MMDS handler. It is not a general
network stack: it does not route, does not do the host network, and exists only to answer
169.254.169.254. That minimalism is a security property — less parser, less attack surface.
flowchart TD
Frame["guest TX frame to 169.254.169.254"] --> Eth["dumbo: parse Ethernet"]
Eth --> Arp{"ARP?"}
Arp -->|yes| ArpReply["synthesize ARP reply"]
Arp -->|no| Ip["parse IPv4"]
Ip --> Tcp["minimal TCP state machine"]
Tcp --> Http["MMDS HTTP handler"]
Http --> Store["read JSON store / check V2 token"]
Store --> Resp["build HTTP response"]
Resp --> Inject["inject reply frame into guest RX queue"]
The net-device interception hook
rg -n "mmds|ns\.|MmdsNetworkStack|detour|intercept|is_mmds|process_tx|process_rx" src/vmm/src/devices/virtio/net/
MMDS does not get its own virtio device. Instead, the virtio-net device's TX path is hooked: when
the guest transmits a frame, the net device checks whether the destination is the MMDS address (and
whether this interface is allowed to reach MMDS per /mmds/config). If so, the frame is diverted to
dumbo/MMDS instead of being written to the host TAP, and any reply is injected back into the net
device's RX queue. Find this in the net device's TX processing (rg -n "mmds" src/vmm/src/devices/virtio/net/).
This is why MMDS is reachable only over a virtio-net interface that was explicitly granted access —
the hook lives in that specific device's data path. The net device itself is covered in
virtio-net-and-tap.md.
Why MMDS is not snapshotted
rg -n "Persist|mmds|snapshot" src/vmm/src/mmds/ || echo "MMDS state not part of the Persist graph — confirm on your branch"
The MMDS contents are configuration the orchestrator owns, not intrinsic VM state. On restore, the
orchestrator re-pushes the metadata document appropriate to the new instance (a clone restored on a
different host should generally get different metadata — a different instance id, hostname, etc.).
Baking the old metadata into the snapshot would be wrong. So MMDS contents are deliberately outside
the Persist graph: a restored microVM starts with an empty (or freshly-pushed)
MMDS. This is a clean illustration of the line between "VM state" (snapshotted) and "orchestrator
policy" (re-applied on restore).
Reading exercise
# 1. The MMDS module and the metadata store.
find src/vmm/src/mmds -name "*.rs"
rg -n "struct Mmds|MmdsVersion|fn get|fn put|169.254.169.254" src/vmm/src/mmds/
# 2. V1 vs V2 token handling.
rg -n "V1|V2|token|session|X-metadata-token|generate_token" src/vmm/src/mmds/
# 3. The dumbo stack.
find src/vmm/src/dumbo -name "*.rs"
rg -n "Tcp|Ipv4|EthernetFrame|arp|checksum" src/vmm/src/dumbo/
# 4. The interception hook in the net device.
rg -n "mmds|is_mmds|MmdsNetworkStack" src/vmm/src/devices/virtio/net/
# 5. Configure and read MMDS by hand following the API blocks above (use a guest with curl).
# 6. The user guide and config docs.
ls docs/mmds/
Answer:
- Where does MMDS run, what address does the guest use, and what's listening there?
- Contrast V1 and V2. What attack does the V2 token model defend against, and what real-world IMDS feature is it modeled on?
- What is the difference between
PUT /mmdsandPUT /mmds/config? Which can you call while the VM is running? - What is
dumboand why is it deliberately minimal? Name three protocol layers it must handle. - Explain the net-device interception hook: how does a frame to
169.254.169.254reach MMDS instead of the host TAP? - Why is MMDS content not part of a snapshot, and what is the orchestrator expected to do on restore?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
Guest gets connection refused / no reply from 169.254.169.254 | interface not granted MMDS access in /mmds/config; wrong version | /mmds/config network_interfaces; the net interception hook |
| V2 GETs return 401/unauthorized | guest didn't fetch/send the session token | V2 token flow; X-metadata-token handling |
| Metadata stale after a config push | updated /mmds but guest cached; or pushed before interface allowed | the JSON store update path; ordering of config vs contents |
| Restored VM serves the old instance's metadata | orchestrator baked metadata expectation into restore | MMDS is not snapshotted — re-push after PUT /snapshot/load |
| Malformed guest packet crashes/hangs the stack | a dumbo parser edge case | the relevant dumbo parser (Ethernet/IPv4/TCP) |
| MMDS reachable from outside the host | guest egress to 169.254.169.254 not blocked | host firewalling per docs/prod-host-setup.md |
Validation: prove you understand this
- Draw the path of a guest request to
169.254.169.254from the guest NIC to the synthesized reply, naming the net hook,dumbo, and the MMDS store. - Explain V1 vs V2 and exactly what extra step a V2 guest must perform.
- Distinguish
/mmdsfrom/mmds/configand say which is pre-boot vs runtime. - Explain why
dumbois minimal and why that minimalism is a security property, not a limitation. - Describe the net-device interception hook and why MMDS is only reachable over an explicitly allowed interface.
- Justify the decision to exclude MMDS contents from snapshots, using the "VM state vs orchestrator policy" distinction.
Next: the-event-manager.md — the epoll loop on the VMM thread that drives the net device (and therefore the MMDS hook), every other device, and the API eventfd.