Skip to content
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

reverse_proxy: h2 websocket stuck if client needs to send the first message and encode is enabled #6733

Closed
WeidiDeng opened this issue Dec 7, 2024 · 11 comments · Fixed by #6738
Labels
bug 🐞 Something isn't working help wanted 🆘 Extra attention is needed

Comments

@WeidiDeng
Copy link
Member

With this merged, I noticed some of my websockets failed to connect while others succeed.

I disabled encode and found out now these requests connect successfully.

The problem is that client requests contain the header accept-encoding: gzip, deflate, br, zstd, which is wrapped

for _, encName := range AcceptedEncodings(r, enc.Prefer) {
if _, ok := enc.writerPools[encName]; !ok {
continue // encoding not offered
}
w = enc.openResponseWriter(encName, w)
defer w.(*responseWriter).Close()

This response writer will write informational header immediately

if 100 <= status && status <= 199 {
rw.ResponseWriter.WriteHeader(status)
}

During websocket upgrade, for http1, the upgrade will write 101 status,

rw.WriteHeader(res.StatusCode)

which will be flushed to the client immediately.

However, for h2, although FlushError is called

flushErr := http.NewResponseController(rw).Flush()

response code is 200 same as any other status that indicates success. Because we want to determine if the response can be encoded, this code is stored and flushed later if there is any data from the upstream and we can be sure whether the response can be encoded

func (rw *responseWriter) FlushError() error {
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
// therefore add the Content-Encoding header; this happens in the first call
// to rw.Write (see bug in #4314)
return nil
}

If the websocket transport a protocol that server sends response first, it is OK as that will cause the status and data to be sent. But if the client sends data first, it won't happen because the response will never be sent, and client thinks the handshake didn't finish.

For now, I created a matcher to ignore encoding for h2 websocket upgrade

@not_h2_ws not {
    header :protocol *
    method CONNECT
    protocol http/2
}
encode @not_h2_ws zstd gzip

But I think an elegant solution should be implemented.

@WeidiDeng WeidiDeng added the help wanted 🆘 Extra attention is needed label Dec 7, 2024
@WeidiDeng
Copy link
Member Author

I suppose I can manually unwrap the response writer until no more unwrap can be done. And use this unwrapped response writer instead. What do you think @mholt ?

@WeidiDeng WeidiDeng added the bug 🐞 Something isn't working label Dec 7, 2024
@WeidiDeng
Copy link
Member Author

h1 rfc, h2 rfc and h3 rfc all states that CONNECT requests are used to establish a tunnel to the remote hosts and 200 means the connections is successful. The server should then forward the packets blindly between the client and the remote host.

In websockets over h2 scenario, this is also true. As in all cases where websockets are involved.

@bt90
Copy link
Contributor

bt90 commented Dec 9, 2024

There seems to be some functionality in place to avoid encoding WebSocket requests:

// AcceptedEncodings returns the list of encodings that the
// client supports, in descending order of preference.
// The client preference via q-factor and the server
// preference via Prefer setting are taken into account. If
// the Sec-WebSocket-Key header is present then non-identity
// encodings are not considered. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.
func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {

I think we should give all CONNECT requests the identity treatment here.

@bt90
Copy link
Contributor

bt90 commented Dec 9, 2024

The h2 WebSocket requests aren't using the Sec-WebSocket-Key header which is probably the culprit here. see https://datatracker.ietf.org/doc/html/rfc8441#section-5

Implementations using this extended CONNECT to bootstrap WebSockets do not do the processing of the Sec-WebSocket-Key and Sec-WebSocket-Accept header fields of [RFC6455] as that functionality has been superseded by the :protocol pseudo-header field.

@bt90

This comment has been minimized.

@WeidiDeng
Copy link
Member Author

:protocol is converted to Upgrade header.

I think for a non successful upgrade, encode is still useful, instead of bypassing it completely. Users may choose to display custom page.

@bt90

This comment has been minimized.

@bt90

This comment has been minimized.

@WeidiDeng
Copy link
Member Author

That header is created. Why do you say it's not?

clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}

Anyway, that's not relevant in this issue.

@bt90
Copy link
Contributor

bt90 commented Dec 9, 2024

My bad, missed it.

@bt90
Copy link
Contributor

bt90 commented Dec 10, 2024

I think for a non successful upgrade, encode is still useful, instead of bypassing it completely. Users may choose to display custom page.

Sounds reasonable 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐞 Something isn't working help wanted 🆘 Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants