Skip to content

WebSocket: plain (non-TLS) websocket ports send auth NOTICEs before the HTTP 101, breaking the handshake #97

Description

@rubinlinux

Branch: ircv3.2-upgrade @ 3868b34 (also present on MrLenin/nefarious2:ircv3.2-hardening as far as I can tell — same code path).

Summary

On a non-TLS listener with websocket = yes;, the ident/DNS progress notices are written to the socket as bare IRC lines before the HTTP/1.1 101 Switching Protocols response. Any HTTP client (Node's parser, every browser) rejects the upgrade, so plain-text WebSocket is unusable. The same listener with ssl = yes; works.

Reproduction

ircd.conf:

Port {
     port = 8067;
     websocket = yes;
};

Then:

printf 'GET / HTTP/1.1\r\nHost: localhost:8067\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: text.ircv3.net\r\n\r\n' | nc -q 3 127.0.0.1 8067 | cat -v

Output:

NOTICE * :*** Looking up your hostname^M
NOTICE * :*** Checking Ident^M
NOTICE * :*** No ident response^M
HTTP/1.1 101 Switching Protocols^M
Upgrade: websocket^M
Connection: Upgrade^M
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=^M
Sec-WebSocket-Protocol: text.ircv3.net^M
^M
M-^A;ERROR :Closing Link: * by irc.seance.test (EOF from client)

Node 22 (ws or the global WebSocket) fails the connection with Parse Error: Expected HTTP/, RTSP/ or ICE/ and close code 1006. Same request against wss:// on an ssl = yes; websocket = yes; port succeeds and registers normally.

Cause

send_queued() in ircd/s_bsd.c:318-325 correctly holds all MsgQ output while IsWSNeedHandshake(cptr) / IsWSSniff(cptr) is set. But the auth notices don't go through the MsgQ: sendheader() in ircd/s_auth.c:164-169 is

#ifdef USE_SSL
#define sendheader(c, r) \
   ssl_send(c, HeaderMessages[(r)].message, HeaderMessages[(r)].length)
#else
#define sendheader(c, r) \
   send(cli_fd(c), HeaderMessages[(r)].message, HeaderMessages[(r)].length, 0)
#endif

i.e. a direct write to the fd, so it lands on the wire before websocket_handshake() has replied. On TLS ports it happens to come out in the right order (I assume because ssl_send can't write until the TLS handshake completes, by which point the 101 has been queued) — I have not verified that part.

Suggested fix

Either route sendheader() through the normal send path so the IsWSNeedHandshake hold applies (and the lines get framed once the socket is a WebSocket), or skip/defer the notices for clients that still have FLAG_WS_NEEDHANDSHAKE / FLAG_WS_SNIFF set. The autodetect mode has the same exposure: the sniff can't complete before the notices are written.

Context

Found while bringing up a browser IRCv3 client (Seance, a TheLounge fork) that talks to nefarious2 directly over WebSocket. Happy to test a fix against the same setup — it's a docker build of the branch with the port above added.

Metadata

Metadata

Assignees

Labels

bugircv3-upgradeIssues against the ircv3.2-upgrade / ircv3.2-hardening branches

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions