Document and test ? wildcards in network proxy domain patterns (#46027)

## What changed

Document the existing `?` wildcard behavior: it matches exactly one character,
including a dot, in allow and deny patterns. For example, `api?.example.com`
matches `api1.example.com`, but not `api.example.com` or `api12.example.com`.

## Testing

Add table-driven tests for both allowlist and denylist compilation, covering
single and repeated `?` wildcards, combinations with `*`, `*.`, and `**.`,
dot matching, and host and pattern normalization.

GitOrigin-RevId: dd5cd1817c6ea0b701335a38a57fbb5e8039c284
This commit is contained in:
viyatb-oai
2026-09-16 20:54:16 +00:00
committed by copyberry
parent a8c36ca6d2
commit 47c27cbffa
2 changed files with 47 additions and 0 deletions

View File

@@ -214,6 +214,9 @@ what it can reasonably guarantee.
- Allowlist-first policy: if `domains` has no `allow` entries, requests are blocked until an allowlist is configured.
- Domain patterns: exact hosts are supported, `*.example.com` matches subdomains only, and `**.example.com` matches the apex plus subdomains; the global `*` wildcard is only accepted when explicitly enabled for allowlist compilation and is otherwise rejected.
- Within a domain pattern, `?` matches exactly one character, including a dot. For example,
`api?.example.com` matches `api1.example.com`, but not `api.example.com` or `api12.example.com`.
It can be combined with `*`, `*.`, and `**.` in both allow and deny entries.
- Deny wins: `domains` entries marked `deny` always override the allowlist.
- Local/private network protection: when `allow_local_binding = false`, the proxy blocks loopback
and common private/link-local ranges. Explicit allowlisting of local IP literals (or `localhost`)

View File

@@ -200,6 +200,9 @@ pub(crate) fn compile_denylist_globset(patterns: &[String]) -> Result<GlobSet> {
compile_globset_with_policy(patterns, GlobalWildcard::Reject)
}
// Browser network-policy matchers implement a subset of this hostname grammar.
// Keep changes to shared grammar and normalization in sync with their contract
// cases and the Rust tests below, including compile_globset_supports_question_mark_wildcards.
fn compile_globset_with_policy(
patterns: &[String],
global_wildcard: GlobalWildcard,
@@ -217,6 +220,7 @@ fn compile_globset_with_policy(
// - "example.com": match the exact host
// - "*.example.com": match any subdomain (not the apex)
// - "**.example.com": match the apex and any subdomain
// - "api?.example.com": match exactly one character after "api"
// - "*": match every host when explicitly enabled for allowlist compilation
for candidate in expand_domain_pattern(&pattern) {
if !seen.insert(candidate.clone()) {
@@ -411,6 +415,46 @@ mod tests {
assert_eq!(false, set.is_match("foo.region1.v2.argotunnel.com"));
}
// Keep this table one-for-one with the browser network-policy matchers'
// question-mark contract cases so grammar changes are checked on both sides.
#[test]
fn compile_globset_supports_question_mark_wildcards() -> Result<()> {
for (pattern, host, expected) in [
("api?.example.com", "api1.example.com", true),
("api?.example.com", "api.example.com", false),
("api?.example.com", "api12.example.com", false),
("api??.example.com", "api12.example.com", true),
("api??.example.com", "api1.example.com", false),
("api*?.example.com", "api.example.com", false),
("api*?.example.com", "api1.example.com", true),
("api*?.example.com", "api123.example.com", true),
("api?example.com", "api.example.com", true),
("*.api?.example.com", "api1.example.com", false),
("*.api?.example.com", "www.api1.example.com", true),
("*.api?.example.com", "nested.www.api1.example.com", true),
("*.api?.example.com", "www.api12.example.com", false),
("**.api?.example.com", "api1.example.com", true),
("**.api?.example.com", "www.api1.example.com", true),
("**.api?.example.com", "nested.www.api1.example.com", true),
("**.api?.example.com", "api12.example.com", false),
("**.api?.example.com", "www.api12.example.com", false),
(" API?.EXAMPLE.COM. ", "API1.EXAMPLE.COM.", true),
] {
let patterns = [pattern.to_string()];
for set in [
compile_allowlist_globset(&patterns)?,
compile_denylist_globset(&patterns)?,
] {
assert_eq!(
set.is_match(normalize_host(host)),
expected,
"pattern {pattern}, host {host}"
);
}
}
Ok(())
}
#[test]
fn compile_globset_normalizes_apex_and_subdomains() {
let set = compile_denylist_globset(&["**.Example.COM.".to_string()]).unwrap();