MINOR: sample: add a reverse_dom converter

In domain-based routing and policy rules, suffix matching on hostnames is
often easier to express as a prefix match on reversed labels. A dedicated
converter makes this convenient with existing fetches and matchers.

This also has a performance benefit for large maps. Prefix string matches use
the prefix-tree index (PAT_MATCH_BEG with pat_idx_tree_pfx), while end matches
use the string-list index (PAT_MATCH_END with pat_idx_list_str), so
reversed-label lookups can avoid linear suffix scans.

This patch adds "reverse_dom", a string converter that reverses domain labels,
ignores one optional trailing dot on input, and rejects empty labels. It
intentionally leaves trailing-dot handling to the caller so configurations can
choose between exact matches, subdomain-only matches, or an explicit dotted
form built with "concat(.)" for prefix lookups.

Examples:
  example.com      -> com.example
  mail.example.com -> com.example.mail

The documentation is updated and a reg-test covers the converter itself, the
explicit dotted form for "map_beg()", and the subdomain-only "-m beg" case.
This commit is contained in:
Manu Nicolas 2026-04-27 14:59:01 +00:00 committed by Willy Tarreau
parent f3fc68e3a2
commit f4edcdf4de
4 changed files with 191 additions and 3 deletions

View File

@ -21146,6 +21146,7 @@ port_only string integer
protobuf(field_number[,field_type]) binary binary
regsub(regex,subst[,flags]) string string
reverse string string
reverse_dom string string
rfc7239_field(field) string string
rfc7239_is_valid string boolean
rfc7239_n2nn string address / str
@ -22663,6 +22664,38 @@ reverse
# Pick a backend based on the domain suffix of the Host header:
use_backend %[req.hdr(host),lower,reverse,map_beg(/etc/haproxy/hosts.map,default)]
reverse_dom
Converts a string containing an FQDN-like hostname into its reversed-label
form. A single trailing dot on the input is ignored. Empty labels cause the
converter to fail.
This converter does not lowercase its input and does not strip any port.
It is meant to be combined with existing converters such as "lower" or
"host_only" when needed.
The trailing-dot policy is intentionally left to the caller. This allows
callers to decide whether they want to match the apex too or only
subdomains.
The reversed-label form is useful for large domain maps because it turns
domain suffix lookups into prefix lookups, allowing the use of indexed prefix
matchers such as "map_beg".
Examples:
"example.com" -> "com.example"
"mail.example.com" -> "com.example.mail"
"example.com." -> "com.example"
# match only subdomains of example.net, not the apex
acl example_net_sub req.hdr(Host),host_only,reverse_dom -m beg net.example.
# match only the apex
acl example_net_apex req.hdr(Host),host_only,reverse_dom -i net.example
# exact-or-subdomain prefix lookup using an explicit dotted form
http-request set-var(txn.rev_host) req.hdr(Host),host_only,reverse_dom,concat(.)
use_backend %[var(txn.rev_host),map_beg(/etc/haproxy/domains.map)]
rfc7239_field(<field>)
Extracts a single field/parameter from RFC 7239 compliant header value input.

View File

@ -0,0 +1,2 @@
com.example. example
com.example.mail. mail

View File

@ -0,0 +1,94 @@
varnishtest "reverse_dom converter test"
feature ignore_unknown_macro
server s1 {
rxreq
txresp -hdr "Connection: close"
} -repeat 8 -start
haproxy h1 -conf {
global
.if feature(THREAD)
thread-groups 1
.endif
defaults
mode http
timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
frontend fe
bind "fd@${fe}"
http-request set-var(txn.rev_const) str(MaIl.EXAMPLE.com),reverse_dom
http-request set-var(txn.rev_host) req.hdr(Host),host_only,reverse_dom if { req.hdr(Host) -m found }
http-request set-var(txn.rev_host_dot) var(txn.rev_host),concat(.) if { var(txn.rev_host) -m found }
http-request set-var(txn.route) var(txn.rev_host_dot),map_beg(${testdir}/reverse_dom.map,miss) if { var(txn.rev_host_dot) -m found }
http-request set-var(txn.sub_only) str(no)
http-request set-var(txn.sub_only) str(yes) if { var(txn.rev_host) -m beg com.example. }
http-request return status 200 hdr X-Rev-Const "%[var(txn.rev_const)]" hdr X-Rev-Host "%[var(txn.rev_host)]" hdr X-Route "%[var(txn.route)]" hdr X-Sub-Only "%[var(txn.sub_only)]"
default_backend be
backend be
server s1 ${s1_addr}:${s1_port}
} -start
client c1 -connect ${h1_fe_sock} {
txreq -url "/" -hdr "Host: example.com"
rxresp
expect resp.status == 200
expect resp.http.x-rev-const == "com.EXAMPLE.MaIl"
expect resp.http.x-rev-host == "com.example"
expect resp.http.x-route == "example"
expect resp.http.x-sub-only == "no"
txreq -url "/" -hdr "Host: mail.example.com"
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "com.example.mail"
expect resp.http.x-route == "mail"
expect resp.http.x-sub-only == "yes"
txreq -url "/" -hdr "Host: example.com."
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "com.example"
expect resp.http.x-route == "example"
expect resp.http.x-sub-only == "no"
txreq -url "/" -hdr "Host: localhost"
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "localhost"
expect resp.http.x-route == "miss"
expect resp.http.x-sub-only == "no"
txreq -url "/" -hdr "Host: badexample.com"
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "com.badexample"
expect resp.http.x-route == "miss"
expect resp.http.x-sub-only == "no"
txreq -url "/" -hdr "Host: foo..bar"
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "<undef>"
expect resp.http.x-route == "<undef>"
txreq -url "/" -hdr "Host: .example.com"
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "<undef>"
expect resp.http.x-route == "<undef>"
txreq -url "/" -hdr "Host: ."
rxresp
expect resp.status == 200
expect resp.http.x-rev-host == "<undef>"
expect resp.http.x-route == "<undef>"
} -run

