subnets 1.0.0pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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