Skip to content

Conversation

@nickgarlis
Copy link
Contributor

When creating set elements that represent a network, the interval range must be half-open [start, end) rather than inclusive [start, end]. For example, for 10.0.0.0/24, the expected range is 10.0.0.0 to 10.0.1.0 instead of 10.0.0.0 to 10.0.0.255.

This change introduces a NetInterval helper that returns the correct range given a CIDR string.

Some notes for consideration:

  • I used net.IP for consistency with NetFirstAndLastIP. However, netip.Addr seems to be the new standard. Should I change this PR to convert the returned values of NetInterval to netip.Addr ?
  • Would this package benefit from a dependency like https://github.com/go4org/netipx so that those utility functions don't need to be re-implemented ?

@nickgarlis nickgarlis force-pushed the add-net-interval branch 2 times, most recently from afe2136 to a183ed5 Compare November 16, 2025 09:19
@stapelberg
Copy link
Collaborator

// This is the range that nftables uses for interval matching with set elements.

Can you explain when/where/how nftables uses such an interval? I would also like to add a reference to the wraparound behavior (ending up with :: or 0.0.0.0), because that sounds a little unexpected…

  • I used net.IP for consistency with NetFirstAndLastIP. However, netip.Addr seems to be the new standard. Should I change this PR to convert the returned values of NetInterval to netip.Addr ?

I haven’t worked much with the new netip type. I would hold off on making this change for now. We can consider updating all net.IP to netip.Addr independently.

No, I would like to keep the dependencies as minimal as possible for this project.

@nickgarlis
Copy link
Contributor Author

nickgarlis commented Nov 17, 2025

Can you explain when/where/how nftables uses such an interval?

When you add a CIDR element to an nftables set, nft internally represents that CIDR as two separate interval elements.

For example, adding 10.0.0.0/24 results in the following elements:

[]nftables.SetElement{
	{Key: net.ParseIP("10.0.0.0").To4()},
	{Key: net.ParseIP("10.0.1.0").To4(), IntervalEnd: true},
}

You can see this by running:

nft --debug=netlink add element ip test-table test-set { 10.0.0.0/24 }

which prints something like:

test-set test-table 0
	element 0000000a  : 0 [end]	element 0001000a  : 1 [end]

If the set is initially empty, nft may show an extra unrelated element. That behavior is explained in more detail in #247 and is outside the scope of this PR.

What happens if you use the normal range?

If you instead create the elements like this:

[]nftables.SetElement{
	{Key: net.ParseIP("10.0.0.0").To4()},
	{Key: net.ParseIP("10.0.0.255").To4(), IntervalEnd: true},
}

and then inspect the set with nft, you’ll see:

table ip test-table {
	set test-set {
		type ipv4_addr
		flags interval
		elements = { 10.0.0.0-10.0.0.254 }
	}
}

This shows that nft normalizes interval boundaries itself so the correct approach should be to follow nft’s internal interval representation.

Edge case: ranges that overflow

If the final IP in the interval overflows (e.g., 255.255.255.254/31) , nft expresses this using a single element and a userdata flag indicating that no end element exists:

test-set test-table 0
	element feffffff  : 0 [end]  userdata = { \x01\x04\x01\x00\x00\x00 }

With #343 this would be addressed like this:

 []nftables.SetElement{
	{Key: net.ParseIP("255.255.255.254").To4(), IntervalOpen: true},
}

Putting it all together

Given an arbitrary CIDR, this is how I would use those two features together:

var elements []nftables.SetElement
first, last, err := nftables.NetInterval(unknownCIDR)
if err != nil {
	return err
}
// last overflowed - there is no end element
if last.IsUnspecified() { // perhaps last == nil would be better ?
	elements = []nftables.SetElement{
		{
			Key:          first.To4(),
			IntervalOpen: true,
		},
	}
} else {
	elements = []nftables.SetElement{
		{
			Key: first.To4(),
		},
		{
			Key:         last.To4(),
			IntervalEnd: true,
		},
	}
}

Please let me know if you would like me to clarify anything. I am happy to go into more detail.

I would also like to add a reference to the wraparound behavior (ending up with :: or 0.0.0.0), because that sounds a little unexpected…

I think the explanation above covers this, but just to make sure we are aligned: the zero IP is used to represent "no end IP", since in some cases, the last IP would go past the maximum valid address. Would using a pointer be a better approach ?

I haven’t worked much with the new netip type. I would hold off on making this change for now. We can consider updating all net.IP to netip.Addr independently.

Sounds good 👍

No, I would like to keep the dependencies as minimal as possible for this project.

Also sounds good 👍

@stapelberg
Copy link
Collaborator

Thanks for the explanation!

I think the explanation above covers this, but just to make sure we are aligned: the zero IP is used to represent "no end IP", since in some cases, the last IP would go past the maximum valid address. Would using a pointer be a better approach ?

So, to be clear, using the zero IP as a sentinel value is our own choice, not required by nftables, yes? And when marshaling, do we convert zero IP to userdata-marker?

If so, I think we should clarify the comment of nextIP, perhaps like so:

// nextIp returns the next IP address after the given one.
// If the next address overflows, the sentinel values 0.0.0.0 (IPv4)
// or :: (IPv6) are returned.

When creating set elements that represent a network, the interval
range must be half-open [start, end) rather than inclusive
[start, end]. For example, for 10.0.0.0/24, the expected range is
10.0.0.0 to 10.0.1.0 instead of 10.0.0.0 to 10.0.0.255.

This change introduces a NetInterval helper that returns the correct
range given a CIDR string.
@nickgarlis
Copy link
Contributor Author

So, to be clear, using the zero IP as a sentinel value is our own choice, not required by nftables, yes? And when marshaling, do we convert zero IP to userdata-marker?

Yes, it's our own choice and not something required by nftables. However, we don’t attempt to automatically detect the zero IP or add the userdata marker during marshaling. Instead, we rely on the user to supply the appropriate flag on the correct element, as shown in the example above.

If so, I think we should clarify the comment of nextIP, perhaps like so:

// nextIp returns the next IP address after the given one.
// If the next address overflows, the sentinel values 0.0.0.0 (IPv4)
// or :: (IPv6) are returned.

👍

@stapelberg stapelberg merged commit 02e7d4f into google:main Nov 18, 2025
2 checks passed
@tacerus
Copy link

tacerus commented Dec 3, 2025

Thank you for this - very useful!

@tacerus tacerus mentioned this pull request Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants