Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add explain subcommand #75

Merged
merged 20 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
98fe336
feat: init `explain` subcommand
bschaatsbergen Dec 2, 2023
627b841
Merge branch 'main' into f/add-explain-subcommand
bschaatsbergen Dec 2, 2023
b8d2d11
Merge branch 'main' into f/add-explain-subcommand
bschaatsbergen Dec 4, 2023
f4eaa78
feat: implement GetNetMask, GetBaseAddress and GetBroadcastAddress
bschaatsbergen Dec 5, 2023
086a911
chore: add read permissions and set cache to false
bschaatsbergen Dec 5, 2023
1e88c11
chore: bump checkout to v4
bschaatsbergen Dec 5, 2023
6e0614e
chore: skip-pkg-cache: true and skip-build-cache: true
bschaatsbergen Dec 5, 2023
37fc787
chore: fix unnecessary conversion
bschaatsbergen Dec 5, 2023
f647fa6
chore: handle IPv4 CIDR ranges with large prefixes having no broadcas…
bschaatsbergen Dec 5, 2023
d937b94
chore: rename file and change variable names to more transparent and …
bschaatsbergen Dec 5, 2023
5bfb6a7
chore: move comment above if logic
bschaatsbergen Dec 5, 2023
44b65d3
chore: add GetLastUsableIPAddress
bschaatsbergen Dec 5, 2023
9c0100e
Merge branch 'main' into f/add-explain-subcommand
bschaatsbergen Dec 8, 2023
3d2a05a
chore: add GetFirstUsableIPAddress, GetPrefixLength and errors
bschaatsbergen Dec 9, 2023
b100164
chore: produce a human readable number and print coloured network det…
bschaatsbergen Dec 9, 2023
c1647fc
chore: ignore goconst on explain func
bschaatsbergen Dec 9, 2023
1252e93
chore: add 2 helper functions to determine if a network is an IPv4 or…
bschaatsbergen Dec 9, 2023
ae631a3
chore: improve the explainCmd by using a networkDetails struct
bschaatsbergen Dec 9, 2023
cb58113
chore: add tests
bschaatsbergen Dec 9, 2023
f9dce3a
chore: add a length indicator to the prefix length
bschaatsbergen Dec 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ name: CI

on: pull_request

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
cache: true
cache: false
- name: Build
run: go build -v ./...
- name: Run linters
uses: golangci/golangci-lint-action@v3
with:
version: latest
skip-pkg-cache: true
skip-build-cache: true
- name: Run tests
run: go test -v ./...
133 changes: 133 additions & 0 deletions cmd/explain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package cmd

import (
"fmt"
"net"
"os"

"github.com/bschaatsbergen/cidr/pkg/core"
"github.com/bschaatsbergen/cidr/pkg/helper"
"github.com/fatih/color"
"github.com/spf13/cobra"
"golang.org/x/text/language"
"golang.org/x/text/message"
)

var (
explainCmd = &cobra.Command{
Use: "explain",
Short: "Provides information about a CIDR range",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Println("error: provide a CIDR range and an IP address")
fmt.Println("See 'cidr contains -h' for help and examples")
os.Exit(1)
}
network, err := core.ParseCIDR(args[0])
if err != nil {
fmt.Printf("error: %s\n", err)
fmt.Println("See 'cidr contains -h' for help and examples")
os.Exit(1)
}
details := getNetworkDetails(network)
explain(details)
},
}
)

func init() {
rootCmd.AddCommand(explainCmd)
}

type networkDetailsToDisplay struct {
IsIPV4Network bool
IsIPV6Network bool
BroadcastAddress string
BroadcastAddressHasError bool
Netmask net.IP
PrefixLength int
BaseAddress net.IP
Count string
UsableAddressRangeHasError bool
FirstUsableIPAddress string
LastUsableIPAddress string
}

