# frozen_string_literal: false
# Copyright (C) 2009-2010 Brian Candler
# Licensed under the same terms as ruby. See LICENCE.txt and COPYING.txt
# The IP Class is the base class for the module
class IP
PROTO_TO_CLASS = {}
class << self
alias orig_new new
# Examples:
# IP.new("1.2.3.4")
# IP.new("1.2.3.4/28")
# IP.new("1.2.3.4/28@routing_context")
#
# Array form (inverse of to_a and to_ah):
# IP.new(["v4", 0x01020304])
# IP.new(["v4", 0x01020304, 28])
# IP.new(["v4", 0x01020304, 28, "routing_context"])
# IP.new(["v4", "01020304", 28, "routing_context"])
#
# Note that this returns an instance of IP::V4 or IP::V6. IP is the
# base class of both of those, but cannot be instantiated itself.
def new(src)
case src
when String
parse(src) || (raise ArgumentError, 'invalid address')
when Array
(PROTO_TO_CLASS[src[0]] ||
(raise ArgumentError, 'invalid protocol')).new(*src[1..-1])
when IP
src.dup
else
raise ArgumentError, 'invalid address'
end
end
# Parse a string as an IP address - return a V4/V6 object or nil
def parse(str)
V4.parse(str) || V6.parse(str)
end
end
# Length of prefix (network portion) of address
attr_reader :pfxlen
# Routing Context indicates the scope of this address (e.g. virtual router)
attr_accessor :ctx
# Examples:
# IP::V4.new(0x01020304)
# IP::V4.new("01020304")
# IP::V4.new(0x01020304, 28)
# IP::V4.new(0x01020304, 28, "routing_context")
def initialize(addr, pfxlen = nil, ctx = nil)
@addr = addr.is_a?(String) ? addr.to_i(16) : addr.to_i
raise ArgumentError, 'Invalid address value' if @addr < 0 || @addr > self.class::MASK
self.pfxlen = pfxlen
self.ctx = ctx
end
# Return the protocol in string form, "v4" or "v6"
def proto
self.class::PROTO
end
# Creates a new ip containing the given network byte ordered
# string form of an IP address.
def self.new_ntoh(addr)
IP.new(IP.ntop(addr))
end
# Convert a network byte ordered string form of an IP address into
# human readable form.
def self.ntop(addr)
case addr.size
when 4
s = addr.unpack('C4').join('.')
when 16
s = (['%.4x'] * 8).join(':') % addr.unpack('n8')
else
raise ArgumentError, 'Invalid address value'
end
s
end
# Return the string representation of the address, x.x.x.x[/pfxlen][@ctx]
def to_s
ctx ? "#{to_addrlen}@#{ctx}" : to_addrlen
end
# Return the string representation of the IP address and prefix, or
# just the IP address if it's a single address
def to_addrlen
pfxlen == self.class::ADDR_BITS ? to_addr : "#{to_addr}/#{pfxlen}"
end
# Return the address as an Integer
def to_i
@addr
end
# returns the address in Binary
def to_b
@addr.to_s(2).to_i
end
# Return the address as a hexadecimal string (8 or 32 digits)
def to_hex
@addr.to_s(16).rjust(self.class::ADDR_BITS >> 2, '0')
end
# Return an array representation of the address, with 3 or 4 elements
# depending on whether there is a routing context set.
# ["v4", 16909060, 28]
# ["v4", 16909060, 28, "context"]
# (Removing the last element makes them Comparable, as nil.<=> doesn't exist)
def to_a
if @ctx
[self.class::PROTO, @addr, @pfxlen, @ctx]
else
[self.class::PROTO, @addr, @pfxlen]
end
end
# Return an array representation of the address, with 3 or 4 elements
# depending on whether there is a routing context set, using hexadecimal.
# ["v4", "01020304", 28]
# ["v4", "01020304", 28, "context"]
def to_ah
if @ctx
[self.class::PROTO, to_hex, @pfxlen, @ctx]
else
[self.class::PROTO, to_hex, @pfxlen]
end
end
# Change the prefix length. If nil, the maximum is used (32 or 128)
def pfxlen=(pfxlen)
@mask = nil
if pfxlen
pfxlen = pfxlen.to_i
raise ArgumentError, 'Invalid prefix length' if pfxlen < 0 || pfxlen > self.class::ADDR_BITS
@pfxlen = pfxlen
else
@pfxlen = self.class::ADDR_BITS
end
end
# Return the mask for this pfxlen as an integer. For example,
# a V4 /24 address has a mask of 255 (0x000000ff)
def mask
@mask ||= (1 << (self.class::ADDR_BITS - @pfxlen)) - 1
end
# Return a new IP object at the base of the subnet, with an optional
# offset applied.
# IP.new("1.2.3.4/24").network => #
# IP.new("1.2.3.4/24").network(7) => #
def network(offset = 0)
self.class.new((@addr & ~mask) + offset, @pfxlen, @ctx)
end
# Return a new IP object at the top of the subnet, with an optional
# offset applied.
# IP.new("1.2.3.4/24").broadcast => #
# IP.new("1.2.3.4/24").broadcast(-1) => #
def broadcast(offset = 0)
self.class.new((@addr | mask) + offset, @pfxlen, @ctx)
end
# Return a new IP object representing the netmask
# IP.new("1.2.3.4/24").netmask => #
def netmask
self.class.new(self.class::MASK & ~mask)
end
# Return a new IP object representing the wildmask (inverse netmask)
# IP.new("1.2.3.4/24").netmask => #
def wildmask
self.class.new(mask)
end
# Masks the address such that it is the base of the subnet
# IP.new("1.2.3.4/24").mask! => #
def mask!
@addr &= ~mask
self
end
# Returns true if this is not the base address of the subnet implied
# from the prefix length (e.g. 1.2.3.4/24 is offset, because the base
# is 1.2.3.0/24)
def offset?
@addr != (@addr & ~mask)
end
# Returns offset from base of subnet to this address
# IP.new("1.2.3.4/24").offset => 4
def offset
@addr - (@addr & ~mask)
end
# If the address is not on the base, turn it into a single IP.
# IP.new("1.2.3.4/24").reset_pfxlen! =>
# IP.new("1.2.3.0/24").reset_pfxlen! =>
def reset_pfxlen!
self.pfxlen = nil if offset?
self
end
def to_irange
a1 = @addr & ~mask
a2 = a1 | mask
(a1..a2)
end
# QUERY: IPAddr (1.9) turns 1.2.3.0/24 into 1.2.3.0/24..1.2.3.255/24
# Here I turn it into 1.2.3.0..1.2.3.255. Which is better?
def to_range
self.class.new(@addr & ~mask, self.class::ADDR_BITS, @ctx)..self.class.new(@addr | mask, self.class::ADDR_BITS, @ctx)
end
# test if the address is in the provided subnet
def is_in?(subnet)
subnet.network.to_i <= network.to_i &&
subnet.broadcast.to_i >= broadcast.to_i
end
# this function sub-divides a subnet into two subnets of equal size
def split
nets = []
if pfxlen < self.class::ADDR_BITS
if self.class::ADDR_BITS == 32
new_base = IP::V4.new(network.to_i, (pfxlen + 1))
nets = [new_base, IP::V4.new((new_base.broadcast + 1).to_i, (pfxlen + 1))]
end
if self.class::ADDR_BITS == 128
new_base = IP::V6.new(network.to_i, (pfxlen + 1))
nets = [new_base, IP::V6.new((new_base.broadcast + 1).to_i, (pfxlen + 1))]
end
end
nets
end
# subdivide a larger subnet into smaller subnets by number of subnets of equal size,
# stop when subnets reach their smallest possible size (i.e. 31 for IP4)
def divide_by_subnets(number_subnets)
nets = []
return nets if split.empty?
nets << self
loop do
new_nets = []
nets.each do |net|
new_nets |= net.split
end
nets = new_nets
break if number_subnets <= nets.length &&
nets[0].pfxlen <= (self.class::ADDR_BITS - 1)
end
nets
end
# subdivide a larger subnet into smaller subnets by number of hosts
def divide_by_hosts(number_hosts)
nets = []
return nets if split.empty?
nets << self
while number_hosts <= (nets[0].split[0].size - 2) &&
nets[0].pfxlen <= (self.class::ADDR_BITS - 1)
new_nets = []
nets.each do |net|
new_nets |= net.split
end
nets = new_nets
end
nets
end
# deaggregate address range
# IP.new('1.2.0.0').deaggregate(IP.new('1.3.255.255'))
# => [#]
# IP.new('1.2.0.0').deaggregate(IP.new('1.4.255.255'))
# => [#, #]
# IP.new('2001:db8:85a3:8d3::').deaggregate(IP.new('2001:0db8:85a3:08d3:ffff:ffff:ffff:ffff'))
# => [#]
# IP.new('2001:db8:85a3:8d3::').deaggregate(IP.new('2001:db8:85a3:8d3:1::'))
# => [#, #]
def deaggregate(other)
nets = []
base = to_i
while base <= other.to_i
step = 0
while (base | (1 << step)) != base
break if (base | (((~0) & self.class::ADDR_MAX) >> (self.class::ADDR_BITS - 1 - step))) > other.to_i
step += 1
end
nets << self.class.new(base, self.class::ADDR_BITS - step, @ctx)
base += 1 << step
end
nets
end
# The number of IP addresses in subnet
# IP.new("1.2.3.4/24").size => 256
def size
mask + 1
end
def +(other)
self.class.new(@addr + other.to_i, @pfxlen, @ctx)
end
def -(other)
self.class.new(@addr - other.to_i, @pfxlen, @ctx)
end
def &(other)
self.class.new(@addr & other.to_i, @pfxlen, @ctx)
end
def |(other)
self.class.new(@addr | other.to_i, @pfxlen, @ctx)
end
def ^(other)
self.class.new(@addr ^ other.to_i, @pfxlen, @ctx)
end
def ~
self.class.new(~@addr & self.class::MASK, @pfxlen, @ctx)
end
def succ
self.class.new(@addr + size, @pfxlen, @ctx)
end
def succ!
@addr += size
self
end
def inspect
"#<#{self.class} #{self}>"
end
def ipv4_mapped?
false
end
def ipv4_compat?
false
end
def native
self
end
def hash
to_a.hash
end
def freeze
mask
super
end
def eql?(other)
to_a.eql?(other.to_a)
end
def <=>(other)
to_a <=> other.to_a
end
include Comparable
class V4 < IP
class << self; alias new orig_new; end
PROTO = 'v4'
PROTO_TO_CLASS[PROTO] = self
ADDR_MAX = 4_294_967_295
ADDR_BITS = 32
MASK = (1 << ADDR_BITS) - 1
ARPA = '.in-addr.arpa.'
# Parse a string; return an V4 instance if it's a valid IPv4 address,
# nil otherwise
def self.parse(str)
if str =~ %r{\A(\d+)\.(\d+)\.(\d+)\.(\d+)(?:/(\d+))?(?:@(.*))?\z}
pfxlen = (Regexp.last_match[5] || ADDR_BITS).to_i
return nil if pfxlen > 32
addrs = [Regexp.last_match[1].to_i,
Regexp.last_match[2].to_i,
Regexp.last_match[3].to_i,
Regexp.last_match[4].to_i]
return nil if addrs.find { |n| n > 255 }
addr = (((((addrs[0] << 8) | addrs[1]) << 8) | addrs[2]) << 8) | addrs[3]
new(addr, pfxlen, Regexp.last_match[6])
end
end
# Return just the address part as a String in dotted decimal form
def to_addr
format('%d.%d.%d.%d',
(@addr >> 24) & 0xff,
(@addr >> 16) & 0xff,
(@addr >> 8) & 0xff,
@addr & 0xff)
end
# return the arpa version of the address for reverse DNS: http://en.wikipedia.org/wiki/Reverse_DNS_lookup
def to_arpa
format("%d.%d.%d.%d#{ARPA}",
@addr & 0xff,
(@addr >> 8) & 0xff,
(@addr >> 16) & 0xff,
(@addr >> 24) & 0xff)
end
# Returns a network byte ordered string form of the IP address.
def hton
[@addr].pack('N')
end
end
class V6 < IP
class << self; alias new orig_new; end
PROTO = 'v6'
PROTO_TO_CLASS[PROTO] = self
ADDR_MAX = 340_282_366_920_938_463_463_374_607_431_768_211_455
ADDR_BITS = 128
MASK = (1 << ADDR_BITS) - 1
ARPA = '.ip6.arpa'
# Parse a string; return an V6 instance if it's a valid IPv6 address,
# nil otherwise
#--
# FIXME: allow larger variations of mapped addrs like 0:0:0:0:ffff:1.2.3.4
#++
def self.parse(str)
case str
when %r{\A\[?::(ffff:)?(\d+\.\d+\.\d+\.\d+)\]?(?:/(\d+))?(?:@(.*))?\z}i
mapped = Regexp.last_match[1]
pfxlen = (Regexp.last_match[3] || 128).to_i
ctx = Regexp.last_match[4]
return nil if pfxlen > 128
v4 = (V4.parse(Regexp.last_match[2]) || return).to_i
v4 |= 0xffff00000000 if mapped
new(v4, pfxlen, ctx)
when %r{\A\[?([0-9a-f:]+)\]?(?:/(\d+))?(?:@(.*))?\z}i
addr = Regexp.last_match[1]
pfxlen = (Regexp.last_match[2] || 128).to_i
return nil if pfxlen > 128
ctx = Regexp.last_match[3]
return nil if pfxlen > 128
if addr =~ /\A(.*?)::(.*)\z/
left = Regexp.last_match[1]
right = Regexp.last_match[2]
l = left.split(':', -1)
r = right.split(':', -1)
rest = 8 - l.length - r.length
return nil if rest < 0
else
l = addr.split(':')
r = []
rest = 0
return nil if l.length != 8
end
out = ''
l.each do |quad|
return nil unless (1..4).include?(quad.length)
out << quad.rjust(4, '0')
end
rest.times { out << '0000' }
r.each do |quad|
return nil unless (1..4).include?(quad.length)
out << quad.rjust(4, '0')
end
new(out, pfxlen, ctx)
end
end
# Return just the address part as a String in compact decimal form
def to_addr
if ipv4_compat?
"::#{native.to_addr}"
elsif ipv4_mapped?
"::ffff:#{native.to_addr}"
elsif @addr.zero?
'::'
else
res = to_hex.scan(/..../).join(':')
res.gsub!(/\b0{1,3}/, '')
res.sub!(/\b0:0:0:0(:0)*\b/, ':') ||
res.sub!(/\b0:0:0\b/, ':') ||
res.sub!(/\b0:0\b/, ':')
res.sub!(/:::+/, '::')
res
end
end
# Return just the address in non-compact form, required for reverse IP.
def to_addr_full
if ipv4_compat?
"::#{native.to_addr}"
elsif ipv4_mapped?
"::ffff:#{native.to_addr}"
elsif @addr.zero?
'::'
else
to_hex.scan(/..../).join(':')
end
end
# Returns a network byte ordered string form of the IP address.
def hton
(0..7).map { |i| (@addr >> (112 - 16 * i)) & 0xffff }.pack('n8')
end
# Returns the address broken into an array of 32 nibbles. Useful for
# to_arpa and use in SPF - http://tools.ietf.org/html/rfc7208#section-7.3
def to_nibbles
to_hex.rjust(32, '0').split(//)
end
# return the arpa version of the address for reverse DNS: http://en.wikipedia.org/wiki/Reverse_DNS_lookup
def to_arpa
to_nibbles.reverse.join('.') + ARPA
end
def ipv4_mapped?
(@addr >> 32) == 0xffff
end
def ipv4_compat?
@addr > 1 && (@addr >> 32) == 0
end
# Convert an IPv6 mapped/compat address to a V4 native address
def native
return self unless (ipv4_mapped? || ipv4_compat?) && (@pfxlen >= 96)
V4.new(@addr & V4::MASK, @pfxlen - 96, @ctx)
end
end
end