- Add IN, VN, BR, TR, ID, TH, BD, PK, RO to blocked list - Update alternative IP ranges for new countries in script - Enhance documentation with rationale, risk assessment, and best practices - Add test script for verifying country blocking functionality - Improve Ansible tasks for dependency installation
221 lines
8.7 KiB
Python
221 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate country IP ranges for Caddy blocking configuration.
|
|
Downloads the latest country IP ranges and formats them for use in Caddyfile.
|
|
"""
|
|
|
|
import json
|
|
import requests
|
|
import sys
|
|
import argparse
|
|
from typing import List, Dict
|
|
import ipaddress
|
|
|
|
|
|
def download_country_ranges(countries: List[str]) -> Dict[str, List[str]]:
|
|
"""
|
|
Download IP ranges for specified countries from ip2location.com free database.
|
|
|
|
Args:
|
|
countries: List of ISO 3166-1 alpha-2 country codes (e.g., ['CN', 'RU'])
|
|
|
|
Returns:
|
|
Dictionary mapping country codes to lists of IP ranges
|
|
"""
|
|
country_ranges = {}
|
|
|
|
for country in countries:
|
|
print(f"Downloading IP ranges for {country}...", file=sys.stderr)
|
|
|
|
# Use ipinfo.io's free API for country IP ranges
|
|
try:
|
|
url = f"https://ipinfo.io/countries/{country.lower()}"
|
|
headers = {'User-Agent': 'Mozilla/5.0 (compatible; country-blocker/1.0)'}
|
|
response = requests.get(url, headers=headers, timeout=30)
|
|
|
|
if response.status_code == 200:
|
|
# The response contains IP ranges in CIDR format, one per line
|
|
ranges = []
|
|
for line in response.text.strip().split('\n'):
|
|
line = line.strip()
|
|
if line and not line.startswith('#'):
|
|
try:
|
|
# Validate the IP range
|
|
ipaddress.ip_network(line, strict=False)
|
|
ranges.append(line)
|
|
except ValueError:
|
|
continue
|
|
|
|
country_ranges[country] = ranges
|
|
print(f"Found {len(ranges)} IP ranges for {country}", file=sys.stderr)
|
|
else:
|
|
print(f"Failed to download ranges for {country}: HTTP {response.status_code}", file=sys.stderr)
|
|
|
|
except Exception as e:
|
|
print(f"Error downloading ranges for {country}: {e}", file=sys.stderr)
|
|
|
|
return country_ranges
|
|
|
|
|
|
def get_alternative_ranges(countries: List[str]) -> Dict[str, List[str]]:
|
|
"""
|
|
Alternative method using a different data source.
|
|
Falls back to manually curated ranges for common blocking scenarios.
|
|
"""
|
|
# Common IP ranges for frequently blocked countries
|
|
# These are examples - you should update with current ranges
|
|
manual_ranges = {
|
|
'CN': [
|
|
'1.0.1.0/24', '1.0.2.0/23', '1.0.8.0/21', '1.0.32.0/19',
|
|
'14.0.12.0/22', '14.0.36.0/22', '14.1.0.0/18', '14.16.0.0/12',
|
|
'27.0.0.0/11', '27.50.40.0/21', '27.54.192.0/18', '27.98.208.0/20',
|
|
'36.0.0.0/10', '39.0.0.0/11', '42.0.0.0/8', '58.0.0.0/9',
|
|
'59.32.0.0/11', '60.0.0.0/8', '61.0.0.0/10', '110.0.0.0/7',
|
|
'112.0.0.0/5', '120.0.0.0/6', '124.0.0.0/7', '180.76.0.0/16'
|
|
],
|
|
'RU': [
|
|
'2.56.96.0/19', '2.58.56.0/21', '5.8.0.0/19', '5.34.96.0/19',
|
|
'5.42.192.0/19', '5.44.64.0/18', '5.53.96.0/19', '5.61.24.0/21',
|
|
'31.31.192.0/19', '37.9.64.0/18', '37.18.0.0/16', '46.17.40.0/21',
|
|
'77.88.0.0/18', '78.24.216.0/21', '79.104.0.0/14', '80.66.176.0/20',
|
|
'81.68.0.0/14', '82.138.140.0/22', '84.201.128.0/18', '85.143.192.0/18',
|
|
'91.103.0.0/17', '91.208.165.0/24', '94.181.160.0/20', '95.165.0.0/16'
|
|
],
|
|
'IN': [
|
|
'1.22.0.0/15', '14.96.0.0/11', '27.34.0.0/15', '36.255.0.0/16',
|
|
'39.32.0.0/11', '49.248.0.0/13', '58.84.0.0/15', '59.144.0.0/12',
|
|
'103.21.58.0/24', '106.51.0.0/16', '110.224.0.0/11', '117.192.0.0/10',
|
|
'122.160.0.0/12', '125.16.0.0/12', '150.129.0.0/16', '157.32.0.0/11',
|
|
'163.47.0.0/16', '180.179.0.0/16', '182.64.0.0/10', '183.82.0.0/15'
|
|
],
|
|
'KP': [
|
|
'175.45.176.0/22', '210.52.109.0/24', '77.94.35.0/24'
|
|
],
|
|
'IR': [
|
|
'2.176.0.0/12', '5.22.0.0/16', '5.23.0.0/16', '5.56.128.0/17',
|
|
'5.144.128.0/17', '31.2.128.0/17', '37.156.0.0/14', '37.191.0.0/16',
|
|
'46.32.0.0/11', '78.39.192.0/18', '79.175.128.0/18', '80.191.0.0/16',
|
|
'81.91.128.0/17', '82.99.192.0/18', '85.15.0.0/16', '91.98.0.0/15'
|
|
],
|
|
'VN': [
|
|
'14.160.0.0/11', '27.64.0.0/10', '42.112.0.0/13', '45.117.0.0/16',
|
|
'103.21.148.0/22', '103.56.156.0/22', '113.160.0.0/11', '115.73.0.0/16',
|
|
'116.96.0.0/10', '118.69.0.0/16', '171.224.0.0/11', '203.113.128.0/18'
|
|
],
|
|
'BR': [
|
|
'138.0.0.0/8', '177.0.0.0/8', '179.0.0.0/8', '186.192.0.0/12',
|
|
'189.0.0.0/8', '191.0.0.0/8', '200.128.0.0/9', '201.0.0.0/8'
|
|
],
|
|
'TR': [
|
|
'31.145.0.0/16', '46.1.0.0/16', '78.160.0.0/11', '85.111.0.0/16',
|
|
'88.229.0.0/16', '94.54.0.0/15', '176.88.0.0/13', '185.2.0.0/16',
|
|
'188.119.0.0/16', '212.156.0.0/14'
|
|
],
|
|
'ID': [
|
|
'36.64.0.0/10', '43.218.0.0/15', '103.10.0.0/15', '103.56.0.0/14',
|
|
'114.4.0.0/14', '118.96.0.0/11', '125.160.0.0/11', '139.228.0.0/15',
|
|
'182.253.0.0/16', '202.43.0.0/16'
|
|
],
|
|
'TH': [
|
|
'27.130.0.0/15', '49.228.0.0/14', '58.8.0.0/14', '101.51.0.0/16',
|
|
'103.10.228.0/22', '110.164.0.0/14', '124.120.0.0/13', '171.96.0.0/13',
|
|
'182.232.0.0/14', '202.28.0.0/14'
|
|
],
|
|
'BD': [
|
|
'27.147.0.0/16', '43.245.8.0/22', '103.4.92.0/22', '103.15.200.0/22',
|
|
'114.130.0.0/16', '118.179.0.0/16', '119.40.64.0/18', '180.211.192.0/18',
|
|
'202.40.32.0/19', '203.83.160.0/19'
|
|
],
|
|
'PK': [
|
|
'39.32.0.0/11', '58.27.0.0/16', '103.11.60.0/22', '110.93.192.0/18',
|
|
'115.42.0.0/16', '119.63.128.0/20', '175.107.0.0/16', '182.176.0.0/12',
|
|
'202.47.96.0/19', '203.124.0.0/14'
|
|
],
|
|
'RO': [
|
|
'31.13.224.0/19', '37.233.0.0/16', '46.19.0.0/16', '79.114.0.0/15',
|
|
'86.35.0.0/16', '89.136.0.0/13', '94.177.0.0/16', '109.166.0.0/15',
|
|
'188.24.0.0/13', '212.146.0.0/15'
|
|
],
|
|
'BY': [
|
|
'31.130.176.0/20', '37.17.128.0/18', '46.16.104.0/21', '78.108.176.0/20',
|
|
'85.113.0.0/16', '86.57.0.0/17', '93.84.64.0/18', '178.120.0.0/13',
|
|
'178.172.160.0/19', '212.98.160.0/19'
|
|
]
|
|
}
|
|
|
|
result = {}
|
|
for country in countries:
|
|
if country in manual_ranges:
|
|
result[country] = manual_ranges[country]
|
|
print(f"Using manual ranges for {country}: {len(manual_ranges[country])} ranges", file=sys.stderr)
|
|
else:
|
|
print(f"No manual ranges available for {country}", file=sys.stderr)
|
|
result[country] = []
|
|
|
|
return result
|
|
|
|
|
|
def format_for_caddy(country_ranges: Dict[str, List[str]]) -> List[str]:
|
|
"""
|
|
Format IP ranges for use in Caddyfile remote_ip matcher.
|
|
|
|
Args:
|
|
country_ranges: Dictionary of country codes to IP ranges
|
|
|
|
Returns:
|
|
List of IP ranges formatted for Caddy
|
|
"""
|
|
all_ranges = []
|
|
for country, ranges in country_ranges.items():
|
|
all_ranges.extend(ranges)
|
|
|
|
return all_ranges
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate country IP ranges for blocking')
|
|
parser.add_argument('countries', nargs='+',
|
|
help='Country codes to block (e.g., CN RU KP IR)')
|
|
parser.add_argument('--format', choices=['caddy', 'json', 'list'],
|
|
default='caddy', help='Output format')
|
|
parser.add_argument('--fallback', action='store_true',
|
|
help='Use manual fallback ranges instead of downloading')
|
|
parser.add_argument('--output', '-o', help='Output file (default: stdout)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Normalize country codes to uppercase
|
|
countries = [c.upper() for c in args.countries]
|
|
|
|
# Download or use fallback ranges
|
|
if args.fallback:
|
|
country_ranges = get_alternative_ranges(countries)
|
|
else:
|
|
country_ranges = download_country_ranges(countries)
|
|
# If download failed, try fallback
|
|
if not any(country_ranges.values()):
|
|
print("Download failed, using fallback ranges...", file=sys.stderr)
|
|
country_ranges = get_alternative_ranges(countries)
|
|
|
|
# Format output
|
|
if args.format == 'caddy':
|
|
ranges = format_for_caddy(country_ranges)
|
|
output = ' '.join(ranges)
|
|
elif args.format == 'json':
|
|
output = json.dumps(country_ranges, indent=2)
|
|
else: # list format
|
|
ranges = format_for_caddy(country_ranges)
|
|
output = '\n'.join(ranges)
|
|
|
|
# Write output
|
|
if args.output:
|
|
with open(args.output, 'w') as f:
|
|
f.write(output)
|
|
print(f"Output written to {args.output}", file=sys.stderr)
|
|
else:
|
|
print(output)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|