From: Kirill A. Korinsky Subject: Re: relayd 1/3: Add new matching syntax to control the matching rules To: Rafael Sadowski Cc: tech@openbsd.org Date: Tue, 20 Oct 2026 23:06:55 +0200 On Fri, 09 Oct 2026 14:18:30 +0200, Rafael Sadowski 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 > 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