Skip to content
Trailer.devDocumentation

Search is only available in production builds. Try building and previewing the site to test it out locally.

Macvlan networks and port publishing

The network creation form with the macvlan driver selected, showing the parent interface, subnet, gateway, and IP range fields.

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.

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:

FieldRequiredNotes
parentInterfaceYesMust be one of the interfaces the agent reported in its heartbeat.
subnetYesMust be canonical (host bits zeroed), e.g. 192.168.1.0/24.
gatewayYesMust fall inside the subnet.
ipRangeNoWhen 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 (-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 link

On 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:

SetupHost 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.YesNo, 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.NoYes

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.

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:

Terminal window
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 /32 exceptions). 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.

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:

Terminal window
docker run --rm <image> which ip

If it is missing, two ways to address it:

  1. Add iproute2 to your image (apt-get install -y iproute2, apk add iproute2, or equivalent). This is the recommended fix and is a one-line image change.
  2. 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:

  1. Reads the VM’s MAC from the macvtap device the VM runtime created.

  2. 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.)

  3. Resolves the container’s own default-bridge IP from the default route.

  4. 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 VM
    iptables -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 us
    iptables -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.

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 inReachable via published host port?Reachable via VM’s LAN IP?
Container itself (e.g. the noVNC viewer on :8006)Yes, port publishing works as normalNo (the VM, not the container, is on the LAN)
Windows VM (RDP :3389, custom services on any port)Yes, via the entrypoint watcher aboveYes

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.

CombinationWorks out of the box?
Bridge only + port publishingYes
Macvlan only, dedicated NIC, no port publishingYes
Macvlan only, shared NIC, no port publishingYes
Macvlan (dedicated NIC) + port publishingYes
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).