-
Notifications
You must be signed in to change notification settings - Fork 111
Description
Is this a security vulnerability?
no
Issue
Gorouter is not able to handle HTTP/2 Websockets requests (RFC 8441).
Affected Versions
latest version when http2 is enabled
Context
We are testing currently the http/2 feature and we noticed that websockets over http/2 are not working. Gorouter is closing the connection without any http response. After activating golang verbose logging (GODEBUG=http2debug=2) you can see the followed log entries:
2021/09/30 10:23:47 http2: invalid pseudo headers: invalid pseudo-header ":protocol"
2021/09/30 10:23:47 http2: Framer 0xc0003ca0e0: wrote RST_STREAM stream=1 len=4 ErrCode=PROTOCOL_ERROR
Traffic Diagram
+----+----+ +----------+ +-------+
\o/ | | | | | |
+ +--->+ HAproxy +--http/2--->+ Gorouter +---->+ App |
/ \ | | | | | |
client +---------+ +----------+ +-------+
Steps to Reproduce
I could also reproduce the issue with the followed nodejs client
'use strict';
'use strict';
const WebSocket = require('ws');
const http2 = require('http2-wrapper');
const head = Buffer.from('');
var urlArg = process.argv[2].trim();
console.log("try to connect to url '" + urlArg + "'")
const connect = (url, options) => {
const ws = new WebSocket(null);
ws._isServer = false;
const destroy = async error => {
ws._readyState = WebSocket.CLOSING;
await Promise.resolve();
ws.emit('error', error);
};
(async () => {
try {
const stream = await http2.globalAgent.request(url, options, {
...options,
':method': 'CONNECT',
':protocol': 'websocket',
origin: (new URL(url)).origin
});
stream.once('error', destroy);
stream.once('response', _headers => {
stream.off('error', destroy);
stream.setNoDelay = () => {};
ws.setSocket(stream, head, 100 * 1024 * 1024);
});
} catch (error) {
destroy(error);
}
})();
return ws;
};
const ws = connect(urlArg, {
rejectUnauthorized: false
});
ws.once('open', () => {
console.log('CONNECTED!');
ws.send('WebSockets over HTTP/2');
});
ws.once('close', () => {
console.log('DISCONNECTED!');
});
ws.once('message', message => {
console.log(message);
ws.close();
});
ws.once('error', error => {
console.error(error);
});
Steps to run it:
- save coding as file
npm install ws http2-wrappernode <filename> https://<url_gorouter>
Expected result
One of the following behaviors would be acceptable:
- Gorouter can handle the connection
- downgrade in case of a websocket connection to HTTP/1.1
- http response 400 Bad request
Current result
HAproxy reporting the issue with the termination state SH-- in the accesslog.
nodejs client reports followed error:
Error [ERR_HTTP2_STREAM_ERROR]: Stream closed with error code NGHTTP2_REFUSED_STREAM
at ClientHttp2Stream._destroy [as __destroy] (internal/http2/core.js:2148:13)
at ClientHttp2Stream.stream._destroy (/Users/d064325/git/mytools/socket_test/node_modules/http2-wrapper/source/utils/delay-async-destroy.js:12:23)
at ClientHttp2Stream.destroy (internal/streams/destroy.js:38:8)
at ClientHttp2Stream.[kMaybeDestroy] (internal/http2/core.js:2164:12)
at Http2Stream.onStreamClose (internal/http2/core.js:511:26) {
code: 'ERR_HTTP2_STREAM_ERROR'
}
Possible Fix
Root cause of the problem is the followed check:
https://github.com/golang/go/blob/e180e2c27c3c3f06a4df6352386efedc15a1e38c/src/net/http/h2_bundle.go#L2770
func (mh *http2MetaHeadersFrame) checkPseudos() error {
var isRequest, isResponse bool
pf := mh.PseudoFields()
for i, hf := range pf {
switch hf.Name {
case ":method", ":path", ":scheme", ":authority":
isRequest = true
case ":status":
isResponse = true
default:
return http2pseudoHeaderError(hf.Name)
}
// Check for duplicates.
// This would be a bad algorithm, but N is 4.
// And this doesn't allocate.
for _, hf2 := range pf[:i] {
if hf.Name == hf2.Name {
return http2duplicatePseudoHeaderError(hf.Name)
}
}
}
if isRequest && isResponse {
return http2errMixPseudoHeaderTypes
}
return nil
}
I also found followed issue golang/go#32763
I don't know if gorouter can workaround the problem or at least return a proper http response.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status