Skip to content

Gorouter: Websockets over HTTP/2 - invalid pseudo-header ":protocol" #230

@thomas-kaltenbach

Description

@thomas-kaltenbach

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-wrapper
  • node <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

No type

Projects

Status

Waiting for Changes | Open for Contribution

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions