Table of Contents

, , ,

Drop lots of IP subnets in shorewall

You can use this method to block lots of bots coming from thousands of IPs.

First create an input file with one IP on each line. Example below is from nginx access logs where we collect the IPs from user-agents identified as python-httx

awk -F'"' '$6 ~ /python-httpx\/0\.28\.1/ { print $1 }' /var/log/nginx/example.com.access.log | awk '{ print $1 }' | sort -u > ips.txt

Then pass the txt file to the following script

aggregate_to_16.py
#!/usr/bin/env python3
"""
aggregate_to_16.py
-----------------
Group a list of IPv4 addresses into /16 subnets and report the count per subnet.
 
Usage:
    python3 aggregate_to_16.py [path/to/file.txt]
 
If no file is given, the script reads from STDIN, so you can also do:
    cat ips.txt | python3 aggregate_to_16.py
"""
 
import sys
from collections import Counter
from ipaddress import IPv4Address, IPv4Network
 
def ip_to_16net(ip_str: str) -> IPv4Network:
    """
    Convert a single IPv4 string to its containing /16 network.
    Example: 10.3.45.78 → IPv4Network('10.3.0.0/16')
    """
    # IPv4Network with strict=False treats the address as a host and
    # expands it to the requested prefix length.
    return IPv4Network(f"{IPv4Address(ip_str)}/16", strict=False)
 
 
def main():
    # ------------------------------------------------------------------
    # 1️⃣  Get an iterator over the input lines (file or stdin)
    # ------------------------------------------------------------------
    if len(sys.argv) > 1:
        source = open(sys.argv[1], "r")
    else:
        source = sys.stdin
 
    # ------------------------------------------------------------------
    # 2️⃣  Build a Counter keyed by the /16 network string
    # ------------------------------------------------------------------
    counter = Counter()
    for raw_line in source:
        line = raw_line.strip()
        if not line:                 # skip empty lines
            continue
        try:
            net = ip_to_16net(line)
            counter[str(net)] += 1
        except ValueError:
            # If the line isn’t a valid IPv4 address we simply ignore it.
            # (You could also collect errors here if you wish.)
            continue
 
    # Close the file if we opened one
    if source is not sys.stdin:
        source.close()
 
    # ------------------------------------------------------------------
    # 3️⃣  Print the summary sorted by network address
    # ------------------------------------------------------------------
    for net in sorted(counter, key=lambda x: tuple(map(int, x.split('/')[0].split('.')))):
        #print(f"{net}\t{counter[net]} addresses")
        print(f"{net}")
 
 
if __name__ == "__main__":
    main()

You can uncomment the #print(f“{net}\t{counter[net]} addresses”) part to see how many addresses are grouped.

Comment that out now and leave the printing of the subnet only print(f“{net}”)

and use that in for loop for shorewall dynamic chain:

# for ip in $(cat ips.txt);do shorewall drop from $ip;done

Tested on

See also

References