From 3e230b2641ec4214d864da86eed7ecbbc69798cd Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Wed, 13 May 2026 19:19:54 +0000 Subject: [PATCH 1/5] add bypass header config option to maxmind_acl plugin --- doc/admin-guide/plugins/maxmind_acl.en.rst | 31 +++++++- .../experimental/maxmind_acl/maxmind_acl.cc | 4 + plugins/experimental/maxmind_acl/mmdb.cc | 79 +++++++++++++++++++ plugins/experimental/maxmind_acl/mmdb.h | 6 ++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/doc/admin-guide/plugins/maxmind_acl.en.rst b/doc/admin-guide/plugins/maxmind_acl.en.rst index d0a4aacb97a..6dae9bef099 100644 --- a/doc/admin-guide/plugins/maxmind_acl.en.rst +++ b/doc/admin-guide/plugins/maxmind_acl.en.rst @@ -113,4 +113,33 @@ The plugin also supports optional fields from GeoGuard databases which includes: ``vpn_datacenter`` ``relay_proxy`` ``proxy_over_vpn`` -``smart_dns_proxy`` \ No newline at end of file +``smart_dns_proxy`` + +Bypass +====== + +An optional ``bypass`` field allows a request to skip all geo checks entirely and pass through +unmodified. If the specified request header is present, the plugin returns immediately without +performing any country, IP, regex, or anonymous evaluation. + +``header`` + Required sub-key. The name of the HTTP request header to look for, e.g. ``@GcdTaBypassGeo``. + +``value`` + Optional sub-key. When set, the header must also match this exact value for the bypass to + trigger. When omitted, the presence of the header alone is sufficient. + +An example configuration :: + + maxmind: + database: GeoIP2-City.mmdb + bypass: + header: "@GcdTaBypassGeo" + value: "1" # optional — omit to bypass on header presence alone + allow: + country: + - US + +This is useful for internal or trusted upstream services that should not be subject to geo +restrictions. If ``bypass`` is absent from the configuration, bypass is disabled and all +requests are evaluated normally. \ No newline at end of file diff --git a/plugins/experimental/maxmind_acl/maxmind_acl.cc b/plugins/experimental/maxmind_acl/maxmind_acl.cc index a6c6a26948f..b05aa50dc4c 100644 --- a/plugins/experimental/maxmind_acl/maxmind_acl.cc +++ b/plugins/experimental/maxmind_acl/maxmind_acl.cc @@ -68,6 +68,10 @@ TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) Dbg(dbg_ctl, "No ACLs configured"); } else { Acl *a = static_cast(ih); + if (a->check_bypass(rh)) { + Dbg(dbg_ctl, "bypassing geo check due to bypass header"); + return TSREMAP_NO_REMAP; + } if (!a->eval(rri, rh)) { Dbg(dbg_ctl, "denying request"); TSHttpTxnStatusSet(rh, TS_HTTP_STATUS_FORBIDDEN, PLUGIN_NAME); diff --git a/plugins/experimental/maxmind_acl/mmdb.cc b/plugins/experimental/maxmind_acl/mmdb.cc index 8600172f050..6b16d31a9b4 100644 --- a/plugins/experimental/maxmind_acl/mmdb.cc +++ b/plugins/experimental/maxmind_acl/mmdb.cc @@ -121,6 +121,9 @@ Acl::init(char const *filename) _proxy_over_vpn = false; _smart_dns_proxy = false; + _bypass_header.clear(); + _bypass_header_value.clear(); + if (loadallow(maxmind["allow"])) { Dbg(dbg_ctl, "Loaded Allow ruleset"); status = true; @@ -139,6 +142,8 @@ Acl::init(char const *filename) _anonymous_blocking = loadanonymous(maxmind["anonymous"]); + loadbypass(maxmind["bypass"]); + if (!status) { Dbg(dbg_ctl, "Failed to load any rulesets, none specified"); status = false; @@ -429,6 +434,38 @@ Acl::parseregex(const YAML::Node ®ex, bool allow) } } +bool +Acl::loadbypass(const YAML::Node &bypassNode) +{ + if (!bypassNode) { + Dbg(dbg_ctl, "No bypass set"); + return false; + } + if (bypassNode.IsNull()) { + Dbg(dbg_ctl, "bypass node is NULL"); + return false; + } + + try { + if (bypassNode["header"]) { + _bypass_header = bypassNode["header"].as(); + Dbg(dbg_ctl, "bypass header set to: %s", _bypass_header.c_str()); + if (bypassNode["value"]) { + _bypass_header_value = bypassNode["value"].as(); + Dbg(dbg_ctl, "bypass value set to: %s", _bypass_header_value.c_str()); + } + } else { + Dbg(dbg_ctl, "bypass missing 'header' key"); + return false; + } + } catch (const YAML::Exception &e) { + TSError("[%s] YAML::Exception %s when parsing bypass config", PLUGIN_NAME, e.what()); + return false; + } + + return !_bypass_header.empty(); +} + void Acl::loadhtml(const YAML::Node &htmlNode) { @@ -503,6 +540,48 @@ Acl::loaddb(const YAML::Node &dbNode) return true; } +bool +Acl::check_bypass(TSHttpTxn txnp) const +{ + if (_bypass_header.empty()) { + return false; + } + + TSMBuffer mbuf; + TSMLoc hdr_loc; + if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &mbuf, &hdr_loc)) { + Dbg(dbg_ctl, "check_bypass: failed to get client request headers"); + return false; + } + + TSMLoc field_loc = TSMimeHdrFieldFind(mbuf, hdr_loc, _bypass_header.c_str(), static_cast(_bypass_header.size())); + if (TS_NULL_MLOC == field_loc) { + TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdr_loc); + return false; + } + + bool bypassed = false; + if (_bypass_header_value.empty()) { + // presence-only check + Dbg(dbg_ctl, "check_bypass: bypass header '%s' present", _bypass_header.c_str()); + bypassed = true; + } else { + int val_len = 0; + const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, 0, &val_len); + if (val != nullptr && static_cast(_bypass_header_value.size()) == val_len && + _bypass_header_value.compare(0, std::string::npos, val, val_len) == 0) { + Dbg(dbg_ctl, "check_bypass: bypass header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str()); + bypassed = true; + } else { + Dbg(dbg_ctl, "check_bypass: bypass header present but value did not match"); + } + } + + TSHandleMLocRelease(mbuf, hdr_loc, field_loc); + TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdr_loc); + return bypassed; +} + bool Acl::eval(TSRemapRequestInfo * /* rri ATS_UNUSED */, TSHttpTxn txnp) { diff --git a/plugins/experimental/maxmind_acl/mmdb.h b/plugins/experimental/maxmind_acl/mmdb.h index 9b1d622a204..5972efb715f 100644 --- a/plugins/experimental/maxmind_acl/mmdb.h +++ b/plugins/experimental/maxmind_acl/mmdb.h @@ -69,6 +69,7 @@ class Acl } bool eval(TSRemapRequestInfo *rri, TSHttpTxn txnp); + bool check_bypass(TSHttpTxn txnp) const; bool init(char const *filename); void @@ -111,6 +112,10 @@ class Acl bool _anonymous_blocking = false; + // Bypass header fields + std::string _bypass_header; + std::string _bypass_header_value; + // Do we want to allow by default or not? Useful // for deny only rules bool default_allow = false; @@ -121,6 +126,7 @@ class Acl bool loaddeny(const YAML::Node &denyNode); void loadhtml(const YAML::Node &htmlNode); bool loadanonymous(const YAML::Node &anonNode); + bool loadbypass(const YAML::Node &bypassNode); bool eval_country(MMDB_entry_data_s *entry_data, const std::string &url); bool eval_anonymous(MMDB_entry_s *entry_data); void parseregex(const YAML::Node ®ex, bool allow); From 4a7c96078a609195e3916f57385d2bce1caebf6a Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Wed, 20 May 2026 13:45:43 -0600 Subject: [PATCH 2/5] fix for header value check, update docs with warning about usage of this new feature --- doc/admin-guide/plugins/maxmind_acl.en.rst | 11 ++++++++++- plugins/experimental/maxmind_acl/mmdb.cc | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/admin-guide/plugins/maxmind_acl.en.rst b/doc/admin-guide/plugins/maxmind_acl.en.rst index 6dae9bef099..ddca10e9087 100644 --- a/doc/admin-guide/plugins/maxmind_acl.en.rst +++ b/doc/admin-guide/plugins/maxmind_acl.en.rst @@ -142,4 +142,13 @@ An example configuration :: This is useful for internal or trusted upstream services that should not be subject to geo restrictions. If ``bypass`` is absent from the configuration, bypass is disabled and all -requests are evaluated normally. \ No newline at end of file +requests are evaluated normally. + +.. warning:: + + Because the bypass skips **all** ACL checks, the configured header must be + unforgeable by external clients. Use an internal ``@``-prefixed header (e.g. + ``@GcdTaBypassGeo``) that is set by ATS itself or a trusted upstream, or + ensure the edge strips/overwrites the header before it reaches this plugin. + Configuring a normal client-supplied header allows end users to opt out of + geo restrictions by simply sending the header in their request. \ No newline at end of file diff --git a/plugins/experimental/maxmind_acl/mmdb.cc b/plugins/experimental/maxmind_acl/mmdb.cc index 6b16d31a9b4..94443b8da6b 100644 --- a/plugins/experimental/maxmind_acl/mmdb.cc +++ b/plugins/experimental/maxmind_acl/mmdb.cc @@ -567,7 +567,7 @@ Acl::check_bypass(TSHttpTxn txnp) const bypassed = true; } else { int val_len = 0; - const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, 0, &val_len); + const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, -1, &val_len); if (val != nullptr && static_cast(_bypass_header_value.size()) == val_len && _bypass_header_value.compare(0, std::string::npos, val, val_len) == 0) { Dbg(dbg_ctl, "check_bypass: bypass header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str()); From c49cbdb25544c3d3a6a1e0bf71fdf23d4d1a73dd Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 22 May 2026 08:35:04 -0600 Subject: [PATCH 3/5] require both header and value. clean up review suggestions --- doc/admin-guide/plugins/maxmind_acl.en.rst | 21 ++++++---- plugins/experimental/maxmind_acl/mmdb.cc | 45 ++++++++++------------ plugins/experimental/maxmind_acl/mmdb.h | 2 +- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/doc/admin-guide/plugins/maxmind_acl.en.rst b/doc/admin-guide/plugins/maxmind_acl.en.rst index ddca10e9087..120f6d1ee92 100644 --- a/doc/admin-guide/plugins/maxmind_acl.en.rst +++ b/doc/admin-guide/plugins/maxmind_acl.en.rst @@ -123,32 +123,37 @@ unmodified. If the specified request header is present, the plugin returns immed performing any country, IP, regex, or anonymous evaluation. ``header`` - Required sub-key. The name of the HTTP request header to look for, e.g. ``@GcdTaBypassGeo``. + Required sub-key. The name of the HTTP request header to look for, e.g. ``@GeoBypass``. ``value`` - Optional sub-key. When set, the header must also match this exact value for the bypass to - trigger. When omitted, the presence of the header alone is sufficient. + Required sub-key. The header field value must match this string exactly for the bypass to + trigger. Both ``header`` and ``value`` must be present and non-empty; omitting either + disables the bypass entirely and a warning is emitted to the ATS error log. + +The comparison uses the complete, raw field value of the first occurrence of the named header. +Requests where the header appears multiple times (comma-separated or repeated lines) will not +match, because the combined multi-value string will not equal the configured ``value``. An example configuration :: maxmind: database: GeoIP2-City.mmdb bypass: - header: "@GcdTaBypassGeo" - value: "1" # optional — omit to bypass on header presence alone + header: "@GeoBypass" + value: "1" allow: country: - US This is useful for internal or trusted upstream services that should not be subject to geo -restrictions. If ``bypass`` is absent from the configuration, bypass is disabled and all -requests are evaluated normally. +restrictions. If ``bypass`` is absent from the configuration, or if either ``header`` or +``value`` is missing, bypass is disabled and all requests are evaluated normally. .. warning:: Because the bypass skips **all** ACL checks, the configured header must be unforgeable by external clients. Use an internal ``@``-prefixed header (e.g. - ``@GcdTaBypassGeo``) that is set by ATS itself or a trusted upstream, or + ``@GeoBypass``) that is set by ATS itself or a trusted upstream, or ensure the edge strips/overwrites the header before it reaches this plugin. Configuring a normal client-supplied header allows end users to opt out of geo restrictions by simply sending the header in their request. \ No newline at end of file diff --git a/plugins/experimental/maxmind_acl/mmdb.cc b/plugins/experimental/maxmind_acl/mmdb.cc index 94443b8da6b..5ab6e80f818 100644 --- a/plugins/experimental/maxmind_acl/mmdb.cc +++ b/plugins/experimental/maxmind_acl/mmdb.cc @@ -434,36 +434,40 @@ Acl::parseregex(const YAML::Node ®ex, bool allow) } } -bool +void Acl::loadbypass(const YAML::Node &bypassNode) { if (!bypassNode) { Dbg(dbg_ctl, "No bypass set"); - return false; + return; } if (bypassNode.IsNull()) { Dbg(dbg_ctl, "bypass node is NULL"); - return false; + return; } try { if (bypassNode["header"]) { + if (!bypassNode["value"]) { + TSWarning("[%s] bypass 'header' set without 'value' — bypass disabled; both are required", PLUGIN_NAME); + return; + } + _bypass_header_value = bypassNode["value"].as(); + if (_bypass_header_value.empty()) { + TSWarning("[%s] bypass 'value' is empty — bypass disabled; a non-empty value is required", PLUGIN_NAME); + return; + } _bypass_header = bypassNode["header"].as(); Dbg(dbg_ctl, "bypass header set to: %s", _bypass_header.c_str()); - if (bypassNode["value"]) { - _bypass_header_value = bypassNode["value"].as(); - Dbg(dbg_ctl, "bypass value set to: %s", _bypass_header_value.c_str()); - } + Dbg(dbg_ctl, "bypass value set to: %s", _bypass_header_value.c_str()); } else { Dbg(dbg_ctl, "bypass missing 'header' key"); - return false; + return; } } catch (const YAML::Exception &e) { TSError("[%s] YAML::Exception %s when parsing bypass config", PLUGIN_NAME, e.what()); - return false; + return; } - - return !_bypass_header.empty(); } void @@ -560,21 +564,14 @@ Acl::check_bypass(TSHttpTxn txnp) const return false; } - bool bypassed = false; - if (_bypass_header_value.empty()) { - // presence-only check - Dbg(dbg_ctl, "check_bypass: bypass header '%s' present", _bypass_header.c_str()); + bool bypassed = false; + int val_len = 0; + const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, -1, &val_len); + if (val != nullptr && 0 < val_len && std::string_view(val, val_len) == _bypass_header_value) { + Dbg(dbg_ctl, "check_bypass: bypass header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str()); bypassed = true; } else { - int val_len = 0; - const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, -1, &val_len); - if (val != nullptr && static_cast(_bypass_header_value.size()) == val_len && - _bypass_header_value.compare(0, std::string::npos, val, val_len) == 0) { - Dbg(dbg_ctl, "check_bypass: bypass header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str()); - bypassed = true; - } else { - Dbg(dbg_ctl, "check_bypass: bypass header present but value did not match"); - } + Dbg(dbg_ctl, "check_bypass: bypass header present but value did not match"); } TSHandleMLocRelease(mbuf, hdr_loc, field_loc); diff --git a/plugins/experimental/maxmind_acl/mmdb.h b/plugins/experimental/maxmind_acl/mmdb.h index 5972efb715f..13667500eb5 100644 --- a/plugins/experimental/maxmind_acl/mmdb.h +++ b/plugins/experimental/maxmind_acl/mmdb.h @@ -126,7 +126,7 @@ class Acl bool loaddeny(const YAML::Node &denyNode); void loadhtml(const YAML::Node &htmlNode); bool loadanonymous(const YAML::Node &anonNode); - bool loadbypass(const YAML::Node &bypassNode); + void loadbypass(const YAML::Node &bypassNode); bool eval_country(MMDB_entry_data_s *entry_data, const std::string &url); bool eval_anonymous(MMDB_entry_s *entry_data); void parseregex(const YAML::Node ®ex, bool allow); From 7b2cdedeb977b9270a7d01fb71ed0573bfe093d3 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 22 May 2026 10:03:25 -0600 Subject: [PATCH 4/5] more explicit documentation about value match, limit debug log --- doc/admin-guide/plugins/maxmind_acl.en.rst | 10 ++++++---- plugins/experimental/maxmind_acl/mmdb.cc | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/admin-guide/plugins/maxmind_acl.en.rst b/doc/admin-guide/plugins/maxmind_acl.en.rst index 120f6d1ee92..32fc35d199e 100644 --- a/doc/admin-guide/plugins/maxmind_acl.en.rst +++ b/doc/admin-guide/plugins/maxmind_acl.en.rst @@ -119,8 +119,9 @@ Bypass ====== An optional ``bypass`` field allows a request to skip all geo checks entirely and pass through -unmodified. If the specified request header is present, the plugin returns immediately without -performing any country, IP, regex, or anonymous evaluation. +unmodified. Both a header name and an expected value must be configured; when the named header +is present in the request **and** its value matches exactly, the plugin returns immediately +without performing any country, IP, regex, or anonymous evaluation. ``header`` Required sub-key. The name of the HTTP request header to look for, e.g. ``@GeoBypass``. @@ -131,8 +132,9 @@ performing any country, IP, regex, or anonymous evaluation. disables the bypass entirely and a warning is emitted to the ATS error log. The comparison uses the complete, raw field value of the first occurrence of the named header. -Requests where the header appears multiple times (comma-separated or repeated lines) will not -match, because the combined multi-value string will not equal the configured ``value``. +Duplicate headers with the same name (repeated lines) are ignored — only the first is evaluated. +Within that first field, the entire value must match exactly, so a comma-separated multi-value +(e.g. ``@GeoBypass: 1, extra``) in a single header line will not match a simple configured value. An example configuration :: diff --git a/plugins/experimental/maxmind_acl/mmdb.cc b/plugins/experimental/maxmind_acl/mmdb.cc index 5ab6e80f818..da9949046d9 100644 --- a/plugins/experimental/maxmind_acl/mmdb.cc +++ b/plugins/experimental/maxmind_acl/mmdb.cc @@ -568,7 +568,7 @@ Acl::check_bypass(TSHttpTxn txnp) const int val_len = 0; const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, -1, &val_len); if (val != nullptr && 0 < val_len && std::string_view(val, val_len) == _bypass_header_value) { - Dbg(dbg_ctl, "check_bypass: bypass header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str()); + Dbg(dbg_ctl, "check_bypass: bypass triggered"); bypassed = true; } else { Dbg(dbg_ctl, "check_bypass: bypass header present but value did not match"); From 9c1689f0b7bdba0657874f4cbfa4006d4ef2cc3f Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 22 May 2026 10:32:45 -0600 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/experimental/maxmind_acl/mmdb.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/experimental/maxmind_acl/mmdb.cc b/plugins/experimental/maxmind_acl/mmdb.cc index da9949046d9..7602c767bce 100644 --- a/plugins/experimental/maxmind_acl/mmdb.cc +++ b/plugins/experimental/maxmind_acl/mmdb.cc @@ -459,7 +459,7 @@ Acl::loadbypass(const YAML::Node &bypassNode) } _bypass_header = bypassNode["header"].as(); Dbg(dbg_ctl, "bypass header set to: %s", _bypass_header.c_str()); - Dbg(dbg_ctl, "bypass value set to: %s", _bypass_header_value.c_str()); + Dbg(dbg_ctl, "bypass value is configured"); } else { Dbg(dbg_ctl, "bypass missing 'header' key"); return;