Containerd on Windows
Mark Elvers
4 min read

Categories

  • containerd

Tags

  • tunbury.org

If you were following along with my previous post on containerd on Windows, you may recall that I lamented the lack of an installer. Since then, I have found a PowerShell script on Microsoft’s GitHub, which does a lot of the grunt work for us.

Trying anything beyond my echo Hello test showed an immediate problem: there is no network. ipconfig didn’t display any network interfaces.

C:\>ctr run --rm mcr.microsoft.com/windows/nanoserver:ltsc2022 my-container ipconfig

Windows IP Configuration

Checking the command line options, there is one called --net-host, which sounded promising, only for that to be immediately dashed:

C:\>ctr run --rm --net-host mcr.microsoft.com/windows/nanoserver:ltsc2022 my-container ipconfig
ctr: Cannot use host mode networking with Windows containers

The solution is --cni, but more work is required to get that working. We need to download the plugins and populate them in the cni/bin subdirectory. Fortunately, the installation script does all of this for us but leaves it unconfigured.

C:\Windows\System32>ctr run --rm --cni mcr.microsoft.com/windows/nanoserver:ltsc2022 my-container ipconfig
ctr: no network config found in C:\Program Files\containerd\cni\conf: cni plugin not initialized

From the top, this is how you get from a fresh install of Windows 11, to a container with networking. Firstly, use installation script to install containerd.

curl.exe https://raw.githubusercontent.com/microsoft/Windows-Containers/refs/heads/Main/helpful_tools/Install-ContainerdRuntime/install-containerd-runtime.ps1 -o install-containerd-runtime.ps1
Set-ExecutionPolicy Bypass
.\install-containerd-runtime.ps1 -ContainerDVersion 2.1.1 -WinCNIVersion 0.3.1 -ExternalNetAdapter Ethernet

Now create C:\Program Files\containerd\cni\conf\0-containerd-nat.conf containing the following:

{
    "cniVersion": "0.3.0",
    "name": "nat",
    "type": "nat",
    "master": "Ethernet",
    "ipam": {
        "subnet": "172.20.0.0/16",
        "routes": [
            {
                "gateway": "172.20.0.1"
            }
        ]
    },
    "capabilities": {
        "portMappings": true,
        "dns": true
    }
}

Easy when you know how…

C:\>ctr run --rm --cni mcr.microsoft.com/windows/nanoserver:ltsc2022 my-container ping 1.1.1.1

Pinging 1.1.1.1 with 32 bytes of data:
Reply from 1.1.1.1: bytes=32 time=5ms TTL=58
Reply from 1.1.1.1: bytes=32 time=7ms TTL=58
Reply from 1.1.1.1: bytes=32 time=7ms TTL=58
Reply from 1.1.1.1: bytes=32 time=6ms TTL=58

Ping statistics for 1.1.1.1:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 5ms, Maximum = 7ms, Average = 6ms

The next challenge is, what do you put in your own config.json to reproduce this behaviour?

Firstly, we need our layerFolders:

C:\>ctr snapshot ls
KEY                                                                     PARENT KIND
sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355        Committed
C:\>ctr snapshot prepare --mounts my-snapshot sha256:44b913d145adda5364b5465664644b11282ed3c4b9bd9739aa17832ee4b2b355
[
    {
        "Type": "windows-layer",
        "Source": "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\14",
        "Target": "",
        "Options": [
            "rw",
            "parentLayerPaths=[\"C:\\\\ProgramData\\\\containerd\\\\root\\\\io.containerd.snapshotter.v1.windows\\\\snapshots\\\\1\"]"
        ]
    }
]

Let’s create a config.json without a network stanza just to check we can create a container:

{
  "ociVersion": "1.1.0",
  "process": {
    "terminal": false,
    "user": { "uid": 0, "gid": 0 },
    "args": [
      "cmd", "/c",
      "ipconfig && ping 1.1.1.1"
    ],
    "cwd": "c:\\"
  },
  "root": { "path": "", "readonly": false },
  "hostname": "builder",
  "windows": {
    "layerFolders": [
      "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\1",
      "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\14"
    ],
    "ignoreFlushesDuringBoot": true
  }
}

The container runs, but there is no network as we’d expect.

C:\>ctr run --rm --config config.json my-container

Windows IP Configuration


Pinging 1.1.1.1 with 32 bytes of data:
PING: transmit failed. General failure.
PING: transmit failed. General failure.
PING: transmit failed. General failure.
PING: transmit failed. General failure.

If we turn on CNI, it crypically tells us what we need to do:

C:\>ctr run --rm --cni --config config.json my-container
ctr: plugin type="nat" name="nat" failed (add): required env variables [CNI_NETNS] missing

