Skip to content

Commit 763a1b7

Browse files
committed
Add NetFromRange and NetFromInterval helpers
When retrieving set elements it can be desired to format the result in the same way `nft` would, which is merging intervals to CIDR representations. To make this easier, introduce helper functions which allow for conversion of IP address ranges to CIDR networks. Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
1 parent 1db35da commit 763a1b7

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

util.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ package nftables
1616

1717
import (
1818
"encoding/binary"
19+
"errors"
1920
"net"
21+
"net/netip"
2022

2123
"github.com/google/nftables/binaryutil"
2224
"golang.org/x/sys/unix"
2325
)
2426

27+
var (
28+
MaxIPv4 = net.IP{255, 255, 255, 255}
29+
MaxIPv6 = net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
30+
)
31+
2532
func extraHeader(family uint8, resID uint16) []byte {
2633
return append([]byte{
2734
family,
@@ -126,3 +133,63 @@ func NetInterval(cidr string) (net.IP, net.IP, error) {
126133

127134
return first, nextIP(last), nil
128135
}
136+
137+
// NetFromRange returns a CIDR IP network given a start and end address.
138+
// If the network is an exact match, ok will be true.
139+
func NetFromRange(first net.IP, last net.IP) (*net.IPNet, bool, error) {
140+
ip1 := net.IP(first)
141+
ip2 := net.IP(last)
142+
143+
maxLen := 32
144+
isIpv6 := ip1.To4() == nil
145+
146+
if isIpv6 && ip2.To4() != nil || !isIpv6 && ip2.To4() == nil {
147+
return nil, false, errors.New("Cannot mix IPv4 and IPv6.")
148+
}
149+
150+
if isIpv6 {
151+
maxLen = 128
152+
}
153+
154+
var match *net.IPNet
155+
for l := maxLen; l >= -1; l-- {
156+
cidrmask := net.CIDRMask(l, maxLen)
157+
ipmask := ip2.Mask(cidrmask)
158+
ipnet := net.IPNet{
159+
IP: ipmask,
160+
Mask: cidrmask,
161+
}
162+
163+
if ipnet.Contains(ip1) {
164+
match = &ipnet
165+
break
166+
}
167+
168+
}
169+
170+
return match, match.IP.Equal(ip1), nil
171+
}
172+
173+
// NetFromInterval returns a CIDR IP network given a start and end address as found in intervals.
174+
// This is similar to NetFromRange, but subtracts one address from the end of the range.
175+
// If the resulting network is an exact match, ok will be true.
176+
func NetFromInterval(first net.IP, last net.IP) (out *net.IPNet, ok bool, err error) {
177+
var previous net.IP
178+
179+
if len(last) == 0 {
180+
if last.To4() == nil {
181+
previous = MaxIPv6
182+
} else {
183+
previous = MaxIPv4
184+
}
185+
} else {
186+
ip2, ok := netip.AddrFromSlice(last)
187+
if !ok {
188+
return nil, false, errors.New("Failed to construct slice from network.")
189+
}
190+
191+
previous = ip2.Prev().AsSlice()
192+
}
193+
194+
return NetFromRange(first, previous)
195+
}

util_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,184 @@ func TestNetInterval(t *testing.T) {
201201
})
202202
}
203203
}
204+
205+
func TestNetFromRange(t *testing.T) {
206+
tests := []struct {
207+
name string
208+
first string
209+
last string
210+
wantNet string
211+
wantOk bool
212+
wantErr bool
213+
}{
214+
{
215+
first: "0.0.0.0",
216+
last: "255.255.255.255",
217+
wantNet: "0.0.0.0/0",
218+
wantOk: true,
219+
wantErr: false,
220+
},
221+
{
222+
first: "0.0.0.1",
223+
last: "255.255.255.254",
224+
wantNet: "0.0.0.0/0",
225+
wantOk: false,
226+
wantErr: false,
227+
},
228+
{
229+
first: "192.168.4.0",
230+
last: "192.168.4.255",
231+
wantNet: "192.168.4.0/24",
232+
wantOk: true,
233+
wantErr: false,
234+
},
235+
{
236+
first: "192.0.2.16",
237+
last: "192.0.2.30",
238+
wantNet: "192.0.2.16/28",
239+
wantOk: true,
240+
wantErr: false,
241+
},
242+
{
243+
first: "2001:db8:100::",
244+
last: "2001:db8:100:ffff:ffff:ffff:ffff:ffff",
245+
wantNet: "2001:db8:100::/48",
246+
wantOk: true,
247+
wantErr: false,
248+
},
249+
{
250+
first: "2001:db8:100::100",
251+
last: "2001:db8:100:0:ffff:ffff:ffff:ffff",
252+
wantNet: "2001:db8:100::/64",
253+
wantOk: false,
254+
wantErr: false,
255+
},
256+
{
257+
first: "2001:db8:100::",
258+
last: "192.0.2.30",
259+
wantNet: "",
260+
wantOk: true,
261+
wantErr: true,
262+
},
263+
{
264+
first: "192.0.2.30",
265+
last: "2001:db8:100::",
266+
wantNet: "",
267+
wantOk: true,
268+
wantErr: true,
269+
},
270+
}
271+
272+
for _, tt := range tests {
273+
t.Run(tt.first+"-"+tt.last, func(t *testing.T) {
274+
gotNet, gotOk, err := NetFromRange(net.ParseIP(tt.first), net.ParseIP(tt.last))
275+
if (err != nil) != tt.wantErr {
276+
t.Errorf("NetFromRange() error = %v, wantErr = %v", err, tt.wantErr)
277+
}
278+
279+
if tt.wantNet == "" {
280+
return
281+
}
282+
283+
_, wantNetParsed, err := net.ParseCIDR(tt.wantNet)
284+
if err != nil {
285+
t.Fatalf("NetFromRange() error parsing test network = %v", err)
286+
}
287+
288+
if tt.wantOk != gotOk {
289+
t.Errorf("NetFromRange() gotOk = %t, wantOk = %t", gotOk, tt.wantOk)
290+
}
291+
292+
if !reflect.DeepEqual(gotNet, wantNetParsed) {
293+
t.Errorf("NetFromRange() gotNet = %+v, wantNet = %+v", gotNet, wantNetParsed)
294+
}
295+
})
296+
}
297+
}
298+
299+
func TestNetFromInterval(t *testing.T) {
300+
tests := []struct {
301+
name string
302+
first string
303+
last string
304+
wantNet string
305+
wantOk bool
306+
wantErr bool
307+
}{
308+
{
309+
first: "192.0.2.16",
310+
last: "192.0.2.32",
311+
wantNet: "192.0.2.16/28",
312+
wantOk: true,
313+
wantErr: false,
314+
},
315+
{
316+
first: "128.0.0.0",
317+
last: "255.255.255.255",
318+
wantNet: "128.0.0.0/1",
319+
wantOk: true,
320+
wantErr: false,
321+
},
322+
{
323+
first: "2001:db8:100::",
324+
last: "2001:db8:101::",
325+
wantNet: "2001:db8:100::/48",
326+
wantOk: true,
327+
wantErr: false,
328+
},
329+
{
330+
first: "2001:db8:a1:11::",
331+
last: "2001:db8:a1:12::",
332+
wantNet: "2001:db8:a1:11::/64",
333+
wantOk: true,
334+
wantErr: false,
335+
},
336+
{
337+
first: "2001:db8:100::100",
338+
last: "2001:db8:100:0:ffff:ffff:ffff:ffff",
339+
wantNet: "2001:db8:100::/64",
340+
wantOk: false,
341+
wantErr: false,
342+
},
343+
{
344+
first: "2001:db8:100::",
345+
last: "192.0.2.30",
346+
wantNet: "",
347+
wantOk: true,
348+
wantErr: true,
349+
},
350+
{
351+
first: "192.0.2.30",
352+
last: "2001:db8:100::",
353+
wantNet: "",
354+
wantOk: true,
355+
wantErr: true,
356+
},
357+
}
358+
359+
for _, tt := range tests {
360+
t.Run(tt.first+"-"+tt.last, func(t *testing.T) {
361+
gotNet, gotOk, err := NetFromInterval(net.ParseIP(tt.first), net.ParseIP(tt.last))
362+
if (err != nil) != tt.wantErr {
363+
t.Errorf("NetFromInterval() error = %v, wantErr = %v", err, tt.wantErr)
364+
}
365+
366+
if tt.wantNet == "" {
367+
return
368+
}
369+
370+
_, wantNetParsed, err := net.ParseCIDR(tt.wantNet)
371+
if err != nil {
372+
t.Fatalf("NetFromInterval() error parsing test network = %v", err)
373+
}
374+
375+
if tt.wantOk != gotOk {
376+
t.Errorf("NetFromInterval() gotOk = %t, wantOk = %t", gotOk, tt.wantOk)
377+
}
378+
379+
if !reflect.DeepEqual(gotNet, wantNetParsed) {
380+
t.Errorf("NetFromInterval() gotNet = %+v, wantNet = %+v", gotNet, wantNetParsed)
381+
}
382+
})
383+
}
384+
}

0 commit comments

Comments
 (0)