├── .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 |
24 | 25 | NullOpsDevs, 0x25CBFC4F 26 |
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 | ![Memory Usage During Stress Test](memory_in_peak.png) 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 | [![NuGet](https://img.shields.io/nuget/v/NullOpsDevs.LibSsh)](https://www.nuget.org/packages/NullOpsDevs.LibSsh/) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 7 | [![.NET](https://img.shields.io/badge/.NET-Standard_2.1-purple.svg)](https://dotnet.microsoft.com/) 8 | [![.NET](https://img.shields.io/badge/.NET-6.0-purple.svg)](https://dotnet.microsoft.com/) 9 | [![.NET](https://img.shields.io/badge/.NET-7.0-purple.svg)](https://dotnet.microsoft.com/) 10 | [![.NET](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/) 11 | [![.NET](https://img.shields.io/badge/.NET-9.0-purple.svg)](https://dotnet.microsoft.com/) 12 | [![.NET](https://img.shields.io/badge/.NET-10.0-purple.svg)](https://dotnet.microsoft.com/) 13 | [![Documentation](https://img.shields.io/badge/Documentation-blue)](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 | --------------------------------------------------------------------------------