So we need to populate the network.networkNamespace with the name (ID) of the network we want to use. This should be a GUID, and I don’t know how to get the right value. I would have assumed that it was one of the many GUID’s returned by Get-HnsNetwork but it isn’t.

PS C:\> Get-HnsNetwork


ActivityId             : 92018CF0-6DCB-4AAF-A14E-DC61120FC958
AdditionalParams       :
CurrentEndpointCount   : 0
Extensions             : {@{Id=E7C3B2F0-F3C5-48DF-AF2B-10FED6D72E7A; IsEnabled=False; Name=Microsoft Windows Filtering Platform},
                         @{Id=F74F241B-440F-4433-BB28-00F89EAD20D8; IsEnabled=False; Name=Microsoft Azure VFP Switch Filter Extension},
                         @{Id=430BDADD-BAB0-41AB-A369-94B67FA5BE0A; IsEnabled=True; Name=Microsoft NDIS Capture}}
Flags                  : 8
Health                 : @{LastErrorCode=0; LastUpdateTime=133943927149605101}
ID                     : 3EB2B18B-A1DD-46A8-A425-256F6B3DF26D
IPv6                   : False
LayeredOn              : 20791F67-012C-4C9B-9C93-530FDA5DE4FA
MacPools               : {@{EndMacAddress=00-15-5D-C3-DF-FF; StartMacAddress=00-15-5D-C3-D0-00}}
MaxConcurrentEndpoints : 1
Name                   : nat
NatName                : NATAC317D6D-8A2E-4E4E-9BCF-33435FE4CD8F
Policies               : {@{Type=VLAN; VLAN=1}}
State                  : 1
Subnets                : {@{AdditionalParams=; AddressPrefix=172.20.0.0/16; Flags=0; GatewayAddress=172.20.0.1; Health=;
                         ID=5D56CE8D-1AD2-47FF-85A7-A0E6D530565D; IpSubnets=System.Object[]; ObjectType=5; Policies=System.Object[]; State=0}}
SwitchGuid             : 3EB2B18B-A1DD-46A8-A425-256F6B3DF26D
TotalEndpoints         : 2
Type                   : NAT
Version                : 64424509440
Resources              : @{AdditionalParams=; AllocationOrder=2; Allocators=System.Object[]; CompartmentOperationTime=0; Flags=0; Health=;
                         ID=92018CF0-6DCB-4AAF-A14E-DC61120FC958; PortOperationTime=0; State=1; SwitchOperationTime=0; VfpOperationTime=0;
                         parentId=71FB2758-F714-4838-8764-7079378D6CB6}

I ran ctr run --rm --cni mcr.microsoft.com/windows/nanoserver:ltsc2022 my-container cmd /c "ping 1.1.1.1 && pause" in one window and ran ctr c info my-container in another, which revealed a GUID was 5f7d467c-3011-48bc-9337-ce78cf399345.

Adding this to my config.json

{
  "ociVersion": "1.1.0",
  "process": {
    "terminal": false,
    "user": { "uid": 0, "gid": 0 },
    "args": [
      "cmd", "/c",
      "ipconfig && ping 1.1.1.1"
    ],
    "cwd": "c:\\"
  },
  "root": { "path": "", "readonly": false },
  "hostname": "builder",
  "windows": {
    "layerFolders": [
      "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\1",
      "C:\\ProgramData\\containerd\\root\\io.containerd.snapshotter.v1.windows\\snapshots\\14"
    ],
    "ignoreFlushesDuringBoot": true,
    "network": {
      "allowUnqualifiedDNSQuery": true,
      "networkNamespace": "5f7d467c-3011-48bc-9337-ce78cf399345"
    }
  }
}

And now I have a network!

C:\>ctr run --rm --cni --config config.json my-container

Windows IP Configuration


Ethernet adapter vEthernet (default-my-container2_nat):

   Connection-specific DNS Suffix  . : Home
   Link-local IPv6 Address . . . . . : fe80::921d:1ce7:a445:8dfa%49
   IPv4 Address. . . . . . . . . . . : 172.20.95.58
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . : 172.20.0.1

Pinging 1.1.1.1 with 32 bytes of data:
Reply from 1.1.1.1: bytes=32 time=5ms TTL=58
Reply from 1.1.1.1: bytes=32 time=6ms TTL=58
Reply from 1.1.1.1: bytes=32 time=6ms TTL=58
Reply from 1.1.1.1: bytes=32 time=6ms TTL=58

Ping statistics for 1.1.1.1:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 5ms, Maximum = 6ms, Average = 5ms