Skip to content

Commit e47c383

Browse files
authored
⭐ ip.inRange (#5271)
After: #5265 This adds a new function to the IP type to check if it is in range of a subnet or two IP addresses. This is how to use it with IPv4 subnets: ```coffee > ip('192.2.3.4').inRange('192.2.3.0/24') true # We can also auto-detect the subnet if you don't specify it. For example, this is a class C address, so: > ip('192.2.3.4').inRange('192.2.3.0') true > ip('192.2.3.4').inRange('192.1.1.0/24') false ``` Here is how you use it with 2 IP addresses: ```coffee > ip('192.2.3.4').inRange('192.2.3.1', '192.2.3.5') true > ip('192.2.3.4').inRange('192.2.3.5', '192.2.3.255') false > ip('2001:db8:3c4d:15::1a2f:1a2b').inRange('2001:db8:3c4d::', '2001:db8:3c4e::') true ``` This PR also improves the auto-detection of netmasks. In IPv4 we can now auto-detect class A, B, and C networks and is able to print the subnet mask via the `subnet` field: ```coffee > ip('1.2.3.4').subnet "255.0.0.0" > ip('172.0.0.1').subnet "255.255.0.0" > ip('192.168.0.1').subnet "255.255.255.0" ``` For IPv6 the `subnet` works differently. From my current understanding it is the subnet-ID that most people care about. The concept of subnet masks doesn't exist here [[1](https://en.wikipedia.org/wiki/IP_address#Subnetworks)]. The subnet is the part of the 64-bit network prefix that is not used by the routing prefix. For example: ```coffee # 64-bit, default IPv6 prefix: > ip("2001:db8:3c4d:15::1a2f:1a2b").prefixLength 64 > ip("2001:db8:3c4d:15::1a2f:1a2b").prefix "2001:db8:3c4d:15::" > ip("2001:db8:3c4d:15::1a2f:1a2b").subnet "" # 48-bit prefix: > ip("2001:db8:3c4d:15::1a2f:1a2b/48").prefixLength 48 > ip("2001:db8:3c4d:15::1a2f:1a2b/48").prefix "2001:db8:3c4d::" > ip("2001:db8:3c4d:15::1a2f:1a2b/48").subnet "15" ``` As you can see, the `prefix` remains in full IPv6 notation, even if the lower bits are appreviated with `"::"`. The subnet, however, is only the identifier since it is not commonly represented as a full IPv6 address, unlike the prefix and host. Other small fixes: - compare IP, string and dict types (e.g. `ip("1.2.3.4") == "1.2.3.4"`) - support the subnet mask of `"/0"` (it was trying to detect the default subnet before, even if users specified that they explicitly wanted this subnet)
1 parent 1943c3d commit e47c383

File tree

6 files changed

+231
-14
lines changed

6 files changed

+231
-14
lines changed

llx/builtin.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ func init() {
269269
string("!=" + types.Dict): {f: stringNotDictV2, Label: "!="},
270270
string("==" + types.Version): {f: versionCmpVersion, Label: "=="},
271271
string("!=" + types.Version): {f: versionNotVersion, Label: "!="},
272+
string("==" + types.IP): {f: ipCmpIP, Label: "=="},
273+
string("!=" + types.IP): {f: ipNotIP, Label: "!="},
272274
string("==" + types.ArrayLike): {f: chunkEqFalseV2, Label: "=="},
273275
string("!=" + types.ArrayLike): {f: chunkNeqTrueV2, Label: "!="},
274276
string("==" + types.Array(types.String)): {f: stringCmpStringarrayV2, Label: "=="},
@@ -463,6 +465,8 @@ func init() {
463465
string("!=" + types.Regex): {f: dictNotRegexV2, Label: "!="},
464466
string("==" + types.Version): {f: versionCmpVersion, Label: "=="},
465467
string("!=" + types.Version): {f: versionNotVersion, Label: "!="},
468+
string("==" + types.IP): {f: ipCmpIP, Label: "=="},
469+
string("!=" + types.IP): {f: ipNotIP, Label: "!="},
466470
string("==" + types.ArrayLike): {f: dictCmpArrayV2, Label: "=="},
467471
string("!=" + types.ArrayLike): {f: dictNotArrayV2, Label: "!="},
468472
string("==" + types.Array(types.String)): {f: dictCmpStringarrayV2, Label: "=="},
@@ -591,12 +595,14 @@ func init() {
591595
string("==" + types.Empty): {f: stringCmpEmptyV2, Label: "=="},
592596
string("!=" + types.Empty): {f: stringNotEmptyV2, Label: "!="},
593597
string("==" + types.IP): {f: ipCmpIP, Label: "=="},
598+
string("!=" + types.IP): {f: ipNotIP, Label: "!="},
594599
"version": {f: ipVersion},
595600
"subnet": {f: ipSubnet},
596601
"prefix": {f: ipPrefix},
597602
"prefixLength": {f: ipPrefixLength},
598603
"suffix": {f: ipSuffix},
599604
"isUnspecified": {f: ipUnspecified},
605+
"inRange": {f: ipInRange, Label: "inRange"},
600606
},
601607
types.ArrayLike: {
602608
"[]": {f: arrayGetIndexV2},

llx/builtin_ip.go

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package llx
55

66
import (
7+
"errors"
78
"net"
89
"strconv"
910
"strings"
@@ -20,14 +21,14 @@ type IP struct {
2021
func NewIP(s string) IP {
2122
prefix := s
2223
suffix := ""
23-
mask := 0
2424
if idx := strings.IndexByte(s, '/'); idx != -1 {
2525
prefix = s[0:idx]
2626
if len(s) > idx+1 {
2727
suffix = s[idx+1:]
2828
}
2929
}
3030

31+
mask := -1
3132
if suffix != "" {
3233
mask64, _ := strconv.ParseInt(suffix, 10, 0)
3334
mask = int(mask64)
@@ -38,9 +39,13 @@ func NewIP(s string) IP {
3839
version := 0
3940
if ip.To4() != nil {
4041
version = 4
42+
if mask == -1 {
43+
m := ip.DefaultMask()
44+
mask = countMaskBits(m)
45+
}
4146
} else if ip.To16() != nil {
4247
version = 6
43-
if mask == 0 {
48+
if mask == -1 {
4449
mask = 64
4550
}
4651
}
@@ -54,6 +59,27 @@ func NewIP(s string) IP {
5459

5560
var bitmasks = []byte{0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff}
5661

62+
func countMaskBits(b []byte) int {
63+
var res int
64+
for _, cur := range b {
65+
// optimization for speed
66+
if cur == 0xff {
67+
res += 8
68+
continue
69+
}
70+
if cur == 0 {
71+
break
72+
}
73+
// and the remaining bits
74+
for cur&0x80 != 0 {
75+
res++
76+
cur = cur << 1
77+
}
78+
break
79+
}
80+
return res
81+
}
82+
5783
func makeBits(bits int, on bool) []byte {
5884
var res []byte
5985
var one byte
@@ -117,7 +143,7 @@ func mask2string(b []byte) string {
117143
return res.String()
118144
}
119145

120-
func (i IP) subnet() string {
146+
func (i IP) Subnet() string {
121147
if i.Version == 4 {
122148
b := createMask(i.Mask, 0, 4)
123149
return mask2string(b)
@@ -150,19 +176,22 @@ func (i IP) subnet() string {
150176
return res
151177
}
152178

153-
func (i IP) prefix() string {
179+
func (i IP) prefix() net.IP {
154180
var b []byte
155181
if i.Version == 4 {
156182
b = createMask(i.Mask, 0, 4)
157183
} else if i.Version == 6 {
158184
b = createMask(i.Mask, 0, 16)
159185
}
160186
if len(b) == 0 {
161-
return ""
187+
return []byte{}
162188
}
163189
mask := net.IPMask(b)
164-
prefix := i.IP.Mask(mask)
165-
return prefix.String()
190+
return i.IP.Mask(mask)
191+
}
192+
193+
func (i IP) Prefix() string {
194+
return i.prefix().String()
166195
}
167196

168197
func flipMask(b []byte) []byte {
@@ -172,7 +201,7 @@ func flipMask(b []byte) []byte {
172201
return b
173202
}
174203

175-
func (i IP) suffix() string {
204+
func (i IP) Suffix() string {
176205
var b []byte
177206
if i.Version == 4 {
178207
b = createMask(i.Mask, 0, 4)
@@ -191,9 +220,46 @@ func (i IP) suffix() string {
191220
return suffix.String()
192221
}
193222

223+
func (i IP) inRange(other IP) bool {
224+
prefix := i.prefix()
225+
otherPrefix := other.prefix()
226+
return prefix.Equal(otherPrefix)
227+
}
228+
229+
func (i IP) Cmp(other IP) int {
230+
for idx, b := range i.IP {
231+
if len(other.IP) <= idx {
232+
return 1
233+
}
234+
o := other.IP[idx]
235+
if b == o {
236+
continue
237+
}
238+
if b < o {
239+
return -1
240+
} else {
241+
return 1
242+
}
243+
}
244+
if len(other.IP) > len(i.IP) {
245+
return -1
246+
}
247+
return 0
248+
}
249+
194250
func ipCmpIP(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
195251
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
196-
return BoolData(left.(string) == right.(string))
252+
lip := NewIP(left.(string))
253+
rip := NewIP(right.(string))
254+
return BoolData(lip.Equal(rip.IP) && lip.Mask == rip.Mask)
255+
})
256+
}
257+
258+
func ipNotIP(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
259+
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
260+
lip := NewIP(left.(string))
261+
rip := NewIP(right.(string))
262+
return BoolData(!lip.Equal(rip.IP) || lip.Mask != rip.Mask)
197263
})
198264
}
199265

@@ -212,7 +278,7 @@ func ipSubnet(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawDa
212278
}
213279

214280
v := NewIP(bind.Value.(string))
215-
return StringData(v.subnet()), 0, nil
281+
return StringData(v.Subnet()), 0, nil
216282
}
217283

218284
func ipPrefix(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
@@ -221,7 +287,7 @@ func ipPrefix(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawDa
221287
}
222288

223289
v := NewIP(bind.Value.(string))
224-
return StringData(v.prefix()), 0, nil
290+
return StringData(v.Prefix()), 0, nil
225291
}
226292

227293
func ipPrefixLength(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
@@ -239,7 +305,7 @@ func ipSuffix(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawDa
239305
}
240306

241307
v := NewIP(bind.Value.(string))
242-
return StringData(v.suffix()), 0, nil
308+
return StringData(v.Suffix()), 0, nil
243309
}
244310

245311
func ipUnspecified(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
@@ -250,3 +316,45 @@ func ipUnspecified(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*
250316
v := NewIP(bind.Value.(string))
251317
return BoolData(v.IP.IsUnspecified()), 0, nil
252318
}
319+
320+
func ipInRange(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
321+
if bind.Value == nil {
322+
return &RawData{Type: types.Int, Error: bind.Error}, 0, nil
323+
}
324+
325+
conditions := []string{}
326+
for i := range chunk.Function.Args {
327+
argRef := chunk.Function.Args[i]
328+
329+
arg, rref, err := e.resolveValue(argRef, ref)
330+
if err != nil || rref > 0 {
331+
return nil, rref, err
332+
}
333+
334+
s, ok := arg.Value.(string)
335+
if !ok {
336+
return nil, 0, errors.New("incorrect type for argument in `inRange` call (expected string)")
337+
}
338+
conditions = append(conditions, s)
339+
}
340+
341+
v := NewIP(bind.Value.(string))
342+
if len(conditions) == 1 {
343+
i := NewIP(conditions[0])
344+
return BoolData(v.inRange(i)), 0, nil
345+
}
346+
347+
min := NewIP(conditions[0])
348+
max := NewIP(conditions[1])
349+
350+
mincmp := min.Cmp(v)
351+
if mincmp == 1 {
352+
return BoolFalse, 0, nil
353+
}
354+
maxcmp := v.Cmp(max)
355+
if maxcmp == 1 {
356+
return BoolFalse, 0, nil
357+
}
358+
359+
return BoolTrue, 0, nil
360+
}

llx/builtin_ip_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package llx
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestCreateMask(t *testing.T) {
14+
tests := []struct {
15+
mask int
16+
offset int
17+
maxBytes int
18+
res []byte
19+
}{
20+
{0, 0, 1, []byte{0x00}},
21+
{1, 0, 1, []byte{0x80}},
22+
{5, 0, 1, []byte{0xf8}},
23+
{8, 0, 1, []byte{0xff}},
24+
{9, 0, 1, []byte{0xff}},
25+
{9, 0, 2, []byte{0xff, 0x80}},
26+
{4, 4, 1, []byte{0x0f}},
27+
{7, 1, 1, []byte{0x7f}},
28+
{5, 3, 1, []byte{0x1f}},
29+
{6, 3, 2, []byte{0x1f, 0x80}},
30+
{6, 3, 1, []byte{0x1f}},
31+
{16, 48, 16, []byte{0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0}},
32+
}
33+
34+
for i := range tests {
35+
cur := tests[i]
36+
t.Run(fmt.Sprintf("bits=%d off=%d max=%d", cur.mask, cur.offset, cur.maxBytes), func(t *testing.T) {
37+
res := createMask(cur.mask, cur.offset, cur.maxBytes)
38+
assert.Equal(t, cur.res, res)
39+
})
40+
}
41+
}

mqlc/builtin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func init() {
107107
"prefixLength": {typ: intType, signature: FunctionSignature{}},
108108
"suffix": {typ: stringType, signature: FunctionSignature{}},
109109
"isUnspecified": {typ: boolType, signature: FunctionSignature{}},
110+
"inRange": {typ: intType, compile: compileIpInRange},
110111
},
111112
types.ArrayLike: {
112113
"[]": {typ: childType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Int}}},

mqlc/builtin_simple.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ func compileVersionInRange(c *compiler, _ types.Type, ref uint64, id string, cal
245245
return types.Nil, errors.New("function " + id + " needs two arguments")
246246
}
247247

248-
min, err := callArgTypeIs(c, call, id, "min", 0, types.String)
248+
min, err := callArgTypeIs(c, call, id, "min", 0, types.String, types.Dict)
249249
if err != nil {
250250
return types.Nil, err
251251
}
252-
max, err := callArgTypeIs(c, call, id, "max", 1, types.String)
252+
max, err := callArgTypeIs(c, call, id, "max", 1, types.String, types.Dict)
253253
if err != nil {
254254
return types.Nil, err
255255
}
@@ -265,3 +265,34 @@ func compileVersionInRange(c *compiler, _ types.Type, ref uint64, id string, cal
265265
})
266266
return types.Bool, nil
267267
}
268+
269+
func compileIpInRange(c *compiler, _ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
270+
if call == nil || (len(call.Function) != 1 && len(call.Function) != 2) {
271+
return types.Nil, errors.New("function " + id + " needs one or two arguments")
272+
}
273+
274+
min, err := callArgTypeIs(c, call, id, "min", 0, types.String, types.IP, types.Dict)
275+
if err != nil {
276+
return types.Nil, err
277+
}
278+
args := []*llx.Primitive{min}
279+
280+
if len(call.Function) == 2 {
281+
max, err := callArgTypeIs(c, call, id, "max", 1, types.String, types.IP, types.Dict)
282+
if err != nil {
283+
return types.Nil, err
284+
}
285+
args = append(args, max)
286+
}
287+
288+
c.addChunk(&llx.Chunk{
289+
Call: llx.Chunk_FUNCTION,
290+
Id: "inRange",
291+
Function: &llx.Function{
292+
Type: string(types.Bool),
293+
Binding: ref,
294+
Args: args,
295+
},
296+
})
297+
return types.Bool, nil
298+
}

0 commit comments

Comments
 (0)