Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2336,8 +2336,8 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcInvalidNetworkDriverOption" xml:space="preserve">
<value>Unsupported network driver option: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
<value>Network driver option '{}' is case-sensitive. Use the exact casing: Internal, Subnet, or Gateway.</value>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does docker the docker API react if it's passed these options with the wrong casing ? Does it just ignore them ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Docker just silently accepts them. I disabled our validation temporarily and tested with --opt internal=true --opt subnet=172.55.0.0/16 --opt gateway=172.55.0.1 - all lowercase. Our InspectNetwork is a direct HTTP GET to Docker's /networks/{id} endpoint (deserialized 1:1), and it shows:
Internal: false (typed field never got set)
Subnet: 172.18.0.0/16, Gateway: 172.18.0.1 (Docker picked its own defaults)
Options: {"internal": "true", "subnet": "172.55.0.0/16", "gateway": "172.55.0.1"} (stored but doing nothing)
So from Docker's side, the lowercase keys just end up as inert entries in the Options map - the network gets created but is silently broken. That's why we reject the case mismatch up front.
image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok in that case I think we should match their behavior. From a UX perspective I think the best way to this would be to implement the CLI options for these values that docker has (--internal, gateway , ...) and not to any specific casing check in the service

<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="Internal"}{Locked="Subnet"}{Locked="Gateway"}</comment>
</data>
<data name="MessageWslcGatewayRequiresSubnet" xml:space="preserve">
<value>Network driver option 'Gateway' requires 'Subnet' to also be specified.</value>
Expand Down
6 changes: 4 additions & 2 deletions src/windows/inc/docker_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,10 @@ struct CreateNetwork
std::string Driver;
bool Internal{};
std::optional<IPAM> IPAM;
std::optional<std::map<std::string, std::string>> Options;
std::map<std::string, std::string> 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
Expand All @@ -159,9 +160,10 @@ struct Network
std::string Scope;
bool Internal{};
IPAM IPAM;
std::optional<std::map<std::string, std::string>> Options;
std::map<std::string, std::string> 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
Expand Down
3 changes: 2 additions & 1 deletion src/windows/inc/wslc_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ struct Network
std::string Scope;
bool Internal{};
IPAM IPAM;
std::optional<std::map<std::string, std::string>> Options;
std::map<std::string, std::string> 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
1 change: 1 addition & 0 deletions src/windows/wslcsession/WSLCNetworkMetadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct NetworkEntry
std::string Scope;
bool Internal{false};
std::map<std::string, std::string> Labels;
std::map<std::string, std::string> Options;
NetworkIPAM IPAM;
};

Expand Down
36 changes: 32 additions & 4 deletions src/windows/wslcsession/WSLCSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view, 3> c_supportedDriverOpts{"Internal", "Subnet", "Gateway"};
// Reject case-mismatches of reserved driver-option keys.
static constexpr std::array<std::string_view, 3> 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);
Comment thread
beena352 marked this conversation as resolved.
}

THROW_HR_WITH_USER_ERROR_IF(
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down
51 changes: 46 additions & 5 deletions test/windows/WSLCTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

Expand Down Expand Up @@ -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<wsl::windows::common::wslc_schema::Network>(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())); });
Expand All @@ -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<wsl::windows::common::wslc_schema::Network>(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)
Expand Down