ipaddr_extensions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README +55 -0
- data/Rakefile +3 -0
- data/ipaddr_extensions.gemspec +17 -0
- data/lib/ipaddr_extensions.rb +719 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 833c7f6ca4de6a0bf24cec680c7d9a9530d59f2f
|
4
|
+
data.tar.gz: 38ed969a1184ea1c8b0f39ab8b0320a6a7a75629
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 82be92d53fbe46c35ad1ae487af3e8fc6dd2e89a43b7cf95d89b649df3585dca1ad5c61a6538a3cc6ca0ccee4cedde5630f3560f07ae4fe74bb3b88f588a60c8
|
7
|
+
data.tar.gz: dd11ed1b3fde478c5d4a209a035436fa75111dbf3b7022b1b0e98e05275e83112beab409c144f1201fcadcfb923aff1450b604a8a14cce5f9f6a167534f57136
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008-2010 Justin French
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
IPAddrExtensions
|
2
|
+
================
|
3
|
+
|
4
|
+
A selection of potentially useful extensions for IPAddr.
|
5
|
+
|
6
|
+
Example
|
7
|
+
=======
|
8
|
+
|
9
|
+
>> IPAddr.new("192.0.2.0/24").length
|
10
|
+
=> 24
|
11
|
+
>> IPAddr.new("192.0.2.0/24").first
|
12
|
+
=> #<IPAddr: IPv4:192.0.2.0/255.255.255.255>
|
13
|
+
>> IPAddr.new("192.0.2.0/24").last
|
14
|
+
=> #<IPAddr: IPv4:192.0.2.255/255.255.255.255>
|
15
|
+
>> IPAddr.new("192.0.2.0/24").scope
|
16
|
+
=> "DOCUMENTATION"
|
17
|
+
>> IPAddr.new("192.0.2.0/24").local?
|
18
|
+
=> false
|
19
|
+
>> IPAddr.new("192.0.2.0/24").unicast?
|
20
|
+
=> false
|
21
|
+
>> IPAddr.new("192.0.2.0/24").multicast?
|
22
|
+
=> false
|
23
|
+
>> IPAddr.new("192.0.2.0/24").link?
|
24
|
+
=> false
|
25
|
+
>> IPAddr.new("192.0.2.0/24").documentation?
|
26
|
+
=> true
|
27
|
+
>> IPAddr.new("192.0.2.0/24").loopback?
|
28
|
+
=> false
|
29
|
+
>> IPAddr.new("192.0.2.0/24").global?
|
30
|
+
=> false
|
31
|
+
>> IPAddr.new("192.0.2.0/24").private?
|
32
|
+
=> false
|
33
|
+
>> IPAddr.new("192.0.2.0/24").space
|
34
|
+
=> 256
|
35
|
+
>> IPAddr.new("192.0.2.0/24").reverses
|
36
|
+
=> ["192.0.2.in-addr.arpa"]
|
37
|
+
>> IPAddr.new("192.0.2.0/24").host?
|
38
|
+
=> false
|
39
|
+
>> IPAddr.new("192.0.2.0/24").prefix?
|
40
|
+
=> true
|
41
|
+
>> IPAddr.new("2001:db8::/64").eui_64('00:0D:60:0F:3C:A8')
|
42
|
+
=> #<IPAddr: IPv6:2001:0db8:0000:0000:020d:60ff:fe0f:3ca8/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
|
43
|
+
>> IPAddr.new("2001:db8::/56").reverses
|
44
|
+
=> ["0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "1.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "2.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "3.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "4.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "5.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "6.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "7.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "8.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "9.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "a.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "b.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "c.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "d.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "e.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa", "f.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"]
|
45
|
+
>> "192.0.2.0/24".to_ip
|
46
|
+
=> #<IPAddr: IPv4:192.0.2.0/255.255.255.0>
|
47
|
+
>> 12345.to_ip
|
48
|
+
=> #<IPAddr: IPv4:0.0.48.57/255.255.255.255>
|
49
|
+
>> 12345.to_ip(Socket::AF_INET6)
|
50
|
+
=> #<IPAddr: IPv6:0000:0000:0000:0000:0000:0000:0000:3039/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
|
51
|
+
>> ("2001:db8::/32".to_ip / 16).collect { |prefix| prefix.to_string_including_length }
|
52
|
+
=> ["2001:db8::/36", "2001:db8:1000::/36", "2001:db8:2000::/36", "2001:db8:3000::/36", "2001:db8:4000::/36", "2001:db8:5000::/36", "2001:db8:6000::/36", "2001:db8:7000::/36", "2001:db8:8000::/36", "2001:db8:9000::/36", "2001:db8:a000::/36", "2001:db8:b000::/36", "2001:db8:c000::/36", "2001:db8:d000::/36", "2001:db8:e000::/36", "2001:db8:f000::/36"]
|
53
|
+
|
54
|
+
|
55
|
+
Copyright (c) 2010,2011 James Harto, Sociable Limited, released under the Mozilla Public License.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'ipaddr_extensions'
|
6
|
+
s.version = '1.0.0'
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["James Harton"]
|
9
|
+
s.email = %q{james@resistor.io}
|
10
|
+
s.homepage = %q{http://github.com/jamesotron/IPAddrExtensions}
|
11
|
+
s.summary = %q{A small gem that adds extra functionality to Rubys IPAddr class}
|
12
|
+
|
13
|
+
s.files = ["MIT-LICENSE", "README", "Rakefile", "ipaddr_extensions.gemspec"] + Dir["lib/**/*"]
|
14
|
+
s.require_paths = ["lib"]
|
15
|
+
|
16
|
+
s.add_development_dependency 'rake'
|
17
|
+
end
|
@@ -0,0 +1,719 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'scanf'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
module IPAddrExtensions
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.class_eval do
|
10
|
+
alias_method :mask_without_a_care!, :mask!
|
11
|
+
alias_method :mask!, :mask_with_a_care!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the bit length of the prefix
|
16
|
+
# ie:
|
17
|
+
# IPAddr.new("2001:db8::/32").length
|
18
|
+
# => 32
|
19
|
+
# IPAddr.new("192.0.2.0/255.255.255.0").length
|
20
|
+
# => 24
|
21
|
+
def length
|
22
|
+
# nasty hack, but works well enough.
|
23
|
+
@mask_addr.to_s(2).count("1")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Modify the bit length of the prefix
|
27
|
+
def length=(length)
|
28
|
+
if self.ipv4?
|
29
|
+
@mask_addr=((1<<32)-1) - ((1<<32-length)-1)
|
30
|
+
elsif self.ipv6?
|
31
|
+
@mask_addr=((1<<128)-1) - ((1<<128-length)-1)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return an old-style subnet mask
|
36
|
+
# ie:
|
37
|
+
# IPAddr.new("2001:db8::/32").subnet_mask
|
38
|
+
# => #<IPAddr: IPv6:ffff:ffff:0000:0000:0000:0000:0000:0000/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
|
39
|
+
# IPAddr.new("192.0.2.0/255.255.255.0").subnet_mask
|
40
|
+
# => #<IPAddr: IPv4:255.255.255.0/255.255.255.255>
|
41
|
+
def subnet_mask
|
42
|
+
@mask_addr.to_ip
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return a "cisco style" subnet mask for use in ACLs:
|
46
|
+
#
|
47
|
+
# IPAddr.new("2001:db8::/32").wildcard_mask
|
48
|
+
# => #<IPAddr: IPv6:0000:0000:ffff:ffff:ffff:ffff:ffff:ffff/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
|
49
|
+
# IPAddr.new("192.0.2.0/255.255.255.0").wildcard_mask
|
50
|
+
# => #<IPAddr: IPv4:0.0.0.255/255.255.255.255>
|
51
|
+
def wildcard_mask
|
52
|
+
if self.ipv4?
|
53
|
+
(@mask_addr ^ IPAddr::IN4MASK).to_ip
|
54
|
+
else
|
55
|
+
(@mask_addr ^ IPAddr::IN6MASK).to_ip
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Retrieve the first address in this prefix
|
60
|
+
# (called a network address in IPv4 land)
|
61
|
+
def first
|
62
|
+
IPAddr.new(@addr & @mask_addr, @family)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Retrieve the last address in this prefix
|
66
|
+
# (called a broadcast address in IPv4 land)
|
67
|
+
def last
|
68
|
+
if @family == Socket::AF_INET
|
69
|
+
IPAddr.new(first.to_i | (@mask_addr ^ IPAddr::IN4MASK), @family)
|
70
|
+
elsif @family == Socket::AF_INET6
|
71
|
+
IPAddr.new(first.to_i | (@mask_addr ^ IPAddr::IN6MASK), @family)
|
72
|
+
else
|
73
|
+
raise "unsupported address family."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return an EUI-64 host address for the current
|
78
|
+
# prefix (must be a 64 bit long IPv6 prefix).
|
79
|
+
def eui_64(mac)
|
80
|
+
if @family != Socket::AF_INET6
|
81
|
+
raise Exception, "EUI-64 only makes sense on IPv6 prefixes."
|
82
|
+
elsif self.length != 64
|
83
|
+
raise Exception, "EUI-64 only makes sense on 64 bit IPv6 prefixes."
|
84
|
+
end
|
85
|
+
if mac.is_a? Integer
|
86
|
+
mac = "%:012x" % mac
|
87
|
+
end
|
88
|
+
if mac.is_a? String
|
89
|
+
mac.gsub!(/[^0-9a-fA-F]/, "")
|
90
|
+
if mac.match(/^[0-9a-f]{12}/).nil?
|
91
|
+
raise ArgumentError, "Second argument must be a valid MAC address."
|
92
|
+
end
|
93
|
+
e64 = (mac[0..5] + "fffe" + mac[6..11]).to_i(16) ^ 0x0200000000000000
|
94
|
+
IPAddr.new(self.first.to_i + e64, Socket::AF_INET6)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def eui_64?
|
99
|
+
if @family != Socket::AF_INET6
|
100
|
+
raise Exception, "EUI-64 only makes sense on IPv6 prefixes."
|
101
|
+
#elsif self.length != 64
|
102
|
+
# raise Exception, "EUI-64 only makes sense on 64 bit IPv6 prefixes."
|
103
|
+
end
|
104
|
+
(self.to_i & 0x20000fffe000000) == 0x20000fffe000000
|
105
|
+
end
|
106
|
+
|
107
|
+
def mac
|
108
|
+
if eui_64?
|
109
|
+
network_bits = self.to_i & 0xffffffffffffffff
|
110
|
+
top_chunk = network_bits >> 40
|
111
|
+
bottom_chunk = network_bits & 0xffffff
|
112
|
+
mac = ((top_chunk << 24) + bottom_chunk) ^ 0x20000000000
|
113
|
+
result = []
|
114
|
+
5.downto(0).each do |i|
|
115
|
+
result << sprintf("%02x", (mac >> i * 8) & 0xff)
|
116
|
+
end
|
117
|
+
result * ':'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Call the original mask! method but don't allow it
|
122
|
+
# to change the internally stored address, since we
|
123
|
+
# might actually need that.
|
124
|
+
def mask_with_a_care!(mask)
|
125
|
+
original_addr = @addr
|
126
|
+
mask_without_a_care!(mask)
|
127
|
+
@addr = original_addr unless self.class.mask_by_default
|
128
|
+
return self
|
129
|
+
end
|
130
|
+
|
131
|
+
MSCOPES = {
|
132
|
+
1 => "INTERFACE LOCAL MULTICAST",
|
133
|
+
2 => "LINK LOCAL MULTICAST",
|
134
|
+
4 => "ADMIN LOCAL MULTICAST",
|
135
|
+
5 => "SITE LOCAL MULTICAST",
|
136
|
+
8 => "ORGANISATION LOCAL MULTICAST",
|
137
|
+
0xe => "GLOBAL MULTICAST"
|
138
|
+
}
|
139
|
+
|
140
|
+
MDESTS = {
|
141
|
+
1 => "ALL NODES",
|
142
|
+
2 => "ALL ROUTERS",
|
143
|
+
3 => "ALL DHCP SERVERS",
|
144
|
+
4 => "DVMRP ROUTERS",
|
145
|
+
5 => "OSPFIGP",
|
146
|
+
6 => "OSPFIGP DESIGNATED ROUTERS",
|
147
|
+
7 => "ST ROUTERS",
|
148
|
+
8 => "ST HOSTS",
|
149
|
+
9 => "RIP ROUTERS",
|
150
|
+
0xa => "EIGRP ROUTERS",
|
151
|
+
0xb => "MOBILE-AGENTS",
|
152
|
+
0xc => "SSDP",
|
153
|
+
0xd => "ALL PIM ROUTERS",
|
154
|
+
0xe => "RSVP ENCAPSULATION",
|
155
|
+
0xf => "UPNP",
|
156
|
+
0x16 => "ALL MLDV2 CAPABLE ROUTERS",
|
157
|
+
0x6a => "ALL SNOOPERS",
|
158
|
+
0x6b => "PTP-PDELAY",
|
159
|
+
0x6c => "SARATOGA",
|
160
|
+
0x6d => "LL MANET ROUTERS",
|
161
|
+
0xfb => "MDNSV6",
|
162
|
+
0x100 => "VMTP MANAGERS GROUP",
|
163
|
+
0x101 => "NTP",
|
164
|
+
0x102 => "SGI-DOGFIGHT",
|
165
|
+
0x103 => "RWHOD",
|
166
|
+
0x104 => "VNP",
|
167
|
+
0x105 => "ARTIFICIAL HORIZONS",
|
168
|
+
0x106 => "NSS",
|
169
|
+
0x107 => "AUDIONEWS",
|
170
|
+
0x108 => "SUN NIS+",
|
171
|
+
0x109 => "MTP",
|
172
|
+
0x10a => "IETF-1-LOW-AUDIO",
|
173
|
+
0x10b => "IETF-1-AUDIO",
|
174
|
+
0x10c => "IETF-1-VIDEO",
|
175
|
+
0x10d => "IETF-2-LOW-AUDIO",
|
176
|
+
0x10e => "IETF-2-AUDIO",
|
177
|
+
0x10f => "IETF-2-VIDEO",
|
178
|
+
0x110 => "MUSIC-SERVICE",
|
179
|
+
0x111 => "SEANET-TELEMETRY",
|
180
|
+
0x112 => "SEANET-IMAGE",
|
181
|
+
0x113 => "MLOADD",
|
182
|
+
0x114 => "ANY PRIVATE EXPERIMENT",
|
183
|
+
0x115 => "DVMRP on MOSPF",
|
184
|
+
0x116 => "SVRLOC",
|
185
|
+
0x117 => "XINGTV",
|
186
|
+
0x118 => "MICROSOFT-DS",
|
187
|
+
0x119 => "NBC-PRO",
|
188
|
+
0x11a => "NBC-PFN",
|
189
|
+
0x10001 => "LINK NAME",
|
190
|
+
0x10002 => "ALL DHCP AGENTS",
|
191
|
+
0x10003 => "LINK LOCAL MULTICAST NAME",
|
192
|
+
0x10004 => "DTCP ANNOUNCEMENT",
|
193
|
+
}
|
194
|
+
|
195
|
+
# Returns a string describing the scope of the
|
196
|
+
# address.
|
197
|
+
def scope
|
198
|
+
if @family == Socket::AF_INET
|
199
|
+
if IPAddr.new("0.0.0.0/8").include? self
|
200
|
+
"CURRENT NETWORK"
|
201
|
+
elsif IPAddr.new("10.0.0.0/8").include? self
|
202
|
+
"RFC1918 PRIVATE"
|
203
|
+
elsif IPAddr.new("127.0.0.0/8").include? self
|
204
|
+
"LOOPBACK"
|
205
|
+
elsif IPAddr.new("168.254.0.0/16").include? self
|
206
|
+
"AUTOCONF PRIVATE"
|
207
|
+
elsif IPAddr.new("172.16.0.0/12").include? self
|
208
|
+
"RFC1918 PRIVATE"
|
209
|
+
elsif IPAddr.new("192.0.0.0/24").include? self
|
210
|
+
"RESERVED (IANA)"
|
211
|
+
elsif IPAddr.new("192.0.2.0/24").include? self
|
212
|
+
"DOCUMENTATION"
|
213
|
+
elsif IPAddr.new("192.88.99.0/24").include? self
|
214
|
+
"6to4 ANYCAST"
|
215
|
+
elsif IPAddr.new("192.168.0.0/16").include? self
|
216
|
+
"RFC1918 PRIVATE"
|
217
|
+
elsif IPAddr.new("198.18.0.0/15").include? self
|
218
|
+
"NETWORK BENCHMARK TESTS"
|
219
|
+
elsif IPAddr.new("198.51.100.0/24").include? self
|
220
|
+
"DOCUMENTATION"
|
221
|
+
elsif IPAddr.new("203.0.113.0/24").include? self
|
222
|
+
"DOCUMENTATION"
|
223
|
+
elsif IPAddr.new("224.0.0.0/4").include? self
|
224
|
+
if IPAddr.new("239.0.0.0/8").include? self
|
225
|
+
"LOCAL MULTICAST"
|
226
|
+
else
|
227
|
+
"GLOBAL MULTICAST"
|
228
|
+
end
|
229
|
+
elsif IPAddr.new("240.0.0.0/4").include? self
|
230
|
+
"RESERVED"
|
231
|
+
elsif IPAddr.new("255.255.255.255") == self
|
232
|
+
"GLOBAL BROADCAST"
|
233
|
+
else
|
234
|
+
"GLOBAL UNICAST"
|
235
|
+
end
|
236
|
+
elsif @family == Socket::AF_INET6
|
237
|
+
if IPAddr.new("2000::/3").include? self
|
238
|
+
require 'scanf'
|
239
|
+
if is_6to4?
|
240
|
+
"GLOBAL UNICAST (6to4: #{from_6to4})"
|
241
|
+
elsif is_teredo?
|
242
|
+
"GLOBAL UNICAST (Teredo #{from_teredo[:client].to_s}:#{from_teredo[:port].to_s} -> #{from_teredo[:server].to_s}:#{from_teredo[:port].to_s})"
|
243
|
+
elsif IPAddr.new("2001:10::/28").include? self
|
244
|
+
"ORCHID"
|
245
|
+
elsif IPAddr.new("2001:db8::/32").include? self
|
246
|
+
"DOCUMENTATION"
|
247
|
+
else
|
248
|
+
"GLOBAL UNICAST"
|
249
|
+
end
|
250
|
+
elsif IPAddr.new("::/128") == self
|
251
|
+
"UNSPECIFIED ADDRESS"
|
252
|
+
elsif IPAddr.new("::1/128") == self
|
253
|
+
"LINK LOCAL LOOPBACK"
|
254
|
+
elsif IPAddr.new("::ffff:0:0/96").include? self
|
255
|
+
a,b,c,d = self.to_string.scanf("%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x:%4x:%4x")
|
256
|
+
"IPv4 MAPPED (#{a.to_s}.#{b.to_s}.#{c.to_s}.#{d.to_s})"
|
257
|
+
elsif IPAddr.new("::/96").include? self
|
258
|
+
a,b,c,d = self.to_string.scanf("%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x:%4x:%4x")
|
259
|
+
"IPv4 TRANSITION (#{a.to_s}.#{b.to_s}.#{c.to_s}.#{d.to_s}, deprecated)"
|
260
|
+
elsif IPAddr.new("fc00::/7").include? self
|
261
|
+
"UNIQUE LOCAL UNICAST"
|
262
|
+
elsif IPAddr.new("fec0::/10").include? self
|
263
|
+
"SITE LOCAL (deprecated)"
|
264
|
+
elsif IPAddr.new("fe80::/10").include? self
|
265
|
+
"LINK LOCAL UNICAST"
|
266
|
+
elsif IPAddr.new("ff00::/8").include? self
|
267
|
+
mscope,mdesta,mdestb = self.to_string.scanf("%*1x%*1x%*1x%1x:%*4x:%*4x:%*4x:%*4x:%*4x:%4x:%4x")
|
268
|
+
mdest = (mdesta << 16) + mdestb
|
269
|
+
s = "MULTICAST"
|
270
|
+
if MSCOPES[mscope]
|
271
|
+
s += " #{MSCOPES[mscope]}"
|
272
|
+
end
|
273
|
+
if MDESTS[mdest]
|
274
|
+
s += " #{MDEST[mdest]}"
|
275
|
+
end
|
276
|
+
if multicast_from_prefix?
|
277
|
+
s += " (prefix = #{prefix_from_multicast.to_string_including_length})"
|
278
|
+
end
|
279
|
+
s
|
280
|
+
else
|
281
|
+
"RESERVED"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Some scope tests
|
287
|
+
def local?
|
288
|
+
self.scope.split(' ').member? 'LOCAL'
|
289
|
+
end
|
290
|
+
def unicast?
|
291
|
+
!self.scope.split(' ').any? { |scope| ['BROADCAST', 'MULTICAST'].member? scope }
|
292
|
+
end
|
293
|
+
def multicast?
|
294
|
+
self.scope.split(' ').member? 'MULTICAST'
|
295
|
+
end
|
296
|
+
def link?
|
297
|
+
self.scope.split(' ').member? 'LINK'
|
298
|
+
end
|
299
|
+
def documentation?
|
300
|
+
self.scope.split(' ').member? 'DOCUMENTATION'
|
301
|
+
end
|
302
|
+
def loopback?
|
303
|
+
self.scope.split(' ').member? 'LOOPBACK'
|
304
|
+
end
|
305
|
+
def global?
|
306
|
+
self.scope.split(' ').member? 'GLOBAL'
|
307
|
+
end
|
308
|
+
def private?
|
309
|
+
self.scope.split(' ').member? 'PRIVATE'
|
310
|
+
end
|
311
|
+
def multicast_from_prefix?
|
312
|
+
ipv6? && ('ff00::/8'.to_ip.include? self) && ((self.to_i >> 116) & 0x03 == 3)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Returns the original prefix a Multicast address was generated from
|
316
|
+
# see RFC3306
|
317
|
+
def prefix_from_multicast
|
318
|
+
if ipv6? && multicast_from_prefix?
|
319
|
+
prefix_length = (to_i >> 92) & 0xff
|
320
|
+
if (prefix_length == 0xff) && (((to_i >> 112) & 0xf) >= 2)
|
321
|
+
# Link local prefix
|
322
|
+
#(((to_i >> 32) & 0xffffffffffffffff) + (0xfe80 << 112)).to_ip(Socket::AF_INET6).tap { |p| p.length = 64 }
|
323
|
+
return nil # See http://redmine.ruby-lang.org/issues/5468
|
324
|
+
else
|
325
|
+
# Global unicast prefix
|
326
|
+
(((to_i >> 32) & 0xffffffffffffffff) << 64).to_ip(Socket::AF_INET6).tap { |p| p.length = prefix_length }
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Convert an IPv4 address into an IPv6
|
332
|
+
# 6to4 address.
|
333
|
+
def to_6to4
|
334
|
+
if @family == Socket::AF_INET
|
335
|
+
IPAddr.new((0x2002 << 112) + (@addr << 80), Socket::AF_INET6).tap { |p| p.length = 48 }
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Return the space available inside this prefix
|
340
|
+
def space
|
341
|
+
self.last.to_i - self.first.to_i + 1
|
342
|
+
end
|
343
|
+
|
344
|
+
# Return usable address space inside this prefix
|
345
|
+
def usable
|
346
|
+
if ipv6?
|
347
|
+
space
|
348
|
+
else
|
349
|
+
space - 2
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Return likely reverse zones for the Address or prefix
|
354
|
+
# (differs from reverse() because it will return the correct
|
355
|
+
# number of zones to adequately delegate the prefix).
|
356
|
+
def reverses
|
357
|
+
if @family == Socket::AF_INET
|
358
|
+
if self.length == 32
|
359
|
+
[ self.reverse ]
|
360
|
+
else
|
361
|
+
boundary = self.length % 8 == 0 && self.length != 0 ? self.length / 8 - 1 : self.length / 8
|
362
|
+
divisor = (boundary + 1) * 8
|
363
|
+
count = (self.last.to_i - self.first.to_i) / (1 << 32 - divisor)
|
364
|
+
res = []
|
365
|
+
(0..count).each do |i|
|
366
|
+
octets = IPAddr.new(first.to_i + ((1<<32-divisor)*i), Socket::AF_INET).to_s.split('.')[0..boundary]
|
367
|
+
res << "#{octets.reverse * '.'}.in-addr.arpa"
|
368
|
+
end
|
369
|
+
res
|
370
|
+
end
|
371
|
+
elsif @family == Socket::AF_INET6
|
372
|
+
if self.length == 128
|
373
|
+
[ self.reverse ]
|
374
|
+
else
|
375
|
+
boundary = self.length % 16 == 0 && self.length != 0 ? self.length / 4 - 1 : self.length / 4
|
376
|
+
divisor = (boundary + 1) * 4
|
377
|
+
count = (self.last.to_i - self.first.to_i) / (1 << 128-divisor)
|
378
|
+
res = []
|
379
|
+
(0..count).each do |i|
|
380
|
+
baseaddr = self.first.to_i + (1<<128-divisor)*i
|
381
|
+
octets = ("%032x" % baseaddr).split('')[0..boundary]
|
382
|
+
res << octets.reverse * '.' + '.ip6.arpa'
|
383
|
+
end
|
384
|
+
res
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Extra quick tests
|
390
|
+
def host?
|
391
|
+
(@family == Socket::AF_INET && self.length == 32) ||
|
392
|
+
(@family == Socket::AF_INET6 && self.length == 128)
|
393
|
+
end
|
394
|
+
|
395
|
+
def prefix?
|
396
|
+
!self.host?
|
397
|
+
end
|
398
|
+
|
399
|
+
def to_string_including_length
|
400
|
+
if host?
|
401
|
+
to_s
|
402
|
+
else
|
403
|
+
"#{to_s}/#{length.to_s}"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
alias bitmask length
|
408
|
+
|
409
|
+
def /(by)
|
410
|
+
if self.ipv4?
|
411
|
+
space = 1 << 32 - length
|
412
|
+
if space % by == 0
|
413
|
+
newmask = (((1<<32)-1) ^ (space/by-1)).to_s(2).count("1")
|
414
|
+
(0..by-1).collect do |i|
|
415
|
+
ip = (self.to_i + ((1 << 32 - newmask)*i)).to_ip(Socket::AF_INET)
|
416
|
+
ip.length = newmask
|
417
|
+
ip
|
418
|
+
end
|
419
|
+
else
|
420
|
+
raise ArgumentError.new "Cannot evenly devide by #{by}"
|
421
|
+
end
|
422
|
+
elsif self.ipv6?
|
423
|
+
space = 1 << 128 - length
|
424
|
+
if space % by == 0
|
425
|
+
newmask = (((1<<128)-1) ^ (space/by-1)).to_s(2).count("1")
|
426
|
+
(0..by-1).collect do |i|
|
427
|
+
ip = (self.to_i + ((1 << 128 - newmask)*i)).to_ip(Socket::AF_INET6)
|
428
|
+
ip.length = newmask
|
429
|
+
ip
|
430
|
+
end
|
431
|
+
else
|
432
|
+
raise ArgumentError.new "Cannot evenly devide by #{by}"
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def is_teredo?
|
438
|
+
IPAddr.new("2001::/32").include? self
|
439
|
+
end
|
440
|
+
|
441
|
+
def from_teredo
|
442
|
+
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)) }
|
443
|
+
end
|
444
|
+
|
445
|
+
def is_6to4?
|
446
|
+
IPAddr.new("2002::/16").include? self
|
447
|
+
end
|
448
|
+
def from_6to4
|
449
|
+
x = self.to_string.scanf("%*4x:%4x:%4x:%s")
|
450
|
+
IPAddr.new((x[0]<<16)+x[1], Socket::AF_INET)
|
451
|
+
end
|
452
|
+
|
453
|
+
module ClassMethods
|
454
|
+
|
455
|
+
# By default IPAddr masks a non all-ones prefix so that the
|
456
|
+
# "network address" is all that's stored. This loses data
|
457
|
+
# for some applications and isn't really necessary since
|
458
|
+
# anyone expecting that should use #first instead.
|
459
|
+
# This defaults to on to retain compatibility with the
|
460
|
+
# rubycore IPAddr class.
|
461
|
+
def mask_by_default
|
462
|
+
# You can't use ||= for bools.
|
463
|
+
if @mask_by_default.nil?
|
464
|
+
@mask_by_default = true
|
465
|
+
end
|
466
|
+
@mask_by_default
|
467
|
+
end
|
468
|
+
def mask_by_default=(x)
|
469
|
+
@mask_by_default = !!x
|
470
|
+
end
|
471
|
+
|
472
|
+
# Generate an IPv6 Unique Local Address using the supplied system MAC address.
|
473
|
+
# Note that the MAC address is just used as a source of randomness, so where you
|
474
|
+
# get it from is not important and doesn't restrict this ULA to just that system.
|
475
|
+
# See RFC4193
|
476
|
+
def generate_ULA(mac, subnet_id = 0, locally_assigned=true)
|
477
|
+
now = Time.now.utc
|
478
|
+
ntp_time = ((now.to_i + 2208988800) << 32) + now.nsec # Convert time to an NTP timstamp.
|
479
|
+
system_id = '::/64'.to_ip.eui_64(mac).to_i # Generate an EUI64 from the provided MAC address.
|
480
|
+
key = [ ntp_time, system_id ].pack('QQ') # Pack the ntp timestamp and the system_id into a binary string
|
481
|
+
global_id = Digest::SHA1.digest( key ).unpack('QQ').last & 0xffffffffff # Use only the last 40 bytes of the SHA1 digest.
|
482
|
+
|
483
|
+
prefix =
|
484
|
+
(126 << 121) + # 0xfc (bytes 0..6)
|
485
|
+
((locally_assigned ? 1 : 0) << 120) + # locally assigned? (byte 7)
|
486
|
+
(global_id << 80) + # 40 bit global idenfitier (bytes 8..48)
|
487
|
+
((subnet_id & 0xffff) << 64) # 16 bit subnet_id (bytes 48..64)
|
488
|
+
|
489
|
+
prefix.to_ip(Socket::AF_INET6).tap { |p| p.length = 64 }
|
490
|
+
end
|
491
|
+
|
492
|
+
end
|
493
|
+
|
494
|
+
end
|
495
|
+
|
496
|
+
module StringIPExtensions
|
497
|
+
def to_ip
|
498
|
+
begin
|
499
|
+
IPAddr.new(self.to_s)
|
500
|
+
rescue ArgumentError => e
|
501
|
+
raise ArgumentError, "invalid address #{self.inspect}"
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
module IntIPExtensions
|
506
|
+
def to_ip(af=nil)
|
507
|
+
if af.nil?
|
508
|
+
## If there is no address family specified then try to guess...
|
509
|
+
if self.to_i > 0xffffffff
|
510
|
+
# If the integer is bigger than any possible IPv4 address
|
511
|
+
# then presume it's an IPv6 address
|
512
|
+
af = Socket::AF_INET6
|
513
|
+
else
|
514
|
+
# otherwise presume it's IPv4
|
515
|
+
af = Socket::AF_INET
|
516
|
+
end
|
517
|
+
end
|
518
|
+
IPAddr.new(self.to_i, af)
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
class IPTuple
|
523
|
+
|
524
|
+
attr_accessor :source_ip, :destination_ip, :ip_protocol, :source_port, :destination_port
|
525
|
+
|
526
|
+
def initialize src_ip=nil, dst_ip=nil, ip_proto=nil, src_port=nil, dst_port=nil
|
527
|
+
@source_ip = src_ip if src_ip.is_a? IPAddr
|
528
|
+
@source_ip = src_ip.to_ip if src_ip.is_a? String
|
529
|
+
@destination_ip = dst_ip if dst_ip.is_a? IPAddr
|
530
|
+
@destination_ip = dst_ip.to_ip if dst_ip.is_a? String
|
531
|
+
@ip_protocol = ip_proto if ip_proto.is_a? IPProtocol
|
532
|
+
@ip_protocol = IPProtocol.new(ip_proto) if ip_proto.is_a? Integer
|
533
|
+
@source_port = src_port if ((1..65535).include? src_port)
|
534
|
+
@destination_port = dst_port if ((1..65535).include? dst_port)
|
535
|
+
end
|
536
|
+
|
537
|
+
def ipv4?
|
538
|
+
if @source_ip && @destination_ip
|
539
|
+
@source_ip.ipv4? && @destination_ip.ipv4?
|
540
|
+
elsif @source_ip
|
541
|
+
@source_ip.ipv4?
|
542
|
+
elsif @destination_ip
|
543
|
+
@destination_ip.ipv4?
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
def ipv6?
|
548
|
+
if @source_ip && @destination_ip
|
549
|
+
@source_ip.ipv6? && @destination_ip.ipv6?
|
550
|
+
elsif @source_ip
|
551
|
+
@source_ip.ipv6?
|
552
|
+
elsif @destination_ip
|
553
|
+
@destination_ip.ipv6?
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
class IPProtocol
|
559
|
+
# See http://www.iana.org/assignments/protocol-numbers/protocol-numbers.txt
|
560
|
+
NUMBERS = (1..142).to_a + [ 255 ]
|
561
|
+
NAMES = {
|
562
|
+
0 => "HOPOPT",
|
563
|
+
1 => "ICMP",
|
564
|
+
2 => "IGMP",
|
565
|
+
3 => "GGP",
|
566
|
+
4 => "IPv4",
|
567
|
+
5 => "ST",
|
568
|
+
6 => "TCP",
|
569
|
+
7 => "CBT",
|
570
|
+
8 => "EGP",
|
571
|
+
9 => "IGP",
|
572
|
+
10 => "BBN-RCC-MON",
|
573
|
+
11 => "NVP-II",
|
574
|
+
12 => "PUP",
|
575
|
+
13 => "ARGUS",
|
576
|
+
14 => "EMCON",
|
577
|
+
15 => "XNET",
|
578
|
+
16 => "CHAOS",
|
579
|
+
17 => "UDP",
|
580
|
+
18 => "MUX",
|
581
|
+
19 => "DCN-MEAS",
|
582
|
+
20 => "HMP",
|
583
|
+
21 => "PRM",
|
584
|
+
22 => "XNS-IDP",
|
585
|
+
23 => "TRUNK-1",
|
586
|
+
24 => "TRUNK-2",
|
587
|
+
25 => "LEAF-1",
|
588
|
+
26 => "LEAF-2",
|
589
|
+
27 => "RDP",
|
590
|
+
28 => "IRTP",
|
591
|
+
29 => "ISO-TP4",
|
592
|
+
30 => "NETBLT",
|
593
|
+
31 => "MFE-NSP",
|
594
|
+
32 => "MERIT-INP",
|
595
|
+
33 => "DCCP",
|
596
|
+
34 => "3PC",
|
597
|
+
35 => "IDPR",
|
598
|
+
36 => "XTP",
|
599
|
+
37 => "DDP",
|
600
|
+
38 => "IDPR-CMTP",
|
601
|
+
39 => "TP++",
|
602
|
+
40 => "IL",
|
603
|
+
41 => "IPv6",
|
604
|
+
42 => "SDRP",
|
605
|
+
43 => "IPv6-Route",
|
606
|
+
44 => "IPv6-Frag",
|
607
|
+
45 => "IDRP",
|
608
|
+
46 => "RSVP",
|
609
|
+
47 => "GRE",
|
610
|
+
48 => "DSR",
|
611
|
+
49 => "BNA",
|
612
|
+
50 => "ESP",
|
613
|
+
51 => "AH",
|
614
|
+
52 => "I-NLSP",
|
615
|
+
53 => "SWIPE",
|
616
|
+
54 => "NARP",
|
617
|
+
55 => "MOBILE",
|
618
|
+
56 => "TLSP",
|
619
|
+
57 => "SKIP",
|
620
|
+
58 => "IPv6-ICMP",
|
621
|
+
59 => "IPv6-NoNxt",
|
622
|
+
60 => "IPv6-Opts",
|
623
|
+
62 => "CFTP",
|
624
|
+
64 => "SAT-EXPAK",
|
625
|
+
65 => "KRYPTOLAN",
|
626
|
+
66 => "RVD",
|
627
|
+
67 => "IPPC",
|
628
|
+
69 => "SAT-MON",
|
629
|
+
70 => "VISA",
|
630
|
+
71 => "IPCV",
|
631
|
+
72 => "CPNX",
|
632
|
+
73 => "CPHB",
|
633
|
+
74 => "WSN",
|
634
|
+
75 => "PVP",
|
635
|
+
76 => "BR-SAT-MON",
|
636
|
+
77 => "SUN-ND",
|
637
|
+
78 => "WB-MON",
|
638
|
+
79 => "WB-EXPAK",
|
639
|
+
80 => "ISO-IP",
|
640
|
+
81 => "VMTP",
|
641
|
+
82 => "SECURE-VMTP",
|
642
|
+
83 => "VINES",
|
643
|
+
84 => "TTP",
|
644
|
+
84 => "IPTM",
|
645
|
+
85 => "NSFNET-IGP",
|
646
|
+
86 => "DGP",
|
647
|
+
87 => "TCF",
|
648
|
+
88 => "EIGRP",
|
649
|
+
89 => "OSPFIGP",
|
650
|
+
90 => "Sprite-RPC",
|
651
|
+
91 => "LARP",
|
652
|
+
92 => "MTP",
|
653
|
+
93 => "AX.25",
|
654
|
+
94 => "IPIP",
|
655
|
+
95 => "MICP",
|
656
|
+
96 => "SCC-SP",
|
657
|
+
97 => "ETHERIP",
|
658
|
+
98 => "ENCAP",
|
659
|
+
100 => "GMTP",
|
660
|
+
101 => "IFMP",
|
661
|
+
102 => "PNNI",
|
662
|
+
103 => "PIM",
|
663
|
+
104 => "ARIS",
|
664
|
+
105 => "SCPS",
|
665
|
+
106 => "QNX",
|
666
|
+
107 => "A/N",
|
667
|
+
108 => "IPComp",
|
668
|
+
109 => "SNP",
|
669
|
+
110 => "Compaq-Peer",
|
670
|
+
111 => "IPX-in-IP",
|
671
|
+
112 => "VRRP",
|
672
|
+
113 => "PGM",
|
673
|
+
115 => "L2TP",
|
674
|
+
116 => "DDX",
|
675
|
+
117 => "IATP",
|
676
|
+
118 => "STP",
|
677
|
+
119 => "SRP",
|
678
|
+
120 => "UTI",
|
679
|
+
121 => "SMP",
|
680
|
+
122 => "SM",
|
681
|
+
123 => "PTP",
|
682
|
+
124 => "ISIS over IPv4",
|
683
|
+
125 => "FIRE",
|
684
|
+
126 => "CRTP",
|
685
|
+
127 => "CRUDP",
|
686
|
+
128 => "SSCOPMCE",
|
687
|
+
129 => "IPLT",
|
688
|
+
130 => "SPS",
|
689
|
+
131 => "PIPE",
|
690
|
+
132 => "SCTP",
|
691
|
+
133 => "FC",
|
692
|
+
134 => "RSVP-E2E-IGNORE",
|
693
|
+
135 => "Mobility Header",
|
694
|
+
136 => "UDPLite",
|
695
|
+
137 => "MPLS-in-IP",
|
696
|
+
138 => "manet",
|
697
|
+
139 => "HIP",
|
698
|
+
140 => "Shim6",
|
699
|
+
141 => "WESP",
|
700
|
+
142 => "ROHC",
|
701
|
+
255 => "Reserved",
|
702
|
+
}
|
703
|
+
|
704
|
+
def initialize proto_num
|
705
|
+
@number = proto_num if NUMBERS.member? proto_num
|
706
|
+
end
|
707
|
+
|
708
|
+
def to_ip
|
709
|
+
@number
|
710
|
+
end
|
711
|
+
|
712
|
+
def to_s
|
713
|
+
NAMES[@number]
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
IPAddr.send(:include, IPAddrExtensions)
|
718
|
+
String.send(:include, StringIPExtensions)
|
719
|
+
Integer.send(:include, IntIPExtensions)
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ipaddr_extensions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Harton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email: james@resistor.io
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- MIT-LICENSE
|
34
|
+
- README
|
35
|
+
- Rakefile
|
36
|
+
- ipaddr_extensions.gemspec
|
37
|
+
- lib/ipaddr_extensions.rb
|
38
|
+
homepage: http://github.com/jamesotron/IPAddrExtensions
|
39
|
+
licenses: []
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 2.0.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: A small gem that adds extra functionality to Rubys IPAddr class
|
61
|
+
test_files: []
|