Firewall
Vedi anche
A collection of scripts I have written and/or adapted that I currently use on my systems as automated tasks 1
Simple shell script for GNU/Linux, built on iptables, which is able to filter incoming packets based on accepted port numbers and countries. It is aimed to SOHO users. 2
Linux Iptables Just Block By Country - nixCraft 3
Simple stateful firewall - ArchWiki 4
iptables - ArchWiki 5
25 Most Frequently Used Linux IPTables Rules Examples 6
Use iptables to block IP addresses by country for inbound ports on a server. This is a whitelist: what is not explicitly allowed is blocked. This is useful, for example, to filter most SSH bruteforce attacks while leaving a webserver freely available.
Nota
This is certainly not the most efficient approach at filtering: you can easily end with thousands of iptables rules that need to be scanned one by one!
Setup
Run as user |
Instruction number |
|
* |
install the dependencies
apt-get install iptables python3-yaml python3-requests iptables-persistent
answer
Yes
to all questionsinstall fpyutils. See reference
create the jobs directories. See reference
mkdir -p /home/jobs/{scripts,services}/by-user/root chmod 700 -R /home/jobs/{scripts,services}/by-user/root
create the
script
/home/jobs/scripts/by-user/root/iptables_geoport.py1#!/usr/bin/env python3 2# 3# iptables_geoport.py 4# 5# Copyright (C) 2020-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com) 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License along 18# with this program; if not, write to the Free Software Foundation, Inc., 19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 21# OLD NOTICES 22# =========== 23# See url for more info - http://www.cyberciti.biz/faq/?p=3402 24# Author: nixCraft <www.cyberciti.biz> under GPL v.2.0+ 25# Post Author: frnmst (Franco Masotti) (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com) 26# New version heavily based on https://wiki.archlinux.org/index.php/Simple_stateful_firewall 27# https://wiki.archlinux.org/index.php/Iptables 28# and a little on http://www.thegeekstuff.com/2011/06/iptables-rules-examples/ as well as nixCraft for the bash stuff. 29# =========== 30r"""iptables_geoport.py.""" 31 32import copy 33import ipaddress 34import os 35import pathlib 36import shlex 37import sys 38import urllib 39 40import fpyutils 41import requests 42import yaml 43 44 45class UserNotRoot(Exception): 46 r"""The user running the script is not root.""" 47 48 49################## 50# Basic commands # 51################## 52 53 54def reset_rules(): 55 r"""Reset the chains and the tables.""" 56 # https://wiki.archlinux.org/index.php/Iptables#Resetting_rules 57 # 58 # Copyright (C) 2020 Arch Wiki contributors. 59 # Permission is granted to copy, distribute and/or modify this document 60 # under the terms of the GNU Free Documentation License, Version 1.3 61 # or any later version published by the Free Software Foundation; 62 # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. 63 # A copy of the license is included in the section entitled "GNU 64 # Free Documentation License". 65 commands = dict() 66 commands['flush_tcp_chain'] = 'iptables --flush TCP' 67 commands['flush_udp_chain'] = 'iptables --flush UDP' 68 commands['flush_input_chain'] = 'iptables --flush INPUT' 69 commands['flush_output_chain'] = 'iptables --flush OUTPUT' 70 commands['flush_logging_chain'] = 'iptables --flush LOGGING' 71 72 commands['delete_tcp_chain'] = 'iptables --delete-chain TCP' 73 commands['delete_udp_chain'] = 'iptables --delete-chain UDP' 74 commands['delete_logging_chain'] = 'iptables --delete-chain LOGGING' 75 76 commands['flush_mangle_table'] = 'iptables --table mangle --flush' 77 commands['delete_mangle_chain'] = 'iptables --table mangle --delete-chain' 78 commands['flush_raw_table'] = 'iptables --table raw --flush' 79 commands['delete_raw_chain'] = 'iptables --table raw --delete-chain' 80 commands['flush_security_table'] = 'iptables --table security --flush' 81 commands['delete_security_chain'] = 'iptables --table security --delete-chain' 82 commands['accept_input'] = 'iptables --policy INPUT ACCEPT' 83 commands['accept_forward'] = 'iptables --policy FORWARD ACCEPT' 84 commands['accept_output'] = 'iptables --policy OUTPUT ACCEPT' 85 86 # sys._getframe().f_code.co_name is the function name. 87 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 88 return commands 89 90 91def initialize_basic_chains() -> dict: 92 r"""Apply some basic rules for a single machine.""" 93 # https://wiki.archlinux.org/index.php/Simple_stateful_firewall#Firewall_for_a_single_machine 94 # 95 # Copyright (C) 2020 Arch Wiki contributors. 96 # Permission is granted to copy, distribute and/or modify this document 97 # under the terms of the GNU Free Documentation License, Version 1.3 98 # or any later version published by the Free Software Foundation; 99 # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. 100 # A copy of the license is included in the section entitled "GNU 101 # Free Documentation License". 102 commands = dict() 103 104 # Output traffic is NOT filtered. 105 commands['output_chain'] = 'iptables --policy OUTPUT ACCEPT' 106 107 # Create two user defined chains that will define tcp an udp protocol rules later. 108 commands['tcp_chain'] = 'iptables --new-chain TCP' 109 commands['udp_chain'] = 'iptables --new-chain UDP' 110 111 # For a single machine, however, we simply set the policy of the FORWARD chain to DROP and move on 112 commands['drop_forward'] = 'iptables --policy FORWARD DROP' 113 114 # The first rule added to the INPUT chain will allow traffic that belongs 115 # to established connections, or new valid traffic that is related to these 116 # connections such as ICMP errors, or echo replies. 117 commands[ 118 'allow_realted_established'] = 'iptables --append INPUT --match conntrack --ctstate RELATED,ESTABLISHED --jump ACCEPT' 119 120 # loopback interface INPUT traffic enabled for ping and debugging stuff. 121 commands[ 122 'loopback'] = 'iptables --append INPUT --in-interface lo --jump ACCEPT' 123 124 # Drop all invalid INPUT (i.e. damaged) packets. 125 # To do this connection must be tracked (conntrack) 126 # and connection state (cstate) is set to INVALID. 127 commands[ 128 'invalid_input'] = 'iptables --append INPUT --match conntrack --ctstate INVALID --jump DROP' 129 130 # Allow icmp type 8 (i.e. ping) to all interfaces. 131 commands[ 132 'ping'] = 'iptables --append INPUT --protocol icmp --icmp-type 8 --match conntrack --ctstate NEW --jump ACCEPT' 133 134 # TCP snd UDP chains are connected to INPUT chains. 135 # These two user-defined chains will manage the ports. 136 # Remember that tcp uses SYN to initialize a connection, unlike UDP 137 commands[ 138 'connect_tcp_chain'] = 'iptables --append INPUT --protocol tcp --syn -m conntrack --ctstate NEW --jump TCP' 139 commands[ 140 'connect_udp_chain'] = 'iptables --append INPUT --protocol udp -m conntrack --ctstate NEW --jump UDP' 141 142 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 143 return commands 144 145 146def initialize_logging_chain() -> dict: 147 r"""Create the logging chain.""" 148 # https://wiki.archlinux.org/index.php/Iptables#Logging 149 # 150 # Copyright (C) 2020 Arch Wiki contributors. 151 # Permission is granted to copy, distribute and/or modify this document 152 # under the terms of the GNU Free Documentation License, Version 1.3 153 # or any later version published by the Free Software Foundation; 154 # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. 155 # A copy of the license is included in the section entitled "GNU 156 # Free Documentation License". 157 commands = dict() 158 159 commands['logging_chain'] = 'iptables --new-chain LOGGING' 160 commands[ 161 'connect_logging_chain'] = 'iptables --append INPUT --jump LOGGING' 162 commands[ 163 'logging_limit'] = 'iptables --append LOGGING --match limit --limit 2/hour --limit-burst 10 --jump LOG' 164 165 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 166 return commands 167 168 169def initialize_blocking_rules(drop_packets: bool = True, 170 logging: bool = True) -> dict: 171 r"""Initialize blocking rules.""" 172 # https://wiki.archlinux.org/index.php/Simple_stateful_firewall#Firewall_for_a_single_machine 173 # 174 # Copyright (C) 2020 Arch Wiki contributors. 175 # Permission is granted to copy, distribute and/or modify this document 176 # under the terms of the GNU Free Documentation License, Version 1.3 177 # or any later version published by the Free Software Foundation; 178 # with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. 179 # A copy of the license is included in the section entitled "GNU 180 # Free Documentation License". 181 commands = dict() 182 183 if logging: 184 chain = 'LOGGING' 185 else: 186 chain = 'INPUT' 187 if drop_packets: 188 # Drop everything. 189 commands['drop'] = 'iptables --append ' + chain + ' --jump DROP' 190 else: 191 # RFC compilant. 192 commands[ 193 'rfc_tcp'] = 'iptables --append ' + chain + ' --protocol tcp --jump REJECT --reject-with tcp-rst' 194 commands[ 195 'rfc_udp'] = 'iptables --append ' + chain + ' --protocol udp --jump REJECT --reject-with icmp-port-unreachable' 196 # Other protocols are usually not used, so REJECT those packets with icmp-proto-unreachable. 197 commands[ 198 'proto_unreachable'] = 'iptables --append ' + chain + ' --jump REJECT --reject-with icmp-proto-unreachable' 199 200 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 201 return commands 202 203 204def set_accepted_rules(ports: dict, accepted_ips: dict) -> dict: 205 r"""Set accepted rules.""" 206 assert_input_ports_struct(ports) 207 assert_accepted_ips_struct(accepted_ips) 208 209 commands = dict() 210 # O(ports*chains*ips) <= O(ips^3) because of how this script works. 211 i = 0 212 for port in ports: 213 source = ports[port]['source'] 214 ips = accepted_ips[source] 215 chains, protocols = get_chains_and_protocols(ports[port]['protocol']) 216 for w, chain in enumerate(chains): 217 for ip in ips: 218 commands[str(i)] = generate_accepted_rule_command( 219 chain, protocols[w], port, ip) 220 i += 1 221 222 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 223 return commands 224 225 226def set_patch_rules(rules: list) -> dict: 227 r"""Pass raw commands directly.""" 228 for r in rules: 229 if not isinstance(r, str): 230 raise TypeError 231 232 commands = dict() 233 for i, rule in enumerate(rules): 234 commands[str(i)] = rule 235 236 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 237 return commands 238 239 240def initialize_drop_rules() -> dict: 241 r"""Initialize drop rules.""" 242 commands = dict() 243 commands['drop'] = 'iptables --policy INPUT DROP' 244 245 fix_dict_keys(commands, sys._getframe().f_code.co_name, '__') 246 return commands 247 248 249######### 250# Utils # 251######### 252 253 254def get_chains_and_protocols(protocol: str) -> tuple: 255 r"""Compute the iptables chain and protocol values.""" 256 if protocol not in ['tcp', 'udp', 'all']: 257 raise ValueError 258 259 chains = list() 260 protocols = list() 261 if protocol == 'tcp': 262 chains.append('TCP') 263 protocols.append('tcp') 264 elif protocol == 'udp': 265 chains.append('UDP') 266 protocols.append('udp') 267 elif protocol == 'all': 268 chains.append('TCP') 269 chains.append('UDP') 270 protocols.append('tcp') 271 protocols.append('udp') 272 273 return chains, protocols 274 275 276def generate_accepted_rule_command(chain: str, protocol: str, port: str, 277 remote_ip: str) -> str: 278 r"""Generate a single command for the accepted rules.""" 279 check_port(port) 280 check_ip_address(remote_ip) 281 282 return ('iptables --append ' + chain + ' --protocol ' + protocol + 283 ' --dport ' + port + ' --source ' + remote_ip + ' --jump ACCEPT') 284 285 286def fix_dict_keys(dictionary: dict, prefix: str, separator: str): 287 r"""Fix the keys of a dictionary by adding a prefix and separator.""" 288 d = copy.deepcopy(dictionary) 289 for key in d: 290 dictionary[prefix + separator + key] = d[key] 291 del dictionary[key] 292 293 294def load_zone_file(zone_file: str) -> list: 295 r"""Load zone file.""" 296 zones = list() 297 with open(zone_file, 'r') as f: 298 line = f.readline().strip() 299 while line: 300 check_ip_address(line) 301 zones.append(line) 302 line = f.readline().strip() 303 304 return zones 305 306 307def load_zone_files(zone_files: list) -> list: 308 r"""Load all the zone files content in a flat data structure.""" 309 for zf in zone_files: 310 if not isinstance(zf, str): 311 raise TypeError 312 313 zones = list() 314 for zf in zone_files: 315 zones += load_zone_file(zf) 316 317 return zones 318 319 320def get_filename_from_url(url: str) -> str: 321 r"""Use some tricks to get the filemame from a URL.""" 322 return pathlib.PurePath(urllib.parse.urlparse(url).path).name 323 324 325def download_zone_file(url: str, dst_directory: str) -> str: 326 r"""Save the zone file.""" 327 filename = get_filename_from_url(url) 328 pathlib.Path(dst_directory).mkdir(mode=0o700, parents=True, exist_ok=True) 329 full_path = str(pathlib.Path(dst_directory, filename)) 330 try: 331 r = requests.get(url) 332 with open(full_path, 'w') as f: 333 f.write(r.text) 334 except requests.ConnectionError: 335 pass 336 337 return full_path 338 339 340def download_zone_files(urls: list, cache_directory: str) -> list: 341 r"""Download multiple zone files.""" 342 for u in urls: 343 if not isinstance(u, str): 344 raise TypeError 345 346 files = list() 347 for u in urls: 348 files.append(download_zone_file(u, cache_directory)) 349 350 return files 351 352 353def update_accepted_ips_structure(accepted_ips: dict, zones: list): 354 r"""Update some data structures.""" 355 assert_accepted_ips_struct(accepted_ips) 356 assert_zones_struct(zones) 357 358 accepted_ips['wan'] = zones 359 accepted_ips['all'] = accepted_ips['lan'] + accepted_ips['wan'] 360 361 362def get_packet_policy(invalid_packet_policy: str) -> bool: 363 r"""Update a variable.""" 364 if invalid_packet_policy not in ['polite', 'rude']: 365 raise ValueError 366 367 drop = True 368 if invalid_packet_policy == 'rude': 369 drop = True 370 elif invalid_packet_policy == 'polite': 371 drop = False 372 return drop 373 374 375############## 376# Assertions # 377############## 378 379 380def check_ip_address(ip: str): 381 r"""Verify that we are dealing with a network address.""" 382 ipaddress.ip_network(ip, strict=True) 383 384 385def check_port(port: str): 386 r"""Check that the input port is a valid port number.""" 387 if not port.isdigit(): 388 raise TypeError 389 if not (int(port) >= 0 and int(port) <= (2**16) - 1): 390 raise ValueError 391 392 393def assert_zones_struct(zones: list): 394 r"""Check that the data structure is a list of ip addresses.""" 395 for z in zones: 396 if not isinstance(z, str): 397 raise TypeError 398 check_ip_address(z) 399 400 401def assert_accepted_ips_struct(accepted_ips: dict): 402 r"""Check that the data structure is a list of ip addresses.""" 403 for ips in accepted_ips: 404 if not isinstance(accepted_ips[ips], list): 405 raise TypeError 406 for j in accepted_ips[ips]: 407 if not isinstance(j, str): 408 raise TypeError 409 check_ip_address(j) 410 411 412def assert_input_ports_struct(ports: dict): 413 r"""Check that the data structure is correct.""" 414 for port in ports: 415 check_port(port) 416 if not isinstance(ports[port], dict): 417 raise TypeError 418 if 'source' not in ports[port]: 419 raise ValueError 420 if 'protocol' not in ports[port]: 421 raise ValueError 422 if ports[port]['source'] not in ['lan', 'wan', 'all']: 423 raise ValueError 424 425 426############ 427# Pipeline # 428############ 429 430if __name__ == '__main__': 431 if os.getuid() != 0: 432 raise UserNotRoot 433 434 # Load the configuration. 435 configuration_file = shlex.quote(sys.argv[1]) 436 config = yaml.load(open(configuration_file, 'r'), Loader=yaml.SafeLoader) 437 dry_run = config['dry_run'] 438 cache_directory = config['cache_directory'] 439 zone_files = config['accepted_ips']['wan'] 440 accepted_ips = dict() 441 accepted_ips['lan'] = config['accepted_ips']['lan'] 442 accepted_ips['wan'] = list() 443 patch_rules = config['patch_rules'] 444 set_patch_rules_first = config['set_patch_rules_first'] 445 input_ports = config['input_ports'] 446 logging = config['logging_enabled'] 447 invalid_packet_policy = config['invalid_packet_policy'] 448 drop_packets = get_packet_policy(invalid_packet_policy) 449 450 # Get the data. 451 zones = load_zone_files(download_zone_files(zone_files, cache_directory)) 452 update_accepted_ips_structure(accepted_ips, zones) 453 454 # Apply the rules. 455 reset = reset_rules() 456 basic_chains = initialize_basic_chains() 457 logging_chain = initialize_logging_chain() 458 blocking_rules = initialize_blocking_rules(drop_packets, logging) 459 rules = set_accepted_rules(input_ports, accepted_ips) 460 patch = set_patch_rules(patch_rules) 461 drop_by_default = initialize_drop_rules() 462 463 # Merge the rules. 464 if set_patch_rules_first: 465 commands = { 466 **reset, 467 **basic_chains, 468 **logging_chain, 469 **blocking_rules, 470 **patch, 471 **rules, 472 **drop_by_default 473 } 474 else: 475 commands = { 476 **reset, 477 **basic_chains, 478 **logging_chain, 479 **blocking_rules, 480 **rules, 481 **patch, 482 **drop_by_default 483 } 484 485 # Apply the rules. 486 for c in commands: 487 fpyutils.shell.execute_command_live_output(commands[c], 488 dry_run=dry_run)
create a
configuration file
/home/jobs/scripts/by-user/root/iptables_geoport.yaml1# 2# iptables_geoport.yaml 3# 4# Copyright (C) 2020-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com) 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License along 17# with this program; if not, write to the Free Software Foundation, Inc., 18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 20# If set to true print the commands that would be executed. 21dry_run: true 22 23logging_enabled: true 24 25# {rude,polite} 26invalid_packet_policy: 'rude' 27 28# The path where the ip addresses list will be saved. 29cache_directory: './.cache' 30 31# Have a look at https://www.ipdeny.com/ipblocks/ 32accepted_ips: 33 wan: 34 - 'https://www.ipdeny.com/ipblocks/data/countries/it.zone' 35 lan: 36 - '192.168.0.0/24' 37 38# If set to 'true' the "patch rules" will be applied before the "input ports" rules. 39set_patch_rules_first: true 40 41# Raw rules that override the default ones. 42# Use "[]" if you do not need patch rules. 43patch_rules: 44 - 'iptables -A TCP -p tcp --dport 80 -j ACCEPT' 45 - 'iptables -A TCP -p tcp --dport 443 -j ACCEPT' 46 47 # SSH LAN only. 48 - 'iptables -A TCP -s 192.168.0.0/24 -p tcp -m tcp --dport 22 -j ACCEPT' 49 50# source: {lan,wan,all} 51# protocol: {tcp,udp,all} 52input_ports: 53 '2222': 54 source: 'lan' 55 protocol: 'tcp' 56 '2223': 57 source: 'lan' 58 protocol: 'tcp' 59 '5555': 60 source: 'lan' 61 protocol: 'tcp' 62 '53': 63 source: 'lan' 64 protocol: 'all' 65 '631': 66 source: 'lan' 67 protocol: 'tcp' 68 '8100': 69 source: 'lan' 70 protocol: 'tcp' 71 72 # Required for SANE. 73 '6566': 74 source: 'lan' 75 protocol: 'all' 76 77 # Move SSH rules here for performance reasons. 78 '22': 79 source: 'wan' 80 protocol: 'tcp'
Nota
Rules are scanned sequentially. Move frequently used rules upper in the file to improve performance.
use this
Systemd service unit file
/home/jobs/services/by-user/root/iptables-geoport.service1[Unit] 2Description=Apply the iptables geoport rules 3Wants=network-online.target 4After=network-online.target 5Requires=netfilter-persistent.service 6After=netfilter-persistent.service 7 8[Service] 9Type=simple 10# Answer Yes (default value) to dpkg-reconfigure. 11ExecStart=/usr/bin/bash -c '/home/jobs/scripts/by-user/root/iptables_geoport.py /home/jobs/scripts/by-user/root/iptables_geoport.yaml && [ -f /sbin/dpkg-reconfigure ] && dpkg-reconfigure --frontend noninteractive iptables-persistent' 12 13User=root 14Group=root
SANE
Run as user |
Instruction number |
|
* |
To be able to use a remote scanner with SANE using this setup you need to follow these steps
open TCP and UDP ports 6566
/home/jobs/scripts/by-user/root/iptables_geoport.yaml (extract)65 '631': 66 source: 'lan' 67 protocol: 'tcp' 68 '8100': 69 source: 'lan' 70 protocol: 'tcp' 71 72 # Required for SANE. 73 '6566': 74 source: 'lan' 75 protocol: 'all' 76 77 # Move SSH rules here for performance reasons. 78 '22': 79 source: 'wan' 80 protocol: 'tcp'
add
this file
to load the kernel module/etc/modprobe.d/nf_conntrack.conf1options nf_conntrack nf_conntrack_helper=1
add
this file
to load the kernel module/etc/modules-load.d/nf_conntrack_sane.conf1nf_conntrack_sane
reboot
check if the rules are active:
1
should be returned by thecat
commandcat /proc/sys/net/netfilter/nf_conntrack_helper 1
Footnotes
- 1
https://software.franco.net.eu.org/frnmst/automated-tasks GNU GPLv3+, copyright (c) 2019-2022, Franco Masotti
- 2
https://software.franco.net.eu.org/frnmst-archives/iptables-geoport-directives GNU GPLv3+ and GNU GPLv2+, copyright (c) 2015 Franco Masotti
- 3
http://www.cyberciti.biz/faq/?p=3402 GPL v.2.0+, copyright (c) Author: nixCraft <www.cyberciti.biz>
- 4
https://wiki.archlinux.org/index.php/Simple_stateful_firewall GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributor
- 5
https://wiki.archlinux.org/index.php/Iptables GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributor
- 6
http://www.thegeekstuff.com/2011/06/iptables-rules-examples/ unknown license
- 7
https://wiki.archlinux.org/title/SANE#Firewall GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributor
- 8
https://home.regit.org/wp-content/uploads/2011/11/secure-conntrack-helpers.html unknown license