====== EVE-KVM: Network Instance lifecycle (Local & Switch) ======
This page documents **exactly what EVE-OS does in KVM (hypervisor) mode** when a
Network Instance (NI) of type **Local** and type **Switch** is created.
> **Important — this is the EVE-KVM datapath, not EVE-K.**
> In EVE-K, NIs are wired through Multus + a ''NetworkAttachmentDefinition'' calling
> the ''eve-bridge'' CNI plugin. In **EVE-KVM there is no Multus, no NAD, no CNI**.
> The pillar agent **zedrouter** programs the Linux network stack directly: Linux
> bridges, ''dnsmasq'', ''iptables'', policy routing tables and ''tc''. Everything
> below is the bridge/iptables world, not the Kubernetes world.
Source of truth: ''lf-edge/eve'' → ''pkg/pillar'' (''cmd/zedrouter'' and ''nireconciler'').
File/line references are given per section so claims are verifiable.
===== NI types (enum) =====
EVE's internal NI type enum (mirrors ''zconfig.ZNetworkInstType'' from the controller API):
NetworkInstanceTypeSwitch = 1 // L2 bridge, no EVE-side L3
NetworkInstanceTypeLocal = 2 // L3 NAT'd bridge with DHCP/DNS
NetworkInstanceTypeCloud = 3 // (VPN)
NetworkInstanceTypeHoneyPot = 5
NetworkInstanceTypeTransparent = 6
''pkg/pillar/types/zedroutertypes.go'' (NetworkInstanceType const block).
Address type drives L2 vs L3:
AddressTypeNone = 0 // Switch NI (no EVE-managed addressing)
AddressTypeIPV4 = 1 // Local NI
AddressTypeIPV6 = 2
===== Actors =====
* **controller** (ZEDEDA Cloud / zedcontrol) — sends the device config.
* **zedagent** — parses controller config, publishes ''NetworkInstanceConfig'' over pubsub.
* **zedrouter** — subscribes to ''NetworkInstanceConfig'' (from zedagent), drives the reconciler, publishes ''NetworkInstanceStatus'' (''cmd/zedrouter/zedrouter.go'' line ~13).
* **nireconciler** (''LinuxNIReconciler'') — converts the desired NI config into a dependency graph of Linux config items and reconciles intended vs current state.
* **NIM** (nim) — owns physical device ports (DPC). For certain Switch NIs, NIM (not zedrouter) owns the bridge.
* **domainmgr** — later attaches the app/VM VIFs; zedrouter wires each VIF into the NI bridge.
===== Control-plane flow (how the config arrives) =====
controller (ZEDEDA Cloud)
| device config (protobuf)
v
zedagent --- publishes types.NetworkInstanceConfig ---> (pubsub)
|
v
zedrouter subscribes NetworkInstanceConfig
| handleNetworkInstanceCreate() cmd/zedrouter/pubsubhandlers.go:183
| -> parse/validate, allocate BridgeNum, bridge MAC, subnet
| -> doActivateNetworkInstance() cmd/zedrouter/networkinstance.go:526
| -> niReconciler.AddNI(config, NIBridge)
v
LinuxNIReconciler builds intended dependency graph, reconciles to kernel
| creates bridge / dnsmasq / iptables / routes / ip rules / tc
v
zedrouter publishes types.NetworkInstanceStatus (Activated=true)
|
v
niStateCollector.StartCollectingForNI() (metrics, learned IPs, flow logs)
''AddNI'' is the single entry point that turns NI config into kernel state
(''cmd/zedrouter/networkinstance.go:531'').
===== The reconciler model (dependency graph) =====
''nireconciler'' does **not** run imperative "create bridge, then add IP" steps.
It builds an **intended graph** of typed config items and a reconciliation engine
makes the kernel match it. The top-level graph (from the big ASCII map at the head
of ''nireconciler/linux_config.go'') has:
* **Global** — sysctl (bridge-nf), Ports (external device ports), IPSets, BlackHole, ACL root chains, L2-fwd chain, TCP-MSS clamping.
* **NI-** — one per Network Instance, containing sub-graphs:
* **L2** — Bridge, BridgePort, VLAN items, L2-forward iptables rule.
* **L3** — Routes, IPRules, IPReserve, **MASQUERADE** rule (empty for Switch).
* **Mirroring** — DummyIf + ''tc'' ingress/mirror (used by Switch NI too).
* **Services** — metadata HTTPServer, **dnsmasq** (DHCP+DNS), radvd (IPv6).
* **AppConn--** — one per VIF, added later when an app attaches: VIF + BridgePort + per-VIF ACL chains (+ VLANPort/BPDUGuard for Switch).
Branch point (''linux_config.go:600''):
if !ni.bridge.IPConflict {
PutSubGraph(getIntendedNIL2Cfg(niID)) // always
PutSubGraph(getIntendedNIL3Cfg(niID)) // empty body for Switch
if ni.config.Type == NetworkInstanceTypeSwitch {
PutSubGraph(getIntendedNIMirroring(niID))
}
}
===== Interface naming =====
''nireconciler/linux_config.go:303-304'', ''generateBridgeIfName()'' (line ~1678):
* Bridge for **Local NI** → ''bnN'' (prefix ''bn'' + BridgeNum), e.g. ''bn1''.
* Bridge for **Switch NI**, created by NIM (single port also used for mgmt or a Local NI) → the bridge **is the port itself**, e.g. ''eth1''.
* Bridge for **Switch NI**, otherwise (air-gapped, or multi-port shared label) → zedrouter creates ''bnN'', same as Local.
* App **VIF** host side → ''nbux'' (prefix ''nbu''), e.g. ''nbu1x3''.
* Mirror dummy interface → ''-m''.
"Created by NIM" rule (''cmd/zedrouter/networkinstance.go:722''): only a **Switch**
NI with a **single physical port** whose ''Dhcp'' is ''Static'' or ''Client'' has its
bridge owned by NIM. Local NIs are **always** bridged by zedrouter.
====== Type LOCAL (type = 2) — step by step ======
A Local NI is an **L3, NAT'd, EVE-managed** network: EVE owns the bridge IP, runs
DHCP/DNS, and SNATs app traffic out the uplink port.
==== 1. Bridge (L2 sub-graph) ====
zedrouter creates a managed Linux bridge ''bnN'', assigns it the **gateway IP**
(the bridge IP = the NI gateway, e.g. ''10.10.1.1/24'') and a deterministic MAC.
(''getIntendedNIL2Cfg'', ''linux_config.go:623''.) For Local NI the L2 sub-graph
returns right after the bridge — **no VLAN/STP/BridgePort items** (those are Switch-only).
ip link add bn1 type bridge
ip addr add 10.10.1.1/24 dev bn1 # bridge IP == NI gateway
ip link set bn1 up
==== 2. L3 sub-graph (Routes / IP rules / SNAT) ====
''getIntendedNIL3Cfg'' (''linux_config.go:756'') — only runs for non-Switch:
* **IPReserve** for the bridge IP so EVE doesn't hand it out via DHCP.
* **Per-NI routing table** = ''NIBaseRTIndex + BrNum'' (''NIBaseRTIndex = 800''). Routes relevant to the NI's uplink port are **copied** from the main table into this per-NI table.
* A final **unreachable** route (IPv4 + IPv6) at the lowest priority — anything not matched is dropped.
* **IP rules** steering NI subnet traffic to that table: ''PbrNatOutGatewayPrio = 9999'' (to-bridge-IP → local table), ''PbrNatOutPrio = 10000'' (src = NI subnet → NI table), ''PbrNatInPrio = 11000'' (dst = NI subnet → NI table).
* **SNAT / MASQUERADE** (IPv4 only): for **each uplink port**, an ''iptables'' rule in the ''nat'' table ''POSTROUTING'' (app-specific chain).
# nat POSTROUTING (per uplink port of a Local NI)
-o eth0 -s 10.10.1.0/24 -j MASQUERADE
(''linux_config.go:972'' — guarded by ''Type == NetworkInstanceTypeLocal'' and IPv4 subnet.)
==== 3. Services sub-graph: dnsmasq (DHCP + DNS) ====
''getIntendedDnsmasqCfg'' (''linux_config.go:1185''). For Local NI EVE runs a
**per-NI dnsmasq** bound to the bridge IP, serving DHCP + DNS to the apps:
* Leases from ''DhcpRange''; advertises **gateway = bridge IP**.
* Advertises a **default route** unless the NI is air-gapped or all ports are app-shared with no default route.
* Propagates host routes to user-configured **NTP/DNS** servers, static routes, and (if ''PropagateConnRoutes'') connected port subnets — all via DHCP option 121.
# one dnsmasq per Local NI, e.g.
dnsmasq --conf-file=/run/zedrouter/dnsmasq.bn1.conf
# interface=bn1, listen-address=10.10.1.1, dhcp-range=...
==== 4. Services sub-graph: metadata server ====
''getIntendedMetadataSrvCfg'' (''linux_config.go:1123''). EVE starts an HTTP server
on the **bridge IPv4:80** and DNATs the well-known metadata IP to it:
# nat PREROUTING
-i bn1 -p tcp -d 169.254.169.254/32 --dport 80 -j DNAT --to-destination 10.10.1.1:80
This is the EVE app-metadata endpoint (''169.254.169.254'', ''metadataSrvIP'').
==== 5. App attach (later, when a VM is deployed) ====
When ''domainmgr'' starts the VM, zedrouter adds an **AppConn--**
sub-graph: a **VIF** ''nbuNxM'' enslaved to ''bnN'' as a **BridgePort**, plus the
**per-VIF ACL chains** (''iptables'' rules derived from the app's ACLs). The VM then
gets its lease from the NI's dnsmasq.
==== Resulting kernel state (Local NI) ====
ip -br link show type bridge # bn1 ... UP
ip addr show dev bn1 # 10.10.1.1/24
bridge link show # nbu1x3 master bn1
iptables -t nat -S | grep -i masquerade
ip rule show | grep -E '9999|10000|11000'
ip route show table 801 # 800 + BrNum
ps aux | grep dnsmasq.bn1
====== Type SWITCH (type = 1) — step by step ======
A Switch NI is an **L2 bridge**. EVE provides Layer-2 connectivity between the app
VIFs and a physical uplink port; **EVE does not provide IP, DHCP, DNS, NAT or routing**.
The app gets its address from an **external DHCP server** on the wire.
==== 1. Bridge ownership ====
''niBridgeIsCreatedByNIM'' (''networkinstance.go:722''):
* **Single port** also used for EVE mgmt or a Local NI (''Dhcp = Static''/''Client'') → **NIM owns the bridge**; the bridge **is the port** (e.g. ''eth1''). zedrouter just bridges into it.
* **Air-gapped** (''PortLabel == ""'') or **multi-port** (shared label) → **zedrouter creates** ''bnN'', exactly like a Local NI bridge.
==== 2. L2 sub-graph (the whole story for Switch) ====
''getIntendedNIL2Cfg'' (''linux_config.go:623''). This is where Switch-specific items live:
* **Bridge** (''bnN'' or the NIM-owned port).
* **BridgeFwdMask** (controls LLDP forwarding).
* **STP** enabled only when the Switch NI has **> 1 port** (''withSTP'').
* **BridgePort** = the physical device port enslaved to the bridge (one per uplink port).
* **VLANBridge** / **VLANPort** (trunk or access) when VLAN filtering is configured (''VlanAccessPorts'').
* **VLANSubIf** for tagged sub-interfaces.
* **BPDUGuard** on designated ports when STP is on.
* **L2-forward iptables rule** — allows forwarding between bridge ports (''getIntendedL2FwdRules'').
ip link add eth1 type bridge # (or bn1 if zedrouter-owned)
ip link set eth0 master eth1 # physical uplink enslaved
# VLAN filtering / trunk / access programmed via 'bridge vlan' when configured
==== 3. L3 sub-graph — EMPTY ====
''getIntendedNIL3Cfg'' returns immediately for Switch (''linux_config.go:762''):
if ni.config.Type == NetworkInstanceTypeSwitch {
// No L3 config for switch network instance.
return intendedL3Cfg
}
**Consequence:** no bridge gateway IP, **no MASQUERADE/SNAT**, no per-NI routing
table, no NAT IP rules.
==== 4. Services — NO dnsmasq ====
''getIntendedDnsmasqCfg'' returns immediately for Switch (''linux_config.go:1187''):
if ni.config.Type == NetworkInstanceTypeSwitch {
// Not running DHCP and DNS servers inside EVE for Switch network instances.
return
}
The app must obtain its IP from an **external DHCP** server reachable over the bridged uplink.
==== 5. Metadata server on a Switch NI ====
The metadata HTTP server only starts if the bridge has an IPv4 address; a pure Switch
NI normally has none, so there is typically **no metadata service**. Where a bridge IP
does exist, EVE adds a guard rule that **DROPs** metadata access arriving from the
external physical port, so outside endpoints can't reach it through the L2 segment
(''linux_config.go'', filter ''INPUT'' with ''physdev --physdev-in '').
==== 6. Mirroring sub-graph (Switch NI) ====
Unlike Local NI, Switch NI explicitly adds the **Mirroring** sub-graph
(''getIntendedNIMirroring'', ''linux_config.go:996''): a **DummyIf** plus ''tc''
ingress + mirror rules that copy a small slice of traffic (DHCP replies, ARP,
ICMPv6 NS, and DNS if flow-logging is on) so the ''niStateCollector'' can learn app
IPs and log flows even though EVE isn't the DHCP/DNS server.
==== 7. App attach ====
Same AppConn- mechanism: a **VIF** ''nbuNxM'' enslaved to the bridge as a
**BridgePort**. For Switch NI the per-VIF sub-graph also gets **VLANPort** and
**BPDUGuard** items when VLAN/STP are configured. The VM sees a plain bridged L2
interface and behaves as if cabled directly to the uplink.
==== Resulting kernel state (Switch NI) ====
ip -br link show # eth1 (or bn1) UP, eth0 enslaved
bridge link show # eth0 + nbu1x3 master eth1
bridge vlan show # trunk/access VLANs if configured
iptables -t nat -S | grep -i masquerade # (empty for Switch)
ps aux | grep dnsmasq # no dnsmasq for this NI
====== Side-by-side: Local vs Switch on EVE-KVM ======
^ Aspect ^ Local (type 2) ^ Switch (type 1) ^
| OSI layer | L3 (routed/NAT) | L2 (bridged) |
| Bridge owner | zedrouter (always), ''bnN'' | NIM if single mgmt/Local port (= ''ethX''); else zedrouter ''bnN'' |
| Bridge IP / gateway | Yes — EVE owns gateway IP | None (EVE assigns no L3) |
| DHCP/DNS (dnsmasq) | Yes, per-NI on bridge IP | No — external DHCP on the wire |
| SNAT / MASQUERADE | Yes, per uplink port (IPv4) | No |
| Per-NI routing table | Yes (''800 + BrNum'') | No |
| NAT IP rules (pbr) | Yes (9999/10000/11000) | No |
| VLAN / STP / BPDUGuard | No | Yes (STP when >1 port) |
| Metadata server (.169.254) | Yes (DNAT to bridge IP:80) | Usually none; guarded if bridge IP exists |
| Mirroring sub-graph | Implicit via state collector | Explicit (''tc'' ingress/mirror) |
| App addressing | Lease from EVE dnsmasq | Lease from external DHCP |
====== Inspecting a live EVE-KVM node ======
Via EdgeView / device debug shell:
# Which NIs exist and their bridges/types (from zedrouter status)
cat /run/zedrouter/NetworkInstanceStatus/*.json | jq '{name:.DisplayName,type:.Type,br:.BrIfName,ip:.BridgeIPAddr}'
# Bridges and enslaved interfaces
ip -br link show type bridge
bridge link show
# Local NI plumbing
ip addr show dev bn1
ip rule show
ip route show table 801 # 800 + BridgeNum
iptables -t nat -S | grep -E 'MASQUERADE|169.254.169.254'
ls /run/zedrouter/dnsmasq.*.conf
# Switch NI plumbing
bridge vlan show
iptables -S | grep -i physdev # metadata guard / L2 fwd rules
# VIFs of running apps
ip -br link show | grep nbu
====== Source references (lf-edge/eve, pkg/pillar) ======
* NI type & config struct — ''types/zedroutertypes.go'' (NetworkInstanceType, NetworkInstanceConfig).
* Routing/PBR constants — ''types/pbr.go'' (''NIBaseRTIndex=800'', ''PbrNatOutGatewayPrio=9999'', ''PbrNatOutPrio=10000'', ''PbrNatInPrio=11000'').
* Config subscription — ''cmd/zedrouter/zedrouter.go'' (subscribes NetworkInstanceConfig from zedagent).
* Create/modify handlers — ''cmd/zedrouter/pubsubhandlers.go:183'' / '':294''.
* Activate / AddNI — ''cmd/zedrouter/networkinstance.go:526'' (''doActivateNetworkInstance'' → ''niReconciler.AddNI'').
* Bridge-owner rule — ''cmd/zedrouter/networkinstance.go:722'' (''niBridgeIsCreatedByNIM'').
* Dependency-graph map + item builders — ''nireconciler/linux_config.go'': ''getIntendedNIL2Cfg'' (:623), ''getIntendedNIL3Cfg'' (:756), MASQUERADE (:972), mirroring (:996), metadata (:1123), dnsmasq (:1185), interface naming (:303, :1678).
* Linux item types — ''nireconciler/linuxitems/'' (bridge, bridgeport, vlanbridge, vlanport, route, iprule, ipset, tcingress, tcmirror, bpduguard, sysctl) and ''genericitems/'' (dnsmasq, radvd, httpsrv, port).