Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
45 changes: 44 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,47 @@ 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. 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. ``@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.
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``.
Comment thread
traeak marked this conversation as resolved.
Outdated

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 set to: %s", _bypass_header_value.c_str());
Comment thread
traeak marked this conversation as resolved.
Outdated
Comment thread
traeak marked this conversation as resolved.
Outdated
} 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 header '%s' matched value '%s'", _bypass_header.c_str(), _bypass_header_value.c_str());
Comment thread
traeak marked this conversation as resolved.
Outdated
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