diff --git a/lib/vagrant/util/network_ip.rb b/lib/vagrant/util/network_ip.rb index fca98f8b4..5f22f69d4 100644 --- a/lib/vagrant/util/network_ip.rb +++ b/lib/vagrant/util/network_ip.rb @@ -1,10 +1,21 @@ +require "ipaddr" + module Vagrant module Util module NetworkIP # Returns the network address of the given IP and subnet. # + # If the IP address is an IPv6 address, subnet should be a prefix + # length such as "64". + # # @return [String] def network_address(ip, subnet) + # If this is an IPv6 address, then just mask it + if subnet.to_s =~ /^\d+$/ + ip = IPAddr.new(ip) + return ip.mask(subnet.to_i).to_s + end + ip = ip_parts(ip) netmask = ip_parts(subnet) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 83eb3aec9..0d8fc8e38 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -30,6 +30,7 @@ module VagrantPlugins autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) autoload :Network, File.expand_path("../action/network", __FILE__) + autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__) autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) @@ -63,6 +64,7 @@ module VagrantPlugins b.use PrepareNFSSettings b.use ClearNetworkInterfaces b.use Network + b.use NetworkFixIPv6 b.use ForwardPorts b.use SetHostname b.use SaneDefaults diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb index 3697cdfef..cf5ca3b47 100644 --- a/plugins/providers/virtualbox/action/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -1,3 +1,4 @@ +require "ipaddr" require "set" require "log4r" @@ -248,7 +249,6 @@ module VagrantPlugins auto_config: true, mac: nil, nic_type: nil, - netmask: "255.255.255.0", type: :static }.merge(options) @@ -258,31 +258,47 @@ module VagrantPlugins # Default IP is in the 20-bit private network block for DHCP based networks options[:ip] = "172.28.128.1" if options[:type] == :dhcp && !options[:ip] - # Calculate our network address for the given IP/netmask - netaddr = network_address(options[:ip], options[:netmask]) + ip = IPAddr.new(options[:ip]) + if ip.ipv4? + options[:netmask] ||= "255.255.255.0" - # Verify that a host-only network subnet would not collide - # with a bridged networking interface. - # - # If the subnets overlap in any way then the host only network - # will not work because the routing tables will force the - # traffic onto the real interface rather than the VirtualBox - # interface. - @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| - that_netaddr = network_address(interface[:ip], interface[:netmask]) - raise Vagrant::Errors::NetworkCollision if \ - netaddr == that_netaddr && interface[:status] != "Down" + # Calculate our network address for the given IP/netmask + netaddr = network_address(options[:ip], options[:netmask]) + + # Verify that a host-only network subnet would not collide + # with a bridged networking interface. + # + # If the subnets overlap in any way then the host only network + # will not work because the routing tables will force the + # traffic onto the real interface rather than the VirtualBox + # interface. + @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| + that_netaddr = network_address(interface[:ip], interface[:netmask]) + raise Vagrant::Errors::NetworkCollision if \ + netaddr == that_netaddr && interface[:status] != "Down" + end + + # Split the IP address into its components + ip_parts = netaddr.split(".").map { |i| i.to_i } + + # Calculate the adapter IP, which we assume is the IP ".1" at + # the end usually. + adapter_ip = ip_parts.dup + adapter_ip[3] += 1 + options[:adapter_ip] ||= adapter_ip.join(".") + elsif ip.ipv6? + # Default subnet prefix length + options[:netmask] ||= 64 + + # IPv6 we just mask the address and use that as the adapter + options[:adapter_ip] ||= ip.mask(options[:netmask].to_i).to_s + + # Append a 6 to the end of the type + options[:type] = "#{options[:type]}6".to_sym + else + raise "BUG: Unknown IP type: #{ip.inspect}" end - # Split the IP address into its components - ip_parts = netaddr.split(".").map { |i| i.to_i } - - # Calculate the adapter IP, which we assume is the IP ".1" at - # the end usually. - adapter_ip = ip_parts.dup - adapter_ip[3] += 1 - options[:adapter_ip] ||= adapter_ip.join(".") - dhcp_options = {} if options[:type] == :dhcp # Calculate the DHCP server IP, which is the network address @@ -456,8 +472,16 @@ module VagrantPlugins @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| return interface if config[:name] && config[:name] == interface[:name] - return interface if this_netaddr == \ - network_address(interface[:ip], interface[:netmask]) + + if interface[:ip] != "" + return interface if this_netaddr == \ + network_address(interface[:ip], interface[:netmask]) + end + + if interface[:ipv6] != "" + return interface if this_netaddr == \ + network_address(interface[:ipv6], interface[:ipv6_prefix]) + end end nil diff --git a/plugins/providers/virtualbox/action/network_fix_ipv6.rb b/plugins/providers/virtualbox/action/network_fix_ipv6.rb new file mode 100644 index 000000000..eb4ca48ef --- /dev/null +++ b/plugins/providers/virtualbox/action/network_fix_ipv6.rb @@ -0,0 +1,64 @@ +require "ipaddr" +require "socket" + +require "log4r" + +require "vagrant/util/scoped_hash_override" + +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware works around a bug in VirtualBox where booting + # a VM with an IPv6 host-only network will someties lose the + # route to that machine. + class NetworkFixIPv6 + include Vagrant::Util::ScopedHashOverride + + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::network") + @app = app + end + + def call(env) + @env = env + + # Determine if we have an IPv6 network + has_v6 = false + env[:machine].config.vm.networks.each do |type, options| + next if type != :private_network + options = scoped_hash_override(options, :virtualbox) + next if options[:ip] == "" + if IPAddr.new(options[:ip]).ipv6? + has_v6 = true + break + end + end + + # Call up + @app.call(env) + + # 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)}") + + @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) + end + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 112e97d5b..768bd9cef 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -263,6 +263,14 @@ module VagrantPlugins def read_vms end + # Reconfigure the hostonly network given by interface (the result + # of read_host_only_networks). This is a sad function that only + # exists to work around VirtualBox bugs. + # + # @return nil + def reconfig_host_only(interface) + end + # Removes the DHCP server identified by the provided network name. # # @param [String] network_name The the full network name associated diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 10bef3f8f..242372452 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -109,6 +109,7 @@ module VagrantPlugins :read_state, :read_used_ports, :read_vms, + :reconfig_host_only, :remove_dhcp_server, :resume, :set_mac_address, diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb index 53361bbbb..66e9a37be 100644 --- a/plugins/providers/virtualbox/driver/version_4_0.rb +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -48,10 +48,19 @@ module VagrantPlugins interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -320,6 +329,10 @@ module VagrantPlugins info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end @@ -429,6 +442,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb index ae1830019..8830297ba 100644 --- a/plugins/providers/virtualbox/driver/version_4_1.rb +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -48,10 +48,19 @@ module VagrantPlugins interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -325,6 +334,10 @@ module VagrantPlugins info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end @@ -434,6 +447,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index dc7da430a..0f1f79698 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -46,12 +46,21 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -356,6 +365,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end @@ -465,6 +478,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index b421e7911..3b08d43cd 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -1,3 +1,4 @@ +require 'ipaddr' require 'log4r' require "vagrant/util/platform" @@ -46,12 +47,21 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -62,6 +72,11 @@ module VagrantPlugins } end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def delete execute("unregistervm", @uuid, "--delete") end @@ -366,6 +381,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 102d838bc..3c84ad3c7 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -46,12 +46,23 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + else + raise "BUG: Unknown IP type: #{ip.inspect}" + end # Return the details return { @@ -366,6 +377,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end @@ -475,6 +490,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/templates/guests/debian/network_static6.erb b/templates/guests/debian/network_static6.erb new file mode 100644 index 000000000..7b9e8a694 --- /dev/null +++ b/templates/guests/debian/network_static6.erb @@ -0,0 +1,10 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +auto eth<%= options[:interface] %> +iface eth<%= options[:interface] %> inet6 static + address <%= options[:ip] %> + netmask <%= options[:netmask] %> +<% if options[:gateway] %> + gateway <%= options[:gateway] %> +<% end %> +#VAGRANT-END diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb index 88f199a82..85dee3cb1 100644 --- a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb @@ -159,6 +159,7 @@ shared_examples "a version 4.x virtualbox driver" do |options| name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', + ipv6_prefix: '0', status: 'Up', }]) end @@ -196,11 +197,40 @@ shared_examples "a version 4.x virtualbox driver" do |options| it "returns a list with one entry for each interface" do expect(subject.read_host_only_interfaces).to eq([ - {name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', status: 'Up'}, - {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', status: 'Up'}, + {name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', ipv6_prefix: "0", status: 'Up'}, + {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', ipv6_prefix: "0", status: 'Up'}, ]) end end + + context "with an IPv6 host-only interface" do + let(:output) { + <<-OUTPUT.gsub(/^ */, '') + Name: vboxnet1 + GUID: 786f6276-656e-4174-8000-0a0027000001 + DHCP: Disabled + IPAddress: 192.168.57.1 + NetworkMask: 255.255.255.0 + IPV6Address: fde4:8dba:82e1:: + IPV6NetworkMaskPrefixLength: 64 + HardwareAddress: 0a:00:27:00:00:01 + MediumType: Ethernet + Status: Up + VBoxNetworkName: HostInterfaceNetworking-vboxnet1 + OUTPUT + } + + it "returns a list with one entry describing that interface" do + expect(subject.read_host_only_interfaces).to eq([{ + name: 'vboxnet1', + ip: '192.168.57.1', + netmask: '255.255.255.0', + ipv6: 'fde4:8dba:82e1::', + ipv6_prefix: '64', + status: 'Up', + }]) + end + end end describe "remove_dhcp_server" do diff --git a/test/unit/vagrant/util/network_ip_test.rb b/test/unit/vagrant/util/network_ip_test.rb index a4ff8944c..9d0d09b12 100644 --- a/test/unit/vagrant/util/network_ip_test.rb +++ b/test/unit/vagrant/util/network_ip_test.rb @@ -13,5 +13,17 @@ describe Vagrant::Util::NetworkIP do it "calculates it properly" do expect(klass.network_address("192.168.2.234", "255.255.255.0")).to eq("192.168.2.0") end + + it "calculates it properly with integer submask" do + expect(klass.network_address("192.168.2.234", "24")).to eq("192.168.2.0") + end + + it "calculates it properly for IPv6" do + expect(klass.network_address("fde4:8dba:82e1::c4", "64")).to eq("fde4:8dba:82e1::") + end + + it "calculates it properly for IPv6" do + expect(klass.network_address("fde4:8dba:82e1::c4", 64)).to eq("fde4:8dba:82e1::") + end end end diff --git a/website/docs/source/v2/networking/private_network.html.md b/website/docs/source/v2/networking/private_network.html.md index 345547f05..e11c7b3c7 100644 --- a/website/docs/source/v2/networking/private_network.html.md +++ b/website/docs/source/v2/networking/private_network.html.md @@ -74,6 +74,35 @@ reachable.

+## IPv6 + +You can specify a static IP via IPv6. DHCP for IPv6 is not supported. +To use IPv6, just specify an IPv6 address as the IP: + +```ruby +Vagrant.configure("2") do |config| + config.vm.network "private_network", ip: "fde4:8dba:82e1::c4" +end +``` + +This will assign that IP to the machine. The entire `/64` subnet will +be reserved. Please make sure to use the reserved local addresses approved +for IPv6. + +You can also modify the prefix length by changing the `netmask` option +(defaults to 64): + +```ruby +Vagrant.configure("2") do |config| + config.vm.network "private_network", + ip: "fde4:8dba:82e1::c4", + netmask: "96" +end +``` + +IPv6 supports for private networks was added in Vagrant 1.7.5 and may +not work with every provider. + ## Disable Auto-Configuration If you want to manually configure the network interface yourself, you