Skip to content

Commit

Permalink
feat: Implemented CIDRContains() function.
Browse files Browse the repository at this point in the history
  • Loading branch information
skyzyx committed Nov 10, 2024
1 parent c1adb32 commit 7a00fc0
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 16 deletions.
99 changes: 99 additions & 0 deletions corefunc/cidr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2023-2024, Northwood Labs
// Copyright 2023-2024, Ryan Parman <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package corefunc

import (
"fmt"
"net"

"github.com/apparentlymart/go-cidr/cidr"
)

/*
CIDRContains checks to see if an IP address or CIDR block is contained within
another CIDR block.
Ported from OpenTofu. This function licensed as MPL-2.0.
----
- containingCidr (string): A CIDR range to check as a containing range.
- containedIpOrCidr (string): An IP address or CIDR range to check as a
contained range.
*/
func CIDRContains(containingCidr, containedIPOrCidr string) (bool, error) {
// The first argument must be a CIDR prefix.
_, containing, err := net.ParseCIDR(containingCidr)
if err != nil {
return false, fmt.Errorf(
"invalid containing CIDR `%s`: %w",
containingCidr,
err,
)
}

// The second argument can be either an IP address or a CIDR prefix. We will
// try parsing it as an IP address first.
startIP := net.ParseIP(containedIPOrCidr)

var endIP net.IP

// If the second argument did not parse as an IP, we will try parsing it as
// a CIDR prefix.
if startIP == nil {
_, contained, err := net.ParseCIDR(containedIPOrCidr)
// If that also fails, we'll return an error.
if err != nil {
return false, fmt.Errorf(
"invalid IP address or containing CIDR: %s",
containedIPOrCidr,
)
}

// Otherwise, we will want to know the start and the end IP of the
// prefix, so that we can check whether both are contained in the
// containing prefix.
startIP, endIP = cidr.AddressRange(contained)
}

// We require that both addresses are of the same type, so that we can't
// accidentally compare an IPv4 address to an IPv6 prefix. The underlying Go
// function will always return false if this happens, but we want to return
// an error instead so that the caller can distinguish between a
// "legitimate" false result and an erroneous check.
if (startIP.To4() == nil) != (containing.IP.To4() == nil) {
return false, fmt.Errorf(
"address family mismatch: %s vs. %s",
containingCidr,
containedIPOrCidr,
)
}

// If the second argument was an IP address, we will check whether it is
// contained in the containing prefix, and that's our result.
result := containing.Contains(startIP)

// If the second argument was a CIDR prefix, we will also check whether the
// end IP of the prefix is contained in the containing prefix. Once CIDR is
// contained in another CIDR if both the start and the end IP of the
// contained CIDR are contained in the containing CIDR.
if endIP != nil {
result = result && containing.Contains(endIP)
}

return result, nil
}
52 changes: 52 additions & 0 deletions corefunc/cidr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2023-2024, Northwood Labs
// Copyright 2023-2024, Ryan Parman <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package corefunc

import (
"fmt"
"testing"

"github.com/northwood-labs/terraform-provider-corefunc/testfixtures"
)

func ExampleCIDRContains() {
output, err := CIDRContains("192.168.2.0/20", "192.168.2.1")
if err != nil {
panic(err)
}

fmt.Println(output)

// Output:
// true
}

func TestCIDRContains(t *testing.T) { // lint:allow_complexity
for name, tc := range testfixtures.CIDRContainsTestTable {
t.Run(name, func(t *testing.T) {
output, err := CIDRContains(tc.ContainerCidr, tc.ContainedIPOrCidr)

// We expect an error.
if err != nil && !tc.ExpectedErr {
t.Errorf("Unexpected error: %v", err)
}

if output != tc.Expected {
t.Errorf("Expected %v, got %v", tc.Expected, output)
}
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
require (
github.com/ProtonMail/go-crypto v1.1.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bits-and-blooms/bitset v1.15.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0G
github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
Expand Down
115 changes: 115 additions & 0 deletions testfixtures/cidr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2023-2024, Northwood Labs
// Copyright 2023-2024, Ryan Parman <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License";
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package testfixtures // lint:no_dupe

// CIDRContainsTestTable is used by both the standard Go tests and also the
// Terraform acceptance tests.
// <https://github.com/golang/go/wiki/TableDrivenTests>
var CIDRContainsTestTable = map[string]struct { // lint:no_dupe
ContainerCidr string
ContainedIPOrCidr string
Expected bool
ExpectedErr bool
}{
"192.168.2.0/20 contains 192.168.2.1": {
// IPv4, contained (IP).
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "192.168.2.1",
Expected: true,
ExpectedErr: false,
},
"192.168.2.0/20 contains 192.168.2.0/22": {
// IPv4, contained (CIDR).
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "192.168.2.0/22",
Expected: true,
ExpectedErr: false,
},
"192.168.2.0/20 contains 192.126.2.1": {
// IPv4, not contained.
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "192.126.2.1",
Expected: false,
ExpectedErr: false,
},
"192.168.2.0/20 contains 192.126.2.0/18": {
// IPv4, not contained (CIDR).
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "192.126.2.0/18",
Expected: false,
ExpectedErr: false,
},
"fe80::/48 contains fe80::1": {
// IPv6, contained.
ContainerCidr: "fe80::/48",
ContainedIPOrCidr: "fe80::1",
Expected: true,
ExpectedErr: false,
},
"fe80::/48 contains fe81::1": {
// IPv6, not contained.
ContainerCidr: "fe80::/48",
ContainedIPOrCidr: "fe81::1",
Expected: false,
ExpectedErr: false,
},
"192.168.2.0/20 contains fe80::1": {
// Address family mismatch: IPv4 containing_prefix, IPv6
// contained_ip_or_prefix (IP).
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "fe80::1",
Expected: false,
ExpectedErr: true,
},
"192.168.2.0/20 contains fe80::/24": {
// Address family mismatch: IPv4 containing_prefix, IPv6
// contained_ip_or_prefix (prefix).
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "fe80::/24",
Expected: false,
ExpectedErr: true,
},
"fe80::/48 contains 192.168.2.1": {
// Address family mismatch: IPv6 containing_prefix, IPv4
// contained_ip_or_prefix (IP).
ContainerCidr: "fe80::/48",
ContainedIPOrCidr: "192.168.2.1",
Expected: false,
ExpectedErr: true,
},
"fe80::/48 contains 192.168.2.0/20": {
// Address family mismatch: IPv6 containing_prefix, IPv4
// contained_ip_or_prefix (prefix).
ContainerCidr: "fe80::/48",
ContainedIPOrCidr: "192.168.2.0/20",
Expected: false,
ExpectedErr: true,
},
"not-a-cidr contains 192.168.2.1": {
// Input error: invalid CIDR address.
ContainerCidr: "not-a-cidr",
ContainedIPOrCidr: "192.168.2.1",
Expected: false,
ExpectedErr: true,
},
"192.168.2.0/20 contains not-an-address": {
// Input error: invalid IP address.
ContainerCidr: "192.168.2.0/20",
ContainedIPOrCidr: "not-an-address",
Expected: false,
ExpectedErr: true,
},
}
Binary file modified unit-coverage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7a00fc0

Please sign in to comment.