-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
http: replace destroySoon with socket.end #36205
base: main
Are you sure you want to change the base?
Conversation
Closing a TCP connection requires 2 FIN/ACK exchanges, so we should wait for both events before destroying a socket Fixes: nodejs#36180
Review requested:
|
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.
lgtm
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.
I think this can be resolved by replacing destroySoon()
with end()
. The suggested changes looks to me like a roundabout way to achieve the same thing.
I think this kind of defeat the point of
|
I totally agree with @ronag. I also think that this might introduce a DoS vulnerability as per my previous comment. |
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.
Adding my -1 to the list.
Maybe I’m not understanding correctly, but this is how sockets work in general? You can receive data on them until you get EOF. I think what’s missing here is a check for the |
Yes, but in the case of HTTP I no longer care about the readable side once I think this opens the risk of waiting for unresponsive clients after |
If the peer never sends an |
What is |
Stream.end should not autoDestroy until drained. It should do exactly what you want as far as I understand. |
I can't find it, where is it? |
See the autoDestroy logic in Writable. It checks for endEmitted. |
There is no shutdown here? It just checks if the readable is finished? |
Yes, I confirm, I just traced it with |
I don't know anything about shutdown. I'm just talking about streams and autoDestroy in the context of the changes in this PR. |
I would suggest you change |
What version of Node are you testing on? master? |
From what I can see this should not be possible, |
I suggest we do it like this for the time being, because this must have been the original system - and then maybe try to devise a better method
|
No, wait, |
@addaleax In fact, I think that this was already the case - there was no race condition, it is only now that I realize it - |
@ronag, what is the point of |
It makes sure |
Yeah, but how is it called? Is it only for a |
What I have in mind is checking whether |
After discussions with @ronag and @lpinca, the best solution to the TLS RST problem is the one that limits the new behaviour to TLSSockets This is complementary to @vtjnash solution which allows the client to ignore this (somewhat normal) RST Both are needed, since both ends of Node.js TLS code must interact with other TLS implementations Refs: nodejs#36205
After discussions with @ronag and @lpinca, the best solution to the TLS RST problem is the one that limits the new behaviour to TLSSockets I restored This is complementary to @vtjnash solution which allows the client to ignore servers that send this (somewhat normal) RST |
lib/_tls_wrap.js
Outdated
TLSSocket.prototype.destroySoon = function() { | ||
if (this.writable) | ||
this.end(); | ||
|
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.
I think this.end()
is enough here. The code below is possibly redundant (at least in Node 14+).
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.
Also should we have a timeout waiting for 'close'
here? @lpinca
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.
Normally, the kernel will expire sockets stuck in FIN_WAIT_2
state as it has always been a problem and there was an old DoS attack that starved out the system of sockets by deliberately not closing them
// If the server immediately destroys its socket, this data will trigger a | ||
// RST packet in response | ||
// https://github.com/nodejs/node/issues/36180 | ||
TLSSocket.prototype.destroySoon = Writable.prototype.end; |
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.
Do we need a timeout for 'close'
here? @lpinca
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.
I think it makes sense.
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.
I don't think you want a timeout here. The underlying socket will already have a timeout (from the time the last packet was received). And if it's slowly trickling in data, any hard timeout could break the connection prematurely.
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.
Wouldn't this allow a Slowloris like attack? To clarify, I think it would make sense to have a timeout on tlsSocket.destroySoon()
if it is changed to be an alias for tlsSocket.end()
, not on tlsSocket.end()
itself that should work like netSocket.end()
for feature parity.
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.
Yes, that seems quite possible. As I mentioned elsewhere, I'm opposed to the general implementation in this PR. At a minimum, it needs to respect the allowHalfOpen / autoDestroy flags. But it also needs to be implemented at the point in the TLS code that is generating this event, and not by disregarding the explicit call to destroy or close it.
Additionally, I expect that, in principle, as soon as this function returns, a sufficiently smart garbage collector could observe that there are no remaining useful references to this TLS socket (we were just requested to destroy the socket for reading and writing, so there's nothing that should generate a new callback event), and thus the underlying socket should be expected to be finalized and then immediately destroyed as soon as the call to 'end' returns. And thus still causing the RST packet this PR was supposedly going to prevent. It's probably not hard to fool the garbage collector into keeping the memory reference live (though who is ever going to clean up this socket in that case?), but I worry that this PR may thus just create new problems, without actually addressing existing ones.
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.
Normally, all three major OSes have a built-in default timeout for sockets in FIN_WAIT_2
state (first FIN
sent and then ACK
from remote) - it is usually about 60-120 seconds.
For sockets in FIN_WAIT_1
state (first FIN
sent, waiting for first ACK
), the much longer default TCP timeout applies - that would be 300s by default on Linux.
As for the Slowloris attack, I don't see how this change could help an attacker? A Slowloris attack exploits the timeouts by always sending just enough data to keep them from triggering. The most advantageous timeout for this would still be the default TCP ACK
timeout - be it by withholding the ACK
on a data
packet or a FIN
packet. In fact, FIN
would be a little bit less practical, because you can delay it only once - the connection will be closed afterwards.
As for the event that triggers (or not) the RST
, it is not the destruction of the socket object by the garbage collector, it is the explicit calling of the kernel close()
syscall by libuv uv_tcp_close()
. If any remote data is received after this call, a RST
is sent back.
I'm strongly opposed to this. The conversation is long, so it's hard to respond to specific comments, but I'll try to respond to many. First a comment on bugs though, what the TLS wrap client code currently does is NOT a simultaneous close of the TCP level—that’s the bug I’m proposing fixing in #36111. The server side (this code) currently assumes a simultaneous close (allowed, per TLS spec), while the client (other) side assumes that the server will do a graceful shutdown of the TCP socket after the TLS socket is destroyed (allowed, per TLS spec, but only if the application layer handles it independently). All types of normal close involve first an active close (except for broken connection timeout failures). What you're proposing would be for the server to additionally wait for a TCP graceful shutdown to complete after the TLS connection is already satisfactorily destroyed. Not all of these state transitions are represented in the diagram you sent earlier (https://users.cs.northwestern.edu/~agupta/cs340/project2/TCPIP_State_Transition_Diagram.pdf), so I wanted to clarify some terminology. For reference again too, here's the relevant paragraphs in the TLS spec for a proper close: https://tools.ietf.org/html/rfc5246#section-7.2.1
This is true of non-TLS sockets using TCP, but is explicitly prohibited by the TLS specification above. As soon as the peer receives or sends a close-notify, it must immediately stop sending data: the In the old TLS specification, it was non-conforming to process a FIN or RST packet as an EOF (e.g. to process any state change notification from the TCP socket). In the current spec, it's permitted to treat a EOF on the underlying socket as a TLS close-notify. As such, this is a misreading of the spec:
In TLS, both simultaneous close and active/passive close of the TCP layer are permitted (in neither case is it necessary to wait for the message exchanges). The underlying socket is permitted to be closed immediately after sending a close-notify message (the exact timing is explicitly undefined in the spec). Thus many implementations (including nodejs) may call the equivalent of forceClose immediately to optimize cleanup. The spec requires the other side to always ignore RST and FIN packets anyways (since they’re unencrypted), so it's not important to wait for the close-notify exchange (the connection is already destroyed). So the current behavior of the active close is completely correct for TLS 1.1. Neither the existing behavior nor this PR would be entirely correct for TLS 1.0 (since it'll also react to a EOF from TCP, which the original spec prohibited).
Yes, but I would guess that |
@vtjnash thank you. I want to clarify only one minor thing. My comment
was referring to the non keep alive case. When keep alive is used |
This is the same old discussion on whether we should ignore this (somewhat normal) This is begging for a So @vtjnash , I agree with what you are saying. But the TLS protocol has been specified independently of TCP - it does not even require it - even if 99% of the time it is used over TCP. So I think that the best possible behavior is that Node's TLS client ignores a And the best possible behavior for the TLS server is to make sure that it does not send a |
A TLS client that reports an error is in explicit violation of this part of the TLS spec. They are going to encounter reliability issues irregardless of this PR, especially since nodejs isn't the only server implementation they may encounter. However, this PR will make it harder to test for correct implementations of the TLS spec at the nodejs client level, since the test server would now usually be more lenient than required. |
No, there are almost no mentions of TCP and no mention of a TCP RST at all in the TLS spec. TLS is specified on top of some reliable transport protocol (e.g., TCP) - this being the only occurrence of TCP in the main document excluding the appendices. And it allows you to implement it without being perfectly compliant with TCP (ie frequently triggering a RST when closing, and then ignoring it) - but does not stop you if you want to comply with both. In this case, when closing, you have to continue reading on the TCP level without interpreting the data. |
This is explicitly mentioned as being not required of a conforming implementation: Argue all you want about hypothetical alternative TLS designs, but that's not what the authors of the 1.2 spec wrote as the behaviors you should expect to encounter when interoperating with real-world TLS implementations. (note that "have to" is not typically terminology used in a spec—it's typically written as MUST, for clarity of communication. But here we actually have the statement to the contrary given with MAY.) One mistake in my last text I realized is that I'm using the text from the older version of the spec. For the current version (1.3), that link is https://tools.ietf.org/html/rfc8446#section-6.1. There's no substantive difference for this purpose, though the wording changed slightly: "Both parties need not wait to receive a close_notify alert before closing their read side of the connection" The reason for the change is that the allowHalfClose state is actually now permitted in TLS 1.3 (receiving close_notify no longer should cause pending writes to be discarded). This will be important, as it will resolve some of the TODO questions I had left in #36111 in the opposite direction as I was expecting. |
@vtjnash @mmomtchev What does this need to move forward or being closed? Can you give a quick summary of any blocking issues or disagreements? |
@ronag |
How can we help resolving this? Could one of you summarize the disagreement and then maybe we can ask the TSC for an opinion? |
The last two exchanges summarize it very well. TLS allows for a behavior that is almost guaranteed to provoke a RFC 4346, section concerning closing of connections: The not required part is the problem. I favor mitigation since we are to respect both protocols (TLS and TCP). |
Could any of you do a quick survey on what other languages/runtimes/load balancers do? |
I (with input from @lpinca) created a small gist for testing the behavior of different HTTP servers: Quick reminder: we are testing which servers will drain the data the client sent after his request in a Such behavior (not reading after So when testing, I came to conclusion that most UNIX-socket based implementations (ie Apache and nginx), the server will read after In the TLS case, this is about the server's behavior in response to a valid client behavior. Currently (after @vtjnash PR) the Node client ignores a This PR is about the server not sending the I think that in order to assure interoperability, both are needed. Do other TLS servers send a |
Have you been able to survey some HTTP and HTTPS server implementations to check for this? |
I think that test may be a bit confusing to some, since you appear to be testing what happens to your http client when your http client violates the http protocol, which isn't precisely the case we're discussing here (which is the desired socket shutdown sequence of a https server for valid request)—though it does allow us to infer some properties relevant here. Just wanted to note that limitation on its applicability: the test was done on an http server (without TLS) while violating the spec (so not applicable to the use of http), but as a means of triggering the likely behavior of the https server while observing the spec, by widening the window for the race to be detected. The behavior you describe isn't a violation of the TCP spec—sending an RST is exactly what the TCP spec said is permitted to happen for that sequence of events. You shouldn't be able to violate the spec while running the kernel state machine, since its job is to enforce compliance. However, the TLS spec defines that the RST packet should be ignored, so there should not be an issue bubbling up from there to the application, and that handling is currently not implemented correctly in nodejs. Since ASIC implementations behave this way also, it may be desirable for performance to close the socket early instead of late (I make no claim either way). Either way, I think the right place to make this change would be to alter which sequence of events the TLSWrap state machine executes, rather than changing the meaning of this function. |
We talked about this at TSC today. The sense of the group is that a path to resolution might be to check what nginx, HAproxy, and Apache do. If they all do the same thing, we certainly won't want to do anything different. Is someone up for investigating that? |
🚓 |
Closing a TCP connection requires 2 FIN/ACK
exchanges, so we should wait for both events
before destroying a socket
Fixes: #36180
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes