Convert arbitrary IP range to a set of CIDR


Problem

I found myself trying to figure out the least amount of rules that would cover a certain IP range (that doesn’t exactly fit CIDR bounds) to find it annoying.

For example, 10.0.0.210.0.0.7 is 10.0.0.2/31 and 10.0.0.4/30.

Solution

I wrote a stupidly simple script to automate this, also downloadable as plaintext:

#!/usr/bin/ruby

# =========================================================================
# Converts arbitrary ip range (from..to) to set of CIDR network addresses
# -------------------------------------------------------------------------
#
# Examples:
#
# $ ./cidr.rb 10.0.0.1 10.0.1.33
# 10.0.0.1/32
# 10.0.0.2/31
# 10.0.0.4/30
# 10.0.0.8/29
# 10.0.0.16/28
# 10.0.0.32/27
# 10.0.0.64/26
# 10.0.0.128/25
# 10.0.1.0/27
# 10.0.1.32/31
#
# $ ./cidr.rb 10.0.0.2 10.0.0.7
# 10.0.0.2/31
# 10.0.0.4/30
#
# -------------------------------------------------------------------------
# Author: Michal Safranek (wejn at box dot cz)
# License: MIT
# Date: 20110214004000
# =========================================================================

module CIDRCompress
  def inet_ntoa(n)
    [n].pack("N").unpack("C*").join "."
  end

  def inet_aton(ip)
    return ip.to_i if /^\d+$/ =~ ip
    raise ArgumentError, "invalid IP: #{ip}" unless /^(\d+\.){3}\d+$/ =~ ip
    ip.split(/\./).map{|c| c.to_i}.pack("C*").unpack("N").first
  end

  def ranges_for(f, l, &b)
    # adapted from: http://phix.me/geodns/#script
    # as my previous version was much less elegant

    raise LocalJumpError, "no block given" unless block_given?

    # auto-convert IPs
    f = inet_aton(f) unless f.kind_of?(Integer)
    l = inet_aton(l) unless l.kind_of?(Integer)

    # flip first,last if first > last
    f, l = l, f if f > l

    # do the magic
    log = (Math.log(l-f+1)/Math.log(2)).to_i # log10(x)/log10(2) == log2(x)
    mask = 2**32 - 2**log

    if f&mask == l&mask
      b.call(inet_ntoa(f), 32-log)
    else
      ranges_for(f, (l&mask)-1, &b)
      ranges_for(l&mask, l, &b)
    end
  end

  # make it both includable and directly callable
  class <<self; include CIDRCompress; end
end

if $0 == __FILE__
  unless ARGV.size == 2
    STDERR.puts "Usage: #{File.basename($0)} <first IP> <last IP>"
    exit 1
  end

  CIDRCompress.ranges_for(ARGV.first, ARGV.last) do |a,m|
    puts [a,m].join('/')
  end
end