Skip to content

Commit

Permalink
Merge pull request #75 from bschaatsbergen/f/add-explain-subcommand
Browse files Browse the repository at this point in the history
Add `explain` subcommand
  • Loading branch information
bschaatsbergen authored Dec 9, 2023
2 parents e163c93 + f9dce3a commit d0323f3
Show file tree
Hide file tree
Showing 10 changed files with 583 additions and 27 deletions.
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

0 comments on commit d0323f3

Please sign in to comment.