Skip to content
Open
8 changes: 8 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2884,6 +2884,14 @@ On first run, creates the file with all settings commented out at their defaults
<value>Invalid {} value: network name cannot be empty or whitespace</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_NetworkDuplicateNameError" xml:space="preserve">
<value>Invalid {} value: network name can only be specified once</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_NetworkUnsupportedOptionError" xml:space="preserve">
<value>Invalid {} value: unsupported network option '{}'</value>
<comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_NetworkHostModeNotSupportedError" xml:space="preserve">
<value>host mode networking is not supported</value>
</data>
Expand Down
23 changes: 21 additions & 2 deletions src/windows/common/WSLCContainerLauncher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,13 @@ void wsl::windows::common::WSLCContainerLauncher::AddTmpfs(const std::string& Co

void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(const std::string& Name)
{
m_additionalNetworks.push_back(Name);
AddAdditionalNetwork(Name, {});
}

void wsl::windows::common::WSLCContainerLauncher::AddAdditionalNetwork(
const std::string& Name, const std::vector<std::string>& Aliases)
{
m_additionalNetworks.push_back({.Name = Name, .Aliases = Aliases});
}

void wsl::windows::common::WSLCContainerLauncher::AddPrimaryNetworkAlias(const std::string& Alias)
Expand Down Expand Up @@ -379,9 +385,22 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::C
// Each additional network becomes an entry in NetworkingConfig.EndpointsConfig.
std::vector<WSLCNetworkConnection> connections;
connections.reserve(m_additionalNetworks.size());
std::vector<std::vector<KeyValuePair>> connectionSettings;
connectionSettings.reserve(m_additionalNetworks.size());
for (const auto& e : m_additionalNetworks)
{
connections.push_back({.NetworkName = e.c_str()});
auto& settings = connectionSettings.emplace_back();
settings.reserve(e.Aliases.size());
for (const auto& alias : e.Aliases)
{
settings.push_back({.Key = "Aliases", .Value = alias.c_str()});
}

connections.push_back({
.NetworkName = e.Name.c_str(),
.Settings = settings.empty() ? nullptr : settings.data(),
.SettingsCount = static_cast<ULONG>(settings.size()),
});
}

options.ContainerNetwork.Networks = connections.empty() ? nullptr : connections.data();
Expand Down
9 changes: 8 additions & 1 deletion src/windows/common/WSLCContainerLauncher.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
void AddLabel(const std::string& Key, const std::string& Value);
void AddTmpfs(const std::string& ContainerPath, const std::string& Options);
void AddAdditionalNetwork(const std::string& Name);
void AddAdditionalNetwork(const std::string& Name, const std::vector<std::string>& Aliases);
void AddPrimaryNetworkAlias(const std::string& Alias);

std::pair<HRESULT, std::optional<RunningWSLCContainer>> CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback = nullptr);
Expand Down Expand Up @@ -93,6 +94,12 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
using WSLCProcessLauncher::SetWorkingDirectory;

private:
struct NetworkConnection
{
std::string Name;
std::vector<std::string> Aliases;
};

std::string m_image;
std::string m_name;
std::vector<WSLCPortMapping> m_ports;
Expand All @@ -112,7 +119,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher
std::vector<std::string> m_dnsServers;
std::vector<std::string> m_dnsSearchDomains;
std::vector<std::string> m_dnsOptions;
std::vector<std::string> m_additionalNetworks;
std::vector<NetworkConnection> m_additionalNetworks;
std::vector<std::string> m_primaryNetworkAliases;
std::vector<WSLCLabel> m_labels;
std::deque<std::string> m_labelKeys;
Expand Down
3 changes: 3 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ typedef struct _WSLCUlimit
typedef struct _WSLCNetworkConnection
{
[string] LPCSTR NetworkName;

// Settings for this endpoint.
// KVP-encoded; duplicate keys are allowed (e.g., multiple "Aliases" entries).
[unique, size_is(SettingsCount)] const KeyValuePair* Settings;
ULONG SettingsCount;
} WSLCNetworkConnection;
Expand Down
149 changes: 136 additions & 13 deletions src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,7 @@ void Argument::Validate(const ArgMap& execArgs) const

case ArgType::Network:
{
for (const auto& value : execArgs.GetAll<ArgType::Network>())
{
if (value.empty() ||
std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); }))
{
throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name));
}

if (IsEqual(value, L"host", true))
{
throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
}
}
validation::ValidateNetwork(execArgs.GetAll<ArgType::Network>(), m_name);
break;
}

Expand Down Expand Up @@ -166,6 +154,34 @@ static const std::unordered_map<std::wstring, WSLCSignal> SignalMap = {
{L"SIGPOLL", WSLCSignalSIGPOLL}, {L"SIGPWR", WSLCSignalSIGPWR}, {L"SIGSYS", WSLCSignalSIGSYS},
};

namespace {
std::vector<std::wstring_view> SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter)
{
std::vector<std::wstring_view> parts;
size_t start = 0;
while (start <= value.size())
{
const auto end = value.find(delimiter, start);
if (end == std::wstring_view::npos)
{
parts.emplace_back(value.substr(start));
break;
}

parts.emplace_back(value.substr(start, end - start));
start = end + 1;
}

return parts;
}

