Validate VirtualBox hostonly network range

VirtualBox introduced a restriction on the valid range for hostonly
    networks. When using a version of VirtualBox which includes this
    restriction a check is performed on the defined IP address to validate
    it is within either the default range (as defined in the VirtualBox
    documentation) or the values defined in the network configuration
    file.
This commit is contained in:
Chris Roberts 2021-10-28 17:12:15 -07:00
parent c45de775e8
commit ae7639ec23
4 changed files with 246 additions and 12 deletions

View File

@ -1024,6 +1024,10 @@ module Vagrant
error_key(:virtualbox_version_empty)
end
class VirtualBoxInvalidHostSubnet < VagrantError
error_key(:virtualbox_invalid_host_subnet)
end
class VMBaseMacNotSpecified < VagrantError
error_key(:no_base_mac, "vagrant.actions.vm.match_mac")
end

View File

@ -16,6 +16,14 @@ module VagrantPlugins
#
# This handles all the `config.vm.network` configurations.
class Network
# Location of the VirtualBox networks configuration file
VBOX_NET_CONF = "/etc/vbox/networks.conf".freeze
# Version of VirtualBox that introduced hostonly network range restrictions
HOSTONLY_VALIDATE_VERSION = Gem::Version.new("6.1.28").freeze
# Default valid range for hostonly networks
HOSTONLY_DEFAULT_RANGE = [IPAddr.new("192.68.56.0/21").freeze].freeze
include Vagrant::Util::NetworkIP
include Vagrant::Util::ScopedHashOverride
@ -255,7 +263,7 @@ module VagrantPlugins
# Make sure the type is a symbol
options[:type] = options[:type].to_sym
if options[:type] == :dhcp && !options[:ip]
# Try to find a matching device to set the config ip to
matching_device = hostonly_find_matching_network(options)
@ -263,7 +271,7 @@ module VagrantPlugins
options[:ip] = matching_device[:ip]
else
# Default IP is in the 20-bit private network block for DHCP based networks
options[:ip] = "172.28.128.1"
options[:ip] = "192.68.56.1"
end
end
@ -288,6 +296,8 @@ module VagrantPlugins
error: e.message
end
validate_hostonly_ip!(options[:ip])
if ip.ipv4?
# Verify that a host-only network subnet would not collide
# with a bridged networking interface.
@ -501,6 +511,33 @@ module VagrantPlugins
nil
end
# Validates the IP used to configure the network is within the allowed
# ranges. It only validates if the network configuration file exists.
# This was introduced in 6.1.28 so previous version won't have restrictions
# placed on the valid ranges
def validate_hostonly_ip!(ip)
return if Gem::Version.new(Driver::Meta.version) < HOSTONLY_VALIDATE_VERSION ||
Vagrant::Util::Platform.windows?
ip = IPAddr.new(ip.to_s) if !ip.is_a?(IPAddr)
valid_ranges = load_net_conf
return if valid_ranges.any?{ |range| range.include?(ip) }
raise Vagrant::Errors::VirtualBoxInvalidHostSubnet,
address: ip,
ranges: valid_ranges.map{ |r| "#{r}/#{r.prefix}" }.join(", ")
end
def load_net_conf
return HOSTONLY_DEFAULT_RANGE if !File.exist?(VBOX_NET_CONF)
File.readlines(VBOX_NET_CONF).map do |line|
line = line.strip
next if !line.start_with?("*")
line[1,line.length].strip.split(" ").map do |entry|
IPAddr.new(entry)
end
end.flatten.compact
end
#-----------------------------------------------------------------
# DHCP Server Helper Functions
#-----------------------------------------------------------------

View File

@ -1842,6 +1842,18 @@ en:
outputted:
%{vboxmanage} --version
virtualbox_invalid_host_subnet: |-
The IP address configured for the host-only network is not within the
allowed ranges. Please update the address used to be within the allowed
ranges and run the command again.
Address: %{address}
Ranges: %{ranges}
Valid ranges can be modified in the /etc/vbox/networks.conf file. For
more information including valid format see:
https://www.virtualbox.org/manual/ch06.html#network_hostonly
vm_creation_required: |-
VM must be created before running this command. Run `vagrant up` first.
vm_inaccessible: |-

View File

