From 2a5e5e15479c72b763b05c6293ed3447d3e5220e Mon Sep 17 00:00:00 2001 From: Martin Schmidt Date: Fri, 23 Oct 2015 00:54:50 +0200 Subject: [PATCH 1/2] Added methods for easy usage with ActiveRecord models. Inspired by IPAddr. --- README.rdoc | 53 ++++++++++++++++++++++++++++++++++++++++++------- lib/ip/base.rb | 30 ++++++++++++++++++++++++++++ test/ip_test.rb | 49 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 7 deletions(-) diff --git a/README.rdoc b/README.rdoc index cb6be78..2de49d9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -72,19 +72,18 @@ Docs:: http://deploy2.github.com/ruby-ip/ * Advanced Subnet Operations sn = IP.new('192.168.0.0/24') ip = IP.new('192.168.0.48/32') - sn.split [#, + sn.split [#, #] (2 evenly divided subnets) - sn.divide_by_subnets(3) [#, - #, - #, + sn.divide_by_subnets(3) [#, + #, + #, #] (4 evenly divided subnets) #keep in mind this always takes into account a network and broadcast address - sn.divide_by_hosts(100) [#, + sn.divide_by_hosts(100) [#, #] (128 hosts each) ip = IP.new('192.168.0.48/32') ip.is_in?(sn) - => true - + => true * Convert to and from a compact Array representation @@ -105,6 +104,46 @@ Docs:: http://deploy2.github.com/ruby-ip/ * Addresses are Comparable, sortable, and can be used as Hash keys +* Addresses can be stored in a VARBINARY(16) database column as string of bytes + + # == Schema Information + # + # Table name: ipaddresses + # + # id :integer not null, primary key + # address :binary(16) + # pfxlen :integer + # created_at :datetime not null + # updated_at :datetime not null + # + # Indexes + # + # index_ipaddresses_on_address (address) + # index_ipaddresses_on_pfxlen (pfxlen) + # + + require 'ip' + class IpAddress < ActiveRecord::Base + validates_uniqueness_of :address, scope: [:pfxlen] + validates_presence_of :address, :pfxlen + + # IpAddress.last.address returns an IP object + def address + IP.new("#{IP.new_ntoh(super).to_s}/#{pfxlen}") + end + + # Address can be set with string: + # ipaddress = IpAddress.new({address: '2001:db8:be00::/128'}) + # Or IP object: + # ip = IP.new('2001:db8:be00::/128') + # ipaddress = IpAddress.new({address: ip}) + def address=(arg) + ip = IP.new(arg) + self.pfxlen = ip.pfxlen + super(ip.to_hton) + end + end + == Why not IPAddr? Ruby bundles an IPAddr class (ipaddr.rb). However there are a number of diff --git a/lib/ip/base.rb b/lib/ip/base.rb index 6ed5a71..dba8eb0 100644 --- a/lib/ip/base.rb +++ b/lib/ip/base.rb @@ -64,6 +64,26 @@ def proto self.class::PROTO end + # Creates a new ip containing the given network byte ordered + # string form of an IP address. + def IP::new_ntoh(addr) + return IP.new(IP::ntop(addr)) + end + + # Convert a network byte ordered string form of an IP address into + # human readable form. + def IP::ntop(addr) + case addr.size + when 4 + s = addr.unpack('C4').join('.') + when 16 + s = (["%.4x"] * 8).join(':') % addr.unpack('n8') + else + fail ArgumentError, 'Invalid address value' + end + return s + end + # Return the string representation of the address, x.x.x.x[/pfxlen][@ctx] def to_s ctx ? "#{to_addrlen}@#{ctx}" : to_addrlen @@ -361,6 +381,11 @@ def to_arpa (@addr >> 16) & 0xff, (@addr >> 24) & 0xff) end + + # Returns a network byte ordered string form of the IP address. + def to_hton + [@addr].pack('N') + end end class V6 < IP @@ -452,6 +477,11 @@ def to_addr_full end end + # Returns a network byte ordered string form of the IP address. + def to_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 diff --git a/test/ip_test.rb b/test/ip_test.rb index 6a69ed3..3f2cbcd 100644 --- a/test/ip_test.rb +++ b/test/ip_test.rb @@ -67,6 +67,14 @@ class IPTest < Minitest::Test assert(s1.object_id != s2.object_id) end + it 'builds from hton' do + hton = "\x01\x02\x03\x04" + res = IP.new_ntoh(hton) + assert_equal IP::V4, res.class + assert_equal 32, res.pfxlen + assert_nil res.ctx + end + it 'disallows invalid addr' do assert_raises(ArgumentError) { IP::V4.new(1 << 32) } assert_raises(ArgumentError) { IP::V4.new(-1) } @@ -112,6 +120,10 @@ class IPTest < Minitest::Test assert_equal '4.3.2.1.in-addr.arpa.', @addr.to_arpa end + it 'has to_hton' do + assert_equal "\x01\x02\x03\x04", @addr.to_hton + end + it 'has to_i' do assert_equal 0x01020304, @addr.to_i end @@ -402,6 +414,14 @@ class IPTest < Minitest::Test assert(s1.object_id != s2.object_id) end + it 'builds from hton' do + hton = IP::V6.new(0xdeadbeef000000000000000000000123, 24, 'foo').to_hton + res = IP.new_ntoh(hton) + assert_equal IP::V6, res.class + assert_equal 128, res.pfxlen + assert_nil res.ctx + end + it 'disallows invalid addr' do assert_raises(ArgumentError) { IP::V6.new(1 << 128) } assert_raises(ArgumentError) { IP::V6.new(-1) } @@ -427,6 +447,14 @@ class IPTest < Minitest::Test res = IP.new('dead:beef::123') assert_equal '3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.ip6.arpa', res.to_arpa end + + it 'has to_hton' do + res = String.new + IP.new('dead:beef::123').to_hton.each_byte { |c| + res += sprintf("%02x", c) + } + assert_equal 'deadbeef000000000000000000000123', res + end end describe 'v6 ::0' do @@ -445,6 +473,10 @@ class IPTest < Minitest::Test it 'has to_arpa' do assert_equal '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end + + it 'has to_hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", @addr.to_hton + end end describe 'v6 ::1' do @@ -463,6 +495,10 @@ class IPTest < Minitest::Test it 'has to_arpa' do assert_equal '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end + + it 'has to_hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", @addr.to_hton + end end describe 'v6 compat' do @@ -487,6 +523,10 @@ class IPTest < Minitest::Test it 'has to_arpa' do assert_equal '4.0.3.0.2.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end + + it 'has to_hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04", @addr.to_hton + end end describe 'v6 mapped' do @@ -512,6 +552,11 @@ class IPTest < Minitest::Test assert_equal '4.0.3.0.2.0.1.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end + it 'has to_hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\x01\x02\x03\x04".force_encoding("ASCII-8BIT"), + @addr.to_hton + end + it 'converts v6 addresses unambiguously' do assert_equal '1:0:2::', IP.new('1:0:2:0:0:0:0:0').to_s assert_equal '1::2:0', IP.new('1:0:0:0:0:0:2:0').to_s @@ -587,6 +632,10 @@ class IPTest < Minitest::Test assert_equal '4.3.2.1.in-addr.arpa.', @addr.to_arpa end + it 'responds to to_hton withouth a TypeError' do + assert_equal "\x01\x02\x03\x04", @addr.to_hton + end + it 'responds to to_i withouth a TypeError' do assert_equal 16_909_060, @addr.to_i end From 1ccf247c81de0150bdfc8e1fe9d4ceebf0910ff6 Mon Sep 17 00:00:00 2001 From: Martin Schmidt Date: Thu, 5 Nov 2015 09:31:31 +0100 Subject: [PATCH 2/2] changed to better name --- README.rdoc | 16 +++++++++++++++- lib/ip/base.rb | 4 ++-- test/ip_test.rb | 30 +++++++++++++++--------------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/README.rdoc b/README.rdoc index 2de49d9..77cdb5c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -22,6 +22,20 @@ Docs:: http://deploy2.github.com/ruby-ip/ ip.to_arpa # "53.2.0.192.in-addr.arpa." ip.pfxlen # 24 +* Convert from IPAddr to IP and back to IPAddr + + # new IPAddr + ipaddr = IPAddr.new('192.168.2.1') + # => # + + # convert it to IP + ip_from_ipaddr = IP.new_ntoh(ipaddr.hton) + # => + + # and back to IPAddr + ipaddr_from_ip = IPAddr.new_ntoh(ip_from_ipaddr.hton) + # => # + * Qualify IP address with "routing context" (VRF) ip = IP.new("192.0.2.53/24@cust1") @@ -140,7 +154,7 @@ Docs:: http://deploy2.github.com/ruby-ip/ def address=(arg) ip = IP.new(arg) self.pfxlen = ip.pfxlen - super(ip.to_hton) + super(ip.hton) end end diff --git a/lib/ip/base.rb b/lib/ip/base.rb index dba8eb0..e195aa6 100644 --- a/lib/ip/base.rb +++ b/lib/ip/base.rb @@ -383,7 +383,7 @@ def to_arpa end # Returns a network byte ordered string form of the IP address. - def to_hton + def hton [@addr].pack('N') end end @@ -478,7 +478,7 @@ def to_addr_full end # Returns a network byte ordered string form of the IP address. - def to_hton + def hton (0..7).map { |i| (@addr >> (112 - 16 * i)) & 0xffff }.pack('n8') end diff --git a/test/ip_test.rb b/test/ip_test.rb index 3f2cbcd..4c867b7 100644 --- a/test/ip_test.rb +++ b/test/ip_test.rb @@ -120,8 +120,8 @@ class IPTest < Minitest::Test assert_equal '4.3.2.1.in-addr.arpa.', @addr.to_arpa end - it 'has to_hton' do - assert_equal "\x01\x02\x03\x04", @addr.to_hton + it 'has hton' do + assert_equal "\x01\x02\x03\x04", @addr.hton end it 'has to_i' do @@ -415,7 +415,7 @@ class IPTest < Minitest::Test end it 'builds from hton' do - hton = IP::V6.new(0xdeadbeef000000000000000000000123, 24, 'foo').to_hton + hton = IP::V6.new(0xdeadbeef000000000000000000000123, 24, 'foo').hton res = IP.new_ntoh(hton) assert_equal IP::V6, res.class assert_equal 128, res.pfxlen @@ -448,9 +448,9 @@ class IPTest < Minitest::Test assert_equal '3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.ip6.arpa', res.to_arpa end - it 'has to_hton' do + it 'has hton' do res = String.new - IP.new('dead:beef::123').to_hton.each_byte { |c| + IP.new('dead:beef::123').hton.each_byte { |c| res += sprintf("%02x", c) } assert_equal 'deadbeef000000000000000000000123', res @@ -474,8 +474,8 @@ class IPTest < Minitest::Test assert_equal '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end - it 'has to_hton' do - assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", @addr.to_hton + it 'has hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", @addr.hton end end @@ -496,8 +496,8 @@ class IPTest < Minitest::Test assert_equal '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end - it 'has to_hton' do - assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", @addr.to_hton + it 'has hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", @addr.hton end end @@ -524,8 +524,8 @@ class IPTest < Minitest::Test assert_equal '4.0.3.0.2.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end - it 'has to_hton' do - assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04", @addr.to_hton + it 'has hton' do + assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04", @addr.hton end end @@ -552,9 +552,9 @@ class IPTest < Minitest::Test assert_equal '4.0.3.0.2.0.1.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', @addr.to_arpa end - it 'has to_hton' do + it 'has hton' do assert_equal "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\x01\x02\x03\x04".force_encoding("ASCII-8BIT"), - @addr.to_hton + @addr.hton end it 'converts v6 addresses unambiguously' do @@ -632,8 +632,8 @@ class IPTest < Minitest::Test assert_equal '4.3.2.1.in-addr.arpa.', @addr.to_arpa end - it 'responds to to_hton withouth a TypeError' do - assert_equal "\x01\x02\x03\x04", @addr.to_hton + it 'responds to hton withouth a TypeError' do + assert_equal "\x01\x02\x03\x04", @addr.hton end it 'responds to to_i withouth a TypeError' do