diff --git a/plugins/providers/virtualbox/action/network_fix_ipv6.rb b/plugins/providers/virtualbox/action/network_fix_ipv6.rb index b8b12be58..7aec8d110 100644 --- a/plugins/providers/virtualbox/action/network_fix_ipv6.rb +++ b/plugins/providers/virtualbox/action/network_fix_ipv6.rb @@ -3,6 +3,7 @@ require "socket" require "log4r" +require "vagrant/util/presence" require "vagrant/util/scoped_hash_override" module VagrantPlugins @@ -12,6 +13,7 @@ module VagrantPlugins # a VM with an IPv6 host-only network will someties lose the # route to that machine. class NetworkFixIPv6 + include Vagrant::Util::Presence include Vagrant::Util::ScopedHashOverride def initialize(app, env) @@ -41,16 +43,15 @@ module VagrantPlugins # If we have no IPv6, forget it return if !has_v6 - # We do, so fix them if we must - env[:machine].provider.driver.read_host_only_interfaces.each do |interface| - # Ignore interfaces without an IPv6 address - next if interface[:ipv6] == "" + host_only_interfaces(env).each do |interface| + next if !present?(interface[:ipv6]) + next if interface[:status] != "Up" - # Make the test IP. This is just the highest value IP ip = IPAddr.new(interface[:ipv6]) - ip |= IPAddr.new(":#{":FFFF" * (interface[:ipv6_prefix].to_i / 16)}") + ip |= ("1" * (128 - interface[:ipv6_prefix].to_i)).to_i(2) @logger.info("testing IPv6: #{ip}") + begin UDPSocket.new(Socket::AF_INET6).connect(ip.to_s, 80) rescue Errno::EHOSTUNREACH @@ -59,6 +60,21 @@ module VagrantPlugins end end end + + # The list of interface names for host-only adapters. + # @return [Array] + def host_only_interface_names(env) + env[:machine].provider.driver.read_network_interfaces + .map { |_, i| i[:hostonly] if i[:type] == :hostonly }.compact + end + + # The list of host_only_interfaces that are tied to a host-only adapter. + # @return [Array] + def host_only_interfaces(env) + iface_names = self.host_only_interface_names(env) + env[:machine].provider.driver.read_host_only_interfaces + .select { |interface| iface_names.include?(interface[:name]) } + end end end end diff --git a/test/unit/plugins/providers/virtualbox/action/network_fix_ipv6_test.rb b/test/unit/plugins/providers/virtualbox/action/network_fix_ipv6_test.rb index d4b985a63..2f99b954b 100644 --- a/test/unit/plugins/providers/virtualbox/action/network_fix_ipv6_test.rb +++ b/test/unit/plugins/providers/virtualbox/action/network_fix_ipv6_test.rb @@ -1,4 +1,5 @@ require_relative "../base" +require 'socket' describe VagrantPlugins::ProviderVirtualBox::Action::NetworkFixIPv6 do include_context "unit" @@ -11,11 +12,14 @@ describe VagrantPlugins::ProviderVirtualBox::Action::NetworkFixIPv6 do end let(:machine) do - iso_env.machine(iso_env.machine_names[0], :dummy) + iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| + m.provider.stub(driver: driver) + end end let(:env) {{ machine: machine }} let(:app) { lambda { |*args| }} + let(:driver) { double("driver") } subject { described_class.new(app, env) } @@ -30,4 +34,132 @@ describe VagrantPlugins::ProviderVirtualBox::Action::NetworkFixIPv6 do .and_return(private_network: { ip: "" }) expect { subject.call(env) }.to_not raise_error end + + context "with IPv6 interfaces" do + let(:socket) { double("socket") } + + before do + # This address is only used to trigger the fixup code. It doesn't matter + # what it is. + allow(machine.config.vm).to receive(:networks) + .and_return(private_network: { ip: 'fe:80::' }) + allow(UDPSocket).to receive(:new).with(Socket::AF_INET6) + .and_return(socket) + socket.stub(:connect) + end + + it "only checks the interfaces associated with the VM" do + all_networks = [{name: "vboxnet0", + ipv6: "dead:beef::", + ipv6_prefix: 64, + status: 'Up' + }, + {name: "vboxnet1", + ipv6: "badd:badd::", + ipv6_prefix: 64, + status: 'Up' + } + ] + ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + subject.call(env) + expect(socket).to have_received(:connect) + .with(all_networks[0][:ipv6] + (['ffff']*4).join(':'), 80) + end + + it "correctly uses the netmask to figure out the probe address" do + all_networks = [{name: "vboxnet0", + ipv6: "dead:beef::", + ipv6_prefix: 113, + status: 'Up' + } + ] + ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + subject.call(env) + expect(socket).to have_received(:connect) + .with(all_networks[0][:ipv6] + '7fff', 80) + end + + it "should ignore interfaces that are down" do + all_networks = [{name: "vboxnet0", + ipv6: "dead:beef::", + ipv6_prefix: 64, + status: 'Down' + } + ] + ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + subject.call(env) + expect(socket).to_not have_received(:connect) + end + + it "should ignore interfaces without an IPv6 address" do + all_networks = [{name: "vboxnet0", + ipv6: "", + ipv6_prefix: 0, + status: 'Up' + } + ] + ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + subject.call(env) + expect(socket).to_not have_received(:connect) + end + + it "should ignore nat interfaces" do + all_networks = [{name: "vboxnet0", + ipv6: "", + ipv6_prefix: 0, + status: 'Up' + } + ] + ifaces = { 1 => {type: :nat} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + subject.call(env) + expect(socket).to_not have_received(:connect) + end + + it "should reconfigure an interface if unreachable" do + all_networks = [{name: "vboxnet0", + ipv6: "dead:beef::", + ipv6_prefix: 64, + status: 'Up' + } + ] + ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} + } + allow(machine.provider.driver).to receive(:read_network_interfaces) + .and_return(ifaces) + allow(machine.provider.driver).to receive(:read_host_only_interfaces) + .and_return(all_networks) + allow(socket).to receive(:connect) + .with(all_networks[0][:ipv6] + (['ffff']*4).join(':'), 80) + .and_raise Errno::EHOSTUNREACH + allow(machine.provider.driver).to receive(:reconfig_host_only) + subject.call(env) + expect(machine.provider.driver).to have_received(:reconfig_host_only) + .with(all_networks[0]) + end + end end