Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
40 changes: 39 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,42 @@ 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. ``@GcdTaBypassGeo``.
Comment thread
traeak marked this conversation as resolved.
Outdated

``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.

.. 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.
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
79 changes: 79 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,38 @@ Acl::parseregex(const YAML::Node &regex, 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;
}
Comment on lines +440 to +447

try {
if (bypassNode["header"]) {
_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());
if (bypassNode["value"]) {
_bypass_header_value = bypassNode["value"].as<std::string>();
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)
{
Expand Down Expand Up @@ -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<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;
if (_bypass_header_value.empty()) {
// presence-only check
Dbg(dbg_ctl, "check_bypass: bypass header '%s' present", _bypass_header.c_str());
bypassed = true;
Comment thread
traeak marked this conversation as resolved.
Outdated
} else {
int val_len = 0;
const char *val = TSMimeHdrFieldValueStringGet(mbuf, hdr_loc, field_loc, -1, &val_len);
if (val != nullptr && static_cast<int>(_bypass_header_value.size()) == val_len &&
Comment thread
traeak marked this conversation as resolved.
Outdated
_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)
{
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);
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 &regex, bool allow);
Expand Down