# Optimizing ipsets for iptables

netfilter/iptables `hash:net` ipsets (netsets) are a fast way to manage IP lists for firewall rules. The number of entries in an ipset does not affect lookup performance. However, each **distinct prefix length** in the netset adds one extra lookup per packet. A netset using all 32 possible IPv4 prefixes forces 32 lookups per packet.

`iprange --ipset-reduce` consolidates prefixes while keeping the matched IP set identical. For example, one /23 entry becomes two /24 entries — same IPs, one fewer prefix.

## Parameters

| Option | Default | Purpose |
|--------|---------|---------|
| `--ipset-reduce PERCENT` | 20 | Allow this % increase in entries |
| `--ipset-reduce-entries ENTRIES` | 16384 | Minimum absolute entry cap |

You enable reduce mode by giving either option. The maximum acceptable entries is computed as:

```
max(current_entries * (1 + PERCENT / 100), ENTRIES)
```

This design works well across all netset sizes:

- Small netsets (hundreds of entries) are scaled up to ENTRIES
- Large netsets (hundreds of thousands) are scaled by PERCENT

## Algorithm

The algorithm is optimal: at each step it finds the prefix whose elimination adds the fewest new entries, merges it into the next available prefix, and repeats until the entry limit is reached. Use `-v` to see the elimination steps.

## Example: country netset

The GeoLite2 netset for Greece:

```bash
$ iprange -C --header country_gr.netset
entries,unique_ips
406,6304132
```

406 entries, 6.3 million unique IPs. The prefix breakdown (`-v`):

```
prefix /13 counts 1 entries
prefix /14 counts 3 entries
prefix /15 counts 7 entries
prefix /16 counts 42 entries
prefix /17 counts 19 entries
prefix /18 counts 17 entries
prefix /19 counts 21 entries
prefix /20 counts 21 entries
prefix /21 counts 30 entries
prefix /22 counts 50 entries
prefix /23 counts 50 entries
prefix /24 counts 98 entries
prefix /25 counts 4 entries
prefix /27 counts 2 entries
prefix /28 counts 7 entries
prefix /29 counts 25 entries
prefix /31 counts 3 entries
prefix /32 counts 6 entries
```

**18 distinct prefixes** = 18 lookups per packet.

After reduction with 20% entry increase:

```bash
$ iprange -v --ipset-reduce 20 country_gr.netset >/dev/null
Eliminated 15 out of 18 prefixes (3 remain in the final set).

prefix /21 counts 3028 entries
prefix /24 counts 398 entries
prefix /32 counts 900 entries
```

**3 prefixes, 4,326 entries** — same 6.3 million unique IPs. The kernel now does 3 lookups instead of 18.

With a higher entry cap:

```bash
$ iprange -v --ipset-reduce 20 --ipset-reduce-entries 50000 country_gr.netset >/dev/null
Eliminated 16 out of 18 prefixes (2 remain in the final set).

prefix /24 counts 24622 entries
prefix /32 counts 900 entries
```

**2 prefixes, 25,522 entries** — one more prefix eliminated thanks to the higher entry budget.

## Example: large blocklist

A large blocklist (218,307 entries, 25 prefixes, 765 million IPs):

```bash
$ iprange -v --ipset-reduce 20 --ipset-reduce-entries 50000 \
    ib_bluetack_level1.netset >/dev/null
Eliminated 17 out of 25 prefixes (8 remain in the final set).

prefix /16 counts 11118 entries
prefix /20 counts 5216 entries
prefix /24 counts 46718 entries
prefix /26 counts 17902 entries
prefix /27 counts 18123 entries
prefix /28 counts 32637 entries
prefix /29 counts 94802 entries
prefix /32 counts 33570 entries
```

From 25 prefixes to 8, entries from 218,307 to 260,086. At 50%: 6 prefixes. At 100%: 5 prefixes.

## Lossless round-trip

The reduction is lossless. Piping reduced output back through `iprange` reproduces the original optimized set:

```bash
iprange --ipset-reduce 100 blocklist.txt | iprange -v >/dev/null
# output is identical to: iprange -v blocklist.txt >/dev/null
```

## Typical usage

```bash
# Moderate reduction (good default)
iprange --ipset-reduce 20 blocklist.txt > reduced.txt

# Aggressive reduction for small lists
iprange --ipset-reduce 20 --ipset-reduce-entries 50000 country.netset > reduced.txt

# Generate ipset restore commands from reduced set
iprange --ipset-reduce 20 --print-prefix "add myset " blocklist.txt
```
