diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt
index 1c6ef9a373c..56ae1aa03ac 100644
--- a/.github/actions/spelling/allow/names.txt
+++ b/.github/actions/spelling/allow/names.txt
@@ -77,6 +77,7 @@ sonpham
stakx
talo
thereses
+Thysell
Walisch
WDX
Wellons
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
index da175280cb0..39124e2f0d5 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
@@ -13,6 +13,9 @@
#include "PowershellCoreProfileGenerator.h"
#include "VisualStudioGenerator.h"
#include "WslDistroGenerator.h"
+#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
+#include "SshHostGenerator.h"
+#endif
// The following files are generated at build time into the "Generated Files" directory.
// defaults(-universal).h is a file containing the default json settings in a std::string_view.
@@ -148,6 +151,9 @@ void SettingsLoader::GenerateProfiles()
_executeGenerator(WslDistroGenerator{});
_executeGenerator(AzureCloudShellGenerator{});
_executeGenerator(VisualStudioGenerator{});
+#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
+ _executeGenerator(SshHostGenerator{});
+#endif
}
// A new settings.json gets a special treatment:
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
index a1aaf301ce8..6231c3664a9 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
@@ -92,6 +92,7 @@
+
@@ -166,6 +167,7 @@
+
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
index fb21fccdc31..3a4aab82329 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
@@ -18,6 +18,9 @@
profileGeneration
+
+ profileGeneration
+
@@ -61,6 +64,9 @@
profileGeneration
+
+ profileGeneration
+
diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp
new file mode 100644
index 00000000000..30dc19039f0
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "SshHostGenerator.h"
+#include "../../inc/DefaultSettings.h"
+
+#include "DynamicProfileUtils.h"
+
+static constexpr std::wstring_view SshHostGeneratorNamespace{ L"Windows.Terminal.SSH" };
+
+static constexpr std::wstring_view PROFILE_TITLE_PREFIX = L"SSH - ";
+static constexpr std::wstring_view PROFILE_ICON_PATH = L"ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png";
+
+// OpenSSH is installed under System32 when installed via Optional Features
+static constexpr std::wstring_view SSH_EXE_PATH1 = L"%SystemRoot%\\System32\\OpenSSH\\ssh.exe";
+
+// OpenSSH (x86/x64) is installed under Program Files when installed via MSI
+static constexpr std::wstring_view SSH_EXE_PATH2 = L"%ProgramFiles%\\OpenSSH\\ssh.exe";
+
+// OpenSSH (x86) is installed under Program Files x86 when installed via MSI on x64 machine
+static constexpr std::wstring_view SSH_EXE_PATH3 = L"%ProgramFiles(x86)%\\OpenSSH\\ssh.exe";
+
+static constexpr std::wstring_view SSH_SYSTEM_CONFIG_PATH = L"%ProgramData%\\ssh\\ssh_config";
+static constexpr std::wstring_view SSH_USER_CONFIG_PATH = L"%UserProfile%\\.ssh\\config";
+
+static constexpr std::wstring_view SSH_CONFIG_HOST_KEY{ L"Host" };
+static constexpr std::wstring_view SSH_CONFIG_HOSTNAME_KEY{ L"HostName" };
+
+using namespace ::Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model;
+
+/*static*/ const std::wregex SshHostGenerator::_configKeyValueRegex{ LR"(^\s*(\w+)\s+([^\s]+.*[^\s])\s*$)" };
+
+/*static*/ std::wstring_view SshHostGenerator::_getProfileName(const std::wstring_view& hostName) noexcept
+{
+ return std::wstring_view{ L"" + PROFILE_TITLE_PREFIX + hostName };
+}
+
+/*static*/ std::wstring_view SshHostGenerator::_getProfileIconPath() noexcept
+{
+ return PROFILE_ICON_PATH;
+}
+
+/*static*/ std::wstring_view SshHostGenerator::_getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept
+{
+ return std::wstring_view{ L"\"" + sshExePath + L"\" " + hostName };
+}
+
+/*static*/ bool SshHostGenerator::_tryFindSshExePath(std::wstring& sshExePath) noexcept
+{
+ try
+ {
+ for (const auto& path : { SSH_EXE_PATH1, SSH_EXE_PATH2, SSH_EXE_PATH3 })
+ {
+ if (std::filesystem::exists(wil::ExpandEnvironmentStringsW(path.data())))
+ {
+ sshExePath = path;
+ return true;
+ }
+ }
+ }
+ CATCH_LOG();
+
+ return false;
+}
+
+/*static*/ bool SshHostGenerator::_tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept
+{
+ try
+ {
+ if (!line.empty() && !line.starts_with(L"#"))
+ {
+ std::wstring input{ line };
+ std::wsmatch match;
+ if (std::regex_search(input, match, SshHostGenerator::_configKeyValueRegex))
+ {
+ key = match[1];
+ value = match[2];
+ return true;
+ }
+ }
+ }
+ CATCH_LOG();
+
+ return false;
+}
+
+/*static*/ void SshHostGenerator::_getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector& hostNames) noexcept
+{
+ try
+ {
+ const std::filesystem::path resolvedConfigPath{ wil::ExpandEnvironmentStringsW(configPath.data()) };
+ if (std::filesystem::exists(resolvedConfigPath))
+ {
+ std::wifstream inputStream(resolvedConfigPath);
+
+ std::wstring line;
+ std::wstring key;
+ std::wstring value;
+
+ std::wstring lastHost;
+
+ while (std::getline(inputStream, line))
+ {
+ if (_tryParseConfigKeyValue(line, key, value))
+ {
+ if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOST_KEY))
+ {
+ // Save potential Host value for later
+ lastHost = value;
+ }
+ else if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOSTNAME_KEY))
+ {
+ // HostName was specified
+ if (!lastHost.empty())
+ {
+ hostNames.emplace_back(lastHost);
+ lastHost = L"";
+ }
+ }
+ }
+ }
+ }
+ }
+ CATCH_LOG();
+}
+
+std::wstring_view SshHostGenerator::GetNamespace() const noexcept
+{
+ return SshHostGeneratorNamespace;
+}
+
+// Method Description:
+// - Generate a list of profiles for each detected OpenSSH host.
+// Arguments:
+// -
+// Return Value:
+// -
+void SshHostGenerator::GenerateProfiles(std::vector>& profiles) const
+{
+ std::wstring sshExePath;
+ if (_tryFindSshExePath(sshExePath))
+ {
+ std::vector hostNames;
+
+ _getHostNamesFromConfigFile(SSH_SYSTEM_CONFIG_PATH, hostNames);
+ _getHostNamesFromConfigFile(SSH_USER_CONFIG_PATH, hostNames);
+
+ for (const auto& hostName : hostNames)
+ {
+ const auto profile{ CreateDynamicProfile(_getProfileName(hostName)) };
+
+ profile->Commandline(winrt::hstring{ _getProfileCommandLine(sshExePath, hostName) });
+ profile->Icon(winrt::hstring{ _getProfileIconPath() });
+
+ profiles.emplace_back(profile);
+ }
+ }
+}
diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h
new file mode 100644
index 00000000000..a355b0c4302
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h
@@ -0,0 +1,40 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SshHostGenerator
+
+Abstract:
+- This is the dynamic profile generator for SSH connections. Enumerates all the
+ SSH hosts to create profiles for them.
+
+Author(s):
+- Jon Thysell - September 2022
+
+--*/
+
+#pragma once
+
+#include "IDynamicProfileGenerator.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model
+{
+ class SshHostGenerator final : public IDynamicProfileGenerator
+ {
+ public:
+ std::wstring_view GetNamespace() const noexcept override;
+ void GenerateProfiles(std::vector>& profiles) const override;
+
+ private:
+ static const std::wregex _configKeyValueRegex;
+
+ static std::wstring_view _getProfileName(const std::wstring_view& hostName) noexcept;
+ static std::wstring_view _getProfileIconPath() noexcept;
+ static std::wstring_view _getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept;
+
+ static bool _tryFindSshExePath(std::wstring& sshExePath) noexcept;
+ static bool _tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept;
+ static void _getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector& hostNames) noexcept;
+ };
+};
diff --git a/src/features.xml b/src/features.xml
index 9d3c36cf7d3..e26c217d3bf 100644
--- a/src/features.xml
+++ b/src/features.xml
@@ -138,4 +138,11 @@
+
+ Feature_DynamicSSHProfiles
+ Enables the dynamic profile generator for OpenSSH config files
+ 9031
+ AlwaysDisabled
+
+