blob: 136be5c2d67b15e1241f9f108d541835094b220d [file] [log] [blame]
Christopher Faulet2b677702022-06-22 16:55:04 +02001/*
2 * Bandwidth limitation filter.
3 *
4 * Copyright 2022 HAProxy Technologies, Christopher Faulet <cfaulet@haproxy.com>
5 *
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version
10 * 2 of the License, or (at your option) any later version.
11 *
12 */
13
14#include <ctype.h>
15
16#include <haproxy/api.h>
17#include <haproxy/channel-t.h>
18#include <haproxy/filters.h>
19#include <haproxy/global.h>
20#include <haproxy/http_ana-t.h>
21#include <haproxy/http_rules.h>
22#include <haproxy/proxy.h>
23#include <haproxy/sample.h>
24#include <haproxy/stream.h>
25#include <haproxy/tcp_rules.h>
26#include <haproxy/time.h>
27#include <haproxy/tools.h>
28
29const char *bwlim_flt_id = "bandwidth limitation filter";
30
31struct flt_ops bwlim_ops;
32
33#define BWLIM_FL_NONE 0x00000000 /* For init purposr */
34#define BWLIM_FL_IN 0x00000001 /* Limit clients uploads */
35#define BWLIM_FL_OUT 0x00000002 /* Limit clients downloads */
36#define BWLIM_FL_SHARED 0x00000004 /* Limit shared between clients (using stick-tables) */
37
Christopher Fauletda2e1172023-01-13 15:33:32 +010038#define BWLIM_ACT_LIMIT_EXPR 0x00000001
39#define BWLIM_ACT_LIMIT_CONST 0x00000002
40#define BWLIM_ACT_PERIOD_EXPR 0x00000004
41#define BWLIM_ACT_PERIOD_CONST 0x00000008
42
Christopher Faulet2b677702022-06-22 16:55:04 +020043struct bwlim_config {
44 struct proxy *proxy;
45 char *name;
46 unsigned int flags;
47 struct sample_expr *expr;
48 union {
49 char *n;
50 struct stktable *t;
51 } table;
52 unsigned int period;
53 unsigned int limit;
54 unsigned int min_size;
55};
56
57struct bwlim_state {
58 struct freq_ctr bytes_rate;
59 struct stksess *ts;
60 struct act_rule *rule;
61 unsigned int limit;
62 unsigned int period;
63 unsigned int exp;
64};
65
66
67/* Pools used to allocate comp_state structs */
68DECLARE_STATIC_POOL(pool_head_bwlim_state, "bwlim_state", sizeof(struct bwlim_state));
69
70
71/* Apply the bandwidth limitation of the filter <filter>. <len> is the maximum
72 * amount of data that the filter can forward. This function applies the
73 * limitation and returns what the stream is authorized to forward. Several
74 * limitation can be stacked.
75 */
76static int bwlim_apply_limit(struct filter *filter, struct channel *chn, unsigned int len)
77{
78 struct bwlim_config *conf = FLT_CONF(filter);
79 struct bwlim_state *st = filter->ctx;
80 struct freq_ctr *bytes_rate;
81 unsigned int period, limit, remain, tokens, users;
82 unsigned int wait = 0;
83 int overshoot, ret = 0;
84
85 /* Don't forward anything if there is nothing to forward or the waiting
86 * time is not expired
87 */
88 if (!len || (tick_isset(st->exp) && !tick_is_expired(st->exp, now_ms)))
89 goto end;
90
91 st->exp = TICK_ETERNITY;
92 ret = len;
93 if (conf->flags & BWLIM_FL_SHARED) {
94 void *ptr;
95 unsigned int type = ((conf->flags & BWLIM_FL_IN) ? STKTABLE_DT_BYTES_IN_RATE : STKTABLE_DT_BYTES_OUT_RATE);
96
97 /* In shared mode, get a pointer on the stick table entry. it
98 * will be used to get the freq-counter. It is also used to get
99 * The number of users.
100 */
101 ptr = stktable_data_ptr(conf->table.t, st->ts, type);
102 if (!ptr)
103 goto end;
104
105 HA_RWLOCK_WRLOCK(STK_SESS_LOCK, &st->ts->lock);
106 bytes_rate = &stktable_data_cast(ptr, std_t_frqp);
107 period = conf->table.t->data_arg[type].u;
108 limit = conf->limit;
109 users = st->ts->ref_cnt;
110 }
111 else {
112 /* On per-stream mode, the freq-counter is private to the
113 * stream. Get it from the filter state. Rely on the custom
Ilya Shipitsin3b64a282022-07-29 22:26:53 +0500114 * limit/period if defined or use the default ones. In this mode,
Christopher Faulet2b677702022-06-22 16:55:04 +0200115 * there is only one user.
116 */
117 bytes_rate = &st->bytes_rate;
118 period = (st->period ? st->period : conf->period);
119 limit = (st->limit ? st->limit : conf->limit);
120 users = 1;
121 }
122
123 /* Be sure the current rate does not exceed the limit over the current
124 * period. In this case, nothing is forwarded and the waiting time is
125 * computed to be sure to not retry too early.
126 *
127 * The test is used to avoid the initial burst. Otherwise, streams will
128 * consume the limit as fast as possible and will then be paused for
129 * long time.
130 */
131 overshoot = freq_ctr_overshoot_period(bytes_rate, period, limit);
132 if (overshoot > 0) {
133 if (conf->flags & BWLIM_FL_SHARED)
134 HA_RWLOCK_WRUNLOCK(STK_SESS_LOCK, &st->ts->lock);
135 wait = div64_32((uint64_t)(conf->min_size + overshoot) * period * users,
136 limit);
137 st->exp = tick_add(now_ms, (wait ? wait : 1));
138 ret = 0;
139 goto end;
140 }
141
142 /* Get the allowed quota per user. */
143 remain = freq_ctr_remain_period(bytes_rate, period, limit, 0);
144 tokens = div64_32((uint64_t)(remain + users - 1), users);
145
146 if (tokens < len) {
147 /* The stream cannot forward all its data. But we will check if
148 * it can perform a small burst if the global quota is large
Ilya Shipitsin3b64a282022-07-29 22:26:53 +0500149 * enough. But, in this case, its waiting time will be
Christopher Faulet2b677702022-06-22 16:55:04 +0200150 * increased accordingly.
151 */
152 ret = tokens;
153 if (tokens < conf->min_size) {
154 ret = (chn->flags & (CF_EOI|CF_SHUTR|CF_READ_ERROR))
155 ? MIN(len, conf->min_size)
156 : conf->min_size;
157
158 if (ret <= remain)
159 wait = div64_32((uint64_t)(ret - tokens) * period * users + limit - 1, limit);
160 else
161 ret = (limit < ret) ? remain : 0;
162 }
163 }
164
165 /* At the end, update the freq-counter and compute the waiting time if
166 * the stream is limited
167 */
168 update_freq_ctr_period(bytes_rate, period, ret);
169 if (ret < len) {
170 wait += next_event_delay_period(bytes_rate, period, limit, MIN(len - ret, conf->min_size * users));
171 st->exp = tick_add(now_ms, (wait ? wait : 1));
172 }
173
174 if (conf->flags & BWLIM_FL_SHARED)
175 HA_RWLOCK_WRUNLOCK(STK_SESS_LOCK, &st->ts->lock);
176
177 end:
178 chn->analyse_exp = tick_first((tick_is_expired(chn->analyse_exp, now_ms) ? TICK_ETERNITY : chn->analyse_exp),
179 st->exp);
180 return ret;
181}
182
183/***************************************************************************
184 * Hooks that manage the filter lifecycle (init/check/deinit)
185 **************************************************************************/
186/* Initialize the filter. Returns -1 on error, else 0. */
187static int bwlim_init(struct proxy *px, struct flt_conf *fconf)
188{
189 fconf->flags |= FLT_CFG_FL_HTX;
190 return 0;
191}
192
193/* Free resources allocated by the bwlim filter. */
194static void bwlim_deinit(struct proxy *px, struct flt_conf *fconf)
195{
196 struct bwlim_config *conf = fconf->conf;
197
198 if (conf) {
Christopher Fauletf0196f42022-06-24 14:52:18 +0200199 ha_free(&conf->name);
Christopher Faulet2b677702022-06-22 16:55:04 +0200200 release_sample_expr(conf->expr);
Christopher Fauletf0196f42022-06-24 14:52:18 +0200201 conf->expr = NULL;
202 ha_free(&fconf->conf);
Christopher Faulet2b677702022-06-22 16:55:04 +0200203 }
204}
205
206/* Check configuration of a bwlim filter for a specified proxy.
207 * Return 1 on error, else 0. */
208static int bwlim_check(struct proxy *px, struct flt_conf *fconf)
209{
210 struct bwlim_config *conf = fconf->conf;
211 struct stktable *target;
212
213 if (!(conf->flags & BWLIM_FL_SHARED))
214 return 0;
215
216 if (conf->table.n)
217 target = stktable_find_by_name(conf->table.n);
218 else
219 target = px->table;
220
221 if (!target) {
222 ha_alert("Proxy %s : unable to find table '%s' referenced by bwlim filter '%s'",
223 px->id, conf->table.n ? conf->table.n : px->id, conf->name);
224 return 1;
225 }
226
227 if ((conf->flags & BWLIM_FL_IN) && !target->data_ofs[STKTABLE_DT_BYTES_IN_RATE]) {
228 ha_alert("Proxy %s : stick-table '%s' uses a data type incompatible with bwlim filter '%s'."
229 " It must be 'bytes_in_rate'",
230 px->id, conf->table.n ? conf->table.n : px->id, conf->name);
231 return 1;
232 }
233 else if ((conf->flags & BWLIM_FL_OUT) && !target->data_ofs[STKTABLE_DT_BYTES_OUT_RATE]) {
234 ha_alert("Proxy %s : stick-table '%s' uses a data type incompatible with bwlim filter '%s'."
235 " It must be 'bytes_out_rate'",
236 px->id, conf->table.n ? conf->table.n : px->id, conf->name);
237 return 1;
238 }
239
240 if (!stktable_compatible_sample(conf->expr, target->type)) {
241 ha_alert("Proxy %s : stick-table '%s' uses a key type incompatible with bwlim filter '%s'",
242 px->id, conf->table.n ? conf->table.n : px->id, conf->name);
243 return 1;
244 }
245 else {
246 if (!in_proxies_list(target->proxies_list, px)) {
247 px->next_stkt_ref = target->proxies_list;
248 target->proxies_list = px;
249 }
Christopher Fauletf0196f42022-06-24 14:52:18 +0200250 ha_free(&conf->table.n);
Christopher Faulet2b677702022-06-22 16:55:04 +0200251 conf->table.t = target;
252 }
253
254 return 0;
255}
256
257/**************************************************************************
258 * Hooks to handle start/stop of streams
259 *************************************************************************/
260/* Called when a filter instance is created and attach to a stream */
261static int bwlim_attach(struct stream *s, struct filter *filter)
262{
263 struct bwlim_state *st;
264
265 st = pool_zalloc(pool_head_bwlim_state);
266 if (!st)
267 return -1;
268 filter->ctx = st;
269 return 1;
270}
271
272/* Called when a filter instance is detach from a stream, just before its
273 * destruction */
274static void bwlim_detach(struct stream *s, struct filter *filter)
275{
276 struct bwlim_config *conf = FLT_CONF(filter);
277 struct bwlim_state *st = filter->ctx;
278 struct stktable *t = conf->table.t;
279
280 if (!st)
281 return;
282
283 if (st->ts)
284 stktable_touch_local(t, st->ts, 1);
285
286 /* release any possible compression context */
287 pool_free(pool_head_bwlim_state, st);
288 filter->ctx = NULL;
289}
290
291/**************************************************************************
292 * Hooks to filter HTTP messages
293 *************************************************************************/
294static int bwlim_http_headers(struct stream *s, struct filter *filter, struct http_msg *msg)
295{
296 msg->chn->analyse_exp = TICK_ETERNITY;
297 return 1;
298}
299
300static int bwlim_http_payload(struct stream *s, struct filter *filter, struct http_msg *msg,
301 unsigned int offset, unsigned int len)
302{
303 return bwlim_apply_limit(filter, msg->chn, len);
304}
305
306/**************************************************************************
307 * Hooks to filter TCP data
308 *************************************************************************/
309static int bwlim_tcp_payload(struct stream *s, struct filter *filter, struct channel *chn,
310 unsigned int offset, unsigned int len)
311{
312 return bwlim_apply_limit(filter, chn, len);
313}
314
315/********************************************************************
316 * Functions that manage the filter initialization
317 ********************************************************************/
318struct flt_ops bwlim_ops = {
319 /* Manage bwlim filter, called for each filter declaration */
320 .init = bwlim_init,
321 .deinit = bwlim_deinit,
322 .check = bwlim_check,
323
324 /* Handle start/stop of streams */
325 .attach = bwlim_attach,
326 .detach = bwlim_detach,
327
328
329 /* Filter HTTP requests and responses */
330 .http_headers = bwlim_http_headers,
331 .http_payload = bwlim_http_payload,
332
333 /* Filter TCP data */
334 .tcp_payload = bwlim_tcp_payload,
335};
336
337/* Set a bandwidth limitation. It always return ACT_RET_CONT. On error, the rule
338 * is ignored. First of all, it looks for the corresponding filter. Then, for a
339 * shared limitation, the stick-table entry is retrieved. For a per-stream
340 * limitation, the custom limit and period are computed, if necessary. At the
341 * end, the filter is registered on the data filtering for the right channel
342 * (bwlim-in = request, bwlim-out = response).
343 */
344static enum act_return bwlim_set_limit(struct act_rule *rule, struct proxy *px,
345 struct session *sess, struct stream *s, int flags)
346{
347 struct bwlim_config *conf = rule->arg.act.p[3];
348 struct filter *filter;
349 struct bwlim_state *st = NULL;
350 struct stktable *t;
351 struct stktable_key *key;
352 struct stksess *ts;
353 int opt;
354
355 list_for_each_entry(filter, &s->strm_flt.filters, list) {
356 if (FLT_ID(filter) == bwlim_flt_id && FLT_CONF(filter) == conf) {
357 st = filter->ctx;
358 break;
359 }
360 }
361
362 if (!st)
363 goto end;
364
365 switch (rule->from) {
366 case ACT_F_TCP_REQ_CNT: opt = SMP_OPT_DIR_REQ | SMP_OPT_FINAL; break;
367 case ACT_F_TCP_RES_CNT: opt = SMP_OPT_DIR_RES | SMP_OPT_FINAL; break;
368 case ACT_F_HTTP_REQ: opt = SMP_OPT_DIR_REQ | SMP_OPT_FINAL; break;
369 case ACT_F_HTTP_RES: opt = SMP_OPT_DIR_RES | SMP_OPT_FINAL; break;
370 default:
371 goto end;
372 }
373
374 if (conf->flags & BWLIM_FL_SHARED) {
375 t = conf->table.t;
376 key = stktable_fetch_key(t, px, sess, s, opt, conf->expr, NULL);
377 if (!key)
378 goto end;
379
380 ts = stktable_get_entry(t, key);
381 if (!ts)
382 goto end;
383
384 st->ts = ts;
385 st->rule = rule;
386 }
387 else {
388 struct sample *smp;
389
390 st->limit = 0;
391 st->period = 0;
Christopher Fauletda2e1172023-01-13 15:33:32 +0100392 if (rule->action & BWLIM_ACT_LIMIT_EXPR) {
Christopher Faulet2b677702022-06-22 16:55:04 +0200393 smp = sample_fetch_as_type(px, sess, s, opt, rule->arg.act.p[1], SMP_T_SINT);
394 if (smp && smp->data.u.sint > 0)
395 st->limit = smp->data.u.sint;
396 }
Christopher Fauletda2e1172023-01-13 15:33:32 +0100397 else if (rule->action & BWLIM_ACT_LIMIT_CONST)
398 st->limit = (uintptr_t)rule->arg.act.p[1];
399
400 if (rule->action & BWLIM_ACT_PERIOD_EXPR) {
Christopher Faulet2b677702022-06-22 16:55:04 +0200401 smp = sample_fetch_as_type(px, sess, s, opt, rule->arg.act.p[2], SMP_T_SINT);
402 if (smp && smp->data.u.sint > 0)
403 st->period = smp->data.u.sint;
404 }
Christopher Fauletda2e1172023-01-13 15:33:32 +0100405 else if (rule->action & BWLIM_ACT_PERIOD_CONST)
406 st->period = (uintptr_t)rule->arg.act.p[2];
Christopher Faulet2b677702022-06-22 16:55:04 +0200407 }
408
409 st->exp = TICK_ETERNITY;
410 if (conf->flags & BWLIM_FL_IN)
411 register_data_filter(s, &s->req, filter);
412 else
413 register_data_filter(s, &s->res, filter);
414
415 end:
416 return ACT_RET_CONT;
417}
418
419/* Check function for "set-bandwidth-limit" aciton. It returns 1 on
420 * success. Otherwise, it returns 0 and <err> is filled.
421 */
422int check_bwlim_action(struct act_rule *rule, struct proxy *px, char **err)
423{
424 struct flt_conf *fconf;
425 struct bwlim_config *conf = NULL;
426 unsigned int where;
427
428 list_for_each_entry(fconf, &px->filter_configs, list) {
429 conf = NULL;
430 if (fconf->id == bwlim_flt_id) {
431 conf = fconf->conf;
Tim Duesterhusa6fc6162022-10-08 12:33:19 +0200432 if (strcmp(rule->arg.act.p[0], conf->name) == 0)
Christopher Faulet2b677702022-06-22 16:55:04 +0200433 break;
434 }
435 }
436 if (!conf) {
437 memprintf(err, "unable to find bwlim filter '%s' referenced by set-bandwidth-limit rule",
438 (char *)rule->arg.act.p[0]);
439 return 0;
440 }
441
442 if ((conf->flags & BWLIM_FL_SHARED) && rule->arg.act.p[1]) {
443 memprintf(err, "set-bandwidth-limit rule cannot define a limit for a shared bwlim filter");
444 return 0;
445 }
446
Christopher Fauletab34ebe2023-01-13 15:21:53 +0100447 if ((conf->flags & BWLIM_FL_SHARED) && rule->arg.act.p[2]) {
448 memprintf(err, "set-bandwidth-limit rule cannot define a period for a shared bwlim filter");
449 return 0;
450 }
451
Christopher Faulet2b677702022-06-22 16:55:04 +0200452 where = 0;
Christopher Faulet6bf86c72023-01-13 15:39:54 +0100453 if (px->cap & PR_CAP_FE) {
454 if (rule->from == ACT_F_TCP_REQ_CNT)
455 where |= SMP_VAL_FE_REQ_CNT;
456 else if (rule->from == ACT_F_HTTP_REQ)
457 where |= SMP_VAL_FE_HRQ_HDR;
458 else if (rule->from == ACT_F_TCP_RES_CNT)
459 where |= SMP_VAL_FE_RES_CNT;
460 else if (rule->from == ACT_F_HTTP_RES)
461 where |= SMP_VAL_FE_HRS_HDR;
462 }
463 if (px->cap & PR_CAP_BE) {
464 if (rule->from == ACT_F_TCP_REQ_CNT)
465 where |= SMP_VAL_BE_REQ_CNT;
466 else if (rule->from == ACT_F_HTTP_REQ)
467 where |= SMP_VAL_BE_HRQ_HDR;
468 else if (rule->from == ACT_F_TCP_RES_CNT)
469 where |= SMP_VAL_BE_RES_CNT;
470 else if (rule->from == ACT_F_HTTP_RES)
471 where |= SMP_VAL_BE_HRS_HDR;
472 }
Christopher Faulet2b677702022-06-22 16:55:04 +0200473
Christopher Fauletda2e1172023-01-13 15:33:32 +0100474 if ((rule->action & BWLIM_ACT_LIMIT_EXPR) && rule->arg.act.p[1]) {
Christopher Faulet2b677702022-06-22 16:55:04 +0200475 struct sample_expr *expr = rule->arg.act.p[1];
476
477 if (!(expr->fetch->val & where)) {
478 memprintf(err, "set-bandwidth-limit rule uses a limit extracting information from '%s', none of which is available here",
479 sample_src_names(expr->fetch->use));
480 return 0;
481 }
482
483 if (rule->from == ACT_F_TCP_REQ_CNT && (px->cap & PR_CAP_FE)) {
484 if (!px->tcp_req.inspect_delay && !(expr->fetch->val & SMP_VAL_FE_SES_ACC)) {
485 ha_warning("%s '%s' : a 'tcp-request content set-bandwidth-limit*' rule explicitly depending on request"
486 " contents without any 'tcp-request inspect-delay' setting."
487 " This means that this rule will randomly find its contents. This can be fixed by"
488 " setting the tcp-request inspect-delay.\n",
489 proxy_type_str(px), px->id);
490 }
491 }
Christopher Faulet6bf86c72023-01-13 15:39:54 +0100492 if (rule->from == ACT_F_TCP_RES_CNT && (px->cap & PR_CAP_BE)) {
493 if (!px->tcp_rep.inspect_delay && !(expr->fetch->val & SMP_VAL_BE_SRV_CON)) {
494 ha_warning("%s '%s' : a 'tcp-response content set-bandwidth-limit*' rule explicitly depending on response"
495 " contents without any 'tcp-response inspect-delay' setting."
496 " This means that this rule will randomly find its contents. This can be fixed by"
497 " setting the tcp-response inspect-delay.\n",
498 proxy_type_str(px), px->id);
499 }
500 }
Christopher Faulet2b677702022-06-22 16:55:04 +0200501 }
502
Christopher Fauletda2e1172023-01-13 15:33:32 +0100503 if ((rule->action & BWLIM_ACT_PERIOD_EXPR) && rule->arg.act.p[2]) {
Christopher Fauletab34ebe2023-01-13 15:21:53 +0100504 struct sample_expr *expr = rule->arg.act.p[2];
505
506 if (!(expr->fetch->val & where)) {
507 memprintf(err, "set-bandwidth-limit rule uses a period extracting information from '%s', none of which is available here",
508 sample_src_names(expr->fetch->use));
509 return 0;
510 }
511
512 if (rule->from == ACT_F_TCP_REQ_CNT && (px->cap & PR_CAP_FE)) {
513 if (!px->tcp_req.inspect_delay && !(expr->fetch->val & SMP_VAL_FE_SES_ACC)) {
514 ha_warning("%s '%s' : a 'tcp-request content set-bandwidth-limit*' rule explicitly depending on request"
515 " contents without any 'tcp-request inspect-delay' setting."
516 " This means that this rule will randomly find its contents. This can be fixed by"
517 " setting the tcp-request inspect-delay.\n",
518 proxy_type_str(px), px->id);
Christopher Faulet6bf86c72023-01-13 15:39:54 +0100519 }
520 }
521 if (rule->from == ACT_F_TCP_RES_CNT && (px->cap & PR_CAP_BE)) {
522 if (!px->tcp_rep.inspect_delay && !(expr->fetch->val & SMP_VAL_BE_SRV_CON)) {
523 ha_warning("%s '%s' : a 'tcp-response content set-bandwidth-limit*' rule explicitly depending on response"
524 " contents without any 'tcp-response inspect-delay' setting."
525 " This means that this rule will randomly find its contents. This can be fixed by"
526 " setting the tcp-response inspect-delay.\n",
527 proxy_type_str(px), px->id);
Christopher Fauletab34ebe2023-01-13 15:21:53 +0100528 }
529 }
530 }
531
Christopher Faulet2b677702022-06-22 16:55:04 +0200532 if (conf->expr) {
533 if (!(conf->expr->fetch->val & where)) {
534 memprintf(err, "bwlim filter '%s uses a key extracting information from '%s', none of which is available here",
535 conf->name, sample_src_names(conf->expr->fetch->use));
536 return 0;
537 }
538
539 if (rule->from == ACT_F_TCP_REQ_CNT && (px->cap & PR_CAP_FE)) {
540 if (!px->tcp_req.inspect_delay && !(conf->expr->fetch->val & SMP_VAL_FE_SES_ACC)) {
541 ha_warning("%s '%s' : a 'tcp-request content set-bandwidth-limit*' rule explicitly depending on request"
542 " contents without any 'tcp-request inspect-delay' setting."
543 " This means that this rule will randomly find its contents. This can be fixed by"
544 " setting the tcp-request inspect-delay.\n",
545 proxy_type_str(px), px->id);
546 }
547 }
Christopher Faulet6bf86c72023-01-13 15:39:54 +0100548 if (rule->from == ACT_F_TCP_RES_CNT && (px->cap & PR_CAP_BE)) {
549 if (!px->tcp_rep.inspect_delay && !(conf->expr->fetch->val & SMP_VAL_BE_SRV_CON)) {
550 ha_warning("%s '%s' : a 'tcp-response content set-bandwidth-limit*' rule explicitly depending on response"
551 " contents without any 'tcp-response inspect-delay' setting."
552 " This means that this rule will randomly find its contents. This can be fixed by"
553 " setting the tcp-response inspect-delay.\n",
554 proxy_type_str(px), px->id);
555 }
556 }
Christopher Faulet2b677702022-06-22 16:55:04 +0200557 }
558
559 end:
560 rule->arg.act.p[3] = conf;
561 return 1;
562}
563
564/* Release memory allocated by "set-bandwidth-limit" action. */
565static void release_bwlim_action(struct act_rule *rule)
566{
Christopher Fauletf0196f42022-06-24 14:52:18 +0200567 ha_free(&rule->arg.act.p[0]);
Christopher Fauletda2e1172023-01-13 15:33:32 +0100568 if ((rule->action & BWLIM_ACT_LIMIT_EXPR) && rule->arg.act.p[1]) {
Christopher Fauletf0196f42022-06-24 14:52:18 +0200569 release_sample_expr(rule->arg.act.p[1]);
570 rule->arg.act.p[1] = NULL;
571 }
Christopher Fauletda2e1172023-01-13 15:33:32 +0100572 if ((rule->action & BWLIM_ACT_PERIOD_EXPR) && rule->arg.act.p[2]) {
Christopher Fauletf0196f42022-06-24 14:52:18 +0200573 release_sample_expr(rule->arg.act.p[2]);
574 rule->arg.act.p[2] = NULL;
575 }
576 rule->arg.act.p[3] = NULL; /* points on the filter's config */
Christopher Faulet2b677702022-06-22 16:55:04 +0200577}
578
579/* Parse "set-bandwidth-limit" action. The filter name must be specified. For
580 * shared limitations, there is no other supported parameter. For per-stream
581 * limitations, a custom limit and period may be specified. In both case, it
582 * must be an expression. On success:
583 *
584 * arg.act.p[0] will be the filter name (mandatory)
585 * arg.act.p[1] will be an expression for the custom limit (optional, may be NULL)
Ilya Shipitsin3b64a282022-07-29 22:26:53 +0500586 * arg.act.p[2] will be an expression for the custom period (optional, may be NULL)
Christopher Faulet2b677702022-06-22 16:55:04 +0200587 *
588 * It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on error.
589 */
590static enum act_parse_ret parse_bandwidth_limit(const char **args, int *orig_arg, struct proxy *px,
591 struct act_rule *rule, char **err)
592{
593 struct sample_expr *expr;
594 int cur_arg;
595
596 cur_arg = *orig_arg;
597
598 if (!*args[cur_arg]) {
599 memprintf(err, "missing bwlim filter name");
600 return ACT_RET_PRS_ERR;
601 }
602
603 rule->arg.act.p[0] = strdup(args[cur_arg]);
604 if (!rule->arg.act.p[0]) {
605 memprintf(err, "out of memory");
606 return ACT_RET_PRS_ERR;
607 }
608 cur_arg++;
609
610 while (1) {
611 if (strcmp(args[cur_arg], "limit") == 0) {
Christopher Fauletda2e1172023-01-13 15:33:32 +0100612 const char *res;
613 unsigned int limit;
614
Christopher Faulet2b677702022-06-22 16:55:04 +0200615 cur_arg++;
616 if (!args[cur_arg]) {
Christopher Fauletda2e1172023-01-13 15:33:32 +0100617 memprintf(err, "missing limit value or expression");
Christopher Faulet2b677702022-06-22 16:55:04 +0200618 goto error;
619 }
620
Christopher Fauletda2e1172023-01-13 15:33:32 +0100621 res = parse_size_err(args[cur_arg], &limit);
622 if (!res) {
623 rule->action |= BWLIM_ACT_LIMIT_CONST;
624 rule->arg.act.p[1] = (void *)(uintptr_t)limit;
625 cur_arg++;
626 continue;
627 }
628
629 expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line, NULL, &px->conf.args, NULL);
630 if (!expr) {
631 memprintf(err, "'%s': invalid size value or unknown fetch method '%s'", args[cur_arg-1], args[cur_arg]);
Christopher Faulet2b677702022-06-22 16:55:04 +0200632 goto error;
Christopher Fauletda2e1172023-01-13 15:33:32 +0100633 }
634 rule->action |= BWLIM_ACT_LIMIT_EXPR;
Christopher Faulet2b677702022-06-22 16:55:04 +0200635 rule->arg.act.p[1] = expr;
636 }
637 else if (strcmp(args[cur_arg], "period") == 0) {
Christopher Fauletda2e1172023-01-13 15:33:32 +0100638 const char *res;
639 unsigned int period;
640
Christopher Faulet2b677702022-06-22 16:55:04 +0200641 cur_arg++;
642 if (!args[cur_arg]) {
Christopher Fauletda2e1172023-01-13 15:33:32 +0100643 memprintf(err, "missing period value or expression");
Christopher Faulet2b677702022-06-22 16:55:04 +0200644 goto error;
645 }
646
Christopher Fauletda2e1172023-01-13 15:33:32 +0100647 res = parse_time_err(args[cur_arg], &period, TIME_UNIT_MS);
648 if (!res) {
649 rule->action |= BWLIM_ACT_PERIOD_CONST;
650 rule->arg.act.p[2] = (void *)(uintptr_t)period;
651 cur_arg++;
652 continue;
653 }
654
655 expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line, NULL, &px->conf.args, NULL);
656 if (!expr) {
657 memprintf(err, "'%s': invalid time value or unknown fetch method '%s'", args[cur_arg-1], args[cur_arg]);
Christopher Faulet2b677702022-06-22 16:55:04 +0200658 goto error;
Christopher Fauletda2e1172023-01-13 15:33:32 +0100659 }
660 rule->action |= BWLIM_ACT_PERIOD_EXPR;
Christopher Faulet2b677702022-06-22 16:55:04 +0200661 rule->arg.act.p[2] = expr;
662 }
663 else
664 break;
665 }
666
667 rule->action_ptr = bwlim_set_limit;
668 rule->check_ptr = check_bwlim_action;
669 rule->release_ptr = release_bwlim_action;
670
671 *orig_arg = cur_arg;
672 return ACT_RET_PRS_OK;
673
674error:
675 release_bwlim_action(rule);
676 return ACT_RET_PRS_ERR;
677}
678
679
680static struct action_kw_list tcp_req_cont_actions = {
681 .kw = {
682 { "set-bandwidth-limit", parse_bandwidth_limit, 0 },
683 { NULL, NULL }
684 }
685};
686
687static struct action_kw_list tcp_res_cont_actions = {
688 .kw = {
689 { "set-bandwidth-limit", parse_bandwidth_limit, 0 },
690 { NULL, NULL }
691 }
692};
693
694static struct action_kw_list http_req_actions = {
695 .kw = {
696 { "set-bandwidth-limit", parse_bandwidth_limit, 0 },
697 { NULL, NULL }
698 }
699};
700
701static struct action_kw_list http_res_actions = {
702 .kw = {
703 { "set-bandwidth-limit", parse_bandwidth_limit, 0 },
704 { NULL, NULL }
705 }
706};
707
708INITCALL1(STG_REGISTER, tcp_req_cont_keywords_register, &tcp_req_cont_actions);
709INITCALL1(STG_REGISTER, tcp_res_cont_keywords_register, &tcp_res_cont_actions);
710INITCALL1(STG_REGISTER, http_req_keywords_register, &http_req_actions);
711INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_actions);
712
713
714/* Generic function to parse bandwidth limitation filter configurartion. It
715 * Returns -1 on error and 0 on success. It handles configuration for per-stream
716 * and shared limitations.
717 */
718static int parse_bwlim_flt(char **args, int *cur_arg, struct proxy *px, struct flt_conf *fconf,
719 char **err, void *private)
720{
721 struct flt_conf *fc;
722 struct bwlim_config *conf;
723 int shared, per_stream;
724 int pos = *cur_arg + 1;
725
726 conf = calloc(1, sizeof(*conf));
727 if (!conf) {
728 memprintf(err, "%s: out of memory", args[*cur_arg]);
729 return -1;
730 }
731 conf->proxy = px;
732
733 if (!*args[pos]) {
734 memprintf(err, "'%s' : a name is expected as first argument ", args[*cur_arg]);
735 goto error;
736 }
737 conf->flags = BWLIM_FL_NONE;
738 conf->name = strdup(args[pos]);
739 if (!conf->name) {
740 memprintf(err, "%s: out of memory", args[*cur_arg]);
741 goto error;
742 }
743
744 list_for_each_entry(fc, &px->filter_configs, list) {
745 if (fc->id == bwlim_flt_id) {
746 struct bwlim_config *c = fc->conf;
747
Tim Duesterhusa6fc6162022-10-08 12:33:19 +0200748 if (strcmp(conf->name, c->name) == 0) {
Christopher Faulet2b677702022-06-22 16:55:04 +0200749 memprintf(err, "bwlim filter '%s' already declared for proxy '%s'\n",
750 conf->name, px->id);
751 goto error;
752 }
753 }
754 }
755 shared = per_stream = 0;
756 pos++;
757 while (*args[pos]) {
758 if (strcmp(args[pos], "key") == 0) {
759 if (per_stream) {
760 memprintf(err, "'%s' : cannot mix per-stream and shared parameter",
761 args[*cur_arg]);
762 goto error;
763 }
764 if (!*args[pos + 1]) {
765 memprintf(err, "'%s' : the sample expression is missing for '%s' option",
766 args[*cur_arg], args[pos]);
767 goto error;
768 }
769 shared = 1;
770 pos++;
771 conf->expr = sample_parse_expr((char **)args, &pos, px->conf.args.file, px->conf.args.line,
772 err, &px->conf.args, NULL);
773 if (!conf->expr)
774 goto error;
775 }
776 else if (strcmp(args[pos], "table") == 0) {
777 if (per_stream) {
778 memprintf(err, "'%s' : cannot mix per-stream and shared parameter",
779 args[*cur_arg]);
780 goto error;
781 }
782 if (!*args[pos + 1]) {
783 memprintf(err, "'%s' : the table name is missing for '%s' option",
784 args[*cur_arg], args[pos]);
785 goto error;
786 }
787 shared = 1;
788 conf->table.n = strdup(args[pos + 1]);
789 if (!conf->table.n) {
790 memprintf(err, "%s: out of memory", args[*cur_arg]);
791 goto error;
792 }
793 pos += 2;
794 }
795 else if (strcmp(args[pos], "default-period") == 0) {
796 const char *res;
797
798 if (shared) {
799 memprintf(err, "'%s' : cannot mix per-stream and shared parameter",
800 args[*cur_arg]);
801 goto error;
802 }
803 if (!*args[pos + 1]) {
804 memprintf(err, "'%s' : the value is missing for '%s' option",
805 args[*cur_arg], args[pos]);
806 goto error;
807 }
808 per_stream = 1;
809 res = parse_time_err(args[pos + 1], &conf->period, TIME_UNIT_MS);
810 if (res) {
811 memprintf(err, "'%s' : invalid value for option '%s' (unexpected character '%c')",
812 args[*cur_arg], args[pos], *res);
813 goto error;
814 }
815 pos += 2;
816 }
817 else if (strcmp(args[pos], "limit") == 0) {
818 const char *res;
819
820 if (per_stream) {
821 memprintf(err, "'%s' : cannot mix per-stream and shared parameter",
822 args[*cur_arg]);
823 goto error;
824 }
825 if (!*args[pos + 1]) {
826 memprintf(err, "'%s' : the value is missing for '%s' option",
827 args[*cur_arg], args[pos]);
828 goto error;
829 }
830 shared = 1;
831 res = parse_size_err(args[pos + 1], &conf->limit);
832 if (res) {
833 memprintf(err, "'%s' : invalid value for option '%s' (unexpected character '%c')",
834 args[*cur_arg], args[pos], *res);
835 goto error;
836 }
837 pos += 2;
838 }
839 else if (strcmp(args[pos], "default-limit") == 0) {
840 const char *res;
841
842 if (shared) {
843 memprintf(err, "'%s' : cannot mix per-stream and shared parameter",
844 args[*cur_arg]);
845 goto error;
846 }
847 if (!*args[pos + 1]) {
848 memprintf(err, "'%s' : the value is missing for '%s' option",
849 args[*cur_arg], args[pos]);
850 goto error;
851 }
852 per_stream = 1;
853 res = parse_size_err(args[pos + 1], &conf->limit);
854 if (res) {
855 memprintf(err, "'%s' : invalid value for option '%s' (unexpected character '%c')",
856 args[*cur_arg], args[pos], *res);
857 goto error;
858 }
859 pos += 2;
860 }
861 else if (strcmp(args[pos], "min-size") == 0) {
862 const char *res;
863
864 if (!*args[pos + 1]) {
865 memprintf(err, "'%s' : the value is missing for '%s' option",
866 args[*cur_arg], args[pos]);
867 goto error;
868 }
869 res = parse_size_err(args[pos + 1], &conf->min_size);
870 if (res) {
871 memprintf(err, "'%s' : invalid value for option '%s' (unexpected character '%c')",
872 args[*cur_arg], args[pos], *res);
873 goto error;
874 }
875 pos += 2;
876 }
877 else
878 break;
879 }
880
881 if (shared) {
882 conf->flags |= BWLIM_FL_SHARED;
883 if (!conf->expr) {
884 memprintf(err, "'%s' : <key> option is missing", args[*cur_arg]);
885 goto error;
886 }
887 if (!conf->limit) {
888 memprintf(err, "'%s' : <limit> option is missing", args[*cur_arg]);
889 goto error;
890 }
891 }
892 else {
893 /* Per-stream: limit downloads only for now */
894 conf->flags |= BWLIM_FL_OUT;
895 if (!conf->period) {
896 memprintf(err, "'%s' : <default-period> option is missing", args[*cur_arg]);
897 goto error;
898 }
899 if (!conf->limit) {
900 memprintf(err, "'%s' : <default-limit> option is missing", args[*cur_arg]);
901 goto error;
902 }
903 }
904
905 *cur_arg = pos;
906 fconf->id = bwlim_flt_id;
907 fconf->ops = &bwlim_ops;
908 fconf->conf = conf;
909 return 0;
910
911 error:
912 if (conf->name)
Christopher Fauletf0196f42022-06-24 14:52:18 +0200913 ha_free(&conf->name);
914 if (conf->expr) {
Christopher Faulet2b677702022-06-22 16:55:04 +0200915 release_sample_expr(conf->expr);
Christopher Fauletf0196f42022-06-24 14:52:18 +0200916 conf->expr = NULL;
917 }
Christopher Faulet2b677702022-06-22 16:55:04 +0200918 if (conf->table.n)
Christopher Fauletf0196f42022-06-24 14:52:18 +0200919 ha_free(&conf->table.n);
Christopher Faulet2b677702022-06-22 16:55:04 +0200920 free(conf);
921 return -1;
922}
923
924
925static int parse_bwlim_in_flt(char **args, int *cur_arg, struct proxy *px, struct flt_conf *fconf,
926 char **err, void *private)
927{
928 int ret;
929
930 ret = parse_bwlim_flt(args, cur_arg, px, fconf, err, private);
931 if (!ret) {
932 struct bwlim_config *conf = fconf->conf;
933
934 conf->flags |= BWLIM_FL_IN;
935 }
936
937 return ret;
938}
939
940static int parse_bwlim_out_flt(char **args, int *cur_arg, struct proxy *px, struct flt_conf *fconf,
941 char **err, void *private)
942{
943 int ret;
944
945 ret = parse_bwlim_flt(args, cur_arg, px, fconf, err, private);
946 if (!ret) {
947 struct bwlim_config *conf = fconf->conf;
948
949 conf->flags |= BWLIM_FL_OUT;
950 }
951 return ret;
952}
953
954/* Declare the filter parser for "trace" keyword */
955static struct flt_kw_list flt_kws = { "BWLIM", { }, {
956 { "bwlim-in", parse_bwlim_in_flt, NULL },
957 { "bwlim-out", parse_bwlim_out_flt, NULL },
958 { NULL, NULL, NULL },
959 }
960};
961
962INITCALL1(STG_REGISTER, flt_register_keywords, &flt_kws);