Configuring Reliable GeoIP Filtering on MikroTik RouterOS
Generate a CIDR list by geographic region, validate it before publishing, import it without wiping the last working version, and apply it to the correct firewall chains.
RouterOS does not contain a built-in GeoIP database allowing you to select a country in a firewall rule. The mechanism relies on external CIDR prefix lists : the router imports them into an address-list, then an input or forward rule uses this list as a criterion.
The tricky part is not the final rule. It consists of obtaining verifiable data, never replacing a functional list with an empty file, and placing the filter in the chain that actually corresponds to the service. This guide builds an IPv4 list for Belgium, while allowing you to easily add France, the Netherlands, or Luxembourg.
How GeoIP filtering works
A geographic database associates address blocks with a territory code. For a firewall, these blocks simply become networks like 203.0.113.0/24. RouterOS performs no geographic lookup during packet processing : it only checks if the source address belongs to one of the entries in the list.
| Component | Role | Risk to control |
|---|---|---|
| RIR data | Provide registered prefixes by ISO code | The registration country may differ from the actual location |
| Generator | Validate and convert CIDRs to RouterOS commands | An empty download must never overwrite the active list |
| Address-list | Store networks in RouterOS | Thousands of entries consume memory and import time |
| Firewall | Allow or deny based on the list | A wrong chain or order makes the filter ineffective |
| Scheduler | Update the data | Permissions and errors must be visible |
The lists used below come from the allocation data of regional Internet registries, grouped by ISO code and published daily. They are smaller than a highly detailed geolocation database, which is better suited for a router. However, they describe the registration of Internet resources, not a GPS position.
Recommended architecture
The router should not directly depend on a web page transformed on the fly. A Linux server or NAS downloads the data, validates each CIDR, builds a .rsc file, and then publishes it over HTTPS. MikroTik downloads this file, performs a dry-run import, and applies it only if its syntax is valid.
Preparing the list server
The example assumes a server with bash, curl, and Python 3. Python only uses the standard ipaddress module here : no additional library is required.
Create a directory published by your web server, for example /srv/www/geoip, then protect it with HTTPS. The following script downloads the Belgian list, rejects a file that is too short, validates each IPv4 network, aggregates the prefixes that can be aggregated, and generates a RouterOS file atomically.
#!/usr/bin/env bash
set -Eeuo pipefail
COUNTRIES=(be)
BASE_URL="https://raw.githubusercontent.com/ipverse/rir-ip/master/country"
PUBLISH_DIR="/srv/www/geoip"
FINAL_FILE="$PUBLISH_DIR/GeoIP-BE.rsc"
WORK_DIR="$(mktemp -d)"
PUBLISH_TMP="$(mktemp "$PUBLISH_DIR/.GeoIP-BE.XXXXXX")"
cleanup() {
rm -rf "$WORK_DIR"
if [[ -n "${PUBLISH_TMP:-}" && -f "$PUBLISH_TMP" ]]; then
rm -f "$PUBLISH_TMP"
fi
}
trap cleanup EXIT
RAW_FILE="$WORK_DIR/all-prefixes.txt"
VALID_FILE="$WORK_DIR/validated-prefixes.txt"
: > "$RAW_FILE"
for country in "${COUNTRIES[@]}"; do
curl --fail --location --silent --show-error --retry 3 \
"$BASE_URL/$country/ipv4-aggregated.txt" \
--output "$WORK_DIR/$country.txt"
grep -Ev '^(#|$)' "$WORK_DIR/$country.txt" >> "$RAW_FILE"
done
python3 - "$RAW_FILE" "$VALID_FILE" <<'PY'
import ipaddress
import sys
source, destination = sys.argv[1:3]
networks = []
with open(source, encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line:
continue
network = ipaddress.ip_network(line, strict=True)
if network.version != 4:
raise SystemExit(f"Non-IPv4 prefix: {line}")
networks.append(network)
collapsed = list(ipaddress.collapse_addresses(networks))
if len(collapsed) < 100:
raise SystemExit("Abnormally short list: publication cancelled")
with open(destination, "w", encoding="ascii") as handle:
for network in collapsed:
handle.write(f"{network}\n")
PY
python3 - "$VALID_FILE" "$PUBLISH_TMP" <<'PY'
import sys
source, destination = sys.argv[1:3]
with open(source, encoding="ascii") as handle:
prefixes = [line.strip() for line in handle if line.strip()]
with open(destination, "w", encoding="ascii") as output:
output.write(':do { /ip firewall address-list remove '
'[find where list="GeoIP-BE-next"] } on-error={}\n')
for prefix in prefixes:
output.write('/ip firewall address-list add '
f'list="GeoIP-BE-next" address={prefix} '
'comment="GeoIP generated"\n')
output.write(':local imported [/ip firewall address-list print '
'count-only where list="GeoIP-BE-next"]\n')
output.write(':if ($imported < 100) do={ '
':error "Liste GeoIP trop courte" }\n')
output.write('/ip firewall address-list remove '
'[find where list="GeoIP-BE"]\n')
output.write(':foreach id in=[/ip firewall address-list find '
'where list="GeoIP-BE-next"] do={ '
'/ip firewall address-list set numbers=$id '
'list="GeoIP-BE" }\n')
output.write(':log info ("GeoIP: active list, " . '
'$imported . " prefixes")\n')
PY
chmod 0644 "$PUBLISH_TMP"
mv -f "$PUBLISH_TMP" "$FINAL_FILE"
PUBLISH_TMP=""
echo "Published: $FINAL_FILE"
To build a broader zone, replace COUNTRIES=(be) with COUNTRIES=(be fr nl lu) and adapt the names GeoIP-BE and GeoIP-BE-next. Start with a single zone : an extended list increases import time and memory usage.
Why this generator is safer
curl --failturns an HTTP error into a failure instead of saving an error page as a list ;ipaddressrejects invalid CIDR syntax and verifies that it is indeed IPv4 ;- a minimum threshold prevents the publication of an empty or manifestly incomplete file ;
- the public file is only replaced after complete generation ;
- the RouterOS file first loads
GeoIP-BE-next, checks its volume, then switches toGeoIP-BE.
Run the generator manually, check the published file, then schedule it daily on the server. Keep the logs and do not immediately delete the last known valid version.
Testing the import on MikroTik
Before any automation, export the firewall and enable Safe Mode. Replace the documentation domain below with your own HTTPS URL.
/ip firewall export file=avant-geoip
/system backup save name=avant-geoip
/tool fetch \
url="https://updates.example.net/geoip/GeoIP-BE.rsc" \
output=file dst-path="GeoIP-BE.rsc"
/import file-name="GeoIP-BE.rsc" verbose=yes dry-run=yes
/import file-name="GeoIP-BE.rsc" verbose=yes
The dry-run mode of the import is available on recent versions of RouterOS 7. It parses the file without modifying the configuration. The environment for this guide targets RouterOS 7.16 or later.
/ip firewall address-list print count-only where list="GeoIP-BE"
/ip firewall address-list print where list="GeoIP-BE"
/log print where message~"GeoIP"
A Belgian import normally produces well over a hundred prefixes. The threshold serves only as a safeguard against a manifest failure ; it proves neither the completeness nor the geographic quality of the dataset.
Automating the download in RouterOS
Create a script named update-geoip with the policies ftp, read, write, policy, and test. The ftp policy is required for file operations, even if the download itself uses HTTPS.
:local geoUrl "https://updates.example.net/geoip/GeoIP-BE.rsc"
:local geoFile "GeoIP-BE.rsc"
:do { /file remove [find where name=$geoFile] } on-error={}
:do {
/tool fetch url=$geoUrl output=file dst-path=$geoFile
/import file-name=$geoFile verbose=yes dry-run=yes
/import file-name=$geoFile verbose=yes
:log info "GeoIP: update complete"
} on-error={
:log error "GeoIP: failed, previous list kept"
}
The imported file works on the temporary list. A download failure, a syntax error, or an abnormal volume interrupts the procedure before the deletion of GeoIP-BE.
After a successful manual test, schedule the execution once a day, at a time when the router is under low load.
/system scheduler add \
name=update-geoip-daily \
start-time=05:40:00 interval=1d \
on-event="/system script run update-geoip" \
policy=ftp,read,write,policy,test \
comment="GeoIP list refresh"
Applying the list to the right place
The input chain handles traffic destined for the router itself. The forward chain handles traffic that traverses the MikroTik to an internal server. A GeoIP rule placed only in input therefore does not restrict a server published via dst-nat.
Case 1 : restricting access to a VPN hosted on the router
The example allows a WireGuard endpoint on UDP 51820 from the GeoIP list. It assumes that a rule dropping WAN traffic to the router exists further down. Do not publish WinBox or WebFig directly to the Internet.
/ip firewall address-list
add list=GeoIP-TRUSTED address=198.51.100.10 \
comment="Fallback address - RFC 5737 example"
/ip firewall filter
add chain=input action=accept in-interface-list=WAN \
protocol=udp dst-port=51820 \
src-address-list=GeoIP-TRUSTED \
comment="VPN: fallback access"
add chain=input action=accept in-interface-list=WAN \
protocol=udp dst-port=51820 \
src-address-list=GeoIP-BE \
comment="VPN: allow GeoIP zone"
Replace the documentation address with a real controlled fallback address. Place the rules after the acceptance of established,related and the rejection of invalid packets, but before the general WAN drop.
Case 2 : restricting a server published via dst-nat
The following rule drops new TCP connections to public port 443 when they do not belong to GeoIP-BE. A fallback rule placed before it allows an explicit exception.
/ip firewall filter
add chain=forward action=accept in-interface-list=WAN \
connection-state=new connection-nat-state=dstnat \
protocol=tcp dst-port=443 \
src-address-list=GeoIP-TRUSTED \
comment="Server: fallback access"
add chain=forward action=drop in-interface-list=WAN \
connection-state=new connection-nat-state=dstnat \
protocol=tcp dst-port=443 \
src-address-list=!GeoIP-BE \
comment="Server: deny outside GeoIP zone"
Insert these rules before any general acceptance of dstnat connections. The port must be the public port seen by the filter. If external port 8443 is translated to 443, use 8443 in this criterion.
Testing without locking yourself out
- Work from the LAN and enable Safe Mode.
- First, add your fallback access in
GeoIP-TRUSTED. - Import the list and check its number of entries before adding a drop rule.
- Test from an external connection located in the allowed zone.
- Then test from another controlled exit, for example a mobile access or a lab VPN.
- Check the counters without enabling per-packet logging.
- Exit Safe Mode only when the service and the fallback path are working.
/ip firewall filter print stats where comment~"GeoIP|zone"
/ip firewall address-list print count-only where list="GeoIP-BE"
/system resource print
/log print where message~"GeoIP"
Limitations and common mistakes
- Confusing allocation with location : a prefix registered in Belgium can be announced or used elsewhere.
- Allowing a zone as proof of identity : a relay located in the zone passes the same filter as a legitimate user.
- Deleting the active list before download : a failure turns a restrictive filter into a complete lockout.
- Downloading over HTTP : a security list must be transported over HTTPS from a controlled origin.
- Importing without
dry-run: a syntax error can leave a partial update. - Placing the rule in
inputfor an internal server : published traffic traversesforward. - Forgetting IPv6 : an IPv4 rule does not protect a service also accessible in IPv6.
- Loading too many zones : large lists slow down the import and consume router resources.
- Logging every drop : use rule counters to avoid log overload.
For IPv6, generate a separate list from ipv6-aggregated.txt, import it into /ipv6 firewall address-list, and create the corresponding rules in /ipv6 firewall filter. Do not let IPv6 become an unintended bypass route.
Conclusion
Reliable GeoIP filtering on MikroTik relies less on the quantity of blocked countries than on the quality of the update chain. The data must be retrieved over HTTPS, validated as CIDR networks, published atomically, imported as a dry run, and loaded into a temporary list before any switchover.
Once this solid foundation is in place, the address-list becomes a simple firewall criterion : input for a router service, forward for a published server. The measure reduces noise and exposure, but intentionally maintains a secondary role behind the VPN, authentication, updates, and a minimal firewall policy.
Discussion
Comments
No comment yet. Feel free to open the discussion with a question or your own experience.