diff --git a/doc/configuration.txt b/doc/configuration.txt index 1399bf510..42efa329e 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -14329,6 +14329,8 @@ alpn server 127.0.0.1:443 ssl crt pub.pem alpn h2,http/1.1 + See also "ws" to use an alternative ALPN for websocket streams. + backup When "backup" is present on a server line, the server is only used in load balancing when all other non-backup servers are unavailable. Requests coming @@ -14942,6 +14944,8 @@ proto Idea behind this option is to bypass the selection of the best multiplexer's protocol for all connections established to this server. + See also "ws" to use an alternative protocol for websocket streams. + redir The "redir" parameter enables the redirection mode for all GET and HEAD requests addressing this server. This means that instead of having HAProxy @@ -15274,6 +15278,26 @@ weight can both grow and shrink, for instance between 10 and 100 to leave enough room above and below for later adjustments. +ws { auto | h1 | h2 } + This option allows to configure the protocol used when relaying websocket + streams. This is most notably useful when using an HTTP/2 backend without the + support for H2 websockets through the RFC8441. + + The default mode is "auto". This will reuse the same protocol as the main + one. The only difference is when using ALPN. In this case, it can try to + downgrade the ALPN to "http/1.1" only for websocket streams if the configured + server ALPN contains it. + + The value "h1" is used to force HTTP/1.1 for websockets streams, through ALPN + if SSL ALPN is activated for the server. Similarly, "h2" can be used to + force HTTP/2.0 websockets. Use this value with care : the server must support + RFC8441 or an error will be reported by haproxy when relaying websockets. + + Note that NPN is not taken into account as its usage has been deprecated in + favor of the ALPN extension. + + See also "alpn" and "proto". + 5.3. Server IP address resolution using DNS ------------------------------------------- diff --git a/doc/management.txt b/doc/management.txt index 8b7d9cd29..c85ae1fee 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -1561,6 +1561,7 @@ add server / [args]* - verify - verifyhost - weight + - ws Their syntax is similar to the server line from the configuration file, please refer to their individual documentation for details. diff --git a/reg-tests/http-messaging/common.pem b/reg-tests/http-messaging/common.pem new file mode 120000 index 000000000..a4433d562 --- /dev/null +++ b/reg-tests/http-messaging/common.pem @@ -0,0 +1 @@ +../ssl/common.pem \ No newline at end of file diff --git a/reg-tests/http-messaging/srv_ws.vtc b/reg-tests/http-messaging/srv_ws.vtc new file mode 100644 index 000000000..bce12f6b1 --- /dev/null +++ b/reg-tests/http-messaging/srv_ws.vtc @@ -0,0 +1,181 @@ +# This reg-test checks websocket support in regards with the server keyword +# 'ws' + +varnishtest "h2 backend websocket management via server keyword" + +feature ignore_unknown_macro + +#REQUIRE_VERSION=2.5 +#REQUIRE_OPTION=OPENSSL + +# haproxy server +haproxy hapsrv -conf { + defaults + mode http + timeout connect 5s + timeout client 5s + timeout server 5s + + frontend fe + bind "fd@${fe}" + bind "fd@${fessl}" ssl crt ${testdir}/common.pem alpn h2,http/1.1 + capture request header sec-websocket-key len 128 + http-request set-var(txn.ver) req.ver + use_backend be + + backend be + # define websocket ACL + acl ws_handshake hdr(upgrade) -m str websocket + + # handle non-ws streams + http-request return status 200 if !ws_handshake + + # handle ws streams + #capture request header sec-websocket-key len 128 + http-request return status 200 hdr connection upgrade hdr upgrade websocket hdr sec-websocket-accept "%[capture.req.hdr(0),concat(258EAFA5-E914-47DA-95CA-C5AB0DC85B11,,),sha1,base64]" if ws_handshake + http-after-response set-status 101 if { status eq 200 } { res.hdr(upgrade) -m str websocket } + http-after-response set-header x-backend-protocol "%[var(txn.ver)]" +} -start + +# haproxy LB +haproxy hap -conf { + defaults + mode http + timeout connect 1s + timeout client 1s + timeout server 1s + + # proto X ws h1 -> websocket on h1 + listen li + bind "fd@${li}" + server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2 ws h1 + + # proto X ws h2 -> websocket on h2 + listen lih2 + bind "fd@${lih2}" + server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2 ws h2 + + # alpn h2,http/1.1 ws h2 -> websocket on h2 + listen lisslh2 + bind "fd@${lisslh2}" + server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1 ws h2 + http-response set-header x-alpn "%[ssl_bc_alpn]" + + # ws auto -> websocket on h1 + listen liauto + bind "fd@${liauto}" + server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} + + # alpn h2,http/1.1 ws auto -> websocket on h1 + listen lissl + bind "fd@${lissl}" + server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1 ws auto + http-response set-header x-alpn "%[ssl_bc_alpn]" + # alpn h2,http/1.1 ws auto -> websocket on h1 + listen lisslauto + bind "fd@${lisslauto}" + server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1 + http-response set-header x-alpn "%[ssl_bc_alpn]" + + # proto h2 ws auto -> websocket on h2 + listen liauto2 + bind "fd@${liauto2}" + server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2 + + # alpn h2 ws auto -> websocket on h2 + listen lisslauto2 + bind "fd@${lisslauto2}" + server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2 ws auto + http-response set-header x-alpn "%[ssl_bc_alpn]" +} -start + +client c1 -connect ${hap_li_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-backend-protocol == "1.1" +} -run + +client c1.2 -connect ${hap_lih2_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-backend-protocol == "2.0" +} -run + +client c1.3 -connect ${hap_liauto_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-backend-protocol == "1.1" +} -run + +client c1.4 -connect ${hap_liauto2_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-backend-protocol == "2.0" +} -run + +client c2 -connect ${hap_lisslauto_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-alpn == "http/1.1" +} -run + +client c2.2 -connect ${hap_lisslauto2_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-alpn == "h2" +} -run + +client c2.3 -connect ${hap_lisslh2_sock} { + txreq \ + -req "GET" \ + -url "/" \ + -hdr "host: 127.0.0.1" \ + -hdr "connection: upgrade" \ + -hdr "upgrade: websocket" \ + -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==" + rxresp + expect resp.status == 101 + expect resp.http.x-alpn == "h2" +} -run diff --git a/src/server.c b/src/server.c index f6e3aa943..600dbfd04 100644 --- a/src/server.c +++ b/src/server.c @@ -505,6 +505,33 @@ static int srv_parse_error_limit(char **args, int *cur_arg, return 0; } +/* Parse the "ws" keyword */ +static int srv_parse_ws(char **args, int *cur_arg, + struct proxy *curproxy, struct server *newsrv, char **err) +{ + if (!args[*cur_arg + 1]) { + memprintf(err, "'%s' expects 'auto', 'h1' or 'h2' value", args[*cur_arg]); + return ERR_ALERT | ERR_FATAL; + } + + if (strcmp(args[*cur_arg + 1], "h1") == 0) { + newsrv->ws = SRV_WS_H1; + } + else if (strcmp(args[*cur_arg + 1], "h2") == 0) { + newsrv->ws = SRV_WS_H2; + } + else if (strcmp(args[*cur_arg + 1], "auto") == 0) { + newsrv->ws = SRV_WS_AUTO; + } + else { + memprintf(err, "'%s' has to be 'auto', 'h1' or 'h2'", args[*cur_arg]); + return ERR_ALERT | ERR_FATAL; + } + + + return 0; +} + /* Parse the "init-addr" server keyword */ static int srv_parse_init_addr(char **args, int *cur_arg, struct proxy *curproxy, struct server *newsrv, char **err) @@ -1733,6 +1760,7 @@ static struct srv_kw_list srv_kws = { "ALL", { }, { { "disabled", srv_parse_disabled, 0, 1, 1 }, /* Start the server in 'disabled' state */ { "enabled", srv_parse_enabled, 0, 1, 1 }, /* Start the server in 'enabled' state */ { "error-limit", srv_parse_error_limit, 1, 1, 1 }, /* Configure the consecutive count of check failures to consider a server on error */ + { "ws", srv_parse_ws, 1, 1, 1 }, /* websocket protocol */ { "id", srv_parse_id, 1, 0, 1 }, /* set id# of server */ { "init-addr", srv_parse_init_addr, 1, 1, 0 }, /* */ { "log-proto", srv_parse_log_proto, 1, 1, 0 }, /* Set the protocol for event messages, only relevant in a ring section */