I'm testing a spoofing MITM SSL proxy based on boost::asio and boost::asio::stream::ssl, using boost 1.55
I register the tlsext_servername_callback, so that I can observe the called name during SSL handshake with the client.
In the callback, I have the information needed to either get/create an appropriate SSL_CTX with certificates to use with the client, or start an SSL handshake with the server, to obtain the server certificate for re-signing and then put that back into the SSL_CTX to be passed to the client, before returning the tlsext_servername callback.
It is clear that I must block the tlsext_servername callback from returning until I have the new SSL_CTX ready, which could take some time if I need to wait on a handshake with the real server.
I can block return by performing a blocking handshake with the server, but then I consume one of the io_service threads merely blocking, and I don't want to waste them like that.
I can successfully run it all as a new thread using blocking IO, in this form:
ssl_connect(...) {
boost::thread([...]() ({
sni_callback = [...](std::string server_name) {
server.handshake(...);
SSL_set_SSL_CTX(client.native_handle(), server.ctx));
};
client_socket.handshake(...);
handler(); // really done by io_service.post
}).detach();
}
But I want to use stackful coroutines in the belief that thay are less costly and that I will be able to have more connections in progress (though I may have to link with BoringSSL).
Using a coroutine spawn, I can convert the handshake with the server, like this:
ssl_connect(...) {
boost::asio::spawn(my_strand, [...](boost::asio::yield_context yield) {
sni_callback = [...](std::string server_name) {
server.async_handshake(..., yield[ec]); // <---- works nicely
SSL_set_SSL_CTX(client.native_handle(), server.ctx));
};
client_socket.handshake(...);
handler(); // really done by io_service.post
});
}
but the handshake with the client must remain blocking, otherwise, with:
client_socket.async_handshake(..., yield[ec]);
I get segfaults in the coroutine with unhelpful stack traces, mentioning only some SSL cleanup functions. However if the client connection hangs, then the client handshake can still hang and block a worker thread.
My guess is that if the handshake with the client is not blocking, the SNI callback does not occur on the coroutine, and so I cannot properly block the return and possibly the use of yield in the SNI callback is invalid.
If there is a solution that doesn't involve patching either libssl or stream::ssl, it will involve some asio_invoke_handler hooks.
However, for that to work, the coroutine would have to be activated, but not to complete the async_handshake call. Instead it would be activated to handle the handshake read completion, leading to the parsing, SNI callback & return, further handshake write, and yield back to the io_service. I don't know if the coroutine/strand implementation can even do that -- be activated for work and not for return.
So my first question is: is it likely possible to make coroutine do this?
Looking in boost source detail/io.hpp for case engine::want_input_and_retry I notice:
next_layer_.async_read_some(
boost::asio::buffer(core_.input_buffer_),
BOOST_ASIO_MOVE_CAST(io_op)(*this));
which seems to make no attempt to use asio_hanlder_invoke to allow it to be hooked, so that might need fixing.
What sort of asio_handler_invoke hook would cause the handler to be invoked on the coroutine? Could strand.dispatch() do it? If it would, I'm not sure how to write such a hook either to work with argument dependent lookup, or to invoke in the coroutine.
Otherwise I may as well stick to starting a scope-limited thread, and using blocking handshake operations.
But I like to know the limits...
Samjam