func getNetworkDetails(network *net.IPNet) *networkDetailsToDisplay {
details := &networkDetailsToDisplay{}

// Determine whether the network is an IPv4 or IPv6 network.
if helper.IsIPv4Network(network) {
details.IsIPV4Network = true
} else if helper.IsIPv6Network(network) {
details.IsIPV6Network = true
}

// Obtain the broadcast address, handling errors if they occur.
ipBroadcast, err := core.GetBroadcastAddress(network)
if err != nil {
// Set error flags and store the error message so that it can be displayed later.
details.BroadcastAddressHasError = true
details.BroadcastAddress = err.Error()
} else {
details.BroadcastAddress = ipBroadcast.String()
}

// Obtain the netmask and prefix length.
details.Netmask = core.GetNetMask(network)
details.PrefixLength = core.GetPrefixLength(details.Netmask)

// Obtain the base address of the network.
details.BaseAddress = core.GetBaseAddress(network)

// Obtain the total count of addresses in the network.
count := core.GetAddressCount(network)
// Format the count as a human-readable string and store it in the details struct.
details.Count = message.NewPrinter(language.English).Sprintf("%d", count)

// Obtain the first and last usable IP addresses, handling errors if they occur.
firstUsableIP, err := core.GetFirstUsableIPAddress(network)
if err != nil {
// Set error flags if an error occurs during the retrieval of the first usable IP address.
details.UsableAddressRangeHasError = true
} else {
details.FirstUsableIPAddress = firstUsableIP.String()
}

lastUsableIP, err := core.GetLastUsableIPAddress(network)
if err != nil {
// Set error flags if an error occurs during the retrieval of the last usable IP address.
details.UsableAddressRangeHasError = true
} else {
details.LastUsableIPAddress = lastUsableIP.String()
}

// Return the populated 'networkDetailsToDisplay' struct.
return details
}

