实现TCP(SSL)代理时读取Payload后无法转发数据的问题
解决SSL代理读取首个数据包后无法转发的问题
核心问题在于:SslStream包装NetworkStream后,所有读取操作都会直接消费底层NetworkStream的加密数据并解密为明文。此时再直接读取NetworkStream,得到的是未解密的后续加密数据,无法作为明文转发;而如果先读取NetworkStream再传给SslStream,若未正确组合流,会导致SslStream无法读取后续的加密数据。
正确解决方案:缓存初始数据+组合流
先读取NetworkStream的初始数据到内存,再将内存流与原始NetworkStream组合成一个可连续读取的流,传给SslStream。这样SslStream既能读取到之前缓存的初始数据,后续也能继续从原始流读取加密数据,不会丢失任何内容。
步骤1:实现组合流类
需要一个自定义流,先返回缓存的内存流数据,读完后自动切换到原始NetworkStream:
public class CompositeStream : Stream { private readonly Stream _firstStream; private readonly Stream _secondStream; private bool _firstStreamCompleted; public CompositeStream(Stream firstStream, Stream secondStream) { _firstStream = firstStream ?? throw new ArgumentNullException(nameof(firstStream)); _secondStream = secondStream ?? throw new ArgumentNullException(nameof(secondStream)); } public override bool CanRead => _firstStream.CanRead && _secondStream.CanRead; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) { if (_firstStreamCompleted) return _secondStream.Read(buffer, offset, count); int bytesRead = _firstStream.Read(buffer, offset, count); if (bytesRead == 0) { _firstStreamCompleted = true; return _secondStream.Read(buffer, offset, count); } return bytesRead; } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_firstStreamCompleted) return await _secondStream.ReadAsync(buffer, offset, count, cancellationToken); int bytesRead = await _firstStream.ReadAsync(buffer, offset, count, cancellationToken); if (bytesRead == 0) { _firstStreamCompleted = true; return await _secondStream.ReadAsync(buffer, offset, count, cancellationToken); } return bytesRead; } public override void Flush() => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); protected override void Dispose(bool disposing) { if (disposing) { _firstStream.Dispose(); _secondStream.Dispose(); } base.Dispose(disposing); } }
步骤2:修改代理逻辑
先缓存初始数据,用组合流初始化SslStream,完成握手后读取明文数据包,再转发到目标服务器:
using (NetworkStream clientStream = client.GetStream()) { // 1. 读取初始数据到内存流 var initialBuffer = new byte[1024]; int initialBytesRead = await clientStream.ReadAsync(initialBuffer, 0, initialBuffer.Length); var ms = new MemoryStream(); await ms.WriteAsync(initialBuffer, 0, initialBytesRead); ms.Position = 0; // 重置流位置,让SslStream能从开头读取 // 2. 组合内存流和原始NetworkStream using (var combinedStream = new CompositeStream(ms, clientStream)) using (SslStream sslStream = new SslStream(combinedStream, false, ValidateClientCertificate)) { try { // 完成SSL握手 await sslStream.AuthenticateAsServerAsync(serverCertificate, false, false); // 3. 读取首个明文数据包(此时会先读取缓存的初始数据) var firstPacketBuffer = new byte[1024]; int firstPacketBytes = await sslStream.ReadAsync(firstPacketBuffer, 0, firstPacketBuffer.Length); var firstPacketContent = Encoding.ASCII.GetString(firstPacketBuffer, 0, firstPacketBytes); // 解析首个数据包,确定目标服务器地址和端口 string targetHost = ParseTargetHost(firstPacketContent); // 自定义解析逻辑 int targetPort = 443; // 根据实际情况调整 // 4. 建立到目标服务器的连接并转发数据 using (var targetClient = new TcpClient(targetHost, targetPort)) using (var targetStream = targetClient.GetStream()) { // 先转发首个明文数据包 await targetStream.WriteAsync(firstPacketBuffer, 0, firstPacketBytes); // 双向转发后续数据 var forwardTask = sslStream.CopyToAsync(targetStream); var reverseTask = targetStream.CopyToAsync(sslStream); await Task.WhenAny(forwardTask, reverseTask); } } catch (Exception ex) { // 处理异常 } } } // 证书验证回调方法 private bool ValidateClientCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { // 根据需求实现验证逻辑,这里示例直接返回true return true; } // 自定义解析目标服务器的方法 private string ParseTargetHost(string packetContent) { // 示例:从HTTP请求头中提取Host var lines = packetContent.Split('\n'); foreach (var line in lines) { if (line.StartsWith("Host: ")) { return line.Substring(6).Trim(); } } throw new InvalidOperationException("无法解析目标服务器"); }
为什么之前的MemoryStream方案无效?
如果只是将单独的MemoryStream传给SslStream,SslStream读完内存流后就会认为数据结束,无法继续读取原始NetworkStream的后续加密数据。必须将内存流与原始流组合成一个连续的流,才能让SslStream完整处理整个SSL会话的数据。
内容的提问来源于stack exchange,提问作者Michael Eckhart-Wöllkart
相关产品推荐
相关产品推荐

