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
31 changes: 30 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,33 @@ 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``.

``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.
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"]);

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

try {
if (bypassNode["header"]) {
_bypass_header = bypassNode["header"].as<std::string>();
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()));
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<int>(_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)
{
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