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)