【问题标题】:Establish remote SSL connection after or before local user connection for SSL wrapper?在 SSL 包装器的本地用户连接之后或之前建立远程 SSL 连接?
【发布时间】:2024-04-26 10:45:01
【问题描述】:

我试图在 C# 中创建一个 stunnel 克隆只是为了好玩。主循环是这样的(暂时忽略 catch-everything-and-do-nothing try-catch)

ServicePointManager.ServerCertificateValidationCallback = Validator;
            TcpListener a = new TcpListener (9999);
            a.Start ();
            while (true) {
                Console.Error.WriteLine ("Spinning...");
                try {
                    TcpClient remote = new TcpClient ("XXX.XX.XXX.XXX", 2376);
                    SslStream ssl = new SslStream(remote.GetStream(), false, new RemoteCertificateValidationCallback(Validator));
                    ssl.AuthenticateAsClient("mirai.ca");
                    TcpClient user = a.AcceptTcpClient ();
                    new Thread (new ThreadStart(() => {
                        Thread.CurrentThread.IsBackground = true;
                        try{
                            forward(user.GetStream(), ssl); //forward is a blocking function I wrote
                        }catch{}
                    })).Start ();
                } catch {
                    Thread.Sleep (1000);
                }
            }

我发现,如果我像我一样在等待用户之前进行远程 SSL 连接,那么当用户连接时,SSL 已经设置(这是用于隧道 HTTP,因此延迟非常重要)。另一方面,我的服务器会关闭长时间不活动的连接,所以如果在 5 分钟内没有新连接发生,一切都会锁定。

最好的方法是什么?

另外,我观察到我的程序生成了多达 200 个线程,这当然意味着上下文切换开销非常大,有时甚至会导致整个程序阻塞几秒钟,即使只有一个用户通过程序隧道。概括地说,我的转发功能就像

    new Thread(new ThreadStart(()=>in.CopyTo(out))).Start();
    out.CopyTo(in);

当然有很多错误处理,以防止断开的连接永远保持不变。不过,这似乎停滞了很多。我不知道如何使用像 BeginRead 这样的异步方法,根据 google 应该会有帮助。

【问题讨论】:

    标签: c# networking ssl tcp


    【解决方案1】:

    对于任何类型的代理服务器(包括stunnel 克隆),在您接受前端连接之后打开后端连接显然更容易实现。

    如果您预先打开后端连接以预期接收前端连接,您当然可以节省 RTT(这有利于延迟),但您必须处理您暗示的问题:后端将关闭空闲连接。在您收到前端连接的任何时候,您都会冒这样的风险,即您将要与此前端连接关联并且已在一段时间前打开的后端连接太旧而无法使用并且可能被后端关闭。您必须管理当前打开的后端连接池,并在它们空闲时间过长时定期关闭和刷新它们。甚至存在竞争条件,如果后端认为连接空闲时间过长并决定关闭它但代理服务器同时接收到新的前端连接,则前端可能会决定通过后端连接转发请求 而后端正在关闭此连接。这意味着您必须能够先验地知道后端连接在后端关闭它们之前可以空闲多长时间(您必须知道后端配置的超时值设置为),这样您就可以放弃它们在后端确定它们太旧之前。

    因此,总而言之:与仅按需打开后端连接相比,预先打开后端连接将节省 RTT,但这是一项大量工作,包括微妙的连接池管理,很难实现无错误。由您来判断额外的复杂性是否值得。

    顺便说一句,关于您关于处理数百个同时连接的评论,我建议将这种 I/O 绑定程序实现为基于事件循环而不是基于线程的代理服务器。基本上,您使用非阻塞套接字并在单个线程中处理事件(例如“此套接字有新数据等待转发到另一端”),而不是为每个连接生成一个线程(这在线程创建中都会变得昂贵和上下文切换)。为了将这种基于事件的模型扩展到多个 CPU 内核,您可以启动少量进程的并行线程(每个 CPU 内核或多或少一个),每个线程处理数百(或数千)个同时连接。

    【讨论】:

    • 正如我所说,我真的很想使用异步方法,但是看到所有异步方法都非常低级并且使用难以管理的字节缓冲区,我真的很想知道怎么做。同步做只是一个读写的大while循环,但是异步呢?
    • 是的,毫无疑问,异步事件循环的编程更复杂。您必须在数据结构中保留有关每个套接字的传入和传出缓冲区的状态,以及哪些已准备好转发数据,哪些尚未准备好。但我认为异步与同步的问题是次要问题的重点,即是否要预先打开后端连接。
    • 好吧,我可能做错了什么。这不应该扩展到成千上万的用户 - 实际上只有一个用户,将浏览器请求从 localhost 转换为 ssl。随着时间的推移,它仍然会停滞很多并且性能会下降,并且从长远来看线程数会越来越多(可能有些线程会停滞并且永远不会恢复)。但是在代码中看不到任何提示:gist.github.com/anonymous/4580d8ff3c6e9ea38090
    • (另外,请注意 SSL 连接需要 三个 RTT。stunnel 可以很好地预先打开连接,我的玩具程序感觉比 stunnel 慢得多。现实世界的使用是一个我想要分发的程序,而不需要捆绑 stunnel 和它附带的所有大型库(尤其是在 Mac 上存在问题))
    • SSL 连接不需要“三个 RTT”。如果包括 SSL 握手,则更像是十次。
    最近更新 更多