diff --git a/bin/varnishd/cache/cache_transport.h b/bin/varnishd/cache/cache_transport.h index 0e5f03c65ef..79f0a7b2d5b 100644 --- a/bin/varnishd/cache/cache_transport.h +++ b/bin/varnishd/cache/cache_transport.h @@ -44,6 +44,7 @@ typedef void vtr_sess_panic_f (struct vsb *, const struct sess *); typedef void vtr_req_panic_f (struct vsb *, const struct req *); typedef void vtr_req_fail_f (struct req *, stream_close_t); typedef void vtr_reembark_f (struct worker *, struct req *); +typedef int vtr_poll_f (struct req *); typedef int vtr_minimal_response_f (struct req *, uint16_t status); struct transport { @@ -64,6 +65,7 @@ struct transport { vtr_sess_panic_f *sess_panic; vtr_req_panic_f *req_panic; vtr_reembark_f *reembark; + vtr_poll_f *poll; vtr_minimal_response_f *minimal_response; VTAILQ_ENTRY(transport) list; diff --git a/bin/varnishd/cache/cache_vrt_vcl.c b/bin/varnishd/cache/cache_vrt_vcl.c index c98274e9d57..85b594b7d86 100644 --- a/bin/varnishd/cache/cache_vrt_vcl.c +++ b/bin/varnishd/cache/cache_vrt_vcl.c @@ -42,6 +42,7 @@ #include "vbm.h" #include "cache_director.h" +#include "cache_transport.h" #include "cache_vcl.h" #include "vcc_interface.h" @@ -521,6 +522,40 @@ VRT_VCL_Allow_Discard(struct vclref **refp) FREE_OBJ(ref); } +/*-------------------------------------------------------------------- + */ + +static int +req_poll(struct worker *wrk, struct req *req) +{ + struct req *top; + + /* NB: Since a fail transition leads to vcl_synth, the request may be + * short-circuited twice. + */ + if (req->req_reset) { + wrk->vpi->handling = VCL_RET_FAIL; + return (-1); + } + + top = req->top->topreq; + CHECK_OBJ_NOTNULL(top, REQ_MAGIC); + CHECK_OBJ_NOTNULL(top->transport, TRANSPORT_MAGIC); + + if (!FEATURE(FEATURE_VCL_REQ_RESET)) + return (0); + if (top->transport->poll == NULL) + return (0); + if (top->transport->poll(top) >= 0) + return (0); + + VSLb_ts_req(req, "Reset", W_TIM_real(wrk)); + wrk->stats->req_reset++; + wrk->vpi->handling = VCL_RET_FAIL; + req->req_reset = 1; + return (-1); +} + /*-------------------------------------------------------------------- * Method functions to call into VCL programs. * @@ -552,6 +587,8 @@ vcl_call_method(struct worker *wrk, struct req *req, struct busyobj *bo, CHECK_OBJ_NOTNULL(req->sp, SESS_MAGIC); CHECK_OBJ_NOTNULL(req->vcl, VCL_MAGIC); CHECK_OBJ_NOTNULL(req->top, REQTOP_MAGIC); + if (req_poll(wrk, req)) + return; VCL_Req2Ctx(&ctx, req); } assert(ctx.now != 0); diff --git a/bin/varnishd/http2/cache_http2.h b/bin/varnishd/http2/cache_http2.h index eb6e8f6a9f3..ca2e6599301 100644 --- a/bin/varnishd/http2/cache_http2.h +++ b/bin/varnishd/http2/cache_http2.h @@ -49,6 +49,7 @@ struct h2_error_s { typedef const struct h2_error_s *h2_error; +#define H2_CUSTOM_ERRORS #define H2EC1(U,v,r,d) extern const struct h2_error_s H2CE_##U[1]; #define H2EC2(U,v,r,d) extern const struct h2_error_s H2SE_##U[1]; #define H2EC3(U,v,r,d) H2EC1(U,v,r,d) H2EC2(U,v,r,d) @@ -193,6 +194,15 @@ struct h2_sess { VTAILQ_HEAD(,h2_req) txqueue; h2_error error; + + // rst rate limit parameters, copied from h2_* parameters + vtim_dur rapid_reset; + int64_t rapid_reset_limit; + vtim_dur rapid_reset_period; + + // rst rate limit state + double rst_budget; + vtim_real last_rst; }; #define ASSERT_RXTHR(h2) do {assert(h2->rxthr == pthread_self());} while(0) diff --git a/bin/varnishd/http2/cache_http2_hpack.c b/bin/varnishd/http2/cache_http2_hpack.c index 36570a751a2..9c179064ba5 100644 --- a/bin/varnishd/http2/cache_http2_hpack.c +++ b/bin/varnishd/http2/cache_http2_hpack.c @@ -39,55 +39,90 @@ #include "http2/cache_http2.h" #include "vct.h" +// rfc9113,l,2493,2528 static h2_error h2h_checkhdr(const struct http *hp, const char *b, size_t namelen, size_t len) { const char *p; + enum { + FLD_NAME_FIRST, + FLD_NAME, + FLD_VALUE_FIRST, + FLD_VALUE + } state; CHECK_OBJ_NOTNULL(hp, HTTP_MAGIC); AN(b); assert(namelen >= 2); /* 2 chars from the ': ' that we added */ assert(namelen <= len); + assert(b[namelen - 2] == ':'); + assert(b[namelen - 1] == ' '); if (namelen == 2) { VSLb(hp->vsl, SLT_BogoHeader, "Empty name"); return (H2SE_PROTOCOL_ERROR); } - for (p = b; p < b + len; p++) { - if (p < b + (namelen - 2)) { - /* Check valid name characters */ - if (p == b && *p == ':') - continue; /* pseudo-header */ + // VSLb(hp->vsl, SLT_Debug, "CHDR [%.*s] [%.*s]", + // (int)namelen, b, (int)(len - namelen), b + namelen); + + state = FLD_NAME_FIRST; + for (p = b; p < b + namelen - 2; p++) { + switch(state) { + case FLD_NAME_FIRST: + state = FLD_NAME; + if (*p == ':') + break; + /* FALL_THROUGH */ + case FLD_NAME: if (isupper(*p)) { VSLb(hp->vsl, SLT_BogoHeader, - "Illegal header name (upper-case): %.*s", + "Illegal field header name (upper-case): %.*s", (int)(len > 20 ? 20 : len), b); return (H2SE_PROTOCOL_ERROR); } - if (vct_istchar(*p)) { - /* XXX: vct should have a proper class for - this avoiding two checks */ - continue; + if (!vct_istchar(*p) || *p == ':') { + VSLb(hp->vsl, SLT_BogoHeader, + "Illegal field header name (non-token): %.*s", + (int)(len > 20 ? 20 : len), b); + return (H2SE_PROTOCOL_ERROR); } - VSLb(hp->vsl, SLT_BogoHeader, - "Illegal header name: %.*s", - (int)(len > 20 ? 20 : len), b); - return (H2SE_PROTOCOL_ERROR); - } else if (p < b + namelen) { - /* ': ' added by us */ - assert(*p == ':' || *p == ' '); - } else { - /* Check valid value characters */ - if (!vct_isctl(*p) || vct_issp(*p)) - continue; - VSLb(hp->vsl, SLT_BogoHeader, - "Illegal header value: %.*s", - (int)(len > 20 ? 20 : len), b); - return (H2SE_PROTOCOL_ERROR); + break; + default: + WRONG("http2 field name validation state"); } } + state = FLD_VALUE_FIRST; + for (p = b + namelen; p < b + len; p++) { + switch(state) { + case FLD_VALUE_FIRST: + if (vct_issp(*p)) { + VSLb(hp->vsl, SLT_BogoHeader, + "Illegal field value start %.*s", + (int)(len > 20 ? 20 : len), b); + return (H2SE_PROTOCOL_ERROR); + } + state = FLD_VALUE; + /* FALL_THROUGH */ + case FLD_VALUE: + if (!vct_ishdrval(*p)) { + VSLb(hp->vsl, SLT_BogoHeader, + "Illegal field value %.*s", + (int)(len > 20 ? 20 : len), b); + return (H2SE_PROTOCOL_ERROR); + } + break; + default: + WRONG("http2 field value validation state"); + } + } + if (state == FLD_VALUE && vct_issp(b[len - 1])) { + VSLb(hp->vsl, SLT_BogoHeader, + "Illegal field value (end) %.*s", + (int)(len > 20 ? 20 : len), b); + return (H2SE_PROTOCOL_ERROR); + } return (0); } @@ -271,6 +306,7 @@ h2h_decode_fini(const struct h2_sess *h2) * block. This is a connection level error. * * H2E_PROTOCOL_ERROR: Malformed header or duplicate pseudo-header. + * Violation of field name/value charsets */ h2_error h2h_decode_bytes(struct h2_sess *h2, const uint8_t *in, size_t in_l) diff --git a/bin/varnishd/http2/cache_http2_proto.c b/bin/varnishd/http2/cache_http2_proto.c index c1ced04b680..dc3d4a14476 100644 --- a/bin/varnishd/http2/cache_http2_proto.c +++ b/bin/varnishd/http2/cache_http2_proto.c @@ -47,6 +47,7 @@ #include "vtcp.h" #include "vtim.h" +#define H2_CUSTOM_ERRORS #define H2EC1(U,v,r,d) const struct h2_error_s H2CE_##U[1] = {{#U,d,v,0,1,r}}; #define H2EC2(U,v,r,d) const struct h2_error_s H2SE_##U[1] = {{#U,d,v,1,0,r}}; #define H2EC3(U,v,r,d) H2EC1(U,v,r,d) H2EC2(U,v,r,d) @@ -317,9 +318,46 @@ h2_rx_push_promise(struct worker *wrk, struct h2_sess *h2, struct h2_req *r2) /********************************************************************** */ +static h2_error +h2_rapid_reset(struct worker *wrk, struct h2_sess *h2, struct h2_req *r2) +{ + vtim_real now; + vtim_dur d; + + CHECK_OBJ_NOTNULL(wrk, WORKER_MAGIC); + ASSERT_RXTHR(h2); + CHECK_OBJ_NOTNULL(r2, H2_REQ_MAGIC); + + if (h2->rapid_reset_limit == 0) + return (0); + + now = VTIM_real(); + CHECK_OBJ_NOTNULL(r2->req, REQ_MAGIC); + AN(r2->req->t_first); + if (now - r2->req->t_first > h2->rapid_reset) + return (0); + + d = now - h2->last_rst; + h2->rst_budget += h2->rapid_reset_limit * d / + h2->rapid_reset_period; + h2->rst_budget = vmin_t(double, h2->rst_budget, + h2->rapid_reset_limit); + h2->last_rst = now; + + if (h2->rst_budget < 1.0) { + Lck_Lock(&h2->sess->mtx); + VSLb(h2->vsl, SLT_Error, "H2: Hit RST limit. Closing session."); + Lck_Unlock(&h2->sess->mtx); + return (H2CE_RAPID_RESET); + } + h2->rst_budget -= 1.0; + return (0); +} + static h2_error v_matchproto_(h2_rxframe_f) h2_rx_rst_stream(struct worker *wrk, struct h2_sess *h2, struct h2_req *r2) { + h2_error h2e; CHECK_OBJ_NOTNULL(wrk, WORKER_MAGIC); ASSERT_RXTHR(h2); @@ -329,8 +367,9 @@ h2_rx_rst_stream(struct worker *wrk, struct h2_sess *h2, struct h2_req *r2) return (H2CE_FRAME_SIZE_ERROR); if (r2 == NULL) return (0); + h2e = h2_rapid_reset(wrk, h2, r2); h2_kill_req(wrk, h2, r2, h2_streamerror(vbe32dec(h2->rxf_data))); - return (0); + return (h2e); } /********************************************************************** diff --git a/bin/varnishd/http2/cache_http2_session.c b/bin/varnishd/http2/cache_http2_session.c index 846b319f183..cdd99d3dfb4 100644 --- a/bin/varnishd/http2/cache_http2_session.c +++ b/bin/varnishd/http2/cache_http2_session.c @@ -128,6 +128,14 @@ h2_init_sess(struct sess *sp, h2->remote_settings = H2_proto_settings; h2->decode = decode; + h2->rapid_reset = cache_param->h2_rapid_reset; + h2->rapid_reset_limit = cache_param->h2_rapid_reset_limit; + h2->rapid_reset_period = cache_param->h2_rapid_reset_period; + + h2->rst_budget = h2->rapid_reset_limit; + h2->last_rst = sp->t_open; + AZ(isnan(h2->last_rst)); + AZ(VHT_Init(h2->dectbl, h2->local_settings.header_table_size)); *up = (uintptr_t)h2; @@ -438,6 +446,16 @@ h2_new_session(struct worker *wrk, void *arg) wrk->vsl = NULL; } +static int v_matchproto_(vtr_poll_f) +h2_poll(struct req *req) +{ + struct h2_req *r2; + + CHECK_OBJ_NOTNULL(req, REQ_MAGIC); + CAST_OBJ_NOTNULL(r2, req->transport_priv, H2_REQ_MAGIC); + return (r2->error ? -1 : 1); +} + struct transport HTTP2_transport = { .name = "HTTP/2", .magic = TRANSPORT_MAGIC, @@ -447,4 +465,5 @@ struct transport HTTP2_transport = { .req_body = h2_req_body, .req_fail = h2_req_fail, .sess_panic = h2_sess_panic, + .poll = h2_poll, }; diff --git a/bin/varnishtest/tests/r03996.vtc b/bin/varnishtest/tests/r03996.vtc new file mode 100644 index 00000000000..7faf78316d5 --- /dev/null +++ b/bin/varnishtest/tests/r03996.vtc @@ -0,0 +1,90 @@ +varnishtest "h2 rapid reset" + +barrier b1 sock 2 -cyclic +barrier b2 sock 5 -cyclic + +server s1 { + rxreq + txresp +} -start + +varnish v1 -cliok "param.set feature +http2" +varnish v1 -cliok "param.set debug +syncvsl" +varnish v1 -cliok "param.set h2_rapid_reset_limit 3" +varnish v1 -cliok "param.set h2_rapid_reset 5" + +varnish v1 -vcl+backend { + import vtc; + + sub vcl_recv { + if (req.http.barrier) { + vtc.barrier_sync(req.http.barrier); + } + vtc.barrier_sync("${b2_sock}"); + } + +} -start + +client c1 { + stream 0 { + rxgoaway + expect goaway.err == ENHANCE_YOUR_CALM + } -start + + stream 1 { + txreq -hdr barrier ${b1_sock} + barrier b1 sync + txrst + } -run + stream 3 { + txreq -hdr barrier ${b1_sock} + barrier b1 sync + txrst + } -run + stream 5 { + txreq -hdr barrier ${b1_sock} + barrier b1 sync + txrst + } -run + stream 7 { + txreq -hdr barrier ${b1_sock} + barrier b1 sync + txrst + } -run + + barrier b2 sync + stream 0 -wait +} -run + +varnish v1 -expect sc_rapid_reset == 1 + +varnish v1 -cliok "param.set feature -vcl_req_reset" + +client c2 { + stream 0 { + rxgoaway + expect goaway.err == ENHANCE_YOUR_CALM + } -start + + stream 1 { + txreq + txrst + } -run + stream 3 { + txreq + txrst + } -run + stream 5 { + txreq + txrst + } -run + stream 7 { + txreq + txrst + } -run + + barrier b2 sync + stream 0 -wait +} -run + +varnish v1 -expect sc_rapid_reset == 2 diff --git a/bin/varnishtest/tests/t02014.vtc b/bin/varnishtest/tests/t02014.vtc index f47eed65767..f4083c89077 100644 --- a/bin/varnishtest/tests/t02014.vtc +++ b/bin/varnishtest/tests/t02014.vtc @@ -1,6 +1,12 @@ varnishtest "Exercise h/2 sender flow control code" -barrier b1 sock 3 -cyclic +barrier b1 sock 3 +barrier b2 sock 3 +barrier b3 sock 3 +barrier b4 sock 3 + +barrier b2_err cond 2 +barrier b3_err cond 2 server s1 { rxreq @@ -23,7 +29,9 @@ varnish v1 -vcl+backend { } sub vcl_deliver { - vtc.barrier_sync("${b1_sock}"); + if (req.http.barrier) { + vtc.barrier_sync(req.http.barrier); + } } } -start @@ -43,7 +51,7 @@ client c1 { } -start stream 1 { - txreq + txreq -hdr barrier ${b1_sock} barrier b1 sync delay .5 txwinup -size 256 @@ -59,26 +67,44 @@ client c1 { stream 0 -wait } -run +varnish v1 -vsl_catchup + +logexpect l2 -v v1 -g raw { + expect * * ReqMethod GET + expect * = VCL_call DELIVER +} -start + client c2 { stream 0 { - barrier b1 sync + barrier b2 sync } -start stream 1 { - txreq + txreq -hdr barrier ${b2_sock} + barrier b2_err sync txdata -data "fail" rxrst expect rst.err == STREAM_CLOSED - barrier b1 sync + barrier b2 sync } -run stream 0 -wait -} -run +} -start + +logexpect l2 -wait +barrier b2_err sync + +client c2 -wait + +logexpect l3 -v v1 -g raw { + expect * * ReqMethod POST + expect * = VCL_call DELIVER +} -start client c3 { stream 0 { - barrier b1 sync - barrier b1 sync + barrier b3 sync + barrier b4 sync delay .5 txwinup -size 256 delay .5 @@ -89,17 +115,18 @@ client c3 { } -start stream 1 { - txreq -req "POST" -nostrend + txreq -req "POST" -hdr barrier ${b3_sock} -nostrend txdata -data "ok" + barrier b3_err sync txdata -data "fail" rxrst expect rst.err == STREAM_CLOSED - barrier b1 sync + barrier b3 sync } -run stream 3 { - txreq - barrier b1 sync + txreq -hdr barrier ${b4_sock} + barrier b4 sync delay .5 txwinup -size 256 delay .5 @@ -112,4 +139,9 @@ client c3 { } -run stream 0 -wait -} -run +} -start + +logexpect l3 -wait +barrier b3_err sync + +client c3 -wait diff --git a/bin/varnishtest/tests/t02023.vtc b/bin/varnishtest/tests/t02023.vtc index cfd843da3ec..13c4cb4459f 100644 --- a/bin/varnishtest/tests/t02023.vtc +++ b/bin/varnishtest/tests/t02023.vtc @@ -1,4 +1,4 @@ -varnishtest "Empty pseudo-headers" +varnishtest "Empty and invalid headers" server s1 { rxreq @@ -30,6 +30,7 @@ client c1 { stream 1 { txreq -url "" rxrst + expect rst.err == PROTOCOL_ERROR } -run } -run @@ -37,6 +38,7 @@ client c1 { stream 1 { txreq -scheme "" rxrst + expect rst.err == PROTOCOL_ERROR } -run } -run @@ -44,5 +46,88 @@ client c1 { stream 1 { txreq -req "" rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "empty" "" + rxresp + expect resp.status == 200 + } -run +} -run + +varnish v1 -vsl_catchup + +client c1 { + stream 1 { + txreq -hdr "foo" " bar" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "foo" " " + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr ":foo" "bar" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "foo" "b\x0car" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "f o" "bar" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "f: o" "bar" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "foo" "bar " + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "foo" " bar" + rxrst + expect rst.err == PROTOCOL_ERROR + } -run +} -run + +client c1 { + stream 1 { + txreq -hdr "foo" "bar " + rxrst + expect rst.err == PROTOCOL_ERROR } -run } -run diff --git a/bin/varnishtest/tests/t02025.vtc b/bin/varnishtest/tests/t02025.vtc new file mode 100644 index 00000000000..578dbf5c84b --- /dev/null +++ b/bin/varnishtest/tests/t02025.vtc @@ -0,0 +1,52 @@ +varnishtest "h2 reset interrupt" + +barrier b1 sock 2 +barrier b2 sock 2 + +varnish v1 -cliok "param.set feature +http2" +varnish v1 -cliok "param.set debug +syncvsl" +varnish v1 -vcl { + import vtc; + + backend be none; + + sub vcl_recv { + vtc.barrier_sync("${b1_sock}"); + vtc.barrier_sync("${b2_sock}"); + } + + sub vcl_miss { + vtc.panic("unreachable"); + } +} -start + +logexpect l1 -v v1 -g raw -i Debug { + expect * * Debug "^H2RXF RST_STREAM" +} -start + +client c1 { + stream 1 { + txreq + barrier b1 sync + txrst + } -run + expect_close +} -start + +logexpect l1 -wait +barrier b2 sync + +client c1 -wait + +varnish v1 -vsl_catchup +varnish v1 -expect req_reset == 1 + +# NB: The varnishncsa command below shows a minimal pattern to collect +# "rapid reset" suspects per session, with the IP address. Here rapid +# is interpreted as before a second elapsed. Session VXIDs showing up +# numerous times become increasingly more suspicious. The format can of +# course be extended to add anything else useful for data mining. +shell -expect "1000 ${localhost}" { + varnishncsa -n ${v1_name} -d \ + -q 'Timestamp:Reset[2] < 1.0' -F '%{VSL:Begin[2]}x %h' +} diff --git a/doc/sphinx/reference/vsl.rst b/doc/sphinx/reference/vsl.rst index e8ea7a36d89..98c126d2d1d 100644 --- a/doc/sphinx/reference/vsl.rst +++ b/doc/sphinx/reference/vsl.rst @@ -76,6 +76,11 @@ Resp Restart Client request is being restarted. +Reset + The client closed its connection, reset its stream or caused + a stream error that forced Varnish to reset the stream. Request + processing is interrupted and considered failed. + Pipe handling timestamps ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/include/tbl/feature_bits.h b/include/tbl/feature_bits.h index 68ae398393c..a3df4a03b35 100644 --- a/include/tbl/feature_bits.h +++ b/include/tbl/feature_bits.h @@ -86,6 +86,11 @@ FEATURE_BIT(BUSY_STATS_RATE, busy_stats_rate, "Make busy workers comply with thread_stats_rate." ) +FEATURE_BIT(VCL_REQ_RESET, vcl_req_reset, + "Stop processing client VCL once the client is gone. " + "When this happens MAIN.req_reset is incremented." +) + #undef FEATURE_BIT /*lint -restore */ diff --git a/include/tbl/h2_error.h b/include/tbl/h2_error.h index 2cd06f01107..c6c6bed8918 100644 --- a/include/tbl/h2_error.h +++ b/include/tbl/h2_error.h @@ -147,5 +147,17 @@ H2_ERROR( /* descr */ "Use HTTP/1.1 for the request" ) +#ifdef H2_CUSTOM_ERRORS +H2_ERROR( + /* name */ RAPID_RESET, + /* val */ 11, /* ENHANCE_YOUR_CALM */ + /* types */ 1, + /* reason */ SC_RAPID_RESET, + /* descr */ "http/2 rapid reset detected" +) + +# undef H2_CUSTOM_ERRORS +#endif + #undef H2_ERROR /*lint -restore */ diff --git a/include/tbl/params.h b/include/tbl/params.h index cf46b165846..4ee8d01e131 100644 --- a/include/tbl/params.h +++ b/include/tbl/params.h @@ -316,7 +316,7 @@ PARAM_SIMPLE( /* type */ bytes_u, /* min */ "128b", /* max */ "99999999b", - /* def */ "48k", + /* def */ "64k", /* units */ "bytes", /* descr */ "Maximum size of CLI response. If the response exceeds this " @@ -1243,6 +1243,52 @@ PARAM_SIMPLE( "HTTP2 maximum size of an uncompressed header list." ) +#define H2_RR_INFO \ + "Changes to this parameter affect the default for new HTTP2 " \ + "sessions. vmod_h2(3) can be used to adjust it from VCL." + +PARAM_SIMPLE( + /* name */ h2_rapid_reset, + /* typ */ timeout, + /* min */ "0.000", + /* max */ NULL, + /* def */ "1.000", + /* units */ "seconds", + /* descr */ + "The upper threshold for how soon an http/2 RST_STREAM frame has " + "to be parsed after a HEADERS frame for it to be treated as " + "suspect and subjected to the rate limits specified by " + "h2_rapid_reset_limit and h2_rapid_reset_period.\n" H2_RR_INFO, + /* flags */ EXPERIMENTAL|DELAYED_EFFECT, +) + +PARAM_SIMPLE( + /* name */ h2_rapid_reset_limit, + /* typ */ uint, + /* min */ "0", + /* max */ NULL, + /* def */ "100", + /* units */ NULL, + /* descr */ + "HTTP2 RST Allowance.\n" + "Specifies the maximum number of allowed stream resets issued by\n" + "a client over a time period before the connection is closed.\n" + "Setting this parameter to 0 disables the limit.\n" H2_RR_INFO, + /* flags */ EXPERIMENTAL|DELAYED_EFFECT, +) + +PARAM_SIMPLE( + /* name */ h2_rapid_reset_period, + /* typ */ timeout, + /* min */ "1.000", + /* max */ NULL, + /* def */ "60.000", + /* units */ "seconds", + /* descr */ + "HTTP2 sliding window duration for h2_rapid_reset_limit.\n" H2_RR_INFO, + /* flags */ EXPERIMENTAL|DELAYED_EFFECT|WIZARD, +) + /*-------------------------------------------------------------------- * Memory pool parameters */ @@ -1768,7 +1814,9 @@ PARAM_PRE PARAM_BITS( /* name */ feature, /* fld */ feature_bits, - /* def */ "+validate_headers", + /* def */ + "+validate_headers," + "+vcl_req_reset", /* descr */ "Enable/Disable various minor features.\n" "\tdefault\tSet default value\n" diff --git a/include/tbl/req_flags.h b/include/tbl/req_flags.h index 88eaff6391c..6a9e6acbb18 100644 --- a/include/tbl/req_flags.h +++ b/include/tbl/req_flags.h @@ -40,6 +40,7 @@ REQ_FLAG(is_hit, 0, 0, "") REQ_FLAG(waitinglist, 0, 0, "") REQ_FLAG(want100cont, 0, 0, "") REQ_FLAG(late100cont, 0, 0, "") +REQ_FLAG(req_reset, 0, 0, "") #define REQ_BEREQ_FLAG(lower, vcl_r, vcl_w, doc) \ REQ_FLAG(lower, vcl_r, vcl_w, doc) #include "tbl/req_bereq_flags.h" diff --git a/include/tbl/sess_close.h b/include/tbl/sess_close.h index b91c93951c9..f15d34a64aa 100644 --- a/include/tbl/sess_close.h +++ b/include/tbl/sess_close.h @@ -50,6 +50,7 @@ SESS_CLOSE(PIPE_OVERFLOW, pipe_overflow,1, "Session pipe overflow") SESS_CLOSE(RANGE_SHORT, range_short, 1, "Insufficient data for range") SESS_CLOSE(REQ_HTTP20, req_http20, 1, "HTTP2 not accepted") SESS_CLOSE(VCL_FAILURE, vcl_failure, 1, "VCL failure") +SESS_CLOSE(RAPID_RESET, rapid_reset, 1, "HTTP2 rapid reset") #undef SESS_CLOSE /*lint -restore */ diff --git a/lib/libvsc/VSC_main.vsc b/lib/libvsc/VSC_main.vsc index 047218ad88a..aa2e3674482 100644 --- a/lib/libvsc/VSC_main.vsc +++ b/lib/libvsc/VSC_main.vsc @@ -342,6 +342,15 @@ Number of times an HTTP/2 stream was refused because the queue was too long already. See also parameter thread_queue_limit. +.. varnish_vsc:: req_reset + :group: wrk + :oneliner: Requests reset + + Number of times a client left before the VCL processing of its + requests completed. For HTTP/2 sessions, either the stream was + reset by an RST_STREAM frame from the client, or a stream or + connection error occurred. + .. varnish_vsc:: n_object :type: gauge :group: wrk @@ -631,6 +640,14 @@ Number of session closes with Error VCL_FAILURE (VCL failure) +.. varnish_vsc:: sc_rapid_reset + :level: diag + :oneliner: Session Err RAPID_RESET + + Number of times we failed an http/2 session because it hit its + configured limits for the number of permitted rapid stream + resets. + .. varnish_vsc:: client_resp_500 :level: diag :group: wrk diff --git a/man/Makefile.am b/man/Makefile.am index b78dc7cfe92..a1f4896c110 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -25,7 +25,8 @@ dist_man_MANS = \ vmod_vtc.3 \ vmod_blob.3 \ vmod_unix.3 \ - vmod_proxy.3 + vmod_proxy.3 \ + vmod_h2.3 CLEANFILES = $(dist_man_MANS) @@ -136,4 +137,7 @@ vmod_unix.3: $(top_builddir)/vmod/vmod_unix.man.rst vmod_proxy.3: $(top_builddir)/vmod/vmod_proxy.man.rst $(BUILD_MAN) $? $@ +vmod_h2.3: $(top_builddir)/vmod/vmod_h2.man.rst + $(BUILD_MAN) $? $@ + .NOPATH: $(dist_man_MANS) diff --git a/vmod/Makefile.am b/vmod/Makefile.am index 29ffcf6bd11..da6eaaeffbf 100644 --- a/vmod/Makefile.am +++ b/vmod/Makefile.am @@ -20,6 +20,7 @@ AM_CPPFLAGS = \ vmod_LTLIBRARIES = include $(srcdir)/automake_boilerplate_blob.am +include $(srcdir)/automake_boilerplate_h2.am include $(srcdir)/automake_boilerplate_cookie.am include $(srcdir)/automake_boilerplate_debug.am include $(srcdir)/automake_boilerplate_directors.am diff --git a/vmod/automake_boilerplate_h2.am b/vmod/automake_boilerplate_h2.am new file mode 100644 index 00000000000..905014e239b --- /dev/null +++ b/vmod/automake_boilerplate_h2.am @@ -0,0 +1,35 @@ +# Generated by vmodtool.py --boilerplate. + +vmod_LTLIBRARIES += libvmod_h2.la + +libvmod_h2_la_SOURCES = \ + vmod_h2.c + +libvmod_h2_la_CFLAGS = + +vmodtoolargs_h2 ?= --strict --boilerplate -o vcc_h2_if +vmod_h2_symbols_regex ?= Vmod_h2_Data + +libvmod_h2_la_LDFLAGS = \ + -export-symbols-regex $(vmod_h2_symbols_regex) \ + $(AM_LDFLAGS) \ + $(VMOD_LDFLAGS) + +nodist_libvmod_h2_la_SOURCES = vcc_h2_if.c vcc_h2_if.h + +EXTRA_libvmod_h2_la_DEPENDENCIES = $(nodist_libvmod_h2_la_SOURCES) + +EXTRA_DIST += $(srcdir)/vmod_h2.vcc automake_boilerplate_h2.am + +$(libvmod_h2_la_OBJECTS): vcc_h2_if.h + +vcc_h2_if.h vmod_h2.rst vmod_h2.man.rst: vcc_h2_if.c + +vcc_h2_if.c: $(vmodtool) $(srcdir)/vmod_h2.vcc + @PYTHON@ $(vmodtool) $(vmodtoolargs_h2) $(srcdir)/vmod_h2.vcc + +clean-local: clean-vmod-h2 + +clean-vmod-h2: + rm -f $(nodist_libvmod_h2_la_SOURCES) + rm -f vmod_h2.rst vmod_h2.man.rst diff --git a/vmod/tests/h2_b00000.vtc b/vmod/tests/h2_b00000.vtc new file mode 100644 index 00000000000..aedb6aad8c5 --- /dev/null +++ b/vmod/tests/h2_b00000.vtc @@ -0,0 +1,90 @@ +varnishtest "VMOD h2 basics" + +varnish v1 -arg "-p feature=+http2" -vcl { + import h2; + + backend proforma none; + + sub vcl_recv { + return(synth(200)); + } + + sub vcl_synth { + set resp.http.http2-is = h2.is(); + set resp.body = ""; + return (deliver); + } +} -start + +client c1 { + txreq + rxresp + expect resp.status == 200 + expect resp.http.http2-is == false +} -start + +client c2 { + stream 7 { + txreq + rxresp + expect resp.status == 200 + expect resp.http.http2-is == true + } -run +} -start + +client c1 -wait +client c2 -wait + +# coverage +varnish v1 -vcl { + import h2; + + backend proforma none; + + sub vcl_recv { + return(synth(200)); + } + + sub vcl_synth { + set resp.http.rapid-reset-o = h2.rapid_reset(10ms); + set resp.http.rapid-reset-n = h2.rapid_reset(); + set resp.http.rapid-reset-limit-o = h2.rapid_reset_limit(10); + set resp.http.rapid-reset-limit-n = h2.rapid_reset_limit(); + set resp.http.rapid-reset-period-o = h2.rapid_reset_period(10s); + set resp.http.rapid-reset-period-n = h2.rapid_reset_period(); + set resp.http.rapid-reset-budget = h2.rapid_reset_budget(); + set resp.body = ""; + return (deliver); + } +} + +client c1 { + txreq + rxresp + expect resp.status == 200 + expect resp.http.rapid-reset-o == -1.000 + expect resp.http.rapid-reset-n == -1.000 + expect resp.http.rapid-reset-limit-o == -1 + expect resp.http.rapid-reset-limit-n == -1 + expect resp.http.rapid-reset-period-o == -1.000 + expect resp.http.rapid-reset-period-n == -1.000 + expect resp.http.rapid-reset-budget == -1.000 +} -start + +client c2 { + stream 7 { + txreq + rxresp + expect resp.status == 200 + expect resp.http.rapid-reset-o == 1.000 + expect resp.http.rapid-reset-n == 0.010 + expect resp.http.rapid-reset-limit-o == 100 + expect resp.http.rapid-reset-limit-n == 10 + expect resp.http.rapid-reset-period-o == 60.000 + expect resp.http.rapid-reset-period-n == 10.000 + expect resp.http.rapid-reset-budget == 10.000 + } -run +} -start + +client c1 -wait +client c2 -wait diff --git a/vmod/vmod_h2.c b/vmod/vmod_h2.c new file mode 100644 index 00000000000..f62105384dd --- /dev/null +++ b/vmod/vmod_h2.c @@ -0,0 +1,107 @@ +/*- + * Copyright 2023 UPLEX - Nils Goroll Systemoptimierung + * All rights reserved. + * + * Author: Nils Goroll + * + * SPDX-License-Identifier: BSD-2-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "config.h" + +#include "cache/cache_varnishd.h" + +#include "vcc_h2_if.h" + +#include "cache/cache_transport.h" +#include "http2/cache_http2.h" + +static struct h2_sess * +h2get(VRT_CTX) +{ + struct h2_sess *h2; + uintptr_t *up; + + CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC); + if (ctx->req == NULL) { + VRT_fail(ctx, + "vmod_h2 can only be called from client-side VCL."); + return (NULL); + } + CHECK_OBJ_NOTNULL(ctx->req, REQ_MAGIC); + if (ctx->req->transport != &HTTP2_transport) + return (NULL); + AZ(SES_Get_proto_priv(ctx->req->sp, &up)); + CAST_OBJ_NOTNULL(h2, (void *)*up, H2_SESS_MAGIC); + return (h2); +} +VCL_BOOL +vmod_is(VRT_CTX) +{ + struct h2_sess *h2 = h2get(ctx); + + return (h2 != NULL); +} + +#define GETSET(type, name, argname) \ +type \ +vmod_ ## name(VRT_CTX, struct VARGS(name) *args) \ +{ \ + struct h2_sess *h2 = h2get(ctx); \ + type r; \ + \ + (void)args; \ + \ + if (h2 == NULL) \ + return (-1); \ + \ + if (! args->valid_ ## argname) \ + return (h2->name); \ + if (h2->name == args->argname) \ + return (h2->name); \ + \ + Lck_Lock(&h2->sess->mtx); \ + r = h2->name; \ + if (h2->name != args->argname) { \ + h2->name = args->argname; \ + h2->rst_budget = h2->rapid_reset_limit; \ + h2->last_rst = ctx->now; \ + } \ + Lck_Unlock(&h2->sess->mtx); \ + return (r); \ +} + +GETSET(VCL_DURATION, rapid_reset, threshold) +GETSET(VCL_INT, rapid_reset_limit, number) +GETSET(VCL_DURATION, rapid_reset_period, duration) + +VCL_REAL +vmod_rapid_reset_budget(VRT_CTX) +{ + struct h2_sess *h2 = h2get(ctx); + + if (h2 == NULL) + return (-1); + + return (h2->rst_budget); +} diff --git a/vmod/vmod_h2.vcc b/vmod/vmod_h2.vcc new file mode 100644 index 00000000000..b96ac5a8bf4 --- /dev/null +++ b/vmod/vmod_h2.vcc @@ -0,0 +1,86 @@ +#- +# Copyright 2023 UPLEX - Nils Goroll Systemoptimierung +# All rights reserved. +# +# Author: Nils Goroll +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +$ABI strict +$Module h2 3 "Module to control the built-in HTTP2 transport" + +DESCRIPTION +=========== + +This VMOD contains functions to control the HTTP2 transport built into +Varnish-Cache. Using VMOD outside of a client context will immediately +fail the VCL transaction. + +$Function BOOL is() + +Returns true when called on a session handled by the built-in HTTP2 transport. + +$Function DURATION rapid_reset([DURATION threshold]) + +Get and optionally set the ``h2_rapid_reset`` parameter (See +:ref:`varnishd(1)`) for this HTTP2 session only. + +Returns -1 when used outside the HTTP2 transport. Otherwise returns +the previous value. + +If the call leads to a change in the rate limit parameters, the +current budget as retuned by `h2.rapid_reset_budget()`_ is reset. + +$Function INT rapid_reset_limit([INT number]) + +Get and optionally set the ``h2_rapid_reset_limit`` parameter (See +:ref:`varnishd(1)`) for this HTTP2 session only. + +Returns -1 when used outside the HTTP2 transport. Otherwise returns +the previous value. + +If the call leads to a change in the rate limit parameters, the +current budget as retuned by `h2.rapid_reset_budget()`_ is reset. + +$Function DURATION rapid_reset_period([DURATION duration]) + +Get and optionally set the ``h2_rapid_reset_period`` parameter (See +:ref:`varnishd(1)`) for this HTTP2 session only. + +Returns -1 when used outside the HTTP2 transport. Otherwise returns +the previous value. + +If the call leads to a change in the rate limit parameters, the +current budget as retuned by `h2.rapid_reset_budget()`_ is reset. + +$Function REAL rapid_reset_budget() + +Return how many RST frames classified as "rapid" the client is still +allowed to send before the session is going to be closed. + +SEE ALSO +======== + +* :ref:`varnishd(1)` +* :ref:`vsl(7)`