Fix nftables auto-merge syntax and block private/reserved ranges

auto-merge is a standalone set statement, not an nft "flags" value -
"flags interval, auto-merge;" is a syntax error; fixed to "flags
interval;" followed by "auto-merge" on its own line. Needed because large
public blocklists (FireHOL, Spamhaus) contain overlapping CIDRs that
nftables otherwise refuses as "conflicting intervals".

Also add a hard guard: reject any "block" entry that overlaps a private/
reserved/bogon range (RFC1918, CGNAT, loopback, link-local, etc.) in both
the single-entry and bulk-import paths. Public feeds like FireHOL level1
routinely include ranges like 10.0.0.0/8 and 172.16.0.0/12, meant for
WAN-only edge firewalls - applied host-wide here (where WireGuard/LAN
subnets legitimately live in that same private space), those entries
would silently block a server's own internal/VPN traffic instead of
actual bad actors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-12 20:44:29 +02:00
co-authored by Claude Sonnet 5
parent a9649f6306
commit 00d084a188
2 changed files with 52 additions and 2 deletions
+50
View File
@@ -1789,6 +1789,53 @@ func FirewallListsPage() echo.HandlerFunc {
}
}
// dangerousBlockRanges are private/reserved/bogon IPv4 supernets that public
// threat-intel feeds (FireHOL level1, some Spamhaus-derived lists, etc.)
// often include, since they're meaningless on a public WAN edge. On a host
// that also uses private address space internally (LAN, WireGuard client
// subnets - as this one does), blocking these at the global (host-wide)
// priority-10 hook would drop that host's own internal/VPN traffic. So
// "block" entries overlapping any of these are rejected outright; "allow"
// entries are never affected.
var dangerousBlockRanges = mustParseCIDRs([]string{
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
"169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16",
"192.0.0.0/24", "192.0.2.0/24", "198.18.0.0/15", "198.51.100.0/24",
"203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",
})
func mustParseCIDRs(cidrs []string) []*net.IPNet {
nets := make([]*net.IPNet, 0, len(cidrs))
for _, c := range cidrs {
_, n, err := net.ParseCIDR(c)
if err != nil {
panic(err)
}
nets = append(nets, n)
}
return nets
}
// overlapsDangerousRange reports whether cidr (an IP or CIDR string)
// overlaps any private/reserved/bogon supernet in dangerousBlockRanges.
func overlapsDangerousRange(cidr string) bool {
var ipNet *net.IPNet
if ip, n, err := net.ParseCIDR(cidr); err == nil {
ipNet = n
_ = ip
} else if ip := net.ParseIP(cidr); ip != nil {
ipNet = &net.IPNet{IP: ip, Mask: net.CIDRMask(32, 32)}
} else {
return false
}
for _, dangerous := range dangerousBlockRanges {
if dangerous.Contains(ipNet.IP) || ipNet.Contains(dangerous.IP) {
return true
}
}
return false
}
func validateIPListEntry(entry model.IPListEntry) error {
if entry.ListType != "allow" && entry.ListType != "block" {
return fmt.Errorf("list_type must be 'allow' or 'block'")
@@ -1796,6 +1843,9 @@ func validateIPListEntry(entry model.IPListEntry) error {
if !util.ValidateServerAddresses([]string{entry.CIDR}) && net.ParseIP(entry.CIDR) == nil {
return fmt.Errorf("cidr must be a valid IP or CIDR")
}
if entry.ListType == "block" && overlapsDangerousRange(entry.CIDR) {
return fmt.Errorf("refusing to block %s: overlaps a private/reserved range - this host uses private address space internally (LAN/WireGuard subnets), blocking it here would cut off your own traffic", entry.CIDR)
}
if len(entry.Comment) > 200 {
return fmt.Errorf("comment too long")
}