MEDIUM: filters: add "filter-sequence" directive

This is another pre-requisite work for upcoming decompression filter.

In this patch we implement the "filter-sequence" directive which can be
used in proxy section (frontend,backend,listen) and takes 2 parameters

The first one is the direction (request or response), the second one
is a comma separated list of filter names previously declared on the
proxy using the "filter" keyword.

The main goal of this directive is to be able to instruct haproxy in which
order the filters should be executed on request and response paths,
especially if the ordering between request and response handling must
differ, and without relying on the filter declaration ordering (within
the proxy) which is used by default by haproxy.

Another benefit of this feature is that it becomes possible to "ignore"
a previously declared filter on the proxy. Indeed, when filter-sequence
is defined for a given direction (request/response), then it will be used
over the implicit filter ordering, but if a filter which was previously
declared is not specified in the related filter-sequence, it will not be
executed on purpose. This can be used as a way to temporarily disable a
filter without completely removing its configuration.

Documentation was updated (check examples for more info)
This commit is contained in:
Aurelien DARRAGON 2026-03-11 15:37:44 +01:00
parent 629a5ae531
commit 8d28c0e37b
5 changed files with 223 additions and 10 deletions

View File

@ -6001,6 +6001,7 @@ external-check path X - X X
force-persist - - X X
force-be-switch - X X -
filter - X X X
filter-sequence - X X X
fullconn X - X X
guid - X X X
hash-balance-factor X - X X
@ -7891,8 +7892,41 @@ filter <name> [param*]
server srv1 192.168.0.1:80
See also : section 9.
See also : section 9., "filter-sequence"
filter-sequence { request | response } <filter_list>
Specifies in which order filters declared on the proxy should be
executed.
May be used in the following contexts: tcp, http
May be used in sections: defaults | frontend | listen | backend
no | yes | yes | yes
Comma-separated list of filter names (<filter_list>) to specify in which
order filters declared on the proxy should be executed, for request
or response path, respectively.
When filter-sequence is not specified for a given path (ie: request vs
response), the order in which filters are declared on the proxy is used.
If filter-sequence omits some filters that were declared on the proxy,
they will not be executed. This is an effective way of temporarily
disabling a filter without removing it from the configuration.
Example:
global
lua-load my-filter.lua # defines custom "lua.my-filter"
frontend myfront
filter comp-req
filter comp-res
filter lua.my-filter
filter-sequence request lua.my-filter,comp-req
filter-sequence response lua.my-filter,comp-res
See also : "filter"
fullconn <conns>
Specify at what backend load the servers will reach their maxconn

View File

@ -215,6 +215,12 @@ struct flt_conf {
unsigned int flags; /* FLT_CFG_FL_* */
};
struct filter_sequence_elt {
char *flt_name; /* filter name (set during parsing) */
struct flt_conf *flt_conf; /* associated filter conf (set after parsing) */
struct list list; /* list element */
};
/*
* Structure reprensenting a filter instance attached to a stream
*

View File

@ -509,6 +509,12 @@ struct proxy {
* name is used
*/
struct list filter_configs; /* list of the filters that are declared on this proxy */
struct { /* sequence in which declared filters on the proxy should be execute
* (list of filter_sequence_elt)
*/
struct list req; /* during request handling */
struct list res; /* during response handling */
} filter_sequence;
struct guid_node guid; /* GUID global tree node */
struct mt_list watcher_list; /* list of elems which currently references this proxy instance (currently only used with backends) */

View File

