fuck scammers, abusers and bad actors!
Some checks failed
Ansible Lint Check / check-ansible (push) Failing after 42s
Nix Format Check / check-format (push) Failing after 37s
Python Lint Check / check-python (push) Has been cancelled

This commit is contained in:
2025-06-15 01:33:04 +00:00
parent 3774ea6233
commit 247aa2d733
14 changed files with 275 additions and 852 deletions

View File

@@ -20,6 +20,10 @@ def help_message():
"green",
" --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)",
)
printfe(
"green",
" --tags TAG Run only specific Ansible tags (e.g., --tags caddy).",
)
printfe(
"green",
" --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)",
@@ -222,6 +226,9 @@ def main():
action="store_true",
help="Upgrade Ansible packages with verbose output",
)
parser.add_argument(
"--tags", type=str, help="Run only specific Ansible tags"
)
parser.add_argument(
"--full-speed", "-F", action="store_true", help="Use all available cores"
)
@@ -338,12 +345,19 @@ def main():
return 1
printfe("cyan", "Running Ansible playbook...")
# Determine which playbook to use based on tags
if args.tags and any(tag.strip() in ['caddy', 'country-blocking', 'caddyfile', 'config'] for tag in args.tags.split(',')):
playbook_path = f"{dotfiles_path}/config/ansible/caddy-playbook.yml"
printfe("cyan", f"Using dedicated Caddy playbook for tags: {args.tags}")
else:
playbook_path = f"{dotfiles_path}/config/ansible/playbook.yml"
ansible_cmd = [
"/usr/bin/env",
"ansible-playbook",
"-i",
f"{dotfiles_path}/config/ansible/inventory.ini",
f"{dotfiles_path}/config/ansible/playbook.yml",
playbook_path,
"--extra-vars",
f"hostname={hostname}",
"--extra-vars",
@@ -353,9 +367,15 @@ def main():
"--ask-become-pass",
]
if args.tags:
ansible_cmd.extend(["--tags", args.tags])
if args.ansible_verbose:
ansible_cmd.append("-vvv")
# Debug: Show the command being executed
printfe("yellow", f"Debug: Executing command: {' '.join(ansible_cmd)}")
result = subprocess.run(ansible_cmd)
if result.returncode != 0:
printfe("red", "Failed to upgrade Ansible packages.")

View File

@@ -0,0 +1,104 @@
---
- name: Configure Caddy service
hosts: all
handlers:
- name: Import handler tasks
ansible.builtin.import_tasks: handlers/main.yml
gather_facts: true
tasks:
- name: Set Caddy directories (basic)
ansible.builtin.set_fact:
caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy"
caddy_data_dir: "/mnt/object_storage/services/caddy"
tags:
- caddy
- setup
- country-blocking
- always
- name: Get Caddy email from 1Password
ansible.builtin.set_fact:
caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}"
ignore_errors: true
tags:
- caddy
- config
- caddyfile
- country-blocking
- name: Set fallback email if 1Password failed
ansible.builtin.set_fact:
caddy_email: "admin@example.com"
when: caddy_email is not defined
tags:
- caddy
- config
- caddyfile
- country-blocking
- name: Setup country blocking
ansible.builtin.include_tasks: tasks/servers/services/caddy/country-blocking.yml
tags:
- caddy
- country-blocking
- security
- name: Create Caddy directory
ansible.builtin.file:
path: "{{ caddy_service_dir }}"
state: directory
mode: "0755"
tags:
- caddy
- setup
- name: Create Caddy network
ansible.builtin.command: docker network create caddy_default
register: create_caddy_network
failed_when:
- create_caddy_network.rc != 0
- "'already exists' not in create_caddy_network.stderr"
changed_when: create_caddy_network.rc == 0
tags:
- caddy
- docker
- network
- name: Deploy Caddy docker-compose.yml
ansible.builtin.template:
src: tasks/servers/services/caddy/docker-compose.yml.j2
dest: "{{ caddy_service_dir }}/docker-compose.yml"
mode: "0644"
register: caddy_compose
tags:
- caddy
- docker
- config
- name: Deploy Caddy Caddyfile
ansible.builtin.template:
src: tasks/servers/services/caddy/Caddyfile.j2
dest: "{{ caddy_service_dir }}/Caddyfile"
mode: "0644"
register: caddy_file
tags:
- caddy
- config
- caddyfile
- name: Stop Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans
when: caddy_compose.changed or caddy_file.changed
tags:
- caddy
- docker
- service
- name: Start Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d
when: caddy_compose.changed or caddy_file.changed
tags:
- caddy
- docker
- service

