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
#!/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