Skip to content

Commit 27636b0

Browse files
committed
Replace netaddr gem with stdlib IPAddr
The netaddr gem is effectively unmaintained (no releases despite open requests). Security group rule validation only needs parsing, family detection, and range-endpoint ordering, all of which IPAddr covers. Behaviour is preserved: - leading-zero IPv4 octets are normalised before parsing (IPAddr rejects them, netaddr accepted them); - range endpoints must be plain addresses, so a prefix is rejected there. Add a rule_validator spec covering the v2 validate_destination path, which previously had no dedicated coverage.
1 parent 12e5d15 commit 27636b0

6 files changed

Lines changed: 130 additions & 28 deletions

File tree

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ gem 'httpclient'
1212
gem 'json-diff'
1313
gem 'json-schema'
1414
gem 'mime-types', '~> 3.7'
15-
gem 'netaddr', '>= 2.0.4'
1615
gem 'newrelic_rpm'
1716
gem 'nokogiri', '>=1.10.5'
1817
gem 'oj'

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ GEM
158158
mutex_m (0.3.0)
159159
mysql2 (0.5.7)
160160
bigdecimal
161-
netaddr (2.0.6)
162161
newrelic_rpm (10.6.0)
163162
logger
164163
nio4r (2.7.5)
@@ -459,7 +458,6 @@ DEPENDENCIES
459458
mime-types (~> 3.7)
460459
mock_redis
461460
mysql2 (~> 0.5.7)
462-
netaddr (>= 2.0.4)
463461
newrelic_rpm
464462
nokogiri (>= 1.10.5)
465463
oj

