Macvlan networks and port publishing
This page explains how Trailer handles workspaces that mix a macvlan network with published host ports, and what the workspaceâs container image needs to provide for that combination to work.
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.
Network drivers
Section titled âNetwork driversâTrailer networks use one of two Docker drivers, selected at creation and immutable afterward:
- bridge: the default. No extra fields. Docker fills in the subnet and gateway.
- macvlan: gives the container a virtual network interface bridged at layer 2 onto a physical host NIC. Other hosts on that LAN segment can talk to the container directly at its macvlan IP, as if the container were a separate physical device.
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 in its heartbeat. |
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.
Port publishing
Section titled âPort publishingâPort publishing (-p hostPort:containerPort in Docker terms) 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). |