View File

@ -2334,6 +2334,64 @@ static int sample_conv_reverse(const struct arg *arg_p, struct sample *smp, void
return 1;
}
/* Reverses the order of labels in an FQDN-like string. A single trailing dot
* on input is ignored. Empty labels are rejected.
*/
static int sample_conv_reverse_dom(const struct arg *arg_p, struct sample *smp, void *private)
{
const char *input = smp->data.u.str.area;
struct buffer *trash;
int input_len = smp->data.u.str.data;
int out = 0;
int label_end;
int label_start;
int label_len;
if (!input_len)
return 0;
if (input[input_len - 1] == '.') {
input_len--;
if (!input_len)
return 0;
}
if (input[0] == '.')
return 0;
trash = get_trash_chunk_sz(input_len + 1);
if (!trash)
return 0;
label_end = input_len;
while (label_end > 0) {
label_start = label_end - 1;
while (label_start >= 0 && input[label_start] != '.')
label_start--;
label_start++;
if (label_start == label_end)
return 0;
label_len = label_end - label_start;
memcpy(trash->area + out, input + label_start, label_len);
out += label_len;
if (label_start == 0)
break;
trash->area[out++] = '.';
label_end = label_start - 1;
}
trash->area[out] = 0;
trash->data = out;
smp->data.u.str = *trash;
smp->data.type = SMP_T_STR;
smp->flags &= ~SMP_F_CONST;
return 1;
}
/* takes the IPv4 mask in args[0] and an optional IPv6 mask in args[1] */
static int sample_conv_ipmask(const struct arg *args, struct sample *smp, void *private)
{
@ -5797,10 +5855,11 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, {
{ "param", sample_conv_param, ARG2(1,STR,STR), sample_conv_param_check, SMP_T_STR, SMP_T_STR },
{ "regsub", sample_conv_regsub, ARG3(2,REG,STR,STR), sample_conv_regsub_check, SMP_T_STR, SMP_T_STR },
{ "sha1", sample_conv_sha1, 0, NULL, SMP_T_BIN, SMP_T_BIN },
{ "strcmp", sample_conv_strcmp, ARG1(1,STR), smp_check_strcmp, SMP_T_STR, SMP_T_SINT },
{ "host_only", sample_conv_host_only, 0, NULL, SMP_T_STR, SMP_T_STR },
{ "port_only", sample_conv_port_only, 0, NULL, SMP_T_STR, SMP_T_SINT },
{ "strcmp", sample_conv_strcmp, ARG1(1,STR), smp_check_strcmp, SMP_T_STR, SMP_T_SINT },
{ "host_only", sample_conv_host_only, 0, NULL, SMP_T_STR, SMP_T_STR },
{ "port_only", sample_conv_port_only, 0, NULL, SMP_T_STR, SMP_T_SINT },
{ "reverse", sample_conv_reverse, 0, NULL, SMP_T_STR, SMP_T_STR },
{ "reverse_dom", sample_conv_reverse_dom, 0, NULL, SMP_T_STR, SMP_T_STR },
/* gRPC converters. */
{ "ungrpc", sample_conv_ungrpc, ARG2(1,PBUF_FNUM,STR), sample_conv_protobuf_check, SMP_T_BIN, SMP_T_BIN },