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);