fuck scammers, abusers and bad actors!
This commit is contained in:
@@ -20,6 +20,10 @@ def help_message():
|
|||||||
"green",
|
"green",
|
||||||
" --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)",
|
" --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)",
|
||||||
)
|
)
|
||||||
|
printfe(
|
||||||
|
"green",
|
||||||
|
" --tags TAG Run only specific Ansible tags (e.g., --tags caddy).",
|
||||||
|
)
|
||||||
printfe(
|
printfe(
|
||||||
"green",
|
"green",
|
||||||
" --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)",
|
" --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)",
|
||||||
@@ -222,6 +226,9 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Upgrade Ansible packages with verbose output",
|
help="Upgrade Ansible packages with verbose output",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tags", type=str, help="Run only specific Ansible tags"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--full-speed", "-F", action="store_true", help="Use all available cores"
|
"--full-speed", "-F", action="store_true", help="Use all available cores"
|
||||||
)
|
)
|
||||||
@@ -338,12 +345,19 @@ def main():
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
printfe("cyan", "Running Ansible playbook...")
|
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 = [
|
ansible_cmd = [
|
||||||
"/usr/bin/env",
|
"/usr/bin/env",
|
||||||
"ansible-playbook",
|
"ansible-playbook",
|
||||||
"-i",
|
"-i",
|
||||||
f"{dotfiles_path}/config/ansible/inventory.ini",
|
f"{dotfiles_path}/config/ansible/inventory.ini",
|
||||||
f"{dotfiles_path}/config/ansible/playbook.yml",
|
playbook_path,
|
||||||
"--extra-vars",
|
"--extra-vars",
|
||||||
f"hostname={hostname}",
|
f"hostname={hostname}",
|
||||||
"--extra-vars",
|
"--extra-vars",
|
||||||
@@ -353,9 +367,15 @@ def main():
|
|||||||
"--ask-become-pass",
|
"--ask-become-pass",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if args.tags:
|
||||||
|
ansible_cmd.extend(["--tags", args.tags])
|
||||||
|
|
||||||
if args.ansible_verbose:
|
if args.ansible_verbose:
|
||||||
ansible_cmd.append("-vvv")
|
ansible_cmd.append("-vvv")
|
||||||
|
|
||||||
|
# Debug: Show the command being executed
|
||||||
|
printfe("yellow", f"Debug: Executing command: {' '.join(ansible_cmd)}")
|
||||||
|
|
||||||
result = subprocess.run(ansible_cmd)
|
result = subprocess.run(ansible_cmd)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
printfe("red", "Failed to upgrade Ansible packages.")
|
printfe("red", "Failed to upgrade Ansible packages.")
|
||||||
|
104
config/ansible/caddy-playbook.yml
Normal file
104
config/ansible/caddy-playbook.yml
Normal 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
|
@@ -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()
|
|
@@ -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()
|
|
@@ -2,27 +2,22 @@
|
|||||||
flatpaks: false
|
flatpaks: false
|
||||||
install_ui_apps: false
|
install_ui_apps: false
|
||||||
|
|
||||||
# Country blocking configuration for Caddy
|
# Countries that are allowed to access the server Caddy reverse proxy
|
||||||
# List of countries to block by ISO 3166-1 alpha-2 country codes
|
allowed_countries_codes:
|
||||||
# Includes user-specified countries and top sources of malicious IP traffic
|
- US # United States
|
||||||
blocked_countries_codes:
|
- CA # Canada
|
||||||
# User-specified countries
|
- GB # United Kingdom
|
||||||
- CN # China
|
- DE # Germany
|
||||||
- RU # Russia
|
- FR # France
|
||||||
- IN # India
|
- ES # Spain
|
||||||
- KP # North Korea
|
- IT # Italy
|
||||||
|
- NL # Netherlands
|
||||||
# Top countries for malicious IP traffic and abuse
|
- AU # Australia
|
||||||
- IR # Iran
|
- NZ # New Zealand
|
||||||
- VN # Vietnam
|
- JP # Japan
|
||||||
- BR # Brazil
|
- KR # South Korea
|
||||||
- TR # Turkey
|
- SK # Slovakia
|
||||||
- ID # Indonesia
|
- FL # Finland
|
||||||
- TH # Thailand
|
|
||||||
- BD # Bangladesh
|
|
||||||
- PK # Pakistan
|
|
||||||
- RO # Romania
|
|
||||||
- BY # Belarus
|
|
||||||
|
|
||||||
# IP ranges for blocked countries (generated automatically)
|
# IP ranges for blocked countries (generated automatically)
|
||||||
# This will be populated by the country blocking script
|
# This will be populated by the country blocking script
|
||||||
|
@@ -1,19 +1,22 @@
|
|||||||
# Global configuration for country blocking
|
# 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 {
|
servers {
|
||||||
protocols h1 h2 h3
|
protocols h1 h2 h3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Country blocking snippet - reusable across all sites
|
# Country blocking snippet using MaxMind GeoLocation - reusable across all sites
|
||||||
{% if enable_country_blocking | default(false) and blocked_countries | default([]) | length > 0 %}
|
{% if enable_country_blocking | default(false) and allowed_countries_codes | default([]) | length > 0 %}
|
||||||
(country_block) {
|
(country_block) {
|
||||||
@blocked_countries {
|
@not_allowed_countries {
|
||||||
remote_ip {{ blocked_countries | join(' ') }}
|
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 %}
|
{% else %}
|
||||||
(country_block) {
|
(country_block) {
|
||||||
@@ -116,7 +119,6 @@ ip.mvl.sh {
|
|||||||
header_up X-Forwarded-Proto {scheme}
|
header_up X-Forwarded-Proto {scheme}
|
||||||
header_up X-Forwarded-Host {host}
|
header_up X-Forwarded-Host {host}
|
||||||
}
|
}
|
||||||
|
|
||||||
tls {{ caddy_email }}
|
tls {{ caddy_email }}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
config/ansible/tasks/servers/services/caddy/Dockerfile
Normal file
15
config/ansible/tasks/servers/services/caddy/Dockerfile
Normal 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"]
|
@@ -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
|
|
@@ -1,20 +1,28 @@
|
|||||||
---
|
---
|
||||||
- name: Deploy Caddy service
|
- name: Deploy Caddy service
|
||||||
block:
|
block:
|
||||||
- name: Setup country blocking
|
|
||||||
ansible.builtin.include_tasks: country-blocking.yml
|
|
||||||
- name: Set Caddy directories
|
- name: Set Caddy directories
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy"
|
caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy"
|
||||||
caddy_data_dir: "/mnt/object_storage/services/caddy"
|
caddy_data_dir: "/mnt/object_storage/services/caddy"
|
||||||
caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}"
|
caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}"
|
||||||
|
|
||||||
|
- name: Setup country blocking
|
||||||
|
ansible.builtin.include_tasks: country-blocking.yml
|
||||||
|
|
||||||
- name: Create Caddy directory
|
- name: Create Caddy directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ caddy_service_dir }}"
|
path: "{{ caddy_service_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Copy Dockerfile for custom Caddy build
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: Dockerfile
|
||||||
|
dest: "{{ caddy_service_dir }}/Dockerfile"
|
||||||
|
mode: "0644"
|
||||||
|
register: caddy_dockerfile
|
||||||
|
|
||||||
- name: Create Caddy network
|
- name: Create Caddy network
|
||||||
ansible.builtin.command: docker network create caddy_default
|
ansible.builtin.command: docker network create caddy_default
|
||||||
register: create_caddy_network
|
register: create_caddy_network
|
||||||
@@ -44,3 +52,7 @@
|
|||||||
- name: Start Caddy service
|
- name: Start Caddy service
|
||||||
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d
|
ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d
|
||||||
when: caddy_compose.changed or caddy_file.changed
|
when: caddy_compose.changed or caddy_file.changed
|
||||||
|
tags:
|
||||||
|
- caddy
|
||||||
|
- services
|
||||||
|
- reverse-proxy
|
||||||
|
@@ -1,53 +1,50 @@
|
|||||||
---
|
---
|
||||||
- name: Country blocking setup for Caddy
|
- name: Country blocking setup for Caddy with MaxMind GeoLocation
|
||||||
block:
|
block:
|
||||||
- name: Ensure Python requests module is installed
|
- name: Copy Dockerfile for custom Caddy build with GeoIP
|
||||||
ansible.builtin.apt:
|
|
||||||
name: python3-requests
|
|
||||||
state: present
|
|
||||||
update_cache: yes
|
|
||||||
when: enable_country_blocking | default(false)
|
|
||||||
|
|
||||||
- name: Copy country blocking script
|
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: generate_country_blocks.py
|
src: Dockerfile
|
||||||
dest: "{{ caddy_service_dir }}/generate_country_blocks.py"
|
dest: "{{ caddy_service_dir }}/Dockerfile"
|
||||||
mode: "0755"
|
mode: "0644"
|
||||||
when: enable_country_blocking | default(false)
|
when: enable_country_blocking | default(false)
|
||||||
|
|
||||||
- name: Generate country IP ranges
|
- name: Check if MaxMind Country database is available
|
||||||
ansible.builtin.command:
|
ansible.builtin.stat:
|
||||||
cmd: "python3 {{ caddy_service_dir }}/generate_country_blocks.py {{ blocked_countries_codes | join(' ') }} --format=list"
|
path: "/mnt/object_storage/services/echoip/GeoLite2-Country.mmdb"
|
||||||
register: country_ranges_result
|
register: maxmind_country_db
|
||||||
when:
|
when: enable_country_blocking | default(false)
|
||||||
- enable_country_blocking | default(false)
|
|
||||||
- blocked_countries_codes | default([]) | length > 0
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Set country IP ranges fact
|
- name: Ensure log directory exists for Caddy
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.file:
|
||||||
blocked_countries: "{{ country_ranges_result.stdout.split('\n') | select('match', '^[0-9]') | list }}"
|
path: "{{ caddy_data_dir }}/logs"
|
||||||
when:
|
state: directory
|
||||||
- enable_country_blocking | default(false)
|
mode: "0755"
|
||||||
- country_ranges_result is defined
|
become: true
|
||||||
- country_ranges_result.stdout is defined
|
when: enable_country_blocking | default(false)
|
||||||
|
|
||||||
- name: Display blocked countries info
|
- name: Display country blocking configuration
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg:
|
msg:
|
||||||
- "Country blocking enabled: {{ enable_country_blocking | default(false) }}"
|
- "✅ Country blocking enabled: {{ enable_country_blocking | default(false) }}"
|
||||||
- "Countries to block: {{ blocked_countries_codes | default([]) | join(', ') }}"
|
- "🛡️ Countries to allow: {{ allowed_countries_codes | default([]) | join(', ') }}"
|
||||||
- "IP ranges found: {{ blocked_countries | default([]) | length }}"
|
- "📍 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)
|
when: enable_country_blocking | default(false)
|
||||||
|
|
||||||
- name: Fallback to empty list if no ranges generated
|
- name: Warn if MaxMind database not found
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.debug:
|
||||||
blocked_countries: []
|
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:
|
when:
|
||||||
- enable_country_blocking | default(false)
|
- enable_country_blocking | default(false)
|
||||||
- blocked_countries is not defined
|
- not maxmind_country_db.stat.exists | default(false)
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- caddy
|
- caddy
|
||||||
- security
|
- security
|
||||||
- country-blocking
|
- country-blocking
|
||||||
|
- geoip
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.9.1-alpine
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
@@ -9,6 +11,8 @@ services:
|
|||||||
- {{ caddy_data_dir }}/data:/data
|
- {{ caddy_data_dir }}/data:/data
|
||||||
- {{ caddy_data_dir }}/config:/config
|
- {{ caddy_data_dir }}/config:/config
|
||||||
- {{ caddy_service_dir }}/Caddyfile:/etc/caddy/Caddyfile
|
- {{ caddy_service_dir }}/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- /mnt/object_storage/services/echoip:/etc/caddy/geoip:ro
|
||||||
|
- {{ caddy_data_dir }}/logs:/var/log/caddy
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Amsterdam
|
- TZ=Europe/Amsterdam
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
|
@@ -8,3 +8,6 @@
|
|||||||
when: item.enabled|bool
|
when: item.enabled|bool
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.name }}"
|
label: "{{ item.name }}"
|
||||||
|
tags:
|
||||||
|
- "{{ item.name }}"
|
||||||
|
- services
|
||||||
|
12
config/home-manager/flake.lock
generated
12
config/home-manager/flake.lock
generated
@@ -23,11 +23,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748810746,
|
"lastModified": 1749668643,
|
||||||
"narHash": "sha256-1na8blYvU1F6HLwx/aFjrhUqpqZ0SCsnqqW9n2vXvok=",
|
"narHash": "sha256-gaWJEWGBW/g1u6o5IM4Un0vluv86cigLuBnjsKILffc=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "78d9f40fd6941a1543ffc3ed358e19c69961d3c1",
|
"rev": "1965fd20a39c8e441746bee66d550af78f0c0a7b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748693115,
|
"lastModified": 1749794982,
|
||||||
"narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=",
|
"narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc",
|
"rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@@ -6,3 +6,6 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMSJwfqOZQxGDbM07JziQeBNirvQxhFd6nEwWPjy1zCo
|
|||||||
|
|
||||||
# Menno's 2025 SSH Key
|
# Menno's 2025 SSH Key
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE22Hfx8wgkc57TXX1TCMHcNrCdjbfog5QeHFJfl7IeD menno_fallback
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE22Hfx8wgkc57TXX1TCMHcNrCdjbfog5QeHFJfl7IeD menno_fallback
|
||||||
|
|
||||||
|
# Menno's Server (rsync mostly)
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwyFxs+Zva2W2Viu9dzznFR9CfsiRWM1gxxvD5FUhfb menno@mennos-server
|
||||||
|
Reference in New Issue
Block a user