diff --git a/plugins/providers/virtualbox/action/network_fix_ipv6.rb b/plugins/providers/virtualbox/action/network_fix_ipv6.rb index b8b12be58..dcfa2632e 100644 --- a/plugins/providers/virtualbox/action/network_fix_ipv6.rb +++ b/plugins/providers/virtualbox/action/network_fix_ipv6.rb @@ -41,21 +41,22 @@ 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] == "" - - # 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)}") - + ifaces = env[:machine].provider.driver.read_network_interfaces + ifaces.select! { |id, iface| iface[:type] == :hostonly } + iface_names = ifaces.values.map { |iface| iface[:hostonly] } + networks = env[:machine].provider.driver.read_host_only_interfaces + networks.select! { |network| iface_names.include?(network[:name]) } + networks.each do |network| + next if network[:ipv6] == "" + next if network[:status] != 'Up' + ip = IPAddr.new(network[:ipv6]) | + ('1' * (128 - network[: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 @logger.info("IPv6 host unreachable. Fixing: #{ip}") - env[:machine].provider.driver.reconfig_host_only(interface) + env[:machine].provider.driver.reconfig_host_only(network) 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