Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion doc/admin-guide/plugins/maxmind_acl.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
``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.
Comment thread
traeak marked this conversation as resolved.

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.
4 changes: 4 additions & 0 deletions plugins/experimental/maxmind_acl/maxmind_acl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
Dbg(dbg_ctl, "No ACLs configured");
} else {
Acl *a = static_cast<Acl *>(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);
Expand Down
76 changes: 76 additions & 0 deletions plugins/experimental/maxmind_acl/mmdb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -139,6 +142,8 @@ Acl::init(char const *filename)

_anonymous_blocking = loadanonymous(maxmind["anonymous"]);

loadbypass(maxmind["bypass"]);
Comment thread
traeak marked this conversation as resolved.

if (!status) {
Dbg(dbg_ctl, "Failed to load any rulesets, none specified");
status = false;
Expand Down Expand Up @@ -429,6 +434,42 @@ Acl::parseregex(const YAML::Node &regex, 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;
}
Comment on lines +440 to +447

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<std::string>();
if (_bypass_header_value.empty()) {
TSWarning("[%s] bypass 'value' is empty — bypass disabled; a non-empty value is required", PLUGIN_NAME);
return;
Comment thread
traeak marked this conversation as resolved.
}
_bypass_header = bypassNode["header"].as<std::string>();
Comment thread
traeak marked this conversation as resolved.
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)
{
Expand Down Expand Up @@ -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<int>(_bypass_header.size()));
Comment thread
traeak marked this conversation as resolved.
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)
{
Expand Down
6 changes: 6 additions & 0 deletions plugins/experimental/maxmind_acl/mmdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class Acl
}

bool eval(TSRemapRequestInfo *rri, TSHttpTxn txnp);
bool check_bypass(TSHttpTxn txnp) const;
bool init(char const *filename);

void
Expand Down Expand Up @@ -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;
Expand All @@ -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 &regex, bool allow);
Expand Down