Add --network option to container create/run (excluding host mode) (#40720)

AmirMS committed Jun 8, 2026 at 12:47 UTC b8b7fbf9e2024f67800c2af84e2086b445eda771
11 files changed +267 -2
localization/strings/en-US/Resources.resw
+14
@@ -2795,6 +2795,16 @@ On first run, creates the file with all settings commented out at their defaults
2795 <data name="WSLCCLI_NameArgDescription" xml:space="preserve">
2796 <value>Name of the container</value>
2797 </data>
2798 + <data name="WSLCCLI_NetworkArgDescription" xml:space="preserve">
2799 + <value>Connect a container to a network</value>
2800 + </data>
2801 + <data name="WSLCCLI_NetworkEmptyError" xml:space="preserve">
2802 + <value>Invalid {} value: network name cannot be empty or whitespace</value>
2803 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2804 + </data>
2805 + <data name="WSLCCLI_NetworkHostModeNotSupportedError" xml:space="preserve">
2806 + <value>host mode networking is not supported</value>
2807 + </data>
2808 <data name="WSLCCLI_NoCacheArgDescription" xml:space="preserve">
2809 <value>Do not use cache when building the image</value>
2810 </data>
@@ -2878,6 +2888,10 @@ On first run, creates the file with all settings commented out at their defaults
2888 <data name="WSLCCLI_WorkingDirArgDescription" xml:space="preserve">
2889 <value>Working directory inside the container</value>
2890 </data>
2891 + <data name="WSLCCLI_WorkingDirEmptyError" xml:space="preserve">
2892 + <value>Invalid {} value: working directory cannot be empty or whitespace</value>
2893 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2894 + </data>
2895 <data name="WSLCCLI_CIDFileArgDescription" xml:space="preserve">
2896 <value>Write the container ID to the provided path</value>
2897 </data>
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -75,6 +75,7 @@ _(Last, "last", L"n", Kind::Value, L
75 _(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
76 _(Memory, "memory", L"m", Kind::Value, Localization::WSLCCLI_MemoryArgDescription()) \
77 _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
78 +_(Network, "network", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NetworkArgDescription()) \
79 _(NetworkName, "network-name", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_NetworkNameArgDescription()) \
80 /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
81 _(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+19 -1
@@ -101,7 +101,25 @@ void Argument::Validate(const ArgMap& execArgs) const
101 if (value.empty() ||
102 std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
103 {
104 - throw ArgumentException(std::format(L"Invalid {} argument value: working directory cannot be empty or whitespace", m_name));
104 + throw ArgumentException(Localization::WSLCCLI_WorkingDirEmptyError(m_name));
105 + }
106 + break;
107 + }
108 +
109 + case ArgType::Network:
110 + {
111 + for (const auto& value : execArgs.GetAll<ArgType::Network>())
112 + {
113 + if (value.empty() ||
114 + std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
115 + {
116 + throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name));
117 + }
118 +
119 + if (IsEqual(value, L"host", true))
120 + {
121 + throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
122 + }
123 }
124 break;
125 }
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -48,6 +48,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
48 Argument::Create(ArgType::Label, false, NO_LIMIT),
49 Argument::Create(ArgType::Memory),
50 Argument::Create(ArgType::Name),
51 + Argument::Create(ArgType::Network, false, NO_LIMIT),
52 // Argument::Create(ArgType::NoDNS),
53 // Argument::Create(ArgType::Progress),
54 Argument::Create(ArgType::Publish, false, NO_LIMIT),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1
@@ -48,6 +48,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
48 Argument::Create(ArgType::Label, false, NO_LIMIT),
49 Argument::Create(ArgType::Memory),
50 Argument::Create(ArgType::Name),
51 + Argument::Create(ArgType::Network, false, NO_LIMIT),
52 // Argument::Create(ArgType::NoDNS),
53 // Argument::Create(ArgType::Progress),
54 Argument::Create(ArgType::Publish, false, NO_LIMIT),
src/windows/wslc/services/ContainerModel.h
+1
@@ -50,6 +50,7 @@ struct ContainerOptions
50 std::vector<std::string> DnsServers;
51 std::vector<std::string> DnsSearchDomains;
52 std::vector<std::string> DnsOptions;
53 + std::vector<std::string> Networks;
54 std::vector<std::string> Tmpfs;
55 std::vector<std::pair<std::string, std::string>> Labels;
56 std::optional<std::wstring> CidFile{};
src/windows/wslc/services/ContainerService.cpp
+8 -1
@@ -51,8 +51,15 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
51 WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
52 WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);
53
54 + std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front();
55 +
56 wsl::windows::common::WSLCContainerLauncher containerLauncher(
55 - image, options.Name, options.Arguments, options.EnvironmentVariables, "bridge", processFlags);
57 + image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags);
58 +
59 + for (size_t i = 1; i < options.Networks.size(); ++i)
60 + {
61 + containerLauncher.AddAdditionalNetwork(options.Networks[i]);
62 + }
63
64 // Set port options if provided
65 for (const auto& port : options.Ports)
src/windows/wslc/tasks/ContainerTasks.cpp
+10
@@ -516,6 +516,16 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
516 }
517 }
518
519 + if (context.Args.Contains(ArgType::Network))
520 + {
521 + auto networks = context.Args.GetAll<ArgType::Network>();
522 + options.Networks.reserve(options.Networks.size() + networks.size());
523 + for (const auto& value : networks)
524 + {
525 + options.Networks.emplace_back(WideToMultiByte(value));
526 + }
527 + }
528 +
529 if (context.Args.Contains(ArgType::User))
530 {
531 options.User = WideToMultiByte(context.Args.Get<ArgType::User>());
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+100
@@ -345,6 +345,106 @@ class WSLCCLIExecutionUnitTests
345 command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
346 }
347
348 + TEST_METHOD(SetContainerOptionsFromArgs_WithoutNetwork_NetworksIsEmpty)
349 + {
350 + CLIExecutionContext context;
351 +
352 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
353 +
354 + const auto& options = context.Data.Get<Data::ContainerOptions>();
355 + VERIFY_IS_TRUE(options.Networks.empty());
356 + }
357 +
358 + TEST_METHOD(RunCommand_ParseNetworkSingleValue_SetsNetwork)
359 + {
360 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 ubuntu sh");
361 +
362 + ContainerRunCommand command{L""};
363 + CLIExecutionContext context;
364 + command.ParseArguments(invocation, context.Args);
365 + command.ValidateArguments(context.Args);
366 +
367 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
368 +
369 + const auto& options = context.Data.Get<Data::ContainerOptions>();
370 + VERIFY_ARE_EQUAL(1u, options.Networks.size());
371 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
372 + }
373 +
374 + TEST_METHOD(RunCommand_ParseNetworkMultipleValues_PreservesOrder)
375 + {
376 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");
377 +
378 + ContainerRunCommand command{L""};
379 + CLIExecutionContext context;
380 + command.ParseArguments(invocation, context.Args);
381 + command.ValidateArguments(context.Args);
382 +
383 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
384 +
385 + const auto& options = context.Data.Get<Data::ContainerOptions>();
386 + VERIFY_ARE_EQUAL(2u, options.Networks.size());
387 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
388 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
389 + }
390 +
391 + TEST_METHOD(RunCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
392 + {
393 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");
394 +
395 + ContainerRunCommand command{L""};
396 + CLIExecutionContext context;
397 + command.ParseArguments(invocation, context.Args);
398 +
399 + VERIFY_THROWS_SPECIFIC(
400 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
401 + }
402 +
403 + TEST_METHOD(CreateCommand_ParseNetworkSingleValue_SetsNetwork)
404 + {
405 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 ubuntu sh");
406 +
407 + ContainerCreateCommand command{L""};
408 + CLIExecutionContext context;
409 + command.ParseArguments(invocation, context.Args);
410 + command.ValidateArguments(context.Args);
411 +
412 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
413 +
414 + const auto& options = context.Data.Get<Data::ContainerOptions>();
415 + VERIFY_ARE_EQUAL(1u, options.Networks.size());
416 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
417 + }
418 +
419 + TEST_METHOD(CreateCommand_ParseNetworkMultipleValues_PreservesOrder)
420 + {
421 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network net1 --network net2 ubuntu sh");
422 +
423 + ContainerCreateCommand command{L""};
424 + CLIExecutionContext context;
425 + command.ParseArguments(invocation, context.Args);
426 + command.ValidateArguments(context.Args);
427 +
428 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
429 +
430 + const auto& options = context.Data.Get<Data::ContainerOptions>();
431 + VERIFY_ARE_EQUAL(2u, options.Networks.size());
432 + VERIFY_ARE_EQUAL(std::string("net1"), options.Networks[0]);
433 + VERIFY_ARE_EQUAL(std::string("net2"), options.Networks[1]);
434 + }
435 +
436 + TEST_METHOD(CreateCommand_ParseNetworkEmptyValue_ThrowsArgumentException)
437 + {
438 + auto invocation = CreateInvocationFromCommandLine(L"wslc --network \"\" ubuntu sh");
439 +
440 + ContainerCreateCommand command{L""};
441 + CLIExecutionContext context;
442 + command.ParseArguments(invocation, context.Args);
443 +
444 + VERIFY_THROWS_SPECIFIC(
445 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
446 + }
447 +
448 // Test: Command Line test parsing all cases defined in CommandLineTestCases.h
449 // This test verifies the command line parsing logic used by the CLI and executes the same
450 // code as the CLI up to the point of command execution, including parsing and argument validtion.
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+60
@@ -44,6 +44,7 @@ class WSLCE2EContainerCreateTests
44 EnsureContainerDoesNotExist(WslcContainerName);
45 EnsureImageIsDeleted(AlpineImage);
46 EnsureImageIsDeleted(DebianImage);
47 + EnsureNetworkDoesNotExist(TestNetworkName);
48
49 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
50 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
@@ -58,6 +59,7 @@ class WSLCE2EContainerCreateTests
59 VolumeTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
60 VolumeTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
61 EnsureContainerDoesNotExist(WslcContainerName);
62 + EnsureNetworkDoesNotExist(TestNetworkName);
63 return true;
64 }
65
@@ -768,10 +770,67 @@ class WSLCE2EContainerCreateTests
770 }
771 }
772
773 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_DefaultIsBridge)
774 + {
775 + auto result = RunWslc(std::format(L"container create --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
776 + result.Verify({.Stderr = L"", .ExitCode = 0});
777 +
778 + const auto inspect = InspectContainer(WslcContainerName);
779 + VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
780 + }
781 +
782 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_HostMode_Rejected)
783 + {
784 + auto result =
785 + RunWslc(std::format(L"container create --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
786 + result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
787 + VerifyContainerIsNotListed(WslcContainerName);
788 + }
789 +
790 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_HostMode_WithMultipleNetworks_Rejected)
791 + {
792 + auto result = RunWslc(std::format(
793 + L"container create --name {} --network bridge --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
794 + result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
795 + VerifyContainerIsNotListed(WslcContainerName);
796 + }
797 +
798 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_UserDefinedNetwork)
799 + {
800 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
801 + result.Verify({.Stderr = L"", .ExitCode = 0});
802 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
803 +
804 + result = RunWslc(std::format(
805 + L"container create --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
806 + result.Verify({.Stderr = L"", .ExitCode = 0});
807 +
808 + const auto inspect = InspectContainer(WslcContainerName);
809 + VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
810 + }
811 +
812 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_EmptyValue_Rejected)
813 + {
814 + auto result = RunWslc(std::format(L"container create --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
815 + result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
816 + VerifyContainerIsNotListed(WslcContainerName);
817 + }
818 +
819 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Network_NonexistentNetwork_Rejected)
820 + {
821 + auto result = RunWslc(
822 + std::format(L"container create --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
823 + result.Verify({.Stderr = L"Network not found: 'does-not-exist'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", .ExitCode = 1});
824 + VerifyContainerIsNotListed(WslcContainerName);
825 + }
826 +
827 private:
828 // Test container name
829 const std::wstring WslcContainerName = L"wslc-test-container";
830
831 + // Test network name
832 + const std::wstring TestNetworkName = L"wslc-test-network";
833 +
834 // Test environment variables
835 const std::wstring HostEnvVariableName = L"WSLC_TEST_HOST_ENV";
836 const std::wstring HostEnvVariableName2 = L"WSLC_TEST_HOST_ENV2";
@@ -842,6 +901,7 @@ private:
901 << L" -l,--label Set metadata on an object\r\n"
902 << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
903 << L" --name Name of the container\r\n"
904 + << L" --network Connect a container to a network\r\n"
905 << L" -p,--publish Publish a port from a container to host\r\n"
906 << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
907 << L" --rm Remove the container after it stops\r\n"
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+52
@@ -44,6 +44,7 @@ class WSLCE2EContainerRunTests
44 EnsureImageIsDeleted(DebianImage);
45 EnsureImageIsDeleted(PythonImage);
46 EnsureVolumeDoesNotExist(WslcVolumeName);
47 + EnsureNetworkDoesNotExist(TestNetworkName);
48
49 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
50 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), nullptr));
@@ -58,6 +59,7 @@ class WSLCE2EContainerRunTests
59 EnsureContainerDoesNotExist(WslcContainerName);
60 EnsureContainerDoesNotExist(WslcContainerName2);
61 EnsureVolumeDoesNotExist(WslcVolumeName);
62 + EnsureNetworkDoesNotExist(TestNetworkName);
63
64 EnvTestFile1 = wsl::windows::common::filesystem::GetTempFilename();
65 EnvTestFile2 = wsl::windows::common::filesystem::GetTempFilename();
@@ -647,6 +649,52 @@ class WSLCE2EContainerRunTests
649 VERIFY_IS_TRUE(result.Stdout->find(L"options ndots:5 timeout:3") != std::wstring::npos);
650 }
651
652 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_DefaultIsBridge)
653 + {
654 + auto result = RunWslc(std::format(L"container run --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
655 + result.Verify({.Stderr = L"", .ExitCode = 0});
656 +
657 + const auto inspect = InspectContainer(WslcContainerName);
658 + VERIFY_ARE_EQUAL(std::string("bridge"), inspect.HostConfig.NetworkMode);
659 + }
660 +
661 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_HostMode_Rejected)
662 + {
663 + auto result =
664 + RunWslc(std::format(L"container run --name {} --network host {} true", WslcContainerName, DebianImage.NameAndTag()));
665 + result.Verify({.Stderr = L"host mode networking is not supported\r\n", .ExitCode = 1});
666 + VerifyContainerIsNotListed(WslcContainerName);
667 + }
668 +
669 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_UserDefinedNetwork)
670 + {
671 + auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
672 + result.Verify({.Stderr = L"", .ExitCode = 0});
673 + auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
674 +
675 + result = RunWslc(std::format(
676 + L"container run --name {} --network {} {} true", WslcContainerName, TestNetworkName, DebianImage.NameAndTag()));
677 + result.Verify({.Stderr = L"", .ExitCode = 0});
678 +
679 + const auto inspect = InspectContainer(WslcContainerName);
680 + VERIFY_ARE_EQUAL(wsl::shared::string::WideToMultiByte(TestNetworkName), inspect.HostConfig.NetworkMode);
681 + VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(wsl::shared::string::WideToMultiByte(TestNetworkName)));
682 + }
683 +
684 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_EmptyValue_Rejected)
685 + {
686 + auto result =
687 + RunWslc(std::format(L"container run --rm --network \"\" --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
688 + result.Verify({.Stderr = L"Invalid network value: network name cannot be empty or whitespace\r\n", .ExitCode = 1});
689 + }
690 +
691 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Network_NonexistentNetwork_Rejected)
692 + {
693 + auto result = RunWslc(std::format(
694 + L"container run --rm --network does-not-exist --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
695 + result.Verify({.Stderr = L"Network not found: 'does-not-exist'\r\nError code: WSLC_E_NETWORK_NOT_FOUND\r\n", .ExitCode = 1});
696 + }
697 +
698 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
699 {
700 // Create a named volume
@@ -850,6 +898,9 @@ private:
898 // Test named volume
899 const std::wstring WslcVolumeName = L"wslc-test-volume";
900
901 + // Test user-defined network
902 + const std::wstring TestNetworkName = L"wslc-test-network";
903 +
904 std::wstring GetHelpMessage() const
905 {
906 std::wstringstream output;
@@ -903,6 +954,7 @@ private:
954 << L" -l,--label Set metadata on an object\r\n"
955 << L" -m,--memory Memory limit (e.g. 512M, 1G)\r\n"
956 << L" --name Name of the container\r\n"
957 + << L" --network Connect a container to a network\r\n"
958 << L" -p,--publish Publish a port from a container to host\r\n"
959 << L" -P,--publish-all Publish all exposed ports to random host ports\r\n"
960 << L" --rm Remove the container after it stops\r\n"