diff --git a/doc/admin-guide/plugins/maxmind_acl.en.rst b/doc/admin-guide/plugins/maxmind_acl.en.rst index d0a4aacb97a..32fc35d199e 100644 --- a/doc/admin-guide/plugins/maxmind_acl.en.rst +++ b/doc/admin-guide/plugins/maxmind_acl.en.rst @@ -113,4 +113,49 @@ 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. 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``. + +``value`` + 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. +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 :: + + maxmind: + database: GeoIP2-City.mmdb + bypass: + 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, 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. + ``@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/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..7602c767bce 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,42 @@ Acl::parseregex(const YAML::Node ®ex, bool allow) } } +void +Acl::loadbypass(const YAML::Node &bypassNode) +{ + if (!bypassNode) { + Dbg(dbg_ctl, "No bypass set"); + return; + } + if (bypassNode.IsNull()) { + Dbg(dbg_ctl, "bypass node is NULL"); + 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()); + Dbg(dbg_ctl, "bypass value is configured"); + } else { + Dbg(dbg_ctl, "bypass missing 'header' key"); + return; + } + } catch (const YAML::Exception &e) { + TSError("[%s] YAML::Exception %s when parsing bypass config", PLUGIN_NAME, e.what()); + return; + } +} + void Acl::loadhtml(const YAML::Node &htmlNode) { @@ -503,6 +544,41 @@ 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; + 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 triggered"); + 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..13667500eb5 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); + 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);