@ -30,6 +30,187 @@ describe VagrantPlugins::ProviderVirtualBox::Action::Network do
before do
allow(driver).to receive(:enable_adapters)
allow(driver).to receive(:read_network_interfaces) { nics }
allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:version).
and_return("6.1.0")
end
describe "#hostonly_config" do
before do
allow(subject).to receive(:hostonly_find_matching_network)
allow(driver).to receive(:read_bridged_interfaces).and_return([])
subject.instance_eval do
def env=(e)
@env = e
end
end
subject.env = env
end
let(:options) {
{
type: type,
ip: address,
}
}
let(:type) { :dhcp }
let(:address) { nil }
it "should validate the IP" do
expect(subject).to receive(:validate_hostonly_ip!)
subject.hostonly_config(options)
end
end
describe "#validate_hostonly_ip!" do
let(:address) { "192.168.1.2" }
let(:net_conf) { [IPAddr.new(address + "/24")]}
before do
allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:version).
and_return("6.1.28")
allow(subject).to receive(:load_net_conf).and_return(net_conf)
expect(subject).to receive(:validate_hostonly_ip!).and_call_original
end
it "should load net configuration" do
expect(subject).to receive(:load_net_conf).and_return(net_conf)
subject.validate_hostonly_ip!(address)
end
context "when address is within ranges" do
it "should not error" do
subject.validate_hostonly_ip!(address)
end
end
context "when address is not found within ranges" do
let(:net_conf) { [IPAddr.new("127.0.0.1/20")] }
it "should raise an error" do
expect {
subject.validate_hostonly_ip!(address)
}.to raise_error(Vagrant::Errors::VirtualBoxInvalidHostSubnet)
end
end
context "when virtualbox version does not restrict range" do
before do
allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:version).
and_return("6.1.20")
end
it "should not error" do
subject.validate_hostonly_ip!(address)
end
it "should not attempt to load network configuration" do
expect(subject).not_to receive(:load_net_conf)
subject.validate_hostonly_ip!(address)
end
end
context "when platform is windows" do
before do
allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true)
end
it "should not error" do
subject.validate_hostonly_ip!(address)
end
it "should not attempt to load network configuration" do
expect(subject).not_to receive(:load_net_conf)
subject.validate_hostonly_ip!(address)
end
end
end
describe "#load_net_conf" do
let(:file_contents) { [""] }
before do
allow(File).to receive(:exist?).
with(described_class.const_get(:VBOX_NET_CONF)).
and_return(true)
allow(File).to receive(:readlines).
with(described_class.const_get(:VBOX_NET_CONF)).
and_return(file_contents)
end
it "should read the configuration file" do
expect(File).to receive(:readlines).
with(described_class.const_get(:VBOX_NET_CONF)).
and_return(file_contents)
subject.load_net_conf
end
context "when file has comments only" do
let(:file_contents) {
[
"# A comment",
"# Another comment",
]
}
it "should return an empty array" do
expect(subject.load_net_conf).to eq([])
end
end
context "when file has valid range entries" do
let(:file_contents) {
[
"* 127.0.0.1/24",
"* 192.168.1.1/24",
]
}
it "should return an array with content" do
expect(subject.load_net_conf).not_to be_empty
end
it "should include IPAddr instances" do
subject.load_net_conf.each do |entry|
expect(entry).to be_a(IPAddr)
end
end
end
context "when file has valid range entries and comments" do
let(:file_contents) {
[
"# Comment in file",
"* 127.0.0.0/8",
"random text",
" * 192.168.2.0/28",
]
}
it "should contain two entries" do
expect(subject.load_net_conf.size).to eq(2)
end
end
context "when file has multiple entries on single line" do
let(:file_contents) {
[
"* 0.0.0.0/0 ::/0"
]
}
it "should contain two entries" do
expect(subject.load_net_conf.size).to eq(2)
end
it "should contain an ipv4 and ipv6 range" do
result = subject.load_net_conf
expect(result.first).to be_ipv4
expect(result.last).to be_ipv6
end
end
end
it "calls the next action in the chain" do
@ -133,29 +314,29 @@ describe VagrantPlugins::ProviderVirtualBox::Action::Network do
subject.call(env)
expect(driver).to have_received(:create_host_only_network).with({
adapter_ip: '172.28.128.1',
adapter_ip: '192.68.56.1',
netmask: '255.255.255.0',
})
expect(driver).to have_received(:create_dhcp_server).with('vboxnet0', {
adapter_ip: "172.28.128.1",
adapter_ip: "192.68.56.1",
auto_config: true,
ip: "172.28.128.1",
ip: "192.68.56.1",
mac: nil,
name: nil,
netmask: "255.255.255.0",
nic_type: nil,
type: :dhcp,
dhcp_ip: "172.28.128.2",
dhcp_lower: "172.28.128.3",
dhcp_upper: "172.28.128.254",
dhcp_ip: "192.68.56.2",
dhcp_lower: "192.68.56.3",
dhcp_upper: "192.68.56.254",
adapter: 2
})
expect(guest).to have_received(:capability).with(:configure_networks, [{
type: :dhcp,
adapter_ip: "172.28.128.1",
ip: "172.28.128.1",
adapter_ip: "192.68.56.1",
ip: "192.68.56.1",
netmask: "255.255.255.0",
auto_config: true,
interface: nil
@ -213,8 +394,8 @@ describe VagrantPlugins::ProviderVirtualBox::Action::Network do
{ ip: 'foo'},
{ ip: '1.2.3'},
{ ip: 'dead::beef::'},
{ ip: '172.28.128.3', netmask: 64},
{ ip: '172.28.128.3', netmask: 'ffff:ffff::'},
{ ip: '192.68.56.3', netmask: 64},
{ ip: '192.68.56.3', netmask: 'ffff:ffff::'},
{ ip: 'dead:beef::', netmask: 'foo:bar::'},
{ ip: 'dead:beef::', netmask: '255.255.255.0'}
].each do |args|