Firewall

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. This is certainly not the most efficient approach at filtering because you can easily end with thousands of iptables rules that need to be scanned one by one.

See also

  • 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

Setup

  1. install the dependencies

    apt-get install iptables python3-yaml python3-requests iptables-persistent
    
  2. answer Yes to all questions

  3. install fpyutils. See reference

  4. create the jobs directories. See reference

    mkdir -p /home/jobs/{scripts,services}/by-user/root
    
  5. create the script

    /home/jobs/scripts/by-user/root/iptables_geoport.py
      1#!/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)
    
  6. create a configuration file

    /home/jobs/scripts/by-user/root/iptables_geoport.yaml
     1#
     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'
    

    Note

    Rules are scanned sequentially. Move frequently used rules upper in the file to improve performance.

  7. use this Systemd service unit file

    /home/jobs/services/by-user/root/iptables-geoport.service
     1[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
    
  8. fix the permissions

    chmod 700 -R /home/jobs/scripts/by-user/iptables_geoport.*
    chmod 700 -R /home/jobs/services/by-user/root
    
  9. run the deploy script

SANE

To be able to use a remote scanner with SANE using this setup you need to follow these steps

See also

  • SANE - ArchWiki 8

  • Secure use of iptables and connection tracking helpers 7

  1. 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'
    
  2. add this file to load the kernel module

    /etc/modprobe.d/nf_conntrack.conf
    1options nf_conntrack nf_conntrack_helper=1
    
  3. add this file to load the kernel module

    /etc/modules-load.d/nf_conntrack_sane.conf
    1nf_conntrack_sane
    
  4. reboot

  5. check if the rules are active: 1 should be returned by the cat command

    cat /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 contributors

5

https://wiki.archlinux.org/index.php/Iptables GNU Free Documentation License 1.3 or late, copyright (c) ArchWiki contributors

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 contributors

8

https://home.regit.org/wp-content/uploads/2011/11/secure-conntrack-helpers.html unknown license