Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
Comment thread
danegsta marked this conversation as resolved.
<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
22 changes: 17 additions & 5 deletions src/windows/wslc/arguments/ArgumentValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,25 @@ void Argument::Validate(const ArgMap& execArgs) const
{
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)); }))
const auto parsed = models::ParseNetworkArgument(value);
Comment thread
danegsta marked this conversation as resolved.
Outdated
if (parsed.Error == models::NetworkArgumentParseError::EmptyNetworkName)
{
throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(m_name));
}
else if (parsed.Error == models::NetworkArgumentParseError::EmptyAlias)
{
throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(m_name));
}
else if (parsed.Error == models::NetworkArgumentParseError::DuplicateNetworkName)
{
throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(m_name));
}
else if (parsed.Error == models::NetworkArgumentParseError::UnsupportedOption)
{
throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(m_name, parsed.ErrorValue));
}

if (IsEqual(value, L"host", true))
if (IsEqual(parsed.Name, L"host", true))
{
throw ArgumentException(Localization::WSLCCLI_NetworkHostModeNotSupportedError());
}
Expand Down Expand Up @@ -357,8 +369,8 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring
}
else
{
throw ArgumentException(std::format(
L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input));
throw ArgumentException(
std::format(L"Invalid {} value: {} is not a recognized format type. Supported format types are: json, table.", argName, input));
Comment thread
danegsta marked this conversation as resolved.
Outdated
}
}

Expand Down
141 changes: 140 additions & 1 deletion src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,156 @@ Module Name:

#include <wslservice.h>
#include <wslc.h>
#include <algorithm>
#include <cwctype>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

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

namespace details {
inline std::wstring ToWideString(std::wstring_view value)
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
return {value.data(), value.size()};
}

inline std::vector<std::wstring_view> SplitPreserveEmpty(std::wstring_view value, wchar_t delimiter)
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
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;
}
} // namespace details

// Valid formats for container list output.
enum class FormatType
{
Table,
Json,
};

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;
};

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

inline ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value)
Comment thread
danegsta marked this conversation as resolved.
Outdated
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
ParsedNetworkArgument result;

auto parseOptions = [&](std::wstring_view options, bool requireName) {
bool parsedName = false;
for (const auto part : details::SplitPreserveEmpty(options, L','))
{
const auto separator = part.find(L'=');
if (separator == std::wstring_view::npos)
{
result.Error = NetworkArgumentParseError::UnsupportedOption;
result.ErrorValue = details::ToWideString(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 = details::ToWideString(key);
return;
}

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

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

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

if (result.Error == NetworkArgumentParseError::None)
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
const auto nameIsEmpty = result.Name.empty() || std::all_of(result.Name.begin(), result.Name.end(), [](wchar_t c) {
return std::iswspace(static_cast<wint_t>(c));
});
if (nameIsEmpty)
{
result.Error = NetworkArgumentParseError::EmptyNetworkName;
return result;
}

for (const auto& alias : result.Aliases)
{
const auto aliasIsEmpty = alias.empty() || std::all_of(alias.begin(), alias.end(), [](wchar_t c) {
return std::iswspace(static_cast<wint_t>(c));
});
if (aliasIsEmpty)
{
result.Error = NetworkArgumentParseError::EmptyAlias;
return result;
}
}
}

return result;
}

struct ContainerOptions
{
std::vector<std::string> Arguments;
Expand All @@ -51,7 +190,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
31 changes: 27 additions & 4 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const
options.CommandLine = {.Values = argsStorage.data(), .Count = static_cast<ULONG>(argsStorage.size())};
}

static bool SupportsNetworkAliases(std::string_view network)
{
return network != "bridge" && network != "host" && network != "none" && !network.starts_with("container:");
Comment thread
danegsta marked this conversation as resolved.
}

static wsl::windows::common::RunningWSLCContainer CreateInternal(
Session& session, const std::string& image, const ContainerOptions& options, IWarningCallback* warningCallback = nullptr)
{
Expand All @@ -54,14 +59,20 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);

std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front();
std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front().Name;

wsl::windows::common::WSLCContainerLauncher containerLauncher(
image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags);

for (size_t i = 1; i < options.Networks.size(); ++i)
{
containerLauncher.AddAdditionalNetwork(options.Networks[i]);
const auto& network = options.Networks[i];
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG,
Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
!network.Aliases.empty() && !SupportsNetworkAliases(network.Name));

containerLauncher.AddAdditionalNetwork(network.Name, network.Aliases);
}

if (!options.NetworkAliases.empty())
Expand All @@ -70,13 +81,25 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(

THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);

const auto& primary = options.Networks.front().Name;
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));

for (const auto& alias : options.NetworkAliases)
{
containerLauncher.AddPrimaryNetworkAlias(alias);
}
}

if (!options.Networks.empty())
{
const auto& primary = options.Networks.front();
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG,
Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
primary == "bridge" || primary == "host" || primary == "none" || primary.starts_with("container:"));
!primary.Aliases.empty() && !SupportsNetworkAliases(primary.Name));

for (const auto& alias : options.NetworkAliases)
for (const auto& alias : primary.Aliases)
{
containerLauncher.AddPrimaryNetworkAlias(alias);
}
Expand Down
9 changes: 8 additions & 1 deletion src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,14 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
options.Networks.reserve(options.Networks.size() + networks.size());
for (const auto& value : networks)
{
options.Networks.emplace_back(WideToMultiByte(value));
const auto parsed = ParseNetworkArgument(value);
auto& network = options.Networks.emplace_back();
network.Name = WideToMultiByte(parsed.Name);
network.Aliases.reserve(parsed.Aliases.size());
for (const auto& alias : parsed.Aliases)
{
network.Aliases.emplace_back(WideToMultiByte(alias));
}
}
}

Expand Down
Loading