Macvlan Networks
The macvlan driver gives a workspace its own address on your LAN. This page covers the macvlan-specific fields, and what happens when you combine a macvlan network with published host ports on the same workspace.
If you are not using macvlan, none of this applies. If you are using macvlan but not publishing any host ports, none of this applies either. The interaction only matters when both are present at the same time on the same workspace.
Macvlan fields
Section titled “Macvlan fields”A macvlan network requires three fields and accepts one optional field:
| Field | Required | Notes |
|---|---|---|
parentInterface | Yes | Must be one of the interfaces the agent reported for its host (see Host Interface Detection). |
subnet | Yes | Must be canonical (host bits zeroed), e.g. 192.168.1.0/24. |
gateway | Yes | Must fall inside the subnet. |
ipRange | No | When set, must be canonical and inside the subnet. When blank, Docker uses the whole subnet for IPAM. |
The driver is immutable after creation. Trailer networks cannot be edited into a different driver, so a non-canonical subnet would leave you stuck recreating. The API rejects it at the boundary instead.
Combined with port publishing
Section titled “Combined with port publishing”See Port Publishing for how to publish host ports from the workspace form. In Docker terms, a published port (-p hostPort:containerPort) installs DNAT rules on the host so that traffic to host-ip:hostPort is rewritten to container-bridge-ip:containerPort and routed in via the Docker bridge. This is how a workspace becomes reachable through the Docker host’s own IP.
Macvlan and port publishing solve different problems. Macvlan is “give my workspace a real LAN address”. Port publishing is “expose my workspace through the host’s address”. Trailer supports using both at the same time on a single workspace, but the combination has a routing caveat.
The caveat: macvlan child cannot reach its own parent NIC
Section titled “The caveat: macvlan child cannot reach its own parent NIC”When Docker attaches a macvlan endpoint to a container, the kernel installs a connected route for the macvlan’s subnet on the macvlan interface inside the container. If the macvlan network is configured with subnet 192.168.1.0/24, the container’s routing table gains:
192.168.1.0/24 dev <macvlan-iface> scope linkOn Linux workspaces Docker assigns the interface name (eth0, eth1, and so on). On Windows VDI workspaces the agent pins it to mvlan0 (so the Windows VM runtime can bridge onto it). The route behaves the same either way.
This route is more specific than the bridge default route, so any reply from the container to an IP in 192.168.1.0/24 is sent out the macvlan interface instead of the bridge.
That breaks the port-publishing reply path whenever one of the host’s own IPs falls inside the macvlan subnet. The Linux kernel forbids a macvlan child interface from communicating with its own parent NIC. So when the container tries to reply to the host’s LAN IP out the macvlan interface, the packet is dropped. Inbound port-published traffic from the host arrived via the bridge (after host-side DNAT), and the reply must leave over the same bridge so the host’s reverse NAT can complete. Routed out the macvlan toward the parent instead, it silently dies and the connection hangs.
This is only a problem when a host interface holds an IP that falls inside the macvlan’s subnet. Two common deployment shapes:
| Setup | Host IP in macvlan subnet? | Port publishing works without extra steps? |
|---|---|---|
| Shared NIC: the same physical NIC carries both the host’s LAN IP and the macvlan parent. Typical of single-NIC homelab or developer machines. | Yes | No, see “What Trailer does” below |
| Dedicated NIC: the macvlan parent is a NIC that has no host IP (or carries an address on a different subnet). Typical of lab and production setups with a separate macvlan NIC. | No | Yes |
In the dedicated-NIC case, no host IP falls inside the macvlan subnet, so the connected route never misdirects a reply to the host. Clients on the macvlan segment reach the container at its macvlan IP directly and do not use port publishing at all.
What Trailer does
Section titled “What Trailer does”On every workspace start, Trailer inspects the macvlan networks attached to the workspace. For each host interface whose IP falls inside an attached macvlan’s subnet (the shared-NIC case), the agent runs a privileged Docker exec inside the container, adding a /32 host-route exception per overlapping host IP:
ip route replace <host-ip>/32 via <bridge-gateway>This sends replies destined for that specific host IP out the Docker bridge instead of the macvlan interface, which is exactly what the port-publishing reply path needs. The broader connected route for the macvlan subnet stays intact, so the container can still reach the rest of the LAN directly over the macvlan.
The agent only does this when the container also has the default Docker bridge attached (there is no published-port reply path to protect otherwise), and the exec is issued privileged so the container itself does not need CAP_NET_ADMIN.
After the fix:
- Ingress for traffic destined to the macvlan IP arrives over the macvlan parent NIC and the container’s macvlan interface.
- Ingress for traffic destined to a published host port arrives over the Docker bridge, after host DNAT.
- Egress to the overlapping host IPs leaves over the Docker bridge (the
/32exceptions). This is what lets a client on the Docker host itself reach a published port. - Egress to everything else on the LAN still leaves directly over the macvlan, under the container’s macvlan IP.
The cost is narrow: only traffic the container initiates to one of the host’s own LAN IPs takes the bridge detour. General LAN egress under the macvlan IP is preserved. Keeping the connected route intact also matters for the Windows VDI VM-discovery step described below.
Requirement: ip (iproute2) in the container image
Section titled “Requirement: ip (iproute2) in the container image”The fix is applied by executing the ip command (from the iproute2 package) inside the container. The image must therefore have the ip binary present. If it does not, the exec fails. The workspace still comes up, but its published ports are unreachable through the host’s IP, and the agent logs a warning:
agent: workspace <id> started but host-route exception setup failed(published ports may be unreachable via host IP): ...To check whether an image ships ip:
docker run --rm <image> which ipIf it is missing, two ways to address it:
- Add
iproute2to your image (apt-get install -y iproute2,apk add iproute2, or equivalent). This is the recommended fix and is a one-line image change. - Avoid the conflict instead. Either use a dedicated macvlan parent NIC that is not also the host’s LAN NIC, or drop the macvlan and rely on the bridge default plus port publishing only.
Windows VDI specifics: forwarding published ports into the VM
Section titled “Windows VDI specifics: forwarding published ports into the VM”Windows VDI workspaces run a real Windows VM. Whenever a macvlan network is attached to a Windows VDI workspace, the agent configures the VM runtime to bridge the VM onto the LAN over the container’s macvlan interface. The runtime creates a macvtap on top of that interface and the VM does its own DHCP onto the LAN. The VM appears on the LAN at its own DHCP-assigned IP, separate from the container’s macvlan IP. Services running inside the VM (RDP on 3389, an HTTP server on 8080, anything else) listen on that VM IP, not on the container’s bridge IP.
That is a problem for port publishing on its own. A Docker port mapping like 9090:8080 installs DNAT from host:9090 to container-bridge-IP:8080, landing the traffic on the container’s bridge interface, where nothing is listening. The VM runtime’s built-in port forwarding is used only when a bridge-style network is attached. A macvlan-only Windows VDI workspace gets none, so the runtime does not forward the ports and the watcher below fills the gap. (The two are independent: a workspace with both a bridge and a macvlan attached gets LAN bridging from the macvlan and built-in port forwarding from the bridge.) Without further help, the connection dies in the container.
To close that gap, the container’s entrypoint (part of the Windows base image) starts a background watcher when the VM is bridged onto the LAN and at least one port is published. The watcher:
-
Reads the VM’s MAC from the macvtap device the VM runtime created.
-
Discovers the VM’s DHCP-assigned IP by matching that MAC against the macvlan parent’s ARP/neighbor cache. (This is one reason the agent keeps the connected route intact rather than flushing it.)
-
Resolves the container’s own default-bridge IP from the default route.
-
Installs, inside the container, one PREROUTING DNAT rule per published port plus one POSTROUTING MASQUERADE rule:
Terminal window # per published port: redirect what the host DNAT delivered to us, on to the VMiptables -t nat -A PREROUTING -d <bridge-ip>/32 -p tcp --dport <port> \-j DNAT --to-destination <vm-ip>:<port># once: rewrite the source so the VM can reply back to usiptables -t nat -A POSTROUTING -d <vm-ip>/32 -o <macvlan-dev> -j MASQUERADE
Both rules are required. The DNAT moves the traffic from the container’s bridge interface to the VM. The MASQUERADE rewrites the source to the container’s macvlan IP, so the VM (which only knows the LAN, not Docker’s internal bridge addresses) can route its reply back to the container, where conntrack reverses every hop.
Packet flow
Section titled “Packet flow”A request to http://<host-ip>:9090/ reaching a server on :8080 inside the VM crosses three NAT translations on the way in, all reversed by conntrack on the way out:
flowchart TD client["Browser to host-ip:9090"] hostdnat["1. Docker host DNAT<br/>host-ip:9090 to container-bridge-ip:8080"] prerouting["2. Container PREROUTING DNAT<br/>container-bridge-ip:8080 to vm-ip:8080"] postrouting["3. Container POSTROUTING MASQUERADE<br/>src rewritten to mvlan0 IP"] vm["Windows VM<br/>eth0 = vm-ip (DHCP), server on :8080<br/>sees src = mvlan0 IP"] client --> hostdnat --> prerouting --> postrouting postrouting -->|"out the macvlan child, L2-bridged to the VM"| vm vm -.->|"reply: un-MASQUERADE, un-DNAT, out the bridge, host un-DNAT"| client
The VM is unaware of any of this. It just sees a normal LAN client at the container’s macvlan IP talking to its service port.
| Service runs in | Reachable via published host port? | Reachable via VM’s LAN IP? |
|---|---|---|
Container itself (e.g. the noVNC viewer on :8006) | Yes, port publishing works as normal | No (the VM, not the container, is on the LAN) |
Windows VM (RDP :3389, custom services on any port) | Yes, via the entrypoint watcher above | Yes |
The watcher lives in the Windows base image, so existing workspaces built against an older image need a rebuild to pick it up. If the VM never finishes DHCP, the watcher exits without installing rules, and the VM is still reachable directly at its own LAN IP.
Summary
Section titled “Summary”| Combination | Works out of the box? |
|---|---|
| Bridge only + port publishing | Yes |
| Macvlan only, dedicated NIC, no port publishing | Yes |
| Macvlan only, shared NIC, no port publishing | Yes |
| Macvlan (dedicated NIC) + port publishing | Yes |
| Macvlan (shared NIC) + port publishing (Linux workspace) | Yes, provided the container image has the ip command. The agent adds host-route exceptions at start. |
| Macvlan + port publishing (Windows VDI) | Yes for both container-served and VM-served ports (the latter via the entrypoint watcher, which requires a current Windows base image). |