View File

@@ -1,220 +0,0 @@
#!/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()

View File

@@ -1,251 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify country blocking functionality in Caddy.
This script tests access to your services from different country IP ranges.
"""
import requests
import sys
import json
import argparse
from typing import List, Dict, Tuple
import time
import random
# Sample IP addresses from blocked countries for testing
TEST_IPS = {
'CN': ['1.2.4.8', '14.17.22.35', '27.115.124.56', '36.248.15.72'],
'RU': ['2.56.96.15', '5.8.15.32', '31.31.192.45', '77.88.8.8'],
'IN': ['1.22.15.45', '14.96.32.78', '27.34.56.123', '117.192.45.89'],
'KP': ['175.45.176.10', '210.52.109.15'],
'IR': ['2.176.15.45', '5.22.78.123', '37.156.32.89'],
'VN': ['14.160.32.45', '27.64.78.123', '113.160.45.89'],
'BR': ['177.15.45.89', '179.32.78.123', '186.192.45.89'],
'TR': ['31.145.15.45', '78.160.32.89', '94.54.78.123'],
'ID': ['36.64.15.45', '114.4.32.89', '182.253.78.123'],
'TH': ['27.130.15.45', '110.164.32.89', '202.28.78.123'],
'BD': ['27.147.15.45', '114.130.32.89', '202.40.78.123'],
'PK': ['39.32.15.45', '115.42.32.89', '202.47.123.45'],
'RO': ['31.13.224.45', '89.136.32.89', '212.146.78.123'],
'BY': ['31.130.176.45', '85.113.32.89', '212.98.160.123']
}
# Sample IPs from allowed countries for comparison
ALLOWED_IPS = {
'US': ['8.8.8.8', '1.1.1.1', '208.67.222.222'],
'DE': ['85.25.12.34', '193.99.144.85'],
'NL': ['145.100.108.155', '194.109.6.66'],
'GB': ['212.58.244.20', '151.101.65.140']
}
def test_ip_blocking(domain: str, test_ip: str, timeout: int = 10) -> Tuple[int, str, float]:
"""
Test if an IP is blocked by making a request with X-Forwarded-For header.
Args:
domain: Domain to test against
test_ip: IP address to simulate request from
timeout: Request timeout in seconds
Returns:
Tuple of (status_code, response_text, response_time)
"""
headers = {
'X-Forwarded-For': test_ip,
'X-Real-IP': test_ip,
'User-Agent': 'Country-Blocking-Test/1.0'
}
try:
start_time = time.time()
response = requests.get(f'https://{domain}',
headers=headers,
timeout=timeout,
allow_redirects=False)
response_time = time.time() - start_time
return response.status_code, response.text[:200], response_time
except requests.exceptions.Timeout:
return -1, 'TIMEOUT', timeout
except requests.exceptions.RequestException as e:
return -2, f'ERROR: {str(e)}', 0.0
def run_blocking_tests(domains: List[str], countries: List[str] = None) -> Dict:
"""
Run comprehensive blocking tests across multiple domains and countries.
Args:
domains: List of domains to test
countries: List of country codes to test (defaults to all blocked countries)
Returns:
Dictionary with test results
"""
if countries is None:
countries = list(TEST_IPS.keys())
results = {
'blocked_tests': {},
'allowed_tests': {},
'summary': {
'total_tests': 0,
'blocked_correctly': 0,
'allowed_correctly': 0,
'errors': 0
}
}
print("🚀 Starting country blocking tests...\n")
# Test blocked countries
for country in countries:
if country not in TEST_IPS:
print(f"⚠️ No test IPs available for {country}")
continue
results['blocked_tests'][country] = {}
test_ips = random.sample(TEST_IPS[country], min(2, len(TEST_IPS[country])))
for domain in domains:
print(f"🔒 Testing {country} -> {domain}")
results['blocked_tests'][country][domain] = []
for ip in test_ips:
status, response, resp_time = test_ip_blocking(domain, ip)
result = {
'ip': ip,
'status_code': status,
'response': response,
'response_time': resp_time,
'blocked_correctly': status == 403
}
results['blocked_tests'][country][domain].append(result)
results['summary']['total_tests'] += 1
if result['blocked_correctly']:
results['summary']['blocked_correctly'] += 1
print(f"{ip} -> {status} (blocked)")
elif status > 0:
print(f"{ip} -> {status} (should be blocked!)")
else:
results['summary']['errors'] += 1
print(f" ⚠️ {ip} -> ERROR: {response}")
time.sleep(0.5) # Rate limiting
# Test allowed countries for comparison
print(f"\n🌍 Testing allowed countries for comparison...\n")
for country, ips in ALLOWED_IPS.items():
results['allowed_tests'][country] = {}
test_ip = random.choice(ips)
for domain in domains:
print(f"🌐 Testing {country} -> {domain}")
status, response, resp_time = test_ip_blocking(domain, test_ip)
result = {
'ip': test_ip,
'status_code': status,
'response': response,
'response_time': resp_time,
'allowed_correctly': status not in [403, -1, -2]
}
results['allowed_tests'][country][domain] = result
results['summary']['total_tests'] += 1
if result['allowed_correctly']:
results['summary']['allowed_correctly'] += 1
print(f"{test_ip} -> {status} (allowed)")
elif status == 403:
print(f"{test_ip} -> {status} (should be allowed!)")
else:
results['summary']['errors'] += 1
print(f" ⚠️ {test_ip} -> ERROR: {response}")
time.sleep(0.5)
return results
def print_summary(results: Dict):
"""Print a summary of test results."""
summary = results['summary']
total = summary['total_tests']
print(f"\n📊 Test Summary")
print(f"{'='*50}")
print(f"Total tests run: {total}")
print(f"Blocked correctly: {summary['blocked_correctly']}")
print(f"Allowed correctly: {summary['allowed_correctly']}")
print(f"Errors: {summary['errors']}")
if total > 0:
success_rate = ((summary['blocked_correctly'] + summary['allowed_correctly']) / total) * 100
print(f"Success rate: {success_rate:.1f}%")
if success_rate >= 95:
print("🎉 Country blocking is working excellently!")
elif success_rate >= 85:
print("✅ Country blocking is working well")
elif success_rate >= 70:
print("⚠️ Country blocking needs attention")
else:
print("❌ Country blocking has serious issues")
def main():
parser = argparse.ArgumentParser(description='Test country blocking functionality')
parser.add_argument('domains', nargs='+', help='Domains to test (e.g., photos.mvl.sh git.mvl.sh)')
parser.add_argument('--countries', nargs='*',
help='Specific countries to test (default: all blocked countries)')
parser.add_argument('--output', '-o', help='Save results to JSON file')
parser.add_argument('--verbose', '-v', action='store_true',
help='Show detailed output')
args = parser.parse_args()
print("🛡️ Country Blocking Test Suite")
print(f"Testing domains: {', '.join(args.domains)}")
if args.countries:
countries = [c.upper() for c in args.countries]
print(f"Testing countries: {', '.join(countries)}")
else:
countries = None
print("Testing all blocked countries")
print()
# Run tests
results = run_blocking_tests(args.domains, countries)
# Print summary
print_summary(results)
# Save detailed results if requested
if args.output:
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
print(f"\n💾 Detailed results saved to {args.output}")
# Exit with appropriate code
if results['summary']['errors'] > 0:
sys.exit(2)
elif results['summary']['total_tests'] == 0:
sys.exit(1)
else:
success_rate = ((results['summary']['blocked_correctly'] +
results['summary']['allowed_correctly']) /
results['summary']['total_tests']) * 100
sys.exit(0 if success_rate >= 85 else 1)
if __name__ == '__main__':
main()

View File

@@ -2,27 +2,22 @@
flatpaks: false
install_ui_apps: false
# Country blocking configuration for Caddy
# List of countries to block by ISO 3166-1 alpha-2 country codes
# Includes user-specified countries and top sources of malicious IP traffic
blocked_countries_codes:
# User-specified countries
- CN # China
- RU # Russia
- IN # India
- KP # North Korea
# Top countries for malicious IP traffic and abuse
- IR # Iran
- VN # Vietnam
- BR # Brazil
- TR # Turkey
- ID # Indonesia
- TH # Thailand
- BD # Bangladesh
- PK # Pakistan
- RO # Romania
- BY # Belarus
# Countries that are allowed to access the server Caddy reverse proxy
allowed_countries_codes:
- US # United States
- CA # Canada
- GB # United Kingdom
- DE # Germany
- FR # France
- ES # Spain
- IT # Italy
- NL # Netherlands
- AU # Australia
- NZ # New Zealand
- JP # Japan
- KR # South Korea
- SK # Slovakia
- FL # Finland
# IP ranges for blocked countries (generated automatically)
# This will be populated by the country blocking script

View File

@@ -1,19 +1,22 @@
# Global configuration for country blocking
{
# Block specific countries (add ISO country codes as needed)
# Examples: CN (China), RU (Russia), KP (North Korea), IR (Iran)
servers {
protocols h1 h2 h3
}
}
# Country blocking snippet - reusable across all sites
{% if enable_country_blocking | default(false) and blocked_countries | default([]) | length > 0 %}
# Country blocking snippet using MaxMind GeoLocation - reusable across all sites
{% if enable_country_blocking | default(false) and allowed_countries_codes | default([]) | length > 0 %}
(country_block) {
@blocked_countries {
remote_ip {{ blocked_countries | join(' ') }}
@not_allowed_countries {
not {
maxmind_geolocation {
db_path "/etc/caddy/geoip/GeoLite2-Country.mmdb"
allow_countries {{ allowed_countries_codes | join(' ') }}
}
}
}
respond @blocked_countries "Access denied from your country" 403
respond @not_allowed_countries "Access denied" 403
}
{% else %}
(country_block) {
@@ -116,7 +119,6 @@ ip.mvl.sh {
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
}
tls {{ caddy_email }}
}

View File

@@ -0,0 +1,15 @@
FROM caddy:2.9.1-builder AS builder
RUN xcaddy build \
--with github.com/porech/caddy-maxmind-geolocation
FROM caddy:2.9.1-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
# Create directory for MaxMind databases and logs
RUN mkdir -p /etc/caddy/geoip /var/log/caddy
EXPOSE 80 443
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

View File

@@ -1,261 +0,0 @@
# Caddy Country Blocking Configuration
This directory contains configuration for implementing country-based IP blocking in Caddy reverse proxy.
## Overview
The country blocking feature allows you to block traffic from specific countries by their IP address ranges. This is useful for:
- Reducing spam and malicious traffic
- Complying with regional restrictions
- Improving security posture
- Reducing server load from unwanted traffic
## How It Works
1. **IP Range Generation**: The `generate_country_blocks.py` script downloads current IP ranges for specified countries
2. **Caddy Configuration**: The Caddyfile template includes a reusable snippet that blocks IPs from those ranges
3. **Automatic Updates**: Ansible task generates fresh IP ranges on each deployment
## Configuration
### Enable/Disable Country Blocking
In `group_vars/servers.yml`:
```yaml
# Enable or disable country blocking
enable_country_blocking: true # Set to false to disable
```
### Specify Countries to Block
Add country codes (ISO 3166-1 alpha-2) to the `blocked_countries_codes` list:
```yaml
blocked_countries_codes:
# User-specified high-risk countries
- CN # China
- RU # Russia
- IN # India
- KP # North Korea
# Top countries for malicious IP traffic and abuse
- IR # Iran
- VN # Vietnam
- BR # Brazil
- TR # Turkey
- ID # Indonesia
- TH # Thailand
- BD # Bangladesh
- PK # Pakistan
- RO # Romania
- BY # Belarus
```
### Currently Blocked Countries
The default configuration blocks these countries based on high levels of malicious traffic:
| Country | Code | Reason |
|---------|------|--------|
| China | CN | High volume of attacks, state-sponsored threats |
| Russia | RU | Cybercrime hub, state-sponsored threats |
| India | IN | Large botnet presence, spam sources |
| North Korea | KP | State-sponsored attacks |
| Iran | IR | State-sponsored threats |
| Vietnam | VN | High malware hosting, botnet activity |
| Brazil | BR | Large botnet networks |
| Turkey | TR | Hosting malicious infrastructure |
| Indonesia | ID | Compromised hosts, botnet activity |
| Thailand | TH | Hosting malicious services |
| Bangladesh | BD | Compromised infrastructure |
| Pakistan | PK | Botnet activity, compromised hosts |
| Romania | RO | Cybercrime activity |
| Belarus | BY | State-aligned threats |
### Additional Country Codes Reference
| Country | Code | | Country | Code |
|---------|------|-|---------|------|
| Syria | SY | | Myanmar | MM |
| Afghanistan | AF | | Cuba | CU |
| Venezuela | VE | | Ukraine | UA |
| Philippines | PH | | Nigeria | NG |
## Files Structure
```
caddy/
├── Caddyfile.j2 # Main Caddy configuration with country blocking
├── caddy.yml # Ansible task for Caddy deployment
├── country-blocking.yml # Ansible task for country blocking setup
├── docker-compose.yml.j2 # Docker Compose configuration
├── generate_country_blocks.py # Script to generate IP ranges
└── README-country-blocking.md # This documentation
```
## Manual Usage
### Generate IP Ranges Manually
```bash
# Generate ranges for specific countries
python3 generate_country_blocks.py CN RU --format=list
# Output to file
python3 generate_country_blocks.py CN RU KP --output=blocked_ranges.txt
# Use fallback/manual ranges if download fails
python3 generate_country_blocks.py CN RU --fallback
# JSON format for inspection
python3 generate_country_blocks.py CN RU --format=json
```
### Test Country Blocking
After deployment, test blocking by checking access from different locations:
```bash
# Test from a VPN/proxy in a blocked country
curl -I https://your-domain.com
# Should return HTTP 403 with "Access denied from your country"
```
## Deployment
The country blocking is automatically configured when you run the Caddy Ansible task:
```bash
ansible-playbook -i inventory.ini playbook.yml --tags caddy
```
Or specifically for country blocking:
```bash
ansible-playbook -i inventory.ini playbook.yml --tags country-blocking
```
## Troubleshooting
### No IP Ranges Generated
If the script fails to download ranges:
1. Check internet connectivity on the Ansible control machine
2. Verify the country codes are valid (2-letter ISO codes)
3. Use fallback ranges: set `--fallback` flag in the script
4. Check the script output for error messages
### Legitimate Traffic Blocked
If legitimate users are blocked:
1. Verify their country isn't in your blocked list
2. Check if they're using a VPN/proxy from a blocked country
3. Consider whitelisting specific IP ranges for known users
4. Review your country blocking list for overly broad restrictions
### Service Not Starting
If Caddy fails to start after enabling country blocking:
1. Check the generated Caddyfile syntax: `caddy validate --config /path/to/Caddyfile`
2. Verify the IP ranges are valid CIDR notation
3. Check Docker logs: `docker compose logs caddy`
## Security Considerations
### Benefits
- **Reduced Attack Surface**: Blocks traffic from high-risk regions
- **Lower Server Load**: Reduces processing of malicious requests (up to 70% reduction)
- **Compliance**: Helps meet regional access restrictions
- **Proactive Defense**: Stops attacks before they reach your applications
- **Cost Savings**: Reduces bandwidth and compute costs from malicious traffic
### Limitations
- **VPN Bypass**: Users can circumvent blocking using VPNs/proxies
- **Legitimate Users**: May block legitimate traffic from blocked countries
- **Maintenance**: IP ranges change over time and need updates
- **False Positives**: Geolocation isn't 100% accurate
- **Business Impact**: May affect legitimate business relationships
- **Overblocking**: Current configuration blocks ~40% of global IP space
### Risk Assessment
The current blocking list includes countries that generate disproportionate amounts of:
- Botnet traffic (85% from blocked countries)
- Brute force attacks (78% from blocked countries)
- Malware hosting (72% from blocked countries)
- Spam campaigns (81% from blocked countries)
### Best Practices
1. **Whitelist Critical Services**: Don't block access to monitoring/health endpoints
2. **Regular Updates**: Update IP ranges regularly (automated with Ansible)
3. **Monitor Logs**: Watch for legitimate users being blocked
4. **Gradual Implementation**: Current config is aggressive - monitor impact
5. **Document Decisions**: Keep records of why specific countries are blocked
6. **Business Review**: Ensure blocking doesn't conflict with business needs
7. **Whitelist Partners**: Add specific IP ranges for known business partners
8. **Regular Assessment**: Review blocked country list quarterly
## Advanced Configuration
### Custom Block Message
Modify the response in `Caddyfile.j2`:
```caddy
respond @blocked_countries "Custom block message" 403
```
### Whitelist Specific IPs
Add to the country_block snippet:
```caddy
(country_block) {
@blocked_countries {
remote_ip {{ blocked_countries | join(' ') }}
not remote_ip 1.2.3.4 5.6.7.8/24 # Whitelist specific IPs
}
respond @blocked_countries "Access denied from your country" 403
}
```
### Log Blocked Requests
Add logging to track blocked requests:
```caddy
(country_block) {
@blocked_countries {
remote_ip {{ blocked_countries | join(' ') }}
}
log @blocked_countries {
output file /var/log/caddy/blocked_countries.log
format json
}
respond @blocked_countries "Access denied from your country" 403
}
```
## Data Sources
The script uses multiple data sources for IP ranges:
1. **Primary**: ipinfo.io free API
2. **Fallback**: Manually curated ranges for common countries
3. **Alternative**: Can be extended to use other sources like MaxMind, IP2Location
## License and Legal
- Ensure compliance with local laws regarding geographic blocking
- Some regions have regulations about access restrictions
- Consider accessibility requirements for your services
- Review terms of service for IP geolocation data providers

View File

@@ -1,46 +1,58 @@
---
- name: Deploy Caddy service
block:
- name: Setup country blocking
ansible.builtin.include_tasks: country-blocking.yml
- name: Set Caddy directories
ansible.builtin.set_fact:
caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy"
caddy_data_dir: "/mnt/object_storage/services/caddy"
caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}"
- name: Set Caddy directories
ansible.builtin.set_fact:
caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy"
caddy_data_dir: "/mnt/object_storage/services/caddy"
caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}"
- name: Create Caddy directory
ansible.builtin.file:
path: "{{ caddy_service_dir }}"
state: directory
mode: "0755"
- name: Setup country blocking
ansible.builtin.include_tasks: country-blocking.yml
- name: Create Caddy network
ansible.builtin.command: docker network create caddy_default
register: create_caddy_network
failed_when:
- create_caddy_network.rc != 0
- "'already exists' not in create_caddy_network.stderr"
changed_when: create_caddy_network.rc == 0
- name: Create Caddy directory
ansible.builtin.file:
path: "{{ caddy_service_dir }}"
state: directory
mode: "0755"
- name: Deploy Caddy docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ caddy_service_dir }}/docker-compose.yml"
mode: "0644"
register: caddy_compose
- name: Copy Dockerfile for custom Caddy build
ansible.builtin.copy:
src: Dockerfile
dest: "{{ caddy_service_dir }}/Dockerfile"
mode: "0644"
register: caddy_dockerfile
- name: Deploy Caddy Caddyfile
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ caddy_service_dir }}/Caddyfile"
mode: "0644"
register: caddy_file
- name: Create Caddy network
ansible.builtin.command: docker network create caddy_default
register: create_caddy_network
failed_when:
- create_caddy_network.rc != 0
- "'already exists' not in create_caddy_network.stderr"
changed_when: create_caddy_network.rc == 0
- name: Stop Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans
when: caddy_compose.changed or caddy_file.changed
- name: Deploy Caddy docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ caddy_service_dir }}/docker-compose.yml"
mode: "0644"
register: caddy_compose
- name: Start Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d
when: caddy_compose.changed or caddy_file.changed
- name: Deploy Caddy Caddyfile
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ caddy_service_dir }}/Caddyfile"
mode: "0644"
register: caddy_file
- name: Stop Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans
when: caddy_compose.changed or caddy_file.changed
- name: Start Caddy service
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d
when: caddy_compose.changed or caddy_file.changed
tags:
- caddy
- services
- reverse-proxy

View File

@@ -1,53 +1,50 @@
---
- name: Country blocking setup for Caddy
- name: Country blocking setup for Caddy with MaxMind GeoLocation
block:
- name: Ensure Python requests module is installed
ansible.builtin.apt:
name: python3-requests
state: present
update_cache: yes
when: enable_country_blocking | default(false)
- name: Copy Dockerfile for custom Caddy build with GeoIP
ansible.builtin.copy:
src: Dockerfile
dest: "{{ caddy_service_dir }}/Dockerfile"
mode: "0644"
when: enable_country_blocking | default(false)
- name: Copy country blocking script
ansible.builtin.copy:
src: generate_country_blocks.py
dest: "{{ caddy_service_dir }}/generate_country_blocks.py"
mode: "0755"
when: enable_country_blocking | default(false)
- name: Check if MaxMind Country database is available
ansible.builtin.stat:
path: "/mnt/object_storage/services/echoip/GeoLite2-Country.mmdb"
register: maxmind_country_db
when: enable_country_blocking | default(false)
- name: Generate country IP ranges
ansible.builtin.command:
cmd: "python3 {{ caddy_service_dir }}/generate_country_blocks.py {{ blocked_countries_codes | join(' ') }} --format=list"
register: country_ranges_result
when:
- enable_country_blocking | default(false)
- blocked_countries_codes | default([]) | length > 0
changed_when: false
- name: Ensure log directory exists for Caddy
ansible.builtin.file:
path: "{{ caddy_data_dir }}/logs"
state: directory
mode: "0755"
become: true
when: enable_country_blocking | default(false)
- name: Set country IP ranges fact
ansible.builtin.set_fact:
blocked_countries: "{{ country_ranges_result.stdout.split('\n') | select('match', '^[0-9]') | list }}"
when:
- enable_country_blocking | default(false)
- country_ranges_result is defined
- country_ranges_result.stdout is defined
- name: Display country blocking configuration
ansible.builtin.debug:
msg:
- "✅ Country blocking enabled: {{ enable_country_blocking | default(false) }}"
- "🛡️ Countries to allow: {{ allowed_countries_codes | default([]) | join(', ') }}"
- "📍 Using MaxMind GeoLocation plugin"
- "💾 Database path: /etc/caddy/geoip/GeoLite2-Country.mmdb"
- "📊 Database available: {{ maxmind_country_db.stat.exists | default(false) }}"
when: enable_country_blocking | default(false)
- name: Display blocked countries info
ansible.builtin.debug:
msg:
- "Country blocking enabled: {{ enable_country_blocking | default(false) }}"
- "Countries to block: {{ blocked_countries_codes | default([]) | join(', ') }}"
- "IP ranges found: {{ blocked_countries | default([]) | length }}"
when: enable_country_blocking | default(false)
- name: Fallback to empty list if no ranges generated
ansible.builtin.set_fact:
blocked_countries: []
when:
- enable_country_blocking | default(false)
- blocked_countries is not defined
- name: Warn if MaxMind database not found
ansible.builtin.debug:
msg:
- "⚠️ WARNING: MaxMind Country database not found!"
- "Expected location: /mnt/object_storage/services/echoip/GeoLite2-Country.mmdb"
- "Country blocking will not work until EchoIP service is deployed"
- "Run: dotf update --ansible --tags echoip"
when:
- enable_country_blocking | default(false)
- not maxmind_country_db.stat.exists | default(false)
tags:
- caddy
- security
- country-blocking
- caddy
- security
- country-blocking
- geoip

View File

@@ -1,6 +1,8 @@
services:
caddy:
image: caddy:2.9.1-alpine
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "80:80"
@@ -9,6 +11,8 @@ services:
- {{ caddy_data_dir }}/data:/data
- {{ caddy_data_dir }}/config:/config
- {{ caddy_service_dir }}/Caddyfile:/etc/caddy/Caddyfile
- /mnt/object_storage/services/echoip:/etc/caddy/geoip:ro
- {{ caddy_data_dir }}/logs:/var/log/caddy
environment:
- TZ=Europe/Amsterdam
- PUID=1000

View File

@@ -7,4 +7,7 @@
loop: "{{ services }}"
when: item.enabled|bool
loop_control:
label: "{{ item.name }}"
label: "{{ item.name }}"
tags:
- "{{ item.name }}"
- services

View File

@@ -23,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1748810746,
"narHash": "sha256-1na8blYvU1F6HLwx/aFjrhUqpqZ0SCsnqqW9n2vXvok=",
"lastModified": 1749668643,
"narHash": "sha256-gaWJEWGBW/g1u6o5IM4Un0vluv86cigLuBnjsKILffc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "78d9f40fd6941a1543ffc3ed358e19c69961d3c1",
"rev": "1965fd20a39c8e441746bee66d550af78f0c0a7b",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1748693115,
"narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=",
"lastModified": 1749794982,
"narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc",
"rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81",
"type": "github"
},
"original": {

View File

@@ -6,3 +6,6 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMSJwfqOZQxGDbM07JziQeBNirvQxhFd6nEwWPjy1zCo
# Menno's 2025 SSH Key
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE22Hfx8wgkc57TXX1TCMHcNrCdjbfog5QeHFJfl7IeD menno_fallback
# Menno's Server (rsync mostly)
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwyFxs+Zva2W2Viu9dzznFR9CfsiRWM1gxxvD5FUhfb menno@mennos-server