SSH.NET is a .NET library for SSH-2 protocol communication, optimized for parallelism. It provides:
- SSH command execution (synchronous and async)
- SFTP file operations (synchronous and async)
- SCP file transfers
- Port forwarding (local, remote, dynamic/SOCKS)
- Interactive shell via
ShellStream - NetConf protocol client
- Multi-factor and certificate-based authentication
Primary entry points are SshClient, SftpClient, ScpClient, and NetConfClient, all extending BaseClient.
| Area | Technology |
|---|---|
| Language | C# (LangVersion=latest) with #nullable enable everywhere |
| Runtimes | .NET Framework 4.6.2, .NET Standard 2.0, .NET 8+, .NET 9+, .NET 10+ |
| Cryptography | BouncyCastle (BouncyCastle.Cryptography) |
| Logging | Microsoft.Extensions.Logging.Abstractions (ILogger/ILoggerFactory) |
| Testing | MSTest v4, Moq, Testcontainers (Docker for integration tests) |
| Build tooling | .NET SDK 10.0.100, Nerdbank.GitVersioning |
| Static analysis | StyleCop.Analyzers, Meziantou.Analyzer, SonarAnalyzer.CSharp |
src/Renci.SshNet/
├── Channels/ # SSH channel types (session, forwarded, X11…)
├── Common/ # Shared utilities, extension methods, custom exceptions
├── Compression/ # Zlib compression support
├── Connection/ # Socket connectors, proxy support (SOCKS4/5, HTTP), key exchange
├── Messages/ # SSH protocol message types
│ ├── Transport/
│ ├── Authentication/
│ └── Connection/
├── Security/ # Algorithms, key-exchange, cryptography wrappers
│ └── Cryptography/ # Ciphers, signatures, key types
├── Sftp/ # SFTP session, requests, responses
├── Netconf/ # NetConf client
└── Abstractions/ # Platform and I/O abstractions
Large classes are split into partial class files per concern (e.g., PrivateKeyFile.PKCS1.cs, PrivateKeyFile.OpenSSH.cs).
| Element | Convention | Example |
|---|---|---|
| Classes, methods, properties | PascalCase | SftpClient, ListDirectory(), IsConnected |
| Private fields | _camelCase |
_isDisposed, _sftpSession |
| Interfaces | I prefix + PascalCase |
ISftpClient, IChannel |
| Constants / static readonly | PascalCase | MaximumSshPacketSize |
| Local variables | camelCase | operationTimeout, connectionInfo |
StyleCop is enforced. Follow existing file conventions:
#nullable enableat the top of every.csfile.usingdirectives outside the namespace block, grouped withSystem.*first, then a blank line, then other namespaces.- 4-space indentation (spaces, not tabs).
- XML doc comments (
///) on all public and internal members; use<inheritdoc/>when inheriting from an interface. - Describe exception conditions in
/// <exception cref="…">tags. - No Hungarian notation.
Use the existing exception hierarchy; do not throw Exception or ApplicationException directly.
SshException
├── SshConnectionException # connection-level failures
├── SshAuthenticationException # auth failures
├── SshOperationTimeoutException # operation timed out
├── SshPassPhraseNullOrEmptyException
├── ProxyException
├── SftpException
│ ├── SftpPermissionDeniedException
│ └── SftpPathNotFoundException
├── ScpException
└── NetConfServerException
- Annotate new exception classes with
#if NETFRAMEWORK [Serializable] #endifand add the protected serialization constructor inside the same#ifblock, matching the pattern inSshException.cs. - Surface async errors by storing them in a
TaskCompletionSourceor re-throwing viaExceptionDispatchInfo.Throw()— never swallow exceptions silently. - Raise
ErrorOccurredevents on long-running components (e.g.,ForwardedPort,Session) rather than propagating the exception across thread boundaries.
- Every public blocking method should have a
…Async(CancellationToken cancellationToken = default)counterpart. Keep both in the same partial class file or co-located partial file. - Validate all arguments at the top of public methods; prefer
ArgumentNullException,ArgumentException,ArgumentOutOfRangeExceptionwith descriptive messages. - Return
IEnumerable<T>for sequences that are already materialized; useIAsyncEnumerable<T>when data streams asynchronously. - Prefer
ReadOnlyMemory<byte>/Memory<byte>overbyte[]in new protocol-layer code. - Do not expose mutable collections directly; use
.AsReadOnly()or copy-on-return.
- Use
async Task/async ValueTaskwithCancellationTokenfor all new async methods. - The socket receive loop and keep-alive timer run on dedicated background threads; protect shared state with
lockor the custom internalLocktype used inSession. - Prefer
TaskCompletionSource<T>to bridge event-driven or callback-based code into the async model. - Never block inside async code with
.Resultor.Wait()— this can cause deadlocks on synchronization-context-bound callers. - Use
ConfigureAwait(false)in library code (this is a class library, not an app).
These areas require extra care:
src/Renci.SshNet/Security/— key exchange, algorithm negotiation. Algorithm choices have direct security implications.src/Renci.SshNet/PrivateKeyFile*.cs— key format parsing (PKCS#1, PKCS#8, OpenSSH, PuTTY, ssh.com). Input is untrusted; validate lengths and offsets before indexing.src/Renci.SshNet/Connection/— host key verification. Never bypass host key checking silently.- Sensitive data in memory: clear key material as soon as it is no longer needed; do not log private key bytes or plaintext passwords even at
Debuglevel. SshNetLoggingConfiguration.WiresharkKeyLogFilePathis a Debug-only diagnostic that writes session keys to disk. It must never be enabled in production builds.- Cryptographic primitives come from BouncyCastle. Prefer existing wrappers over direct calls to BouncyCastle APIs; adding new algorithms requires corresponding unit tests and algorithm negotiation entries.
- Framework: MSTest (
[TestClass],[TestMethod],[TestInitialize],[TestCleanup]) - Mocking: Moq — use
Mock<T>,.Setup(…),.Verify(…) - File naming:
{ClassName}Test[_{Scenario}].cs(e.g.,SftpClientTest.ConnectAsync.cs) - Each scenario lives in its own partial class file inside
Classes/ - Base classes:
TestBase,SessionTestBase,BaseClientTestBase— extend the appropriate base - Arrange-Act-Assert style; each test method is focused on a single observable behaviour
[TestMethod]
public void MethodName_Scenario_ExpectedOutcome()
{
// Arrange
var connectionInfo = new PasswordConnectionInfo("host", 22, "user", "pwd");
var target = new SftpClient(connectionInfo);
// Act
var actual = target.SomeProperty;
// Assert
Assert.AreEqual(expected, actual);
}- Require Docker (via Testcontainers); a running Docker daemon is a prerequisite.
- Run with
dotnet testlike any other project once Docker is available.
# Unit tests only
dotnet test test/Renci.SshNet.Tests/
# All tests (requires Docker)
dotnet test
# With code coverage
dotnet test --collect:"XPlat Code Coverage"- Add unit tests for every new public method and every non-trivial internal method.
- Test both the happy path and error/edge cases (e.g.,
nullarguments, disposed state, cancellation). - For async methods, test cancellation via
CancellationTokenand timeout behaviour. - Do not remove or weaken existing tests; add new tests instead.
- SSH.NET is designed for parallelism; avoid introducing
lockcontention on hot paths. - Prefer
ArrayPool<byte>.SharedorMemoryPool<byte>.Sharedover allocating newbyte[]in protocol encoding/decoding. - The
BufferSizeonSftpClientcontrols read/write payload sizes; keep protocol overhead in mind when changing defaults. - Benchmark significant changes with
test/Renci.SshNet.Benchmarks/using BenchmarkDotNet.
- Run the unit tests first (
dotnet test test/Renci.SshNet.Tests/) to establish a clean baseline. - Keep changes focused. Small, targeted PRs merge faster. Separate refactoring from behaviour changes.
- Multi-targeting: the library targets .NET Framework 4.6.2, .NET Standard 2.0, and modern .NET. Use
#if NETFRAMEWORK/#if NETguards for platform-specific code; confirm all TFMs build withdotnet build. - Public API changes: adding new overloads or members is generally safe. Removing or changing existing signatures is a breaking change — discuss in an issue first.
- Cryptographic changes: consult existing algorithm registration lists (in
ConnectionInfo.csandSession.cs) before adding or removing algorithms. - StyleCop and analyzers: the build treats analyzer warnings as errors in Release/CI mode. Run
dotnet build -c Releaselocally to catch these before pushing. - CI note: some integration tests can flake due to timing or networking. If an existing (unrelated) test fails intermittently in CI but passes locally, it is likely a known flake.