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"