When implementing country-level IP blocking, we need to consider three critical constraints:
- Shared hosting environment (no root access)
- Apache server with .htaccess capability
- PHP execution environment
For maximum efficiency, we'll implement a hybrid approach combining:
1. .htaccess for initial filtering
2. PHP fallback for dynamic verification
3. Local GeoIP database for reduced latency
First, create/modify your .htaccess file:
<IfModule mod_geoip.c>
GeoIPEnable On
GeoIPDBFile /path/to/GeoIP.dat
SetEnvIf GEOIP_COUNTRY_CODE COUNTRY_TO_BLOCK BlockCountry
Deny from env=BlockCountry
</IfModule>
For shared hosting without GeoIP module, use this alternative:
RewriteEngine on
RewriteCond %{ENV:GEOIP_COUNTRY_CODE} ^(CN|RU)$ [NC]
RewriteRule ^ - [F]
Create a geo_block.php file:
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
$blockedCountries = ['CN', 'RU', 'KP'];
$ip = $_SERVER['REMOTE_ADDR'];
try {
$reader = new Reader('/path/to/GeoLite2-Country.mmdb');
$record = $reader->country($ip);
if (in_array($record->country->isoCode, $blockedCountries)) {
header('HTTP/1.0 403 Forbidden');
exit('Access denied from your region');
}
} catch (Exception $e) {
// Log error but don't block
error_log("GeoIP error: " . $e->getMessage());
}
To reduce database lookups:
<?php
$ip = $_SERVER['REMOTE_ADDR'];
$cacheFile = sys_get_temp_dir() . '/geoip_cache.json';
// Simple caching mechanism
if (file_exists($cacheFile) &&
time()-filemtime($cacheFile) < 86400) {
$cache = json_decode(file_get_contents($cacheFile), true);
} else {
$cache = [];
}
if (!isset($cache[$ip])) {
// Perform GeoIP lookup and update cache
// ... (previous lookup code)
$cache[$ip] = $record->country->isoCode;
file_put_contents($cacheFile, json_encode($cache));
}
Service | Free Tier | Accuracy | Latency |
---|---|---|---|
MaxMind | Yes (GeoLite) | High | Low |
IP2Location | Limited | Medium | Medium |
Geoplugin | Yes | Medium | High |
For VPN/TOR detection:
<?php
// Check for common VPN exit nodes
$vpnRanges = [
'185.107.80.0/24',
'212.102.50.0/24'
// Add more known ranges
];
foreach ($vpnRanges as $range) {
if (ip_in_range($ip, $range)) {
header('HTTP/1.0 403 Forbidden');
exit('VPN/Proxy detected');
}
}
function ip_in_range($ip, $range) {
// Implementation of CIDR check
// ... (omitted for brevity)
}
When implementing country-level IP blocking, we face several technical constraints, especially in shared hosting environments. The primary challenge lies in efficiently matching incoming requests against country IP ranges without causing significant performance overhead.
The most reliable approach involves using a GeoIP database. For PHP implementations, consider these options:
// Using MaxMind GeoIP2 (recommended)
require 'vendor/autoload.php';
use GeoIp2\Database\Reader;
$reader = new Reader('/path/to/GeoLite2-Country.mmdb');
$record = $reader->country($_SERVER['REMOTE_ADDR']);
if ($record->country->isoCode === 'CN') { // Block China for example
header('HTTP/1.0 403 Forbidden');
exit;
}
For Apache servers, you can implement country blocking via .htaccess using mod_geoip (if available) or by generating IP range rules:
# Block by country code using mod_geoip
<IfModule mod_geoip.c>
GeoIPEnable On
GeoIPDBFile /path/to/GeoIP.dat
SetEnvIf GEOIP_COUNTRY_CODE CN BlockCountry
Deny from env=BlockCountry
</IfModule>
# Alternative: Pre-generated IP blocks
Deny from 1.0.1.0/24
Deny from 1.0.2.0/23
# Add more CIDR ranges for the target country
The most efficient method depends on your traffic volume:
- Low traffic: PHP-level checking is sufficient
- Medium traffic: .htaccess with pre-generated CIDR blocks
- High traffic: Server-level solutions like Cloudflare or Nginx geoip module
GeoIP databases require regular updates. Implement a cron job to fetch fresh data:
#!/bin/bash
wget -O /tmp/GeoLite2-Country.tar.gz "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE&suffix=tar.gz"
tar -xzf /tmp/GeoLite2-Country.tar.gz -C /tmp --strip-components=1
mv /tmp/GeoLite2-Country.mmdb /path/to/database.mmdb
For those without server access, consider these API-based solutions:
// Using ipapi.co
$response = file_get_contents("https://ipapi.co/{$_SERVER['REMOTE_ADDR']}/country/");
if (trim($response) === 'CN') {
die('Access denied');
}