Module: IPAddrExtensions

Defined in:
lib/ipaddr_extensions.rb

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

MSCOPES =
{
  1 => "INTERFACE LOCAL MULTICAST",
  2 => "LINK LOCAL MULTICAST",
  4 => "ADMIN LOCAL MULTICAST",
  5 => "SITE LOCAL MULTICAST",
  8 => "ORGANISATION LOCAL MULTICAST",
  0xe => "GLOBAL MULTICAST"
}
MDESTS =
{
  1 => "ALL NODES",
  2 => "ALL ROUTERS",
  3 => "ALL DHCP SERVERS",
  4 => "DVMRP ROUTERS",
  5 => "OSPFIGP",
  6 => "OSPFIGP DESIGNATED ROUTERS",
  7 => "ST ROUTERS",
  8 => "ST HOSTS",
  9 => "RIP ROUTERS",
  0xa => "EIGRP ROUTERS",
  0xb => "MOBILE-AGENTS",
  0xc => "SSDP",
  0xd => "ALL PIM ROUTERS",
  0xe => "RSVP ENCAPSULATION",
  0xf => "UPNP",
  0x16 => "ALL MLDV2 CAPABLE ROUTERS",
  0x6a => "ALL SNOOPERS",
  0x6b => "PTP-PDELAY",
  0x6c => "SARATOGA",
  0x6d => "LL MANET ROUTERS",
  0xfb => "MDNSV6",
  0x100 => "VMTP MANAGERS GROUP",
  0x101 => "NTP",
  0x102 => "SGI-DOGFIGHT",
  0x103 => "RWHOD",
  0x104 => "VNP",
  0x105 => "ARTIFICIAL HORIZONS",
  0x106 => "NSS",
  0x107 => "AUDIONEWS",
  0x108 => "SUN NIS+",
  0x109 => "MTP",
  0x10a => "IETF-1-LOW-AUDIO",
  0x10b => "IETF-1-AUDIO",
  0x10c => "IETF-1-VIDEO",
  0x10d => "IETF-2-LOW-AUDIO",
  0x10e => "IETF-2-AUDIO",
  0x10f => "IETF-2-VIDEO",
  0x110 => "MUSIC-SERVICE",
  0x111 => "SEANET-TELEMETRY",
  0x112 => "SEANET-IMAGE",
  0x113 => "MLOADD",
  0x114 => "ANY PRIVATE EXPERIMENT",
  0x115 => "DVMRP on MOSPF",
  0x116 => "SVRLOC",
  0x117 => "XINGTV",
  0x118 => "MICROSOFT-DS",
  0x119 => "NBC-PRO",
  0x11a => "NBC-PFN",
  0x10001 => "LINK NAME",
  0x10002 => "ALL DHCP AGENTS",
  0x10003 => "LINK LOCAL MULTICAST NAME",
  0x10004 => "DTCP ANNOUNCEMENT",
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



7
8
9
10
11
12
13
# File 'lib/ipaddr_extensions.rb', line 7

def self.included(base)
  base.extend(ClassMethods)
  base.class_eval do
    alias_method :mask_without_a_care!, :mask!
    alias_method :mask!, :mask_with_a_care!
  end
end

Instance Method Details

#/(by) ⇒ Object



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/ipaddr_extensions.rb', line 409

def /(by)
  if self.ipv4?
    space = 1 << 32 - length
    if space % by == 0
      newmask = (((1<<32)-1) ^ (space/by-1)).to_s(2).count("1")
      (0..by-1).collect do |i|
        ip = (self.to_i + ((1 << 32 - newmask)*i)).to_ip(Socket::AF_INET)
        ip.length = newmask
        ip
      end
    else
      raise ArgumentError.new "Cannot evenly devide by #{by}"
    end
  elsif self.ipv6?
    space = 1 << 128 - length
    if space % by == 0
      newmask = (((1<<128)-1) ^ (space/by-1)).to_s(2).count("1")
      (0..by-1).collect do |i|
        ip = (self.to_i + ((1 << 128 - newmask)*i)).to_ip(Socket::AF_INET6)
        ip.length = newmask
        ip
      end
    else
      raise ArgumentError.new "Cannot evenly devide by #{by}"
    end
  end
end

#documentation?Boolean

Returns:

  • (Boolean)


299
300
301
# File 'lib/ipaddr_extensions.rb', line 299

def documentation?
  self.scope.split(' ').member? 'DOCUMENTATION'
end

#eui_64(mac) ⇒ Object

Return an EUI-64 host address for the current prefix (must be a 64 bit long IPv6 prefix).



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/ipaddr_extensions.rb', line 79

def eui_64(mac)
  if @family != Socket::AF_INET6
    raise Exception, "EUI-64 only makes sense on IPv6 prefixes."
  elsif self.length != 64
    raise Exception, "EUI-64 only makes sense on 64 bit IPv6 prefixes."
  end
  if mac.is_a? Integer
    mac = "%:012x" % mac
  end
  if mac.is_a? String
    mac.gsub!(/[^0-9a-fA-F]/, "")
    if mac.match(/^[0-9a-f]{12}/).nil?
      raise ArgumentError, "Second argument must be a valid MAC address."
    end
    e64 = (mac[0..5] + "fffe" + mac[6..11]).to_i(16) ^ 0x0200000000000000
    IPAddr.new(self.first.to_i + e64, Socket::AF_INET6)
  end
end

#eui_64?Boolean

Returns:

  • (Boolean)


98
99
100
101
102
103
104
105
# File 'lib/ipaddr_extensions.rb', line 98

def eui_64?
  if @family != Socket::AF_INET6
    raise Exception, "EUI-64 only makes sense on IPv6 prefixes."
    #elsif self.length != 64
    #  raise Exception, "EUI-64 only makes sense on 64 bit IPv6 prefixes."
  end
  (self.to_i & 0x20000fffe000000) == 0x20000fffe000000
end

#firstObject

Retrieve the first address in this prefix (called a network address in IPv4 land)



61
62
63
# File 'lib/ipaddr_extensions.rb', line 61

def first
  IPAddr.new(@addr & @mask_addr, @family)
end

#from_6to4Object



448
449
450
451
# File 'lib/ipaddr_extensions.rb', line 448

def from_6to4
  x = self.to_string.scanf("%*4x:%4x:%4x:%s")
  IPAddr.new((x[0]<<16)+x[1], Socket::AF_INET)
end

#from_teredoObject



441
442
443
# File 'lib/ipaddr_extensions.rb', line 441

def from_teredo
  is_teredo? && { :server => IPAddr.new((@addr >> 64) & ((1<<32)-1), Socket::AF_INET), :client => IPAddr.new((@addr & ((1<<32)-1)) ^ ((1<<32)-1), Socket::AF_INET), :port => ((@addr >> 32) & ((1<<16)-1)) }
end

#global?Boolean

Returns:

  • (Boolean)


305
306
307
# File 'lib/ipaddr_extensions.rb', line 305

def global?
  self.scope.split(' ').member? 'GLOBAL'
end

#host?Boolean

Extra quick tests

Returns:

  • (Boolean)


390
391
392
393
# File 'lib/ipaddr_extensions.rb', line 390

def host?
  (@family == Socket::AF_INET && self.length == 32) ||
    (@family == Socket::AF_INET6 && self.length == 128)
end

#is_6to4?Boolean

Returns:

  • (Boolean)


445
446
447
# File 'lib/ipaddr_extensions.rb', line 445

def is_6to4?
  IPAddr.new("2002::/16").include? self
end

#is_teredo?Boolean

Returns:

  • (Boolean)


437
438
439
# File 'lib/ipaddr_extensions.rb', line 437

def is_teredo?
  IPAddr.new("2001::/32").include? self
end

#lastObject

Retrieve the last address in this prefix (called a broadcast address in IPv4 land)



67
68
69
70
71
72
73
74
75
# File 'lib/ipaddr_extensions.rb', line 67

def last
  if @family == Socket::AF_INET
    IPAddr.new(first.to_i | (@mask_addr ^ IPAddr::IN4MASK), @family)
  elsif @family == Socket::AF_INET6
    IPAddr.new(first.to_i | (@mask_addr ^ IPAddr::IN6MASK), @family)
  else
    raise "unsupported address family."
  end
end

#lengthObject Also known as: bitmask

Return the bit length of the prefix ie:

IPAddr.new("2001:db8::/32").length
=> 32
IPAddr.new("192.0.2.0/255.255.255.0").length
=> 24


21
22
23
24
# File 'lib/ipaddr_extensions.rb', line 21

def length
  # nasty hack, but works well enough.
  @mask_addr.to_s(2).count("1")
end

#length=(length) ⇒ Object

Modify the bit length of the prefix



27
28
29
30
31
32
33
# File 'lib/ipaddr_extensions.rb', line 27

def length=(length)
  if self.ipv4?
    @mask_addr=((1<<32)-1) - ((1<<32-length)-1)
  elsif self.ipv6?
    @mask_addr=((1<<128)-1) - ((1<<128-length)-1)
  end
end

#link?Boolean

Returns:

  • (Boolean)


296
297
298
# File 'lib/ipaddr_extensions.rb', line 296

def link?
  self.scope.split(' ').member? 'LINK'
end

#local?Boolean

Some scope tests

Returns:

  • (Boolean)


287
288
289
# File 'lib/ipaddr_extensions.rb', line 287

def local?
  self.scope.split(' ').member? 'LOCAL'
end

#loopback?Boolean

Returns:

  • (Boolean)


302
303
304
# File 'lib/ipaddr_extensions.rb', line 302

def loopback?
  self.scope.split(' ').member? 'LOOPBACK'
end

#macObject



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ipaddr_extensions.rb', line 107

def mac
  if eui_64?
    network_bits = self.to_i & 0xffffffffffffffff
    top_chunk = network_bits >> 40
    bottom_chunk = network_bits & 0xffffff
    mac = ((top_chunk << 24) + bottom_chunk) ^ 0x20000000000
    result = []
    5.downto(0).each do |i|
      result << sprintf("%02x", (mac >> i * 8) & 0xff)
    end
    result * ':'
  end
end

#mask_with_a_care!(mask) ⇒ Object

Call the original mask! method but don’t allow it to change the internally stored address, since we might actually need that.



124
125
126
127
128
129
# File 'lib/ipaddr_extensions.rb', line 124

def mask_with_a_care!(mask)
  original_addr = @addr
  mask_without_a_care!(mask)
  @addr = original_addr unless self.class.mask_by_default
  return self
end

#multicast?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'lib/ipaddr_extensions.rb', line 293

def multicast?
  self.scope.split(' ').member? 'MULTICAST'
end

#multicast_from_prefix?Boolean

Returns:

  • (Boolean)


311
312
313
# File 'lib/ipaddr_extensions.rb', line 311

def multicast_from_prefix?
  ipv6? && ('ff00::/8'.to_ip.include? self) && ((self.to_i >> 116) & 0x03 == 3)
end

#prefix?Boolean

Returns:

  • (Boolean)


395
396
397
# File 'lib/ipaddr_extensions.rb', line 395

def prefix?
  !self.host?
end

#prefix_from_multicastObject

Returns the original prefix a Multicast address was generated from see RFC3306



317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/ipaddr_extensions.rb', line 317

def prefix_from_multicast
  if ipv6? && multicast_from_prefix?
    prefix_length = (to_i >> 92) & 0xff
    if (prefix_length == 0xff) && (((to_i >> 112) & 0xf) >= 2)
      # Link local prefix
      #(((to_i >> 32) & 0xffffffffffffffff) + (0xfe80 << 112)).to_ip(Socket::AF_INET6).tap { |p| p.length = 64 }
      return nil # See http://redmine.ruby-lang.org/issues/5468
    else
      # Global unicast prefix
      (((to_i >> 32) & 0xffffffffffffffff) << 64).to_ip(Socket::AF_INET6).tap { |p| p.length = prefix_length }
    end
  end
end

#private?Boolean

Returns:

  • (Boolean)


308
309
310
# File 'lib/ipaddr_extensions.rb', line 308

def private?
  self.scope.split(' ').member? 'PRIVATE'
end

#reversesObject

Return likely reverse zones for the Address or prefix (differs from reverse() because it will return the correct

number of zones to adequately delegate the prefix).


356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/ipaddr_extensions.rb', line 356

def reverses
  if @family == Socket::AF_INET
    if self.length == 32
      [ self.reverse ]
    else
      boundary = self.length % 8 == 0 && self.length != 0 ? self.length / 8 - 1 : self.length / 8
      divisor = (boundary + 1) * 8
      count = (self.last.to_i - self.first.to_i) / (1 << 32 - divisor)
      res = []
      (0..count).each do |i|
        octets = IPAddr.new(first.to_i + ((1<<32-divisor)*i), Socket::AF_INET).to_s.split('.')[0..boundary]
        res << "#{octets.reverse * '.'}.in-addr.arpa"
      end
      res
    end
  elsif @family == Socket::AF_INET6
    if self.length == 128
      [ self.reverse ]
    else
      boundary = self.length % 16 == 0 && self.length != 0 ? self.length / 4 - 1 : self.length / 4
      divisor = (boundary + 1) * 4
      count = (self.last.to_i - self.first.to_i) / (1 << 128-divisor)
      res = []
      (0..count).each do |i|
        baseaddr = self.first.to_i + (1<<128-divisor)*i
        octets = ("%032x" % baseaddr).split('')[0..boundary]
        res << octets.reverse * '.' + '.ip6.arpa'
      end
      res
    end
  end
end

#scopeObject

Returns a string describing the scope of the address.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/ipaddr_extensions.rb', line 197

def scope
  if @family == Socket::AF_INET
    if IPAddr.new("0.0.0.0/8").include? self
      "CURRENT NETWORK"
    elsif IPAddr.new("10.0.0.0/8").include? self
      "RFC1918 PRIVATE"
    elsif IPAddr.new("127.0.0.0/8").include? self
      "LOOPBACK"
    elsif IPAddr.new("168.254.0.0/16").include? self
      "AUTOCONF PRIVATE"
    elsif IPAddr.new("172.16.0.0/12").include? self
      "RFC1918 PRIVATE"
    elsif IPAddr.new("192.0.0.0/24").include? self
      "RESERVED (IANA)"
    elsif IPAddr.new("192.0.2.0/24").include? self
      "DOCUMENTATION"
    elsif IPAddr.new("192.88.99.0/24").include? self
      "6to4 ANYCAST"
    elsif IPAddr.new("192.168.0.0/16").include? self
      "RFC1918 PRIVATE"
    elsif IPAddr.new("198.18.0.0/15").include? self
      "NETWORK BENCHMARK TESTS"
    elsif IPAddr.new("198.51.100.0/24").include? self
      "DOCUMENTATION"
    elsif IPAddr.new("203.0.113.0/24").include? self
      "DOCUMENTATION"
    elsif IPAddr.new("224.0.0.0/4").include? self
      if IPAddr.new("239.0.0.0/8").include? self
        "LOCAL MULTICAST"
      else
        "GLOBAL MULTICAST"
      end
    elsif IPAddr.new("240.0.0.0/4").include? self
      "RESERVED"
    elsif IPAddr.new("255.255.255.255") == self
      "GLOBAL BROADCAST"
    else
      "GLOBAL UNICAST"
    end
  elsif @family == Socket::AF_INET6
    if IPAddr.new("2000::/3").include? self
      require 'scanf'
      if is_6to4?
        "GLOBAL UNICAST (6to4: #{from_6to4})"
      elsif is_teredo?
        "GLOBAL UNICAST (Teredo #{from_teredo[:client].to_s}:#{from_teredo[:port].to_s} -> #{from_teredo[:server].to_s}:#{from_teredo[:port].to_s})"
      elsif IPAddr.new("2001:10::/28").include? self
        "ORCHID"
      elsif IPAddr.new("2001:db8::/32").include? self
        "DOCUMENTATION"
      else
        "GLOBAL UNICAST"
      end
    elsif IPAddr.new("::/128") ==  self
      "UNSPECIFIED ADDRESS"
    elsif IPAddr.new("::1/128") == self
      "LINK LOCAL LOOPBACK"
    elsif IPAddr.new("::ffff:0:0/96").include? self
      a,b,c,d = self.to_string.scanf("%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x:%4x:%4x")
      "IPv4 MAPPED (#{a.to_s}.#{b.to_s}.#{c.to_s}.#{d.to_s})"
    elsif IPAddr.new("::/96").include? self
      a,b,c,d = self.to_string.scanf("%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x:%4x:%4x")
      "IPv4 TRANSITION (#{a.to_s}.#{b.to_s}.#{c.to_s}.#{d.to_s}, deprecated)"
    elsif IPAddr.new("fc00::/7").include? self
      "UNIQUE LOCAL UNICAST"
    elsif IPAddr.new("fec0::/10").include? self
      "SITE LOCAL (deprecated)"
    elsif IPAddr.new("fe80::/10").include? self
      "LINK LOCAL UNICAST"
    elsif IPAddr.new("ff00::/8").include? self
      mscope,mdesta,mdestb = self.to_string.scanf("%*1x%*1x%*1x%1x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x")
      mdest = (mdesta << 16) + mdestb
      s = "MULTICAST"
      if MSCOPES[mscope]
        s += " #{MSCOPES[mscope]}"
      end
      if MDESTS[mdest]
        s += " #{MDESTS[mdest]}"
      end
      if multicast_from_prefix?
        s += " (prefix = #{prefix_from_multicast.to_string_including_length})"
      end
      s
    else
      "RESERVED"
    end
  end
end

#spaceObject

Return the space available inside this prefix



340
341
342
# File 'lib/ipaddr_extensions.rb', line 340

def space
  self.last.to_i - self.first.to_i + 1
end

#subnet_maskObject

Return an old-style subnet mask ie:

IPAddr.new("2001:db8::/32").subnet_mask
=> #<IPAddr: IPv6:ffff:ffff:0000:0000:0000:0000:0000:0000/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
IPAddr.new("192.0.2.0/255.255.255.0").subnet_mask
=> #<IPAddr: IPv4:255.255.255.0/255.255.255.255>


41
42
43
# File 'lib/ipaddr_extensions.rb', line 41

def subnet_mask
  @mask_addr.to_ip
end

#to_6to4Object

Convert an IPv4 address into an IPv6 6to4 address.



333
334
335
336
337
# File 'lib/ipaddr_extensions.rb', line 333

def to_6to4
  if @family == Socket::AF_INET
    IPAddr.new((0x2002 << 112) + (@addr << 80), Socket::AF_INET6).tap { |p| p.length = 48 }
  end
end

#to_string_including_lengthObject



399
400
401
402
403
404
405
# File 'lib/ipaddr_extensions.rb', line 399

def to_string_including_length
  if host?
    to_s
  else
    "#{to_s}/#{length.to_s}"
  end
end

#unicast?Boolean

Returns:

  • (Boolean)


290
291
292
# File 'lib/ipaddr_extensions.rb', line 290

def unicast?
  !self.scope.split(' ').any? { |scope| ['BROADCAST', 'MULTICAST'].member? scope }
end

#usableObject

Return usable address space inside this prefix



345
346
347
348
349
350
351
# File 'lib/ipaddr_extensions.rb', line 345

def usable
  if ipv6?
    space
  else
    space - 2
  end
end

#wildcard_maskObject

Return a “cisco style” subnet mask for use in ACLs:

IPAddr.new("2001:db8::/32").wildcard_mask
=> #<IPAddr: IPv6:0000:0000:ffff:ffff:ffff:ffff:ffff:ffff/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
IPAddr.new("192.0.2.0/255.255.255.0").wildcard_mask
=> #<IPAddr: IPv4:0.0.0.255/255.255.255.255>


51
52
53
54
55
56
57
# File 'lib/ipaddr_extensions.rb', line 51

def wildcard_mask
  if self.ipv4?
    (@mask_addr ^ IPAddr::IN4MASK).to_ip
  else
    (@mask_addr ^ IPAddr::IN6MASK).to_ip
  end
end