bool IsEmptyOrWhitespace(std::wstring_view value)
{
return value.empty() ||
std::all_of(value.begin(), value.end(), [](wchar_t c) { return std::iswspace(static_cast<wint_t>(c)); });
}
} // namespace

void ValidateWSLCSignalFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
{
for (const auto& value : values)
Expand All @@ -192,6 +208,35 @@ void ValidateFilter(const std::vector<std::wstring>& values)
}
}

void ValidateNetwork(const std::vector<std::wstring>& values, const std::wstring& argName)
{
for (const auto& value : values)
{
const auto parsed = ParseNetworkArgument(value);
if (parsed.Error == NetworkArgumentParseError::EmptyNetworkName)
{
throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
}
else if (parsed.Error == NetworkArgumentParseError::EmptyAlias)
{
throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(argName));
}
else if (parsed.Error == NetworkArgumentParseError::DuplicateNetworkName)
{
throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(argName));
}
else if (parsed.Error == NetworkArgumentParseError::UnsupportedOption)
{
throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, parsed.ErrorValue));
}

if (IsEqual(parsed.Name, L"host", true))
{
throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
}
}
}

// Convert string to WSLCSignal enum - accepts either signal name (e.g., "SIGKILL") or number (e.g., "9")
WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName)
{
Expand Down Expand Up @@ -494,6 +539,84 @@ std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input,
return {WideToMultiByte(input.substr(0, equalsPos)), soft, hard};
}

ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value)
{
ParsedNetworkArgument result;

auto parseOptions = [&](std::wstring_view options, bool requireName) {
bool parsedName = false;
for (const auto part : SplitPreserveEmpty(options, L','))
{
const auto separator = part.find(L'=');
if (separator == std::wstring_view::npos)
{
result.Error = NetworkArgumentParseError::UnsupportedOption;
result.ErrorValue = std::wstring{part};
return;
}

const auto key = part.substr(0, separator);
const auto optionValue = part.substr(separator + 1);
if (key == L"name")
{
if (parsedName)
{
result.Error = NetworkArgumentParseError::DuplicateNetworkName;
result.ErrorValue = std::wstring{key};
return;
}

parsedName = true;
result.Name = std::wstring{optionValue};
}
else if (key == L"alias")
{
result.Aliases.emplace_back(optionValue);
}
else
{
result.Error = NetworkArgumentParseError::UnsupportedOption;
result.ErrorValue = std::wstring{key};
return;
}
}

if (requireName && !parsedName)
{
result.Error = NetworkArgumentParseError::EmptyNetworkName;
}
};

if (value.find(L'=') != std::wstring_view::npos)
{
parseOptions(value, true);
}
else
{
result.Name = std::wstring{value};
}

if (result.Error == NetworkArgumentParseError::None)
{
if (IsEmptyOrWhitespace(result.Name))
{
result.Error = NetworkArgumentParseError::EmptyNetworkName;
return result;
}

for (const auto& alias : result.Aliases)
{
if (IsEmptyOrWhitespace(alias))
{
result.Error = NetworkArgumentParseError::EmptyAlias;
return result;
}
}
}

return result;
}

std::pair<std::string, std::string> ParseLabel(const std::wstring& value)
{
std::pair<std::string, std::string> result{};
Expand Down
21 changes: 21 additions & 0 deletions src/windows/wslc/arguments/ArgumentValidation.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Module Name:
#include "ContainerModel.h"
#include "InspectModel.h"
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include <charconv>
Expand Down Expand Up @@ -82,9 +83,29 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri
void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
void ValidateVolumeMount(const std::vector<std::wstring>& values);
void ValidateFilter(const std::vector<std::wstring>& values);
void ValidateNetwork(const std::vector<std::wstring>& values, const std::wstring& argName);

std::pair<std::string, std::string> ParseLabel(const std::wstring& value);
std::pair<std::string, std::string> ParseDriverOption(const std::wstring& value);
std::pair<std::string, std::string> ParseFilter(const std::wstring& value);

enum class NetworkArgumentParseError
{
None,
EmptyNetworkName,
EmptyAlias,
DuplicateNetworkName,
UnsupportedOption,
};

struct ParsedNetworkArgument
{
std::wstring Name;
std::vector<std::wstring> Aliases;
NetworkArgumentParseError Error = NetworkArgumentParseError::None;
std::wstring ErrorValue;
};

ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value);

} // namespace wsl::windows::wslc::validation
10 changes: 9 additions & 1 deletion src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Module Name:

#include <wslservice.h>
#include <wslc.h>
#include <optional>
#include <string>
#include <vector>

namespace wsl::windows::wslc::models {

Expand All @@ -27,6 +29,12 @@ enum class FormatType
Json,
};

struct ContainerNetwork
{
std::string Name;
std::vector<std::string> Aliases;
};

struct ContainerOptions
{
std::vector<std::string> Arguments;
Expand All @@ -51,7 +59,7 @@ struct ContainerOptions
std::vector<std::string> DnsServers;
std::vector<std::string> DnsSearchDomains;
std::vector<std::string> DnsOptions;
std::vector<std::string> Networks;
std::vector<ContainerNetwork> Networks;
std::vector<std::string> NetworkAliases;
std::vector<std::string> Tmpfs;
std::vector<std::pair<std::string, std::string>> Labels;
Expand Down
Loading