Add --ip flag to container create and run (#41374)
beena352 committed
Aug 19, 2026 at 12:23 UTC
5422fc47a17e75464e8e14cb74f64f35befe779d
11 files changed
+242
-2
localization/strings/en-US/Resources.resw
+8
@@ -2496,6 +2496,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2496
<value>Network aliases cannot be specified when multiple networks are requested. Use a single --network option.</value>
2497
<comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2498
</data>
2499
+ <data name="MessageWslcIpRequiresUserDefinedNetwork" xml:space="preserve">
2500
+ <value>An IP address requires a user-defined network. Use --network to specify one.</value>
2501
+ <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2502
+ </data>
2503
+ <data name="MessageWslcIpAmbiguousWithMultipleNetworks" xml:space="preserve">
2504
+ <value>An IP address cannot be specified when multiple networks are requested. Use a single --network option.</value>
2505
+ <comment>{Locked="--network "}Command line arguments, file names and string inserts should not be translated</comment>
2506
+ </data>
2507
<data name = "MessageWslcFailedToMountVolume" xml:space = "preserve" >
2508
<value>Failed to create volume '{}': {}</value>
2509
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/common/WSLCContainerLauncher.cpp
+12
-2
@@ -336,6 +336,11 @@ void wsl::windows::common::WSLCContainerLauncher::AddPrimaryNetworkAlias(const s
336
m_primaryNetworkAliases.push_back(Alias);
337
}
338
339
+void wsl::windows::common::WSLCContainerLauncher::SetPrimaryNetworkIpAddress(std::string&& Address)
340
+{
341
+ m_primaryNetworkIpAddress = std::move(Address);
342
+}
343
+
344
std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::LaunchNoThrow(
345
IWSLCSession& Session, WSLCContainerStartFlags Flags, IWarningCallback* WarningCallback)
346
{
@@ -485,14 +490,19 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
490
options.ContainerNetwork.Networks = connections.empty() ? nullptr : connections.data();
491
options.ContainerNetwork.NetworksCount = static_cast<ULONG>(connections.size());
492
488
- // Aliases for the primary endpoint.
493
+ // Settings for the primary endpoint.
494
std::vector<KeyValuePair> primarySettings;
490
- primarySettings.reserve(m_primaryNetworkAliases.size());
495
+ primarySettings.reserve(m_primaryNetworkAliases.size() + (m_primaryNetworkIpAddress.has_value() ? 1 : 0));
496
for (const auto& alias : m_primaryNetworkAliases)
497
{
498
primarySettings.push_back({.Key = "Aliases", .Value = alias.c_str()});
499
}
500
501
+ if (m_primaryNetworkIpAddress.has_value())
502
+ {
503
+ primarySettings.push_back({.Key = "IPAddress", .Value = m_primaryNetworkIpAddress->c_str()});
504
+ }
505
+
506
options.ContainerNetwork.Settings = primarySettings.empty() ? nullptr : primarySettings.data();
507
options.ContainerNetwork.SettingsCount = static_cast<ULONG>(primarySettings.size());
508
src/windows/common/WSLCContainerLauncher.h
+2
@@ -67,6 +67,7 @@ public:
67
void AddAdditionalNetwork(const std::string& Name);
68
void AddAdditionalNetwork(const std::string& Name, const std::vector<std::string>& Aliases);
69
void AddPrimaryNetworkAlias(const std::string& Alias);
70
+ void SetPrimaryNetworkIpAddress(std::string&& Address);
71
72
std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
73
RunningWSLCContainer Create(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
@@ -133,6 +134,7 @@ private:
134
std::vector<std::string> m_dnsOptions;
135
std::vector<NetworkConnection> m_additionalNetworks;
136
std::vector<std::string> m_primaryNetworkAliases;
137
+ std::optional<std::string> m_primaryNetworkIpAddress;
138
std::vector<WSLCLabel> m_labels;
139
std::deque<std::string> m_labelKeys;
140
std::deque<std::string> m_labelValues;
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -50,6 +50,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
50
Argument::Create(ArgType::HealthTimeout),
51
Argument::Create(ArgType::Hostname),
52
Argument::Create(ArgType::Interactive),
53
+ Argument::Create(ArgType::IpAddress, false),
54
Argument::Create(ArgType::Label, false, Limit::Unlimited),
55
Argument::Create(ArgType::Memory),
56
Argument::Create(ArgType::Mount, false, Limit::Unlimited),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1
@@ -50,6 +50,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
50
Argument::Create(ArgType::HealthTimeout),
51
Argument::Create(ArgType::Hostname),
52
Argument::Create(ArgType::Interactive),
53
+ Argument::Create(ArgType::IpAddress, false),
54
Argument::Create(ArgType::Label, false, Limit::Unlimited),
55
Argument::Create(ArgType::Memory),
56
Argument::Create(ArgType::Mount, false, Limit::Unlimited),
src/windows/wslc/services/ContainerModel.h
+2
@@ -87,6 +87,8 @@ struct ContainerOptions
87
std::vector<std::string> DnsOptions;
88
std::vector<ContainerNetwork> Networks;
89
std::vector<std::string> NetworkAliases;
90
+ std::optional<std::string> IpAddress{};
91
+ std::vector<std::string> Tmpfs;
92
std::vector<std::pair<std::string, std::string>> Labels;
93
std::optional<std::wstring> CidFile{};
94
std::optional<int64_t> MemoryBytes{};
src/windows/wslc/services/ContainerService.cpp
+12
@@ -105,6 +105,18 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
105
}
106
}
107
108
+ if (options.IpAddress.has_value())
109
+ {
110
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), options.Networks.empty());
111
+
112
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);
113
+
114
+ const auto& primary = options.Networks.front().Name;
115
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));
116
+
117
+ containerLauncher.SetPrimaryNetworkIpAddress(std::string(options.IpAddress.value()));
118
+ }
119
+
120
if (!options.Networks.empty())
121
{
122
const auto& primary = options.Networks.front();
src/windows/wslc/tasks/ContainerTasks.cpp
+5
@@ -822,6 +822,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
822
}
823
}
824
825
+ if (context.Args.Contains(ArgType::IpAddress))
826
+ {
827
+ options.IpAddress = WideToMultiByte(context.Args.GetValue<ArgType::IpAddress>());
828
+ }
829
+
830
if (context.Args.Contains(ArgType::User))
831
{
832
options.User = WideToMultiByte(context.Args.GetValue<ArgType::User>());
test/windows/WSLCTests.cpp
+38
@@ -8355,6 +8355,44 @@ class WSLCTests
8355
VERIFY_ARE_EQUAL(driverOptValue, opt->second);
8356
}
8357
8358
+ // Launcher-driven pinned IP alongside an alias — both settings survive the same KVP batch.
8359
+ {
8360
+ const std::string networkName = "alias-net-ip";
8361
+ const std::string ipAddress = "172.67.0.42";
8362
+ const std::string alias = "db";
8363
+ createNetwork(networkName, "172.67.0.0/16");
8364
+ auto netCleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
8365
+
8366
+ WSLCContainerLauncher launcher("debian:latest", "alias-ctr-ip", {"sleep", "99999"}, {}, networkName);
8367
+ launcher.AddPrimaryNetworkAlias(alias);
8368
+ launcher.SetPrimaryNetworkIpAddress(std::string(ipAddress));
8369
+ auto container = launcher.Launch(*m_defaultSession);
8370
+
8371
+ auto inspect = container.Inspect();
8372
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
8373
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
8374
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAddress);
8375
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
8376
+ VERIFY_ARE_EQUAL(ipAddress, endpoint.IPAMConfig->IPv4Address);
8377
+ VERIFY_IS_TRUE(std::ranges::find(endpoint.Aliases, alias) != endpoint.Aliases.end());
8378
+ }
8379
+
8380
+ // Pinned IP outside a user-defined network — rejected for callers that bypass the CLI checks.
8381
+ {
8382
+ auto expectEndpointSettingsError = [&](const std::string& containerName, const std::string& networkMode) {
8383
+ WSLCContainerLauncher launcher("debian:latest", containerName, {"sleep", "99999"}, {}, networkMode);
8384
+ launcher.SetPrimaryNetworkIpAddress("172.67.0.42");
8385
+
8386
+ auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
8387
+ VERIFY_ARE_EQUAL(E_INVALIDARG, retVal.first);
8388
+ ValidateCOMErrorMessage(std::format(
8389
+ L"Endpoint settings are not supported for network mode '{}'.", std::wstring(networkMode.begin(), networkMode.end())));
8390
+ };
8391
+
8392
+ expectEndpointSettingsError("alias-ctr-ip-host", "host");
8393
+ expectEndpointSettingsError("alias-ctr-ip-none", "none");
8394
+ }
8395
+
8396
// Primary endpoint Links: launch a target container with an alias, then a source with --link at create time.
8397
{
8398
const std::string networkName = "alias-net-link";
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+79
@@ -1410,6 +1410,85 @@ class WSLCE2EContainerCreateTests
1410
VerifyContainerIsNotListed(WslcContainerName);
1411
}
1412
1413
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_Success)
1414
+ {
1415
+ const std::wstring subnet = L"172.74.0.0/16";
1416
+ const std::wstring ipAddress = L"172.74.0.42";
1417
+
1418
+ auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1419
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1420
+ auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1421
+
1422
+ result = RunWslc(std::format(
1423
+ L"container create --name {} --network {} --ip {} {} true", WslcContainerName, TestNetworkName, ipAddress, DebianImage.NameAndTag()));
1424
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1425
+
1426
+ const auto inspect = InspectContainer(WslcContainerName);
1427
+ const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1428
+ const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1429
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1430
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1431
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1432
+ VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1433
+ }
1434
+
1435
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_NoNetwork_Rejected)
1436
+ {
1437
+ const std::wstring ipAddress = L"172.74.0.42";
1438
+
1439
+ auto result =
1440
+ RunWslc(std::format(L"container create --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1441
+ result.Verify(
1442
+ {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1443
+ .ExitCode = 1});
1444
+ VerifyContainerIsNotListed(WslcContainerName);
1445
+ }
1446
+
1447
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_BridgeMode_Rejected)
1448
+ {
1449
+ const std::wstring ipAddress = L"172.74.0.42";
1450
+
1451
+ auto result = RunWslc(std::format(
1452
+ L"container create --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1453
+ result.Verify(
1454
+ {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1455
+ .ExitCode = 1});
1456
+ VerifyContainerIsNotListed(WslcContainerName);
1457
+ }
1458
+
1459
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_MultipleNetworks_Rejected)
1460
+ {
1461
+ const std::wstring ipAddress = L"172.74.0.42";
1462
+
1463
+ auto result = RunWslc(std::format(
1464
+ L"container create --network bridge --network bridge --ip {} --name {} {} true",
1465
+ ipAddress,
1466
+ WslcContainerName,
1467
+ DebianImage.NameAndTag()));
1468
+ result.Verify({.Stdout = L"", .ExitCode = 1});
1469
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
1470
+ wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1471
+ VerifyContainerIsNotListed(WslcContainerName);
1472
+ }
1473
+
1474
+ WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ip_InvalidValue_Rejected)
1475
+ {
1476
+ const std::wstring badIp = L"not-an-ip";
1477
+
1478
+ auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1479
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1480
+ auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1481
+
1482
+ result = RunWslc(std::format(
1483
+ L"container create --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1484
+ result.Verify({.Stdout = L"", .ExitCode = 1});
1485
+ VERIFY_IS_TRUE(result.Stderr.has_value());
1486
+ VerifyPatternMatch(
1487
+ wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1488
+ std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1489
+ VerifyContainerIsNotListed(WslcContainerName);
1490
+ }
1491
+
1492
WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus)
1493
{
1494
auto result = RunWslc(std::format(L"container create --name {} --cpus 0.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+82
@@ -1041,6 +1041,88 @@ class WSLCE2EContainerRunTests
1041
result.StderrContainsSubstring(L"Invalid network-alias value: network alias cannot be empty or whitespace"));
1042
}
1043
1044
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_Success)
1045
+ {
1046
+ const std::wstring subnet = L"172.73.0.0/16";
1047
+ const std::wstring ipAddress = L"172.73.0.42";
1048
+
1049
+ auto result = RunWslc(std::format(L"network create --driver bridge --subnet {} {}", subnet, TestNetworkName));
1050
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1051
+ auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1052
+
1053
+ result = RunWslc(std::format(
1054
+ L"container run -d --name {} --network {} --ip {} {} sleep infinity",
1055
+ WslcContainerName,
1056
+ TestNetworkName,
1057
+ ipAddress,
1058
+ DebianImage.NameAndTag()));
1059
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1060
+ // Registered after the network so it runs first; the network cannot be deleted while the container holds an endpoint.
1061
+ auto cleanupContainer = wil::scope_exit([&] { EnsureContainerDoesNotExist(WslcContainerName); });
1062
+
1063
+ const auto inspect = InspectContainer(WslcContainerName);
1064
+ const auto networkName = wsl::shared::string::WideToMultiByte(TestNetworkName);
1065
+ const auto expectedIp = wsl::shared::string::WideToMultiByte(ipAddress);
1066
+ VERIFY_IS_TRUE(inspect.NetworkSettings.Networks.contains(networkName));
1067
+ const auto& endpoint = inspect.NetworkSettings.Networks.at(networkName);
1068
+ VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAddress);
1069
+ VERIFY_IS_TRUE(endpoint.IPAMConfig.has_value());
1070
+ VERIFY_ARE_EQUAL(expectedIp, endpoint.IPAMConfig->IPv4Address);
1071
+ }
1072
+
1073
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_NoNetwork_Rejected)
1074
+ {
1075
+ const std::wstring ipAddress = L"172.73.0.42";
1076
+
1077
+ auto result =
1078
+ RunWslc(std::format(L"container run --rm --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1079
+ result.Verify(
1080
+ {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1081
+ .ExitCode = 1});
1082
+ }
1083
+
1084
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_BridgeMode_Rejected)
1085
+ {
1086
+ const std::wstring ipAddress = L"172.73.0.42";
1087
+
1088
+ auto result = RunWslc(std::format(
1089
+ L"container run --rm --network bridge --ip {} --name {} {} true", ipAddress, WslcContainerName, DebianImage.NameAndTag()));
1090
+ result.Verify(
1091
+ {.Stderr = std::format(L"{}\r\nError code: E_INVALIDARG\r\n", wsl::shared::Localization::MessageWslcIpRequiresUserDefinedNetwork()),
1092
+ .ExitCode = 1});
1093
+ }
1094
+
1095
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_MultipleNetworks_Rejected)
1096
+ {
1097
+ const std::wstring ipAddress = L"172.73.0.42";
1098
+
1099
+ auto result = RunWslc(std::format(
1100
+ L"container run --rm --network bridge --network bridge --ip {} --name {} {} true",
1101
+ ipAddress,
1102
+ WslcContainerName,
1103
+ DebianImage.NameAndTag()));
1104
+ result.Verify({.Stdout = L"", .ExitCode = 1});
1105
+ VERIFY_IS_TRUE(result.StderrContainsSubstring(
1106
+ wsl::shared::Localization::MessageWslcIpAmbiguousWithMultipleNetworks() + L"\r\nError code: E_INVALIDARG"));
1107
+ }
1108
+
1109
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_Ip_InvalidValue_Rejected)
1110
+ {
1111
+ const std::wstring badIp = L"not-an-ip";
1112
+
1113
+ auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName));
1114
+ result.Verify({.Stderr = L"", .ExitCode = 0});
1115
+ auto cleanupNetwork = wil::scope_exit([&] { EnsureNetworkDoesNotExist(TestNetworkName); });
1116
+
1117
+ result = RunWslc(std::format(
1118
+ L"container run --rm --network {} --ip {} --name {} {} true", TestNetworkName, badIp, WslcContainerName, DebianImage.NameAndTag()));
1119
+ result.Verify({.Stdout = L"", .ExitCode = 1});
1120
+ VERIFY_IS_TRUE(result.Stderr.has_value());
1121
+ VerifyPatternMatch(
1122
+ wsl::shared::string::WideToMultiByte(result.Stderr.value()),
1123
+ std::format("*Invalid IP address '{}'*", wsl::shared::string::WideToMultiByte(badIp)));
1124
+ }
1125
+
1126
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_Success)
1127
{
1128
// Create a named volume