-
Notifications
You must be signed in to change notification settings - Fork 578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a dedicated method for disconnecting TLS connections #10005
base: master
Are you sure you want to change the base?
Conversation
By now, I'm pretty sure the answer is yes. Evidence: Take two connected Icinga 2 nodes and break individual connections by dropping the packets specific to that connection in a firewall. Both nodes will detect that no messages were received and reconnect, however, the old connection remains in an established state:
That implies that there's probably a resource leak in that scenario (until the kernel decides that the connection is actually dead and returns an error for the socket operations). Unverified theory of what might happen: icinga2/lib/remote/jsonrpcconnection.cpp Line 226 in 9a8620d
Which waits for icinga2/lib/remote/jsonrpcconnection.cpp Lines 112 to 120 in 9a8620d
|
Thing to consider
|
e90acc5
to
3a72a6f
Compare
I resolved conflicts and started implementing |
All usages of `AsioTlsStream` were already using `Shared<AsioTlsStream>` to keep a reference-counted instance. This commit moves the reference counting to `AsioTlsStream` itself by inheriting from `SharedObject`. This will allow to implement methods making use of the fact that these objects are reference-counted. The changes outside of `lib/base/tlsstream.hpp` are merely replacing `Shared<AsioTlsStream>::Ptr` with `AsioTlsStream::Ptr` everywhere.
3a72a6f
to
9d67c26
Compare
While continuing that work, I figured that this might become a bigger rework of This PR on it's own should already be enough of an improvement on its own, after all it even fixes a problem in the HTTP connection handling.
In that regard, I figured that adding comments to these calls why they are fine is good enough, especially when compared that was needed just to add a redundant timeout and spawn a pointless coroutine (e90acc5). I'm leaving the PR in a draft state for the moment because I still want to answer a few detail questions regarding the two new disconnect methods (I removed a |
9d67c26
to
e5b9ff4
Compare
I still couldn't find a good reason to call
That on the other hand turned out to be necessary: as confirmed using strace, without calling |
e5b9ff4
to
7430618
Compare
Timeout::Ptr shutdownTimeout(new Timeout(strand.context(), strand, boost::posix_time::seconds(10), | ||
[this, keepAlive = AsioTlsStream::Ptr(this)](boost::asio::yield_context yc) { | ||
// Forcefully terminate the connection if async_shutdown() blocked more than 10 seconds. | ||
ForceDisconnect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use lowest_layer().cancel()
here as before? Shouldn't this call force async_shutdown
to return? Right now, if you force disconnect here, which basically means you literally destroy the socket file descriptor, the call to shutdown()
on the underlying TCP layer becomes pointless since the socket no longer exists at that point. I know we suppress the errors, but shutdown()
would return a bad file descriptor
error in this case, which I don't think is right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My basic idea here was that ForceDisconnect()
should be safe to call at any time, including when this timeout happens. And thus call it here, because it does exactly what is needed here.
Why not use
lowest_layer().cancel()
here as before? Shouldn't this call forceasync_shutdown
to return?
For two reasons: the close()
used in ForceDisconnect()
cancels these operations as well (see docs):
Any asynchronous send, receive or connect operations will be cancelled immediately, and will complete with the boost::asio::error::operation_aborted error.
And second, if the socket is closed, it prevents accidentally starting new operations.
Right now, if you force disconnect here, which basically means you literally destroy the socket file descriptor, the call to
shutdown()
on the underlying TCP layer becomes pointless since the socket no longer exists at that point. I know we suppress the errors, butshutdown()
would return abad file descriptor
error in this case, which I don't think is right.
Yes, that error can be returned, however, it should come from Asio, not from the underlying syscall as Asio internally checks the socket state before issuing the syscall:
Adding a few calls to is_open()
should not hurt though, that would hopefully make this a bit more obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And second, if the socket is closed, it prevents accidentally starting new operations.
Even then, shouldn't the last ForceDisconnect()
call be enough?
Adding a few calls to
is_open()
should not hurt though, that would hopefully make this a bit more obvious.
Instead of adding such conditions, I would simply use cancel()
here as before, as you are going to destroy the socket at the end of the coroutine anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the discussion you had in #10005 (comment) is also relevant here. The check in the code I linked works because close()
sets the internal file descriptor to -1 (which is the value of invalid_socket
).
|
||
m_Stream->next_layer().async_shutdown(yc[ec]); | ||
|
||
m_Stream->lowest_layer().shutdown(m_Stream->lowest_layer().shutdown_both, ec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So ec
isn't needed anymore, right?
lib/base/tlsstream.hpp
Outdated
} | ||
|
||
void ForceDisconnect(); | ||
void GracefulDisconnect(boost::asio::io_context::strand strand, boost::asio::yield_context yc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
void GracefulDisconnect(boost::asio::io_context::strand strand, boost::asio::yield_context yc); | |
void GracefulDisconnect(boost::asio::io_context::strand& strand, boost::asio::yield_context yc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, using a reference here wouldn't work because that method spawns a coroutine, and there's nothing that keeps the m_IoStrand
object from the Http
or Rpc
class alive till that coroutine finishes.
Calling `AsioTlsStream::async_shutdown()` performs a TLS shutdown which exchanges messages (that's why it takes a `yield_context`) and thus has the potential to block the coroutine. Therefore, it should be protected with a timeout. As `async_shutdown()` doesn't simply take a timeout, this has to be implemented using a timer. So far, these timers are scattered throughout the codebase with some places missing them entirely. This commit adds helper functions to properly shutdown a TLS connection with a single function call.
This new helper functions allows deduplicating the timeout handling for `async_shutdown()`.
This new helper function has proper timeout handling which was missing here.
The reason for introducing AsioTlsStream::GracefulDisconnect() was to handle the TLS shutdown properly with a timeout since it involves a timeout. However, the implementation of this timeout involves spwaning coroutines which are redundant in some cases. This commit adds comments to the remaining calls of async_shutdown() stating why calling it is safe in these places.
7430618
to
a6445c7
Compare
Properly closing a TLS connection involves sending some shutdown messages so that both ends know that the connection wasn't truncated maliciously. Exchanging those messages can stall for a long time if the underlying TCP connection is broken. The HTTP connection handling was missing any kind of timeout for the TLS shutdown so that dead connections could hang around for a long time.
This PR introduces two new methods on
AsioTlsStream
, namelyForceDisconnect()
which just wraps the call for closing the TCP connection andGracefulShutdown()
which performs the TLS shutdown with a timeout similar to it was done inJsonRpcConnection::Disconnect()
before.As the lambda passed to
Timeout
has to keep the connection object alive,AsioTlsStream
is changed to inherit fromSharedObject
, adding reference counting to it directly. Previously, it was already created asShared<AsioTlsStream>
everywhere. Thus, a good part of the first commit is changing that type across multiple files.fixes #9986
Edit (@Al2Klimov)
closes #10216
closes #10219
closes #10220