如何不依赖外部库连接并认证SSH服务器?附代码框架求指导
可行性分析与实现方案
你的TCP框架骨架是可行的,它的读写线程分离、并发队列处理命令的结构符合网络通信的最佳实践。但要实现SSH功能,必须补全SSH应用层协议的核心逻辑——SSH不是简单的TCP字符串传输,它包含版本协商、密钥交换、加密通信、用户认证、会话管理等复杂流程,以下是具体的实现步骤:
1. 补全Connect方法的SSH核心流程
TCP连接建立后,必须完成SSH的版本协商、密钥交换、用户认证三步,才能进入可执行命令的状态:
private void Connect() { try { TCP_Client = new TcpClient(computer.IpAddress, computer.IpPort) { NoDelay = true }; TCP_Stream = TCP_Client.GetStream(); TCP_Stream.ReadTimeout = Timeout.Infinite; // ====== 1. SSH版本协商 ====== byte[] versionBuffer = new byte[1024]; int readLen = TCP_Stream.Read(versionBuffer, 0, versionBuffer.Length); string serverVersion = Encoding.ASCII.GetString(versionBuffer, 0, readLen).Trim(); if (!serverVersion.StartsWith("SSH-2.0-")) throw new Exception("目标不是SSH 2.0服务器"); string clientVersion = "SSH-2.0-CustomSSHClient\r\n"; byte[] clientVersionBytes = Encoding.ASCII.GetBytes(clientVersion); TCP_Stream.Write(clientVersionBytes, 0, clientVersionBytes.Length); TCP_Stream.Flush(); // ====== 2. SSH密钥交换(KEX) ====== // 此处需实现KEX_INIT消息交换、算法协商、共享密钥生成 // 示例:协商使用diffie-hellman-group1-sha1算法,生成会话加密密钥 PerformKeyExchange(); if (!Connected) throw new Exception("密钥交换失败"); // ====== 3. 用户认证(示例:密码认证) ====== if (!AuthenticateWithPassword(computer.Username, computer.Password)) throw new Exception("认证失败"); pendingCommands = new ConcurrentQueue<string>(); Connected = true; Authenticated = true; } catch (Exception e) { Disconnect(); } }
2. 实现SSH加密通信的基础方法
SSH在密钥交换后,所有数据都需要加密传输,需封装以下核心方法:
加密发送SSH数据包
private void SendEncryptedPacket(byte[] payload) { // SSH包结构:长度(4字节大端) + 填充长度(1字节) + 有效载荷 + 填充 int blockSize = _sessionCipher.BlockSize / 8; // 如AES-256的块大小为32字节 int paddingLen = blockSize - ((payload.Length + 1) % blockSize); if (paddingLen < 4) paddingLen += blockSize; // SSH要求填充至少4字节 List<byte> rawPacket = new List<byte>(); // 添加长度(不含自身4字节) rawPacket.AddRange(BitConverter.GetBytes((uint)(payload.Length + 1 + paddingLen)).Reverse()); // 添加填充长度 rawPacket.Add((byte)paddingLen); // 添加有效载荷 rawPacket.AddRange(payload); // 添加随机填充字节 byte[] padding = new byte[paddingLen]; new Random().NextBytes(padding); rawPacket.AddRange(padding); // 加密后发送 byte[] encrypted = _sessionCipher.CreateEncryptor().TransformFinalBlock(rawPacket.ToArray(), 0, rawPacket.Count); TCP_Stream.Write(encrypted, 0, encrypted.Length); TCP_Stream.Flush(); }
解密读取SSH数据包
private byte[] ReadEncryptedPacket() { // 读取加密的长度字段并解密 byte[] encryptedLen = new byte[4]; TCP_Stream.Read(encryptedLen, 0, 4); byte[] lenBytes = _sessionCipher.CreateDecryptor().TransformFinalBlock(encryptedLen, 0, 4); uint totalLen = BitConverter.ToUInt32(lenBytes.Reverse().ToArray(), 0); // 读取剩余加密数据并解密 byte[] encryptedRest = new byte[totalLen]; int read = 0; while (read < totalLen) { int chunk = TCP_Stream.Read(encryptedRest, read, (int)totalLen - read); if (chunk == 0) throw new IOException("连接已关闭"); read += chunk; } byte[] decryptedRest = _sessionCipher.CreateDecryptor().TransformFinalBlock(encryptedRest, 0, encryptedRest.Length); // 提取有效载荷(跳过填充长度和填充部分) byte paddingLen = decryptedRest[0]; return decryptedRest.Skip(1).Take((int)(totalLen - 1 - paddingLen)).ToArray(); }
3. 补全TCP_Write的命令发送逻辑
SSH执行命令需要先建立会话通道,再发送exec请求:
private void TCP_Write() { uint? currentChannelId = null; while (isRunning) { try { if (!Connected) { Connect(); } else { while (pendingCommands.TryDequeue(out string commandString)) { // 1. 打开SSH会话通道 if (!currentChannelId.HasValue) { currentChannelId = OpenSessionChannel(); } // 2. 发送exec命令请求 List<byte> execRequest = new List<byte>(); execRequest.Add((byte)98); // SSH_MSG_CHANNEL_REQUEST类型 // 添加通道ID execRequest.AddRange(BitConverter.GetBytes((uint)currentChannelId).Reverse()); // 添加请求类型:exec byte[] execType = Encoding.UTF8.GetBytes("exec"); execRequest.AddRange(BitConverter.GetBytes((uint)execType.Length).Reverse()); execRequest.AddRange(execType); // 要求服务器回复 execRequest.Add(0x01); // 添加命令内容 byte[] commandBytes = Encoding.UTF8.GetBytes(commandString); execRequest.AddRange(BitConverter.GetBytes((uint)commandBytes.Length).Reverse()); execRequest.AddRange(commandBytes); SendEncryptedPacket(execRequest.ToArray()); } TCP_FlushEvt.Reset(); TCP_FlushEvt.Wait(); } } catch (Exception e) { Disconnect(); } } }
4. 补全TCP_Read的响应解析逻辑
需要解析SSH返回的通道数据、错误信息等:
private void TCP_Read() { byte[] buffer = new byte[4096]; while (isRunning) { try { if (Connected && Authenticated) { byte[] payload = ReadEncryptedPacket(); byte msgType = payload[0]; switch (msgType) { case 94: // SSH_MSG_CHANNEL_DATA(命令标准输出) uint channelId = BitConverter.ToUInt32(payload.Skip(1).Take(4).Reverse().ToArray(), 0); uint dataLen = BitConverter.ToUInt32(payload.Skip(5).Take(4).Reverse().ToArray(), 0); byte[] outputData = payload.Skip(9).Take((int)dataLen).ToArray(); Debug.WriteLine($"命令输出:{Encoding.UTF8.GetString(outputData)}"); break; case 95: // SSH_MSG_CHANNEL_EXTENDED_DATA(命令错误输出) // 类似标准输出处理,提取错误内容 break; case 96: // SSH_MSG_CHANNEL_EOF(命令执行结束) Debug.WriteLine("命令执行完成"); break; // 处理其他消息类型:心跳、错误等 } } } catch { Disconnect(); } } Disconnect(); }
5. 完善Disconnect的资源清理
private void Disconnect() { isRunning = false; TCP_FlushEvt.Set(); // 唤醒阻塞的写线程 TCP_Stream?.Close(); TCP_Client?.Close(); _sessionCipher?.Clear(); // 清理加密对象 Connected = false; Authenticated = false; }
关键注意事项
- SSH协议复杂度:手动实现需要处理大量细节,比如算法协商、MAC校验、重放保护等,上述示例仅覆盖核心流程,实际使用需完善所有协议细节。
- 线程安全:
Connected、Authenticated等状态变量需用volatile修饰或加锁,避免多线程竞争问题。 - 错误处理:需针对SSH的各种错误消息(如认证失败、通道错误)添加分支处理,提升鲁棒性。
内容的提问来源于stack exchange,提问作者Hugo_vdms
相关产品推荐
相关产品推荐