app/messages/validators/security_group_rule_validator.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,30 +153,33 @@ def validate_destination(destination, protocol, allowed_ip_version, record, inde
153153
if address_list.length == 1
154154
parsed_ip = CloudController::RuleValidator.parse_ip(address_list.first)
155155
add_rule_error(error_message, record, index) unless parsed_ip
156-
add_rule_error("for protocol \"#{protocol}\" you cannot use IPv#{parsed_ip.version} addresses", record, index) \
156+
add_rule_error("for protocol \"#{protocol}\" you cannot use IPv#{ip_version(parsed_ip)} addresses", record, index) \
157157
unless valid_ip_version?(allowed_ip_version, parsed_ip)
158158
elsif address_list.length == 2
159159
ips = CloudController::RuleValidator.parse_ip(address_list)
160160

161161
return add_rule_error('destination IP address range is invalid', record, index) unless ips
162162

163-
sorted_ips = if ips.first.is_a?(NetAddr::IPv4)
164-
NetAddr.sort_IPv4(ips)
165-
else
166-
NetAddr.sort_IPv6(ips)
167-
end
163+
sorted_ips = ips.sort
168164

169165
reversed_range_error = 'beginning of IP address range is numerically greater than the end of its range (range endpoints are inverted)'
170166
add_rule_error(reversed_range_error, record, index) unless ips.first == sorted_ips.first
171-
add_rule_error("for protocol \"#{protocol}\" you cannot use IPv#{ips.first.version} addresses", record, index) \
167+
add_rule_error("for protocol \"#{protocol}\" you cannot use IPv#{ip_version(ips.first)} addresses", record, index) \
172168
unless valid_ip_version?(allowed_ip_version, sorted_ips.first)
173169
else
174170
add_rule_error(error_message, record, index)
175171
end
176172
end
177173

174+
def ip_version(parsed_ip)
175+
return 4 if parsed_ip.ipv4?
176+
return 6 if parsed_ip.ipv6?
177+
178+
raise ArgumentError.new("unsupported IP family: #{parsed_ip}")
179+
end
180+
178181
def valid_ip_version?(allowed_ip_version, parsed_ip)
179-
parsed_ip.nil? || allowed_ip_version.nil? || parsed_ip.version == allowed_ip_version
182+
parsed_ip.nil? || allowed_ip_version.nil? || ip_version(parsed_ip) == allowed_ip_version
180183
end
181184

182185
def add_rule_error(message, record, index)

app/models/runtime/security_group.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require 'netaddr'
2-
31
module VCAP::CloudController
42
class SecurityGroup < Sequel::Model
53
SECURITY_GROUP_NAME_REGEX = /\A[[:alnum:][:punct:][:print:]]+\Z/

lib/cloud_controller/rule_validator.rb

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'ipaddr'
2+
13
module CloudController
24
class RuleValidator
35
class_attribute :required_fields, :optional_fields
@@ -53,11 +55,7 @@ def self.validate_destination(destination)
5355
ips = parse_ip(address_list)
5456
return false if ips.nil?
5557

56-
sorted_ips = if ips.first.is_a?(NetAddr::IPv4)
57-
NetAddr.sort_IPv4(ips)
58-
else
59-
NetAddr.sort_IPv6(ips)
60-
end
58+
sorted_ips = ips.sort
6159

6260
return true if ips.first == sorted_ips.first
6361
end
@@ -122,26 +120,46 @@ def self.no_leading_zeros(destination)
122120

123121
private_class_method def self.parse_ipv4(val)
124122
if val.is_a?(Array)
125-
val.map do |ip|
126-
NetAddr::IPv4.parse(ip)
127-
end
123+
val.map { |ip| parse_address(ip, :ipv4?) }
128124
else
129-
NetAddr::IPv4Net.parse(val)
125+
parse_cidr(val, :ipv4?)
130126
end
131-
rescue NetAddr::ValidationError
127+
rescue IPAddr::Error
132128
nil
133129
end
134130

135131
private_class_method def self.parse_ipv6(val)
136132
if val.is_a?(Array)
137-
val.map do |ip|
138-
NetAddr::IPv6.parse(ip)
139-
end
133+
val.map { |ip| parse_address(ip, :ipv6?) }
140134
else
141-
NetAddr::IPv6Net.parse(val)
135+
parse_cidr(val, :ipv6?)
142136
end
143-
rescue NetAddr::ValidationError
137+
rescue IPAddr::Error
144138
nil
145139
end
140+
141+
# A range endpoint must be a plain address, so a prefix is not allowed here.
142+
private_class_method def self.parse_address(val, family)
143+
raise IPAddr::InvalidAddressError if val.include?('/')
144+
145+
parse_cidr(val, family)
146+
end
147+
148+
private_class_method def self.parse_cidr(val, family)
149+
ip = IPAddr.new(drop_leading_zeros(val))
150+
raise IPAddr::InvalidAddressError unless ip.public_send(family)
151+
152+
ip
153+
end
154+
155+
# IPAddr rejects zero-padded IPv4 octets (e.g. '010.0.0.53'); normalise them to plain decimals.
156+
private_class_method def self.drop_leading_zeros(val)
157+
return val unless val.include?('.')
158+
159+
addr, prefix = val.split('/', 2)
160+
# For every octet, delete its leading zeros when a digit follows (e.g. '03.005.10.02' -> '3.5.10.2', but a lone '0' stays); non-numeric octets are left for IPAddr to reject.
161+
addr = addr.split('.').map { |octet| octet.sub(/\A0+(?=\d)/, '') }.join('.')
162+
prefix ? "#{addr}/#{prefix}" : addr
163+
end
146164
end
147165
end
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
require 'spec_helper'
2+
3+
module CloudController
4+
RSpec.describe RuleValidator do
5+
describe '.validate_destination' do
6+
subject { described_class.validate_destination(destination) }
7+
8+
before do
9+
TestConfig.override(
10+
enable_ipv6: true,
11+
security_groups: { enable_comma_delimited_destinations: false }
12+
)
13+
end
14+
15+
context 'with a single valid IPv4 address' do
16+
let(:destination) { '192.168.10.2' }
17+
18+
it { is_expected.to be true }
19+
end
20+
21+
context 'with a valid CIDR' do
22+
let(:destination) { '10.0.0.0/8' }
23+
24+
it { is_expected.to be true }
25+
end
26+
27+
context 'with a valid ascending range' do
28+
let(:destination) { '192.168.10.2-192.168.15.254' }
29+
30+
it { is_expected.to be true }
31+
end
32+
33+
context 'with an inverted range' do
34+
let(:destination) { '200.0.0.0-150.0.0.0' }
35+
36+
it { is_expected.to be false }
37+
end
38+
39+
context 'with a range whose second endpoint is a CIDR' do
40+
let(:destination) { '1.1.1.1-2.2.2.2/30' }
41+
42+
it { is_expected.to be false }
43+
end
44+
45+
context 'with a malformed address' do
46+
let(:destination) { '999.999.999.999' }
47+
48+
it { is_expected.to be false }
49+
end
50+
51+
# This path deliberately does not run the no_leading_zeros check (unlike the v3 message
52+
# validator), so zero-padded octets are accepted and normalised, matching prior behaviour.
53+
context 'with leading zeros' do
54+
context 'in the first octet' do
55+
let(:destination) { '010.0.0.53' }
56+
57+
it { is_expected.to be true }
58+
end
59+
60+
context 'in every octet' do
61+
let(:destination) { '03.005.010.02' }
62+
63+
it { is_expected.to be true }
64+
end
65+
66+
context 'in a CIDR' do
67+
let(:destination) { '010.000.000.000/24' }
68+
69+
it { is_expected.to be true }
70+
end
71+
end
72+
73+
context 'with a valid IPv6 address' do
74+
let(:destination) { '2001:db8::1' }
75+
76+
it { is_expected.to be true }
77+
end
78+
79+
context 'with an inverted IPv6 range' do
80+
let(:destination) { '2001:db8::ff-2001:db8::1' }
81+
82+
it { is_expected.to be false }
83+
end
84+
end
85+
end
86+
end

0 commit comments

Comments
 (0)