Skip to content

Commit 09ed124

Browse files
authored
Merge branch 'develop' into bcl-mlkem
2 parents 46dbd2e + 25a931c commit 09ed124

30 files changed

+557
-189
lines changed

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,12 @@ dotnet_diagnostic.MA0112.severity = warning
620620
# MA0165: Make interpolated string
621621
dotnet_diagnostic.MA0165.severity = none
622622

623+
# MA0182: Avoid unused internal types
624+
dotnet_diagnostic.MA0182.severity = none
625+
626+
# MA0184: Do not use interpolated string without parameters
627+
dotnet_diagnostic.MA0184.severity = none
628+
623629
#### MSTest rules ####
624630

625631
# MSTEST0015: Test method should not be ignored
@@ -841,6 +847,11 @@ dotnet_diagnostic.IDE0300.severity = none
841847
# TODO: Discuss whether we want to start using this
842848
dotnet_diagnostic.IDE0301.severity = none
843849

850+
# IDE0370: Remove unnecessary suppression
851+
#
852+
# This can lead to many false positives in different target frameworks.
853+
dotnet_diagnostic.IDE0370.severity = suggestion
854+
844855
#### .NET Compiler Platform code style rules ####
845856

846857
### Language rules ###

.github/copilot-instructions.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# GitHub Copilot Instructions for SSH.NET
2+
3+
## What this repository does
4+
5+
SSH.NET is a .NET library for SSH-2 protocol communication, optimized for parallelism. It provides:
6+
7+
- **SSH command execution** (synchronous and async)
8+
- **SFTP** file operations (synchronous and async)
9+
- **SCP** file transfers
10+
- **Port forwarding** (local, remote, dynamic/SOCKS)
11+
- **Interactive shell** via `ShellStream`
12+
- **NetConf** protocol client
13+
- **Multi-factor and certificate-based authentication**
14+
15+
Primary entry points are `SshClient`, `SftpClient`, `ScpClient`, and `NetConfClient`, all extending `BaseClient`.
16+
17+
---
18+
19+
## Core technologies
20+
21+
| Area | Technology |
22+
|---|---|
23+
| Language | C# (`LangVersion=latest`) with `#nullable enable` everywhere |
24+
| Runtimes | .NET Framework 4.6.2, .NET Standard 2.0, .NET 8+, .NET 9+, .NET 10+ |
25+
| Cryptography | BouncyCastle (`BouncyCastle.Cryptography`) |
26+
| Logging | `Microsoft.Extensions.Logging.Abstractions` (`ILogger`/`ILoggerFactory`) |
27+
| Testing | MSTest v4, Moq, Testcontainers (Docker for integration tests) |
28+
| Build tooling | .NET SDK 10.0.100, Nerdbank.GitVersioning |
29+
| Static analysis | StyleCop.Analyzers, Meziantou.Analyzer, SonarAnalyzer.CSharp |
30+
31+
---
32+
33+
## Code organization
34+
35+
```
36+
src/Renci.SshNet/
37+
├── Channels/ # SSH channel types (session, forwarded, X11…)
38+
├── Common/ # Shared utilities, extension methods, custom exceptions
39+
├── Compression/ # Zlib compression support
40+
├── Connection/ # Socket connectors, proxy support (SOCKS4/5, HTTP), key exchange
41+
├── Messages/ # SSH protocol message types
42+
│ ├── Transport/
43+
│ ├── Authentication/
44+
│ └── Connection/
45+
├── Security/ # Algorithms, key-exchange, cryptography wrappers
46+
│ └── Cryptography/ # Ciphers, signatures, key types
47+
├── Sftp/ # SFTP session, requests, responses
48+
├── Netconf/ # NetConf client
49+
└── Abstractions/ # Platform and I/O abstractions
50+
```
51+
52+
Large classes are split into **partial class files** per concern (e.g., `PrivateKeyFile.PKCS1.cs`, `PrivateKeyFile.OpenSSH.cs`).
53+
54+
---
55+
56+
## Naming and style conventions
57+
58+
| Element | Convention | Example |
59+
|---|---|---|
60+
| Classes, methods, properties | PascalCase | `SftpClient`, `ListDirectory()`, `IsConnected` |
61+
| Private fields | `_camelCase` | `_isDisposed`, `_sftpSession` |
62+
| Interfaces | `I` prefix + PascalCase | `ISftpClient`, `IChannel` |
63+
| Constants / static readonly | PascalCase | `MaximumSshPacketSize` |
64+
| Local variables | camelCase | `operationTimeout`, `connectionInfo` |
65+
66+
**StyleCop is enforced.** Follow existing file conventions:
67+
68+
- `#nullable enable` at the top of every `.cs` file.
69+
- `using` directives **outside** the namespace block, grouped with `System.*` first, then a blank line, then other namespaces.
70+
- 4-space indentation (spaces, not tabs).
71+
- XML doc comments (`///`) on all public and internal members; use `<inheritdoc/>` when inheriting from an interface.
72+
- Describe exception conditions in `/// <exception cref="…">` tags.
73+
- No Hungarian notation.
74+
75+
---
76+
77+
## Error handling
78+
79+
Use the existing exception hierarchy; do not throw `Exception` or `ApplicationException` directly.
80+
81+
```
82+
SshException
83+
├── SshConnectionException # connection-level failures
84+
├── SshAuthenticationException # auth failures
85+
├── SshOperationTimeoutException # operation timed out
86+
├── SshPassPhraseNullOrEmptyException
87+
├── ProxyException
88+
├── SftpException
89+
│ ├── SftpPermissionDeniedException
90+
│ └── SftpPathNotFoundException
91+
├── ScpException
92+
└── NetConfServerException
93+
```
94+
95+
- Annotate new exception classes with `#if NETFRAMEWORK [Serializable] #endif` and add the protected serialization constructor inside the same `#if` block, matching the pattern in `SshException.cs`.
96+
- Surface async errors by storing them in a `TaskCompletionSource` or re-throwing via `ExceptionDispatchInfo.Throw()` — never swallow exceptions silently.
97+
- Raise `ErrorOccurred` events on long-running components (e.g., `ForwardedPort`, `Session`) rather than propagating the exception across thread boundaries.
98+
99+
---
100+
101+
## API design
102+
103+
- **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.
104+
- Validate all arguments at the top of public methods; prefer `ArgumentNullException`, `ArgumentException`, `ArgumentOutOfRangeException` with descriptive messages.
105+
- Return `IEnumerable<T>` for sequences that are already materialized; use `IAsyncEnumerable<T>` when data streams asynchronously.
106+
- Prefer `ReadOnlyMemory<byte>` / `Memory<byte>` over `byte[]` in new protocol-layer code.
107+
- Do not expose mutable collections directly; use `.AsReadOnly()` or copy-on-return.
108+
109+
---
110+
111+
## Async and concurrency
112+
113+
- Use `async Task` / `async ValueTask` with `CancellationToken` for all new async methods.
114+
- The socket receive loop and keep-alive timer run on dedicated background threads; protect shared state with `lock` or the custom internal `Lock` type used in `Session`.
115+
- Prefer `TaskCompletionSource<T>` to bridge event-driven or callback-based code into the async model.
116+
- Never block inside async code with `.Result` or `.Wait()` — this can cause deadlocks on synchronization-context-bound callers.
117+
- Use `ConfigureAwait(false)` in library code (this is a class library, not an app).
118+
119+
---
120+
121+
## Security-sensitive areas
122+
123+
These areas require extra care:
124+
125+
- **`src/Renci.SshNet/Security/`** — key exchange, algorithm negotiation. Algorithm choices have direct security implications.
126+
- **`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.
127+
- **`src/Renci.SshNet/Connection/`** — host key verification. Never bypass host key checking silently.
128+
- **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 `Debug` level.
129+
- **`SshNetLoggingConfiguration.WiresharkKeyLogFilePath`** is a Debug-only diagnostic that writes session keys to disk. It must never be enabled in production builds.
130+
- **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.
131+
132+
---
133+
134+
## Testing
135+
136+
### Unit tests (`test/Renci.SshNet.Tests/`)
137+
138+
- Framework: **MSTest** (`[TestClass]`, `[TestMethod]`, `[TestInitialize]`, `[TestCleanup]`)
139+
- Mocking: **Moq** — use `Mock<T>`, `.Setup(…)`, `.Verify(…)`
140+
- File naming: `{ClassName}Test[_{Scenario}].cs` (e.g., `SftpClientTest.ConnectAsync.cs`)
141+
- Each scenario lives in its own partial class file inside `Classes/`
142+
- Base classes: `TestBase`, `SessionTestBase`, `BaseClientTestBase` — extend the appropriate base
143+
- Arrange-Act-Assert style; each test method is focused on a single observable behaviour
144+
145+
```csharp
146+
[TestMethod]
147+
public void MethodName_Scenario_ExpectedOutcome()
148+
{
149+
// Arrange
150+
var connectionInfo = new PasswordConnectionInfo("host", 22, "user", "pwd");
151+
var target = new SftpClient(connectionInfo);
152+
153+
// Act
154+
var actual = target.SomeProperty;
155+
156+
// Assert
157+
Assert.AreEqual(expected, actual);
158+
}
159+
```
160+
161+
### Integration tests (`test/Renci.SshNet.IntegrationTests/`)
162+
163+
- Require **Docker** (via Testcontainers); a running Docker daemon is a prerequisite.
164+
- Run with `dotnet test` like any other project once Docker is available.
165+
166+
### Running tests
167+
168+
```bash
169+
# Unit tests only
170+
dotnet test test/Renci.SshNet.Tests/
171+
172+
# All tests (requires Docker)
173+
dotnet test
174+
175+
# With code coverage
176+
dotnet test --collect:"XPlat Code Coverage"
177+
```
178+
179+
### What to test
180+
181+
- Add unit tests for every new public method and every non-trivial internal method.
182+
- Test both the happy path and error/edge cases (e.g., `null` arguments, disposed state, cancellation).
183+
- For async methods, test cancellation via `CancellationToken` and timeout behaviour.
184+
- Do **not** remove or weaken existing tests; add new tests instead.
185+
186+
---
187+
188+
## Performance considerations
189+
190+
- SSH.NET is designed for parallelism; avoid introducing `lock` contention on hot paths.
191+
- Prefer `ArrayPool<byte>.Shared` or `MemoryPool<byte>.Shared` over allocating new `byte[]` in protocol encoding/decoding.
192+
- The `BufferSize` on `SftpClient` controls read/write payload sizes; keep protocol overhead in mind when changing defaults.
193+
- Benchmark significant changes with `test/Renci.SshNet.Benchmarks/` using BenchmarkDotNet.
194+
195+
---
196+
197+
## Making changes safely
198+
199+
1. **Run the unit tests first** (`dotnet test test/Renci.SshNet.Tests/`) to establish a clean baseline.
200+
2. **Keep changes focused.** Small, targeted PRs merge faster. Separate refactoring from behaviour changes.
201+
3. **Multi-targeting**: the library targets .NET Framework 4.6.2, .NET Standard 2.0, and modern .NET. Use `#if NETFRAMEWORK` / `#if NET` guards for platform-specific code; confirm all TFMs build with `dotnet build`.
202+
4. **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.
203+
5. **Cryptographic changes**: consult existing algorithm registration lists (in `ConnectionInfo.cs` and `Session.cs`) before adding or removing algorithms.
204+
6. **StyleCop and analyzers**: the build treats analyzer warnings as errors in Release/CI mode. Run `dotnet build -c Release` locally to catch these before pushing.
205+
7. **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.