//nolint:goconst
func explain(details *networkDetailsToDisplay) {
var lengthIndicator string

fmt.Printf(color.BlueString("Base Address:\t\t ")+"%s\n", details.BaseAddress)
if !details.UsableAddressRangeHasError {
fmt.Printf(color.BlueString("Usable Address Range:\t ")+"%s to %s\n", details.FirstUsableIPAddress, details.LastUsableIPAddress)
} else {
fmt.Printf(color.RedString("Usable Address Range:\t ")+"%s\n", "unable to calculate usable address range")
}
if !details.BroadcastAddressHasError && details.IsIPV4Network {
fmt.Printf(color.BlueString("Broadcast Address:\t ")+"%s\n", details.BroadcastAddress)
} else if details.BroadcastAddressHasError && details.IsIPV4Network {
fmt.Printf(color.RedString("Broadcast Address:\t ")+"%s\n", details.BroadcastAddress)
}
fmt.Printf(color.BlueString("Address Count:\t\t ")+"%s\n", details.Count)

if details.PrefixLength > 1 {
lengthIndicator = "bits"
} else {
lengthIndicator = "bit"
}

fmt.Printf(color.BlueString("Netmask:\t\t ")+"%s (/%d %s)\n", details.Netmask, details.PrefixLength, lengthIndicator)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.4
require (
github.com/fatih/color v1.16.0
github.com/spf13/cobra v1.8.0
golang.org/x/text v0.14.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
12 changes: 12 additions & 0 deletions pkg/core/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package core

const (
IPv6HasNoBroadcastAddressError = "IPv6 network has no broadcast addresses"
IPv4HasNoBroadcastAddressError = "IPv4 network has no broadcast address"

IPv4NetworkHasNoFirstUsableAddressError = "IPv4 network has no first usable address"
IPv6NetworkHasNoFirstUsableAddressError = "IPv6 network has no first usable address"

IPv4NetworkHasNoLastUsableAddressError = "IPv4 network has no last usable address"
IPv6NetworkHasNoLastUsableAddressError = "IPv6 network has no last usable address"
)
134 changes: 125 additions & 9 deletions pkg/core/core.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
package core

import (
"errors"
"net"

"github.com/bschaatsbergen/cidr/pkg/helper"
)

// ParseCIDR parses the given CIDR notation string and returns the corresponding IP network.
func ParseCIDR(network string) (*net.IPNet, error) {
_, ip, err := net.ParseCIDR(network)
if err != nil {
return nil, err
}
return ip, err
}

// GetAddressCount returns the number of usable addresses in the given IP network.
// It considers the network type (IPv4 or IPv6) and handles edge cases for specific prefix lengths.
// The result excludes the network address and broadcast address.
Expand All @@ -24,15 +36,6 @@ func GetAddressCount(network *net.IPNet) uint64 {
return 1<<(uint64(bits)-uint64(prefixLen)) - 2
}

// ParseCIDR parses the given CIDR notation string and returns the corresponding IP network.
func ParseCIDR(network string) (*net.IPNet, error) {
_, ip, err := net.ParseCIDR(network)
if err != nil {
return nil, err
}
return ip, err
}

// ContainsAddress checks if the given IP network contains the specified IP address.
// It returns true if the address is within the network, otherwise false.
func ContainsAddress(network *net.IPNet, ip net.IP) bool {
Expand All @@ -44,3 +47,116 @@ func ContainsAddress(network *net.IPNet, ip net.IP) bool {
func Overlaps(network1, network2 *net.IPNet) bool {
return network1.Contains(network2.IP) || network2.Contains(network1.IP)
}

// GetNetMask returns the netmask of the given IP network.
func GetNetMask(network *net.IPNet) net.IP {
netMask := net.IP(network.Mask)
return netMask
}

// GetPrefixLength returns the prefix length from the given netmask.
func GetPrefixLength(netmask net.IP) int {
ones, _ := net.IPMask(netmask).Size()
return ones
}

// GetBaseAddress returns the base address of the given IP network.
func GetBaseAddress(network *net.IPNet) net.IP {
return network.IP
}

// GetFirstUsableIPAddress returns the first usable IP address in the given IP network.
func GetFirstUsableIPAddress(network *net.IPNet) (net.IP, error) {
// If it's an IPv6 network
if network.IP.To4() == nil {
ones, bits := network.Mask.Size()
if ones == bits {
return nil, errors.New(IPv6NetworkHasNoFirstUsableAddressError)
}

// The first address is the first usable address
firstIP := make(net.IP, len(network.IP))
copy(firstIP, network.IP)

return firstIP, nil
}

// If it's an IPv4 network, first handle edge cases
switch ones, _ := network.Mask.Size(); ones {
case 32:
return nil, errors.New(IPv4NetworkHasNoFirstUsableAddressError)
case 31:
// For /31 network, the current address is the only usable address
firstIP := make(net.IP, len(network.IP))
copy(firstIP, network.IP)
return firstIP, nil
default:
// Add 1 to the network address to get the first usable address
ip := make(net.IP, len(network.IP))
copy(ip, network.IP)
ip[3]++ // Add 1 to the last octet

return ip, nil
}
}

// GetLastUsableIPAddress returns the last usable IP address in the given IP network.
func GetLastUsableIPAddress(network *net.IPNet) (net.IP, error) {
// If it's an IPv6 network
if network.IP.To4() == nil {
ones, bits := network.Mask.Size()
if ones == bits {
return nil, errors.New(IPv6NetworkHasNoLastUsableAddressError)
}

// The last address is the last usable address
lastIP := make(net.IP, len(network.IP))
copy(lastIP, network.IP)
for i := range lastIP {
lastIP[i] |= ^network.Mask[i]
}

return lastIP, nil
}

// If it's an IPv4 network, first handle edge cases
switch ones, _ := network.Mask.Size(); ones {
case 32:
return nil, errors.New(IPv4NetworkHasNoLastUsableAddressError)
case 31:
// For /31 network, the other address is the last usable address
lastIP := make(net.IP, len(network.IP))
copy(lastIP, network.IP)
lastIP[3] |= 1 // Flip the last bit to get the other address
return lastIP, nil
default:
// Subtract 1 from the broadcast address to get the last usable address
ip := make(net.IP, len(network.IP))
for i := range ip {
ip[i] = network.IP[i] | ^network.Mask[i]
}
ip[3]-- // Subtract 1 from the last octet

return ip, nil
}
}

// GetBroadcastAddress returns the broadcast address of the given IPv4 network, or an error if the IP network is IPv6.
func GetBroadcastAddress(network *net.IPNet) (net.IP, error) {
if network.IP.To4() == nil {
// IPv6 networks do not have broadcast addresses.
return nil, errors.New(IPv6HasNoBroadcastAddressError)
}

// Handle edge case for /31 and /32 networks as they have no broadcast address.
if prefixLen, _ := network.Mask.Size(); helper.ContainsInt([]int{31, 32}, prefixLen) {
return nil, errors.New(IPv4HasNoBroadcastAddressError)
}

ip := make(net.IP, len(network.IP))
for i := range ip {
ip[i] = network.IP[i] | ^network.Mask[i]
}

return ip, nil
}
Loading
Loading