diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index d95f6a646..2051750d2 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2336,8 +2336,8 @@ For privacy information about this product please visit https://aka.ms/privacy.<
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
- Unsupported network driver option: '{}'
- {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+ Network driver option '{}' is case-sensitive. Use the exact casing: Internal, Subnet, or Gateway.
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="Internal"}{Locked="Subnet"}{Locked="Gateway"}
Network driver option 'Gateway' requires 'Subnet' to also be specified.
diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h
index a7e998bc6..13c354a06 100644
--- a/src/windows/inc/docker_schema.h
+++ b/src/windows/inc/docker_schema.h
@@ -146,9 +146,10 @@ struct CreateNetwork
std::string Driver;
bool Internal{};
std::optional IPAM;
+ std::optional> Options;
std::map Labels;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateNetwork, Name, Driver, Internal, IPAM, Labels);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateNetwork, Name, Driver, Internal, IPAM, Options, Labels);
};
struct Network
@@ -159,9 +160,10 @@ struct Network
std::string Scope;
bool Internal{};
IPAM IPAM;
+ std::optional> Options;
std::map Labels;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
};
struct ContainerNetworkRequest
diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h
index db4354ba3..3e4bbd545 100644
--- a/src/windows/inc/wslc_schema.h
+++ b/src/windows/inc/wslc_schema.h
@@ -194,9 +194,10 @@ struct Network
std::string Scope;
bool Internal{};
IPAM IPAM;
+ std::optional> Options;
std::map Labels;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Labels);
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, Options, Labels);
};
} // namespace wsl::windows::common::wslc_schema
diff --git a/src/windows/wslcsession/WSLCNetworkMetadata.h b/src/windows/wslcsession/WSLCNetworkMetadata.h
index 005b0535c..a2f738763 100644
--- a/src/windows/wslcsession/WSLCNetworkMetadata.h
+++ b/src/windows/wslcsession/WSLCNetworkMetadata.h
@@ -45,6 +45,7 @@ struct NetworkEntry
std::string Scope;
bool Internal{false};
std::map Labels;
+ std::map Options;
NetworkIPAM IPAM;
};
diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp
index eefba3048..a492770f7 100644
--- a/src/windows/wslcsession/WSLCSession.cpp
+++ b/src/windows/wslcsession/WSLCSession.cpp
@@ -2447,12 +2447,14 @@ try
auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel);
- static constexpr std::array c_supportedDriverOpts{"Internal", "Subnet", "Gateway"};
+ // Reject case-mismatches of reserved driver-option keys.
+ static constexpr std::array c_reservedDriverOpts{"Internal", "Subnet", "Gateway"};
for (const auto& [key, _] : driverOpts)
{
- const bool supported = std::any_of(
- c_supportedDriverOpts.begin(), c_supportedDriverOpts.end(), [&](std::string_view opt) { return key == opt; });
- THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidNetworkDriverOption(key), !supported);
+ const bool caseMismatch = std::any_of(c_reservedDriverOpts.begin(), c_reservedDriverOpts.end(), [&](std::string_view opt) {
+ return key != opt && wsl::shared::string::IsEqual(key, opt, true);
+ });
+ THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidNetworkDriverOption(key), caseMismatch);
}
THROW_HR_WITH_USER_ERROR_IF(
@@ -2492,6 +2494,20 @@ try
ipam.Config.emplace().push_back(std::move(ipamConfig));
}
+ // Forward any non-reserved driver options to Docker. Reserved keys (Internal, Subnet, Gateway)
+ // are translated into Docker's typed network fields above and not echoed back here.
+ for (const auto& [key, value] : driverOpts)
+ {
+ if (std::none_of(c_reservedDriverOpts.begin(), c_reservedDriverOpts.end(), [&](std::string_view opt) { return key == opt; }))
+ {
+ if (!request.Options.has_value())
+ {
+ request.Options.emplace();
+ }
+ (*request.Options)[key] = value;
+ }
+ }
+
docker_schema::CreateNetworkResponse createResult;
try
{
@@ -2529,6 +2545,10 @@ try
entry.Scope = full.Scope;
entry.Internal = full.Internal;
entry.Labels = full.Labels;
+ if (full.Options)
+ {
+ entry.Options = *full.Options;
+ }
entry.IPAM.Driver = full.IPAM.Driver;
if (full.IPAM.Config)
{
@@ -2653,6 +2673,10 @@ try
result.Scope = entry.Scope;
result.Internal = entry.Internal;
result.Labels = entry.Labels;
+ if (!entry.Options.empty())
+ {
+ result.Options = entry.Options;
+ }
result.IPAM.Driver = entry.IPAM.Driver;
if (entry.IPAM.Config)
@@ -3483,6 +3507,10 @@ void WSLCSession::RecoverExistingNetworks()
entry.Scope = network.Scope;
entry.Internal = network.Internal;
entry.Labels = network.Labels;
+ if (network.Options)
+ {
+ entry.Options = *network.Options;
+ }
entry.IPAM.Driver = network.IPAM.Driver;
if (network.IPAM.Config)
{
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index 8baad20ab..baf775126 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -5263,15 +5263,16 @@ class WSLCTests
}
}
- // Invalid driver options (wrong case and unknown keys)
+ // Reserved driver-option keys must be spelled exactly; case-mismatches are rejected
+ // so users don't silently fall through to opaque pass-through.
{
options.Driver = "bridge";
- for (const char* key : {"internal", "subnet", "gateway", "foo"})
+ for (const char* key : {"internal", "subnet", "gateway"})
{
WSLCDriverOption opt{key, "true"};
options.DriverOpts = &opt;
options.DriverOptsCount = 1;
- verifyInvalid(wsl::shared::string::MultiByteToWide(key).c_str());
+ verifyInvalid(L"case-sensitive");
}
}
@@ -5411,17 +5412,49 @@ class WSLCTests
VERIFY_ARE_EQUAL(std::string("172.31.0.1"), inspect.IPAM.Config->at(0).Gateway);
}
+ WSLC_TEST_METHOD(NetworkCreateWithArbitraryDriverOptsTest)
+ {
+ const std::string networkName = "arbitrary-opts-test-net";
+
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
+
+ WSLCDriverOption opts[] = {{"my.abc.key", "mygod"}, {"com.example.flag", "1"}};
+
+ WSLCNetworkOptions options{};
+ options.Name = networkName.c_str();
+ options.Driver = "bridge";
+ options.DriverOpts = opts;
+ options.DriverOptsCount = ARRAYSIZE(opts);
+
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
+
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
+
+ wil::unique_cotaskmem_ansistring output;
+ VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
+ VERIFY_IS_NOT_NULL(output.get());
+
+ auto inspect = wsl::shared::FromJson(output.get());
+ VERIFY_IS_TRUE(inspect.Options.has_value());
+ VERIFY_IS_TRUE(inspect.Options->contains("my.abc.key"));
+ VERIFY_IS_TRUE(inspect.Options->contains("com.example.flag"));
+ VERIFY_ARE_EQUAL(std::string("mygod"), inspect.Options->at("my.abc.key"));
+ VERIFY_ARE_EQUAL(std::string("1"), inspect.Options->at("com.example.flag"));
+ }
+
WSLC_TEST_METHOD(NetworkSessionRecoveryTest)
{
const std::string networkName = "recovery-test-net";
LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
+ WSLCDriverOption recoveryOpts[] = {{"recovery.test.key", "preserved"}};
+
WSLCNetworkOptions options{};
options.Name = networkName.c_str();
options.Driver = "bridge";
- options.DriverOpts = nullptr;
- options.DriverOptsCount = 0;
+ options.DriverOpts = recoveryOpts;
+ options.DriverOptsCount = ARRAYSIZE(recoveryOpts);
VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options, nullptr));
auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
@@ -5435,6 +5468,14 @@ class WSLCTests
VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
+
+ // Verify arbitrary driver options survive session recovery.
+ wil::unique_cotaskmem_ansistring output;
+ VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
+ auto inspect = wsl::shared::FromJson(output.get());
+ VERIFY_IS_TRUE(inspect.Options.has_value());
+ VERIFY_IS_TRUE(inspect.Options->contains("recovery.test.key"));
+ VERIFY_ARE_EQUAL(std::string("preserved"), inspect.Options->at("recovery.test.key"));
}
WSLC_TEST_METHOD(NetworkMultipleCreateListDeleteTest)