You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 17:35:53