.github/workflows/build.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
test/Renci.SshNet.IntegrationTests/
4949
5050
- name: Archive Coverlet Results
51-
uses: actions/upload-artifact@v5
51+
uses: actions/upload-artifact@v6
5252
with:
5353
name: Coverlet Results Linux
5454
path: coverlet
@@ -74,7 +74,7 @@ jobs:
7474
run: dotnet pack
7575

7676
- name: Archive NuGet Package
77-
uses: actions/upload-artifact@v5
77+
uses: actions/upload-artifact@v6
7878
with:
7979
name: NuGet Package
8080
path: src/Renci.SshNet/bin/Release/*.*nupkg
@@ -166,7 +166,7 @@ jobs:
166166
test\Renci.SshNet.IntegrationTests\
167167

168168
- name: Archive Coverlet Results
169-
uses: actions/upload-artifact@v5
169+
uses: actions/upload-artifact@v6
170170
with:
171171
name: Coverlet Results Windows .NET Framework
172172
path: coverlet
@@ -234,7 +234,7 @@ jobs:
234234
test\Renci.SshNet.IntegrationTests\
235235

236236
- name: Archive Coverlet Results
237-
uses: actions/upload-artifact@v5
237+
uses: actions/upload-artifact@v6
238238
with:
239239
name: Coverlet Results Windows .NET
240240
path: coverlet
@@ -252,7 +252,7 @@ jobs:
252252
- Windows-Integration-Tests-Net
253253
steps:
254254
- name: Download NuGet Package
255-
uses: actions/download-artifact@v6
255+
uses: actions/download-artifact@v7
256256
with:
257257
name: NuGet Package
258258

@@ -281,7 +281,7 @@ jobs:
281281
- Windows-Integration-Tests-Net
282282
steps:
283283
- name: Download NuGet Package
284-
uses: actions/download-artifact@v6
284+
uses: actions/download-artifact@v7
285285
with:
286286
name: NuGet Package
287287

Directory.Packages.props

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
1010
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
1111
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.1" />
12-
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.264" />
12+
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.18" />
1313
<!-- Should stay on LTS .NET releases. -->
14-
<PackageVersion Include="Microsoft.Bcl.Cryptography" Version="10.0.1" />
14+
<PackageVersion Include="Microsoft.Bcl.Cryptography" Version="10.0.3" />
1515
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
16-
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
17-
<PackageVersion Include="MSTest" Version="4.0.2" />
16+
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
17+
<PackageVersion Include="MSTest" Version="4.1.0" />
1818
<PackageVersion Include="Moq" Version="4.20.72" />
1919
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
2020
<PackageVersion Include="PolySharp" Version="1.15.0" />
21-
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.17.0.131074" />
21+
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.20.0.135146" />
2222
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
2323
<!-- Should stay on LTS .NET releases. -->
24-
<PackageVersion Include="System.Formats.Asn1" Version="10.0.1" />
25-
<PackageVersion Include="Testcontainers" Version="4.9.0" />
24+
<PackageVersion Include="System.Formats.Asn1" Version="10.0.3" />
25+
<PackageVersion Include="Testcontainers" Version="4.10.0" />
2626
</ItemGroup>
2727
</Project>

global.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "10.0.100",
4-
"rollForward": "latestFeature"
3+
"version": "10.0.201",
4+
"rollForward": "latestMinor"
55
}
66
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Renci.SshNet
2+
{
3+
/// <summary>
4+
/// Provides the progress for a file download.
5+
/// </summary>
6+
public struct DownloadFileProgressReport
7+
{
8+
/// <summary>
9+
/// Gets the total number of bytes downloaded.
10+
/// </summary>
11+
public ulong TotalBytesDownloaded { get; internal set; }
12+
}
13+
}

src/Renci.SshNet/ForwardedPort.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ protected virtual void StopPort(TimeSpan timeout)
105105
RaiseClosing();
106106

107107
var session = Session;
108-
if (session is not null)
109-
{
110-
session.ErrorOccured -= Session_ErrorOccurred;
111-
}
108+
session?.ErrorOccured -= Session_ErrorOccurred;
112109
}
113110

114111
/// <summary>

0 commit comments

Comments
 (0)