Skip to content

sec(dind): Windows NAT nodes never open the dind host port, so every Windows docker command times out #162

Description

@luthermonson

On a Windows node running the default NAT container network, a job container cannot reach its own dind Docker API. Every docker command inside a Windows job fails:

Post "http://10.88.0.1:51064/v1.47/auth": dial tcp 10.88.0.1:51064: i/o timeout

This has blocked build-images.yml's Windows job since at least 2026-07-24 — the last successful Windows CI image build was 2026-07-12. Reproduced today on mfl-win-amd64-101 running v0.2.2 (runs 31526577360 and 31976688505, different ephemeral ports, identical failure).

Cause

On Windows the dind Docker API is exposed over TCP on the container network's gateway (10.88.0.1), because a bind-mounted unix socket is not available there. Reaching it is an inbound connection to the host.

The Windows host firewall is enabled on all three profiles with the effective inbound default of block:

Name    Enabled DefaultInboundAction
Domain     True        NotConfigured
Private    True        NotConfigured
Public     True        NotConfigured

and the only ephemerd rule present is unrelated:

DisplayName           Direction Action Enabled
ephemerd-metrics-9090   Inbound  Allow    True

windowsNetworking.openHostPort returns early unless cfg.L2BridgeEgress is set:

func (w *windowsNetworking) openHostPort(port int, containerIP string) error {
    if !w.cfg.L2BridgeEgress || w.plan == nil || w.plan.HostIP == "" {
        return nil
    }
    ...

So on a NAT node nothing ever opens the port, and the host silently drops the container's SYN. i/o timeout rather than connection refused is consistent with a drop, not a closed port.

This node has no [network] section at all — L2Bridge was never enabled here — so it takes the early return on every job.

Blast radius

Any Windows job that talks to the Docker API: docker build, docker login, docker push, testcontainers, anything using dind. Jobs that never touch Docker are unaffected, which is why this stayed invisible — the ephemerd release build passes on this node because it only runs mage.

Suggested fix

openHostPort/closeHostPort should open a scoped inbound allow on the NAT path too, not only under L2Bridge. Under NAT the natural scope is the container's own address within the NAT subnet, keeping the same per-container /32 scoping #152 established — a pool-wide allow would reintroduce exactly the cross-job dind takeover that issue closed.

Worth checking as part of this: whether the NAT path previously had its own inbound rule installation that was removed. #144 stripped a large amount of NAT-era firewall code (installNetshFirewallRules, hostFirewallRules, and others) on the grounds that it was egress-enforcement theatre, which was correct for egress — but if any of it was doing legitimate inbound allows for dind, that would be a regression introduced there. Note the timeline does not obviously fit: the Windows image build was already failing on 2026-07-24, well before #144 merged, so there may be two separate causes.

Why this matters now

It blocks rebuilding the Windows CI runner image, which in turn blocks #161 (seeding the Go tool cache so actions/setup-go stops timing out). So this is on the critical path for the Windows setup-go failures too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions