Download raw body.
relayd 1/3: Add new matching syntax to control the matching rules
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.
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 *);
relayd 1/3: Add new matching syntax to control the matching rules