├── .gitmodules
├── icons
└── logo.png
├── .dockerignore
├── .editorconfig
├── docs
└── Writerside
│ ├── images
│ ├── logo.png
│ └── memory_in_peak.png
│ ├── v.list
│ ├── c.list
│ ├── writerside.cfg
│ ├── redirection-rules.xml
│ ├── cfg
│ └── buildprofiles.xml
│ ├── main.tree
│ └── topics
│ ├── quickstart.md
│ ├── performance-and-reliability.md
│ ├── keeping-connection-alive.md
│ ├── authentication.md
│ ├── running-tests-locally.md
│ ├── scp.md
│ ├── host-key-retrieval-and-verification.md
│ └── command-execution.md
├── src
├── NullOpsDevs.LibSsh.Test
│ ├── docker
│ │ ├── test-keys
│ │ │ ├── .gitattributes
│ │ │ ├── id_ed25519.pub
│ │ │ ├── id_rsa.pub
│ │ │ ├── id_rsa_protected.pub
│ │ │ ├── id_ed25519
│ │ │ ├── id_rsa
│ │ │ └── id_rsa_protected
│ │ ├── setup-keys.sh
│ │ └── Dockerfile
│ ├── docker-compose.yml
│ ├── NullOpsDevs.LibSsh.Test.csproj
│ ├── AnsiConsoleLogger.cs
│ ├── TestConfig.cs
│ ├── TestNativePreloader.cs
│ └── TestHelper.cs
├── NullOpsDevs.LibSsh
│ ├── Generated
│ │ ├── _LIBSSH2_SFTP.cs
│ │ ├── _LIBSSH2_AGENT.cs
│ │ ├── _LIBSSH2_CHANNEL.cs
│ │ ├── _LIBSSH2_LISTENER.cs
│ │ ├── _LIBSSH2_PUBLICKEY.cs
│ │ ├── _LIBSSH2_SESSION.cs
│ │ ├── _LIBSSH2_KNOWNHOSTS.cs
│ │ ├── _LIBSSH2_SFTP_HANDLE.cs
│ │ ├── libssh2_crypto_engine_t.cs
│ │ ├── _LIBSSH2_USERAUTH_KBDINT_RESPONSE.cs
│ │ ├── _LIBSSH2_USERAUTH_KBDINT_PROMPT.cs
│ │ ├── libssh2_knownhost.cs
│ │ ├── libssh2_agent_publickey.cs
│ │ ├── _libssh2_publickey_attribute.cs
│ │ ├── _LIBSSH2_SK_SIG_INFO.cs
│ │ ├── _LIBSSH2_SFTP_ATTRIBUTES.cs
│ │ ├── _libssh2_publickey_list.cs
│ │ ├── _LIBSSH2_PRIVKEY_SK.cs
│ │ ├── NativeTypeNameAttribute.cs
│ │ ├── _LIBSSH2_POLLFD.cs
│ │ ├── _LIBSSH2_SFTP_STATVFS.cs
│ │ └── NativeAnnotationAttribute.cs
│ ├── generate.ps1
│ ├── generate.sh
│ ├── native
│ │ ├── libssh2-osx-x64
│ │ │ ├── libssh2.a
│ │ │ └── libssh2.dylib
│ │ ├── libssh2-win-x64
│ │ │ ├── libssh2.a
│ │ │ └── libssh2.dll
│ │ ├── libssh2-linux-x64
│ │ │ ├── libssh2.a
│ │ │ ├── libssh2.so
│ │ │ ├── libssh2.so.1
│ │ │ └── libssh2.so.1.0.1
│ │ ├── libssh2-osx-arm64
│ │ │ ├── libssh2.a
│ │ │ └── libssh2.dylib
│ │ └── libssh2-linux-arm64
│ │ │ ├── libssh2.a
│ │ │ ├── libssh2.so
│ │ │ ├── libssh2.so.1
│ │ │ └── libssh2.so.1.0.1
│ ├── Compatability.cs
│ ├── Core
│ │ ├── HostKey.cs
│ │ ├── SshConnectionStatus.cs
│ │ ├── SshHashType.cs
│ │ ├── SshCommandResult.cs
│ │ ├── SshHostKeyType.cs
│ │ ├── SshMethod.cs
│ │ ├── CommandExecutionOptions.cs
│ │ ├── ChannelReader.cs
│ │ └── SshError.cs
│ ├── Interop
│ │ ├── StringPointers.cs
│ │ ├── LibSsh2.cs
│ │ └── NativeBuffer.cs
│ ├── Credentials
│ │ ├── SshPasswordCredential.cs
│ │ ├── SshPublicKeyFromMemoryCredential.cs
│ │ ├── SshPublicKeyCredential.cs
│ │ ├── SshAgentCredential.cs
│ │ ├── SshHostBasedCredential.cs
│ │ └── SshCredential.cs
│ ├── Platform
│ │ ├── StatMingw64.cs
│ │ ├── StatLinuxX64.cs
│ │ ├── StatDarwin.cs
│ │ ├── StatFreebsd.cs
│ │ └── PlatformDependentStat.cs
│ ├── Terminal
│ │ ├── TerminalType.cs
│ │ ├── TerminalModesBuilder.cs
│ │ └── TerminalMode.cs
│ ├── Exceptions
│ │ └── SshException.cs
│ ├── Extensions
│ │ └── LibSshExtensions.cs
│ └── NullOpsDevs.LibSsh.csproj
├── NullOpsDevs.Libssh.NuGetTest
│ ├── Program.cs
│ └── NullOpsDevs.Libssh.NuGetTest.csproj
└── NullOpsDevs.LibSsh.slnx
├── .github
└── workflows
│ ├── docs-workflow.yml
│ ├── test.yml
│ ├── test-dotnet-versions.yml
│ └── publish.yml
├── LICENSE
├── README.md
└── .gitignore
/.gitmodules:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/icons/logo.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/bin/
2 | **/obj/
3 | **/.vs/
4 | **/.git/
5 | **/node_modules/
6 | ssh-server-config/
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [**/Generated/**/*.cs]
2 | generated_code = true
3 | dotnet_diagnostic.CS1591.severity = none
4 |
--------------------------------------------------------------------------------
/docs/Writerside/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/docs/Writerside/images/logo.png
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/.gitattributes:
--------------------------------------------------------------------------------
1 | # Force LF line endings for SSH key files
2 | * text eol=lf
3 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SFTP.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_SFTP;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_AGENT.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_AGENT;
4 |
--------------------------------------------------------------------------------
/docs/Writerside/images/memory_in_peak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/docs/Writerside/images/memory_in_peak.png
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_CHANNEL.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_CHANNEL;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_LISTENER.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_LISTENER;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_PUBLICKEY.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_PUBLICKEY;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SESSION.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_SESSION;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_KNOWNHOSTS.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_KNOWNHOSTS;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SFTP_HANDLE.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_SFTP_HANDLE;
4 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_ed25519.pub:
--------------------------------------------------------------------------------
1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB+g+CmnpSHg5hZ+ImBKSvfmpghB+0qNKTCAw/x4DCLE test-key-ed25519
2 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/generate.ps1:
--------------------------------------------------------------------------------
1 | dotnet tool install --global ClangSharpPInvokeGenerator --version 18.1.0.4;
2 | ClangSharpPInvokeGenerator '@generate.rsp';
3 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/generate.sh:
--------------------------------------------------------------------------------
1 | dotnet tool install --global ClangSharpPInvokeGenerator --version 18.1.0.4;
2 | ClangSharpPInvokeGenerator '@generate.rsp';
3 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-osx-x64/libssh2.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-osx-x64/libssh2.a
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-win-x64/libssh2.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-win-x64/libssh2.a
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.a
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-osx-arm64/libssh2.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-osx-arm64/libssh2.a
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-win-x64/libssh2.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-win-x64/libssh2.dll
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.a
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so.1
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-osx-x64/libssh2.dylib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-osx-x64/libssh2.dylib
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so.1
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-osx-arm64/libssh2.dylib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-osx-arm64/libssh2.dylib
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so.1.0.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-x64/libssh2.so.1.0.1
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so.1.0.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NullOpsDevs/LibSshNet/HEAD/src/NullOpsDevs.LibSsh/native/libssh2-linux-arm64/libssh2.so.1.0.1
--------------------------------------------------------------------------------
/docs/Writerside/v.list:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Compatability.cs:
--------------------------------------------------------------------------------
1 | #if NETSTANDARD2_0_OR_GREATER
2 | using JetBrains.Annotations;
3 |
4 | // ReSharper disable once CheckNamespace
5 | namespace System.Runtime.CompilerServices;
6 | [PublicAPI]
7 | internal static class IsExternalInit;
8 | #endif
9 |
--------------------------------------------------------------------------------
/docs/Writerside/c.list:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.Libssh.NuGetTest/Program.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh;
2 | using NullOpsDevs.LibSsh.Credentials;
3 |
4 | var ssh = new SshSession();
5 | ssh.Connect("localhost", 2222);
6 | ssh.Authenticate(SshCredential.FromPassword("user", "12345"));
7 |
8 | Console.WriteLine(ssh.ExecuteCommand("ls").Stdout);
9 |
--------------------------------------------------------------------------------
/docs/Writerside/writerside.cfg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/libssh2_crypto_engine_t.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal enum libssh2_crypto_engine_t
4 | {
5 | libssh2_no_crypto = 0,
6 | libssh2_openssl,
7 | libssh2_gcrypt,
8 | libssh2_mbedtls,
9 | libssh2_wincng,
10 | libssh2_os400qc3,
11 | }
12 |
--------------------------------------------------------------------------------
/docs/Writerside/redirection-rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_USERAUTH_KBDINT_RESPONSE.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _LIBSSH2_USERAUTH_KBDINT_RESPONSE
4 | {
5 | [NativeTypeName("char *")]
6 | public sbyte* text;
7 |
8 | [NativeTypeName("unsigned int")]
9 | public uint length;
10 | }
11 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_USERAUTH_KBDINT_PROMPT.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _LIBSSH2_USERAUTH_KBDINT_PROMPT
4 | {
5 | [NativeTypeName("unsigned char *")]
6 | public byte* text;
7 |
8 | [NativeTypeName("size_t")]
9 | public nuint length;
10 |
11 | [NativeTypeName("unsigned char")]
12 | public byte echo;
13 | }
14 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/libssh2_knownhost.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct libssh2_knownhost
4 | {
5 | [NativeTypeName("unsigned int")]
6 | public uint magic;
7 |
8 | public void* node;
9 |
10 | [NativeTypeName("char *")]
11 | public sbyte* name;
12 |
13 | [NativeTypeName("char *")]
14 | public sbyte* key;
15 |
16 | public int typemask;
17 | }
18 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCupxD+73xKHfI2Z7dn0giHOxK1uqndHhqE1pncX+jcsolymD2yJ4d1FvLjQZNnVqyfjYXg6ZZGWdK8Hj+6SwFvb38Ka1mEzO27scPSQuyRCKWkWK9tF5Ryi1QRlIZAFSvBv7znUcztzrY0YYHjzw20Ymry9D95maWK0/2BmJ16KtR5X0vgVtZDJS1WAUgRtJyJEpTMG5nZRukQp/rPYHwzQQOHj8wyXLsp6q2hh1C9CxPW2ckiMzCLoWLQ7cKgHz5fsJR7QAwu2KidxQ/pi7Q8QDMDh/Sw/Bp4SnL0HIghVLG/s0Y1PN2huuMd8YiFbl0sOxaqe2kP3E1vNISzL14N test-key
2 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa_protected.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGg0eOfJfDl07zo6kBwbFMe9R0EbCCVTSj+P7QiFtq9KCixrHLO6AWRJeLa6a4UQtEFt7M/qMkqk0ZcKMbjq6SK4ZqqjKFogpwUT7lUmJGIfs9/JIgW6wG57PQlLqW8Ie518mnh2Pimkjmd5BoDu7OS+8VJg/twItYZL+dh1oDA+zZzkziqmmIB5b45IED2rUjfTyPO9r8t1OepE3Mx2wACcuwjz4+VqSh0w9Zwaw045l417RjX4hwUluX+AN4o0JLFM1mnbciwi/pKlAi+7SJm6AFOfEXHWi/37ElSSe9NHqt3sWl05Qq5+otS/C/9a8F0vlMu0wrlDsXqq+8xKF7 test-key-protected
2 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_ed25519:
--------------------------------------------------------------------------------
1 | -----BEGIN OPENSSH PRIVATE KEY-----
2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
3 | QyNTUxOQAAACAfoPgpp6Uh4OYWfiJgSkr35qYIQftKjSkwgMP8eAwixAAAAJh1tPN9dbTz
4 | fQAAAAtzc2gtZWQyNTUxOQAAACAfoPgpp6Uh4OYWfiJgSkr35qYIQftKjSkwgMP8eAwixA
5 | AAAEBClE8fRi7jmFtqfUedniCvmWSv+MmO/kpWlEZtj9oJ8R+g+CmnpSHg5hZ+ImBKSvfm
6 | pghB+0qNKTCAw/x4DCLEAAAAEHRlc3Qta2V5LWVkMjU1MTkBAgMEBQ==
7 | -----END OPENSSH PRIVATE KEY-----
8 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/libssh2_agent_publickey.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct libssh2_agent_publickey
4 | {
5 | [NativeTypeName("unsigned int")]
6 | public uint magic;
7 |
8 | public void* node;
9 |
10 | [NativeTypeName("unsigned char *")]
11 | public byte* blob;
12 |
13 | [NativeTypeName("size_t")]
14 | public nuint blob_len;
15 |
16 | [NativeTypeName("char *")]
17 | public sbyte* comment;
18 | }
19 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.Libssh.NuGetTest/NullOpsDevs.Libssh.NuGetTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_libssh2_publickey_attribute.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _libssh2_publickey_attribute
4 | {
5 | [NativeTypeName("const char *")]
6 | public sbyte* name;
7 |
8 | [NativeTypeName("unsigned long")]
9 | public uint name_len;
10 |
11 | [NativeTypeName("const char *")]
12 | public sbyte* value;
13 |
14 | [NativeTypeName("unsigned long")]
15 | public uint value_len;
16 |
17 | [NativeTypeName("char")]
18 | public sbyte mandatory;
19 | }
20 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/HostKey.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Core;
2 |
3 | ///
4 | /// Represents an SSH server's host key used for server authentication and verification.
5 | ///
6 | public readonly struct SshHostKey
7 | {
8 | ///
9 | /// Gets the raw host key data as a byte array.
10 | ///
11 | public byte[] Key { get; init; }
12 |
13 | ///
14 | /// Gets the type of the host key algorithm (e.g., RSA, Ed25519, ECDSA).
15 | ///
16 | public SshHostKeyType Type { get; init; }
17 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ssh-server:
3 | build:
4 | context: ./docker
5 | dockerfile: Dockerfile
6 | container_name: libssh-test-server
7 | hostname: ssh-test
8 | network_mode: "host"
9 | restart: unless-stopped
10 | ulimits:
11 | nofile:
12 | soft: 65536
13 | hard: 65536
14 | nproc:
15 | soft: 32768
16 | hard: 32768
17 | healthcheck:
18 | test: ["CMD", "nc", "-z", "localhost", "2222"]
19 | interval: 5s
20 | timeout: 3s
21 | retries: 10
22 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SK_SIG_INFO.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _LIBSSH2_SK_SIG_INFO
4 | {
5 | [NativeTypeName("uint8_t")]
6 | public byte flags;
7 |
8 | [NativeTypeName("uint32_t")]
9 | public uint counter;
10 |
11 | [NativeTypeName("unsigned char *")]
12 | public byte* sig_r;
13 |
14 | [NativeTypeName("size_t")]
15 | public nuint sig_r_len;
16 |
17 | [NativeTypeName("unsigned char *")]
18 | public byte* sig_s;
19 |
20 | [NativeTypeName("size_t")]
21 | public nuint sig_s_len;
22 | }
23 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SFTP_ATTRIBUTES.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_SFTP_ATTRIBUTES
4 | {
5 | [NativeTypeName("unsigned long")]
6 | public uint flags;
7 |
8 | [NativeTypeName("libssh2_uint64_t")]
9 | public ulong filesize;
10 |
11 | [NativeTypeName("unsigned long")]
12 | public uint uid;
13 |
14 | [NativeTypeName("unsigned long")]
15 | public uint gid;
16 |
17 | [NativeTypeName("unsigned long")]
18 | public uint permissions;
19 |
20 | [NativeTypeName("unsigned long")]
21 | public uint atime;
22 |
23 | [NativeTypeName("unsigned long")]
24 | public uint mtime;
25 | }
26 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_libssh2_publickey_list.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _libssh2_publickey_list
4 | {
5 | [NativeTypeName("unsigned char *")]
6 | public byte* packet;
7 |
8 | [NativeTypeName("const unsigned char *")]
9 | public byte* name;
10 |
11 | [NativeTypeName("unsigned long")]
12 | public uint name_len;
13 |
14 | [NativeTypeName("const unsigned char *")]
15 | public byte* blob;
16 |
17 | [NativeTypeName("unsigned long")]
18 | public uint blob_len;
19 |
20 | [NativeTypeName("unsigned long")]
21 | public uint num_attrs;
22 |
23 | [NativeTypeName("libssh2_publickey_attribute *")]
24 | public _libssh2_publickey_attribute* attrs;
25 | }
26 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshConnectionStatus.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Core;
2 |
3 | ///
4 | /// Represents the connection status of an SSH session.
5 | ///
6 | public enum SshConnectionStatus
7 | {
8 | ///
9 | /// The session is not connected to any server.
10 | ///
11 | Disconnected,
12 |
13 | ///
14 | /// The session has established a connection to the server but is not yet authenticated.
15 | ///
16 | Connected,
17 |
18 | ///
19 | /// The session is connected and successfully authenticated (logged in).
20 | ///
21 | LoggedIn,
22 |
23 | ///
24 | /// Session has been disposed.
25 | ///
26 | Disposed
27 | }
28 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/setup-keys.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Setup authorized keys and test files
3 |
4 | echo "Setting up SSH keys..."
5 |
6 | # Setup authorized_keys
7 | if [ -d /keys ]; then
8 | cat /keys/*.pub > /home/user/.ssh/authorized_keys 2>/dev/null || true
9 | chmod 600 /home/user/.ssh/authorized_keys
10 | chown user:user /home/user/.ssh/authorized_keys
11 | echo "Authorized keys configured"
12 | fi
13 |
14 | # Create test files
15 | echo "Creating test files..."
16 | echo "Small test file content" > /test-files/small.txt
17 | dd if=/dev/urandom of=/test-files/medium.bin bs=1024 count=1024 2>/dev/null
18 | dd if=/dev/urandom of=/test-files/large.dat bs=1024 count=10240 2>/dev/null
19 | chmod 644 /test-files/*
20 | echo "Test files created"
21 |
22 | echo "Setup complete, starting SSHD..."
23 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_PRIVKEY_SK.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal unsafe struct _LIBSSH2_PRIVKEY_SK
4 | {
5 | public int algorithm;
6 |
7 | [NativeTypeName("uint8_t")]
8 | public byte flags;
9 |
10 | [NativeTypeName("const char *")]
11 | public sbyte* application;
12 |
13 | [NativeTypeName("const unsigned char *")]
14 | public byte* key_handle;
15 |
16 | [NativeTypeName("size_t")]
17 | public nuint handle_len;
18 |
19 | [NativeTypeName("int (*)(LIBSSH2_SESSION *, LIBSSH2_SK_SIG_INFO *, const unsigned char *, size_t, int, uint8_t, const char *, const unsigned char *, size_t, void **)")]
20 | public delegate* unmanaged[Cdecl]<_LIBSSH2_SESSION*, _LIBSSH2_SK_SIG_INFO*, byte*, nuint, int, byte, sbyte*, byte*, nuint, void**, int> sign_callback;
21 |
22 | public void** orig_abstract;
23 | }
24 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/NullOpsDevs.LibSsh.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0;net7.0;net8.0;net9.0;net10.0
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Interop/StringPointers.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Interop;
4 |
5 | ///
6 | /// Provides cached native string pointers for commonly used libssh2 string constants.
7 | ///
8 | internal static class StringPointers
9 | {
10 | ///
11 | /// Pointer to the "session" string.
12 | ///
13 | public static readonly unsafe sbyte* Session = (sbyte*)Marshal.StringToHGlobalAnsi("session");
14 |
15 | ///
16 | /// Pointer to the "exec" string.
17 | ///
18 | public static readonly unsafe sbyte* Exec = (sbyte*)Marshal.StringToHGlobalAnsi("exec");
19 |
20 | ///
21 | /// Pointer to the "Session disposed" string.
22 | ///
23 | public static readonly unsafe sbyte* SessionDisposed = (sbyte*)Marshal.StringToHGlobalAnsi("Session disposed");
24 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/NativeTypeNameAttribute.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace NullOpsDevs.LibSsh.Generated;
4 |
5 | /// Defines the type of a member as it was used in the native signature.
6 | [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
7 | [Conditional("DEBUG")]
8 | internal sealed class NativeTypeNameAttribute : Attribute
9 | {
10 | private readonly string _name;
11 |
12 | /// Initializes a new instance of the class.
13 | /// The name of the type that was used in the native signature.
14 | public NativeTypeNameAttribute(string name)
15 | {
16 | _name = name;
17 | }
18 |
19 | /// Gets the name of the type that was used in the native signature.
20 | public string Name => _name;
21 | }
22 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_POLLFD.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Generated;
4 |
5 | internal unsafe struct _LIBSSH2_POLLFD
6 | {
7 | [NativeTypeName("unsigned char")]
8 | public byte type;
9 |
10 | [NativeTypeName("__AnonymousRecord_libssh2_L452_C5")]
11 | public _fd_e__Union fd;
12 |
13 | [NativeTypeName("unsigned long")]
14 | public uint events;
15 |
16 | [NativeTypeName("unsigned long")]
17 | public uint revents;
18 |
19 | [StructLayout(LayoutKind.Explicit)]
20 | internal struct _fd_e__Union
21 | {
22 | [FieldOffset(0)]
23 | [NativeTypeName("libssh2_socket_t")]
24 | public ulong socket;
25 |
26 | [FieldOffset(0)]
27 | [NativeTypeName("LIBSSH2_CHANNEL *")]
28 | public _LIBSSH2_CHANNEL* channel;
29 |
30 | [FieldOffset(0)]
31 | [NativeTypeName("LIBSSH2_LISTENER *")]
32 | public _LIBSSH2_LISTENER* listener;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/_LIBSSH2_SFTP_STATVFS.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Generated;
2 |
3 | internal struct _LIBSSH2_SFTP_STATVFS
4 | {
5 | [NativeTypeName("libssh2_uint64_t")]
6 | public ulong f_bsize;
7 |
8 | [NativeTypeName("libssh2_uint64_t")]
9 | public ulong f_frsize;
10 |
11 | [NativeTypeName("libssh2_uint64_t")]
12 | public ulong f_blocks;
13 |
14 | [NativeTypeName("libssh2_uint64_t")]
15 | public ulong f_bfree;
16 |
17 | [NativeTypeName("libssh2_uint64_t")]
18 | public ulong f_bavail;
19 |
20 | [NativeTypeName("libssh2_uint64_t")]
21 | public ulong f_files;
22 |
23 | [NativeTypeName("libssh2_uint64_t")]
24 | public ulong f_ffree;
25 |
26 | [NativeTypeName("libssh2_uint64_t")]
27 | public ulong f_favail;
28 |
29 | [NativeTypeName("libssh2_uint64_t")]
30 | public ulong f_fsid;
31 |
32 | [NativeTypeName("libssh2_uint64_t")]
33 | public ulong f_flag;
34 |
35 | [NativeTypeName("libssh2_uint64_t")]
36 | public ulong f_namemax;
37 | }
38 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Generated/NativeAnnotationAttribute.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace NullOpsDevs.LibSsh.Generated;
4 |
5 | /// Defines the annotation found in a native declaration.
6 | [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true)]
7 | [Conditional("DEBUG")]
8 | internal sealed class NativeAnnotationAttribute : Attribute
9 | {
10 | private readonly string _annotation;
11 |
12 | /// Initializes a new instance of the class.
13 | /// The annotation that was used in the native declaration.
14 | public NativeAnnotationAttribute(string annotation)
15 | {
16 | _annotation = annotation;
17 | }
18 |
19 | /// Gets the annotation that was used in the native declaration.
20 | public string Annotation => _annotation;
21 | }
22 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshHashType.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using JetBrains.Annotations;
3 | using NullOpsDevs.LibSsh.Generated;
4 |
5 | namespace NullOpsDevs.LibSsh.Core;
6 |
7 | ///
8 | /// Specifies the hash algorithm to use for computing SSH host key fingerprints.
9 | ///
10 | [PublicAPI]
11 | [SuppressMessage("ReSharper", "InconsistentNaming")]
12 | public enum SshHashType
13 | {
14 | ///
15 | /// MD5 hash algorithm (128-bit). Note: MD5 is cryptographically weak and should only be used for compatibility with legacy systems.
16 | ///
17 | MD5 = LibSshNative.LIBSSH2_HOSTKEY_HASH_MD5,
18 |
19 | ///
20 | /// SHA-1 hash algorithm (160-bit). Note: SHA-1 is considered weak and SHA-256 is recommended for new implementations.
21 | ///
22 | SHA1 = LibSshNative.LIBSSH2_HOSTKEY_HASH_SHA1,
23 |
24 | ///
25 | /// SHA-256 hash algorithm (256-bit). This is the recommended hash algorithm for host key verification.
26 | ///
27 | SHA256 = LibSshNative.LIBSSH2_HOSTKEY_HASH_SHA256
28 | }
29 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/AnsiConsoleLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Spectre.Console;
3 |
4 | namespace NullOpsDevs.LibSsh.Test;
5 |
6 | public class AnsiConsoleLogger : ILogger
7 | {
8 | ///
9 | public IDisposable? BeginScope(TState state) where TState : notnull
10 | {
11 | throw new NotImplementedException();
12 | }
13 |
14 | ///
15 | public bool IsEnabled(LogLevel logLevel) => true;
16 |
17 | ///
18 | public void Log(LogLevel logLevel, EventId _, TState state, Exception? exception, Func formatter)
19 | {
20 | var logLevelColor = logLevel switch
21 | {
22 | LogLevel.Trace => "dim",
23 | LogLevel.Debug => "dim",
24 | LogLevel.Information => "white",
25 | LogLevel.Warning => "yellow",
26 | LogLevel.Error => "red",
27 | LogLevel.Critical => "red",
28 | _ => "white"
29 | };
30 |
31 | var formatted = formatter(state, exception);
32 | AnsiConsole.MarkupLine($"[{logLevelColor}]{logLevel:G}[/] {Markup.Escape(formatted)}");
33 | }
34 | }
--------------------------------------------------------------------------------
/.github/workflows/docs-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - 'docs/**'
8 | - '.github/workflows/docs-workflow.yml'
9 |
10 | env:
11 | WRITERSIDE_VERSION: "2025.04.8412"
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Build docs
20 | run: >
21 | docker run --rm -v ${{ github.workspace }}:/opt/sources
22 | -e SOURCE_DIR=/opt/sources/docs/
23 | -e MODULE_INSTANCE=Writerside/main
24 | -e OUTPUT_DIR=/opt/sources/output
25 | -e RUNNER=other
26 | jetbrains/writerside-builder:${{ env.WRITERSIDE_VERSION }}
27 |
28 | - name: Extract built docs
29 | run: |
30 | mkdir -p deploy
31 | unzip output/webHelpMAIN2-all.zip -d deploy
32 |
33 | - name: Deploy to Cloudflare Pages
34 | uses: cloudflare/wrangler-action@v3
35 | with:
36 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
37 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
38 | command: pages deploy deploy --project-name=nullopsdevs-libsshnet --branch=main
39 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshPasswordCredential.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 | using NullOpsDevs.LibSsh.Interop;
3 |
4 | namespace NullOpsDevs.LibSsh.Credentials;
5 |
6 | ///
7 | /// Represents SSH authentication using username and password.
8 | ///
9 | /// The username for authentication.
10 | /// The password for authentication.
11 | public class SshPasswordCredential(string username, string password) : SshCredential
12 | {
13 | ///
14 | public override unsafe bool Authenticate(SshSession session)
15 | {
16 | if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
17 | return false;
18 |
19 | using var usernameBuffer = NativeBuffer.Allocate(username);
20 | using var passwordBuffer = NativeBuffer.Allocate(password);
21 |
22 | var authResult = LibSshNative.libssh2_userauth_password_ex(
23 | session.SessionPtr,
24 | usernameBuffer.AsPointer(), (uint)usernameBuffer.Length,
25 | passwordBuffer.AsPointer(), (uint)passwordBuffer.Length, null);
26 |
27 | return authResult >= 0;
28 | }
29 | }
--------------------------------------------------------------------------------
/docs/Writerside/cfg/buildprofiles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 | iris
9 | false
10 |
11 | logo.png
12 | logo.png
13 |
14 | Library on NuGet
15 | https://www.nuget.org/packages/NullOpsDevs.LibSsh/
16 | true
17 |
18 | 1400
19 |
20 | VIS011
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Platform/StatMingw64.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Platform;
4 |
5 | ///
6 | /// Platform-specific file stat structure for MinGW64 (Windows).
7 | ///
8 | [StructLayout(LayoutKind.Sequential)]
9 | internal struct StatMingw64
10 | {
11 | /// Device ID containing the file.
12 | public uint st_dev;
13 |
14 | /// Inode number.
15 | public ushort st_ino;
16 |
17 | /// File mode (type and permissions).
18 | public ushort st_mode;
19 |
20 | /// Number of hard links.
21 | public short st_nlink;
22 |
23 | /// User ID of the file owner.
24 | public short st_uid;
25 |
26 | /// Group ID of the file owner.
27 | public short st_gid;
28 |
29 | /// Device ID if this is a special file.
30 | public uint st_rdev;
31 |
32 | /// Total size in bytes.
33 | public long st_size;
34 |
35 | /// Time of last access (Unix timestamp).
36 | public long st_atime;
37 |
38 | /// Time of last modification (Unix timestamp).
39 | public long st_mtime;
40 |
41 | /// Time of last status change (Unix timestamp).
42 | public long st_ctime;
43 | }
--------------------------------------------------------------------------------
/docs/Writerside/main.tree:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quickstart
2 |
3 | ### 1. Install NuGet package
4 |
5 |
6 |
7 |
8 | dotnet add NullOpsDevs.LibSsh
9 |
10 |
11 |
12 |
13 | <ItemGroup>
14 | <PackageReference Include="NullOpsDevs.LibSsh" Version="<...>" />
15 | </ItemGroup>
16 |
17 |
18 |
19 |
20 | ### 2. Connect to the server and execute some commands!
21 |
22 | ```c#
23 | using NullOpsDevs.LibSsh;
24 | using NullOpsDevs.LibSsh.Credentials;
25 |
26 | var ssh = new SshSession();
27 | ssh.Connect("localhost", 2222);
28 | ssh.Authenticate(SshCredential.FromPassword("user", "12345"));
29 |
30 | Console.WriteLine(ssh.ExecuteCommand("ls").Stdout);
31 | ```
32 |
33 | ## Next Steps
34 |
35 | - [Session Lifecycle](session-lifecycle.md) - Understand how sessions progress through states
36 | - [Authentication](authentication.md) - Learn about all authentication methods
37 | - [Command Execution](command-execution.md) - Execute commands with PTY support
38 | - [File Transfer with SCP](scp.md) - Upload and download files
39 | - [Error Handling](error-handling.md) - Handle SSH errors properly
40 | - [Algorithm and Method Preferences](algorithm-preferences.md) - Configure security settings
41 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshCommandResult.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Core;
2 |
3 | ///
4 | /// Represents the result of an SSH command execution.
5 | ///
6 | public readonly struct SshCommandResult
7 | {
8 | ///
9 | /// Gets a result indicating an unsuccessful command execution.
10 | ///
11 | public static SshCommandResult Unsuccessful => new() { Successful = false };
12 |
13 | ///
14 | /// Gets a value indicating whether the command execution was successful.
15 | ///
16 | public bool Successful { get; init; }
17 |
18 | ///
19 | /// Gets the standard output (stdout) from the command execution.
20 | ///
21 | public string? Stdout { get; init; }
22 |
23 | ///
24 | /// Gets the standard error (stderr) from the command execution.
25 | ///
26 | public string? Stderr { get; init; }
27 |
28 | ///
29 | /// Gets the exit code from the command execution.
30 | /// A value of 0 typically indicates success, while non-zero values indicate an error.
31 | /// This value may be null if the exit code could not be retrieved.
32 | ///
33 | public int? ExitCode { get; init; }
34 |
35 | ///
36 | /// Gets the exit signal from the command execution if the command was terminated by a signal.
37 | /// This value may be null if no signal was received or if the signal could not be retrieved.
38 | /// Common signals include "TERM", "KILL", "HUP", etc.
39 | ///
40 | public string? ExitSignal { get; init; }
41 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshHostKeyType.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 |
3 | namespace NullOpsDevs.LibSsh.Core;
4 |
5 | ///
6 | /// Specifies the type of SSH host key algorithm used by the server.
7 | ///
8 | public enum SshHostKeyType
9 | {
10 | ///
11 | /// Unknown or unsupported host key type.
12 | ///
13 | Unknown = LibSshNative.LIBSSH2_HOSTKEY_TYPE_UNKNOWN,
14 |
15 | ///
16 | /// RSA (Rivest-Shamir-Adleman) public key algorithm.
17 | ///
18 | Rsa = LibSshNative.LIBSSH2_HOSTKEY_TYPE_RSA,
19 |
20 | ///
21 | /// DSS (Digital Signature Standard) / DSA public key algorithm.
22 | ///
23 | [Obsolete("Deprecated, see https://libssh2.org/libssh2_session_hostkey.html")]
24 | Dss = LibSshNative.LIBSSH2_HOSTKEY_TYPE_DSS,
25 |
26 | ///
27 | /// ECDSA (Elliptic Curve Digital Signature Algorithm) with NIST P-256 curve.
28 | ///
29 | Ecdsa256 = LibSshNative.LIBSSH2_HOSTKEY_TYPE_ECDSA_256,
30 |
31 | ///
32 | /// ECDSA (Elliptic Curve Digital Signature Algorithm) with NIST P-384 curve.
33 | ///
34 | Ecdsa384 = LibSshNative.LIBSSH2_HOSTKEY_TYPE_ECDSA_384,
35 |
36 | ///
37 | /// ECDSA (Elliptic Curve Digital Signature Algorithm) with NIST P-521 curve.
38 | ///
39 | Ecdsa521 = LibSshNative.LIBSSH2_HOSTKEY_TYPE_ECDSA_521,
40 |
41 | ///
42 | /// Ed25519 elliptic curve signature algorithm (recommended for new deployments).
43 | ///
44 | Ed25519 = LibSshNative.LIBSSH2_HOSTKEY_TYPE_ED25519
45 | }
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'src/**'
9 | - '**.csproj'
10 | - '**.sln'
11 | - '.github/workflows/test.yml'
12 | workflow_dispatch:
13 |
14 | jobs:
15 | test:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v4
21 |
22 | - name: Setup .NET
23 | uses: actions/setup-dotnet@v4
24 | with:
25 | dotnet-version: '10.0.x'
26 | dotnet-quality: 'preview'
27 |
28 | - name: Set up Docker Compose
29 | uses: docker/setup-compose-action@v1
30 |
31 | - name: Start Docker Compose services
32 | working-directory: ./src/NullOpsDevs.LibSsh.Test
33 | run: docker compose up --build -d
34 |
35 | - name: Setup SSH Agent
36 | run: |
37 | eval $(ssh-agent -s)
38 | echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
39 | echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV
40 | cp src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa /tmp/id_rsa
41 | chmod 600 /tmp/id_rsa
42 | ssh-add /tmp/id_rsa
43 |
44 | - name: Build test project (Release)
45 | run: dotnet build -c Release --self-contained -r linux-x64 ./src/NullOpsDevs.LibSsh.Test/NullOpsDevs.LibSsh.Test.csproj
46 |
47 | - name: Run tests
48 | working-directory: ./src/NullOpsDevs.LibSsh.Test/bin/Release/net9.0/linux-x64/
49 | run: ./NullOpsDevs.LibSsh.Test
50 |
51 | - name: Cleanup Docker Compose
52 | if: always()
53 | working-directory: ./src/NullOpsDevs.LibSsh.Test
54 | run: docker compose down -v
55 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Interop/LibSsh2.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NullOpsDevs.LibSsh.Generated;
3 |
4 | namespace NullOpsDevs.LibSsh.Interop;
5 |
6 | ///
7 | /// Provides global utilities and configuration for the libssh2 library.
8 | ///
9 | [PublicAPI]
10 | public static class LibSsh2
11 | {
12 | ///
13 | /// Gets the version of the libssh2 library.
14 | ///
15 | public static string Version => $"{LibSshNative.LIBSSH2_VERSION_MAJOR}.{LibSshNative.LIBSSH2_VERSION_MINOR}.{LibSshNative.LIBSSH2_VERSION_PATCH}";
16 |
17 | ///
18 | /// Gets the global lock used for thread-safe library initialization.
19 | ///
20 | #if NET9_0_OR_GREATER
21 | public static readonly Lock GlobalLock = new();
22 | #else
23 | public static readonly object GlobalLock = new();
24 | #endif
25 |
26 | ///
27 | /// Performs global deinitialization of the libssh2 library and frees all internal memory.
28 | ///
29 | ///
30 | /// WARNING: Only call this method at application exit. Never interact with the library again after calling this method.
31 | /// This is a global shutdown operation that frees all internal libssh2 resources including cryptographic library state.
32 | /// After calling this method, the library is in an uninitialized state and cannot be used again without restarting the application.
33 | /// Do not call this while any instances are still in use or may be created in the future.
34 | ///
35 | public static void Exit()
36 | {
37 | LibSshNative.libssh2_exit();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Terminal/TerminalType.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 |
3 | namespace NullOpsDevs.LibSsh.Terminal;
4 |
5 | ///
6 | /// Terminal type for PTY (pseudo-terminal) requests.
7 | ///
8 | [PublicAPI]
9 | public enum TerminalType
10 | {
11 | ///
12 | /// Standard xterm terminal emulator.
13 | ///
14 | Xterm,
15 |
16 | ///
17 | /// xterm with color support.
18 | ///
19 | XtermColor,
20 |
21 | ///
22 | /// xterm with 256 color support.
23 | ///
24 | Xterm256Color,
25 |
26 | ///
27 | /// DEC VT100 terminal.
28 | ///
29 | VT100,
30 |
31 | ///
32 | /// DEC VT220 terminal.
33 | ///
34 | VT220,
35 |
36 | ///
37 | /// Linux console terminal.
38 | ///
39 | Linux,
40 |
41 | ///
42 | /// GNU Screen terminal multiplexer.
43 | ///
44 | Screen
45 | }
46 |
47 | ///
48 | /// Extension methods for .
49 | ///
50 | public static class TerminalTypeExtensions
51 | {
52 | ///
53 | /// Converts the terminal type to the string value expected by libssh2.
54 | ///
55 | public static string ToLibSsh2String(this TerminalType terminalType) => terminalType switch
56 | {
57 | TerminalType.Xterm => "xterm",
58 | TerminalType.XtermColor => "xterm-color",
59 | TerminalType.Xterm256Color => "xterm-256color",
60 | TerminalType.VT100 => "vt100",
61 | TerminalType.VT220 => "vt220",
62 | TerminalType.Linux => "linux",
63 | TerminalType.Screen => "screen",
64 | _ => "xterm"
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN OPENSSH PRIVATE KEY-----
2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
3 | NhAAAAAwEAAQAAAQEArqcQ/u98Sh3yNme3Z9IIhzsStbqp3R4ahNaZ3F/o3LKJcpg9sieH
4 | dRby40GTZ1asn42F4OmWRlnSvB4/uksBb29/CmtZhMztu7HD0kLskQilpFivbReUcotUEZ
5 | SGQBUrwb+851HM7c62NGGB488NtGJq8vQ/eZmlitP9gZideirUeV9L4FbWQyUtVgFIEbSc
6 | iRKUzBuZ2UbpEKf6z2B8M0EDh4/MMly7KeqtoYdQvQsT1tnJIjMwi6Fi0O3CoB8+X7CUe0
7 | AMLtioncUP6Yu0PEAzA4f0sPwaeEpy9ByIIVSxv7NGNTzdobrjHfGIhW5dLDsWqntpD9xN
8 | bzSEsy9eDQAAA8DvmN2j75jdowAAAAdzc2gtcnNhAAABAQCupxD+73xKHfI2Z7dn0giHOx
9 | K1uqndHhqE1pncX+jcsolymD2yJ4d1FvLjQZNnVqyfjYXg6ZZGWdK8Hj+6SwFvb38Ka1mE
10 | zO27scPSQuyRCKWkWK9tF5Ryi1QRlIZAFSvBv7znUcztzrY0YYHjzw20Ymry9D95maWK0/
11 | 2BmJ16KtR5X0vgVtZDJS1WAUgRtJyJEpTMG5nZRukQp/rPYHwzQQOHj8wyXLsp6q2hh1C9
12 | CxPW2ckiMzCLoWLQ7cKgHz5fsJR7QAwu2KidxQ/pi7Q8QDMDh/Sw/Bp4SnL0HIghVLG/s0
13 | Y1PN2huuMd8YiFbl0sOxaqe2kP3E1vNISzL14NAAAAAwEAAQAAAQA9yhl3OB8O0b1phhQb
14 | BPHDdiDObnW+JvJW4N2aW8w0mG2MP1REfTutLytLP135B28XG6irw7hIt2qY51LAg9zEIf
15 | weIZCQLThGWUPgVZEAVsDTfhTCUb9RLv3VImjErzjF2SHp7MTFtYY8zep4QD6m/NK9lbAH
16 | Q1aP4SQk/2tnVr10MtIQCe+gaWj75E6KZZvjcPrCl7qkvBWkb9IxXb+eDjuzUloQ9QDiuk
17 | kBBMow0rya4LlvzGF19snS+LUIRK8ay4yQJ4yk4QuFPZzDN4D2QCwIIS4sufGECBNsFAkC
18 | Lm2kBlpn6+ixQ8Ve8GPVlaXHsQoKZgv9y5FaL25V/1w5AAAAgDmGPNJVtfEWCflXGnkFIp
19 | gfBD2ptqL10iqI3dGLy+kIBe3xd2BBafUibERigzyn322OU3M2XnqP98cfOOjUdwAr1UE5
20 | 1qD9up0d95C9iSpZfn2KtSggTdeIuCFa5RR7GEMFRuG6LHYvjCNFv/l5OYpyQ5D56hzul4
21 | KXwfy/X3UVAAAAgQDpI2ylyETjs9WQyCSmoi3as91J3fIyA74KXvOBCjlX9Z2TmhYb2Bwp
22 | 1qsaBfN+PaIOvdFHPakIXBFFCeEIpuCLmP5+KAHxtbHjscNSMkfZxYLQ5qR66Xxaj96sbk
23 | qog/CDpM4PNPDjpEhLICkZOnwjJhbIJXup8lh6f857m4Y2qwAAAIEAv8dyood/UyTXP2WM
24 | IskzcAGgzyRHsFjk55wR/LoibVt+zi4FdFcrxJTGkY/PUhfsIPhK58ZPoAAgoAsq71uaQi
25 | mHXXSbkaWKT9D81rnYgmcBMeG9IzIWxIuivoygjT7XV1mK+1qWebN3gtQjbWTavPUKhH6u
26 | uOlzI6BmzXn/HicAAAAIdGVzdC1rZXkBAgM=
27 | -----END OPENSSH PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa_protected:
--------------------------------------------------------------------------------
1 | -----BEGIN OPENSSH PRIVATE KEY-----
2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBTsGw+fr
3 | OrxoifS9pI1w2YAAAAGAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDGg0eOfJfD
4 | l07zo6kBwbFMe9R0EbCCVTSj+P7QiFtq9KCixrHLO6AWRJeLa6a4UQtEFt7M/qMkqk0ZcK
5 | Mbjq6SK4ZqqjKFogpwUT7lUmJGIfs9/JIgW6wG57PQlLqW8Ie518mnh2Pimkjmd5BoDu7O
6 | S+8VJg/twItYZL+dh1oDA+zZzkziqmmIB5b45IED2rUjfTyPO9r8t1OepE3Mx2wACcuwjz
7 | 4+VqSh0w9Zwaw045l417RjX4hwUluX+AN4o0JLFM1mnbciwi/pKlAi+7SJm6AFOfEXHWi/
8 | 37ElSSe9NHqt3sWl05Qq5+otS/C/9a8F0vlMu0wrlDsXqq+8xKF7AAAD0EZXqvD7VPlN14
9 | 1klPJuhBTY/K31qFTZ7298zbRS6ZAEebjcL2rJf9Am8mDY+LFovOAkbxqxW5ldn/il0fqi
10 | imfmfiGUby6gocVMwreVMyj7x2WwxqV6IjLhm8uxqWBWGweRjLIVFPCz620+QvbjDppGO0
11 | FSdlULFOXgZF8FH9kOmKgTfzspeiXU1twWITzzI4h9eHOiHe+eIuKN6qLF9ArT30YiC61t
12 | 9zj64X4LNTHJDPgVjta7p8bdbZRHSyy24a+M3afvZFKras8NbL1IMyU13NkBjz6B62w31Q
13 | 8EOd1w8lSefvZjoZLjLdJStKkrOAUr7N37NsYDUXlFP/m3qBik2/F1FetcFk1BpvXOIGY1
14 | 4sIJBrYDTImJP4nfdx+PbIDc8S82qLDO4eY2jZ+/4em5j5U1xFHrH6LaVSpTZA0PuA4uCf
15 | jkftVsu0l1J0WOCuPlwveEjrnqVQyd8BhCkvQkyrsu84je7X7CnwfAvrdFM/7Zxe1q1Y1M
16 | XiKFs36bLeafK9gd629BRRqsNPOEWpNiiLTElfyjbCm68Jg5uWYbFh9FTcJQwweeu6GlZH
17 | 3T07NnIXqXwDMpJSHZcl1UGyIJWTTb+eJzrr1cmrU9yR/Yf/LY/d3QxjSliHDGX2dZ2/mD
18 | wX9ZQG4TmzCZhLt7U2Sz+QZs1psFBY3V+FZunECXisiBJ00uy671KUAoBNl6VXpO0yBOXH
19 | WX6TL1tQ0KXIFLvcc71ZTnNbNdrn1o7qYEaxxKOu9H8WjBYP2iNKwOsiB9B6M3oTBouRVl
20 | oaEORzq5QOakgI46o0a9o2NpmJr+ys9pJxTzGnC1kiZCVRAoSiydTHCqBk64oepvWzNG3/
21 | TGuDJ8nyE8cCvRK7MuurF1scRLogwO4ujEQgCHCJYxWctU1mXUVEOr6ceDd6rqrSBockdz
22 | jFXGoTTd/SjaQQcwU6rBZLXmV+O9Q9ufpHLxbAZHiE3exdHIcc3eyqrE5vYN3XyIPACHik
23 | 5W0xwLAjBim25uoqy+Uq1UqfcU+U62LhgBJ2hkhJIWd5bNVcIhH2B6jopiPlc0h571TQCk
24 | SNbBkPiozW9B/o/0c5CTJ0Ey9D2NWFVvBVvappwU3pGs1Xfxm3G57Lp8CwYdWT203v+tC6
25 | WaW0qfUp4F9CXsOToN+tGcFVZeyBuWvR5EYWd2LLCKyw4RxTaUXAKVeiHrn1gVCmtBUZoW
26 | mZj2GYs9anhI6LmVa3tsJ3act/JDfN7H+PeWp/d6N+OqL7/B+fiAGRs686wYRUQE8B3D/s
27 | 52nwj+DTQMG7vgr9T1FvA9oxl8H00=
28 | -----END OPENSSH PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Exceptions/SshException.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using NullOpsDevs.LibSsh.Core;
3 | using NullOpsDevs.LibSsh.Generated;
4 |
5 | namespace NullOpsDevs.LibSsh.Exceptions;
6 |
7 | ///
8 | /// Exception thrown when an SSH operation fails.
9 | ///
10 | /// The error message that explains the reason for the exception.
11 | /// The SSH error code associated with this exception.
12 | /// The exception that is the cause of the current exception, or null if no inner exception is specified.
13 | public class SshException(string message, SshError error, Exception? innerException = null) : Exception(message, innerException)
14 | {
15 | ///
16 | /// Gets the SSH error code associated with this exception.
17 | ///
18 | public SshError Error { get; } = error;
19 |
20 | ///
21 | /// Creates an from the last error that occurred in the specified libssh2 session.
22 | ///
23 | /// Pointer to the libssh2 session.
24 | /// Additional message.
25 | /// An containing the error code and message from the session.
26 | internal static unsafe SshException FromLastSessionError(_LIBSSH2_SESSION* session, string? additionalMessage = null)
27 | {
28 | sbyte* errorMsg = null;
29 | var errorMsgLen = 0;
30 | var errorCode = LibSshNative.libssh2_session_last_error(session, &errorMsg, &errorMsgLen, 0);
31 |
32 | var errorText = errorMsg != null ?
33 | Marshal.PtrToStringAnsi((IntPtr)errorMsg, errorMsgLen) :
34 | "Unknown error";
35 |
36 | return new SshException($"{(additionalMessage != null ? $"{additionalMessage}: " : "")} [{(SshError)errorCode:G}] {errorText}", (SshError) errorCode);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Extensions/LibSshExtensions.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Core;
2 | using NullOpsDevs.LibSsh.Exceptions;
3 |
4 | namespace NullOpsDevs.LibSsh.Extensions;
5 |
6 | ///
7 | /// Internal extension methods for libssh2 operations.
8 | ///
9 | internal static class LibSshExtensions
10 | {
11 | ///
12 | /// Throws an if the libssh2 return code indicates failure (negative value).
13 | ///
14 | /// The libssh2 function return code.
15 | /// Libssh2 session.
16 | /// Optional custom error message.
17 | /// Optional action to execute before throwing the exception (e.g., cleanup).
18 | /// Thrown when the return code is negative (indicates error).
19 | internal static unsafe void ThrowIfNotSuccessful(this int @return, SshSession session,
20 | string? message = null, Action? also = null)
21 | {
22 | if (@return >= 0)
23 | return;
24 |
25 | also?.Invoke();
26 |
27 | if (session.SessionPtr != null)
28 | {
29 | if (message != null)
30 | throw SshException.FromLastSessionError(session.SessionPtr, message);
31 |
32 | throw SshException.FromLastSessionError(session.SessionPtr);
33 | }
34 |
35 | throw new SshException(message ?? "Unknown error", (SshError)@return);
36 | }
37 |
38 | ///
39 | /// Converts a standard exception to an with the InnerException error code.
40 | ///
41 | /// The exception to convert.
42 | /// An SshException wrapping the original exception.
43 | internal static SshException AsSshException(this Exception exception)
44 | {
45 | return new SshException(exception.Message, SshError.InnerException, exception);
46 | }
47 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/TestConfig.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Test;
2 |
3 | ///
4 | /// Configuration constants for SSH testing
5 | ///
6 | public static class TestConfig
7 | {
8 | // SSH Server connection
9 | public const string Host = "127.0.0.1";
10 | public const int Port = 2222;
11 |
12 | // Test credentials
13 | public const string Username = "user";
14 | public const string Password = "12345";
15 |
16 | // SSH key paths (relative to test project directory)
17 | public const string PrivateKeyPath = "docker/test-keys/id_rsa";
18 | public const string PublicKeyPath = "docker/test-keys/id_rsa.pub";
19 | public const string PrivateKeyProtectedPath = "docker/test-keys/id_rsa_protected";
20 | public const string PublicKeyProtectedPath = "docker/test-keys/id_rsa_protected.pub";
21 | public const string KeyPassphrase = "testpass";
22 |
23 | // Test file paths (remote paths on SSH server)
24 | public const string RemoteSmallFile = "/test-files/small.txt";
25 | public const string RemoteLargeFile = "/test-files/large.dat";
26 |
27 | ///
28 | /// Gets the absolute path to a test key file
29 | ///
30 | public static string GetKeyPath(string relativePath)
31 | {
32 | relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar);
33 |
34 | // Get the directory where the test assembly is located
35 | var assemblyDir = AppContext.BaseDirectory;
36 |
37 | // First try the output directory (files are copied via .csproj)
38 | var outputPath = Path.Combine(assemblyDir, relativePath);
39 | if (File.Exists(outputPath))
40 | return outputPath;
41 |
42 | // Fallback: Navigate up to the project directory (from bin/Release/net9.0 or bin/Debug/net9.0)
43 | var projectDir = Path.Combine(assemblyDir, "..", "..", "..");
44 | var normalizedProjectDir = Path.GetFullPath(projectDir);
45 |
46 | return Path.Combine(normalizedProjectDir, relativePath);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Platform/StatLinuxX64.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Platform;
4 |
5 | ///
6 | /// Platform-specific file stat structure for Linux x64.
7 | ///
8 | [StructLayout(LayoutKind.Sequential)]
9 | internal unsafe struct StatLinuxX64
10 | {
11 | /// Device ID containing the file.
12 | public ulong st_dev;
13 |
14 | /// Inode number.
15 | public ulong st_ino;
16 |
17 | /// Number of hard links.
18 | public ulong st_nlink;
19 |
20 | /// File mode (type and permissions).
21 | public uint st_mode;
22 |
23 | /// User ID of the file owner.
24 | public uint st_uid;
25 |
26 | /// Group ID of the file owner.
27 | public uint st_gid;
28 |
29 | /// Padding for alignment.
30 | public uint __pad0;
31 |
32 | /// Device ID if this is a special file.
33 | public ulong st_rdev;
34 |
35 | /// Total size in bytes.
36 | public long st_size;
37 |
38 | /// Optimal block size for I/O.
39 | public long st_blksize;
40 |
41 | /// Number of 512-byte blocks allocated.
42 | public long st_blocks;
43 |
44 | /// Access time in seconds since epoch.
45 | public long st_atime;
46 |
47 | /// Access time nanoseconds component.
48 | public long st_atime_nsec;
49 |
50 | /// Modification time in seconds since epoch.
51 | public long st_mtime;
52 |
53 | /// Modification time nanoseconds component.
54 | public long st_mtime_nsec;
55 |
56 | /// Status change time in seconds since epoch.
57 | public long st_ctime;
58 |
59 | /// Status change time nanoseconds component.
60 | public long st_ctime_nsec;
61 |
62 | /// Reserved for future use.
63 | public fixed long __unused[3];
64 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshPublicKeyFromMemoryCredential.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 | using NullOpsDevs.LibSsh.Interop;
3 |
4 | namespace NullOpsDevs.LibSsh.Credentials;
5 |
6 | ///
7 | /// Represents SSH authentication using public key from memory buffers.
8 | ///
9 | /// The username for authentication.
10 | /// The public key data as a byte array.
11 | /// The private key data as a byte array.
12 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
13 | public class SshPublicKeyFromMemoryCredential(string username, byte[] publicKeyData, byte[] privateKeyData, string? passphrase = null) : SshCredential
14 | {
15 | ///
16 | public override unsafe bool Authenticate(SshSession session)
17 | {
18 | if (string.IsNullOrWhiteSpace(username))
19 | return false;
20 |
21 | if (publicKeyData.Length == 0)
22 | return false;
23 |
24 | if (privateKeyData.Length == 0)
25 | return false;
26 |
27 | using var usernameBuffer = NativeBuffer.Allocate(username);
28 | using var publicKeyBuffer = NativeBuffer.Allocate(publicKeyData);
29 | using var privateKeyBuffer = NativeBuffer.Allocate(privateKeyData);
30 |
31 | using var passphraseBuffer = string.IsNullOrEmpty(passphrase) ?
32 | NativeBuffer.Allocate(0) :
33 | NativeBuffer.Allocate(passphrase);
34 |
35 | var authResult = LibSshNative.libssh2_userauth_publickey_frommemory(
36 | session.SessionPtr,
37 | usernameBuffer.AsPointer(),
38 | (nuint)usernameBuffer.Length - 1,
39 | publicKeyBuffer.AsPointer(),
40 | (nuint)publicKeyBuffer.Length,
41 | privateKeyBuffer.AsPointer(),
42 | (nuint)privateKeyBuffer.Length,
43 | string.IsNullOrEmpty(passphrase) ? null : passphraseBuffer.AsPointer());
44 |
45 | return authResult >= 0;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | # Install OpenSSH server and utilities needed for tests
4 | RUN apk add --no-cache openssh-server openssh-keygen openssh-client ncurses coreutils
5 |
6 | # Create user
7 | RUN adduser -D -s /bin/sh user && \
8 | echo "user:12345" | chpasswd
9 |
10 | # Setup SSH directories
11 | RUN mkdir -p /run/sshd /home/user/.ssh && \
12 | chmod 700 /home/user/.ssh && \
13 | chown -R user:user /home/user/.ssh
14 |
15 | # Generate host keys
16 | RUN ssh-keygen -A
17 |
18 | # Create sshd_config with high limits
19 | RUN echo 'Port 2222' > /etc/ssh/sshd_config && \
20 | echo 'PermitRootLogin no' >> /etc/ssh/sshd_config && \
21 | echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
22 | echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config && \
23 | echo 'AuthorizedKeysFile /home/user/.ssh/authorized_keys' >> /etc/ssh/sshd_config && \
24 | echo 'MaxStartups 1000:30:2000' >> /etc/ssh/sshd_config && \
25 | echo 'MaxSessions 1000' >> /etc/ssh/sshd_config && \
26 | echo 'LoginGraceTime 30' >> /etc/ssh/sshd_config && \
27 | echo 'ClientAliveInterval 30' >> /etc/ssh/sshd_config && \
28 | echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config && \
29 | echo 'UseDNS no' >> /etc/ssh/sshd_config && \
30 | echo 'MaxAuthTries 10' >> /etc/ssh/sshd_config
31 |
32 | # Create test files directory and generate test files
33 | RUN mkdir -p /test-files && \
34 | echo "Small test file content" > /test-files/small.txt && \
35 | dd if=/dev/urandom of=/test-files/medium.bin bs=1024 count=1024 2>/dev/null && \
36 | dd if=/dev/urandom of=/test-files/large.dat bs=1024 count=10240 2>/dev/null && \
37 | chmod 644 /test-files/* && \
38 | chmod 755 /test-files
39 |
40 | # Setup /tmp directory with proper permissions for the user
41 | RUN mkdir -p /tmp && \
42 | chmod 1777 /tmp && \
43 | chown user:user /home/user && \
44 | chmod 755 /home/user
45 |
46 | # Copy SSH keys
47 | COPY test-keys/*.pub /tmp/keys/
48 | RUN cat /tmp/keys/*.pub > /home/user/.ssh/authorized_keys 2>/dev/null || true && \
49 | chmod 600 /home/user/.ssh/authorized_keys && \
50 | chown user:user /home/user/.ssh/authorized_keys && \
51 | rm -rf /tmp/keys
52 |
53 | EXPOSE 2222
54 |
55 | # Start sshd in foreground
56 | CMD ["/usr/sbin/sshd", "-D", "-e"]
57 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Platform/StatDarwin.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Platform;
4 |
5 | ///
6 | /// Platform-specific file stat structure for macOS (Darwin).
7 | ///
8 | [StructLayout(LayoutKind.Sequential)]
9 | internal unsafe struct StatDarwin
10 | {
11 | /// Device ID containing the file.
12 | public int st_dev;
13 |
14 | /// File mode (type and permissions).
15 | public ushort st_mode;
16 |
17 | /// Number of hard links.
18 | public ushort st_nlink;
19 |
20 | /// Inode number.
21 | public ulong st_ino;
22 |
23 | /// User ID of the file owner.
24 | public uint st_uid;
25 |
26 | /// Group ID of the file owner.
27 | public uint st_gid;
28 |
29 | /// Device ID if this is a special file.
30 | public int st_rdev;
31 |
32 | /// Access time in seconds since epoch.
33 | public long st_atimespec_sec;
34 |
35 | /// Access time nanoseconds component.
36 | public long st_atimespec_nsec;
37 |
38 | /// Modification time in seconds since epoch.
39 | public long st_mtimespec_sec;
40 |
41 | /// Modification time nanoseconds component.
42 | public long st_mtimespec_nsec;
43 |
44 | /// Status change time in seconds since epoch.
45 | public long st_ctimespec_sec;
46 |
47 | /// Status change time nanoseconds component.
48 | public long st_ctimespec_nsec;
49 |
50 | /// File creation time in seconds since epoch.
51 | public long st_birthtimespec_sec;
52 |
53 | /// File creation time nanoseconds component.
54 | public long st_birthtimespec_nsec;
55 |
56 | /// Total size in bytes.
57 | public long st_size;
58 |
59 | /// Number of 512-byte blocks allocated.
60 | public long st_blocks;
61 |
62 | /// Optimal block size for I/O.
63 | public int st_blksize;
64 |
65 | /// User-defined flags for file.
66 | public uint st_flags;
67 |
68 | /// File generation number.
69 | public uint st_gen;
70 |
71 | /// Reserved for future use.
72 | public int st_lspare;
73 |
74 | /// Reserved for future use.
75 | public fixed long st_qspare[2];
76 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshMethod.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NullOpsDevs.LibSsh.Generated;
3 |
4 | namespace NullOpsDevs.LibSsh.Core;
5 |
6 | ///
7 | /// Specifies the SSH protocol method types that can be negotiated between client and server.
8 | /// CS = Client-to-Server, SC = Server-to-Client.
9 | ///
10 | [PublicAPI]
11 | public enum SshMethod
12 | {
13 | ///
14 | /// Key exchange method (e.g., diffie-hellman-group14-sha256, curve25519-sha256).
15 | ///
16 | Kex = LibSshNative.LIBSSH2_METHOD_KEX,
17 |
18 | ///
19 | /// Host key algorithm (e.g., ssh-ed25519, rsa-sha2-512, ecdsa-sha2-nistp256).
20 | ///
21 | HostKey = LibSshNative.LIBSSH2_METHOD_HOSTKEY,
22 |
23 | ///
24 | /// Encryption cipher for client-to-server traffic (e.g., aes256-ctr, chacha20-poly1305@openssh.com).
25 | ///
26 | CryptCs = LibSshNative.LIBSSH2_METHOD_CRYPT_CS,
27 |
28 | ///
29 | /// Encryption cipher for server-to-client traffic (e.g., aes256-ctr, chacha20-poly1305@openssh.com).
30 | ///
31 | CryptSc = LibSshNative.LIBSSH2_METHOD_CRYPT_SC,
32 |
33 | ///
34 | /// Message authentication code (MAC) algorithm for client-to-server traffic (e.g., hmac-sha2-256).
35 | ///
36 | MacCs = LibSshNative.LIBSSH2_METHOD_MAC_CS,
37 |
38 | ///
39 | /// Message authentication code (MAC) algorithm for server-to-client traffic (e.g., hmac-sha2-256).
40 | ///
41 | MacSc = LibSshNative.LIBSSH2_METHOD_MAC_SC,
42 |
43 | ///
44 | /// Compression method for client-to-server traffic (e.g., none, zlib@openssh.com).
45 | ///
46 | CompCs = LibSshNative.LIBSSH2_METHOD_COMP_CS,
47 |
48 | ///
49 | /// Compression method for server-to-client traffic (e.g., none, zlib@openssh.com).
50 | ///
51 | CompSc = LibSshNative.LIBSSH2_METHOD_COMP_SC,
52 |
53 | ///
54 | /// Language preference for client-to-server communication (rarely used).
55 | ///
56 | LangCs = LibSshNative.LIBSSH2_METHOD_LANG_CS,
57 |
58 | ///
59 | /// Language preference for server-to-client communication (rarely used).
60 | ///
61 | LangSc = LibSshNative.LIBSSH2_METHOD_LANG_SC,
62 |
63 | ///
64 | /// Signature algorithm used for authentication (e.g., rsa-sha2-512, rsa-sha2-256).
65 | ///
66 | SignAlgo = LibSshNative.LIBSSH2_METHOD_SIGN_ALGO
67 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Platform/StatFreebsd.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace NullOpsDevs.LibSsh.Platform;
4 |
5 | ///
6 | /// Platform-specific file stat structure for FreeBSD.
7 | ///
8 | [StructLayout(LayoutKind.Sequential)]
9 | internal unsafe struct StatFreebsd
10 | {
11 | /// Device ID containing the file.
12 | public ulong st_dev;
13 |
14 | /// Inode number.
15 | public ulong st_ino;
16 |
17 | /// Number of hard links.
18 | public ulong st_nlink;
19 |
20 | /// File mode (type and permissions).
21 | public ushort st_mode;
22 |
23 | /// Padding for alignment.
24 | public short st_padding0;
25 |
26 | /// User ID of the file owner.
27 | public uint st_uid;
28 |
29 | /// Group ID of the file owner.
30 | public uint st_gid;
31 |
32 | /// Padding for alignment.
33 | public int st_padding1;
34 |
35 | /// Device ID if this is a special file.
36 | public ulong st_rdev;
37 |
38 | /// Access time in seconds since epoch.
39 | public long st_atim_sec;
40 |
41 | /// Access time nanoseconds component.
42 | public long st_atim_nsec;
43 |
44 | /// Modification time in seconds since epoch.
45 | public long st_mtim_sec;
46 |
47 | /// Modification time nanoseconds component.
48 | public long st_mtim_nsec;
49 |
50 | /// Status change time in seconds since epoch.
51 | public long st_ctim_sec;
52 |
53 | /// Status change time nanoseconds component.
54 | public long st_ctim_nsec;
55 |
56 | /// File creation time in seconds since epoch.
57 | public long st_birthtim_sec;
58 |
59 | /// File creation time nanoseconds component.
60 | public long st_birthtim_nsec;
61 |
62 | /// Total size in bytes.
63 | public long st_size;
64 |
65 | /// Number of 512-byte blocks allocated.
66 | public long st_blocks;
67 |
68 | /// Optimal block size for I/O.
69 | public int st_blksize;
70 |
71 | /// User-defined flags for file.
72 | public uint st_flags;
73 |
74 | /// File generation number.
75 | public ulong st_gen;
76 |
77 | /// Reserved for future use.
78 | public fixed long st_spare[10];
79 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshPublicKeyCredential.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 | using NullOpsDevs.LibSsh.Interop;
3 |
4 | namespace NullOpsDevs.LibSsh.Credentials;
5 |
6 | ///
7 | /// Represents SSH authentication using public key from file paths.
8 | ///
9 | /// The username for authentication.
10 | /// The path to the public key file.
11 | /// The path to the private key file.
12 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
13 | public class SshPublicKeyCredential(string username, string publicKeyPath, string privateKeyPath, string? passphrase = null) : SshCredential
14 | {
15 | ///
16 | public override unsafe bool Authenticate(SshSession session)
17 | {
18 | if (string.IsNullOrWhiteSpace(username))
19 | return false;
20 |
21 | if (string.IsNullOrWhiteSpace(privateKeyPath))
22 | return false;
23 |
24 | using var usernameBuffer = NativeBuffer.Allocate(username);
25 | using var privateKeyPathBuffer = NativeBuffer.Allocate(privateKeyPath);
26 | using var passphraseBuffer = string.IsNullOrEmpty(passphrase)
27 | ? NativeBuffer.Allocate(0)
28 | : NativeBuffer.Allocate(passphrase);
29 |
30 | // Try without public key file first (let libssh2 extract it from private key)
31 | var authResult = LibSshNative.libssh2_userauth_publickey_fromfile_ex(
32 | session.SessionPtr,
33 | usernameBuffer.AsPointer(),
34 | (uint)usernameBuffer.Length - 1,
35 | null,
36 | privateKeyPathBuffer.AsPointer(),
37 | string.IsNullOrEmpty(passphrase) ? null : passphraseBuffer.AsPointer());
38 |
39 | // If that fails and we have a public key path, try with explicit public key
40 | if (authResult < 0 && !string.IsNullOrWhiteSpace(publicKeyPath))
41 | {
42 | using var publicKeyPathBuffer = NativeBuffer.Allocate(publicKeyPath);
43 | authResult = LibSshNative.libssh2_userauth_publickey_fromfile_ex(
44 | session.SessionPtr,
45 | usernameBuffer.AsPointer(),
46 | (uint)usernameBuffer.Length - 1,
47 | publicKeyPathBuffer.AsPointer(),
48 | privateKeyPathBuffer.AsPointer(),
49 | string.IsNullOrEmpty(passphrase) ? null : passphraseBuffer.AsPointer());
50 | }
51 |
52 | return authResult >= 0;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/test-dotnet-versions.yml:
--------------------------------------------------------------------------------
1 | name: Test .NET Versions
2 |
3 | on:
4 | push:
5 | branches:
6 | - matrix-test
7 | paths:
8 | - 'src/**'
9 | - '**.csproj'
10 | - '**.sln'
11 | - '.github/workflows/test-dotnet-versions.yml'
12 | workflow_dispatch:
13 |
14 | jobs:
15 | test-dotnet-versions:
16 | name: Test on ${{ matrix.target-framework }}
17 | runs-on: ubuntu-latest
18 |
19 | strategy:
20 | fail-fast: false
21 | matrix:
22 | include:
23 | - target-framework: net6.0
24 | dotnet-version: '6.0.x'
25 | quality: 'ga'
26 | - target-framework: net7.0
27 | dotnet-version: '7.0.x'
28 | quality: 'ga'
29 | - target-framework: net8.0
30 | dotnet-version: '8.0.x'
31 | quality: 'ga'
32 | - target-framework: net9.0
33 | dotnet-version: '9.0.x'
34 | quality: 'ga'
35 | - target-framework: net10.0
36 | dotnet-version: '10.0.x'
37 | quality: 'preview'
38 |
39 | steps:
40 | - name: Checkout code
41 | uses: actions/checkout@v4
42 |
43 | - name: Setup .NET ${{ matrix.dotnet-version }}
44 | uses: actions/setup-dotnet@v4
45 | with:
46 | dotnet-version: ${{ matrix.dotnet-version }}
47 | dotnet-quality: ${{ matrix.quality }}
48 |
49 | - name: Set up Docker Compose
50 | uses: docker/setup-compose-action@v1
51 |
52 | - name: Start Docker Compose services
53 | working-directory: ./src/NullOpsDevs.LibSsh.Test
54 | run: docker compose up --build -d
55 |
56 | - name: Wait for SSH server to be ready
57 | run: |
58 | echo "Waiting for SSH server to start..."
59 | sleep 5
60 |
61 | - name: Setup SSH Agent
62 | run: |
63 | eval $(ssh-agent -s)
64 | echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
65 | echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV
66 | cp src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa /tmp/id_rsa
67 | chmod 600 /tmp/id_rsa
68 | ssh-add /tmp/id_rsa
69 |
70 | - name: Build test project for ${{ matrix.target-framework }}
71 | run: dotnet build -c Release --self-contained -r linux-x64 -f ${{ matrix.target-framework }} ./src/NullOpsDevs.LibSsh.Test/NullOpsDevs.LibSsh.Test.csproj
72 |
73 | - name: Run tests
74 | working-directory: ./src/NullOpsDevs.LibSsh.Test/bin/Release/${{ matrix.target-framework }}/linux-x64/
75 | run: ./NullOpsDevs.LibSsh.Test
76 |
77 | - name: Cleanup Docker Compose
78 | if: always()
79 | working-directory: ./src/NullOpsDevs.LibSsh.Test
80 | run: docker compose down -v
81 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshAgentCredential.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 | using NullOpsDevs.LibSsh.Interop;
3 |
4 | namespace NullOpsDevs.LibSsh.Credentials;
5 |
6 | ///
7 | /// Represents SSH authentication using the SSH agent (ssh-agent on Unix, pageant on Windows).
8 | ///
9 | /// The username for authentication.
10 | ///
11 | /// This credential type connects to the running SSH agent and attempts to authenticate
12 | /// using the identities available in the agent. The agent manages the private keys,
13 | /// so no key files or passphrases need to be provided.
14 | ///
15 | public class SshAgentCredential(string username) : SshCredential
16 | {
17 | ///
18 | public override unsafe bool Authenticate(SshSession session)
19 | {
20 | if (string.IsNullOrWhiteSpace(username))
21 | return false;
22 |
23 | // Initialize the agent
24 | var agent = LibSshNative.libssh2_agent_init(session.SessionPtr);
25 | if (agent == null)
26 | return false;
27 |
28 | try
29 | {
30 | // Connect to the agent
31 | var connectResult = LibSshNative.libssh2_agent_connect(agent);
32 | if (connectResult != 0)
33 | return false;
34 |
35 | try
36 | {
37 | // Request list of identities from the agent
38 | var listResult = LibSshNative.libssh2_agent_list_identities(agent);
39 | if (listResult != 0)
40 | return false;
41 |
42 | using var usernameBuffer = NativeBuffer.Allocate(username);
43 |
44 | // Iterate through available identities
45 | libssh2_agent_publickey* identity = null;
46 | libssh2_agent_publickey* prevIdentity = null;
47 |
48 | while (true)
49 | {
50 | var getIdentityResult = LibSshNative.libssh2_agent_get_identity(agent, &identity, prevIdentity);
51 |
52 | if (getIdentityResult == 1) // No more identities
53 | break;
54 |
55 | if (getIdentityResult < 0) // Error
56 | return false;
57 |
58 | // Try to authenticate with this identity
59 | var authResult = LibSshNative.libssh2_agent_userauth(
60 | agent,
61 | usernameBuffer.AsPointer(),
62 | identity);
63 |
64 | if (authResult == 0) // Success
65 | return true;
66 |
67 | prevIdentity = identity;
68 | }
69 |
70 | return false; // No identity worked
71 | }
72 | finally
73 | {
74 | LibSshNative.libssh2_agent_disconnect(agent);
75 | }
76 | }
77 | finally
78 | {
79 | LibSshNative.libssh2_agent_free(agent);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/CommandExecutionOptions.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NullOpsDevs.LibSsh.Generated;
3 | using NullOpsDevs.LibSsh.Terminal;
4 |
5 | namespace NullOpsDevs.LibSsh.Core;
6 |
7 | ///
8 | /// Options for SSH command execution.
9 | ///
10 | [PublicAPI]
11 | public class CommandExecutionOptions
12 | {
13 | ///
14 | /// Gets the default command execution options.
15 | ///
16 | public static readonly CommandExecutionOptions Default = new();
17 |
18 | ///
19 | /// Gets or sets the SSH channel window size (flow control buffer).
20 | /// Default is 2MB. This controls how much data the remote server can send before acknowledgment.
21 | ///
22 | public uint WindowSize { get; set; } = LibSshNative.LIBSSH2_CHANNEL_WINDOW_DEFAULT;
23 |
24 | ///
25 | /// Gets or sets the SSH channel packet size.
26 | /// Default is 32KB. This controls the maximum packet size for the channel.
27 | ///
28 | public uint PacketSize { get; set; } = LibSshNative.LIBSSH2_CHANNEL_PACKET_DEFAULT;
29 |
30 | ///
31 | /// Gets or sets whether to request a pseudo-terminal (PTY) for the command.
32 | /// Default is false. Enable this for commands that need terminal features like color output or interactive input.
33 | ///
34 | public bool RequestPty { get; set; }
35 |
36 | ///
37 | /// Gets or sets the terminal type when PTY is requested.
38 | /// Default is Xterm. Only used when is true.
39 | ///
40 | public TerminalType TerminalType { get; set; } = TerminalType.Xterm;
41 |
42 | ///
43 | /// Gets or sets the terminal width in characters (columns).
44 | /// Default is 80. Only used when is true.
45 | ///
46 | public int TerminalWidth { get; set; } = LibSshNative.LIBSSH2_TERM_WIDTH;
47 |
48 | ///
49 | /// Gets or sets the terminal height in characters (rows).
50 | /// Default is 24. Only used when is true.
51 | ///
52 | public int TerminalHeight { get; set; } = LibSshNative.LIBSSH2_TERM_HEIGHT;
53 |
54 | ///
55 | /// Gets or sets the terminal width in pixels.
56 | /// Default is 0. Only used when is true.
57 | ///
58 | public int TerminalWidthPixels { get; set; } = LibSshNative.LIBSSH2_TERM_WIDTH_PX;
59 |
60 | ///
61 | /// Gets or sets the terminal height in pixels.
62 | /// Default is 0. Only used when is true.
63 | ///
64 | public int TerminalHeightPixels { get; set; } = LibSshNative.LIBSSH2_TERM_HEIGHT_PX;
65 |
66 | ///
67 | /// Gets or sets the terminal modes byte array (RFC 4254 encoding).
68 | /// Default is null (uses empty modes). Only used when is true.
69 | /// Use to construct custom terminal modes.
70 | ///
71 | public byte[]? TerminalModes { get; set; }
72 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshHostBasedCredential.cs:
--------------------------------------------------------------------------------
1 | using NullOpsDevs.LibSsh.Generated;
2 | using NullOpsDevs.LibSsh.Interop;
3 |
4 | namespace NullOpsDevs.LibSsh.Credentials;
5 |
6 | ///
7 | /// Represents SSH authentication using host-based authentication.
8 | ///
9 | /// The username for authentication.
10 | /// The path to the public key file.
11 | /// The path to the private key file.
12 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
13 | /// The hostname of the client machine.
14 | /// The local username on the client machine. If null or empty, uses the same value as username.
15 | ///
16 | /// Host-based authentication allows a host to authenticate on behalf of its users.
17 | /// This is typically used in trusted environments where the server trusts certain client hosts.
18 | /// This authentication method is rarely used in modern SSH deployments.
19 | ///
20 | public class SshHostBasedCredential(
21 | string username,
22 | string publicKeyPath,
23 | string privateKeyPath,
24 | string? passphrase,
25 | string hostname,
26 | string? localUsername = null) : SshCredential
27 | {
28 | ///
29 | public override unsafe bool Authenticate(SshSession session)
30 | {
31 | if (string.IsNullOrWhiteSpace(username))
32 | return false;
33 |
34 | if (string.IsNullOrWhiteSpace(publicKeyPath) || string.IsNullOrWhiteSpace(privateKeyPath))
35 | return false;
36 |
37 | if (string.IsNullOrWhiteSpace(hostname))
38 | return false;
39 |
40 | var effectiveLocalUsername = string.IsNullOrWhiteSpace(localUsername) ? username : localUsername;
41 |
42 | using var usernameBuffer = NativeBuffer.Allocate(username);
43 | using var publicKeyPathBuffer = NativeBuffer.Allocate(publicKeyPath);
44 | using var privateKeyPathBuffer = NativeBuffer.Allocate(privateKeyPath);
45 | using var passphraseBuffer = string.IsNullOrEmpty(passphrase)
46 | ? NativeBuffer.Allocate(0)
47 | : NativeBuffer.Allocate(passphrase);
48 | using var hostnameBuffer = NativeBuffer.Allocate(hostname);
49 | using var localUsernameBuffer = NativeBuffer.Allocate(effectiveLocalUsername);
50 |
51 | var authResult = LibSshNative.libssh2_userauth_hostbased_fromfile_ex(
52 | session.SessionPtr,
53 | usernameBuffer.AsPointer(),
54 | (uint)usernameBuffer.Length,
55 | publicKeyPathBuffer.AsPointer(),
56 | privateKeyPathBuffer.AsPointer(),
57 | string.IsNullOrEmpty(passphrase) ? null : passphraseBuffer.AsPointer(),
58 | hostnameBuffer.AsPointer(),
59 | (uint)hostnameBuffer.Length,
60 | localUsernameBuffer.AsPointer(),
61 | (uint)localUsernameBuffer.Length);
62 |
63 | return authResult >= 0;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to NuGet
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 | paths:
8 | - 'src/NullOpsDevs.LibSsh/**'
9 | - 'src/NullOpsDevs.LibSsh.Test/**'
10 | - '**.csproj'
11 | - '**.sln'
12 | workflow_dispatch:
13 |
14 | jobs:
15 | test:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v4
21 |
22 | - name: Setup .NET
23 | uses: actions/setup-dotnet@v4
24 | with:
25 | dotnet-version: '10.0.x'
26 | dotnet-quality: 'preview'
27 |
28 | - name: Set up Docker Compose
29 | uses: docker/setup-compose-action@v1
30 |
31 | - name: Start Docker Compose services
32 | working-directory: ./src/NullOpsDevs.LibSsh.Test
33 | run: docker compose up --build -d
34 |
35 | - name: Setup SSH Agent
36 | run: |
37 | eval $(ssh-agent -s)
38 | echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
39 | echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV
40 | cp src/NullOpsDevs.LibSsh.Test/docker/test-keys/id_rsa /tmp/id_rsa
41 | chmod 600 /tmp/id_rsa
42 | ssh-add /tmp/id_rsa
43 |
44 | - name: Build test project (Release)
45 | run: dotnet build -c Release --self-contained -r linux-x64 ./src/NullOpsDevs.LibSsh.Test/NullOpsDevs.LibSsh.Test.csproj
46 |
47 | - name: Run tests
48 | working-directory: ./src/NullOpsDevs.LibSsh.Test/bin/Release/net9.0/linux-x64/
49 | run: ./NullOpsDevs.LibSsh.Test
50 |
51 | - name: Cleanup Docker Compose
52 | if: always()
53 | working-directory: ./src/NullOpsDevs.LibSsh.Test
54 | run: docker compose down -v
55 |
56 | publish:
57 | needs: test
58 | runs-on: ubuntu-latest
59 | permissions:
60 | contents: write
61 |
62 | steps:
63 | - name: Checkout code
64 | uses: actions/checkout@v4
65 |
66 | - name: Setup .NET
67 | uses: actions/setup-dotnet@v4
68 | with:
69 | dotnet-version: '10.0.x'
70 | dotnet-quality: 'preview'
71 |
72 | - name: Extract version from tag
73 | id: get_version
74 | run: |
75 | TAG_NAME=${GITHUB_REF#refs/tags/}
76 | echo "VERSION=$TAG_NAME" >> $GITHUB_OUTPUT
77 | echo "Building version: $TAG_NAME"
78 |
79 | - name: Build library with version
80 | run: dotnet build -c Release /p:Version=${{ steps.get_version.outputs.VERSION }} ./src/NullOpsDevs.LibSsh/NullOpsDevs.LibSsh.csproj
81 |
82 | - name: Pack NuGet package
83 | run: dotnet pack -c Release --no-build /p:Version=${{ steps.get_version.outputs.VERSION }} ./src/NullOpsDevs.LibSsh/NullOpsDevs.LibSsh.csproj -o ./artifacts
84 |
85 | - name: Publish to NuGet
86 | run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
87 |
88 | - name: Create GitHub Release
89 | uses: softprops/action-gh-release@v2
90 | with:
91 | name: Release ${{ steps.get_version.outputs.VERSION }}
92 | tag_name: ${{ steps.get_version.outputs.VERSION }}
93 | draft: false
94 | prerelease: false
95 | files: ./artifacts/*.nupkg
96 | generate_release_notes: true
97 | env:
98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
99 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 NullOpsDevs.LibSsh, 0x25CBFC4F
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | Parts of this software are licensed under BSD License:
24 |
25 | https://libssh2.org/license.html
26 |
27 | /* Copyright (C) 2004-2007 Sara Golemon
28 | * Copyright (C) 2005,2006 Mikhail Gusarov
29 | * Copyright (C) 2006-2007 The Written Word, Inc.
30 | * Copyright (C) 2007 Eli Fant
31 | * Copyright (C) 2009-2023 Daniel Stenberg
32 | * Copyright (C) 2008, 2009 Simon Josefsson
33 | * Copyright (C) 2000 Markus Friedl
34 | * Copyright (C) 2015 Microsoft Corp.
35 | * All rights reserved.
36 | *
37 | * Redistribution and use in source and binary forms,
38 | * with or without modification, are permitted provided
39 | * that the following conditions are met:
40 | *
41 | * Redistributions of source code must retain the above
42 | * copyright notice, this list of conditions and the
43 | * following disclaimer.
44 | *
45 | * Redistributions in binary form must reproduce the above
46 | * copyright notice, this list of conditions and the following
47 | * disclaimer in the documentation and/or other materials
48 | * provided with the distribution.
49 | *
50 | * Neither the name of the copyright holder nor the names
51 | * of any other contributors may be used to endorse or
52 | * promote products derived from this software without
53 | * specific prior written permission.
54 | *
55 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
56 | * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
57 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
58 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
59 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
60 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
61 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
62 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
63 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
64 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
65 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
66 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
67 | * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
68 | * OF SUCH DAMAGE.
69 | */
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/TestNativePreloader.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 | using NullOpsDevs.LibSsh.Generated;
4 | using Spectre.Console;
5 |
6 | namespace NullOpsDevs.LibSsh.Test;
7 |
8 | public static class NativePreloader
9 | {
10 | private static IntPtr _libraryHandle;
11 | private static string? _libraryPath;
12 |
13 | public static bool Preload()
14 | {
15 | // Get the directory where the test executable is located
16 | var executableDir = AppContext.BaseDirectory;
17 | var nativeDir = Path.Combine(executableDir, "native");
18 |
19 | if (!Directory.Exists(nativeDir))
20 | {
21 | AnsiConsole.MarkupLine($"[red]Folder 'native' was not found at: {nativeDir}[/]");
22 | return false;
23 | }
24 |
25 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
26 | var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
27 | var isMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
28 |
29 | var processArchitecture = RuntimeInformation.ProcessArchitecture;
30 | var isx64 = processArchitecture == Architecture.X64;
31 | var isArm64 = processArchitecture == Architecture.Arm64;
32 |
33 | if (isWindows && isx64)
34 | _libraryPath = Path.Combine(nativeDir, "libssh2-win-x64", "libssh2.dll");
35 | else if (isLinux && isx64)
36 | _libraryPath = Path.Combine(nativeDir, "libssh2-linux-x64", "libssh2.so");
37 | else if (isLinux && isArm64)
38 | _libraryPath = Path.Combine(nativeDir, "libssh2-linux-arm64", "libssh2.so");
39 | else if (isMac && isx64)
40 | _libraryPath = Path.Combine(nativeDir, "libssh2-osx-x64", "libssh2.dylib");
41 | else if (isMac && isArm64)
42 | _libraryPath = Path.Combine(nativeDir, "libssh2-osx-arm64", "libssh2.dylib");
43 |
44 | if (_libraryPath == null)
45 | {
46 | AnsiConsole.MarkupLine("[red]Unsupported platform/architecture combination.[/]");
47 | return false;
48 | }
49 |
50 | if (!File.Exists(_libraryPath))
51 | {
52 | AnsiConsole.MarkupLine($"[red]Native library not found at: {_libraryPath}[/]");
53 | return false;
54 | }
55 |
56 | try
57 | {
58 | // Load the library
59 | _libraryHandle = NativeLibrary.Load(_libraryPath);
60 |
61 | // Set up the DllImport resolver for the LibSSH2 assembly
62 | var libssh2Assembly = typeof(LibSshNative).Assembly;
63 | NativeLibrary.SetDllImportResolver(libssh2Assembly, DllImportResolver);
64 |
65 | AnsiConsole.MarkupLine($"[green]Successfully loaded native library from: {_libraryPath}[/]");
66 | return _libraryHandle != IntPtr.Zero;
67 | }
68 | catch (Exception ex)
69 | {
70 | AnsiConsole.MarkupLine($"[red]Failed to load native library: {ex.Message}[/]");
71 | return false;
72 | }
73 | }
74 |
75 | private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
76 | {
77 | // If the library being imported is "libssh2", return our pre-loaded handle
78 | if (libraryName == "libssh2" && _libraryHandle != IntPtr.Zero)
79 | {
80 | return _libraryHandle;
81 | }
82 |
83 | // Otherwise, use default resolution
84 | return IntPtr.Zero;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Terminal/TerminalModesBuilder.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 |
3 | namespace NullOpsDevs.LibSsh.Terminal;
4 |
5 | ///
6 | /// Fluent builder for constructing terminal modes byte array for PTY requests.
7 | /// Terminal modes are encoded as defined in RFC 4254, Section 8.
8 | ///
9 | [PublicAPI]
10 | public class TerminalModesBuilder
11 | {
12 | ///
13 | /// Gets an empty terminal modes array (just TTY_OP_END).
14 | /// This tells the server to use default terminal settings.
15 | ///
16 | public static readonly byte[] Empty = [0];
17 |
18 | private readonly List buffer = [];
19 |
20 | ///
21 | /// Sets a control character mode value.
22 | ///
23 | /// The terminal mode (e.g., VINTR, VEOF).
24 | /// The character value (e.g., 3 for Ctrl-C).
25 | /// This builder for fluent chaining.
26 | public TerminalModesBuilder SetCharacter(TerminalMode mode, byte value)
27 | {
28 | buffer.Add((byte)mode);
29 | WriteUInt32(value);
30 | return this;
31 | }
32 |
33 | ///
34 | /// Sets a boolean flag mode (enabled = 1, disabled = 0).
35 | ///
36 | /// The terminal mode flag (e.g., ECHO, ICANON, ISIG).
37 | /// True to enable the flag, false to disable.
38 | /// This builder for fluent chaining.
39 | public TerminalModesBuilder SetFlag(TerminalMode mode, bool enabled)
40 | {
41 | buffer.Add((byte)mode);
42 | WriteUInt32(enabled ? 1u : 0u);
43 | return this;
44 | }
45 |
46 | ///
47 | /// Sets a terminal mode with a custom uint32 value.
48 | ///
49 | /// The terminal mode.
50 | /// The value to set.
51 | /// This builder for fluent chaining.
52 | public TerminalModesBuilder SetMode(TerminalMode mode, uint value)
53 | {
54 | buffer.Add((byte)mode);
55 | WriteUInt32(value);
56 | return this;
57 | }
58 |
59 | ///
60 | /// Sets both input and output baud rates.
61 | ///
62 | /// The baud rate (e.g., 38400).
63 | /// This builder for fluent chaining.
64 | public TerminalModesBuilder SetSpeed(uint speed)
65 | {
66 | // Set input speed
67 | buffer.Add((byte)TerminalMode.TTY_OP_ISPEED);
68 | WriteUInt32(speed);
69 |
70 | // Set output speed
71 | buffer.Add((byte)TerminalMode.TTY_OP_OSPEED);
72 | WriteUInt32(speed);
73 |
74 | return this;
75 | }
76 |
77 | ///
78 | /// Builds the terminal modes byte array, appending TTY_OP_END terminator.
79 | ///
80 | /// The encoded terminal modes ready for use with libssh2.
81 | public byte[] Build()
82 | {
83 | // Add TTY_OP_END terminator
84 | buffer.Add((byte)TerminalMode.TTY_OP_END);
85 | return [.. buffer];
86 | }
87 |
88 | ///
89 | /// Writes a uint32 value in network byte order (big-endian) as required by SSH protocol.
90 | ///
91 | private void WriteUInt32(uint value)
92 | {
93 | buffer.Add((byte)(value >> 24));
94 | buffer.Add((byte)(value >> 16));
95 | buffer.Add((byte)(value >> 8));
96 | buffer.Add((byte)value);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Interop/NativeBuffer.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.InteropServices;
3 | using System.Text;
4 |
5 | namespace NullOpsDevs.LibSsh.Interop;
6 |
7 | ///
8 | /// Represents a disposable wrapper around unmanaged memory allocated from the global heap.
9 | ///
10 | /// The pointer to the allocated memory.
11 | /// The length of the allocated memory in bytes.
12 | internal readonly ref struct NativeBuffer(IntPtr pointer, int length) : IDisposable
13 | {
14 | ///
15 | /// Gets the pointer to the allocated native memory.
16 | ///
17 | public IntPtr Pointer { get; } = pointer;
18 |
19 | ///
20 | /// Gets the length of the allocated memory in bytes.
21 | ///
22 | public int Length { get; } = length;
23 |
24 | ///
25 | /// Returns the allocated memory as a UTF-8 encoded string.
26 | ///
27 | public string AsString() => Marshal.PtrToStringUTF8(Pointer) ?? string.Empty;
28 |
29 | ///
30 | /// Gets a span view of the allocated memory as bytes.
31 | ///
32 | public unsafe Span Span => new(Pointer.ToPointer(), Length);
33 |
34 | ///
35 | /// Gets an untyped pointer to the allocated memory.
36 | ///
37 | /// A void pointer to the start of the allocated memory buffer.
38 | public unsafe void* AsPointer() => Pointer.ToPointer();
39 |
40 | ///
41 | /// Gets a typed pointer to the allocated memory.
42 | ///
43 | /// The unmanaged type to cast the pointer to.
44 | /// A pointer to the allocated memory cast as the specified type.
45 | public unsafe T* AsPointer() where T : unmanaged => (T*)Pointer.ToPointer();
46 |
47 | ///
48 | public unsafe void Dispose()
49 | {
50 | Unsafe.InitBlockUnaligned(AsPointer(), 0, (uint)Length);
51 | Marshal.FreeHGlobal(Pointer);
52 | }
53 |
54 | ///
55 | /// Allocates a native buffer of the specified length.
56 | ///
57 | /// The number of bytes to allocate.
58 | /// A new NativeBuffer with allocated memory.
59 | public static unsafe NativeBuffer Allocate(int length)
60 | {
61 | var buf = Marshal.AllocHGlobal(length);
62 | Unsafe.InitBlockUnaligned(buf.ToPointer(), 0, (uint)length);
63 | return new NativeBuffer(buf, length);
64 | }
65 |
66 | ///
67 | /// Allocates a native buffer and copies a UTF-8 encoded string into it.
68 | ///
69 | /// The string to encode and copy.
70 | /// A new NativeBuffer containing the UTF-8 encoded string.
71 | public static NativeBuffer Allocate(string value)
72 | {
73 | var buf = Allocate(Encoding.UTF8.GetByteCount(value) + 1);
74 | Encoding.UTF8.GetBytes(value, buf.Span);
75 | buf.Span[^1] = 0;
76 |
77 | return buf;
78 | }
79 |
80 | ///
81 | /// Allocates a native buffer and copies the byte array data into it.
82 | ///
83 | /// The byte array to copy into native memory.
84 | /// A new NativeBuffer containing a copy of the data.
85 | public static NativeBuffer Allocate(byte[] data)
86 | {
87 | var buf = Allocate(data.Length);
88 | data.CopyTo(buf.Span);
89 |
90 | return buf;
91 | }
92 | }
--------------------------------------------------------------------------------
/docs/Writerside/topics/performance-and-reliability.md:
--------------------------------------------------------------------------------
1 | # Performance and Reliability
2 |
3 | ## Stress Testing
4 |
5 | A local stress test was conducted with 2,000 parallel SSH connections to a Docker container on an AMD Ryzen 9 5900X system, completing in 7,921ms.
6 |
7 | ### Test Environment
8 |
9 | **Docker Compose Configuration** (`docker-compose.yml`):
10 |
11 | ```yaml
12 | services:
13 | ssh-server:
14 | build:
15 | context: ./docker
16 | dockerfile: Dockerfile
17 | container_name: libssh-test-server
18 | hostname: ssh-test
19 | network_mode: "host"
20 | restart: unless-stopped
21 | ulimits:
22 | nofile:
23 | soft: 65536
24 | hard: 65536
25 | nproc:
26 | soft: 32768
27 | hard: 32768
28 | healthcheck:
29 | test: ["CMD", "nc", "-z", "localhost", "2222"]
30 | interval: 5s
31 | timeout: 3s
32 | retries: 10
33 | ```
34 |
35 | **Dockerfile**:
36 |
37 | ```Docker
38 | FROM alpine:latest
39 |
40 | # Install OpenSSH server
41 | RUN apk add --no-cache openssh-server openssh-keygen
42 |
43 | # Create user
44 | RUN adduser -D -s /bin/sh user && \
45 | echo "user:12345" | chpasswd
46 |
47 | # Setup SSH directories
48 | RUN mkdir -p /run/sshd /home/user/.ssh && \
49 | chmod 700 /home/user/.ssh && \
50 | chown -R user:user /home/user/.ssh
51 |
52 | # Generate host keys
53 | RUN ssh-keygen -A
54 |
55 | # Create sshd_config with high limits
56 | RUN echo 'Port 2222' > /etc/ssh/sshd_config && \
57 | echo 'PermitRootLogin no' >> /etc/ssh/sshd_config && \
58 | echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
59 | echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config && \
60 | echo 'AuthorizedKeysFile /home/user/.ssh/authorized_keys' >> /etc/ssh/sshd_config && \
61 | echo 'MaxStartups 1000:30:2000' >> /etc/ssh/sshd_config && \
62 | echo 'MaxSessions 1000' >> /etc/ssh/sshd_config && \
63 | echo 'LoginGraceTime 30' >> /etc/ssh/sshd_config && \
64 | echo 'ClientAliveInterval 30' >> /etc/ssh/sshd_config && \
65 | echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config && \
66 | echo 'UseDNS no' >> /etc/ssh/sshd_config && \
67 | echo 'MaxAuthTries 10' >> /etc/ssh/sshd_config
68 |
69 | # Create test files directory and generate test files
70 | RUN mkdir -p /test-files && \
71 | echo "Small test file content" > /test-files/small.txt && \
72 | dd if=/dev/urandom of=/test-files/medium.bin bs=1024 count=1024 2>/dev/null && \
73 | dd if=/dev/urandom of=/test-files/large.dat bs=1024 count=10240 2>/dev/null && \
74 | chmod 644 /test-files/*
75 |
76 | # Copy SSH keys
77 | COPY test-keys/*.pub /tmp/keys/
78 | RUN cat /tmp/keys/*.pub > /home/user/.ssh/authorized_keys 2>/dev/null || true && \
79 | chmod 600 /home/user/.ssh/authorized_keys && \
80 | chown user:user /home/user/.ssh/authorized_keys && \
81 | rm -rf /tmp/keys
82 |
83 | EXPOSE 2222
84 |
85 | # Start sshd in foreground
86 | CMD ["/usr/sbin/sshd", "-D", "-e"]
87 | ```
88 |
89 | ### Memory Profile
90 |
91 | 
92 |
93 | The memory profile shows stable behavior with no memory leaks. Memory peaked at ~320MB during the 2,000 parallel connections and was automatically reclaimed by .NET garbage collection afterward.
94 |
95 | ## See Also
96 |
97 | - [Session Lifecycle](session-lifecycle.md) - Proper session management
98 | - [Session Timeouts](session-timeouts.md) - Configuring timeouts
99 | - [Keeping Connection Alive](keeping-connection-alive.md) - Keepalive configuration
100 | - [Error Handling](error-handling.md) - Handling errors gracefully
101 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh.Test/TestHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography;
2 | using Microsoft.Extensions.Logging;
3 | using NullOpsDevs.LibSsh.Credentials;
4 |
5 | namespace NullOpsDevs.LibSsh.Test;
6 |
7 | ///
8 | /// Helper utilities for SSH testing
9 | ///
10 | public static class TestHelper
11 | {
12 | ///
13 | /// Creates a new SSH session and connects to the test server
14 | ///
15 | public static SshSession CreateAndConnect()
16 | {
17 | ILogger? logger = null;
18 |
19 | if (Environment.GetEnvironmentVariable("DEBUG_LOGGING") != null)
20 | logger = new AnsiConsoleLogger();
21 |
22 | var session = new SshSession(logger);
23 | session.Connect(TestConfig.Host, TestConfig.Port);
24 | return session;
25 | }
26 |
27 | ///
28 | /// Creates a new SSH session, connects, and authenticates with password
29 | ///
30 | public static SshSession CreateConnectAndAuthenticate()
31 | {
32 | var session = CreateAndConnect();
33 | var credential = SshCredential.FromPassword(TestConfig.Username, TestConfig.Password);
34 | session.Authenticate(credential);
35 | return session;
36 | }
37 |
38 | ///
39 | /// Computes SHA256 hash of a stream
40 | ///
41 | public static string GetStreamHash(Stream stream)
42 | {
43 | stream.Position = 0;
44 | using var sha256 = SHA256.Create();
45 | var hash = sha256.ComputeHash(stream);
46 | stream.Position = 0;
47 | return Convert.ToHexString(hash);
48 | }
49 |
50 | ///
51 | /// Creates a temporary file with random content
52 | ///
53 | public static string CreateTempFile(long sizeInBytes)
54 | {
55 | var path = Path.GetTempFileName();
56 | var random = new Random();
57 | var buffer = new byte[4096];
58 |
59 | using var fs = File.OpenWrite(path);
60 |
61 | var remaining = sizeInBytes;
62 | while (remaining > 0)
63 | {
64 | var toWrite = (int)Math.Min(buffer.Length, remaining);
65 | random.NextBytes(buffer.AsSpan(0, toWrite));
66 | fs.Write(buffer, 0, toWrite);
67 | remaining -= toWrite;
68 | }
69 |
70 | return path;
71 | }
72 |
73 | ///
74 | /// Waits for Docker containers to be ready
75 | ///
76 | public static async Task WaitForContainersAsync(TimeSpan timeout)
77 | {
78 | var start = DateTime.UtcNow;
79 |
80 | while (DateTime.UtcNow - start < timeout)
81 | {
82 | try
83 | {
84 | using var session = new SshSession();
85 | session.Connect(TestConfig.Host, TestConfig.Port);
86 | var credential = SshCredential.FromPassword(TestConfig.Username, TestConfig.Password);
87 | if (session.Authenticate(credential))
88 | {
89 | return;
90 | }
91 | }
92 | catch
93 | {
94 | // Ignore and retry
95 | }
96 |
97 | await Task.Delay(1000);
98 | }
99 |
100 | throw new TimeoutException($"Docker containers did not become ready within {timeout.TotalSeconds} seconds");
101 | }
102 |
103 | ///
104 | /// Loads a key file into a byte array
105 | ///
106 | public static byte[] LoadKeyFile(string path)
107 | {
108 | return File.ReadAllBytes(TestConfig.GetKeyPath(path));
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/keeping-connection-alive.md:
--------------------------------------------------------------------------------
1 | # Keeping Connection Alive
2 |
3 | SSH connections can be terminated by network infrastructure (firewalls, NAT devices, load balancers) or the remote server when there's no activity for an extended period. If you're not actively using the connection for a long time, you need to send keepalive messages to prevent these timeouts.
4 |
5 | ## Why You Need Keepalives
6 |
7 | When your SSH connection is idle (not executing commands or transferring files), it can be terminated:
8 |
9 | - **Network devices may drop idle connections** - Firewalls and NAT gateways often have idle timeout policies (typically 5-15 minutes)
10 | - **SSH servers may disconnect idle sessions** - Many servers have `ClientAliveInterval` configured to terminate inactive connections
11 | - **Long-running operations may appear stalled** - Without activity, monitoring systems may flag the connection as dead
12 |
13 | Keepalive messages solve this by sending periodic SSH protocol messages that keep the connection active.
14 |
15 | ## Configuring and Sending Keepalives
16 |
17 | Configure keepalives after connecting and send them periodically when you're not actively using the connection:
18 |
19 | ```c#
20 | using NullOpsDevs.LibSsh;
21 | using NullOpsDevs.LibSsh.Credentials;
22 |
23 | var session = new SshSession();
24 | session.Connect("example.com", 22);
25 |
26 | // Configure keepalive: send every 60 seconds, don't require server response
27 | session.ConfigureKeepAlive(
28 | wantReply: false,
29 | interval: TimeSpan.FromSeconds(60)
30 | );
31 |
32 | session.Authenticate(SshCredential.FromPassword("user", "password"));
33 |
34 | // When the connection is idle, send keepalives periodically
35 | // For example, in a loop while waiting for something
36 | while (waiting)
37 | {
38 | Thread.Sleep(60_000); // Wait 60 seconds
39 | session.SendKeepAlive(); // Send keepalive to keep connection alive
40 | }
41 | ```
42 |
43 | ### Parameters
44 |
45 | - **wantReply**:
46 | - `false` - Send keepalive without expecting a reply (recommended, lower overhead)
47 | - `true` - Require the server to acknowledge keepalives (useful for detecting broken connections)
48 |
49 | - **interval**:
50 | - The time between keepalive messages
51 | - Typical values: 30-60 seconds
52 | - Must be less than the smallest timeout in your network path
53 |
54 | ## Detecting Connection Failures
55 |
56 | When `wantReply: true`, the server must acknowledge keepalives. This helps detect broken connections:
57 |
58 | ```c#
59 | session.ConfigureKeepAlive(wantReply: true, interval: TimeSpan.FromSeconds(30));
60 |
61 | try
62 | {
63 | session.SendKeepAlive();
64 | Console.WriteLine("Connection is alive");
65 | }
66 | catch (SshException ex)
67 | {
68 | Console.WriteLine("Connection appears to be broken!");
69 | // Reconnect or handle the failure
70 | }
71 | ```
72 |
73 | > **Note:** Using `wantReply: true` adds network round-trip overhead. Use it only when you need to actively monitor connection health.
74 |
75 | ## Best Practices
76 |
77 | 1. **Choose appropriate intervals**:
78 | - 30-60 seconds for typical use cases
79 | - Shorter intervals (15-30s) for restrictive networks
80 | - Avoid very short intervals (<15s) - they waste bandwidth
81 |
82 | 2. **Use wantReply: false by default**:
83 | - Lower overhead
84 | - Sufficient for most keepalive scenarios
85 | - Only use `wantReply: true` when actively monitoring connection health
86 |
87 | 3. **Only send keepalives when idle**:
88 | - No need to send keepalives while actively executing commands or transferring files
89 | - SSH activity naturally keeps the connection alive
90 |
91 | 4. **Coordinate with server timeouts**:
92 | - Set keepalive interval lower than server's `ClientAliveInterval`
93 | - Check network infrastructure timeouts (firewalls, load balancers)
94 |
95 | ## See Also
96 |
97 | - `SshSession.ConfigureKeepAlive()` (SshSession.cs:325) - Configure keepalive settings
98 | - `SshSession.SendKeepAlive()` (SshSession.cs:300) - Manually send a keepalive message
99 | - [Session Timeouts](session-timeouts.md) - Understanding timeouts vs keepalives
100 | - [Session Lifecycle](session-lifecycle.md) - When to send keepalives
101 | - [Authentication](authentication.md) - Authenticate before using keepalives
102 | - [Error Handling](error-handling.md) - Handle keepalive errors
103 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Credentials/SshCredential.cs:
--------------------------------------------------------------------------------
1 | namespace NullOpsDevs.LibSsh.Credentials;
2 |
3 | ///
4 | /// Abstract base class for SSH authentication credentials.
5 | ///
6 | public abstract class SshCredential
7 | {
8 | ///
9 | /// Initializes a new instance of the class.
10 | ///
11 | protected SshCredential() {}
12 |
13 | ///
14 | /// Authenticates the session using this credential.
15 | ///
16 | /// The libssh2 session pointer.
17 | /// True if authentication succeeded; false otherwise.
18 | public abstract bool Authenticate(SshSession session);
19 |
20 | ///
21 | /// Creates a password-based SSH credential.
22 | ///
23 | /// The username for authentication.
24 | /// The password for authentication.
25 | /// A password-based SSH credential.
26 | public static SshCredential FromPassword(string username, string password) => new SshPasswordCredential(username, password);
27 |
28 | ///
29 | /// Creates a public key-based SSH credential using key files.
30 | ///
31 | /// The username for authentication.
32 | /// The path to the public key file.
33 | /// The path to the private key file.
34 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
35 | /// A public key-based SSH credential.
36 | public static SshCredential FromPublicKeyFile(string username, string publicKeyPath, string privateKeyPath, string? passphrase = null)
37 | => new SshPublicKeyCredential(username, publicKeyPath, privateKeyPath, passphrase);
38 |
39 | ///
40 | /// Creates a public key-based SSH credential using key data from memory.
41 | ///
42 | /// The username for authentication.
43 | /// The public key data as a byte array.
44 | /// The private key data as a byte array.
45 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
46 | /// A public key-based SSH credential.
47 | public static SshCredential FromPublicKeyMemory(string username, byte[] publicKeyData, byte[] privateKeyData, string? passphrase = null)
48 | => new SshPublicKeyFromMemoryCredential(username, publicKeyData, privateKeyData, passphrase);
49 |
50 | ///
51 | /// Creates an SSH credential that uses the SSH agent for authentication.
52 | ///
53 | /// The username for authentication.
54 | /// An SSH agent-based credential.
55 | ///
56 | /// This credential type connects to the running SSH agent (ssh-agent on Unix, pageant on Windows)
57 | /// and attempts to authenticate using the identities available in the agent.
58 | ///
59 | public static SshCredential FromAgent(string username) => new SshAgentCredential(username);
60 |
61 | ///
62 | /// Creates a host-based SSH credential.
63 | ///
64 | /// The username for authentication.
65 | /// The path to the public key file.
66 | /// The path to the private key file.
67 | /// Optional passphrase to decrypt the private key. Use null or empty string if no passphrase is required.
68 | /// The hostname of the client machine.
69 | /// The local username on the client machine. If null or empty, uses the same value as username.
70 | /// A host-based SSH credential.
71 | ///
72 | /// Host-based authentication allows a host to authenticate on behalf of its users.
73 | /// This is typically used in trusted environments where the server trusts certain client hosts.
74 | /// This authentication method is rarely used in modern SSH deployments.
75 | ///
76 | public static SshCredential FromHostBased(string username, string publicKeyPath, string privateKeyPath, string? passphrase, string hostname, string? localUsername = null)
77 | => new SshHostBasedCredential(username, publicKeyPath, privateKeyPath, passphrase, hostname, localUsername);
78 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Platform/PlatformDependentStat.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using NullOpsDevs.LibSsh.Core;
3 | using NullOpsDevs.LibSsh.Exceptions;
4 |
5 | namespace NullOpsDevs.LibSsh.Platform;
6 |
7 | ///
8 | /// Platform-agnostic wrapper for file stat structures across different operating systems.
9 | ///
10 | public readonly struct PlatformInDependentStat
11 | {
12 | ///
13 | /// Gets the size of the file in bytes.
14 | ///
15 | public long FileSize { get; init; }
16 |
17 | ///
18 | /// Creates a instance from a platform-specific stat structure.
19 | ///
20 | /// Pointer to the platform-specific stat structure.
21 | /// A platform-agnostic instance.
22 | /// Thrown when an internal exception occurs during conversion.
23 | /// Thrown when the current operating system is not supported.
24 | public static unsafe PlatformInDependentStat From(void* structure)
25 | {
26 | try
27 | {
28 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
29 | return CreateFromUnixStruct(structure);
30 |
31 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
32 | return CreateFromWindowsMingwStruct(structure);
33 |
34 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
35 | return CreateFromMacOsStruct(structure);
36 |
37 | #if NETSTANDARD2_1
38 | // Behavior verified on FreeBSD 14.3-RELEASE
39 | if (RuntimeInformation.OSDescription.Contains("freebsd", StringComparison.OrdinalIgnoreCase)) {
40 | #else
41 | if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) {
42 | #endif
43 | return CreateFromFreeBsdStruct(structure);
44 | }
45 | }
46 | catch (Exception ex)
47 | {
48 | throw new SshException("Internal exception occured", SshError.InnerException, ex);
49 | }
50 |
51 | throw new NotSupportedException("Your OS is not supported by this library.");
52 | }
53 |
54 | ///
55 | /// Creates a from a Windows MinGW64 stat structure.
56 | ///
57 | /// Pointer to the Windows MinGW64 stat structure.
58 | /// A instance with data from the Windows structure.
59 | private static unsafe PlatformInDependentStat CreateFromWindowsMingwStruct(void* structure)
60 | {
61 | var stat = (StatMingw64*)structure;
62 |
63 | return new PlatformInDependentStat
64 | {
65 | FileSize = stat->st_size
66 | };
67 | }
68 |
69 | ///
70 | /// Creates a from a Linux x64 stat structure.
71 | ///
72 | /// Pointer to the Linux x64 stat structure.
73 | /// A instance with data from the Linux structure.
74 | private static unsafe PlatformInDependentStat CreateFromUnixStruct(void* structure)
75 | {
76 | var stat = (StatLinuxX64*)structure;
77 |
78 | return new PlatformInDependentStat
79 | {
80 | FileSize = stat->st_size
81 | };
82 | }
83 |
84 | ///
85 | /// Creates a from a macOS (Darwin) stat structure.
86 | ///
87 | /// Pointer to the macOS stat structure.
88 | /// A instance with data from the macOS structure.
89 | private static unsafe PlatformInDependentStat CreateFromMacOsStruct(void* structure)
90 | {
91 | var stat = (StatDarwin*)structure;
92 |
93 | return new PlatformInDependentStat
94 | {
95 | FileSize = stat->st_size
96 | };
97 | }
98 |
99 | ///
100 | /// Creates a from a FreeBSD stat structure.
101 | ///
102 | /// Pointer to the FreeBSD stat structure.
103 | /// A instance with data from the FreeBSD structure.
104 | private static unsafe PlatformInDependentStat CreateFromFreeBsdStruct(void* structure)
105 | {
106 | var stat = (StatFreebsd*)structure;
107 |
108 | return new PlatformInDependentStat
109 | {
110 | FileSize = stat->st_size
111 | };
112 | }
113 | }
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/ChannelReader.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using NullOpsDevs.LibSsh.Generated;
3 | using NullOpsDevs.LibSsh.Interop;
4 |
5 | namespace NullOpsDevs.LibSsh.Core;
6 |
7 | ///
8 | /// Provides utilities for reading data from SSH channel streams.
9 | ///
10 | internal static class ChannelReader
11 | {
12 | ///
13 | /// Stream ID for standard output (stdout).
14 | ///
15 | public const int StdoutStreamId = 0;
16 |
17 | ///
18 | /// Stream ID for standard error (stderr).
19 | ///
20 | public const int StderrStreamId = 1;
21 |
22 | ///
23 | /// Reads all data from a channel stream and decodes it as a UTF-8 string.
24 | ///
25 | /// The libssh2 channel pointer.
26 | /// The stream ID to read from (0 for stdout, 1 for stderr).
27 | /// Cancellation token to cancel the read operation.
28 | /// The UTF-8 decoded string from the channel stream.
29 | public static unsafe string ReadUtf8String(_LIBSSH2_CHANNEL* channel, int streamId, CancellationToken cancellationToken)
30 | {
31 | using var memoryStream = new MemoryStream(4096);
32 | CopyToStream(channel, streamId, memoryStream, cancellationToken: cancellationToken);
33 |
34 | return Encoding.UTF8.GetString(memoryStream.ToArray());
35 | }
36 |
37 | ///
38 | /// Reads data from a libssh2 channel stream and copies it to a destination stream.
39 | ///
40 | /// The libssh2 channel pointer to read from.
41 | /// The stream ID to read from (0 for stdout, 1 for stderr).
42 | /// The destination stream to write the data to.
43 | /// The size of the buffer to use for reading. Default is 4096 bytes.
44 | /// Optional expected number of bytes to read. If null, reads until the channel returns 0 bytes.
45 | /// Optional cancellation token to cancel the read operation.
46 | /// The total number of bytes read from the channel and written to the destination stream.
47 | ///
48 | /// This method reads data in chunks using the specified buffer size. It continues reading until
49 | /// either the expected size is reached (if specified), the channel returns 0 bytes indicating EOF,
50 | /// or the operation is cancelled.
51 | ///
52 | public static unsafe long CopyToStream(_LIBSSH2_CHANNEL* channel, int streamId, Stream destination, int bufferSize = 4096, int? expectedSize = null, CancellationToken cancellationToken = default)
53 | {
54 | using var buffer = NativeBuffer.Allocate(bufferSize);
55 | var totalBytesRead = 0L;
56 |
57 | while (expectedSize == null || totalBytesRead < expectedSize)
58 | {
59 | cancellationToken.ThrowIfCancellationRequested();
60 |
61 | var bytesRead = LibSshNative.libssh2_channel_read_ex(channel, streamId, buffer.AsPointer(), (nuint)buffer.Length);
62 |
63 | if (bytesRead > 0)
64 | {
65 | // Limit bytes to write if we have an expected size
66 | var bytesToWrite = bytesRead;
67 | if (expectedSize.HasValue)
68 | {
69 | var remaining = expectedSize.Value - totalBytesRead;
70 | bytesToWrite = (int)Math.Min(bytesRead, remaining);
71 | }
72 |
73 | totalBytesRead += bytesToWrite;
74 | destination.Write(new ReadOnlySpan(buffer.AsPointer(), (int)bytesToWrite));
75 |
76 | // If we've written exactly the expected amount, stop reading
77 | if (expectedSize.HasValue && totalBytesRead >= expectedSize.Value)
78 | break;
79 | }
80 | else if (bytesRead == 0)
81 | {
82 | break;
83 | }
84 | }
85 |
86 | return totalBytesRead;
87 | }
88 |
89 | ///
90 | /// Writes data from a source stream to a libssh2 channel stream.
91 | ///
92 | /// The libssh2 channel pointer to write to.
93 | /// The stream ID to write to (typically 0 for standard input).
94 | /// The source stream to read data from.
95 | /// The total number of bytes to write to the channel.
96 | /// The size of the buffer to use for writing. Default is 4096 bytes.
97 | /// Optional cancellation token to cancel the write operation.
98 | /// The total number of bytes successfully written to the channel. Returns 0 if a write error occurs.
99 | ///
100 | /// This method reads data from the source stream in chunks and writes it to the channel.
101 | /// It handles partial writes by continuing to write remaining data from the current buffer.
102 | /// If a write operation returns a negative value (indicating an error), the method returns 0.
103 | /// The operation continues until all bytes are written, the source stream ends, or the operation is cancelled.
104 | ///
105 | public static unsafe long CopyToChannel(_LIBSSH2_CHANNEL* channel, int streamId, Stream source, long bytesToWrite, int bufferSize = 4096, CancellationToken cancellationToken = default)
106 | {
107 | using var buffer = NativeBuffer.Allocate(bufferSize);
108 | var totalBytesWritten = 0L;
109 |
110 | while (totalBytesWritten < bytesToWrite)
111 | {
112 | cancellationToken.ThrowIfCancellationRequested();
113 |
114 | var bytesRead = source.Read(buffer.Span);
115 |
116 | if (bytesRead == 0)
117 | break;
118 |
119 | var offset = 0;
120 | while (offset < bytesRead)
121 | {
122 | var bytesWritten = LibSshNative.libssh2_channel_write_ex(
123 | channel,
124 | streamId,
125 | (sbyte*)(buffer.AsPointer() + offset),
126 | (nuint)(bytesRead - offset));
127 |
128 | if (bytesWritten < 0)
129 | return 0;
130 |
131 | offset += (int)bytesWritten;
132 | totalBytesWritten += bytesWritten;
133 | }
134 | }
135 |
136 | return totalBytesWritten;
137 | }
138 | }
--------------------------------------------------------------------------------
/docs/Writerside/topics/authentication.md:
--------------------------------------------------------------------------------
1 | # Authentication
2 |
3 | After establishing a connection to an SSH server, you must authenticate before you can execute commands or transfer files. `NullOpsDevs.LibSsh` supports multiple authentication methods to suit different security requirements and deployment scenarios.
4 |
5 | ## Overview
6 |
7 | Authentication is performed by calling the `Authenticate()` method on your `SshSession` instance with an appropriate `SshCredential` object. The session must be in the `Connected` state before authenticating.
8 |
9 | ```c#
10 | using NullOpsDevs.LibSsh;
11 | using NullOpsDevs.LibSsh.Credentials;
12 |
13 | var session = new SshSession();
14 | session.Connect("example.com", 22);
15 |
16 | // Authenticate using your chosen method
17 | bool success = session.Authenticate(credential);
18 |
19 | if (success)
20 | {
21 | // Session is now in LoggedIn state
22 | // You can execute commands, transfer files, etc.
23 | }
24 | ```
25 |
26 | ## Password Authentication
27 |
28 | Password authentication is the simplest method but is generally less secure than key-based authentication. It's suitable for development environments or when key-based authentication isn't available.
29 |
30 | ```c#
31 | var credential = SshCredential.FromPassword("username", "password");
32 | session.Authenticate(credential);
33 | ```
34 |
35 |
36 | Security Note: Password authentication transmits credentials over an encrypted channel, but passwords can be compromised through various attacks. Consider using key-based authentication for production environments.
37 |
38 |
39 | ## Public Key Authentication (File-based)
40 |
41 | Public key authentication is the recommended method for most use cases. It uses asymmetric cryptography where you authenticate with a private key that corresponds to a public key registered on the server.
42 |
43 | ### Basic Usage
44 |
45 | ```c#
46 | var credential = SshCredential.FromPublicKeyFile(
47 | username: "username",
48 | publicKeyPath: "~/.ssh/id_rsa.pub",
49 | privateKeyPath: "~/.ssh/id_rsa",
50 | passphrase: null // or provide passphrase if key is encrypted
51 | );
52 |
53 | session.Authenticate(credential);
54 | ```
55 |
56 | ### With Encrypted Private Key
57 |
58 | ```c#
59 | var credential = SshCredential.FromPublicKeyFile(
60 | username: "username",
61 | publicKeyPath: "~/.ssh/id_ed25519.pub",
62 | privateKeyPath: "~/.ssh/id_ed25519",
63 | passphrase: "my-secure-passphrase"
64 | );
65 |
66 | session.Authenticate(credential);
67 | ```
68 |
69 | > **Best Practice:** Use Ed25519 keys (`ssh-keygen -t ed25519`) for new deployments. They offer better security and performance than RSA keys.
70 |
71 | ## Public Key Authentication (Memory-based)
72 |
73 | When you need to load keys from sources other than the filesystem (e.g., databases, configuration systems, or encrypted stores), you can use memory-based authentication.
74 |
75 | ```c#
76 | byte[] publicKeyData = LoadPublicKeyFromSecureStore();
77 | byte[] privateKeyData = LoadPrivateKeyFromSecureStore();
78 |
79 | var credential = SshCredential.FromPublicKeyMemory(
80 | username: "username",
81 | publicKeyData: publicKeyData,
82 | privateKeyData: privateKeyData,
83 | passphrase: null // or provide passphrase if key is encrypted
84 | );
85 |
86 | session.Authenticate(credential);
87 | ```
88 |
89 | > **Note:** The key data should be in OpenSSH format (PEM). Both public and private key data must be provided as UTF-8 encoded byte arrays.
90 |
91 | ## SSH Agent Authentication
92 |
93 | SSH agent authentication delegates key management to an SSH agent (ssh-agent on Linux/macOS, Pageant on Windows). This method is convenient when you have multiple keys or want to avoid storing private keys in your application.
94 |
95 | ```c#
96 | var credential = SshCredential.FromAgent("username");
97 | session.Authenticate(credential);
98 | ```
99 |
100 | The SSH agent will:
101 | 1. Try each available identity in the agent
102 | 2. Return success when a valid key is found
103 | 3. Return failure if no keys authenticate successfully
104 |
105 | > **Platform Note:** Ensure an SSH agent is running before using this method:
106 | > - **Linux/macOS:** `eval $(ssh-agent)` and `ssh-add ~/.ssh/id_rsa`
107 | > - **Windows:** Use Pageant or Windows OpenSSH Authentication Agent
108 |
109 | ## Host-based Authentication
110 |
111 | Host-based authentication allows a trusted client host to authenticate users without requiring individual credentials. This method is rarely used in modern deployments and is typically restricted to tightly controlled environments.
112 |
113 | ```c#
114 | var credential = SshCredential.FromHostBased(
115 | username: "username",
116 | publicKeyPath: "/etc/ssh/ssh_host_rsa_key.pub",
117 | privateKeyPath: "/etc/ssh/ssh_host_rsa_key",
118 | passphrase: null,
119 | hostname: "client-hostname.example.com",
120 | localUsername: "local-username" // optional, defaults to username
121 | );
122 | session.Authenticate(credential);
123 | ```
124 |
125 | > **Warning:** Host-based authentication requires server-side configuration (in `/etc/ssh/sshd_config` and `~/.shosts` or `/etc/ssh/shosts.equiv`) and is considered less secure than user-based authentication methods. Use with caution.
126 |
127 | ## Error Handling
128 |
129 | Authentication failures can occur for various reasons. Always check the return value and handle failures appropriately:
130 |
131 | ```c#
132 | try
133 | {
134 | var credential = SshCredential.FromPassword("username", "wrong-password");
135 | bool success = session.Authenticate(credential);
136 |
137 | if (!success)
138 | {
139 | Console.WriteLine("Authentication failed. Check your credentials.");
140 | return;
141 | }
142 | }
143 | catch (SshException ex)
144 | {
145 | Console.WriteLine($"SSH error during authentication: {ex.Message}");
146 | }
147 | ```
148 |
149 | Common authentication failures:
150 | - **Invalid credentials:** Wrong password or key not authorized on server
151 | - **Key format issues:** Unsupported key type or corrupted key file
152 | - **Permission denied:** Server configuration doesn't allow the authentication method
153 | - **Agent not available:** SSH agent not running when using agent authentication
154 |
155 | ## See Also
156 |
157 | - [Host Key Retrieval and Verification](host-key-retrieval-and-verification.md) - Verify server identity before authenticating
158 | - [Session Lifecycle](session-lifecycle.md) - Understanding session states during authentication
159 | - [Command Execution](command-execution.md) - Execute commands after authenticating
160 | - [File Transfer with SCP](scp.md) - Transfer files after authenticating
161 | - [Error Handling](error-handling.md) - Handle authentication errors
162 | - [Quickstart](quickstart.md) - Complete connection and authentication examples
163 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Terminal/TerminalMode.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using JetBrains.Annotations;
3 |
4 | namespace NullOpsDevs.LibSsh.Terminal;
5 |
6 | ///
7 | /// Terminal mode opcodes as defined in RFC 4254, Section 8.
8 | /// Used for encoding terminal behavior settings in PTY requests.
9 | ///
10 | [PublicAPI]
11 | [SuppressMessage("ReSharper", "InconsistentNaming")]
12 | public enum TerminalMode : byte
13 | {
14 | ///
15 | /// End of terminal modes. Must be last in the encoded stream.
16 | ///
17 | TTY_OP_END = 0,
18 |
19 | // Character codes (opcodes 1-18)
20 |
21 | ///
22 | /// Interrupt character (typically Ctrl-C, sends SIGINT).
23 | ///
24 | VINTR = 1,
25 |
26 | ///
27 | /// Quit character (typically Ctrl-\, sends SIGQUIT).
28 | ///
29 | VQUIT = 2,
30 |
31 | ///
32 | /// Erase the character to the left of the cursor.
33 | ///
34 | VERASE = 3,
35 |
36 | ///
37 | /// Kill the current input line.
38 | ///
39 | VKILL = 4,
40 |
41 | ///
42 | /// End-of-file character (typically Ctrl-D).
43 | ///
44 | VEOF = 5,
45 |
46 | ///
47 | /// End-of-line character.
48 | ///
49 | VEOL = 6,
50 |
51 | ///
52 | /// Additional end-of-line character.
53 | ///
54 | VEOL2 = 7,
55 |
56 | ///
57 | /// Continue paused output (typically Ctrl-Q).
58 | ///
59 | VSTART = 8,
60 |
61 | ///
62 | /// Pause output (typically Ctrl-S).
63 | ///
64 | VSTOP = 9,
65 |
66 | ///
67 | /// Suspend character (typically Ctrl-Z, sends SIGTSTP).
68 | ///
69 | VSUSP = 10,
70 |
71 | ///
72 | /// Delayed suspend character.
73 | ///
74 | VDSUSP = 11,
75 |
76 | ///
77 | /// Reprint the current input line.
78 | ///
79 | VREPRINT = 12,
80 |
81 | ///
82 | /// Erase the previous word.
83 | ///
84 | VWERASE = 13,
85 |
86 | ///
87 | /// Enter the next character literally.
88 | ///
89 | VLNEXT = 14,
90 |
91 | ///
92 | /// Flush output.
93 | ///
94 | VFLUSH = 15,
95 |
96 | ///
97 | /// Switch to a different shell layer.
98 | ///
99 | VSWTCH = 16,
100 |
101 | ///
102 | /// Status request character.
103 | ///
104 | VSTATUS = 17,
105 |
106 | ///
107 | /// Toggle discarding of output.
108 | ///
109 | VDISCARD = 18,
110 |
111 | // Input flags (opcodes 30-41)
112 |
113 | ///
114 | /// Ignore parity errors.
115 | ///
116 | IGNPAR = 30,
117 |
118 | ///
119 | /// Mark parity and framing errors.
120 | ///
121 | PARMRK = 31,
122 |
123 | ///
124 | /// Enable input parity checking.
125 | ///
126 | INPCK = 32,
127 |
128 | ///
129 | /// Strip 8th bit off characters.
130 | ///
131 | ISTRIP = 33,
132 |
133 | ///
134 | /// Map NL to CR on input.
135 | ///
136 | INLCR = 34,
137 |
138 | ///
139 | /// Ignore CR.
140 | ///
141 | IGNCR = 35,
142 |
143 | ///
144 | /// Map CR to NL on input.
145 | ///
146 | ICRNL = 36,
147 |
148 | ///
149 | /// Translate uppercase to lowercase on input.
150 | ///
151 | IUCLC = 37,
152 |
153 | ///
154 | /// Enable XON/XOFF flow control on output.
155 | ///
156 | IXON = 38,
157 |
158 | ///
159 | /// Any character will restart output.
160 | ///
161 | IXANY = 39,
162 |
163 | ///
164 | /// Enable XON/XOFF flow control on input.
165 | ///
166 | IXOFF = 40,
167 |
168 | ///
169 | /// Ring bell when input queue is full.
170 | ///
171 | IMAXBEL = 41,
172 |
173 | // Local flags (opcodes 50-62)
174 |
175 | ///
176 | /// Enable signals (INTR, QUIT, SUSP).
177 | ///
178 | ISIG = 50,
179 |
180 | ///
181 | /// Canonical input mode (line-buffered).
182 | ///
183 | ICANON = 51,
184 |
185 | ///
186 | /// Enable extended case processing.
187 | ///
188 | XCASE = 52,
189 |
190 | ///
191 | /// Echo input characters.
192 | ///
193 | ECHO = 53,
194 |
195 | ///
196 | /// Visually erase characters.
197 | ///
198 | ECHOE = 54,
199 |
200 | ///
201 | /// Echo KILL character.
202 | ///
203 | ECHOK = 55,
204 |
205 | ///
206 | /// Echo NL.
207 | ///
208 | ECHONL = 56,
209 |
210 | ///
211 | /// Disable flushing after interrupt or quit.
212 | ///
213 | NOFLSH = 57,
214 |
215 | ///
216 | /// Send SIGTTOU for background output.
217 | ///
218 | TOSTOP = 58,
219 |
220 | ///
221 | /// Enable implementation-defined input processing.
222 | ///
223 | IEXTEN = 59,
224 |
225 | ///
226 | /// Echo control characters as ^X.
227 | ///
228 | ECHOCTL = 60,
229 |
230 | ///
231 | /// Visual erase for line kill.
232 | ///
233 | ECHOKE = 61,
234 |
235 | ///
236 | /// Retype pending input.
237 | ///
238 | PENDIN = 62,
239 |
240 | // Output flags (opcodes 70-75)
241 |
242 | ///
243 | /// Enable output processing.
244 | ///
245 | OPOST = 70,
246 |
247 | ///
248 | /// Map lowercase to uppercase on output.
249 | ///
250 | OLCUC = 71,
251 |
252 | ///
253 | /// Map NL to CR-NL on output.
254 | ///
255 | ONLCR = 72,
256 |
257 | ///
258 | /// Map CR to NL on output.
259 | ///
260 | OCRNL = 73,
261 |
262 | ///
263 | /// Do not output CR at column 0.
264 | ///
265 | ONOCR = 74,
266 |
267 | ///
268 | /// NL performs CR function.
269 | ///
270 | ONLRET = 75,
271 |
272 | // Control flags (opcodes 90-93)
273 |
274 | ///
275 | /// 7-bit mode.
276 | ///
277 | CS7 = 90,
278 |
279 | ///
280 | /// 8-bit mode.
281 | ///
282 | CS8 = 91,
283 |
284 | ///
285 | /// Enable parity generation and detection.
286 | ///
287 | PARENB = 92,
288 |
289 | ///
290 | /// Use odd parity (else even).
291 | ///
292 | PARODD = 93,
293 |
294 | // Speed settings (opcodes 128-129)
295 |
296 | ///
297 | /// Input baud rate (uint32 value).
298 | ///
299 | TTY_OP_ISPEED = 128,
300 |
301 | ///
302 | /// Output baud rate (uint32 value).
303 | ///
304 | TTY_OP_OSPEED = 129
305 | }
306 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/NullOpsDevs.LibSsh.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0
6 | enable
7 | enable
8 | true
9 | latest
10 |
11 |
12 |
13 |
14 | NullOpsDevs.LibSsh
15 | 1.0.0
16 | NullOpsDevs
17 | .NET bindings for libssh2 with native libraries included for Windows, Linux, and macOS
18 | ssh;libssh2;sftp;scp;ssh2;native;interop
19 | MIT
20 | https://github.com/NullOpsDevs/LibSshNet
21 | https://github.com/NullOpsDevs/LibSshNet
22 | false
23 | README.md
24 | logo.png
25 |
26 |
27 |
28 |
29 | true
30 | embedded
31 | bin/Release/NullOpsDevs.LibSsh.xml
32 |
33 |
34 | true
35 |
36 |
37 | true
38 | true
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | runtimes/win-x64/native/libssh2.dll
51 | true
52 | PreserveNewest
53 |
54 |
55 |
56 |
57 | runtimes/linux-x64/native/libssh2.so.1.0.1
58 | true
59 | PreserveNewest
60 |
61 |
62 | runtimes/linux-x64/native/libssh2.so.1
63 | true
64 | PreserveNewest
65 |
66 |
67 | runtimes/linux-x64/native/libssh2.so
68 | true
69 | PreserveNewest
70 |
71 |
72 |
73 |
74 | runtimes/linux-arm64/native/libssh2.so.1.0.1
75 | true
76 | PreserveNewest
77 |
78 |
79 | runtimes/linux-arm64/native/libssh2.so.1
80 | true
81 | PreserveNewest
82 |
83 |
84 | runtimes/linux-arm64/native/libssh2.so
85 | true
86 | PreserveNewest
87 |
88 |
89 |
90 |
91 | runtimes/osx-x64/native/libssh2.dylib
92 | true
93 | PreserveNewest
94 |
95 |
96 |
97 |
98 | runtimes/osx-arm64/native/libssh2.dylib
99 | true
100 | PreserveNewest
101 |
102 |
103 |
104 |
105 |
106 |
107 | all
108 | runtime; build; native; contentfiles; analyzers; buildtransitive
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/running-tests-locally.md:
--------------------------------------------------------------------------------
1 | # Running Tests Locally
2 |
3 | This guide walks you through setting up and running the NullOpsDevs.LibSsh test suite on your local machine.
4 |
5 | ## Prerequisites
6 |
7 | Before running tests, ensure you have the following installed:
8 |
9 | - **.NET 8.0 or 9.0 SDK** - [Download](https://dotnet.microsoft.com/download)
10 | - **Docker Desktop** - [Download](https://www.docker.com/products/docker-desktop)
11 | - Windows: Docker Desktop with WSL 2 backend
12 | - Linux: Docker Engine and Docker Compose
13 | - macOS: Docker Desktop
14 |
15 | ## Quick Start
16 |
17 | ### 1. Clone the Repository
18 |
19 | ```bash
20 | git clone https://github.com/NullOpsDevs/LibSshNet.git
21 | cd LibSshNet
22 | ```
23 |
24 | ### 2. Start the SSH Test Server
25 |
26 | The test suite requires a Docker-based SSH server to be running:
27 |
28 | ```bash
29 | cd src/NullOpsDevs.LibSsh.Test
30 | docker compose up -d
31 | ```
32 |
33 | Wait for the container to be healthy (usually takes 5-10 seconds):
34 |
35 | ```bash
36 | docker compose ps
37 | ```
38 |
39 | You should see:
40 |
41 | ```
42 | NAME IMAGE STATUS
43 | libssh-test-server libssh-test-server:latest Up (healthy)
44 | ```
45 |
46 | ### 3. Run the Tests
47 |
48 | From the test project directory, simply run:
49 |
50 | ```bash
51 | dotnet run
52 | ```
53 |
54 | The test runner will:
55 | 1. Load the native libssh2 library
56 | 2. Wait for Docker containers to be ready
57 | 3. Run all test categories
58 | 4. Display a summary of results
59 |
60 | ## Test Categories
61 |
62 | The test suite includes the following categories:
63 |
64 | ### Authentication Tests
65 | - Retrieve negotiated methods
66 | - Host key retrieval
67 | - Host key hash retrieval
68 | - Password authentication
69 | - Public key authentication (with and without passphrase)
70 | - Public key from memory
71 | - SSH agent authentication
72 |
73 | ### Command Execution Tests
74 | - Basic command execution
75 | - Command with exit codes
76 | - Command with stderr output
77 | - Commands with PTY allocation
78 | - Long-running commands
79 |
80 | ### File Transfer Tests
81 | - SCP upload (small, medium, large files)
82 | - SCP download (small, medium, large files)
83 | - File transfer error handling
84 |
85 | ### Terminal Features Tests
86 | - PTY allocation
87 | - Terminal modes configuration
88 | - Window size configuration
89 |
90 | ### Error Handling Tests
91 | - Authentication failures
92 | - Connection timeouts
93 | - Invalid commands
94 | - File transfer errors
95 |
96 | ### Edge Case Tests
97 | - Multiple sequential operations
98 | - Timeout handling
99 | - Large output handling
100 | - Connection with deprecated methods
101 | - Parallel sessions (15 concurrent connections)
102 |
103 | ## Running Stress Tests
104 |
105 | For performance testing with 2,000 parallel connections:
106 |
107 |
108 |
109 |
110 | CRAZY_LOAD_TEST=1 dotnet run
111 |
112 |
113 |
114 |
115 | $env:CRAZY_LOAD_TEST="1"
116 | dotnet run
117 |
118 |
119 |
120 |
121 | set CRAZY_LOAD_TEST=1
122 | dotnet run
123 |
124 |
125 |
126 |
127 | > **Note**: Stress tests require sufficient system resources. Recommended: 8+ CPU cores, 16GB+ RAM.
128 |
129 | ## Test Server Configuration
130 |
131 | The Docker-based SSH test server is configured with:
132 |
133 | - **Port**: 2222 (mapped to host)
134 | - **Username**: `user`
135 | - **Password**: `12345`
136 | - **SSH Keys**: Located in `docker/test-keys/`
137 | - **Test Files**: Pre-generated in `/test-files/` inside the container
138 | - `small.txt` - Small text file
139 | - `medium.bin` - 1MB binary file
140 | - `large.dat` - 10MB binary file
141 |
142 | ### Server Limits
143 |
144 | The test server is configured to handle high connection loads:
145 |
146 | - **MaxStartups**: 1000:30:2000
147 | - **MaxSessions**: 1000
148 | - **File descriptors**: 65536
149 | - **Max processes**: 32768
150 |
151 | ## Troubleshooting
152 |
153 | ### Docker Container Not Starting
154 |
155 | Check Docker daemon status:
156 |
157 | ```bash
158 | docker ps
159 | ```
160 |
161 | View container logs:
162 |
163 | ```bash
164 | docker compose logs ssh-server
165 | ```
166 |
167 | Rebuild the container if needed:
168 |
169 | ```bash
170 | docker compose down
171 | docker compose build --no-cache
172 | docker compose up -d
173 | ```
174 |
175 | ### Connection Refused Errors
176 |
177 | Ensure the SSH server is listening on port 2222:
178 |
179 | ```bash
180 | docker compose exec ssh-server nc -z localhost 2222
181 | ```
182 |
183 | If the connection fails, restart the container:
184 |
185 | ```bash
186 | docker compose restart
187 | ```
188 |
189 | ### Tests Timing Out
190 |
191 | The test runner waits up to 60 seconds for containers to be ready. If tests still time out:
192 |
193 | 1. Check Docker resource allocation (CPU/memory)
194 | 2. Verify no firewall is blocking port 2222
195 | 3. Manually test SSH connection:
196 |
197 | ```bash
198 | ssh -p 2222 user@localhost
199 | # Password: 12345
200 | ```
201 |
202 | ### Native Library Load Failures
203 |
204 | If you see "Failed to load native library":
205 |
206 | 1. Ensure the NullOpsDevs.LibSsh package is properly restored
207 | 2. Check that your platform is supported (Windows x64, Linux x64/ARM64, macOS x64/ARM64)
208 | 3. Try cleaning and rebuilding:
209 |
210 | ```bash
211 | dotnet clean
212 | dotnet restore
213 | dotnet build
214 | dotnet run
215 | ```
216 |
217 | The native libraries are automatically copied to the output directory (`bin/Debug/net9.0/native/`) during build from the NuGet package.
218 |
219 | ### Port 2222 Already in Use
220 |
221 | If port 2222 is already in use:
222 |
223 | ```bash
224 | # Find what's using the port
225 | lsof -i :2222 # macOS/Linux
226 | netstat -ano | findstr :2222 # Windows
227 |
228 | # Stop the conflicting service or change the port in docker-compose.yml
229 | ```
230 |
231 | ## Cleaning Up
232 |
233 | ### Stop the Test Server
234 |
235 | ```bash
236 | docker compose down
237 | ```
238 |
239 | ### Remove Test Container and Images
240 |
241 | ```bash
242 | docker compose down --rmi all
243 | ```
244 |
245 | ### Full Cleanup (including volumes)
246 |
247 | ```bash
248 | docker compose down --rmi all --volumes
249 | ```
250 |
251 | ## CI/CD Integration
252 |
253 | The test suite is designed to run in CI/CD environments. See `.github/workflows/` for GitHub Actions examples.
254 |
255 | ### Environment Variables
256 |
257 | - `CRAZY_LOAD_TEST` - Set to `1` to enable stress testing with 2,000 parallel connections
258 |
259 | ### Example GitHub Actions Workflow
260 |
261 | ```yaml
262 | - name: Start SSH Test Server
263 | run: |
264 | cd src/NullOpsDevs.LibSsh.Test
265 | docker compose up -d
266 |
267 | - name: Run Tests
268 | run: |
269 | cd src/NullOpsDevs.LibSsh.Test
270 | dotnet run
271 |
272 | - name: Cleanup
273 | if: always()
274 | run: |
275 | cd src/NullOpsDevs.LibSsh.Test
276 | docker compose down
277 | ```
278 |
279 | ## Test Output
280 |
281 | Successful test runs will display colorful output using Spectre.Console:
282 |
283 | ```
284 | _ _ _ ____ ____ _ _ ____ _____ _
285 | | | (_) |_/ ___/ ___|| | | |___ \ |_ _|__ ___| |_ ___
286 | | | | | __\___ \___ \| |_| | __) | | |/ _ \/ __| __/ __|
287 | | |___| | |_ ___) |__) | _ |/ __/ | | __/\__ \ |_\__ \
288 | |_____|_|\__|____/____/|_| |_|_____| |_|\___||___/\__|___/
289 |
290 | ✓ Passed: 25
291 | ✗ Failed: 0
292 | - Skipped: 0
293 | ```
294 |
295 | ## See Also
296 |
297 | - [Performance and Reliability](performance-and-reliability.md) - Stress test results and memory profiling
298 | - [Session Lifecycle](session-lifecycle.md) - Understanding SSH session states
299 | - [Error Handling](error-handling.md) - Handling SSH errors in your applications
300 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/scp.md:
--------------------------------------------------------------------------------
1 | # File Transfer with SCP
2 |
3 | SCP (Secure Copy Protocol) allows you to securely transfer files between your local system and a remote SSH server. NullOpsDevs.LibSsh provides simple methods for both uploading and downloading files using the SCP protocol.
4 |
5 | ## Downloading Files
6 |
7 | To download a file from the remote server to your local system:
8 |
9 | ```c#
10 | using NullOpsDevs.LibSsh;
11 | using NullOpsDevs.LibSsh.Credentials;
12 |
13 | var session = new SshSession();
14 | session.Connect("example.com", 22);
15 | session.Authenticate(SshCredential.FromPassword("user", "password"));
16 |
17 | // Download a file
18 | using var fileStream = File.Create("local-file.txt");
19 | bool success = session.ReadFile("/remote/path/file.txt", fileStream);
20 |
21 | if (success)
22 | {
23 | Console.WriteLine("File downloaded successfully!");
24 | }
25 | ```
26 |
27 | ### Download with Custom Buffer Size
28 |
29 | You can specify a custom buffer size for better performance with large files:
30 |
31 | ```c#
32 | using var fileStream = File.Create("large-file.bin");
33 |
34 | // Use 64KB buffer instead of default 32KB
35 | bool success = session.ReadFile(
36 | path: "/remote/path/large-file.bin",
37 | destination: fileStream,
38 | bufferSize: 65536
39 | );
40 | ```
41 |
42 | ### Async Download
43 |
44 | For non-blocking file downloads:
45 |
46 | ```c#
47 | using var fileStream = File.Create("local-file.txt");
48 |
49 | bool success = await session.ReadFileAsync(
50 | "/remote/path/file.txt",
51 | fileStream,
52 | cancellationToken: cancellationToken
53 | );
54 | ```
55 |
56 | ## Uploading Files
57 |
58 | To upload a file from your local system to the remote server:
59 |
60 | ```c#
61 | using var fileStream = File.OpenRead("local-file.txt");
62 |
63 | bool success = session.WriteFile(
64 | path: "/remote/path/file.txt",
65 | source: fileStream
66 | );
67 |
68 | if (success)
69 | {
70 | Console.WriteLine("File uploaded successfully!");
71 | }
72 | ```
73 |
74 | ### Upload with Unix Permissions
75 |
76 | You can specify Unix file permissions when uploading:
77 |
78 | ```c#
79 | using var fileStream = File.OpenRead("script.sh");
80 |
81 | // Upload with rwxr-xr-x permissions (755 in octal, 493 in decimal)
82 | bool success = session.WriteFile(
83 | path: "/remote/path/script.sh",
84 | source: fileStream,
85 | mode: 493 // 0755 in octal
86 | );
87 | ```
88 |
89 | Common Unix permission modes:
90 |
91 | | Permissions | Octal | Decimal | Description |
92 | |-------------|-------|---------|-------------|
93 | | `rw-r--r--` | 0644 | 420 | Default file (owner read/write, others read) |
94 | | `rwxr-xr-x` | 0755 | 493 | Executable file (owner full, others read/execute) |
95 | | `rw-------` | 0600 | 384 | Private file (owner read/write only) |
96 | | `rwxrwxrwx` | 0777 | 511 | Full permissions (not recommended) |
97 |
98 | ### Upload with Custom Buffer Size
99 |
100 | ```c#
101 | using var fileStream = File.OpenRead("large-file.bin");
102 |
103 | // Use 128KB buffer for better performance
104 | bool success = session.WriteFile(
105 | path: "/remote/path/large-file.bin",
106 | source: fileStream,
107 | mode: 420, // rw-r--r--
108 | bufferSize: 131072
109 | );
110 | ```
111 |
112 | ### Async Upload
113 |
114 | For non-blocking file uploads:
115 |
116 | ```c#
117 | using var fileStream = File.OpenRead("local-file.txt");
118 |
119 | bool success = await session.WriteFileAsync(
120 | path: "/remote/path/file.txt",
121 | source: fileStream,
122 | mode: 420,
123 | cancellationToken: cancellationToken
124 | );
125 | ```
126 |
127 | ## Complete Example
128 |
129 | Here's a complete example showing both upload and download:
130 |
131 | ```c#
132 | using NullOpsDevs.LibSsh;
133 | using NullOpsDevs.LibSsh.Credentials;
134 |
135 | var session = new SshSession();
136 |
137 | try
138 | {
139 | // Connect and authenticate
140 | session.Connect("example.com", 22);
141 | session.Authenticate(SshCredential.FromPublicKeyFile(
142 | "username",
143 | "~/.ssh/id_ed25519.pub",
144 | "~/.ssh/id_ed25519"
145 | ));
146 |
147 | // Upload a file
148 | Console.WriteLine("Uploading file...");
149 | using (var uploadStream = File.OpenRead("local-data.txt"))
150 | {
151 | bool uploaded = session.WriteFile(
152 | "/home/user/data.txt",
153 | uploadStream,
154 | mode: 420 // rw-r--r--
155 | );
156 |
157 | if (uploaded)
158 | Console.WriteLine("Upload successful!");
159 | else
160 | Console.WriteLine("Upload failed!");
161 | }
162 |
163 | // Download a file
164 | Console.WriteLine("Downloading file...");
165 | using (var downloadStream = File.Create("downloaded-backup.tar.gz"))
166 | {
167 | bool downloaded = session.ReadFile(
168 | "/home/user/backup.tar.gz",
169 | downloadStream
170 | );
171 |
172 | if (downloaded)
173 | Console.WriteLine("Download successful!");
174 | else
175 | Console.WriteLine("Download failed!");
176 | }
177 | }
178 | finally
179 | {
180 | session.Dispose();
181 | }
182 | ```
183 |
184 | ## Important Notes
185 |
186 | 1. **Stream Management**:
187 | - The `ReadFile()` and `WriteFile()` methods do NOT close the streams
188 | - You are responsible for disposing streams (use `using` statements)
189 |
190 | 2. **Stream Requirements**:
191 | - Upload streams must be readable and seekable
192 | - Download streams must be writable
193 | - File size is determined from stream length for uploads
194 |
195 | 3. **Return Value**:
196 | - Both methods return `true` if the entire file was transferred successfully
197 | - Returns `false` if the transfer was incomplete
198 |
199 | 4. **Session State**:
200 | - The session must be in `LoggedIn` state (authenticated) before transferring files
201 |
202 | 5. **File Paths**:
203 | - Use absolute paths on the remote server
204 | - Path interpretation depends on the user's home directory and permissions
205 |
206 | ## Error Handling
207 |
208 | Always handle potential errors when transferring files:
209 |
210 | ```c#
211 | try
212 | {
213 | using var fileStream = File.OpenRead("local-file.txt");
214 | bool success = session.WriteFile("/remote/path/file.txt", fileStream);
215 |
216 | if (!success)
217 | {
218 | Console.WriteLine("File transfer incomplete!");
219 | }
220 | }
221 | catch (FileNotFoundException ex)
222 | {
223 | Console.WriteLine($"Local file not found: {ex.Message}");
224 | }
225 | catch (SshException ex)
226 | {
227 | Console.WriteLine($"SSH error during transfer: {ex.Message}");
228 | }
229 | catch (IOException ex)
230 | {
231 | Console.WriteLine($"I/O error: {ex.Message}");
232 | }
233 | ```
234 |
235 | ## Performance Tips
236 |
237 | 1. **Buffer Size**:
238 | - Default buffer size is 32KB
239 | - For large files, consider 64KB or 128KB buffers
240 | - Don't use excessively large buffers (>1MB) - diminishing returns
241 |
242 | 2. **Network Conditions**:
243 | - Larger buffers help on high-latency networks
244 | - Smaller buffers may be better on unstable connections
245 |
246 | 3. **File Size**:
247 | - SCP is efficient for single file transfers
248 | - For multiple small files, consider archiving them first (tar/zip)
249 |
250 | 4. **Async Methods**:
251 | - Use async methods to avoid blocking the UI thread
252 | - Helpful for responsive applications during large transfers
253 |
254 | ## See Also
255 |
256 | - `SshSession.ReadFile()` (SshSession.cs:555) - Download files via SCP
257 | - `SshSession.WriteFile()` (SshSession.cs:636) - Upload files via SCP
258 | - `SshSession.ReadFileAsync()` (SshSession.cs:614) - Async download
259 | - `SshSession.WriteFileAsync()` (SshSession.cs:707) - Async upload
260 | - [Authentication](authentication.md) - Authenticate before file transfers
261 | - [Session Timeouts](session-timeouts.md) - Set timeouts for large file transfers
262 | - [Session Lifecycle](session-lifecycle.md) - Understanding session states
263 | - [Error Handling](error-handling.md) - Handle file transfer errors
264 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NullOpsDevs.LibSsh
2 |
3 | A modern, cross-platform .NET library providing managed bindings for libssh2, enabling SSH operations including remote command execution, SCP file transfers, and advanced terminal (PTY) features.
4 |
5 | [](https://www.nuget.org/packages/NullOpsDevs.LibSsh/)
6 | [](LICENSE)
7 | [](https://dotnet.microsoft.com/)
8 | [](https://dotnet.microsoft.com/)
9 | [](https://dotnet.microsoft.com/)
10 | [](https://dotnet.microsoft.com/)
11 | [](https://dotnet.microsoft.com/)
12 | [](https://dotnet.microsoft.com/)
13 | [](https://libsshnet.nullops.systems/quickstart)
14 |
15 | ## Features
16 |
17 | | Category | Feature | Method(s) | Supported |
18 | |-------------------------|------------------------------------------|------------------------------------------------------|-----------|
19 | | **Authentication** | | | |
20 | | | Password authentication | `Authenticate(PasswordCredential)` | ✅ |
21 | | | Public key authentication | `Authenticate(PublicKeyCredential)` | ✅ |
22 | | | SSH agent authentication | `Authenticate(SshAgentCredential)` | ✅ |
23 | | | Host-based authentication | `Authenticate(HostBasedCredential)` | ✅ |
24 | | | Keyboard-interactive authentication | - | ❌ |
25 | | **Session Management** | | | |
26 | | | Connection | `Connect`, `ConnectAsync` | ✅ |
27 | | | Host key retrieval | `GetHostKey` | ✅ |
28 | | | Host key verification | `GetHostKeyHash` | ✅ |
29 | | | Session timeout configuration | `SetSessionTimeout`, `DisableSessionTimeout` | ✅ |
30 | | | Keepalive configuration | `ConfigureKeepAlive`, `SendKeepAlive` | ✅ |
31 | | | Method preference configuration | `SetMethodPreferences` | ✅ |
32 | | | Secure default algorithms | `SetSecureMethodPreferences` | ✅ |
33 | | | Negotiated method inspection | `GetNegotiatedMethod` | ✅ |
34 | | **File Transfer (SCP)** | | | |
35 | | | File upload | `WriteFile`, `WriteFileAsync` | ✅ |
36 | | | File download | `ReadFile`, `ReadFileAsync` | ✅ |
37 | | **Command Execution** | | | |
38 | | | One-shot command execution | `ExecuteCommand`, `ExecuteCommandAsync` | ✅ |
39 | | | Exit code retrieval | `SshCommandResult.ExitCode` | ✅ |
40 | | | Exit signal retrieval | `SshCommandResult.ExitSignal` | ✅ |
41 | | | stdout/stderr separation | `SshCommandResult.Stdout`, `SshCommandResult.Stderr` | ✅ |
42 | | **Terminal (PTY)** | | | |
43 | | | PTY allocation | `CommandExecutionOptions.RequestPty` | ✅ |
44 | | | Terminal type selection | `CommandExecutionOptions.TerminalType` | ✅ |
45 | | | Terminal modes | `CommandExecutionOptions.TerminalModes` | ✅ |
46 | | | Window size configuration | `CommandExecutionOptions.TerminalWidth/Height` | ✅ |
47 | | | Interactive shell mode | - | ❌ |
48 | | **Error Handling** | | | |
49 | | | Typed exceptions | `SshException` | ✅ |
50 | | | Detailed error messages | `SshException.Message` | ✅ |
51 | | | 60+ error code mappings | `SshError` enum | ✅ |
52 | | **Advanced Features** | | | |
53 | | | Host key type detection | `SshHostKey.Type` | ✅ |
54 | | | Microsoft.Extensions.Logging integration | Constructor `ILogger` parameter | ✅ |
55 | | | Cross-platform native binaries | Bundled in NuGet package | ✅ |
56 | | | Global library cleanup | `LibSsh2.Exit()` | ✅ |
57 | | **Thread Safety** | | | |
58 | | | `SshSession` is *NOT* thread-safe. | - | ❌ |
59 | | | Multiple `SshSession`s are supported. | - | ✅ |
60 |
61 |
62 | ## Installation
63 |
64 | Install via NuGet Package Manager:
65 |
66 | ```bash
67 | dotnet add package NullOpsDevs.LibSsh
68 | ```
69 |
70 | Or via Package Manager Console:
71 |
72 | ```powershell
73 | Install-Package NullOpsDevs.LibSsh
74 | ```
75 |
76 | ### Supported Platforms
77 |
78 | - .NET 9.0+
79 | - Windows (x64)
80 | - Linux (x64, ARM64)
81 | - macOS (x64, ARM64/Apple Silicon)
82 |
83 | ## Quick Start
84 |
85 | Quickstart guide is available [here](https://libsshnet.nullops.systems/quickstart).
86 |
87 | ## Building from Source
88 |
89 | ### Prerequisites
90 |
91 | - .NET 9.0 SDK or later
92 | - Git
93 |
94 | ### Build Steps
95 |
96 | ```bash
97 | # Clone the repository
98 | git clone https://github.com/NullOpsDevs/LibSshNet.git
99 | cd LibSshNet
100 |
101 | # Restore dependencies and build
102 | dotnet restore
103 | dotnet build
104 |
105 | # Run tests
106 | cd NullOpsDevs.LibSsh.Test
107 | docker compose up -d
108 | dotnet run
109 | ```
110 |
111 | ## Architecture
112 |
113 | The library consists of three layers:
114 |
115 | 1. **Managed Layer**: Clean, idiomatic C# API with async/await support (not true async)
116 | 2. **Interop Layer**: P/Invoke bindings to libssh2 native library
117 | 3. **Native Layer**: Pre-compiled libssh2 binaries for all supported platforms
118 |
119 | All native dependencies are bundled in the NuGet package for zero-configuration deployment.
120 |
121 | ## License
122 |
123 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
124 |
125 | ## Contributing
126 |
127 | Contributions are welcome! Please feel free to submit a Pull Request.
128 |
129 | ## Acknowledgments
130 |
131 | This library uses [libssh2](https://www.libssh2.org/), a client-side C library implementing the SSH2 protocol.
132 |
133 | libssh2 is licensed under [BSD License](https://libssh2.org/license.html).
134 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/host-key-retrieval-and-verification.md:
--------------------------------------------------------------------------------
1 | # Host Key Retrieval and Verification
2 |
3 | Host key verification is a critical security mechanism in SSH that prevents man-in-the-middle (MITM) attacks. When you connect to an SSH server, the server presents its host key to prove its identity. You should verify this key matches what you expect before proceeding with authentication.
4 |
5 | ## Why Host Key Verification Matters
6 |
7 | Without host key verification, an attacker could intercept your connection and impersonate the legitimate server. This would allow them to:
8 | - Capture your authentication credentials
9 | - Monitor all data transmitted during the session
10 | - Manipulate commands and responses
11 |
12 | Traditional SSH clients (like OpenSSH) maintain a `known_hosts` file that stores fingerprints of previously-seen servers and warn users when keys change. NullOpsDevs.LibSsh provides the APIs to implement similar verification in your applications.
13 |
14 | ## Basic Host Key Retrieval
15 |
16 | After connecting to a server (but before authenticating), you can retrieve the server's host key:
17 |
18 | ```c#
19 | using NullOpsDevs.LibSsh;
20 | using NullOpsDevs.LibSsh.Core;
21 |
22 | var session = new SshSession();
23 | session.Connect("example.com", 22);
24 |
25 | // Retrieve the host key
26 | SshHostKey hostKey = session.GetHostKey();
27 |
28 | Console.WriteLine($"Host key type: {hostKey.Type}");
29 | Console.WriteLine($"Host key length: {hostKey.Key.Length} bytes");
30 | ```
31 |
32 | The `SshHostKey` structure contains:
33 | - `Key` - The raw host key data as a byte array
34 | - `Type` - The key algorithm type (see [Host Key Types](#host-key-types))
35 |
36 | ## Host Key Types
37 |
38 | The server's host key can use one of several cryptographic algorithms:
39 |
40 | | Type | Description | Recommended |
41 | |------|-------------|-------------|
42 | | `Ed25519` | Ed25519 elliptic curve signature algorithm | ✅ **Recommended** for new deployments |
43 | | `Ecdsa256` | ECDSA with NIST P-256 curve | ✅ Good choice |
44 | | `Ecdsa384` | ECDSA with NIST P-384 curve | ✅ Good choice |
45 | | `Ecdsa521` | ECDSA with NIST P-521 curve | ✅ Good choice |
46 | | `Rsa` | RSA public key algorithm | ⚠️ Acceptable with sufficient key size (2048+ bits) |
47 | | `Dss` | DSA algorithm (deprecated) | ❌ **Avoid** - deprecated and insecure |
48 |
49 | ```c#
50 | SshHostKey hostKey = session.GetHostKey();
51 |
52 | switch (hostKey.Type)
53 | {
54 | case SshHostKeyType.Ed25519:
55 | Console.WriteLine("Server uses Ed25519 (recommended)");
56 | break;
57 | case SshHostKeyType.Rsa:
58 | Console.WriteLine("Server uses RSA (ensure key is 2048+ bits)");
59 | break;
60 | case SshHostKeyType.Dss:
61 | Console.WriteLine("WARNING: Server uses deprecated DSA keys!");
62 | break;
63 | // ... handle other types
64 | }
65 | ```
66 |
67 | ## Computing Host Key Fingerprints
68 |
69 | To make host keys human-readable and easier to verify, you can compute cryptographic fingerprints (hashes) of the key:
70 |
71 | ```c#
72 | var session = new SshSession();
73 | session.Connect("example.com", 22);
74 |
75 | // Get fingerprints using different hash algorithms
76 | byte[] sha256Hash = session.GetHostKeyHash(SshHashType.SHA256);
77 | byte[] sha1Hash = session.GetHostKeyHash(SshHashType.SHA1);
78 | byte[] md5Hash = session.GetHostKeyHash(SshHashType.MD5);
79 |
80 | Console.WriteLine($"SHA256: {ConvertToFingerprint(sha256Hash, "SHA256")}");
81 | Console.WriteLine($"SHA1: {ConvertToFingerprint(sha1Hash, "SHA1")}");
82 | Console.WriteLine($"MD5: {ConvertToFingerprint(md5Hash, "MD5")}");
83 |
84 | // Helper method to format fingerprints
85 | static string ConvertToFingerprint(byte[] hash, string algorithm)
86 | {
87 | string hex = BitConverter.ToString(hash).Replace("-", ":");
88 | return $"{algorithm}:{hex}";
89 | }
90 | ```
91 |
92 | ### Hash Algorithm Comparison
93 |
94 | | Algorithm | Output Size | Security | Use Case |
95 | |-----------|-------------|----------|----------|
96 | | `SHA256` | 32 bytes (256 bits) | ✅ Strong | **Recommended** for new implementations |
97 | | `SHA1` | 20 bytes (160 bits) | ⚠️ Weak | Legacy compatibility only |
98 | | `MD5` | 16 bytes (128 bits) | ❌ Broken | Legacy compatibility only |
99 |
100 | > **Best Practice:** Always use SHA-256 for new implementations. MD5 and SHA-1 are cryptographically weak but may be needed for compatibility with older systems.
101 |
102 | ## Retrieving Negotiated Algorithms
103 |
104 | After connection, you can inspect which algorithms were negotiated for various SSH protocol operations:
105 |
106 | ```c#
107 | var session = new SshSession();
108 | session.Connect("example.com", 22);
109 |
110 | // Check negotiated algorithms
111 | string? kexAlgorithm = session.GetNegotiatedMethod(SshMethod.Kex);
112 | string? hostKeyAlgorithm = session.GetNegotiatedMethod(SshMethod.HostKey);
113 | string? cipherCs = session.GetNegotiatedMethod(SshMethod.CryptCs);
114 | string? macCs = session.GetNegotiatedMethod(SshMethod.MacCs);
115 |
116 | Console.WriteLine($"Key Exchange: {kexAlgorithm}");
117 | Console.WriteLine($"Host Key: {hostKeyAlgorithm}");
118 | Console.WriteLine($"Cipher (Client→Server): {cipherCs}");
119 | Console.WriteLine($"MAC (Client→Server): {macCs}");
120 | ```
121 |
122 | This is useful for:
123 | - Debugging connection issues
124 | - Auditing security configurations
125 | - Ensuring strong algorithms are being used
126 |
127 | ## Advanced: Configuring Accepted Host Key Types
128 |
129 | You can restrict which host key types your client will accept by setting method preferences before connecting:
130 |
131 | ```c#
132 | var session = new SshSession();
133 |
134 | // Only accept Ed25519 and ECDSA host keys (reject RSA and DSA)
135 | session.SetMethodPreferences(
136 | SshMethod.HostKey,
137 | "ssh-ed25519,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256"
138 | );
139 |
140 | session.Connect("example.com", 22);
141 | ```
142 |
143 | Or use the secure defaults which already prefer modern algorithms:
144 |
145 | ```c#
146 | var session = new SshSession();
147 |
148 | // Apply secure algorithm preferences for all methods
149 | session.SetSecureMethodPreferences();
150 |
151 | session.Connect("example.com", 22);
152 | ```
153 |
154 | The secure defaults prefer:
155 | - **Host Keys:** Ed25519 → ECDSA (521/384/256) → RSA-SHA2 (no DSA or legacy RSA)
156 | - **Key Exchange:** Curve25519 → ECDH with NIST curves → DH group exchange
157 | - **Ciphers:** ChaCha20-Poly1305 → AES-GCM → AES-CTR
158 | - **MACs:** HMAC-SHA2 with encrypt-then-MAC
159 |
160 | See `SshSession.cs:240-249` for the complete list of secure defaults.
161 |
162 | ## Complete Secure Connection Example
163 |
164 | Here's a complete example that combines host key verification with authentication:
165 |
166 | ```c#
167 | using NullOpsDevs.LibSsh;
168 | using NullOpsDevs.LibSsh.Core;
169 | using NullOpsDevs.LibSsh.Credentials;
170 |
171 | var session = new SshSession();
172 |
173 | try
174 | {
175 | // 1. Apply secure algorithm preferences
176 | session.SetSecureMethodPreferences();
177 |
178 | // 2. Connect to the server
179 | session.Connect("example.com", 22);
180 |
181 | // 3. Verify host key
182 | byte[] hostKeyHash = session.GetHostKeyHash(SshHashType.SHA256);
183 | string fingerprint = Convert.ToBase64String(hostKeyHash);
184 |
185 | Console.WriteLine($"Server fingerprint: SHA256:{fingerprint}");
186 | Console.WriteLine("Verify this matches your known fingerprint!");
187 |
188 | // In production, compare against a known-good value
189 | // if (fingerprint != expectedFingerprint) { throw ... }
190 |
191 | // 4. Authenticate
192 | var credential = SshCredential.FromPublicKeyFile(
193 | "username",
194 | "~/.ssh/id_ed25519.pub",
195 | "~/.ssh/id_ed25519"
196 | );
197 |
198 | if (!session.Authenticate(credential))
199 | {
200 | Console.WriteLine("Authentication failed!");
201 | return;
202 | }
203 |
204 | // 5. Execute commands securely
205 | var result = session.ExecuteCommand("whoami");
206 | Console.WriteLine($"Logged in as: {result.Stdout.Trim()}");
207 | }
208 | finally
209 | {
210 | session.Dispose();
211 | }
212 | ```
213 |
214 | ## Security Recommendations
215 |
216 | 1. **Always verify host keys** before authenticating - never blindly trust server identities
217 | 2. **Use SHA-256 fingerprints** for new implementations (avoid MD5 and SHA-1)
218 | 3. **Implement key pinning** for production systems (pre-configure known fingerprints)
219 | 4. **Monitor for key changes** - a changed host key may indicate an attack or server redeployment
220 | 5. **Use secure algorithm preferences** to reject weak cryptographic algorithms
221 | 6. **Log verification failures** for security auditing and incident response
222 | 7. **Educate users** about the importance of verifying fingerprints when prompted
223 |
224 | ## See Also
225 |
226 | - [Authentication](authentication.md) - Learn about authentication methods after host key verification
227 | - [Algorithm and Method Preferences](algorithm-preferences.md) - Configure accepted algorithms
228 | - [Session Lifecycle](session-lifecycle.md) - When to verify host keys
229 | - [Error Handling](error-handling.md) - Handle host key verification errors
230 | - [Quickstart](quickstart.md) - Complete connection examples
231 | - `SshSession.GetHostKey()` (SshSession.cs:156) - Retrieve the raw host key
232 | - `SshSession.GetHostKeyHash()` (SshSession.cs:347) - Compute host key fingerprints
233 | - `SshSession.SetSecureMethodPreferences()` (SshSession.cs:240) - Configure secure algorithm defaults
234 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | # dotenv files
7 | .env
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # Tye
69 | .tye/
70 |
71 | # ASP.NET Scaffolding
72 | ScaffoldingReadMe.txt
73 |
74 | # StyleCop
75 | StyleCopReport.xml
76 |
77 | # Files built by Visual Studio
78 | *_i.c
79 | *_p.c
80 | *_h.h
81 | *.ilk
82 | *.meta
83 | *.obj
84 | *.iobj
85 | *.pch
86 | *.pdb
87 | *.ipdb
88 | *.pgc
89 | *.pgd
90 | *.rsp
91 | *.sbr
92 | *.tlb
93 | *.tli
94 | *.tlh
95 | *.tmp
96 | *.tmp_proj
97 | *_wpftmp.csproj
98 | *.log
99 | *.tlog
100 | *.vspscc
101 | *.vssscc
102 | .builds
103 | *.pidb
104 | *.svclog
105 | *.scc
106 |
107 | # Chutzpah Test files
108 | _Chutzpah*
109 |
110 | # Visual C++ cache files
111 | ipch/
112 | *.aps
113 | *.ncb
114 | *.opendb
115 | *.opensdf
116 | *.sdf
117 | *.cachefile
118 | *.VC.db
119 | *.VC.VC.opendb
120 |
121 | # Visual Studio profiler
122 | *.psess
123 | *.vsp
124 | *.vspx
125 | *.sap
126 |
127 | # Visual Studio Trace Files
128 | *.e2e
129 |
130 | # TFS 2012 Local Workspace
131 | $tf/
132 |
133 | # Guidance Automation Toolkit
134 | *.gpState
135 |
136 | # ReSharper is a .NET coding add-in
137 | _ReSharper*/
138 | *.[Rr]e[Ss]harper
139 | *.DotSettings.user
140 |
141 | # TeamCity is a build add-in
142 | _TeamCity*
143 |
144 | # DotCover is a Code Coverage Tool
145 | *.dotCover
146 |
147 | # AxoCover is a Code Coverage Tool
148 | .axoCover/*
149 | !.axoCover/settings.json
150 |
151 | # Coverlet is a free, cross platform Code Coverage Tool
152 | coverage*.json
153 | coverage*.xml
154 | coverage*.info
155 |
156 | # Visual Studio code coverage results
157 | *.coverage
158 | *.coveragexml
159 |
160 | # NCrunch
161 | _NCrunch_*
162 | .*crunch*.local.xml
163 | nCrunchTemp_*
164 |
165 | # MightyMoose
166 | *.mm.*
167 | AutoTest.Net/
168 |
169 | # Web workbench (sass)
170 | .sass-cache/
171 |
172 | # Installshield output folder
173 | [Ee]xpress/
174 |
175 | # DocProject is a documentation generator add-in
176 | DocProject/buildhelp/
177 | DocProject/Help/*.HxT
178 | DocProject/Help/*.HxC
179 | DocProject/Help/*.hhc
180 | DocProject/Help/*.hhk
181 | DocProject/Help/*.hhp
182 | DocProject/Help/Html2
183 | DocProject/Help/html
184 |
185 | # Click-Once directory
186 | publish/
187 |
188 | # Publish Web Output
189 | *.[Pp]ublish.xml
190 | *.azurePubxml
191 | # Note: Comment the next line if you want to checkin your web deploy settings,
192 | # but database connection strings (with potential passwords) will be unencrypted
193 | *.pubxml
194 | *.publishproj
195 |
196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
197 | # checkin your Azure Web App publish settings, but sensitive information contained
198 | # in these scripts will be unencrypted
199 | PublishScripts/
200 |
201 | # NuGet Packages
202 | *.nupkg
203 | # NuGet Symbol Packages
204 | *.snupkg
205 | # The packages folder can be ignored because of Package Restore
206 | **/[Pp]ackages/*
207 | # except build/, which is used as an MSBuild target.
208 | !**/[Pp]ackages/build/
209 | # Uncomment if necessary however generally it will be regenerated when needed
210 | #!**/[Pp]ackages/repositories.config
211 | # NuGet v3's project.json files produces more ignorable files
212 | *.nuget.props
213 | *.nuget.targets
214 |
215 | # Microsoft Azure Build Output
216 | csx/
217 | *.build.csdef
218 |
219 | # Microsoft Azure Emulator
220 | ecf/
221 | rcf/
222 |
223 | # Windows Store app package directories and files
224 | AppPackages/
225 | BundleArtifacts/
226 | Package.StoreAssociation.xml
227 | _pkginfo.txt
228 | *.appx
229 | *.appxbundle
230 | *.appxupload
231 |
232 | # Visual Studio cache files
233 | # files ending in .cache can be ignored
234 | *.[Cc]ache
235 | # but keep track of directories ending in .cache
236 | !?*.[Cc]ache/
237 |
238 | # Others
239 | ClientBin/
240 | ~$*
241 | *~
242 | *.dbmdl
243 | *.dbproj.schemaview
244 | *.jfm
245 | *.pfx
246 | *.publishsettings
247 | orleans.codegen.cs
248 |
249 | # Including strong name files can present a security risk
250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251 | #*.snk
252 |
253 | # Since there are multiple workflows, uncomment next line to ignore bower_components
254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255 | #bower_components/
256 |
257 | # RIA/Silverlight projects
258 | Generated_Code/
259 |
260 | # Backup & report files from converting an old project file
261 | # to a newer Visual Studio version. Backup files are not needed,
262 | # because we have git ;-)
263 | _UpgradeReport_Files/
264 | Backup*/
265 | UpgradeLog*.XML
266 | UpgradeLog*.htm
267 | ServiceFabricBackup/
268 | *.rptproj.bak
269 |
270 | # SQL Server files
271 | *.mdf
272 | *.ldf
273 | *.ndf
274 |
275 | # Business Intelligence projects
276 | *.rdl.data
277 | *.bim.layout
278 | *.bim_*.settings
279 | *.rptproj.rsuser
280 | *- [Bb]ackup.rdl
281 | *- [Bb]ackup ([0-9]).rdl
282 | *- [Bb]ackup ([0-9][0-9]).rdl
283 |
284 | # Microsoft Fakes
285 | FakesAssemblies/
286 |
287 | # GhostDoc plugin setting file
288 | *.GhostDoc.xml
289 |
290 | # Node.js Tools for Visual Studio
291 | .ntvs_analysis.dat
292 | node_modules/
293 |
294 | # Visual Studio 6 build log
295 | *.plg
296 |
297 | # Visual Studio 6 workspace options file
298 | *.opt
299 |
300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301 | *.vbw
302 |
303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304 | *.vbp
305 |
306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307 | *.dsw
308 | *.dsp
309 |
310 | # Visual Studio 6 technical files
311 | *.ncb
312 | *.aps
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # CodeRush personal settings
330 | .cr/personal
331 |
332 | # Python Tools for Visual Studio (PTVS)
333 | __pycache__/
334 | *.pyc
335 |
336 | # Cake - Uncomment if you are using it
337 | # tools/**
338 | # !tools/packages.config
339 |
340 | # Tabs Studio
341 | *.tss
342 |
343 | # Telerik's JustMock configuration file
344 | *.jmconfig
345 |
346 | # BizTalk build output
347 | *.btp.cs
348 | *.btm.cs
349 | *.odx.cs
350 | *.xsd.cs
351 |
352 | # OpenCover UI analysis results
353 | OpenCover/
354 |
355 | # Azure Stream Analytics local run output
356 | ASALocalRun/
357 |
358 | # MSBuild Binary and Structured Log
359 | *.binlog
360 |
361 | # NVidia Nsight GPU debugger configuration file
362 | *.nvuser
363 |
364 | # MFractors (Xamarin productivity tool) working folder
365 | .mfractor/
366 |
367 | # Local History for Visual Studio
368 | .localhistory/
369 |
370 | # Visual Studio History (VSHistory) files
371 | .vshistory/
372 |
373 | # BeatPulse healthcheck temp database
374 | healthchecksdb
375 |
376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
377 | MigrationBackup/
378 |
379 | # Ionide (cross platform F# VS Code tools) working folder
380 | .ionide/
381 |
382 | # Fody - auto-generated XML schema
383 | FodyWeavers.xsd
384 |
385 | # VS Code files for those working on multiple tools
386 | .vscode/*
387 | !.vscode/settings.json
388 | !.vscode/tasks.json
389 | !.vscode/launch.json
390 | !.vscode/extensions.json
391 | *.code-workspace
392 |
393 | # Local History for Visual Studio Code
394 | .history/
395 |
396 | # Windows Installer files from build outputs
397 | *.cab
398 | *.msi
399 | *.msix
400 | *.msm
401 | *.msp
402 |
403 | # JetBrains Rider
404 | *.sln.iml
405 | .idea/
406 |
407 | ##
408 | ## Visual studio for Mac
409 | ##
410 |
411 |
412 | # globs
413 | Makefile.in
414 | *.userprefs
415 | *.usertasks
416 | config.make
417 | config.status
418 | aclocal.m4
419 | install-sh
420 | autom4te.cache/
421 | *.tar.gz
422 | tarballs/
423 | test-results/
424 |
425 | # Mac bundle stuff
426 | *.dmg
427 | *.app
428 |
429 | # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
430 | # General
431 | .DS_Store
432 | .AppleDouble
433 | .LSOverride
434 |
435 | # Icon must end with two \r
436 | Icon
437 |
438 |
439 | # Thumbnails
440 | ._*
441 |
442 | # Files that might appear in the root of a volume
443 | .DocumentRevisions-V100
444 | .fseventsd
445 | .Spotlight-V100
446 | .TemporaryItems
447 | .Trashes
448 | .VolumeIcon.icns
449 | .com.apple.timemachine.donotpresent
450 |
451 | # Directories potentially created on remote AFP share
452 | .AppleDB
453 | .AppleDesktop
454 | Network Trash Folder
455 | Temporary Items
456 | .apdisk
457 |
458 | # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
459 | # Windows thumbnail cache files
460 | Thumbs.db
461 | ehthumbs.db
462 | ehthumbs_vista.db
463 |
464 | # Dump file
465 | *.stackdump
466 |
467 | # Folder config file
468 | [Dd]esktop.ini
469 |
470 | # Recycle Bin used on file shares
471 | $RECYCLE.BIN/
472 |
473 | # Windows Installer files
474 | *.cab
475 | *.msi
476 | *.msix
477 | *.msm
478 | *.msp
479 |
480 | # Windows shortcuts
481 | *.lnk
482 |
483 | # Vim temporary swap files
484 | *.swp
485 |
--------------------------------------------------------------------------------
/src/NullOpsDevs.LibSsh/Core/SshError.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using JetBrains.Annotations;
3 | using NullOpsDevs.LibSsh.Generated;
4 |
5 | namespace NullOpsDevs.LibSsh.Core;
6 |
7 | ///
8 | /// Represents SSH error codes from libssh2 and custom error codes.
9 | ///
10 | [PublicAPI]
11 | [SuppressMessage("Design", "CA1069:Enums values should not be duplicated")]
12 | public enum SshError
13 | {
14 | ///
15 | /// No error occurred; the operation completed successfully.
16 | ///
17 | None = LibSshNative.LIBSSH2_ERROR_NONE,
18 |
19 | ///
20 | /// No socket has been established for the SSH session.
21 | ///
22 | SocketNone = LibSshNative.LIBSSH2_ERROR_SOCKET_NONE,
23 |
24 | ///
25 | /// Failed to receive the SSH protocol banner from the server.
26 | ///
27 | BannerRecv = LibSshNative.LIBSSH2_ERROR_BANNER_RECV,
28 |
29 | ///
30 | /// Failed to send the SSH protocol banner to the server.
31 | ///
32 | BannerSend = LibSshNative.LIBSSH2_ERROR_BANNER_SEND,
33 |
34 | ///
35 | /// No valid SSH banner was exchanged during the connection handshake.
36 | ///
37 | BannerNone = LibSshNative.LIBSSH2_ERROR_BANNER_NONE,
38 |
39 | ///
40 | /// Message authentication code (MAC) verification failed; data may be corrupted or tampered with.
41 | ///
42 | InvalidMac = LibSshNative.LIBSSH2_ERROR_INVALID_MAC,
43 |
44 | ///
45 | /// SSH key exchange (KEX) negotiation failed.
46 | ///
47 | KexFailure = LibSshNative.LIBSSH2_ERROR_KEX_FAILURE,
48 |
49 | ///
50 | /// Memory allocation failed.
51 | ///
52 | Alloc = LibSshNative.LIBSSH2_ERROR_ALLOC,
53 |
54 | ///
55 | /// Failed to send data over the socket.
56 | ///
57 | SocketSend = LibSshNative.LIBSSH2_ERROR_SOCKET_SEND,
58 |
59 | ///
60 | /// SSH key exchange process failed.
61 | ///
62 | KeyExchangeFailure = LibSshNative.LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE,
63 |
64 | ///
65 | /// Operation timed out.
66 | ///
67 | Timeout = LibSshNative.LIBSSH2_ERROR_TIMEOUT,
68 |
69 | ///
70 | /// Failed to initialize the host key during key exchange.
71 | ///
72 | HostkeyInit = LibSshNative.LIBSSH2_ERROR_HOSTKEY_INIT,
73 |
74 | ///
75 | /// Failed to sign data with the host key.
76 | ///
77 | HostkeySign = LibSshNative.LIBSSH2_ERROR_HOSTKEY_SIGN,
78 |
79 | ///
80 | /// Failed to decrypt incoming data.
81 | ///
82 | Decrypt = LibSshNative.LIBSSH2_ERROR_DECRYPT,
83 |
84 | ///
85 | /// The socket was disconnected during operation.
86 | ///
87 | SocketDisconnect = LibSshNative.LIBSSH2_ERROR_SOCKET_DISCONNECT,
88 |
89 | ///
90 | /// SSH protocol error occurred.
91 | ///
92 | Proto = LibSshNative.LIBSSH2_ERROR_PROTO,
93 |
94 | ///
95 | /// The user's password has expired and must be changed.
96 | ///
97 | PasswordExpired = LibSshNative.LIBSSH2_ERROR_PASSWORD_EXPIRED,
98 |
99 | ///
100 | /// File operation failed.
101 | ///
102 | File = LibSshNative.LIBSSH2_ERROR_FILE,
103 |
104 | ///
105 | /// No suitable authentication method is available.
106 | ///
107 | MethodNone = LibSshNative.LIBSSH2_ERROR_METHOD_NONE,
108 |
109 | ///
110 | /// Authentication failed; invalid credentials or method not accepted by server.
111 | ///
112 | AuthenticationFailed = LibSshNative.LIBSSH2_ERROR_AUTHENTICATION_FAILED,
113 |
114 | ///
115 | /// Public key authentication failed; the provided public key was not verified by the server.
116 | ///
117 | PublickeyUnverified = LibSshNative.LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED,
118 |
119 | ///
120 | /// SSH channel packets received out of order.
121 | ///
122 | ChannelOutoforder = LibSshNative.LIBSSH2_ERROR_CHANNEL_OUTOFORDER,
123 |
124 | ///
125 | /// General SSH channel failure.
126 | ///
127 | ChannelFailure = LibSshNative.LIBSSH2_ERROR_CHANNEL_FAILURE,
128 |
129 | ///
130 | /// SSH channel request was denied by the server.
131 | ///
132 | ChannelRequestDenied = LibSshNative.LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED,
133 |
134 | ///
135 | /// Received data for an unknown or invalid SSH channel.
136 | ///
137 | ChannelUnknown = LibSshNative.LIBSSH2_ERROR_CHANNEL_UNKNOWN,
138 |
139 | ///
140 | /// SSH channel window size was exceeded.
141 | ///
142 | ChannelWindowExceeded = LibSshNative.LIBSSH2_ERROR_CHANNEL_WINDOW_EXCEEDED,
143 |
144 | ///
145 | /// SSH channel packet size was exceeded.
146 | ///
147 | ChannelPacketExceeded = LibSshNative.LIBSSH2_ERROR_CHANNEL_PACKET_EXCEEDED,
148 |
149 | ///
150 | /// SSH channel has been closed.
151 | ///
152 | ChannelClosed = LibSshNative.LIBSSH2_ERROR_CHANNEL_CLOSED,
153 |
154 | ///
155 | /// End-of-file (EOF) has already been sent on this SSH channel.
156 | ///
157 | ChannelEofSent = LibSshNative.LIBSSH2_ERROR_CHANNEL_EOF_SENT,
158 |
159 | ///
160 | /// SCP protocol error occurred during file transfer.
161 | ///
162 | ScpProtocol = LibSshNative.LIBSSH2_ERROR_SCP_PROTOCOL,
163 |
164 | ///
165 | /// Compression or decompression error (zlib).
166 | ///
167 | Zlib = LibSshNative.LIBSSH2_ERROR_ZLIB,
168 |
169 | ///
170 | /// Socket operation timed out.
171 | ///
172 | SocketTimeout = LibSshNative.LIBSSH2_ERROR_SOCKET_TIMEOUT,
173 |
174 | ///
175 | /// SFTP protocol error occurred.
176 | ///
177 | SftpProtocol = LibSshNative.LIBSSH2_ERROR_SFTP_PROTOCOL,
178 |
179 | ///
180 | /// Request was denied by the server.
181 | ///
182 | RequestDenied = LibSshNative.LIBSSH2_ERROR_REQUEST_DENIED,
183 |
184 | ///
185 | /// The requested method or algorithm is not supported by the server.
186 | ///
187 | MethodNotSupported = LibSshNative.LIBSSH2_ERROR_METHOD_NOT_SUPPORTED,
188 |
189 | ///
190 | /// Invalid argument or parameter provided.
191 | ///
192 | Inval = LibSshNative.LIBSSH2_ERROR_INVAL,
193 |
194 | ///
195 | /// Invalid polling type specified for non-blocking operations.
196 | ///
197 | InvalidPollType = LibSshNative.LIBSSH2_ERROR_INVALID_POLL_TYPE,
198 |
199 | ///
200 | /// Public key subsystem protocol error.
201 | ///
202 | PublickeyProtocol = LibSshNative.LIBSSH2_ERROR_PUBLICKEY_PROTOCOL,
203 |
204 | ///
205 | /// Operation would block; try again later (non-blocking mode only).
206 | ///
207 | Eagain = LibSshNative.LIBSSH2_ERROR_EAGAIN,
208 |
209 | ///
210 | /// The provided buffer is too small to hold the requested data.
211 | ///
212 | BufferTooSmall = LibSshNative.LIBSSH2_ERROR_BUFFER_TOO_SMALL,
213 |
214 | ///
215 | /// Incorrect usage of the libssh2 API; check function parameters and state requirements.
216 | ///
217 | BadUse = LibSshNative.LIBSSH2_ERROR_BAD_USE,
218 |
219 | ///
220 | /// Data compression failed.
221 | ///
222 | Compress = LibSshNative.LIBSSH2_ERROR_COMPRESS,
223 |
224 | ///
225 | /// An out-of-boundary access was attempted.
226 | ///
227 | OutOfBoundary = LibSshNative.LIBSSH2_ERROR_OUT_OF_BOUNDARY,
228 |
229 | ///
230 | /// SSH agent protocol error occurred.
231 | ///
232 | AgentProtocol = LibSshNative.LIBSSH2_ERROR_AGENT_PROTOCOL,
233 |
234 | ///
235 | /// Failed to receive data over the socket.
236 | ///
237 | SocketRecv = LibSshNative.LIBSSH2_ERROR_SOCKET_RECV,
238 |
239 | ///
240 | /// Failed to encrypt outgoing data.
241 | ///
242 | Encrypt = LibSshNative.LIBSSH2_ERROR_ENCRYPT,
243 |
244 | ///
245 | /// Invalid or bad socket descriptor.
246 | ///
247 | BadSocket = LibSshNative.LIBSSH2_ERROR_BAD_SOCKET,
248 |
249 | ///
250 | /// Known hosts file operation failed.
251 | ///
252 | KnownHosts = LibSshNative.LIBSSH2_ERROR_KNOWN_HOSTS,
253 |
254 | ///
255 | /// SSH channel window is full; cannot send more data until window is adjusted.
256 | ///
257 | ChannelWindowFull = LibSshNative.LIBSSH2_ERROR_CHANNEL_WINDOW_FULL,
258 |
259 | ///
260 | /// Authentication using a key file failed.
261 | ///
262 | KeyfileAuthFailed = LibSshNative.LIBSSH2_ERROR_KEYFILE_AUTH_FAILED,
263 |
264 | ///
265 | /// Random number generation failed.
266 | ///
267 | Randgen = LibSshNative.LIBSSH2_ERROR_RANDGEN,
268 |
269 | ///
270 | /// Expected user authentication banner was not received from the server.
271 | ///
272 | MissingUserauthBanner = LibSshNative.LIBSSH2_ERROR_MISSING_USERAUTH_BANNER,
273 |
274 | ///
275 | /// The requested algorithm is not supported.
276 | ///
277 | AlgoUnsupported = LibSshNative.LIBSSH2_ERROR_ALGO_UNSUPPORTED,
278 |
279 | ///
280 | /// Message authentication code (MAC) operation failed.
281 | ///
282 | MacFailure = LibSshNative.LIBSSH2_ERROR_MAC_FAILURE,
283 |
284 | ///
285 | /// Hash function initialization failed.
286 | ///
287 | HashInit = LibSshNative.LIBSSH2_ERROR_HASH_INIT,
288 |
289 | ///
290 | /// Hash calculation failed.
291 | ///
292 | HashCalc = LibSshNative.LIBSSH2_ERROR_HASH_CALC,
293 |
294 | ///
295 | /// Custom error: Failed to initialize the SSH session.
296 | ///
297 | FailedToInitializeSession = int.MaxValue - 2,
298 |
299 | ///
300 | /// Custom error: An inner exception was thrown; check the exception's InnerException property for details.
301 | ///
302 | InnerException = int.MaxValue - 1,
303 |
304 | ///
305 | /// Custom error: Library was used incorrectly by the developer; review API usage and session state.
306 | ///
307 | DevWrongUse = int.MaxValue
308 | }
309 |
--------------------------------------------------------------------------------
/docs/Writerside/topics/command-execution.md:
--------------------------------------------------------------------------------
1 | # Command Execution
2 |
3 | Execute commands on remote SSH servers and retrieve their output, exit codes, and error messages. NullOpsDevs.LibSsh makes it simple to run commands remotely and process their results.
4 |
5 | ## Basic Command Execution
6 |
7 | Execute a simple command and get the result:
8 |
9 | ```c#
10 | using NullOpsDevs.LibSsh;
11 | using NullOpsDevs.LibSsh.Credentials;
12 |
13 | var session = new SshSession();
14 | session.Connect("example.com", 22);
15 | session.Authenticate(SshCredential.FromPassword("user", "password"));
16 |
17 | // Execute a command
18 | var result = session.ExecuteCommand("ls -la /home/user");
19 |
20 | Console.WriteLine("Output:");
21 | Console.WriteLine(result.Stdout);
22 |
23 | Console.WriteLine($"Exit code: {result.ExitCode}");
24 | ```
25 |
26 | ## Understanding Command Results
27 |
28 | The `SshCommandResult` structure contains all information about the command execution:
29 |
30 | ```c#
31 | var result = session.ExecuteCommand("whoami");
32 |
33 | // Standard output (stdout)
34 | Console.WriteLine($"Output: {result.Stdout}");
35 |
36 | // Standard error (stderr)
37 | if (!string.IsNullOrEmpty(result.Stderr))
38 | {
39 | Console.WriteLine($"Errors: {result.Stderr}");
40 | }
41 |
42 | // Exit code (0 typically means success)
43 | if (result.ExitCode == 0)
44 | {
45 | Console.WriteLine("Command succeeded!");
46 | }
47 | else
48 | {
49 | Console.WriteLine($"Command failed with exit code: {result.ExitCode}");
50 | }
51 |
52 | // Exit signal (if the command was terminated by a signal)
53 | if (result.ExitSignal != null)
54 | {
55 | Console.WriteLine($"Terminated by signal: {result.ExitSignal}");
56 | }
57 |
58 | // Overall success indicator
59 | if (result.Successful)
60 | {
61 | Console.WriteLine("Execution was successful");
62 | }
63 | ```
64 |
65 | ## Async Command Execution
66 |
67 | For non-blocking operations, use the async version:
68 |
69 | ```c#
70 | var result = await session.ExecuteCommandAsync("apt update", cancellationToken: cancellationToken);
71 | Console.WriteLine(result.Stdout);
72 | ```
73 |
74 | ## Handling Command Errors
75 |
76 | Always check the exit code to determine if a command succeeded:
77 |
78 | ```c#
79 | var result = session.ExecuteCommand("cat /nonexistent/file");
80 |
81 | if (result.ExitCode != 0)
82 | {
83 | Console.WriteLine("Command failed!");
84 | Console.WriteLine($"Exit code: {result.ExitCode}");
85 | Console.WriteLine($"Error output: {result.Stderr}");
86 | }
87 | else
88 | {
89 | Console.WriteLine(result.Stdout);
90 | }
91 | ```
92 |
93 | ## Commands Requiring PTY (Pseudo-Terminal)
94 |
95 | Some commands need a pseudo-terminal to work correctly. Enable PTY when:
96 | - Commands check for terminal presence
97 | - You need ANSI color output and terminal control codes
98 | - Programs behave differently when attached to a terminal
99 |
100 | ```c#
101 | using NullOpsDevs.LibSsh.Core;
102 |
103 | var options = new CommandExecutionOptions
104 | {
105 | RequestPty = true,
106 | TerminalType = TerminalType.Xterm256Color // Enable color support
107 | };
108 |
109 | var result = session.ExecuteCommand("ls --color=always", options);
110 | Console.WriteLine(result.Stdout); // Contains ANSI color codes
111 | ```
112 |
113 | > **Note**: PTY enables passthrough of ANSI terminal control codes (colors, cursor positioning, etc.). However, truly interactive commands that require user input (like password prompts, `vim`, interactive `sudo`) are not supported as the library cannot handle interactive terminal I/O.
114 |
115 | ### Common PTY Use Cases
116 |
117 | #### Getting Colored Output
118 |
119 | ```c#
120 | var options = new CommandExecutionOptions
121 | {
122 | RequestPty = true,
123 | TerminalType = TerminalType.Xterm256Color
124 | };
125 |
126 | var result = session.ExecuteCommand("ls --color=always", options);
127 | // Output contains ANSI color codes that can be displayed in a terminal
128 | Console.WriteLine(result.Stdout);
129 | ```
130 |
131 | #### Running Commands That Check for TTY
132 |
133 | ```c#
134 | // Some commands behave differently when they detect a terminal
135 | var options = new CommandExecutionOptions { RequestPty = true };
136 | var result = session.ExecuteCommand("./script-that-checks-tty.sh", options);
137 |
138 | if (result.ExitCode == 0)
139 | {
140 | Console.WriteLine("Script executed successfully");
141 | }
142 | ```
143 |
144 | ## Customizing Terminal Settings
145 |
146 | When requesting a PTY, you can customize the terminal:
147 |
148 | ```c#
149 | using NullOpsDevs.LibSsh.Core;
150 | using NullOpsDevs.LibSsh.Terminal;
151 |
152 | var options = new CommandExecutionOptions
153 | {
154 | RequestPty = true,
155 | TerminalType = TerminalType.Xterm256Color, // Terminal emulation type
156 | TerminalWidth = 120, // Width in characters
157 | TerminalHeight = 40, // Height in characters
158 | };
159 |
160 | var result = session.ExecuteCommand("top -b -n 1", options);
161 | Console.WriteLine(result.Stdout);
162 | ```
163 |
164 | ### Available Terminal Types
165 |
166 | | Terminal Type | Description | Use Case |
167 | |---------------|-------------|----------|
168 | | `Xterm` | Standard xterm (default) | General purpose |
169 | | `XtermColor` | xterm with color support | Basic color output |
170 | | `Xterm256Color` | xterm with 256 colors | Full color support |
171 | | `VT100` | DEC VT100 | Legacy compatibility |
172 | | `VT220` | DEC VT220 | Legacy compatibility |
173 | | `Linux` | Linux console | Linux-specific features |
174 | | `Screen` | GNU Screen multiplexer | Screen sessions |
175 |
176 | ## Running Multiple Commands
177 |
178 | ### Sequential Commands (with error handling)
179 |
180 | ```c#
181 | // Stop on first failure
182 | var result1 = session.ExecuteCommand("mkdir -p /tmp/myapp");
183 | if (result1.ExitCode != 0)
184 | {
185 | Console.WriteLine("Failed to create directory");
186 | return;
187 | }
188 |
189 | var result2 = session.ExecuteCommand("cd /tmp/myapp && touch file.txt");
190 | if (result2.ExitCode != 0)
191 | {
192 | Console.WriteLine("Failed to create file");
193 | return;
194 | }
195 |
196 | Console.WriteLine("All commands executed successfully");
197 | ```
198 |
199 | ### Using Shell Operators
200 |
201 | ```c#
202 | // Run multiple commands in a single execution (with && for conditional execution)
203 | var result = session.ExecuteCommand("cd /tmp && mkdir test && cd test && pwd");
204 | Console.WriteLine(result.Stdout); // Should print: /tmp/test
205 |
206 | // Run commands regardless of success (with ;)
207 | result = session.ExecuteCommand("command1; command2; command3");
208 |
209 | // Run command in background (with &)
210 | result = session.ExecuteCommand("long-running-task &");
211 | ```
212 |
213 | ## Long-Running Commands
214 |
215 | For commands that take a long time, consider using cancellation tokens:
216 |
217 | ```c#
218 | using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
219 |
220 | try
221 | {
222 | var result = await session.ExecuteCommandAsync(
223 | "./long-script.sh",
224 | cancellationToken: cts.Token
225 | );
226 |
227 | Console.WriteLine("Script completed!");
228 | Console.WriteLine(result.Stdout);
229 | }
230 | catch (OperationCanceledException)
231 | {
232 | Console.WriteLine("Command timed out after 5 minutes");
233 | }
234 | ```
235 |
236 | ## Advanced: Channel Settings
237 |
238 | For fine-tuning performance, adjust channel settings:
239 |
240 | ```c#
241 | var options = new CommandExecutionOptions
242 | {
243 | WindowSize = 4 * 1024 * 1024, // 4MB window (default: 2MB)
244 | PacketSize = 64 * 1024, // 64KB packets (default: 32KB)
245 | };
246 |
247 | var result = session.ExecuteCommand("cat large-file.txt", options);
248 | ```
249 |
250 | > **Note:** Only adjust these settings if you experience performance issues with large data transfers. The defaults work well for most use cases.
251 |
252 | ## Complete Example
253 |
254 | Here's a comprehensive example showing various command execution scenarios:
255 |
256 | ```c#
257 | using NullOpsDevs.LibSsh;
258 | using NullOpsDevs.LibSsh.Core;
259 | using NullOpsDevs.LibSsh.Credentials;
260 | using NullOpsDevs.LibSsh.Exceptions;
261 |
262 | var session = new SshSession();
263 |
264 | try
265 | {
266 | // Connect and authenticate
267 | session.Connect("example.com", 22);
268 | session.Authenticate(SshCredential.FromPublicKeyFile(
269 | "username",
270 | "~/.ssh/id_ed25519.pub",
271 | "~/.ssh/id_ed25519"
272 | ));
273 |
274 | // 1. Simple command
275 | Console.WriteLine("=== System Information ===");
276 | var result = session.ExecuteCommand("uname -a");
277 | Console.WriteLine(result.Stdout);
278 |
279 | // 2. Command with error checking
280 | Console.WriteLine("\n=== Disk Usage ===");
281 | result = session.ExecuteCommand("df -h /");
282 | if (result.ExitCode == 0)
283 | {
284 | Console.WriteLine(result.Stdout);
285 | }
286 | else
287 | {
288 | Console.WriteLine($"Error: {result.Stderr}");
289 | }
290 |
291 | // 3. Command requiring PTY
292 | Console.WriteLine("\n=== Running with sudo ===");
293 | var ptyOptions = new CommandExecutionOptions { RequestPty = true };
294 | result = session.ExecuteCommand("sudo ls /root", ptyOptions);
295 |
296 | if (result.ExitCode == 0)
297 | {
298 | Console.WriteLine(result.Stdout);
299 | }
300 |
301 | // 4. Multiple commands
302 | Console.WriteLine("\n=== Creating test directory ===");
303 | result = session.ExecuteCommand("mkdir -p /tmp/test && cd /tmp/test && pwd");
304 | Console.WriteLine($"Created: {result.Stdout.Trim()}");
305 |
306 | // 5. Async command
307 | Console.WriteLine("\n=== Async operation ===");
308 | result = await session.ExecuteCommandAsync("ps aux | head -n 5");
309 | Console.WriteLine(result.Stdout);
310 | }
311 | catch (SshException ex)
312 | {
313 | Console.WriteLine($"SSH error: {ex.Message}");
314 | }
315 | finally
316 | {
317 | session.Dispose();
318 | }
319 | ```
320 |
321 | ## Best Practices
322 |
323 | 1. **Always check exit codes**:
324 | - Don't rely solely on `Successful` - check `ExitCode`
325 | - Zero typically means success, non-zero means failure
326 |
327 | 2. **Use PTY when needed**:
328 | - Enable for `sudo`, interactive commands, or color output
329 | - Disable for scripting and automation (default)
330 |
331 | 3. **Handle both stdout and stderr**:
332 | - Some programs write to stderr even on success
333 | - Check both streams for complete information
334 |
335 | 4. **Quote arguments properly**:
336 | - Use shell quoting for arguments with spaces
337 | - Example: `"ls '/path with spaces/'"`
338 |
339 | 5. **Avoid command injection**:
340 | - Don't concatenate user input directly into commands
341 | - Validate and sanitize all user-provided data
342 |
343 | 6. **Set appropriate timeouts**:
344 | - Use cancellation tokens for long-running commands
345 | - Consider session timeouts with `SetSessionTimeout()`
346 |
347 | ## Common Issues
348 |
349 | ### Issue: Command works locally but not via SSH
350 | **Solution**: The command might require PTY. Enable `RequestPty = true`.
351 |
352 | ### Issue: Interactive commands hang or fail
353 | **Solution**: Interactive commands (password prompts, `vim`, interactive `sudo`) are not supported. The library cannot handle interactive terminal I/O. Use non-interactive alternatives:
354 | - For sudo: Configure passwordless sudo
355 | - For passwords: Pass via command arguments or environment variables
356 | - For interactive tools: Use non-interactive flags (e.g., `vim -e` for ex mode)
357 |
358 | ### Issue: Color codes appear as garbage
359 | **Solution**: Either enable PTY with appropriate terminal type, or disable colors in the command (e.g., `ls --color=never`).
360 |
361 | ### Issue: Working directory not persisted
362 | **Solution**: Each `ExecuteCommand()` starts in the user's home directory. Use `cd /path && command` or absolute paths.
363 |
364 | ## See Also
365 |
366 | - `SshSession.ExecuteCommand()` (SshSession.cs:401) - Execute commands synchronously
367 | - `SshSession.ExecuteCommandAsync()` (SshSession.cs:536) - Execute commands asynchronously
368 | - `CommandExecutionOptions` (CommandExecutionOptions.cs:11) - Configure command execution
369 | - `SshCommandResult` (SshCommandResult.cs:6) - Command execution results
370 | - [Authentication](authentication.md) - Authenticate before executing commands
371 | - [Advanced Terminal Control](advanced-terminal-control.md) - Configure terminal modes for PTY
372 | - [Session Timeouts](session-timeouts.md) - Set timeouts for long-running commands
373 | - [Session Lifecycle](session-lifecycle.md) - Understanding session states
374 | - [Error Handling](error-handling.md) - Handle command execution errors
375 |
--------------------------------------------------------------------------------