Skip to content

Instantly share code, notes, and snippets.

@ironiridis
Last active December 24, 2023 05:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ironiridis/e51f46e33f925439e80aa469676bbde7 to your computer and use it in GitHub Desktop.
Save ironiridis/e51f46e33f925439e80aa469676bbde7 to your computer and use it in GitHub Desktop.
notes about blocking a whole AS with IP ranges

Blocking a whole AS using iptables

Why

Sometimes you don't want any traffic from a certain organization... none at all, not even from their employees. An effective way to do just that is to determine every IP adress that belongs to a certain Autonomous System which is how IPs are generally allocated to organizations that have a significant1 presence on the internet.

What you need

  • Typical stuff like curl and iptables and maybe sudo if you aren't already root.
  • hxselect and hxnormalize. This is probably packaged by your distro. Mine is gentoo but some folks use arch and normal people use debian.
  • Some version of Go2 that is at least 1.18.

Let's go

Getting the IP addresses assigned to an AS

This example abuses Hurricane Electric's public AS query page to get a list of CIDRs for an AS. There are probably better ways to do this, but I trust HE to be up-to-date, where a lot of databases are not.

Note that HE blocks curl through detection of the user agent, which is completely valid; they would not want an automated system hammering this webpage for anything at scale. You, however, are a human, and you can indicate as much with a little fib.

curl -vL -A 'human/person' https://bgp.he.net/AS32934 -o as32934.html

HTML is not a good data transport format. It's really meant to be transformed into something humans can eat with their eyes, but that doesn't mean we can't ask a computer to nibble on it too. We use two tools; hxnormalize to fix up some of the sloppy HTML and hxselect to extract data from the HTML tables.

hxnormalize -x < as32934.html | hxselect -c -s '\n' '#table_prefixes4 > tbody > tr > td.nowrap > a' > list.v4
hxnormalize -x < as32934.html | hxselect -c -s '\n' '#table_prefixes6 > tbody > tr > td.nowrap > a' > list.v6

Note that the above two lines are specific for the format of the webpage that he.net is using today, but that may change in the future. There are other ways to get CIDRs for an AS, this one worked for me at the end of 2023.

Generating the firewall rules

This step uses the Go program that accompanies this gist. You can download it directly or copy/paste it into a text file. I called it cidrsquish.go because I like a good pun (about how apples are turned into cider) but you do you. Use the cat command to combine the IPv4 and IPv6 lists into a set of iptables and ip6tables commands. Adjust the flags to the commands to your liking.3

cat list.v4 list.v6 | go run cidrsquish.go -chain as32934 -sudo

You should have a list of commands that create a chain named as32934, adds a bunch of DROP rules, and ends the chain with a RETURN action (if you used the command above as-is) which should be safe to paste into your server. Note that this is only a "should"... you must look at the commands before you run them to validate that they are not dangerous.4

The expected output from pasting these commands is nothing; after each command, there should be no errors. If the very first command fails, you are probably trying to update an existing chain with new IPs, which means you need to add -flush to the command. This is dangerous! It is probably best to choose a new chain name, create it, and update the references in your INPUT chain to point to the new one.

Enabling the new rules

We're almost done, but we have the scary part left; touching your actual live firewall rules. If you're connected remotely (eg via ssh or mosh) then you risk locking yourself out of your server. Ideally, have a backup access option, or have a snapshot-rollback option. You need to know how to regain access anyway; today is a great day to learn if you don't.

We need to jump from our main "a packet arrived" INPUT chain to our new chain that blocks all those IP ranges. This is done in precisely the same way as how we drop a packet, with the -j flag. It will look like this:

sudo iptables -I INPUT -i internetif0 -j as32934
sudo ip6tables -I INPUT -i internetif0 -j as32934

You need to know what internetif0 is supposed to be. It's the network interface that's facing the internet on your server.

Once you run these, the changes are immediate. You may need to do something on your distribution to save the rules so they are restored when your server reboots. On gentoo, this is rc-service iptables save; rc-service ip6tables save and everyone else on Earth will need to search their documentation for their distro.

Good luck!

If you need help, search @ironiridis@mspsocial.net on the fediverse and send me a DM.

License

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <https://unlicense.org>

Footnotes

  1. Signifigant in this situation means organizations large enough to want/need a huge block of IPs. They're usually assigned either to organizations that have been on the internet for a long time, or to organizations that deal with big internet infrastrucutre.

  2. "uuggghh why go" Well, because the AS listing may include hundreds of CIDRs with overlaps. You don't want to process every inbound packet against 120 redundant address comparisons when two will do. The Go standard library has great built-in support for calculating ipv4 and ipv6 prefixes, and I like it, and this gist is free.

  3. You can run go run cidrsquish.go -help to see the available flags, or look at the source; they're near the top.

  4. I am just an enby on the internet giving you dangerous security advice with no guard rails. I could be deleting your files or seducing your loved ones. You should approach everything here with a sense of dread and foreboding. Actually, that's true for everything on the internet.

package main
import (
"bufio"
"flag"
"fmt"
"net/netip"
"os"
)
func main() {
flagSudo := flag.Bool("sudo", false, "[DANGEROUS] set to true to prefix every command with sudo")
flagFlush := flag.Bool("flush", false, "[DANGEROUS] instead of creating a new chain, empty an existing one")
flagChain := flag.String("chain", "asblock", "iptables chain name to manipulate")
flagAction := flag.String("action", "-j DROP", "iptables behavior arguments, default drops traffic")
flagSkipSix := flag.Bool("skip6", false, "don't output ip6tables commands (boo)")
flag.Parse()
cidrs := make(map[netip.Prefix]bool)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
cidr := netip.MustParsePrefix(scanner.Text()).Masked()
if _, ok := cidrs[cidr]; ok {
continue
}
cidrs[cidr] = true
for b := range cidrs {
if !cidrs[b] || cidr == b {
continue
}
if b.Overlaps(cidr) {
if b.Bits() > cidr.Bits() {
cidrs[b] = false
continue
} else {
cidrs[cidr] = false
break
}
}
}
}
if err := scanner.Err(); err != nil {
panic(err)
}
var cmdv4, cmdv6 string
if *flagSudo {
cmdv4, cmdv6 = "sudo iptables", "sudo ip6tables"
} else {
cmdv4, cmdv6 = "iptables", "ip6tables"
}
if *flagFlush {
fmt.Fprintf(os.Stdout, "%s -F %s\n", cmdv4, *flagChain)
if !*flagSkipSix {
fmt.Fprintf(os.Stdout, "%s -F %s\n", cmdv6, *flagChain)
}
} else {
fmt.Fprintf(os.Stdout, "%s -N %s\n", cmdv4, *flagChain)
if !*flagSkipSix {
fmt.Fprintf(os.Stdout, "%s -N %s\n", cmdv6, *flagChain)
}
}
for cidr := range cidrs {
if !cidrs[cidr] {
continue
}
if cidr.Addr().Is4() {
fmt.Fprintf(os.Stdout, "%s -A %s -s %s %s\n", cmdv4, *flagChain, cidr, *flagAction)
} else if cidr.Addr().Is6() && !*flagSkipSix {
fmt.Fprintf(os.Stdout, "%s -A %s -s %s %s\n", cmdv6, *flagChain, cidr, *flagAction)
}
}
fmt.Fprintf(os.Stdout, "%s -A %s -j RETURN\n", cmdv4, *flagChain)
if !*flagSkipSix {
fmt.Fprintf(os.Stdout, "%s -A %s -j RETURN\n", cmdv6, *flagChain)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment