mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-09-10 09:15:47 +00:00
3e12c96317
Co-authored-by: cedricMicrovision <cedric.legoff@microvision.fr>
566 lines
21 KiB
C#
566 lines
21 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using Renci.SshNet.TestTools.OpenSSH.Formatters;
|
|
|
|
namespace Renci.SshNet.TestTools.OpenSSH
|
|
{
|
|
public sealed class SshdConfig
|
|
{
|
|
private static readonly Regex MatchRegex = new Regex($@"\s*Match\s+(User\s+(?<users>[\S]+))?\s*(Address\s+(?<addresses>[\S]+))?\s*",
|
|
RegexOptions.Compiled | RegexOptions.ExplicitCapture);
|
|
|
|
private readonly SubsystemFormatter _subsystemFormatter;
|
|
private readonly Int32Formatter _int32Formatter;
|
|
private readonly BooleanFormatter _booleanFormatter;
|
|
private readonly MatchFormatter _matchFormatter;
|
|
|
|
private SshdConfig()
|
|
{
|
|
AcceptedEnvironmentVariables = new List<string>();
|
|
Ciphers = new List<Cipher>();
|
|
HostKeyFiles = new List<string>();
|
|
HostKeyAlgorithms = new List<HostKeyAlgorithm>();
|
|
KeyExchangeAlgorithms = new List<KeyExchangeAlgorithm>();
|
|
PublicKeyAcceptedAlgorithms = new List<PublicKeyAlgorithm>();
|
|
MessageAuthenticationCodeAlgorithms = new List<MessageAuthenticationCodeAlgorithm>();
|
|
Subsystems = new List<Subsystem>();
|
|
Matches = new List<Match>();
|
|
LogLevel = LogLevel.Info;
|
|
Port = 22;
|
|
Protocol = "2,1";
|
|
|
|
_booleanFormatter = new BooleanFormatter();
|
|
_int32Formatter = new Int32Formatter();
|
|
_matchFormatter = new MatchFormatter();
|
|
_subsystemFormatter = new SubsystemFormatter();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the port number that sshd listens on.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The port number that sshd listens on. The default is 22.
|
|
/// </value>
|
|
public int Port { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the list of private host key files used by sshd.
|
|
/// </summary>
|
|
/// <value>
|
|
/// A list of private host key files used by sshd.
|
|
/// </value>
|
|
public List<string> HostKeyFiles { get; }
|
|
|
|
public string? HostCertificate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value specifying whether challenge-response authentication is allowed.
|
|
/// </summary>
|
|
/// <value>
|
|
/// A value specifying whether challenge-response authentication is allowed, or <see langword="null"/>
|
|
/// if this option is not configured.
|
|
/// </value>
|
|
public bool? ChallengeResponseAuthentication { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to allow keyboard-interactive authentication.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <see langword="true"/> to allow and <see langword="false"/> to disallow keyboard-interactive
|
|
/// authentication, or <see langword="null"/> if this option is not configured.
|
|
/// </value>
|
|
public bool? KeyboardInteractiveAuthentication { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the verbosity when logging messages from sshd.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The verbosity when logging messages from sshd. The default is <see cref="LogLevel.Info"/>.
|
|
/// </value>
|
|
public LogLevel LogLevel { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a sets a value indicating whether the Pluggable Authentication Module interface is enabled.
|
|
/// </summary>
|
|
/// <value>
|
|
/// A value indicating whether the Pluggable Authentication Module interface is enabled.
|
|
/// </value>
|
|
public bool? UsePAM { get; set; }
|
|
|
|
public List<Subsystem> Subsystems { get; }
|
|
|
|
/// <summary>
|
|
/// Gets a list of conditional blocks.
|
|
/// </summary>
|
|
public List<Match> Matches { get; }
|
|
|
|
public bool X11Forwarding { get; private set; }
|
|
public List<string> AcceptedEnvironmentVariables { get; private set; }
|
|
public List<Cipher> Ciphers { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the host key signature algorithms that the server offers.
|
|
/// </summary>
|
|
public List<HostKeyAlgorithm> HostKeyAlgorithms { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the available KEX (Key Exchange) algorithms.
|
|
/// </summary>
|
|
public List<KeyExchangeAlgorithm> KeyExchangeAlgorithms { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the signature algorithms that will be accepted for public key authentication.
|
|
/// </summary>
|
|
public List<PublicKeyAlgorithm> PublicKeyAcceptedAlgorithms { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the available MAC (message authentication code) algorithms.
|
|
/// </summary>
|
|
public List<MessageAuthenticationCodeAlgorithm> MessageAuthenticationCodeAlgorithms { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the filepaths of the trusted user CA (certificate authority) keys.
|
|
/// </summary>
|
|
public string? TrustedUserCAKeys { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether <c>sshd</c> should print <c>/etc/motd</c> when a user logs in interactively.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <see langword="true"/> if <c>sshd</c> should print <c>/etc/motd</c> when a user logs in interactively
|
|
/// and <see langword="false"/> if it should not; <see langword="null"/> if this option is not configured.
|
|
/// </value>
|
|
public bool? PrintMotd { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the protocol versions sshd supported.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The protocol versions sshd supported. The default is <c>2,1</c>.
|
|
/// </value>
|
|
public string Protocol { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether TTY is permitted.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <see langword="true"/> to permit and <see langword="false"/> to not permit TTY,
|
|
/// or <see langword="null"/> if this option is not configured.
|
|
/// </value>
|
|
public bool? PermitTTY { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether TCP forwarding is allowed.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <see langword="true"/> to allow and <see langword="false"/> to disallow TCP forwarding,
|
|
/// or <see langword="null"/> if this option is not configured.
|
|
/// </value>
|
|
public bool? AllowTcpForwarding { get; set; }
|
|
|
|
public static SshdConfig LoadFrom(Stream stream, Encoding encoding)
|
|
{
|
|
using (var sr = new StreamReader(stream, encoding))
|
|
{
|
|
var sshdConfig = new SshdConfig();
|
|
|
|
Match? currentMatchConfiguration = null;
|
|
|
|
string? line;
|
|
while ((line = sr.ReadLine()) != null)
|
|
{
|
|
// Skip empty lines
|
|
if (line.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip comments
|
|
if (line[0] == '#')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var match = MatchRegex.Match(line);
|
|
if (match.Success)
|
|
{
|
|
var usersGroup = match.Groups["users"];
|
|
var addressesGroup = match.Groups["addresses"];
|
|
var users = usersGroup.Success ? usersGroup.Value.Split(',') : Array.Empty<string>();
|
|
var addresses = addressesGroup.Success ? addressesGroup.Value.Split(',') : Array.Empty<string>();
|
|
|
|
currentMatchConfiguration = new Match(users, addresses);
|
|
sshdConfig.Matches.Add(currentMatchConfiguration);
|
|
continue;
|
|
}
|
|
|
|
if (currentMatchConfiguration != null)
|
|
{
|
|
ProcessMatchOption(currentMatchConfiguration, line);
|
|
}
|
|
else
|
|
{
|
|
ProcessGlobalOption(sshdConfig, line);
|
|
}
|
|
}
|
|
|
|
if (sshdConfig.Ciphers == null)
|
|
{
|
|
// Obtain supported ciphers using ssh -Q cipher
|
|
}
|
|
|
|
if (sshdConfig.KeyExchangeAlgorithms == null)
|
|
{
|
|
// Obtain supports key exchange algorithms using ssh -Q kex
|
|
}
|
|
|
|
if (sshdConfig.HostKeyAlgorithms == null)
|
|
{
|
|
// Obtain supports host key algorithms using ssh -Q key
|
|
}
|
|
|
|
if (sshdConfig.MessageAuthenticationCodeAlgorithms == null)
|
|
{
|
|
// Obtain supported MACs using ssh -Q mac
|
|
}
|
|
|
|
return sshdConfig;
|
|
}
|
|
}
|
|
|
|
public void SaveTo(TextWriter writer)
|
|
{
|
|
if (writer is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(writer));
|
|
}
|
|
|
|
writer.WriteLine("Protocol " + Protocol);
|
|
writer.WriteLine("Port " + _int32Formatter.Format(Port));
|
|
if (HostKeyFiles.Count > 0)
|
|
{
|
|
writer.WriteLine("HostKey " + string.Join(",", HostKeyFiles.ToArray()));
|
|
}
|
|
|
|
if (ChallengeResponseAuthentication is not null)
|
|
{
|
|
writer.WriteLine("ChallengeResponseAuthentication " + _booleanFormatter.Format(ChallengeResponseAuthentication.Value));
|
|
}
|
|
|
|
if (KeyboardInteractiveAuthentication is not null)
|
|
{
|
|
writer.WriteLine("KbdInteractiveAuthentication " + _booleanFormatter.Format(KeyboardInteractiveAuthentication.Value));
|
|
}
|
|
|
|
if (PermitTTY is not null)
|
|
{
|
|
writer.WriteLine("PermitTTY " + _booleanFormatter.Format(PermitTTY.Value));
|
|
}
|
|
|
|
if (AllowTcpForwarding is not null)
|
|
{
|
|
writer.WriteLine("AllowTcpForwarding " + _booleanFormatter.Format(AllowTcpForwarding.Value));
|
|
}
|
|
|
|
if (PrintMotd is not null)
|
|
{
|
|
writer.WriteLine("PrintMotd " + _booleanFormatter.Format(PrintMotd.Value));
|
|
}
|
|
|
|
writer.WriteLine("LogLevel " + new LogLevelFormatter().Format(LogLevel));
|
|
|
|
foreach (var subsystem in Subsystems)
|
|
{
|
|
writer.WriteLine("Subsystem " + _subsystemFormatter.Format(subsystem));
|
|
}
|
|
|
|
if (UsePAM is not null)
|
|
{
|
|
writer.WriteLine("UsePAM " + _booleanFormatter.Format(UsePAM.Value));
|
|
}
|
|
|
|
writer.WriteLine("X11Forwarding " + _booleanFormatter.Format(X11Forwarding));
|
|
|
|
foreach (var acceptedEnvVar in AcceptedEnvironmentVariables)
|
|
{
|
|
writer.WriteLine("AcceptEnv " + acceptedEnvVar);
|
|
}
|
|
|
|
if (Ciphers.Count > 0)
|
|
{
|
|
writer.WriteLine("Ciphers " + string.Join(",", Ciphers.Select(c => c.Name).ToArray()));
|
|
}
|
|
|
|
if (HostKeyAlgorithms.Count > 0)
|
|
{
|
|
writer.WriteLine("HostKeyAlgorithms " + string.Join(",", HostKeyAlgorithms.Select(c => c.Name).ToArray()));
|
|
}
|
|
|
|
if (HostCertificate is not null)
|
|
{
|
|
writer.WriteLine("HostCertificate " + HostCertificate);
|
|
}
|
|
|
|
if (KeyExchangeAlgorithms.Count > 0)
|
|
{
|
|
writer.WriteLine("KexAlgorithms " + string.Join(",", KeyExchangeAlgorithms.Select(c => c.Name).ToArray()));
|
|
}
|
|
|
|
if (MessageAuthenticationCodeAlgorithms.Count > 0)
|
|
{
|
|
writer.WriteLine("MACs " + string.Join(",", MessageAuthenticationCodeAlgorithms.Select(c => c.Name).ToArray()));
|
|
}
|
|
|
|
if (PublicKeyAcceptedAlgorithms.Count > 0)
|
|
{
|
|
writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray()));
|
|
}
|
|
|
|
if (TrustedUserCAKeys is not null)
|
|
{
|
|
writer.WriteLine("TrustedUserCAKeys " + TrustedUserCAKeys);
|
|
}
|
|
|
|
foreach (var match in Matches)
|
|
{
|
|
_matchFormatter.Format(match, writer);
|
|
}
|
|
}
|
|
|
|
private static void ProcessGlobalOption(SshdConfig sshdConfig, string line)
|
|
{
|
|
var matchOptionRegex = new Regex(@"^\s*(?<name>[\S]+)\s+(?<value>.+?){1}\s*$");
|
|
|
|
var optionsMatch = matchOptionRegex.Match(line);
|
|
if (!optionsMatch.Success)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nameGroup = optionsMatch.Groups["name"];
|
|
var valueGroup = optionsMatch.Groups["value"];
|
|
|
|
var name = nameGroup.Value;
|
|
var value = valueGroup.Value;
|
|
|
|
switch (name)
|
|
{
|
|
case "Port":
|
|
sshdConfig.Port = ToInt(value);
|
|
break;
|
|
case "HostKey":
|
|
ParseCommaSeparatedValue(sshdConfig.HostKeyFiles, value);
|
|
break;
|
|
case "ChallengeResponseAuthentication":
|
|
sshdConfig.ChallengeResponseAuthentication = ToBool(value);
|
|
break;
|
|
case "KbdInteractiveAuthentication":
|
|
sshdConfig.KeyboardInteractiveAuthentication = ToBool(value);
|
|
break;
|
|
case "LogLevel":
|
|
sshdConfig.LogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), value, ignoreCase: true);
|
|
break;
|
|
case "Subsystem":
|
|
sshdConfig.Subsystems.Add(Subsystem.FromConfig(value));
|
|
break;
|
|
case "UsePAM":
|
|
sshdConfig.UsePAM = ToBool(value);
|
|
break;
|
|
case "X11Forwarding":
|
|
sshdConfig.X11Forwarding = ToBool(value);
|
|
break;
|
|
case "Ciphers":
|
|
sshdConfig.Ciphers = ParseCiphers(value);
|
|
break;
|
|
case "KexAlgorithms":
|
|
sshdConfig.KeyExchangeAlgorithms = ParseKeyExchangeAlgorithms(value);
|
|
break;
|
|
case "PubkeyAcceptedAlgorithms":
|
|
sshdConfig.PublicKeyAcceptedAlgorithms = ParsePublicKeyAcceptedAlgorithms(value);
|
|
break;
|
|
case "HostKeyAlgorithms":
|
|
sshdConfig.HostKeyAlgorithms = ParseHostKeyAlgorithms(value);
|
|
break;
|
|
case "MACs":
|
|
sshdConfig.MessageAuthenticationCodeAlgorithms = ParseMacs(value);
|
|
break;
|
|
case "PrintMotd":
|
|
sshdConfig.PrintMotd = ToBool(value);
|
|
break;
|
|
case "AcceptEnv":
|
|
ParseAcceptedEnvironmentVariable(sshdConfig, value);
|
|
break;
|
|
case "Protocol":
|
|
sshdConfig.Protocol = value;
|
|
break;
|
|
case "PermitTTY":
|
|
sshdConfig.PermitTTY = ToBool(value);
|
|
break;
|
|
case "AllowTcpForwarding":
|
|
sshdConfig.AllowTcpForwarding = ToBool(value);
|
|
break;
|
|
case "TrustedUserCAKeys":
|
|
sshdConfig.TrustedUserCAKeys = value;
|
|
break;
|
|
case "HostCertificate":
|
|
sshdConfig.HostCertificate = value;
|
|
break;
|
|
case "KeyRegenerationInterval":
|
|
case "HostbasedAuthentication":
|
|
case "ServerKeyBits":
|
|
case "SyslogFacility":
|
|
case "LoginGraceTime":
|
|
case "PermitRootLogin":
|
|
case "StrictModes":
|
|
case "RSAAuthentication":
|
|
case "PubkeyAuthentication":
|
|
case "IgnoreRhosts":
|
|
case "RhostsRSAAuthentication":
|
|
case "PermitEmptyPasswords":
|
|
case "X11DisplayOffset":
|
|
case "PrintLastLog":
|
|
case "TCPKeepAlive":
|
|
case "AuthorizedKeysFile":
|
|
case "PasswordAuthentication":
|
|
case "GatewayPorts":
|
|
case "Include":
|
|
break;
|
|
default:
|
|
throw new NotSupportedException($"Global option '{name}' is not supported.");
|
|
}
|
|
}
|
|
|
|
private static void ParseAcceptedEnvironmentVariable(SshdConfig sshdConfig, string value)
|
|
{
|
|
var acceptedEnvironmentVariables = value.Split(' ');
|
|
foreach (var acceptedEnvironmentVariable in acceptedEnvironmentVariables)
|
|
{
|
|
sshdConfig.AcceptedEnvironmentVariables.Add(acceptedEnvironmentVariable);
|
|
}
|
|
}
|
|
|
|
private static List<Cipher> ParseCiphers(string value)
|
|
{
|
|
var cipherNames = value.Split(',');
|
|
var ciphers = new List<Cipher>(cipherNames.Length);
|
|
|
|
foreach (var cipherName in cipherNames)
|
|
{
|
|
ciphers.Add(new Cipher(cipherName.Trim()));
|
|
}
|
|
|
|
return ciphers;
|
|
}
|
|
|
|
private static List<KeyExchangeAlgorithm> ParseKeyExchangeAlgorithms(string value)
|
|
{
|
|
var kexNames = value.Split(',');
|
|
var keyExchangeAlgorithms = new List<KeyExchangeAlgorithm>(kexNames.Length);
|
|
|
|
foreach (var kexName in kexNames)
|
|
{
|
|
keyExchangeAlgorithms.Add(new KeyExchangeAlgorithm(kexName.Trim()));
|
|
}
|
|
|
|
return keyExchangeAlgorithms;
|
|
}
|
|
|
|
public static List<PublicKeyAlgorithm> ParsePublicKeyAcceptedAlgorithms(string value)
|
|
{
|
|
if (value is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(value));
|
|
}
|
|
|
|
var publicKeyAlgorithmNames = value.Split(',');
|
|
var publicKeyAlgorithms = new List<PublicKeyAlgorithm>(publicKeyAlgorithmNames.Length);
|
|
|
|
foreach (var publicKeyAlgorithmName in publicKeyAlgorithmNames)
|
|
{
|
|
publicKeyAlgorithms.Add(new PublicKeyAlgorithm(publicKeyAlgorithmName.Trim()));
|
|
}
|
|
|
|
return publicKeyAlgorithms;
|
|
}
|
|
|
|
private static List<HostKeyAlgorithm> ParseHostKeyAlgorithms(string value)
|
|
{
|
|
var algorithmNames = value.Split(',');
|
|
var hostKeyAlgorithms = new List<HostKeyAlgorithm>(algorithmNames.Length);
|
|
|
|
foreach (var algorithmName in algorithmNames)
|
|
{
|
|
hostKeyAlgorithms.Add(new HostKeyAlgorithm(algorithmName.Trim()));
|
|
}
|
|
|
|
return hostKeyAlgorithms;
|
|
}
|
|
|
|
private static List<MessageAuthenticationCodeAlgorithm> ParseMacs(string value)
|
|
{
|
|
var macNames = value.Split(',');
|
|
var macAlgorithms = new List<MessageAuthenticationCodeAlgorithm>(macNames.Length);
|
|
|
|
foreach (var algorithmName in macNames)
|
|
{
|
|
macAlgorithms.Add(new MessageAuthenticationCodeAlgorithm(algorithmName.Trim()));
|
|
}
|
|
|
|
return macAlgorithms;
|
|
}
|
|
|
|
private static void ProcessMatchOption(Match matchConfiguration, string line)
|
|
{
|
|
var matchOptionRegex = new Regex(@"^\s+(?<name>[\S]+)\s+(?<value>.+?){1}\s*$");
|
|
|
|
var optionsMatch = matchOptionRegex.Match(line);
|
|
if (!optionsMatch.Success)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nameGroup = optionsMatch.Groups["name"];
|
|
var valueGroup = optionsMatch.Groups["value"];
|
|
|
|
var name = nameGroup.Value;
|
|
var value = valueGroup.Value;
|
|
|
|
switch (name)
|
|
{
|
|
case "AuthenticationMethods":
|
|
matchConfiguration.AuthenticationMethods = value;
|
|
break;
|
|
default:
|
|
throw new NotSupportedException($"Match option '{name}' is not supported.");
|
|
}
|
|
}
|
|
|
|
private static void ParseCommaSeparatedValue(List<string> list, string value)
|
|
{
|
|
var values = value.Split(',');
|
|
list.AddRange(values);
|
|
}
|
|
|
|
private static bool ToBool(string value)
|
|
{
|
|
switch (value)
|
|
{
|
|
case "yes":
|
|
return true;
|
|
case "no":
|
|
return false;
|
|
default:
|
|
throw new ArgumentException($"Value '{value}' cannot be mapped to a boolean.",
|
|
nameof(value));
|
|
}
|
|
}
|
|
|
|
private static int ToInt(string value)
|
|
{
|
|
return int.Parse(value, NumberFormatInfo.InvariantInfo);
|
|
}
|
|
}
|
|
}
|