# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require "vagrant/util/platform" describe VagrantPlugins::ProviderVirtualBox::Action::Network do include_context "unit" include_context "virtualbox" let(:iso_env) do # We have to create a Vagrantfile so there is a root path env = isolated_environment env.vagrantfile("") env.create_vagrant_env end let(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", version: vbox_version) } let(:vbox_version) { "6.1.0" } let(:nics) { {} } subject { described_class.new(app, env) } before do allow(driver).to receive(:enable_adapters) allow(driver).to receive(:read_network_interfaces) { nics } 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 context "when address is ipv6" do let(:address) { "::1" } context "when type is static6" do let(:type) { :static6 } it "should have a static6 type" do result = subject.hostonly_config(options) expect(result[:type]).to eq(:static6) end end context "when type is static" do let(:type) { :static } it "should have static6 type" do result = subject.hostonly_config(options) expect(result[:type]).to eq(:static6) end end end end describe "#validate_hostonly_ip!" do let(:address) { "192.168.1.2" } let(:net_conf) { [IPAddr.new(address + "/24")]} let(:vbox_version) { "6.1.28" } before do 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, driver) end context "when address is within ranges" do it "should not error" do subject.validate_hostonly_ip!(address, driver) 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, driver) }.to raise_error(Vagrant::Errors::VirtualBoxInvalidHostSubnet) end end context "when virtualbox version does not restrict range" do let(:vbox_version) { "6.1.20" } it "should not error" do subject.validate_hostonly_ip!(address, driver) end it "should not attempt to load network configuration" do expect(subject).not_to receive(:load_net_conf) subject.validate_hostonly_ip!(address, driver) 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, driver) end it "should not attempt to load network configuration" do expect(subject).not_to receive(:load_net_conf) subject.validate_hostonly_ip!(address, driver) end end end describe "#load_net_conf" do let(:file_contents) { [""] } before do allow(File).to receive(:exist?).and_call_original 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 called = false app = lambda { |*args| called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "creates a host-only interface with an IPv6 address :1" do guest = double("guest") machine.config.vm.network 'private_network', type: :static, ip: 'dead:beef::100' #allow(driver).to receive(:read_bridged_interfaces) { [] } allow(driver).to receive(:read_host_only_interfaces) { [] } #allow(driver).to receive(:read_dhcp_servers) { [] } allow(machine).to receive(:guest) { guest } allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(guest).to receive(:capability) interface_ip = 'dead:beef::1' subject.call(env) expect(driver).to have_received(:create_host_only_network).with(hash_including({ adapter_ip: interface_ip, netmask: 64, })) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :static6, adapter_ip: 'dead:beef::1', ip: 'dead:beef::100', netmask: 64, auto_config: true, interface: nil }]) end it "raises the appropriate error when provided with an invalid IP address" do machine.config.vm.network 'private_network', ip: '192.168.33.06' expect{ subject.call(env) }.to raise_error(Vagrant::Errors::NetworkAddressInvalid) end context "with a dhcp private network" do let(:bridgedifs) { [] } let(:hostonlyifs) { [] } let(:dhcpservers) { [] } let(:guest) { double("guest") } let(:network_args) {{ type: :dhcp }} before do machine.config.vm.network 'private_network', **network_args allow(driver).to receive(:read_bridged_interfaces) { bridgedifs } allow(driver).to receive(:read_host_only_interfaces) { hostonlyifs } allow(driver).to receive(:read_dhcp_servers) { dhcpservers } allow(machine).to receive(:guest) { guest } end it "tries to setup dhpc server using the ip for the specified network" do allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) allow(subject).to receive(:hostonly_find_matching_network).and_return({name: "vboxnet1", ip: "192.168.55.1"}) subject.call(env) expect(driver).to have_received(:create_dhcp_server).with('vboxnet1', { adapter_ip: "192.168.55.1", auto_config: true, ip: "192.168.55.1", mac: nil, name: nil, netmask: "255.255.255.0", nic_type: nil, type: :dhcp, dhcp_ip: "192.168.55.2", dhcp_lower: "192.168.55.3", dhcp_upper: "192.168.55.254", adapter: 2 }) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :dhcp, adapter_ip: "192.168.55.1", ip: "192.168.55.1", netmask: "255.255.255.0", auto_config: true, interface: nil }]) end it "creates a host only interface and a dhcp server using default ips, then tells the guest to configure the network after boot" do allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) allow(subject).to receive(:hostonly_find_matching_network).and_return(nil) subject.call(env) expect(driver).to have_received(:create_host_only_network).with(hash_including({ adapter_ip: '192.168.56.1', netmask: '255.255.255.0', })) expect(driver).to have_received(:create_dhcp_server).with('vboxnet0', { adapter_ip: "192.168.56.1", auto_config: true, ip: "192.168.56.1", mac: nil, name: nil, netmask: "255.255.255.0", nic_type: nil, type: :dhcp, dhcp_ip: "192.168.56.2", dhcp_lower: "192.168.56.3", dhcp_upper: "192.168.56.254", adapter: 2 }) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :dhcp, adapter_ip: "192.168.56.1", ip: "192.168.56.1", netmask: "255.255.255.0", auto_config: true, interface: nil }]) end context "when the default vbox dhcpserver is present from a fresh vbox install (see issue #3803)" do let(:dhcpservers) {[ { network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '192.168.56.100', netmask: '255.255.255.0', lower: '192.168.56.101', upper: '192.168.56.254' } ]} it "removes the invalid dhcpserver so it won't collide with any host only interface" do allow(driver).to receive(:remove_dhcp_server) allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) subject.call(env) expect(driver).to have_received(:remove_dhcp_server).with('HostInterfaceNetworking-vboxnet0') end context "but the user has intentionally configured their network just that way" do let (:network_args) {{ type: :dhcp, adapter_ip: '192.168.56.1', dhcp_ip: '192.168.56.100', dhcp_lower: '192.168.56.101', dhcp_upper: '192.168.56.254' }} it "does not attempt to remove the dhcpserver" do allow(driver).to receive(:remove_dhcp_server) allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) subject.call(env) expect(driver).not_to have_received(:remove_dhcp_server).with('HostInterfaceNetworking-vboxnet0') end end end end context 'with invalid settings' do [ { ip: 'foo'}, { ip: '1.2.3'}, { ip: 'dead::beef::'}, { ip: '192.168.56.3', netmask: 64}, { ip: '192.168.56.3', netmask: 'ffff:ffff::'}, { ip: 'dead:beef::', netmask: 'foo:bar::'}, { ip: 'dead:beef::', netmask: '255.255.255.0'} ].each do |args| it 'raises an exception' do machine.config.vm.network 'private_network', **args expect { subject.call(env) }. to raise_error(Vagrant::Errors::NetworkAddressInvalid) end end end context "without type set" do before { allow(subject).to receive(:hostonly_adapter).and_return({}) } [ { ip: "192.168.63.5" }, { ip: "192.168.63.5", netmask: "255.255.255.0" }, { ip: "dead:beef::100" }, { ip: "dead:beef::100", netmask: 96 }, ].each do |args| it "sets the type automatically" do machine.config.vm.network "private_network", **args expect(subject).to receive(:hostonly_config) do |config| expect(config).to have_key(:type) addr = IPAddr.new(args[:ip]) if addr.ipv4? expect(config[:type]).to eq(:static) else expect(config[:type]).to eq(:static6) end config end subject.call(env) end end end describe "#hostonly_find_matching_network" do let(:ip){ "192.168.55.2" } let(:config){ {ip: ip, netmask: "255.255.255.0"} } let(:interfaces){ [] } before do allow(driver).to receive(:read_host_only_interfaces).and_return(interfaces) subject.instance_variable_set(:@env, env) end context "with no defined host interfaces" do it "should return nil" do expect(subject.hostonly_find_matching_network(config)).to be_nil end end context "with matching host interface" do let(:interfaces){ [{ip: "192.168.55.1", netmask: "255.255.255.0", name: "vnet"}] } it "should return matching interface" do expect(subject.hostonly_find_matching_network(config)).to eq(interfaces.first) end context "with matching name" do let(:config){ {ip: ip, netmask: "255.255.255.0", name: "vnet"} } it "should return matching interface" do expect(subject.hostonly_find_matching_network(config)).to eq(interfaces.first) end end context "with non-matching name" do let(:config){ {ip: ip, netmask: "255.255.255.0", name: "unknown"} } it "should return nil" do expect(subject.hostonly_find_matching_network(config)).to be_nil end end end end end