You want a container, VM helper namespace, or service netns to own a real LAN address—same subnet as the host, reachable from the rest of the network—without standing up br0, enslaving the NIC, and living with bridge hairpin quirks forever.
That is exactly what MACVLAN and IPVLAN are for.
They stack a virtual interface on top of a parent NIC. The parent keeps carrying frames; the virtual device filters what it receives—by MAC (MACVLAN) or by IP (IPVLAN)—and shows up as a normal interface you can move into a network namespace, hand to systemd-nspawn, or address on the host.
This post is a practical operator guide:
- when to pick MACVLAN vs IPVLAN
- what the modes actually mean
- how to wire them with
ip linkfor a lab - how to make them persistent and declarative with
systemd-networkd - how to fix the classic “host cannot talk to its own MACVLAN guests” trap
- how to verify and roll back safely
No bridge required. No NAT hairpin required for LAN peers.
What problem these solve
A Linux bridge is a software switch. It is powerful (VLAN filtering, STP, FDB control), and it is also more machinery than many homelab and single-host container layouts need.
MACVLAN / IPVLAN give you:
| Goal | Bridge | MACVLAN / IPVLAN |
|---|---|---|
| Extra L2/L3 endpoints on one NIC | Yes | Yes |
| Software switch FDB/STP/VLAN filtering | Yes | No (parent NIC + kernel mux) |
| Each endpoint looks like another host on the LAN | Yes (via bridge port) | Yes (especially MACVLAN) |
| Survives “switch allows only one MAC per port” | Depends | Prefer IPVLAN |
| Hostile netns that must not own a unique MAC | Awkward | Prefer IPVLAN |
Kernel IPVLAN docs put the decision bluntly: choose IPVLAN when the upstream switch policy allows only one MAC per port, when too many MACs would force the NIC into promiscuous mode and hurt performance, or when a slave will live in an untrusted namespace that should not control L2 identity.
Mental model in one paragraph
- MACVLAN: parent NIC demuxes by destination (and optionally source) MAC. Each MACVLAN gets its own hardware address. LAN switches see multiple MACs behind one port (unless your switch forbids that).
- IPVLAN: parent NIC demuxes by IP. Slaves share the parent MAC. L2 identity stays with the master; L3 is where isolation happens.
Both are “stacked devices” in systemd-networkd terms: Kind=macvlan / Kind=ipvlan in a .netdev file, then a .network file that binds the parent and addresses the child.
MACVLAN modes (what ip-link and systemd actually support)
From ip link and systemd.netdev(5), MACVLAN modes are:
| Mode | Behavior | Typical use |
|---|---|---|
| private | MACVLAN instances on the same parent cannot talk to each other, even if the external switch supports hairpin | Isolation between guests on one host |
| vepa | Guest-to-guest traffic is sent out the parent toward the external switch (802.1Qbg VEPA style). Needs switch hairpin or an upstream router. Default in ip link
|
Data-center VEPA topologies |
| bridge | Guests on the same parent can talk directly without bouncing off the external switch | Homelab containers that must reach each other |
| passthru | Single endpoint gets almost full control of the parent (often with macvtap); only one endpoint allowed; default forces parent promisc unless nopromisc
|
Handing a NIC-like device to one VM/guest |
| source | Allowlist of source MACs (SourceMACAddress= in systemd) for MAC-based association |
Specialized MAC-based VLANs / 802.1X-style setups |
macvtap is the same filtering idea plus a /dev/tapX character device for QEMU-style consumers. systemd.netdev exposes [MACVTAP] with the same keys as [MACVLAN].
IPVLAN modes and flags
From the kernel IPVLAN HOWTO and systemd.netdev(5):
Modes (Mode= in systemd; mode in ip link):
| Mode | What happens | Multicast/broadcast |
|---|---|---|
| L2 | TX up through the slave stack, then queued on the master for send; RX like a normal L2 endpoint | Slaves can RX/TX mcast/bcast |
| L3 (kernel default) | TX up to L3 on the slave; L2/routing decisions happen in the master stack instance | Slaves do not send/receive mcast/bcast |
| L3S | Like L3, but symmetric so conntrack/iptables-style path works | Same L3 restrictions; slight cost for conntrack correctness |
Flags (Flags= in systemd):
| Flag | Meaning |
|---|---|
| bridge (default) | Slaves can cross-talk among themselves and via the master |
| private | No cross-talk between slaves |
| vepa | Offload switching to external entity (802.1Qbg). Note: IPVLAN reuses the master MAC, so VEPA neighbor paths can look like same src/dst MAC and upset switches |
Important operational rule from the kernel docs: for a given master, all IPVLAN slaves share one operating mode (L2 or L3/L3S). Pick once per parent.
Lab: create devices with ip link (ephemeral)
Replace eth0 with your real parent (ip -br link).
MACVLAN bridge mode (guests can talk)
# Create two MACVLAN endpoints on the parent
sudo ip link add link eth0 name macvlan0 type macvlan mode bridge
sudo ip link add link eth0 name macvlan1 type macvlan mode bridge
# Optional: pin MACs if your LAN/DHCP cares
# sudo ip link set macvlan0 address 02:11:22:33:44:50
# sudo ip link set macvlan1 address 02:11:22:33:44:51
sudo ip link set macvlan0 up
sudo ip link set macvlan1 up
# Address them (example static lab subnet — use YOUR LAN plan)
sudo ip addr add 192.0.2.10/24 dev macvlan0
sudo ip addr add 192.0.2.11/24 dev macvlan1
Move into a netns when you want isolation:
sudo ip netns add ns-app
sudo ip link set macvlan0 netns ns-app
sudo ip -n ns-app link set lo up
sudo ip -n ns-app link set macvlan0 up
sudo ip -n ns-app addr add 192.0.2.10/24 dev macvlan0
sudo ip -n ns-app route add default via 192.0.2.1 dev macvlan0
IPVLAN L2 bridge (share parent MAC, still L2-ish)
sudo ip link add link eth0 name ipvl0 type ipvlan mode l2 bridge
sudo ip link add link eth0 name ipvl1 type ipvlan mode l2 bridge
sudo ip link set ipvl0 up
sudo ip link set ipvl1 up
sudo ip addr add 192.0.2.20/24 dev ipvl0
sudo ip addr add 192.0.2.21/24 dev ipvl1
IPVLAN L3 (strict; good for many netns behind one MAC)
sudo ip link add link eth0 name ipvl0 type ipvlan mode l3 bridge
# L3: no mcast/bcast on slaves; routing context is master-oriented
Inspect details:
ip -d link show type macvlan
ip -d link show type ipvlan
ip -d link show macvlan0
The host-reachability trap (MACVLAN)
This is the footgun that wastes hours:
If the host keeps its IP on the bare parent NIC, and guests use MACVLAN on that same parent, the host often cannot reach those guests (and vice versa), because frames never hairpin the way people expect.
The fix used by the ArchWiki systemd-networkd MACVLAN guide (and it matches field practice):
- Do not address the parent for host traffic.
- Create a host-side MACVLAN in
bridgemode on the parent. - Put the host’s IP/DHCP on that MACVLAN.
- Give containers/netns their own MACVLANs on the same parent.
- Use
BindCarrier=so the host MACVLAN tracks the parent’s carrier.
Parent becomes a pure underlay; everyone—including the host—talks MACVLAN-to-MACVLAN in bridge mode.
Persistent setup with systemd-networkd
Assume parent NIC enp1s0, host MACVLAN mv-host, and you want containers to attach later.
1) Create the host MACVLAN netdev
/etc/systemd/network/25-mv-host.netdev:
[NetDev]
Name=mv-host
Kind=macvlan
# Optional stable MAC (else generated from name + machine-id):
# MACAddress=02:11:22:33:44:10
[MACVLAN]
Mode=bridge
2) Attach parent to that MACVLAN; strip addressing from parent
/etc/systemd/network/30-enp1s0.network:
[Match]
Name=enp1s0
[Link]
# Parent will not get an IP — wait only for carrier
RequiredForOnline=carrier
[Network]
MACVLAN=mv-host
DHCP=no
IPv6AcceptRA=false
LinkLocalAddressing=no
MulticastDNS=false
LLMNR=false
MACVLAN= in the parent’s [Network] section is how systemd-networkd binds the lower device to the named macvlan netdev.
3) Address the host on mv-host
DHCP example — /etc/systemd/network/35-mv-host.network:
[Match]
Name=mv-host
[Link]
RequiredForOnline=routable
[Network]
BindCarrier=enp1s0
DHCP=yes
# Optional: keep DNS via resolved
# Domains=lan
Static example:
[Match]
Name=mv-host
[Link]
RequiredForOnline=routable
[Network]
BindCarrier=enp1s0
Address=192.0.2.5/24
Gateway=192.0.2.1
DNS=192.0.2.1
BindCarrier=enp1s0 ties admin/carrier behavior to the physical link so a cable pull takes the host endpoint down with the parent.
4) Optional second MACVLAN for a service netns / app endpoint
/etc/systemd/network/25-mv-app.netdev:
[NetDev]
Name=mv-app
Kind=macvlan
[MACVLAN]
Mode=bridge
/etc/systemd/network/36-mv-app.network:
[Match]
Name=mv-app
[Link]
RequiredForOnline=routable
[Network]
BindCarrier=enp1s0
Address=192.0.2.50/24
Gateway=192.0.2.1
DNS=192.0.2.1
You still need the parent file to also create/bind mv-app. With multiple MACVLANs, list them on the parent:
[Network]
MACVLAN=mv-host
MACVLAN=mv-app
(Exact multi-value repetition is the normal systemd networkd pattern for several stacked devices of the same type.)
5) Apply
sudo networkctl reload
# or:
sudo systemctl restart systemd-networkd.service
networkctl
networkctl status mv-host
networkctl status enp1s0
ip -br addr
Confirm parent has no global unicast IP, host IP lives on mv-host, and mv-host shows macvlan kind in ip -d link show mv-host.
systemd-nspawn: attach MACVLAN without a bridge
On the host, keep the parent underlay pattern above (host on mv-host).
For a machine unit, /etc/systemd/nspawn/web.nspawn:
[Network]
MACVLAN=enp1s0
Or on the CLI:
sudo systemd-nspawn \
--machine=web \
--network-macvlan=enp1s0 \
-b
Inside the container, nspawn names the interface mv-<parent> (for example mv-enp1s0). Configure it with networkd inside the guest:
/etc/systemd/network/30-mv-enp1s0.network in the container:
[Match]
Name=mv-enp1s0
[Link]
RequiredForOnline=routable
[Network]
DHCP=yes
That is the ArchWiki-documented naming and DHCP pattern for nspawn MACVLAN guests.
IPVLAN with systemd-networkd
Same stacking idea; different kind and section.
/etc/systemd/network/25-ipvl-app.netdev:
[NetDev]
Name=ipvl-app
Kind=ipvlan
[IPVLAN]
Mode=L2
Flags=bridge
# For many netns + one MAC upstream, prefer:
# Mode=L3
# Flags=private
Parent binding uses IPVLAN= the same way MACVLAN= works (see systemd.network(5) network device binding keys alongside Bridge=, Bond=, VRF=, etc.).
Minimal parent snippet:
[Match]
Name=enp1s0
[Network]
IPVLAN=ipvl-app
# Still strip host addressing from parent if host traffic moves elsewhere
Address ipvl-app in its own .network file. For L3 mode, remember: no multicast/broadcast on the slave—DHCP and neighbor discovery designs must respect that (static addressing or master-side helpers are common).
Kernel example workflow for netns (document-accurate):
sudo ip netns add ns0
sudo ip link add link eth0 name ipvl0 type ipvlan mode l2
sudo ip link set dev ipvl0 netns ns0
sudo ip netns exec ns0 bash -c '
ip link set lo up
ip link set ipvl0 up
ip -4 addr add 192.0.2.30/24 dev ipvl0
ip -4 route add default via 192.0.2.1 dev ipvl0
'
Choosing quickly
Use MACVLAN bridge when:
- your switch is fine with multiple MACs per port
- you want each guest to look like a normal Ethernet host
- guests must talk to each other and the host (with host-on-MACVLAN pattern)
- you integrate with
systemd-nspawn --network-macvlan=
Use IPVLAN L2/L3 when:
- upstream allows one MAC only
- you create lots of endpoints and MAC pressure / promisc is a concern
- untrusted netns must not control L2 identity
- you are OK with L3-mode multicast limitations when you pick L3/L3S
Use a real bridge when:
- you need VLAN-aware switching, STP, hairpin control, or complex L2 policy on the host
- you already standardized on
br0+ veth for the fleet
MACVLAN/IPVLAN are not a replacement for VRF isolation, bond failover, or nftables edge policy—they compose with those layers.
Verification checklist
# Kinds and modes
ip -d link show type macvlan
ip -d link show type ipvlan
# Addresses and routes
ip -br addr
ip route
networkctl status mv-host
networkctl status mv-app
# From host MACVLAN to guest MACVLAN (same L2)
ping -c3 192.0.2.50
# From another LAN host (not the parent-only host mistake)
# ping the guest IP from a laptop on the same subnet
# nspawn
machinectl status web
# inside: ip -br link; ip -br addr
If host↔guest fails but laptop↔guest works, you almost certainly left the host IP on the bare parent. Move host addressing onto a MACVLAN in bridge mode.
If guest↔guest fails, check mode: private and some vepa layouts will not local-switch. Prefer Mode=bridge for same-host east-west.
If DHCP fails on IPVLAN L3, that is expected multicast/broadcast limitation—use static addressing or redesign with L2 mode.
Rollback
# Ephemeral lab devices
sudo ip link del macvlan0 2>/dev/null || true
sudo ip link del macvlan1 2>/dev/null || true
sudo ip link del ipvl0 2>/dev/null || true
sudo ip netns del ns-app 2>/dev/null || true
sudo ip netns del ns0 2>/dev/null || true
# networkd: remove or mask the .netdev/.network files you added
sudo rm -f /etc/systemd/network/25-mv-host.netdev \
/etc/systemd/network/25-mv-app.netdev \
/etc/systemd/network/35-mv-host.network \
/etc/systemd/network/36-mv-app.network
# Restore a normal parent .network with DHCP/static on enp1s0
sudo networkctl reload
# or: sudo systemctl restart systemd-networkd.service
Keep a known-good parent .network snippet nearby before you cut over. Cable-pull and reboot once after the first successful cutover—RequiredForOnline= mistakes show up there, not in a happy networkctl reload.
Security and ops notes
- MACVLAN guests are on your LAN. Treat them like physical hosts: firewall them (
nftableson the guest or at the gateway), do not assume “container” means “isolated from L2.” - IPVLAN private + untrusted netns is a better story when L2 spoofing is in the threat model; still enforce IP policy upstream.
-
passthruMACVLAN/MACVTAP is powerful and exclusive—do not stack casual guests on the same parent in that mode. - Parent NIC offloads, MTU, and bonding still matter: you can stack MACVLAN on a bond; keep MTU consistent end-to-end.
- This is not hairpin NAT. If you need LAN clients to reach a DNAT VIP on the gateway itself, that is a different nftables problem (SNAT/DNAT/hairpin NAT). MACVLAN solves “give the workload its own L2/L3 identity,” not “rewrite ports on a router.”
References
-
ip-link(8) — MACVLAN/MACVTAP modes (
private,vepa,bridge,passthru,source),bcqueuelen -
systemd.netdev(5) —
Kind=macvlan/ipvlan,[MACVLAN] Mode=,[IPVLAN] Mode=/Flags= -
systemd.network(5) — matching,
RequiredForOnline=,BindCarrier=, parent binding keys - IPVLAN Driver HOWTO — L2/L3/L3S, flags, macvlan vs ipvlan decision criteria, netns example
-
ArchWiki: systemd-networkd — MACVLAN bridge — host-on-MACVLAN pattern, nspawn
MACVLAN=/--network-macvlan=,mv-<parent>naming
Wrap-up
If you only remember three things:
- MACVLAN = per-guest MAC on a shared parent; IPVLAN = shared MAC, IP demux—pick IPVLAN when the switch or threat model hates extra MACs.
- For same-host east-west and host access, use bridge mode and put the host IP on a MACVLAN too, not on the bare parent.
- Make it boring with
systemd-networkd:.netdevfor the virtual device, parent.networkwithMACVLAN=/IPVLAN=, child.networkwith addressing +BindCarrier=.
That is how you stop paying bridge tax for every “just give this container a LAN IP” request—without pretending NAT hairpins or full software switches are free.
Top comments (0)