Download raw body.
relayd 1/3: Add new matching syntax to control the matching rules
relayd 1/3: Add new matching syntax to control the matching rules
On Fri, 09 Oct 2026 14:18:30 +0200,
Rafael Sadowski <rafael@sizeofvoid.org> wrote:
>
> Hi relayd users,
>
> This is one of three diff in a row. The basic idea is described in the
> commit message below. During working on the pattern matching, I noticed
> that the pattern matching depends on the type and is not documented.
>
> The documentation error has been fixed, but I was still annoyed by the
> situation. 1. I want it to be done explicitly rather than implicitly based on
> the type. 2. I want to control what I want to match, and I want
> the entire pattern log
>
> I think this is how one should deal with it: be explicit rather than
> implicit!
>
> Please test it with your setup and test the new keywords.
>
> This manpage changes also include the other two diffs.
>
> Also looking for OKs on the code base!
>
> Rafael
>
> commit f1b77253c65820103f976b4edfd2e6865232c708
> Author: Rafael Sadowski <rafael@sizeofvoid.org>
> Date: Fri Sep 18 01:00:15 2026 +0200
>
> relayd: Add new matching syntax to control the matching rules
>
> Besides "pattern", the cookie, header, path, query and url keys and
> values now accept "glob" and "glob ignorecase". Without a keyword
> the old per-field defaults stay as they were.
>
> Header names are case-insensitive (RFC 9110), so "glob" on a header
> name is treated as "glob ignorecase". "pattern" stays case-sensitive.
>
I though about it and if I understand right how it works I see two behivour
changes:
1. block request method "HEAD" path log
seems that before it mathces every path, and now it should stop
2. block request path file "/etc/blocked-paths"
let assume that file of blocked paths has /private/*;
I think that before it matches /private/a and now it stops
Next that I think may be a bug here. How this should work?
http protocol "header-test" {
pass request
block request quick header pattern "^X" header "Host"
}
relay "header-test" {
listen on 127.0.0.1 port 8080
protocol "header-test"
forward to 127.0.0.1 port 8000
}
If I not mistaken before the patch both request with:
Host: example.com
and
host: example.com
leads to 403, after the patch lowercased is passed.
If I recall right HTTP states that header key is case insensetive.
But, again, I may miss something here.
> diff --git a/parse.y b/parse.y
> index b21ae26..a317f84 100644
> --- a/parse.y
> +++ b/parse.y
> @@ -125,7 +125,8 @@ static enum key_type keytype = KEY_TYPE_NONE;
> static enum direction dir = RELAY_DIR_ANY;
> static char *rulefile = NULL;
> static union hashkey *hashkey = NULL;
> -static int value_pattern = 0;
> +static u_int16_t val_flag = 0;
> +static u_int16_t key_flag = 0;
>
> struct address *host_ip(const char *);
> int host_dns(const char *, struct addresslist *,
> @@ -144,6 +145,7 @@ struct relay *relay_inherit(struct relay *, struct relay *);
> int getservice(char *);
> int is_if_in_group(const char *, const char *);
> static struct keyname *proto_keyname(char *);
> +static void rule_kv_setvalue(struct kv *, const char *, u_int16_t);
>
> typedef struct {
> union {
> @@ -180,8 +182,9 @@ typedef struct {
> %token DEMOTE DESTINATION DIGEST DISABLE
> %token ECDHE EDH ERROR ERRORS EXPECT EXTERNAL
> %token FILENAME FORWARD FROM
> +%token GLOB
> %token HASH HEADER HEADERLEN HOST HTTP
> -%token ICMP INCLUDE INET INET6 INTERFACE INTERVAL IP
> +%token ICMP IGNORECASE INCLUDE INET INET6 INTERFACE INTERVAL IP
> %token KEY KEYPAIR
> %token LABEL LEASTSTATES LISTEN LOADBALANCE LOG LOOKUP
> %token MATCH METHOD MODE NAT NO NODELAY NOTHING
> @@ -1603,14 +1606,13 @@ ruleopts : METHOD STRING {
> keytype = KEY_TYPE_COOKIE;
> rule->rule_kv[keytype].kv_key = strdup($4);
> rule->rule_kv[keytype].kv_option = $2;
> - if ($3)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_KEY_PATTERN;
> - rule->rule_kv[keytype].kv_value = (($5 != NULL) ?
> - strdup($5) : strdup("*"));
> - if (value_pattern)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_VAL_PATTERN;
> +
> + /* default (no keyword): literal, case-insensitive. */
> + rule->rule_kv[keytype].kv_flags |= key_flag;
> +
> + rule_kv_setvalue(&rule->rule_kv[keytype], $5,
> + KV_FLAG_VAL_GLOBBING_ICASE);
> +
> if (rule->rule_kv[keytype].kv_key == NULL ||
> rule->rule_kv[keytype].kv_value == NULL)
> fatal("out of memory");
> @@ -1626,21 +1628,26 @@ ruleopts : METHOD STRING {
> }
> | HEADER key_option optpattern STRING value {
> keytype = KEY_TYPE_HEADER;
> - memset(&rule->rule_kv[keytype], 0,
> - sizeof(rule->rule_kv[keytype]));
> rule->rule_kv[keytype].kv_option = $2;
> - if ($3)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_KEY_PATTERN;
> rule->rule_kv[keytype].kv_key = strdup($4);
> - rule->rule_kv[keytype].kv_value = (($5 != NULL) ?
> - strdup($5) : strdup("*"));
> +
> + if (key_flag == KV_FLAG_KEY_GLOBBING)
> + log_warnx("header \"%s\": names are "
> + "case-insensitive (RFC 9110), using glob "
> + "ignorecase", rule->rule_kv[keytype].kv_key);
> +
> + if (key_flag == 0 || key_flag == KV_FLAG_KEY_GLOBBING)
> + key_flag = KV_FLAG_KEY_GLOBBING_ICASE;
> +
> + rule->rule_kv[keytype].kv_flags |= key_flag;
> +
> + rule_kv_setvalue(&rule->rule_kv[keytype], $5,
> + KV_FLAG_VAL_GLOBBING);
> +
> if (rule->rule_kv[keytype].kv_key == NULL ||
> rule->rule_kv[keytype].kv_value == NULL)
> fatal("out of memory");
> - if (value_pattern)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_VAL_PATTERN;
> +
> free($4);
> if ($5)
> free($5);
> @@ -1654,15 +1661,15 @@ ruleopts : METHOD STRING {
> | PATH key_option optpattern STRING value {
> keytype = KEY_TYPE_PATH;
> rule->rule_kv[keytype].kv_option = $2;
> - if ($3)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_KEY_PATTERN;
> + /* default: path matching is case-sensitive. */
> + if (key_flag == 0)
> + key_flag = KV_FLAG_KEY_GLOBBING;
> + rule->rule_kv[keytype].kv_flags |= key_flag;
> rule->rule_kv[keytype].kv_key = strdup($4);
> - rule->rule_kv[keytype].kv_value = (($5 != NULL) ?
> - strdup($5) : strdup("*"));
> - if (value_pattern)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_VAL_PATTERN;
> +
> + rule_kv_setvalue(&rule->rule_kv[keytype], $5,
> + KV_FLAG_VAL_GLOBBING_ICASE);
> +
> if (rule->rule_kv[keytype].kv_key == NULL ||
> rule->rule_kv[keytype].kv_value == NULL)
> fatal("out of memory");
> @@ -1705,15 +1712,15 @@ ruleopts : METHOD STRING {
> }
> keytype = KEY_TYPE_QUERY;
> rule->rule_kv[keytype].kv_option = $2;
> - if ($3)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_KEY_PATTERN;
> + /* default: query name matching is case-sensitive. */
> + if (key_flag == 0)
> + key_flag = KV_FLAG_KEY_GLOBBING;
> + rule->rule_kv[keytype].kv_flags |= key_flag;
> rule->rule_kv[keytype].kv_key = strdup($4);
> - rule->rule_kv[keytype].kv_value = (($5 != NULL) ?
> - strdup($5) : strdup("*"));
> - if (value_pattern)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_VAL_PATTERN;
> +
> + rule_kv_setvalue(&rule->rule_kv[keytype], $5,
> + KV_FLAG_VAL_GLOBBING);
> +
> if (rule->rule_kv[keytype].kv_key == NULL ||
> rule->rule_kv[keytype].kv_value == NULL)
> fatal("out of memory");
> @@ -1750,16 +1757,14 @@ ruleopts : METHOD STRING {
> }
> keytype = KEY_TYPE_URL;
> rule->rule_kv[keytype].kv_option = $2;
> - if ($3)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_KEY_PATTERN;
> + /* default (no keyword): literal, case-insensitive. */
> + rule->rule_kv[keytype].kv_flags |= key_flag;
> rule->rule_kv[keytype].kv_key = strdup($4.digest);
> rule->rule_kv[keytype].kv_digest = $4.type;
> - rule->rule_kv[keytype].kv_value = (($5 != NULL) ?
> - strdup($5) : strdup("*"));
> - if (value_pattern)
> - rule->rule_kv[keytype].kv_flags |=
> - KV_FLAG_VAL_PATTERN;
> +
> + rule_kv_setvalue(&rule->rule_kv[keytype], $5,
> + KV_FLAG_VAL_GLOBBING_ICASE);
> +
> if (rule->rule_kv[keytype].kv_key == NULL ||
> rule->rule_kv[keytype].kv_value == NULL)
> fatal("out of memory");
> @@ -1919,21 +1924,37 @@ ruleopts : METHOD STRING {
> }
> ;
>
> -optpattern : /* empty */ { $$ = 0; }
> - | PATTERN { $$ = 1; }
> - ;
> -
> -value : /* empty */ { $$ = NULL;
> - value_pattern = 0;
> +optpattern : /* empty */ {
> + key_flag = 0;
> + }
> + | PATTERN {
> + key_flag = KV_FLAG_KEY_PATTERN;
> }
> - | VALUE STRING { $$ = $2;
> - value_pattern = 0;
> + | GLOB {
> + key_flag = KV_FLAG_KEY_GLOBBING;
> }
> - | VALUE PATTERN STRING { $$ = $3;
> - value_pattern = 1;
> + | GLOB IGNORECASE {
> + key_flag = KV_FLAG_KEY_GLOBBING_ICASE;
> }
> ;
>
> +value : /* empty */ { $$ = NULL;
> + val_flag = 0;
> + }
> + | VALUE STRING { $$ = $2;
> + val_flag = 0;
> + }
> + | VALUE PATTERN STRING { $$ = $3;
> + val_flag = KV_FLAG_VAL_PATTERN;
> + }
> + | VALUE GLOB STRING { $$ = $3;
> + val_flag = KV_FLAG_VAL_GLOBBING;
> + }
> + | VALUE GLOB IGNORECASE STRING { $$ = $4;
> + val_flag = KV_FLAG_VAL_GLOBBING_ICASE;
> + }
> + ;
> +
> key_option : /* empty */ { $$ = KEY_OPTION_NONE; }
> | APPEND { $$ = KEY_OPTION_APPEND; }
> | SET { $$ = KEY_OPTION_SET; }
> @@ -2607,12 +2628,14 @@ lookup(char *s)
> { "file", FILENAME },
> { "forward", FORWARD },
> { "from", FROM },
> + { "glob", GLOB },
> { "hash", HASH },
> { "header", HEADER },
> { "headerlen", HEADERLEN },
> { "host", HOST },
> { "http", HTTP },
> { "icmp", ICMP },
> + { "ignorecase", IGNORECASE },
> { "include", INCLUDE },
> { "inet", INET },
> { "inet6", INET6 },
> @@ -3798,3 +3821,16 @@ proto_keyname(char *name)
> TAILQ_INSERT_TAIL(&proto->tlscerts, kname, entry);
> return kname;
> }
> +
> +/*
> + * Set the value of a rule kv. An omitted value matches anything,
> + * otherwise use the matching keyword or the per-type default.
> + */
> +static void
> +rule_kv_setvalue(struct kv *kv, const char *val, u_int16_t dflag)
> +{
> + kv->kv_value = strdup(val != NULL ? val : "*");
> + if (kv->kv_value == NULL)
> + fatal("out of memory");
> + kv->kv_flags |= (val_flag != 0) ? val_flag : dflag;
> +}
> diff --git a/relay_http.c b/relay_http.c
> index 043d78e..a802ebe 100644
> --- a/relay_http.c
> +++ b/relay_http.c
> @@ -82,6 +82,7 @@ void relay_httpdesc_free(struct http_descriptor *);
> char *server_root_strip(char *, int);
> int relay_http_parse_startline(struct ctl_relay_event *, char *,
> enum httpmethod *);
> +static int relay_url_log_match(const struct kv *, const char *);
>
> static struct relayd *env = NULL;
>
> @@ -910,15 +911,10 @@ _relay_lookup_url(struct ctl_relay_event *cre, char *host, char *path,
> log_debug("%s: session %d: %s, %s", __func__, con->se_id,
> str, kv->kv_key);
>
> - if (kv->kv_flags & KV_FLAG_KEY_PATTERN) {
> - if (kv_match_key(kv, str, 0)) {
> - log_debug("%s: session %d: pattern \"%s\" matched "
> - "url \"%s\"",
> - __func__, con->se_id, kv->kv_key, str);
> - ret = RES_DROP;
> - goto done;
> - }
> - } else if (strcasecmp(kv->kv_key, str) == 0) {
> + if (kv_match_key(kv, str)) {
> + log_debug("%s: session %d: %s key \"%s\" matched url \"%s\"",
> + __func__, con->se_id, kv_key_flag_str(kv->kv_flags),
> + kv->kv_key, str);
> ret = RES_DROP;
> goto done;
> }
> @@ -1046,11 +1042,8 @@ relay_lookup_cookie(struct ctl_relay_event *cre, const char *str,
> if (value[strlen(value) - 1] == '"')
> value[strlen(value) - 1] = '\0';
>
> - if (((kv->kv_flags & KV_FLAG_KEY_PATTERN) ?
> - kv_match_key(kv, key, 0) :
> - strcasecmp(kv->kv_key, key) == 0) &&
> - (kv->kv_value == NULL ||
> - kv_match_val(kv, value, FNM_CASEFOLD))) {
> + if ((kv_match_key(kv, key)) &&
> + (kv->kv_value == NULL || kv_match_val(kv, value))) {
> log_debug("%s: matched cookie \"%s\" value \"%s\" "
> "(rule value \"%s\")",
> __func__, key, value,
> @@ -1095,8 +1088,8 @@ relay_lookup_query(struct ctl_relay_event *cre, struct kv *kv)
> continue;
> *tmpval++ = '\0';
>
> - if (kv_match_key(kv, tmpkey, 0) &&
> - (kv->kv_value == NULL || kv_match_val(kv, tmpval, 0))) {
> + if (kv_match_key(kv, tmpkey) &&
> + (kv->kv_value == NULL || kv_match_val(kv, tmpval))) {
> log_debug("%s: matched query key \"%s\" value \"%s\" "
> "(rule key \"%s\" value \"%s\")",
> __func__, tmpkey, tmpval, kv->kv_key,
> @@ -1537,7 +1530,7 @@ relay_httpheader_test(struct ctl_relay_event *cre, struct relay_rule *rule,
> return (-1);
> } else {
> if (kv->kv_value != NULL && match->kv_value != NULL &&
> - !kv_match_val(kv, match->kv_value, 0)) {
> + !kv_match_val(kv, match->kv_value)) {
> log_debug("%s: rule %d: header \"%s\" value mismatch: "
> "rule \"%s\" vs. actual \"%s\"",
> __func__, rule->rule_id, kv->kv_key,
> @@ -1571,7 +1564,7 @@ relay_httppath_test(struct ctl_relay_event *cre, struct relay_rule *rule,
> else if (kv->kv_option != KEY_OPTION_STRIP) {
> if (kv->kv_key == NULL)
> return (0);
> - else if (!kv_match_key(kv, desc->http_path, 0)) {
> + else if (!kv_match_key(kv, desc->http_path)) {
> log_debug("%s: rule %d: path \"%s\" does not match "
> "rule key \"%s\"",
> __func__, rule->rule_id, desc->http_path,
> @@ -1581,7 +1574,7 @@ relay_httppath_test(struct ctl_relay_event *cre, struct relay_rule *rule,
> kv->kv_option == KEY_OPTION_NONE) {
> query = desc->http_query == NULL ? "" :
> desc->http_query;
> - if (!kv_match_val(kv, query, FNM_CASEFOLD)) {
> + if (!kv_match_val(kv, query)) {
> log_debug("%s: rule %d: query \"%s\" does not "
> "match rule value \"%s\"",
> __func__, rule->rule_id, query,
> @@ -1621,9 +1614,9 @@ relay_httpurl_test(struct ctl_relay_event *cre, struct relay_rule *rule,
> return (RES_BAD);
> else if (rule->rule_action != RULE_ACTION_BLOCK &&
> kv->kv_option == KEY_OPTION_LOG &&
> - kv_match_key(kv, match->kv_key, FNM_CASEFOLD)) {
> - log_info("%s: rule %d: url \"%s\" matched",
> - __func__, rule->rule_id,
> + relay_url_log_match(kv, match->kv_key)) {
> + log_info("%s: rule %d: url \"%s\" matched", __func__,
> + rule->rule_id,
> match->kv_key ? match->kv_key : "");
> /* match url only for logging */
> } else if ((res = relay_lookup_url(cre, host->kv_value, kv)) != 0)
> @@ -2242,3 +2235,13 @@ fail:
> relay_abort_http(con, 500, strerror(errno), 0);
> return (-1);
> }
> +
> +static int
> +relay_url_log_match(const struct kv *kv, const char *str)
> +{
> + if (kv->kv_flags & (KV_FLAG_KEY_PATTERN | KV_FLAG_KEY_GLOBBING |
> + KV_FLAG_KEY_GLOBBING_ICASE))
> + return (kv_match_key(kv, str));
> + /* url log keys have always been case-insensitive glob(7) patterns */
> + return (fnmatch(kv->kv_key, str, FNM_CASEFOLD) != FNM_NOMATCH);
> +}
> diff --git a/relayd.c b/relayd.c
> index 1c3f682..a906d13 100644
> --- a/relayd.c
> +++ b/relayd.c
> @@ -850,35 +850,63 @@ kv_log(struct rsession *con, struct kv *kv, u_int16_t labelid,
> * Returns 1 on match, 0 on no match or error.
> */
> static int
> -_kv_match(const char *pattern, const char *str, int is_pattern, int fnflags)
> +_kv_match(const char *pattern, const char *str, u_int16_t matching)
> {
> struct str_find sm;
> const char *errstr = NULL;
>
> if (pattern == NULL || str == NULL)
> return (0);
> - if (!is_pattern)
> - return (fnmatch(pattern, str, fnflags) != FNM_NOMATCH);
>
> - if (str_find(str, pattern, &sm, 1, &errstr) > 0 && errstr == NULL)
> - return (1);
> - if (errstr != NULL)
> - log_warnx("%s: pattern \"%s\": %s", __func__, pattern, errstr);
> - return (0);
> + if (matching & (KV_FLAG_VAL_PATTERN | KV_FLAG_KEY_PATTERN)) {
> + if (str_find(str, pattern, &sm, 1, &errstr) > 0 &&
> + errstr == NULL)
> + return (1);
> + if (errstr != NULL)
> + log_warnx("%s: pattern \"%s\": %s", __func__, pattern,
> + errstr);
> + return (0);
> + }
> + if (matching &
> + (KV_FLAG_VAL_GLOBBING_ICASE | KV_FLAG_KEY_GLOBBING_ICASE))
> + return (fnmatch(pattern, str, FNM_CASEFOLD) != FNM_NOMATCH);
> +
> + if (matching &
> + (KV_FLAG_VAL_GLOBBING | KV_FLAG_KEY_GLOBBING))
> + return (fnmatch(pattern, str, 0) != FNM_NOMATCH);
> +
> + /* no matching keyword: literal, case-insensitive (historic behavior) */
> + return (strcasecmp(pattern, str) == 0);
> }
>
> int
> -kv_match_key(const struct kv *kv, const char *str, int fnflags)
> +kv_match_key(const struct kv *kv, const char *str)
> {
> - return (_kv_match(kv->kv_key, str, kv->kv_flags & KV_FLAG_KEY_PATTERN,
> - fnflags));
> + u_int16_t matching = kv->kv_flags & (KV_FLAG_KEY_PATTERN |
> + KV_FLAG_KEY_GLOBBING | KV_FLAG_KEY_GLOBBING_ICASE);
> +
> + return (_kv_match(kv->kv_key, str, matching));
> }
>
> int
> -kv_match_val(const struct kv *kv, const char *str, int fnflags)
> +kv_match_val(const struct kv *kv, const char *str)
> +{
> + u_int16_t matching = kv->kv_flags & (KV_FLAG_VAL_PATTERN |
> + KV_FLAG_VAL_GLOBBING | KV_FLAG_VAL_GLOBBING_ICASE);
> +
> + return (_kv_match(kv->kv_value, str, matching));
> +}
> +
> +const char *
> +kv_key_flag_str(u_int16_t flags)
> {
> - return (_kv_match(kv->kv_value, str, kv->kv_flags & KV_FLAG_VAL_PATTERN,
> - fnflags));
> + if (flags & KV_FLAG_KEY_PATTERN)
> + return ("pattern");
> + if (flags & KV_FLAG_KEY_GLOBBING_ICASE)
> + return ("glob ignorecase");
> + if (flags & KV_FLAG_KEY_GLOBBING)
> + return ("glob");
> + return ("literal");
> }
>
> /*
> @@ -894,13 +922,14 @@ kv_find(struct kvtree *keys, struct kv *kv)
> * If the key uses glob(7) or a patterns(7) expression, fall back
> * to a linear scan and match each entry.
> */
> - if (kv->kv_flags & (KV_FLAG_GLOBBING | KV_FLAG_KEY_PATTERN)) {
> + if (kv->kv_flags &
> + (KV_FLAG_KEY_GLOBBING | KV_FLAG_KEY_GLOBBING_ICASE |
> + KV_FLAG_KEY_PATTERN)) {
> RB_FOREACH(match, kvtree, keys) {
> - if (kv_match_key(kv, match->kv_key, FNM_CASEFOLD)) {
> + if (kv_match_key(kv, match->kv_key)) {
> log_debug("%s: %s \"%s\" matched key \"%s\"",
> __func__,
> - (kv->kv_flags & KV_FLAG_KEY_PATTERN) ?
> - "pattern" : "glob",
> + kv_key_flag_str(kv->kv_flags),
> kv->kv_key, match->kv_key);
> break;
> }
> @@ -1003,10 +1032,11 @@ rule_add(struct protocol *proto, struct relay_rule *rule, const char *rulefile)
> !(kv->kv_flags & KV_FLAG_VAL_PATTERN) &&
> strchr(kv->kv_value, '$') != NULL)
> kv->kv_flags |= KV_FLAG_MACRO;
> + /* Fallback to literal if no metacharacters found. */
> if (kv->kv_key != NULL &&
> - !(kv->kv_flags & KV_FLAG_KEY_PATTERN) &&
> - strpbrk(kv->kv_key, "*?[") != NULL)
> - kv->kv_flags |= KV_FLAG_GLOBBING;
> + (kv->kv_flags & KV_FLAG_KEY_GLOBBING_ICASE) &&
> + strpbrk(kv->kv_key, "*?[\\") == NULL)
> + kv->kv_flags &= ~KV_FLAG_KEY_GLOBBING_ICASE;
> }
>
> if (rulefile == NULL) {
> diff --git a/relayd.conf.5 b/relayd.conf.5
> index 07788f4..802999a 100644
> --- a/relayd.conf.5
> +++ b/relayd.conf.5
> @@ -1323,8 +1323,8 @@ $ echo -n "example.com/path/?args" | sha1
> may be one of:
> .Bl -tag -width Ds
> .It Xo Ic cookie Ar option
> -.Oo Oo Ic pattern Oc Ar key
> -.Oo Ic value Oo Ic pattern Oc Ar value Oc Oc
> +.Oo Oo Ar matching Oc Ar key
> +.Oo Ic value Oo Ar matching Oc Ar value Oc Oc
> .Xc
> Look up the entity as a value in the Cookie header.
> This type is only available with the direction
> @@ -1337,9 +1337,19 @@ block request quick cookie "session" value pattern "^admin_"
> # Only accept two-letter language cookies
> pass request cookie "lang" value pattern "^%a%a$"
> .Ed
> +.Pp
> +Without a
> +.Ar matching
> +keyword, the
> +.Ar key
> +is compared literally and case-insensitively, and the
> +.Ar value
> +is a case-insensitive
> +.Xr glob 7
> +pattern.
> .It Xo Ic header Ar option
> -.Oo Oo Ic pattern Oc Ar key
> -.Oo Ic value Oo Ic pattern Oc Ar value Oc Oc
> +.Oo Oo Ar matching Oc Ar key
> +.Oo Ic value Oo Ar matching Oc Ar value Oc Oc
> .Xc
> Look up the entity in the application protocol headers, like HTTP
> headers in
> @@ -1356,12 +1366,35 @@ block request quick header "User-Agent" \e
> pass request header "Content-Type" \e
> value pattern "^application/json"
>
> -# Strip any custom debug header (matches key by pattern)
> -pass request header remove pattern "^X%-Debug%-"
> +# Strip any custom debug header
> +# (pattern is case-sensitive, use classes for RFC 9110)
> +pass request header remove pattern "^[Xx]%-[Dd]ebug%-"
> .Ed
> +.Pp
> +Without a
> +.Ar matching
> +keyword or with
> +.Ic glob ,
> +the
> +.Ar key
> +is treated as a
> +.Ic glob ignorecase
> +pattern, so header names are matched case-insensitively (RFC 9110).
> +.Ic pattern
> +on a header
> +.Ar key
> +is case-sensitive.
> +Use character classes such as
> +.Dq [Xx]%-[Dd]ebug
> +to retain RFC 9110 semantics.
> +The
> +.Ar value
> +defaults to a case-sensitive
> +.Xr glob 7
> +pattern.
> .It Xo Ic path Ar option
> -.Oo Oo Ic pattern Oc Ar key
> -.Oo Ic value Oo Ic pattern Oc Ar value Oc Oc
> +.Oo Oo Ar matching Oc Ar key
> +.Oo Ic value Oo Ar matching Oc Ar value Oc Oc
> .Xc
> Look up the entity as a value in the URL path when using the
> .Ic http
> @@ -1387,6 +1420,18 @@ pass request path pattern "^/api/v%d+/"
> # Match image extensions
> pass request path pattern "%.jpe?g$"
> .Ed
> +.Pp
> +Without a
> +.Ar matching
> +keyword, the
> +.Ar key
> +is a case-sensitive
> +.Xr glob 7
> +pattern and the
> +.Ar value
> +is a case-insensitive
> +.Xr glob 7
> +pattern.
> .It Ic path strip Ar number
> Strip
> .Ar number
> @@ -1397,8 +1442,8 @@ protocol.
> This type is only available with the direction
> .Ic request .
> .It Xo Ic query Ar option
> -.Oo Oo Ic pattern Oc Ar key
> -.Oo Ic value Oo Ic pattern Oc Ar value Oc Oc
> +.Oo Oo Ar matching Oc Ar key
> +.Oo Ic value Oo Ar matching Oc Ar value Oc Oc
> .Xc
> Look up the entity as a query variable in the URL when using the
> .Ic http
> @@ -1416,9 +1461,19 @@ pass request query "id" value pattern "^%d+$"
> # Reject requests that carry a debug flag on any query key
> block request quick query pattern "^debug" value "*"
> .Ed
> +.Pp
> +Without a
> +.Ar matching
> +keyword, the
> +.Ar key
> +and
> +.Ar value
> +are case-sensitive
> +.Xr glob 7
> +patterns.
> .It Xo Ic url Ar option
> -.Oo Oo Ic pattern Oc Oo Ic digest Oc Ar key
> -.Oo Ic value Oo Ic pattern Oc Ar value Oc Oc
> +.Oo Oo Ar matching Oc Oo Ic digest Oc Ar key
> +.Oo Ic value Oo Ar matching Oc Ar value Oc Oc
> .Xc
> Look up the entity as a URL suffix/prefix expression consisting of a
> canonicalized hostname without port or suffix and a path name or
> @@ -1436,6 +1491,23 @@ block url pattern "^example%.com/index%.html$"
> pass request url pattern "^[%w%-]+%.example%.com/api/"
> .Ed
> .Pp
> +Without a
> +.Ar matching
> +keyword, the
> +.Ar key
> +is compared literally and case-insensitively, and the
> +.Ar value
> +is a case-insensitive
> +.Xr glob 7
> +pattern.
> +With the
> +.Ic log
> +option, the
> +.Ar key
> +instead defaults to a case-insensitive
> +.Xr glob 7
> +pattern for backward compatibility.
> +.Pp
> .Xr relayd 8
> will match the full URL and different possible suffix/prefix
> combinations by stripping subdomains and path components (up to 5
> @@ -1459,17 +1531,26 @@ example.com/1/2/3/
> .Ed
> .El
> .Pp
> -The optional
> -.Ic pattern
> -keyword makes the following string a
> -.Xr patterns 7
> -expression instead of the default
> -.Xr glob 7
> -pattern.
> +An optional
> +.Ar matching
> +keyword
> +.Po
> +.Ic pattern ,
> +.Ic glob ,
> +or
> +.Ic glob ignorecase
> +.Pc
> +selects how the following string is interpreted.
> +See
> +.Sx Matching options
> +below for details.
> It can appear before any matchable string, for example:
> .Bd -literal -offset indent
> pass request path pattern "^/api/v%d+/" \e
> value pattern "^id=%d+$"
> +
> +pass request header "User-Agent" \e
> + value glob ignorecase "*mozilla*"
> .Ed
> .Pp
> Note that macro expansion is disabled on a
> @@ -1485,25 +1566,12 @@ Use a plain
> .Ar value
> if macro expansion is required.
> .Pp
> -Matching with
> -.Ic pattern
> -is always case-sensitive.
> -The default
> +Use
> +.Ic glob ignorecase
> +to force case-insensitive
> .Xr glob 7
> -matching is case-insensitive for the
> -.Ic url
> -lookup, for
> -.Ic cookie
> -names and values, and for
> -.Ic header
> -names.
> -It is case-sensitive for
> -.Ic header
> -values, for the
> -.Ic path
> -name, and for
> -.Ic query
> -names and values, matching how the client sent them on the wire.
> +matching on fields that default to case-sensitive.
> +The per-type defaults are described with each lookup type above.
> When porting existing rules to
> .Ic pattern ,
> add character classes such as
> @@ -1518,34 +1586,54 @@ is omitted, it defaults to
> .Sq *
> and matches any value using
> .Xr glob 7
> -rules regardless of whether the key uses
> -.Ic pattern .
> +rules regardless of which
> +.Ar matching
> +keyword the key uses.
> .Pp
> For
> .Ic url ,
> -.Ic pattern
> -on the
> +a
> +.Ar matching
> +keyword on the
> .Ar key
> -is tested against every candidate form listed above; anchor patterns
> -carefully.
> +is tested against every candidate form listed above.
> +Anchor
> +.Xr patterns 7
> +expressions carefully.
> When combined with
> .Ic digest ,
> -the pattern applies to the digest string, not to the URL.
> +the match applies to the digest string, not to the URL.
> .Pp
> -.Ic pattern
> -on the
> +A
> +.Ar matching
> +keyword on the
> .Ar key
> is most useful with the
> .Ic remove
> and
> .Ic log
> options.
> -It has no effect for
> +It is rejected for
> .Ic append
> -or
> +and
> .Ic set ,
> which create a new entity from a literal name.
> .Pp
> +A single rule may contain at most one clause of each lookup type.
> +A second
> +.Ic cookie ,
> +.Ic header ,
> +.Ic path ,
> +.Ic query ,
> +or
> +.Ic url
> +clause in the same rule is rejected.
> +Use
> +.Ic tag
> +and
> +.Ic tagged
> +on separate rules to combine such conditions.
> +.Pp
> .Bq Ar option
> may be one of:
> .Bl -tag -width Ds
> @@ -1557,6 +1645,14 @@ to a protocol entity with the selected
> name.
> If it does not exist, it will be created with the new value.
> .Pp
> +The
> +.Ar key
> +and
> +.Ar value
> +are written rather than matched, so no
> +.Ar matching
> +keyword may be given.
> +.Pp
> The value string may contain predefined macros that will be expanded
> at runtime:
> .Pp
> @@ -1613,7 +1709,41 @@ may contain predefined macros that will be expanded at runtime,
> as detailed for the
> .Ic append
> directive above.
> +.Pp
> +As with
> +.Ic append ,
> +no
> +.Ar matching
> +keyword may be given.
> .El
> +.Ss Matching options
> +An optional
> +.Ar matching
> +keyword before a
> +.Ar key
> +or
> +.Ar value
> +string selects how the following string is interpreted.
> +It may be one of:
> +.Bl -tag -width Ds
> +.It Ic pattern
> +Treat the following string as a
> +.Xr patterns 7
> +expression.
> +Matching is always case-sensitive.
> +.It Ic glob
> +Treat the following string as a case-sensitive
> +.Xr glob 7
> +pattern.
> +.It Ic glob Ic ignorecase
> +Like
> +.Ic glob ,
> +but match case-insensitively.
> +.El
> +.Pp
> +The behavior without a
> +.Ar matching
> +keyword depends on the lookup type and is described with each type above.
> .Ss Routers
> Routers represent routing table entries in the kernel forwarding
> database, see
> diff --git a/relayd.h b/relayd.h
> index 4e055c5..192bc76 100644
> --- a/relayd.h
> +++ b/relayd.h
> @@ -323,11 +323,14 @@ enum digest_type {
> TAILQ_HEAD(kvlist, kv);
> RB_HEAD(kvtree, kv);
>
> -#define KV_FLAG_MACRO 0x01
> -#define KV_FLAG_INVALID 0x02
> -#define KV_FLAG_GLOBBING 0x04
> -#define KV_FLAG_KEY_PATTERN 0x08
> -#define KV_FLAG_VAL_PATTERN 0x10
> +#define KV_FLAG_MACRO 0x01
> +#define KV_FLAG_INVALID 0x02
> +#define KV_FLAG_KEY_PATTERN 0x04
> +#define KV_FLAG_KEY_GLOBBING 0x08
> +#define KV_FLAG_KEY_GLOBBING_ICASE 0x10
> +#define KV_FLAG_VAL_PATTERN 0x20
> +#define KV_FLAG_VAL_GLOBBING 0x40
> +#define KV_FLAG_VAL_GLOBBING_ICASE 0x80
>
> struct kv {
> char *kv_key;
> @@ -337,7 +340,8 @@ struct kv {
> enum key_option kv_option;
> enum digest_type kv_digest;
>
> - u_int8_t kv_flags;
> + /* 0 = literal, otherwise KV_FLAG_* */
> + u_int16_t kv_flags;
>
> struct kvlist kv_children;
> struct kv *kv_parent;
> @@ -1357,8 +1361,9 @@ int kv_log(struct rsession *, struct kv *, u_int16_t,
> struct kv *kv_find(struct kvtree *, struct kv *);
> struct kv *kv_find_value(struct kvtree *, char *, const char *,
> const char *);
> -int kv_match_key(const struct kv *, const char *, int);
> -int kv_match_val(const struct kv *, const char *, int);
> +int kv_match_key(const struct kv *, const char *);
> +int kv_match_val(const struct kv *, const char *);
> +const char *kv_key_flag_str(u_int16_t);
> int kv_cmp(struct kv *, struct kv *);
> int rule_add(struct protocol *, struct relay_rule *,
> const char *);
>
--
wbr, Kirill
relayd 1/3: Add new matching syntax to control the matching rules
relayd 1/3: Add new matching syntax to control the matching rules