@ -297,6 +297,136 @@ parse_filter(char **args, int section_type, struct proxy *curpx,
}
/*
* Parses the "filter-sequence" keyword
*/
static int
parse_filter_sequence(char **args, int section_type, struct proxy *curpx,
const struct proxy *defpx, const char *file, int line, char **err)
{
/* filter-sequence cannot be defined on a default proxy */
if (curpx == defpx) {
memprintf(err, "parsing [%s:%d] : %s is not allowed in a 'default' section.",
file, line, args[0]);
return -1;
}
if (strcmp(args[0], "filter-sequence") == 0) {
struct list *list;
char *str;
size_t cur_sep;
if (!*args[1]) {
memprintf(err,
"parsing [%s:%d] : missing argument for '%s' in %s '%s'.",
file, line, args[0], proxy_type_str(curpx), curpx->id);
goto error;
}
if (!strcmp(args[1], "request"))
list = &curpx->filter_sequence.req;
else if (!strcmp(args[1], "response"))
list = &curpx->filter_sequence.res;
else {
memprintf(err,
"parsing [%s:%d] : expected either 'request' or 'response' for '%s' in %s '%s'.",
file, line, args[0], proxy_type_str(curpx), curpx->id);
goto error;
}
if (!*args[2]) {
memprintf(err,
"parsing [%s:%d] : missing filter list for '%s' in %s '%s'.",
file, line, args[0], proxy_type_str(curpx), curpx->id);
goto error;
}
str = args[2];
while (str[0]) {
struct filter_sequence_elt *elt;
elt = calloc(1, sizeof(*elt));
if (!elt) {
memprintf(err, "'%s %s' : out of memory", args[0], args[1]);
goto error;
}
cur_sep = strcspn(str, ",");
elt->flt_name = my_strndup(str, cur_sep);
if (!elt->flt_name) {
ha_free(&elt);
goto error;
}
LIST_APPEND(list, &elt->list);
if (str[cur_sep])
str += cur_sep + 1;
else
str += cur_sep;
}
}
return 0;
error:
return -1;
}
static int compile_filter_sequence_elt(struct proxy *px, struct filter_sequence_elt *elt, char **errmsg)
{
struct flt_conf *fconf;
int ret = ERR_NONE;
list_for_each_entry(fconf, &px->filter_configs, list) {
if (!strcmp(elt->flt_name, fconf->name)) {
elt->flt_conf = fconf;
break;
}
}
if (!elt->flt_conf) {
memprintf(errmsg, "invalid filter name: '%s' is not defined on the proxy", elt->flt_name);
ret = ERR_FATAL;
}
return ret;
}
/* after config is checked, time to resolve filter-sequence (both request and response)
* used on the proxy in order to associate filter names with valid flt_conf entries
* this will help decrease filter lookup time during runtime (filter ids are compared
* using their address, not string content)
*/
static int postcheck_filter_sequence(struct proxy *px)
{
struct filter_sequence_elt *elt;
char *errmsg = NULL;
int ret = ERR_NONE;
list_for_each_entry(elt, &px->filter_sequence.req, list) {
ret = compile_filter_sequence_elt(px, elt, &errmsg);
if (ret & ERR_CODE) {
memprintf(&errmsg, "error while postparsing request filter-sequence '%s' : %s", elt->flt_name, errmsg);
goto error;
}
}
list_for_each_entry(elt, &px->filter_sequence.res, list) {
ret = compile_filter_sequence_elt(px, elt, &errmsg);
if (ret & ERR_CODE) {
memprintf(&errmsg, "error while postparsing response filter-sequence '%s' : %s", elt->flt_name, errmsg);
goto error;
}
}
return ret;
error:
ha_alert("%s: %s\n", px->id, errmsg);
ha_free(&errmsg);
return ret;
}
REGISTER_POST_PROXY_CHECK(postcheck_filter_sequence);
/*
* Calls 'init' callback for all filters attached to a proxy. This happens after
* the configuration parsing. Filters can finish to fill their config. Returns
@ -401,6 +531,7 @@ void
flt_deinit(struct proxy *proxy)
{
struct flt_conf *fconf, *back;
struct filter_sequence_elt *fsequence, *fsequenceb;
list_for_each_entry_safe(fconf, back, &proxy->filter_configs, list) {
if (fconf->ops->deinit)
@ -408,6 +539,16 @@ flt_deinit(struct proxy *proxy)
LIST_DELETE(&fconf->list);
free(fconf);
}
list_for_each_entry_safe(fsequence, fsequenceb, &proxy->filter_sequence.req, list) {
LIST_DEL_INIT(&fsequence->list);
ha_free(&fsequence->flt_name);
ha_free(&fsequence);
}
list_for_each_entry_safe(fsequence, fsequenceb, &proxy->filter_sequence.res, list) {
LIST_DEL_INIT(&fsequence->list);
ha_free(&fsequence->flt_name);
ha_free(&fsequence);
}
}
/*
@ -438,7 +579,7 @@ flt_deinit_all_per_thread()
/* Attaches a filter to a stream. Returns -1 if an error occurs, 0 otherwise. */
static int
flt_stream_add_filter(struct stream *s, struct flt_conf *fconf, unsigned int flags)
flt_stream_add_filter(struct stream *s, struct proxy *px, struct flt_conf *fconf, unsigned int flags)
{
struct filter *f;
@ -461,18 +602,38 @@ flt_stream_add_filter(struct stream *s, struct flt_conf *fconf, unsigned int fla
}
LIST_APPEND(&strm_flt(s)->filters, &f->list);
LIST_INIT(&f->req_list);
LIST_INIT(&f->res_list);
/* for now f->req_list == f->res_list to preserve
* historical behavior, but the ordering will change
* in the future
*/
LIST_APPEND(&s->req.flt.filters, &f->req_list);
LIST_APPEND(&s->res.flt.filters, &f->res_list);
/* use filter config ordering unless filter-sequence says otherwise */
if (LIST_ISEMPTY(&px->filter_sequence.req))
LIST_APPEND(&s->req.flt.filters, &f->req_list);
if (LIST_ISEMPTY(&px->filter_sequence.res))
LIST_APPEND(&s->res.flt.filters, &f->res_list);
strm_flt(s)->flags |= STRM_FLT_FL_HAS_FILTERS;
return 0;
}
static void flt_stream_organize_filters(struct stream *s, struct proxy *px)
{
struct filter_sequence_elt *fsequence;
struct filter *filter;
list_for_each_entry(fsequence, &px->filter_sequence.req, list) {
list_for_each_entry(filter, &strm_flt(s)->filters, list) {
if (filter->config == fsequence->flt_conf && !LIST_INLIST(&filter->req_list))
LIST_APPEND(&s->req.flt.filters, &filter->req_list);
}
}
list_for_each_entry(fsequence, &px->filter_sequence.res, list) {
list_for_each_entry(filter, &strm_flt(s)->filters, list) {
if (filter->config == fsequence->flt_conf && !LIST_INLIST(&filter->res_list))
LIST_APPEND(&s->res.flt.filters, &filter->res_list);
}
}
}
/*
* Called when a stream is created. It attaches all frontend filters to the
* stream. Returns -1 if an error occurs, 0 otherwise.
@ -489,9 +650,10 @@ flt_stream_init(struct stream *s)
memset(&s->res.flt, 0, sizeof(s->res.flt));
LIST_INIT(&s->res.flt.filters);
list_for_each_entry(fconf, &strm_fe(s)->filter_configs, list) {
if (flt_stream_add_filter(s, fconf, 0) < 0)
if (flt_stream_add_filter(s, strm_fe(s), fconf, 0) < 0)
return -1;
}
flt_stream_organize_filters(s, strm_fe(s));
return 0;
}
@ -605,10 +767,12 @@ flt_set_stream_backend(struct stream *s, struct proxy *be)
goto end;
list_for_each_entry(fconf, &be->filter_configs, list) {
if (flt_stream_add_filter(s, fconf, FLT_FL_IS_BACKEND_FILTER) < 0)
if (flt_stream_add_filter(s, be, fconf, FLT_FL_IS_BACKEND_FILTER) < 0)
return -1;
}
flt_stream_organize_filters(s, be);
end:
list_for_each_entry(filter, &strm_flt(s)->filters, list) {
if (FLT_OPS(filter)->stream_set_backend) {
@ -1231,6 +1395,7 @@ handle_analyzer_result(struct stream *s, struct channel *chn,
* not enabled. */
static struct cfg_kw_list cfg_kws = {ILH, {
{ CFG_LISTEN, "filter", parse_filter },
{ CFG_LISTEN, "filter-sequence", parse_filter_sequence },
{ 0, NULL, NULL },
}
};

View File

@ -1578,6 +1578,8 @@ void init_new_proxy(struct proxy *p)
LIST_INIT(&p->conf.lf_checks);
LIST_INIT(&p->filter_configs);
LIST_INIT(&p->tcpcheck.preset_vars);
LIST_INIT(&p->filter_sequence.req);
LIST_INIT(&p->filter_sequence.res);
MT_LIST_INIT(&p->lbprm.lb_free_list);