pax_global_header00006660000000000000000000000064146472235310014521gustar00rootroot0000000000000052 comment=929e667e11da8021fd182b331bcb2e713bc048a6 cidr-2.2.0/000077500000000000000000000000001464722353100124435ustar00rootroot00000000000000cidr-2.2.0/.github/000077500000000000000000000000001464722353100140035ustar00rootroot00000000000000cidr-2.2.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000004101464722353100175770ustar00rootroot00000000000000## What * Describe what changes as a result of your merged commits. ## Why * Provide the justifications for the changes ## References * Use `Closes #123`, if this PR closes a GitHub issue `#123` * If possible, link to relevant documentation to add some context. cidr-2.2.0/.github/dependabot.yaml000066400000000000000000000004221464722353100167720ustar00rootroot00000000000000# Copyright (c) Bruno Schaatsbergen # SPDX-License-Identifier: MIT version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cidr-2.2.0/.github/labeler.yaml000066400000000000000000000010251464722353100162730ustar00rootroot00000000000000# Copyright (c) Bruno Schaatsbergen # SPDX-License-Identifier: MIT github-actions: - changed-files: - any-glob-to-any-file: - '.github/**' - '.goreleaser.yaml' dependencies: - changed-files: - any-glob-to-any-file: - 'go.mod' - 'go.sum' documentation: - changed-files: - any-glob-to-any-file: - 'README.md' legal: - changed-files: - any-glob-to-any-file: - 'LICENSE' tests: - changed-files: - any-glob-to-any-file: - '**/*_test.go' cidr-2.2.0/.github/stale.yaml000066400000000000000000000013611464722353100160000ustar00rootroot00000000000000# Copyright (c) Bruno Schaatsbergen # SPDX-License-Identifier: MIT # Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Number of days of inactivity before a stale issue is closed daysUntilClose: 14 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false cidr-2.2.0/.github/workflows/000077500000000000000000000000001464722353100160405ustar00rootroot00000000000000cidr-2.2.0/.github/workflows/ci.yaml000066400000000000000000000011221464722353100173130ustar00rootroot00000000000000name: CI on: pull_request permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod cache: false - name: Build run: go build -v ./... - name: Run linters uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 with: version: latest skip-pkg-cache: true skip-build-cache: true - name: Run tests run: go test -v ./... cidr-2.2.0/.github/workflows/codeql-analysis.yaml000066400000000000000000000045231464722353100220200ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: [push, pull_request] jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 cidr-2.2.0/.github/workflows/goreleaser.yaml000066400000000000000000000010361464722353100210540ustar00rootroot00000000000000name: Release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: v1.26.2 args: release --clean env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} cidr-2.2.0/.github/workflows/triage-ci.yaml000066400000000000000000000004011464722353100205630ustar00rootroot00000000000000name: "Triage PRs" on: - pull_request_target jobs: triage: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 with: configuration-path: .github/labeler.yaml cidr-2.2.0/.github/workflows/triage.yaml000066400000000000000000000006471464722353100202060ustar00rootroot00000000000000name: Label issues on: issues: types: - reopened - opened jobs: label_issues: runs-on: ubuntu-latest permissions: issues: write steps: - run: gh issue edit "$NUMBER" --add-label "$LABELS" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} LABELS: needs-triage cidr-2.2.0/.gitignore000066400000000000000000000004611464722353100144340ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib cidr # cidr binary ./cobra # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .idea/ cidr-2.2.0/.golangci.yml000066400000000000000000000013321464722353100150260ustar00rootroot00000000000000# Copyright (c) Bruno Schaatsbergen # SPDX-License-Identifier: MIT # Visit https://golangci-lint.run/ for usage documentation and information on other useful linters issues: max-per-linter: 0 max-same-issues: 0 exclude-rules: # disable funlen for test funcs - source: "^func Test" linters: - funlen linters: disable-all: true enable: - bidichk - durationcheck - decorder - dogsled - errcheck - exportloopref - forcetypeassert - funlen - godot - godox - gofmt - gosimple - goconst - ineffassign - makezero - misspell - nilerr - predeclared - staticcheck - tenv - unconvert - unparam - unused - govet cidr-2.2.0/.goreleaser.yaml000066400000000000000000000021451464722353100155370ustar00rootroot00000000000000# Copyright (c) Bruno Schaatsbergen # SPDX-License-Identifier: MIT project_name: "cidr" # before are hooks that will be run before any builds are done, so good to put install scripts and stuff that your builds need here before: hooks: # Remove unused packaged from the build process - go mod tidy - go generate builds: - main: ./main.go binary: cidr goos: ["linux", "darwin", "windows"] goarch: ["386", "amd64", "arm64"] ldflags: - -s -w -X "github.com/bschaatsbergen/cidr/cmd.version={{.Version}}" env: - CGO_ENABLED=0 changelog: sort: "asc" filters: exclude: ["^docs:", "demo", "^hugo:", "Merge pull request", "Merge branch"] brews: - tap: owner: bschaatsbergen name: homebrew-cidr name: cidr homepage: "https://github.com/bschaatsbergen/cidr" description: "CLI to perform various actions on CIDR ranges" license: "MIT" skip_upload: auto commit_author: name: Bruno Schaatsbergen email: git@bschaatsbergen.com folder: Formula install: |- bin.install "cidr" test: | system "#{bin}/cidr -v" cidr-2.2.0/LICENSE000066400000000000000000000020641464722353100134520ustar00rootroot00000000000000MIT License Copyright (c) 2023 Bruno Schaatsbergen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cidr-2.2.0/Makefile000066400000000000000000000003671464722353100141110ustar00rootroot00000000000000default: fmt lint build install test build: go build -v ./... install: build go install -v ./... lint: golangci-lint run fmt: gofmt -s -w -e . test: go test -v -cover -timeout=120s -parallel=10 ./... .PHONY: build install lint fmt test cidr-2.2.0/README.md000066400000000000000000000055441464722353100137320ustar00rootroot00000000000000# cidr ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bschaatsbergen/cidr) [![Go Reference](https://pkg.go.dev/badge/github.com/bschaatsbergen/cidr.svg)](https://pkg.go.dev/github.com/bschaatsbergen/cidr) CLI to perform various actions on CIDR ranges ## Brew To install cidr using brew, simply run: ```sh brew install cidr ``` ## Binaries You can download the [latest binary](https://github.com/bschaatsbergen/cidr/releases/latest) for Linux, MacOS, and Windows. ## Examples Using `cidr` is very simple. ### Explain a CIDR range To get more information on a CIDR range: ``` $ cidr explain 10.0.0.0/16 Base Address: 10.0.0.0 Usable Address Range: 10.0.0.1 to 10.0.255.254 (65,534) Broadcast Address: 10.0.255.255 Addresses: 65,536 Netmask: 255.255.0.0 (/16 bits) ``` This also works with IPv6 CIDR ranges, for example: ``` $ cidr explain 2001:db8:1234:1a00::/110 Base Address: 2001:db8:1234:1a00:: Usable Address Range: 2001:db8:1234:1a00:: to 2001:db8:1234:1a00::3:ffff (262,142) Addresses: 262,144 Netmask: ffff:ffff:ffff:ffff:ffff:ffff:fffc:0 (/110 bits) ``` ### Check whether an address belongs to a CIDR range To check if a CIDR range contains an IP: ``` $ cidr contains 10.0.0.0/16 10.0.14.5 true ``` This also works with IPv6 addresses, for example: ``` $ cidr contains 2001:db8:1234:1a00::/106 2001:db8:1234:1a00:: true ``` ### Count To get a count of all addresses in a CIDR range: ``` $ cidr count 10.0.0.0/16 65536 ``` This also works with a IPv6 CIDR range, for example: ``` $ cidr count 2001:db8:1234:1a00::/106 4194304 ``` Or with a large prefix like a point-to-point link CIDR range: ``` $ cidr count 172.16.18.0/31 2 ``` ### CIDR range intersection To check if a CIDR range overlaps with another CIDR range: ``` $ cidr overlaps 10.0.0.0/16 10.0.14.0/22 true ``` This also works with IPv6 CIDR ranges, for example: ``` $ cidr overlaps 2001:db8:1111:2222:1::/80 2001:db8:1111:2222:1:1::/96 true ``` ### CIDR division To divide a CIDR range into N distinct networks: ``` $ cidr divide 10.0.0.0/16 9 10.0.0.0/20 10.0.16.0/20 10.0.32.0/20 10.0.48.0/20 10.0.64.0/20 10.0.80.0/20 10.0.96.0/20 10.0.112.0/20 10.0.128.0/20 ``` This also works with IPv6 CIDR ranges, for example: ``` $ cidr divide 2001:db8:1111:2222:1::/80 9 2001:db8:1111:2222:1::/84 2001:db8:1111:2222:1:1000::/84 2001:db8:1111:2222:1:2000::/84 2001:db8:1111:2222:1:3000::/84 2001:db8:1111:2222:1:4000::/84 2001:db8:1111:2222:1:5000::/84 2001:db8:1111:2222:1:6000::/84 2001:db8:1111:2222:1:7000::/84 2001:db8:1111:2222:1:8000::/84 ``` ## Contributing Contributions are highly appreciated and always welcome. Have a look through existing [Issues](https://github.com/bschaatsbergen/cidr/issues) and [Pull Requests](https://github.com/bschaatsbergen/cidr/pulls) that you could help with. cidr-2.2.0/cmd/000077500000000000000000000000001464722353100132065ustar00rootroot00000000000000cidr-2.2.0/cmd/contains.go000066400000000000000000000026051464722353100153560ustar00rootroot00000000000000package cmd import ( "fmt" "net" "os" "github.com/bschaatsbergen/cidr/pkg/core" "github.com/spf13/cobra" ) const ( containsExample = "# Check whether an IPv4 CIDR range contains a given IPv4 address\n" + "cidr contains 10.0.0.0/16 10.0.14.5\n" + "\n" + "# Check whether an IPv6 CIDR range contains a given IPv6 address\n" + "cidr contains 2001:db8:1234:1a00::/106 2001:db8:1234:1a00::" ) var ( containsCmd = &cobra.Command{ Use: "contains", Short: "Checks whether an IP address belongs to a CIDR range", Example: containsExample, Run: func(cmd *cobra.Command, args []string) { if len(args) != 2 { 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) } ip := net.ParseIP(args[1]) if ip == nil { fmt.Printf("error: invalid IP address: %s\n", args[1]) fmt.Println("See 'cidr contains -h' for help and examples") os.Exit(1) } networkContainsIP := contains(network, ip) fmt.Println(networkContainsIP) }, } ) func init() { rootCmd.AddCommand(containsCmd) } func contains(network *net.IPNet, ip net.IP) bool { contains := core.ContainsAddress(network, ip) return contains } cidr-2.2.0/cmd/count.go000066400000000000000000000022111464722353100146610ustar00rootroot00000000000000package cmd import ( "fmt" "math/big" "net" "os" "github.com/bschaatsbergen/cidr/pkg/core" "github.com/spf13/cobra" ) const ( countExample = "# Return the count of all addresses within a given IPv4 CIDR range\n" + "cidr count 10.0.0.0/16\n" + "\n" + "# Return the count of all addresses within a given IPv6 CIDR range\n" + "cidr count 2001:db8:1234:1a00::/106" ) var ( countCmd = &cobra.Command{ Use: "count", Short: "Return the count of all addresses in a given CIDR range", Example: countExample, Run: func(cmd *cobra.Command, args []string) { if len(args) != 1 { fmt.Println("error: provide a CIDR range") fmt.Println("See 'cidr count -h' for help and examples") os.Exit(1) } network, err := core.ParseCIDR(args[0]) if err != nil { fmt.Printf("error: invalid CIDR range: %s\n", args[0]) fmt.Println("See 'cidr count -h' for help and examples") os.Exit(1) } hostAddressCount := count(network) fmt.Println(hostAddressCount) }, } ) func init() { rootCmd.AddCommand(countCmd) } func count(network *net.IPNet) *big.Int { count := core.GetAddressCount(network) return count } cidr-2.2.0/cmd/divide.go000066400000000000000000000043411464722353100150030ustar00rootroot00000000000000package cmd import ( "fmt" "net" "strconv" "github.com/bschaatsbergen/cidr/pkg/core" "github.com/bschaatsbergen/cidr/pkg/helper" "github.com/fatih/color" "github.com/spf13/cobra" ) const ( divideExample = "# Divides the given CIDR range into N distinct networks *Truncates output to 50\n" + "$ cidr divide 10.0.0.0/16 9\n" + "10.0.0.0/20\n" + "10.0.16.0/20\n" + "10.0.32.0/20\n" + "10.0.48.0/20\n" + "10.0.64.0/20\n" + "10.0.80.0/20\n" + "10.0.96.0/20\n" + "10.0.112.0/20\n" + "10.0.128.0/20\n" ) var divideCmd = &cobra.Command{ Use: "divide", Short: "Divides the given CIDR range into N distinct networks", Args: cobra.MinimumNArgs(2), Example: divideExample, PreRunE: validateDivideArguments, RunE: executeDivide, } func init() { rootCmd.AddCommand(divideCmd) } func validateDivideArguments(cmd *cobra.Command, args []string) error { // Ensure CIDR is valid _, _, err := net.ParseCIDR(args[0]) if err != nil { return fmt.Errorf("invalid network: %s", args[0]) } // Ensure divisor is a valid integer _, err = strconv.ParseInt(args[1], 10, 64) if err != nil { return fmt.Errorf("invalid divisor: %s", args[1]) } return nil } func executeDivide(cmd *cobra.Command, args []string) error { network, err := core.ParseCIDR(args[0]) if err != nil { return fmt.Errorf("invalid network: %s", args[0]) } maskSize, _ := network.Mask.Size() if (helper.IsIPv4Network(network) && maskSize == 32) || maskSize >= 128 { return fmt.Errorf("invalid network mask size: %s", args[0]) } divisor, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return fmt.Errorf("invalid divisor: %s", args[1]) } networks, err := core.DivideCIDR(network, divisor) if err != nil { return err } printNetworkPartitions(networks) return nil } func printNetworkPartitions(networks []net.IPNet) { const truncateLimit = 50 networkCount := len(networks) if networkCount <= truncateLimit { for _, network := range networks { fmt.Println(network.String()) } } else { for i := 0; i < truncateLimit/2; i++ { fmt.Println(networks[i].String()) } fmt.Println(color.BlueString("......")) for i := networkCount - truncateLimit/2; i < networkCount; i++ { fmt.Println(networks[i].String()) } } } cidr-2.2.0/cmd/explain.go000066400000000000000000000114271464722353100152020ustar00rootroot00000000000000package 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" ) const ( explainExample = "# Explain the details of a given IPv4 CIDR range\n" + "cidr explain 10.1.0.0/16\n" + "\n" + "# Explain the details of a given IPv6 CIDR range\n" + "cidr explain 2001:db8:1234:1a00::/106" ) var ( explainCmd = &cobra.Command{ Use: "explain", Short: "Provides information about a CIDR range", Example: explainExample, Run: func(cmd *cobra.Command, args []string) { if len(args) != 1 { fmt.Println("error: provide a CIDR range") 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 HostCount 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. netmask := core.GetNetmask(network) // A human-readable representation of the netmask is displayed in the output. details.Netmask = core.NetMaskToIPAddress(netmask) 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 = helper.FormatNumber(count.String()) // Obtain the total count of distinct host addresses in the network. hostCount := core.GetHostAddressCount(network) // Format the count as a human-readable string and store it in the details struct. details.HostCount = helper.FormatNumber(hostCount.String()) // 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 (%s)\n", details.FirstUsableIPAddress, details.LastUsableIPAddress, details.HostCount) } 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("Addresses:\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) } cidr-2.2.0/cmd/overlaps.go000066400000000000000000000024631464722353100153750ustar00rootroot00000000000000package cmd import ( "fmt" "net" "os" "github.com/bschaatsbergen/cidr/pkg/core" "github.com/spf13/cobra" ) const ( overlapsExample = "# Check whether 2 IPv4 CIDR ranges overlap\n" + "cidr overlaps 10.0.0.0/16 10.0.14.0/22\n" + "\n" + "# Check whether 2 IPv6 CIDR ranges overlap\n" + "cidr overlaps 2001:db8:1111:2222:1::/80 2001:db8:1111:2222:1:1::/96" ) var overlapsCmd = &cobra.Command{ Use: "overlaps", Short: "Checks if a CIDR range overlaps with another CIDR range", Example: overlapsExample, Run: func(cmd *cobra.Command, args []string) { if len(args) != 2 { fmt.Println("error: provide 2 CIDR ranges") fmt.Println("See 'cidr overlaps -h' for help and examples") os.Exit(1) } network1, err := core.ParseCIDR(args[0]) if err != nil { fmt.Printf("error: %s\n", err) fmt.Println("See 'cidr overlaps -h' for help and examples") os.Exit(1) } network2, err := core.ParseCIDR(args[1]) if err != nil { fmt.Printf("error: %s\n", err) fmt.Println("See 'cidr overlaps -h' for help and examples") os.Exit(1) } networksOverlap := overlaps(network1, network2) fmt.Println(networksOverlap) }, } func init() { rootCmd.AddCommand(overlapsCmd) } func overlaps(network1, network2 *net.IPNet) bool { overlaps := core.Overlaps(network1, network2) return overlaps } cidr-2.2.0/cmd/root.go000066400000000000000000000021471464722353100145240ustar00rootroot00000000000000package cmd import ( "fmt" "os" "strings" "github.com/fatih/color" "github.com/spf13/cobra" ) var ( version string rootCmd = &cobra.Command{ Use: "cidr", Short: "cidr - CLI to perform various actions on CIDR ranges", Version: version, // The version is set during the build by making using of `go build -ldflags`. Run: func(cmd *cobra.Command, args []string) { err := cmd.Help() if err != nil { fmt.Println(err) os.Exit(1) } }, } ) func setupCobraUsageTemplate() { cobra.AddTemplateFunc("StyleHeading", color.New(color.FgBlue).SprintFunc()) usageTemplate := rootCmd.UsageTemplate() usageTemplate = strings.NewReplacer( `Usage:`, `{{StyleHeading "Usage:"}}`, `Examples:`, `{{StyleHeading "Examples:"}}`, `Available Commands:`, `{{StyleHeading "Available Commands:"}}`, `Flags:`, `{{StyleHeading "Flags:"}}`, ).Replace(usageTemplate) rootCmd.SetUsageTemplate(usageTemplate) } func init() { setupCobraUsageTemplate() rootCmd.CompletionOptions.DisableDefaultCmd = true } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } cidr-2.2.0/go.mod000066400000000000000000000010561464722353100135530ustar00rootroot00000000000000module github.com/bschaatsbergen/cidr go 1.21.4 require ( github.com/fatih/color v1.17.0 github.com/spf13/cobra v1.8.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 ) cidr-2.2.0/go.sum000066400000000000000000000047501464722353100136040ustar00rootroot00000000000000github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= cidr-2.2.0/main.go000066400000000000000000000001321464722353100137120ustar00rootroot00000000000000package main import "github.com/bschaatsbergen/cidr/cmd" func main() { cmd.Execute() } cidr-2.2.0/pkg/000077500000000000000000000000001464722353100132245ustar00rootroot00000000000000cidr-2.2.0/pkg/core/000077500000000000000000000000001464722353100141545ustar00rootroot00000000000000cidr-2.2.0/pkg/core/const.go000066400000000000000000000010041464722353100156240ustar00rootroot00000000000000package 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" ) cidr-2.2.0/pkg/core/core.go000066400000000000000000000171611464722353100154410ustar00rootroot00000000000000package core import ( "errors" "fmt" "math/big" "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 total number of addresses in the given IP network. // It accounts for both IPv4 and IPv6 networks, and handles specific cases for certain prefix lengths. func GetAddressCount(network *net.IPNet) *big.Int { prefixLen, bits := network.Mask.Size() // Handle specific cases for IPv4 prefix lengths. if network.IP.To4() != nil { switch prefixLen { case 32: // A /32 prefix contains a single address. return big.NewInt(1) case 31: // A /31 prefix is used for point-to-point links and contains two addresses. return big.NewInt(2) } } // Calculate the total number of addresses based on the prefix length. return new(big.Int).Lsh(big.NewInt(1), uint(bits-prefixLen)) } // GetHostAddressCount returns the number of distinct host 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 the broadcast address, if applicable. func GetHostAddressCount(network *net.IPNet) *big.Int { prefixLen, bits := network.Mask.Size() // Handle edge cases for specific IPv4 prefix lengths. if network.IP.To4() != nil { switch prefixLen { case 32: // Single IP address for /32 (e.g., point-to-point link). return big.NewInt(1) case 31: // Two IP addresses for /31 (point-to-point link). return big.NewInt(2) } } // Calculate the total number of addresses and subtract 2 (network and broadcast addresses). totalAddresses := new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(bits-prefixLen)), nil) return totalAddresses.Sub(totalAddresses, big.NewInt(2)) } // 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 { return network.Contains(ip) } // Overlaps checks if there is an overlap between two IP networks. // It returns true if there is any overlap, otherwise false. func Overlaps(network1, network2 *net.IPNet) bool { return network1.Contains(network2.IP) || network2.Contains(network1.IP) } // GetNetmask retrieves the netmask associated with the provided IP network. func GetNetmask(network *net.IPNet) net.IPMask { return network.Mask } // NetMaskToIPAddress converts a netmask represented as a sequence of bytes // to its corresponding IP address representation. func NetMaskToIPAddress(netmask net.IPMask) net.IP { return net.IP(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 } // GetMaskWithDivisor calculates the subnet mask for the given divisor and address count. func GetMaskWithDivisor(divisor int64, addressCount *big.Int, IPv4 bool) (net.IPMask, error) { div := big.NewInt(divisor) if addressCount.Cmp(div) == -1 || div.Cmp(big.NewInt(0)) == 0 { return nil, fmt.Errorf("cannot divide %d addresses into %d divisions", addressCount, div) } addressPartition := new(big.Int).Div(addressCount, div) two := big.NewInt(2) exponent := big.NewInt(0) for two.Cmp(addressPartition) <= 0 { two.Lsh(two, 1) exponent.Add(exponent, big.NewInt(1)) } subnetPrefix := int(exponent.Int64()) bits := net.IPv6len * 8 if IPv4 { bits = net.IPv4len * 8 if subnetPrefix > 30 { return nil, fmt.Errorf("address Space is insufficient for %d subnets", div) } } if subnetPrefix > 126 { return nil, fmt.Errorf("address Space is insufficient for %d subnets", div) } return net.CIDRMask(bits-subnetPrefix, bits), nil } // DivideCIDR divides the given IP network into the specified number of subnets. func DivideCIDR(network *net.IPNet, divisor int64) ([]net.IPNet, error) { isIPv4 := helper.IsIPv4Network(network) addressCount := GetAddressCount(network) newSubnetMask, err := GetMaskWithDivisor(divisor, addressCount, isIPv4) if err != nil { return nil, fmt.Errorf("%s", err) } networks := make([]net.IPNet, divisor) nextAddress := new(net.IPNet) nextAddress.IP = network.IP nextAddress.Mask = newSubnetMask subnetSize := GetAddressCount(nextAddress) for i := int64(0); i < divisor; i++ { networks[i] = *nextAddress ipAsInt := new(big.Int).SetBytes(nextAddress.IP) nextAddress.IP = new(big.Int).Add(ipAsInt, subnetSize).Bytes() } return networks, nil } cidr-2.2.0/pkg/core/core_test.go000066400000000000000000000314651464722353100165030ustar00rootroot00000000000000package core_test import ( "math/big" "net" "testing" "github.com/bschaatsbergen/cidr/pkg/core" "github.com/stretchr/testify/assert" ) func TestGetAddressCount(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } largeIPv4PrefixCIDR, err := core.ParseCIDR("172.16.18.0/31") if err != nil { t.Log(err) t.Fail() } largestIPv4PrefixCIDR, err := core.ParseCIDR("172.16.18.0/32") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string cidr *net.IPNet expectedCount *big.Int }{ { name: "Return the count of all addresses in a common IPv4 CIDR", cidr: IPv4CIDR, expectedCount: big.NewInt(65536), }, { name: "Return the count of all addresses in a common IPv6 CIDR", cidr: IPv6CIDR, expectedCount: big.NewInt(4194304), }, { name: "Return the count of all addresses in an uncommon (large prefix) IPv4 CIDR", cidr: largeIPv4PrefixCIDR, expectedCount: big.NewInt(2), }, { name: "Return the count of all addresses in an uncommon (largest prefix) IPv4 CIDR", cidr: largestIPv4PrefixCIDR, expectedCount: big.NewInt(1), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { count := core.GetAddressCount(tt.cidr) assert.Equal(t, tt.expectedCount, count, "Both address counts should be equal") }) } } func TestOverlaps(t *testing.T) { firstIPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } secondIPv4CIDR, err := core.ParseCIDR("10.0.14.0/22") if err != nil { t.Log(err) t.Fail() } thirdIPv4CIDR, err := core.ParseCIDR("10.1.0.0/28") if err != nil { t.Log(err) t.Fail() } firstIPv6CIDR, err := core.ParseCIDR("2001:db8:1111:2222:1::/80") if err != nil { t.Log(err) t.Fail() } secondIPv6CIDR, err := core.ParseCIDR("2001:db8:1111:2222:1:1::/96") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string cidrA *net.IPNet cidrB *net.IPNet overlaps bool }{ { name: "2 IPv4 CIDR ranges should overlap", cidrA: firstIPv4CIDR, cidrB: secondIPv4CIDR, overlaps: true, }, { name: "2 IPv4 CIDR ranges should NOT overlap", cidrA: firstIPv4CIDR, cidrB: thirdIPv4CIDR, overlaps: false, }, { name: "2 IPv6 CIDR ranges should overlap", cidrA: firstIPv6CIDR, cidrB: secondIPv6CIDR, overlaps: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { overlaps := core.Overlaps(tt.cidrA, tt.cidrB) assert.Equal(t, tt.overlaps, overlaps, "Given CIDRs should overlap") }) } } func TestContainsAddress(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string cidr *net.IPNet ip net.IP contains bool }{ { name: "IPv4 CIDR that does contain an IPv4 IP", cidr: IPv4CIDR, ip: net.ParseIP("10.0.14.5"), contains: true, }, { name: "IPv4 CIDR that does NOT contain an IPv4 IP", cidr: IPv4CIDR, ip: net.ParseIP("10.1.55.5"), contains: false, }, { name: "IPv6 CIDR that does contain an IPv6 IP", cidr: IPv6CIDR, ip: net.ParseIP("2001:db8:1234:1a00::"), contains: true, }, { name: "IPv6 CIDR that does NOT contain an IPv6 IP", cidr: IPv6CIDR, ip: net.ParseIP("2001:af1:1222:1a20::"), contains: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { overlaps := core.ContainsAddress(tt.cidr, tt.ip) assert.Equal(t, tt.contains, overlaps, "Given IP address should be part of the given CIDR") }) } } func TestGetPrefixLength(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string netmask net.IP expectedPrefixLength int }{ { name: "Get the prefix length of an IPv4 netmask", netmask: net.IP(IPv4CIDR.Mask), expectedPrefixLength: 16, }, { name: "Get the prefix length of an IPv6 netmask", netmask: net.IP(IPv6CIDR.Mask), expectedPrefixLength: 106, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { prefixLength := core.GetPrefixLength(tt.netmask) assert.Equal(t, tt.expectedPrefixLength, prefixLength, "Prefix length is not correct") }) } } func TestGetNetMask(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string CIDR *net.IPNet expectedNetmask net.IPMask }{ { name: "Get the netmask of an IPv4 CIDR range", CIDR: IPv4CIDR, expectedNetmask: net.IPMask{0xff, 0xff, 0x00, 0x00}, }, { name: "Get the netmask of an IPv6 CIDR range", CIDR: IPv6CIDR, expectedNetmask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0, 0x0}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { netmask := core.GetNetmask(tt.CIDR) if err != nil { t.Log(err) t.Fail() } assert.Equal(t, tt.expectedNetmask, netmask, "Netmask is not correct") }) } } func TestGetFirstUsableIPAddress(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string CIDR *net.IPNet expectedFirstUsableIPAddress net.IP }{ { name: "Get the first usable IP address of an IPv4 CIDR range", CIDR: IPv4CIDR, expectedFirstUsableIPAddress: net.ParseIP("10.0.0.1").To4(), }, { name: "Get the first usable IP address of an IPv6 CIDR range", CIDR: IPv6CIDR, expectedFirstUsableIPAddress: net.ParseIP("2001:db8:1234:1a00::").To16(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { firstUsableIPAddress, err := core.GetFirstUsableIPAddress(tt.CIDR) if err != nil { t.Log(err) t.Fail() } assert.Equal(t, tt.expectedFirstUsableIPAddress, firstUsableIPAddress, "First usable IP address is not correct") }) } } func TestGetLastUsableIPAddress(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string CIDR *net.IPNet expectedLastUsableIPAddress net.IP }{ { name: "Get the last usable IP address of an IPv4 CIDR range", CIDR: IPv4CIDR, expectedLastUsableIPAddress: net.ParseIP("10.0.255.254").To4(), }, { name: "Get the last usable IP address of an IPv6 CIDR range", CIDR: IPv6CIDR, expectedLastUsableIPAddress: net.ParseIP("2001:db8:1234:1a00::3f:ffff").To16(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lastUsableIPAddress, err := core.GetLastUsableIPAddress(tt.CIDR) if err != nil { t.Log(err) t.Fail() } assert.Equal(t, tt.expectedLastUsableIPAddress, lastUsableIPAddress, "Last usable IP address is not correct") }) } } func TestGetBroadcastAddress(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("10.0.0.0/16") if err != nil { t.Log(err) t.Fail() } IPv4CIDRWithNoBroadcastAddress, err := core.ParseCIDR("10.0.0.0/31") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("2001:db8:1234:1a00::/106") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string CIDR *net.IPNet expectedBroadcastAddress net.IP wantErr bool }{ { name: "Get the broadcast IP address of an IPv4 CIDR range", CIDR: IPv4CIDR, expectedBroadcastAddress: net.ParseIP("10.0.255.255").To4(), wantErr: false, }, { name: "Get the broadcast IP address of an IPv4 CIDR range that has no broadcast address", CIDR: IPv4CIDRWithNoBroadcastAddress, expectedBroadcastAddress: nil, wantErr: true, }, { name: "Get the broadcast IP address of an IPv6 CIDR range", CIDR: IPv6CIDR, expectedBroadcastAddress: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { broadcastAddress, err := core.GetBroadcastAddress(tt.CIDR) if err != nil { assert.Equal(t, tt.wantErr, true, "Expected error when getting broadcast address, but got none") } else { assert.Equal(t, tt.expectedBroadcastAddress, broadcastAddress, "Broadcast IP address is not correct") } }) } } func TestGetBaseAddress(t *testing.T) { IPv4CIDR, err := core.ParseCIDR("192.168.90.4/30") if err != nil { t.Log(err) t.Fail() } IPv6CIDR, err := core.ParseCIDR("4a00:db8:1234:1a00::/127") if err != nil { t.Log(err) t.Fail() } tests := []struct { name string cidr *net.IPNet expectedBaseAddress net.IP }{ { name: "Get the base address of an IPv4 CIDR", cidr: IPv4CIDR, expectedBaseAddress: IPv4CIDR.IP, }, { name: "Get the base address of an IPv6 CIDR", cidr: IPv6CIDR, expectedBaseAddress: IPv6CIDR.IP, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { baseAddress := core.GetBaseAddress(tt.cidr) assert.Equal(t, tt.expectedBaseAddress, baseAddress, "Base address is not correct") }) } } func TestParseCIDR(t *testing.T) { tests := []struct { name string cidrStr string wantErr bool }{ { name: "Parse a valid IPv4 CIDR", cidrStr: "10.0.0.0/16", wantErr: false, }, { name: "Parse a valid IPv6 CIDR", cidrStr: "2001:db8:1234:1a00::/106", wantErr: false, }, { name: "Parse an invalid IPv4 CIDR", cidrStr: "356.356.356.356/16", wantErr: true, }, { name: "Parse an invalid IPv6 CIDR", cidrStr: "2001:db8:1234:1a00::/129", wantErr: true, }, { name: "Parse an empty CIDR", cidrStr: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := core.ParseCIDR(tt.cidrStr) if (err != nil) != tt.wantErr { t.Errorf("ParseCIDR() error = %v, wantErr %v", err, tt.wantErr) return } }) } } func TestDivideCIDR(t *testing.T) { tests := []struct { name string cidr string divisor int64 expected []string shouldErr bool }{ { name: "Divide IPv4 CIDR into 2 subnets", cidr: "10.0.0.0/16", divisor: 2, expected: []string{"10.0.0.0/17", "10.0.128.0/17"}, shouldErr: false, }, { name: "Divide IPv4 CIDR into 4 subnets", cidr: "192.168.0.0/24", divisor: 4, expected: []string{"192.168.0.0/26", "192.168.0.64/26", "192.168.0.128/26", "192.168.0.192/26"}, shouldErr: false, }, { name: "Divide IPv6 CIDR into 3 subnets", cidr: "2001:db8::/32", divisor: 3, expected: []string{"2001:db8::/34", "2001:db8:4000::/34", "2001:db8:8000::/34"}, shouldErr: false, }, { name: "Error case: Divisor is zero", cidr: "10.0.0.0/16", divisor: 0, expected: nil, shouldErr: true, }, { name: "Error case: Cannot divide /128 CIDR", cidr: "2001:db8::/128", divisor: 3, expected: nil, shouldErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, ipNet, err := net.ParseCIDR(tt.cidr) assert.NoError(t, err, "Unexpected error parsing CIDR: %v", err) subnets, err := core.DivideCIDR(ipNet, tt.divisor) if tt.shouldErr { assert.Error(t, err, "Expected error but got none") return } assert.NoError(t, err, "Unexpected error: %v", err) assert.Equal(t, len(tt.expected), len(subnets), "Incorrect number of subnets") for i, expectedCIDR := range tt.expected { assert.Equal(t, expectedCIDR, subnets[i].String(), "Incorrect subnet at index %d", i) } }) } } cidr-2.2.0/pkg/helper/000077500000000000000000000000001464722353100145035ustar00rootroot00000000000000cidr-2.2.0/pkg/helper/contains.go000066400000000000000000000004741464722353100166550ustar00rootroot00000000000000package helper // ContainsInt checks if the given slice of integers contains the specified integer. // It returns true if the integer is within the slice, otherwise false. func ContainsInt(ints []int, specifiedInt int) bool { for _, i := range ints { if i == specifiedInt { return true } } return false } cidr-2.2.0/pkg/helper/contains_test.go000066400000000000000000000014211464722353100177050ustar00rootroot00000000000000package helper_test import ( "testing" "github.com/bschaatsbergen/cidr/pkg/helper" ) func TestContainsInt(t *testing.T) { type args struct { ints []int specifiedInt int } tests := []struct { name string args args want bool }{ { name: "ContainsInt() should return true", args: args{ ints: []int{1, 2, 3, 4, 5}, specifiedInt: 3, }, want: true, }, { name: "ContainsInt() should return false", args: args{ ints: []int{1, 2, 3, 4, 5}, specifiedInt: 6, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := helper.ContainsInt(tt.args.ints, tt.args.specifiedInt); got != tt.want { t.Errorf("ContainsInt() = %v, want %v", got, tt.want) } }) } } cidr-2.2.0/pkg/helper/formatter.go000066400000000000000000000007351464722353100170420ustar00rootroot00000000000000package helper // Takes a numbered string [123456789]. // Outputs -> [123,456,789]. func FormatNumber(s string) string { length := len(s) if length == 0 { return "" } newLength := length + (length-1)/3 newNumber := make([]rune, newLength) commas := 0 for i, c := range s { if c < '0' || c > '9' { return "" } if (length-i)%3 == 0 && i != 0 { newNumber[i+commas] = ',' commas += 1 } newNumber[i+commas] = c } return string(newNumber) } cidr-2.2.0/pkg/helper/formatter_test.go000066400000000000000000000014371464722353100201010ustar00rootroot00000000000000package helper_test import ( "testing" "github.com/bschaatsbergen/cidr/pkg/helper" ) func TestFormatNumber(t *testing.T) { testCases := []struct { input string expected string }{ {"1234", "1,234"}, {"12345", "12,345"}, {"123456", "123,456"}, {"1234567", "1,234,567"}, {"12345678", "12,345,678"}, {"123456789", "123,456,789"}, {"1234567890", "1,234,567,890"}, {"", ""}, // Empty input {"abc", ""}, // Non-numeric input {"1.23", ""}, // Decimal input {"-1234", ""}, // Negative input } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { result := helper.FormatNumber(tc.input) if result != tc.expected { t.Errorf("For input '%s', expected '%s' but got '%s'", tc.input, tc.expected, result) } }) } } cidr-2.2.0/pkg/helper/network.go000066400000000000000000000007201464722353100165220ustar00rootroot00000000000000package helper import "net" // isIPv4Network checks if the given network is an IPv4 network. // It returns true if the network is an IPv4 network, otherwise false. func IsIPv4Network(network *net.IPNet) bool { return network.IP.To4() != nil } // isIPv6Network checks if the given network is an IPv6 network. // It returns true if the network is an IPv6 network, otherwise false. func IsIPv6Network(network *net.IPNet) bool { return network.IP.To16() != nil }