subnets 1.0.0pre

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.
@@ -0,0 +1,59 @@
1
+ require 'benchmark'
2
+ require 'ipaddr'
3
+ require 'ipaddress'
4
+ require 'socket'
5
+
6
+ require 'subnets'
7
+ require 'well_known_subnets'
8
+
9
+ # RANDOM_IPS = (
10
+ # (1..1000).map{random_ipv4(CLOUDFRONT_SUBNETS.sample)} +
11
+ # (1..3000).map{PRIVATE_SUBNETS_IPV4.sample}
12
+ # (1..4000).map{random_ipv4}
13
+ # ).shuffle
14
+
15
+ def random_ip(subnet)
16
+ net = Subnets.parse(subnet)
17
+ Subnets::IP4.random & ~net.mask | net.address
18
+ end
19
+
20
+ def plotbarslogscale(prefix: '%-15.15s %5.2f ', width:, min:, max:, tics:[], data:{})
21
+ pos = proc { |x| (Math.log10(x) - Math.log10(min)) * width/Math.log10(max) }
22
+
23
+ # https://en.wikipedia.org/wiki/Block_Elements
24
+ eighths = {
25
+ 8 => "\u2588",
26
+ 7 => "\u2589",
27
+ 6 => "\u258a",
28
+ 5 => "\u258b",
29
+ 4 => "\u258c",
30
+ 3 => "\u258d",
31
+ 2 => "\u258e",
32
+ 1 => "\u258f",
33
+ 0 => "",
34
+ }
35
+
36
+ bar = proc do |w|
37
+ eighths[8]*(w.floor) + eighths[((w - w.floor)*8).round]
38
+ end
39
+
40
+ prefixwidth = 0
41
+ data.each do |k,v|
42
+ str = prefix % [k,v]
43
+ prefixwidth = [prefixwidth, str.size].max
44
+ puts(str + bar.call(pos.call(v)))
45
+ end
46
+
47
+ if tics.size > 0
48
+ ticbar = ''
49
+ labelbar = ''
50
+ tics.each do |tic|
51
+ x = pos.call(tic).round
52
+ ticbar += (' '*(x - ticbar.size) + "'")
53
+ labelbar += (' '*(x - labelbar.size) + "#{tic}")
54
+ end
55
+
56
+ puts(' '*prefixwidth + ticbar)
57
+ puts(' '*prefixwidth + labelbar)
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ module EqlAndHash
2
+ def new_obj(c=klass)
3
+ c.new(*constructor_args)
4
+ end
5
+
6
+ def new_obj_subclass
7
+ new_obj(Class.new(klass))
8
+ end
9
+
10
+ def test_eq
11
+ a, b = [new_obj, new_obj]
12
+ refute_equal a.object_id, b.object_id
13
+ assert_equal a, b
14
+ end
15
+
16
+ def test_eq_subclass
17
+ refute_equal new_obj, new_obj_subclass
18
+ end
19
+
20
+ def test_eq_string
21
+ refute_equal new_obj, 'str'
22
+ end
23
+
24
+ def test_hash
25
+ a, b = [new_obj, new_obj]
26
+ refute_equal a.object_id, b.object_id
27
+ assert_equal a.hash, b.hash
28
+
29
+ h = {}
30
+ h[a] = 'found'
31
+ assert_equal 'found', h[a]
32
+ assert_equal 'found', h[b]
33
+ assert_nil h[new_obj_subclass]
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ require 'benchmark_helper'
2
+
3
+ require 'ipaddr'
4
+ require 'ipaddress'
5
+ require 'netaddr'
6
+ require 'subnets'
7
+ require 'rack'
8
+
9
+ nets = PRIVATE_SUBNETS_IPV4
10
+ ipaddr_nets = nets.map(&IPAddr.method(:new))
11
+ ipaddress_nets = nets.map(&IPAddress::IPv4.method(:new))
12
+ netaddr_nets = nets.map(&NetAddr::IPv4Net.method(:parse))
13
+ subnets_nets = nets.map(&Subnets.method(:parse))
14
+ rack_nets = Rack::Request.new({})
15
+
16
+ # note: ipaddress and netaddr checks would need additional cases to handle ipv6 too
17
+ ipaddr_check = lambda {|ip| ipaddr_nets.any? { |net| net.include? ip } }
18
+ ipaddress_check = lambda {|ip| ipaddress_nets.any? { |net| net.include?(IPAddress::IPv4.new(ip)) } }
19
+ netaddr_check = lambda {|ip| netaddr_nets.any? { |net| net.contains(NetAddr::IPv4.parse(ip)) } }
20
+ subnets_check = lambda {|ip| Subnets.include?(subnets_nets, ip) }
21
+ rack_check = lambda {|ip| rack_nets.trusted_proxy?(ip) }
22
+
23
+ def ipaddr_check.name; 'ipaddr'; end
24
+ def ipaddress_check.name; 'ipaddress'; end
25
+ def netaddr_check.name; 'netaddr'; end
26
+ def subnets_check.name; 'subnets'; end
27
+ def rack_check.name; 'rack (regexp)'; end
28
+
29
+ results = {}
30
+
31
+ puts '#'*60
32
+ puts "# check if single IP is in the private IPv4 subnets"
33
+ [ipaddr_check, ipaddress_check, netaddr_check, subnets_check, rack_check].each do |check|
34
+ total = count = hits = 0
35
+ until total >= 2
36
+ net = Subnets.parse((['0.0.0.0/0'] + PRIVATE_SUBNETS_IPV4).sample)
37
+ ip = (Subnets::IP4.random & ~net.mask | net.address).to_s
38
+
39
+ total += Benchmark.measure {
40
+ 100.times do |i|
41
+ hits += 1 if check.call(ip)
42
+ end
43
+ }.total
44
+ count += 100
45
+ end
46
+ puts "%10.10s: checked %8d (%6d hits, %2d%%) in %2.2f for %7.2fus/ip" %
47
+ [check.name, count, hits, 100.0*hits/count, total, total/count*1e6]
48
+
49
+ results[check.name] = total/count*1e6
50
+ end
51
+
52
+ puts
53
+ plotbarslogscale(prefix: '%-15.15s: %5.2fus/ip ', width: 46, min: 2, max: 65, tics: [2,5,10,20,50], data: results)
@@ -0,0 +1,45 @@
1
+ require 'benchmark_helper'
2
+
3
+ require 'rack'
4
+
5
+ def rack_trusted_proxy_benchmark(name, request)
6
+ total = count = hits = 0
7
+ until total >= 3
8
+ ip = random_ip((['0.0.0.0/0'] + PRIVATE_SUBNETS_IPV4).sample).to_s
9
+
10
+ 100.times {
11
+ is_trusted = false
12
+ total += Benchmark.measure {
13
+ is_trusted = request.trusted_proxy?(ip)
14
+ }.total
15
+ hits += 1 if is_trusted
16
+ count += 1
17
+ }
18
+ end
19
+
20
+ puts
21
+ puts "checked %d IPs @ %.2fμs/ip (%d%% trusted)" %
22
+ [count, 1e6*total/count, 100.0*hits/count]
23
+ plotbarslogscale(
24
+ prefix: '%-12.12s %4.2fμs/ip ', width: 36, min: 1, max: 5, tics: [1,2,5],
25
+ data: { name => 1e6*total/count })
26
+ end
27
+
28
+ rack_trusted_proxy_benchmark(
29
+ 'rack', Rack::Request.new({}))
30
+
31
+ subnets = PRIVATE_SUBNETS.map(&Subnets.method(:parse))
32
+ def subnets.trusted_proxy?(ip)
33
+ Subnets.include?(self, ip)
34
+ end
35
+
36
+ rack_trusted_proxy_benchmark(
37
+ 'subnets', subnets)
38
+
39
+ subnets_cf = (PRIVATE_SUBNETS + CLOUDFRONT_SUBNETS).map(&Subnets.method(:parse))
40
+ def subnets_cf.trusted_proxy?(ip)
41
+ Subnets.include?(self, ip)
42
+ end
43
+
44
+ rack_trusted_proxy_benchmark(
45
+ 'subnets +CF', subnets_cf)
@@ -0,0 +1,104 @@
1
+ require 'benchmark_helper'
2
+
3
+ require 'action_dispatch/middleware/remote_ip'
4
+
5
+ def benchmark(name, getip, mkrequest)
6
+ total = count = request_count = untrusted = 0
7
+
8
+ until total >= 1
9
+ request = mkrequest.call
10
+
11
+ total += Benchmark.measure {
12
+ untrusted += getip.send(:filter_proxies, request).size
13
+ }.total
14
+
15
+ count += request.size
16
+ request_count += 1
17
+ end
18
+
19
+ trusted = count - untrusted
20
+ puts
21
+ puts "checked %d requests at %.2fμs/req finding %d trusted ips (%d%%)" %
22
+ [request_count, total/request_count*1e6, trusted, 100.0*trusted/count]
23
+
24
+ plotbarslogscale(prefix: '%-15.15s %7.2fμs/req ',
25
+ width: 36, min: 1, max: 2000, tics: [1,10,100,1000],
26
+ data: { name => total/request_count*1e6 })
27
+ end
28
+
29
+ pub_priv_priv = ->() {
30
+ [
31
+ random_ip('0.0.0.0/0'),
32
+ random_ip(PRIVATE_SUBNETS_IPV4.sample),
33
+ random_ip(PRIVATE_SUBNETS_IPV4.sample),
34
+ ].map(&:to_s)
35
+ }
36
+
37
+ ############################################################
38
+ # as-is rails remote_ip
39
+
40
+ proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES
41
+ getip = ActionDispatch::RemoteIp::GetIp.new(nil, nil, proxies)
42
+ benchmark('remote_ip', getip, pub_priv_priv)
43
+
44
+ ############################################################
45
+ # as-is rails remote_ip with CloudFront
46
+
47
+ pub_cf_priv_priv = ->() {
48
+ [
49
+ random_ip('0.0.0.0/0'),
50
+ random_ip(CLOUDFRONT_SUBNETS.sample),
51
+ random_ip(PRIVATE_SUBNETS_IPV4.sample),
52
+ random_ip(PRIVATE_SUBNETS_IPV4.sample),
53
+ ].map(&:to_s)
54
+ }
55
+
56
+ proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES +
57
+ CLOUDFRONT_SUBNETS.map(&IPAddr.method(:new))
58
+ getip = ActionDispatch::RemoteIp::GetIp.new(nil, nil, proxies)
59
+ benchmark('remote_ip +CF', getip, pub_cf_priv_priv)
60
+
61
+ ############################################################
62
+ # naive replacement of IPAddr with Subnets-derived objects
63
+
64
+ proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES.
65
+ map{|p| "#{p}/#{p.prefix}"}.map(&Subnets.method(:parse))
66
+ getip = ActionDispatch::RemoteIp::GetIp.new(nil, nil, proxies)
67
+ benchmark('subnets', getip, pub_priv_priv)
68
+
69
+ proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES.
70
+ map{|p| "#{p}/#{p.prefix}"}.map(&Subnets.method(:parse)) +
71
+ CLOUDFRONT_SUBNETS.map(&Subnets.method(:parse))
72
+ getip = ActionDispatch::RemoteIp::GetIp.new(nil, nil, proxies)
73
+ benchmark('subnets +CF', getip, pub_cf_priv_priv)
74
+
75
+ ############################################################
76
+ # hack to use fast Subnets.include?
77
+
78
+ Identity = Object.new
79
+ def Identity.===(v); v; end
80
+
81
+ # instead of letting Ruby call the block for every element of the
82
+ # list, call it once to extract the ip to be tested, then use fast
83
+ # Subnets.include? method.
84
+ proxies2 = proxies.dup
85
+
86
+ def proxies2.any?(&block)
87
+ ip = block.call(Identity)
88
+ Subnets.include?(self, ip)
89
+ end
90
+
91
+ getip = ActionDispatch::RemoteIp::GetIp.new(nil, nil, proxies2)
92
+ benchmark('hack +CF', getip, pub_cf_priv_priv)
93
+
94
+ ############################################################
95
+ # Alternate impl to really use fast Subnets.include?
96
+
97
+ Alternate = Struct.new(:proxies) do
98
+ def filter_proxies(ips)
99
+ ips.reject{|ip| Subnets.include?(self.proxies, ip)}
100
+ end
101
+ end
102
+
103
+ getip = Alternate.new(proxies)
104
+ benchmark('alt +CF', getip, pub_cf_priv_priv)
@@ -0,0 +1,74 @@
1
+ require 'test_helper'
2
+
3
+ module Subnets
4
+ class TestNet4 < Minitest::Test
5
+ include EqlAndHash
6
+
7
+ def klass
8
+ Net4
9
+ end
10
+
11
+ def constructor_args
12
+ [1,24]
13
+ end
14
+
15
+ def test_new_creates_net4
16
+ assert_instance_of Net4, Net4.new(1, 1)
17
+ end
18
+
19
+ def test_new_rejects_invalid_prefixlen
20
+ assert_raises(ArgumentError) { Net4.new(1, 33) }
21
+ end
22
+
23
+ class ParseBadInput < Minitest::Test
24
+ data = [
25
+ 'a', '12', '1.2.3', '1/2', '',
26
+ ]
27
+
28
+ data.each_with_index do |s, i|
29
+ define_method("test_parse_fail_%02d" % i) do
30
+ assert_raises(ParseError) { Net4.parse(s) }
31
+ end
32
+ end
33
+ end
34
+
35
+ class Parse < Minitest::Test
36
+ data = ['127.0.0.1/32', '192.168.0.0/24', '10.1.0.0/16', '1.2.3.4/2']
37
+
38
+ data.each_with_index do |cidr, i|
39
+ define_method("test_parse_%02d" % i) do
40
+ assert_instance_of Net4, Net4.parse(cidr)
41
+ end
42
+ end
43
+ end
44
+
45
+ def test_includes_ip
46
+ net = Net4.parse '192.168.0.0/24'
47
+ assert_include net, '192.168.0.0'
48
+ assert_include net, Subnets.parse('192.168.0.2')
49
+ assert_include net, '192.168.0.255'
50
+
51
+ refute_include net, '192.168.1.0'
52
+ refute_include net, Subnets.parse('10.168.0.2')
53
+ end
54
+
55
+ def test_includes_net
56
+ net = Net4.parse '192.168.0.0/24'
57
+ assert_include net, '192.168.0.0/24'
58
+ assert_include net, Net4.parse('192.168.0.2/26')
59
+ refute_include net, '192.168.0.0/22'
60
+ refute_include net, Net4.parse('10.168.0.0/16')
61
+ end
62
+
63
+ def test_random_roundtrip
64
+ random = Random.new
65
+ start = Time.now
66
+ until Time.now - start > TIMED_TEST_DURATION
67
+ 1000.times do
68
+ net = Net4.random(random).to_s
69
+ assert_equal net, Net4.parse(net).to_s
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,125 @@
1
+ require 'test_helper'
2
+ require 'ipaddr'
3
+
4
+ module Subnets
5
+ class TestNet6 < Minitest::Test
6
+ include EqlAndHash
7
+
8
+ # for EqlAndHash
9
+ def klass
10
+ Net6
11
+ end
12
+
13
+ # for EqlAndHash
14
+ def constructor_args
15
+ [[1,2,3,4,5,6,7,8], 96]
16
+ end
17
+
18
+ class New < Minitest::Test
19
+ def test_rejects_invalid_prefixlen
20
+ assert_raises(ArgumentError) { Net6.new([0,0,0,0,0,0,0,0], 129) }
21
+ end
22
+
23
+ def test_rejects_invalid_hextets
24
+ assert_raises(ArgumentError) { Net6.new([0,0,0,0,0,0,0], 128) }
25
+ assert_raises(ArgumentError) { Net6.new([0,0,0,0,0,0,0,0,0], 128) }
26
+ end
27
+
28
+ def test_creates_net6
29
+ assert_equal Net6.parse('1:2:44:55:ef::/128'),
30
+ Net6.new([0x1,0x2,0x44,0x55,0xef,0,0,0], 128)
31
+ end
32
+ end
33
+
34
+ class ParseBadInput < Minitest::Test
35
+ data = [
36
+ { name: 'leading single colon', s: ':1::' },
37
+ { name: 'non-hexadecimal', s: 'u' },
38
+ { name: 'too short', s: '12:' },
39
+ { name: 'too short', s: ':1' },
40
+ { name: 'missing compression', s: '1/2' },
41
+ { name: 'trailing single colon', s: '1::1:/1' },
42
+ { name: 'leading zero in hextet', s: '045:1:2::/32' },
43
+ { name: 'prefixlen too large', s: '1::/129' },
44
+ ]
45
+
46
+ data.each_with_index do |d, i|
47
+ define_method("test_parse_fail_%02d #{d[:name]}" % i) do
48
+ assert_raises(ParseError) { Net6.parse(d[:s]) }
49
+ end
50
+ end
51
+ end
52
+
53
+ class Parse < Minitest::Test
54
+ data = [
55
+ { name: 'zero', s: '::/0' },
56
+ { name: 'localhost', s: '::1/128' },
57
+ { name: 'first-one', s: '1::/128' },
58
+ { name: 'one-one', s: '1::1/32' },
59
+ { name: 'ip', s: 'ffe3::ff01/32' },
60
+ { name: 'ip', s: '46db:20af:2b68:4034::871f:0/83' },
61
+ { name: 'full', s: '1:2:3:4:5:6:7:8/96' },
62
+ { name: 'many-zeros', s: '0:0:0:1:0:0:0:0/44', to_s: '0:0:0:1::/44' },
63
+ { name: 'embedded ipv4', s: '::1.2.3.4/96', to_s: '::102:304/96' },
64
+ { name: 'embedded ipv4', s: 'fe:55::1.2.3.4/96', to_s: 'fe:55::102:304/96' },
65
+ { name: 'embedded ipv4', s: 'a:b:c:d:e:f:1.2.3.4/128', to_s: 'a:b:c:d:e:f:102:304/128' },
66
+ ]
67
+
68
+ data.each_with_index do |d, i|
69
+ define_method("test_parse_%02d #{d[:name]}" % i) do
70
+ assert_instance_of Net6, Net6.parse(d[:s])
71
+ end
72
+
73
+ define_method("test_to_s_%02d #{d[:name]}" % i) do
74
+ assert_equal (d[:to_s] || d[:s]), Net6.parse(d[:s]).to_s
75
+ end
76
+
77
+ define_method("test_prefixlen_%02d #{d[:name]}" % i) do
78
+ raise "#{d[:s]} didn't match regex" unless d[:s] =~ /\/(\d+)\z/
79
+ assert_equal $1.to_i, Net6.parse(d[:s]).prefixlen
80
+ end
81
+ end
82
+ end
83
+
84
+ def test_random_roundtrip
85
+ random = Random.new
86
+ start = Time.now
87
+ until Time.now - start > TIMED_TEST_DURATION
88
+ 1000.times do
89
+ net = Net6.random(random, zeros: true).to_s
90
+ assert_equal net, Net6.parse(net).to_s
91
+ end
92
+ end
93
+ end
94
+
95
+ def test_prefixlen
96
+ (0..128).each do |i|
97
+ assert_equal i, Net6.parse("::1/#{i}").prefixlen
98
+ end
99
+ end
100
+
101
+ class Include < Minitest::Test
102
+ def setup
103
+ @net = Net6.parse '1:2:3:4:5:6:7:8/96'
104
+ end
105
+
106
+ def test_includes_ips
107
+ ['1:2:3:4:5:6:7:9', '1:2:3:4:5:6:99::'].each do |ip|
108
+ assert_include @net, ip
109
+ end
110
+ end
111
+
112
+ def test_excludes_ips
113
+ ['5::', '1:2:3:99::'].each do |ip|
114
+ refute_include @net, ip
115
+ end
116
+ end
117
+
118
+ def test_returns_false_with_non_ips_or_nets
119
+ ['a', /a/, 2].each do |obj|
120
+ refute_include @net, obj
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end