From b424d00081fb646edf06dbe27bce308c6d8e26ef Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Mon, 8 Aug 2016 19:07:57 +0300 Subject: [PATCH 1/3] Add support of IPv6 host-only networks --- lib/vagrant-parallels/action/network.rb | 68 ++++++++++++++++--------- lib/vagrant-parallels/driver/base.rb | 5 ++ lib/vagrant-parallels/driver/pd_12.rb | 36 +++++++++++++ test/unit/action/network_test.rb | 30 +++++++++++ 4 files changed, 115 insertions(+), 24 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index ac6b9737..89220120 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -1,5 +1,5 @@ +require 'ipaddr' require 'set' - require 'log4r' require 'vagrant/util/network_ip' @@ -253,7 +253,6 @@ def hostonly_config(options) mac: nil, name: nil, nic_type: nil, - netmask: '255.255.255.0', type: :static }.merge(options) @@ -263,30 +262,46 @@ def hostonly_config(options) # Default IP is in the 20-bit private network block for DHCP based networks options[:ip] = '10.37.129.1' if options[:type] == :dhcp && !options[:ip] - # 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 virtual - # network 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 + # TODO: catch wrong type exception + ip = IPAddr.new(options[:ip]) + if ip.ipv4? + options[:netmask] ||= '255.255.255.0' + + # 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 virtual + # network 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? + options[:netmask] ||= 64 - # Split the IP address into its components - ip_parts = netaddr.split('.').map { |i| i.to_i } + # Set adapter IP to ::1 + options[:adapter_ip] ||= (ip.mask(options[:netmask].to_i) | 1).to_s - # 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('.') + # 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 dhcp_options = {} if options[:type] == :dhcp @@ -453,6 +468,11 @@ def hostonly_find_matching_network(config) 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/lib/vagrant-parallels/driver/base.rb b/lib/vagrant-parallels/driver/base.rb index 6ad69e42..e08c1e32 100644 --- a/lib/vagrant-parallels/driver/base.rb +++ b/lib/vagrant-parallels/driver/base.rb @@ -524,6 +524,11 @@ def read_host_only_interfaces iface[:netmask] = adapter['Subnet mask'] || adapter['IPv4 subnet mask'] iface[:bound_to] = net_info['Bound To'] iface[:status] = 'Up' + + if adapter['IPv6 address'] && adapter['IPv6 subnet mask'] + iface[:ipv6] = adapter['IPv6 address'] + iface[:ipv6_prefix] = adapter['IPv6 subnet mask'] + end end hostonly_ifaces << iface diff --git a/lib/vagrant-parallels/driver/pd_12.rb b/lib/vagrant-parallels/driver/pd_12.rb index 2bb4f3ba..6fdca8ff 100644 --- a/lib/vagrant-parallels/driver/pd_12.rb +++ b/lib/vagrant-parallels/driver/pd_12.rb @@ -14,6 +14,42 @@ def initialize(uuid) @logger = Log4r::Logger.new('vagrant_parallels::driver::pd_12') end + + def create_host_only_network(options) + # Create the interface + execute_prlsrvctl('net', 'add', options[:network_id], '--type', 'host-only') + + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + if ip.ipv4? + args = ['--ip', "#{options[:adapter_ip]}/#{options[:netmask]}"] + if options[:dhcp] + args.concat(['--dhcp-ip', options[:dhcp][:ip], + '--ip-scope-start', options[:dhcp][:lower], + '--ip-scope-end', options[:dhcp][:upper]]) + end + elsif ip.ipv6? + # Convert prefix length to netmask ("32" -> "ffff:ffff::") + options[:netmask] = IPAddr.new(IPAddr::IN6MASK, Socket::AF_INET6) + .mask(options[:netmask]).to_s + + args = ['--host-assign-ip6', 'on', + '--ip6', "#{options[:adapter_ip]}/#{options[:netmask]}"] + # DHCPv6 setting is not supported by Vagrant yet. + else + raise IPAddr::AddressFamilyError, 'BUG: unknown address family' + end + + execute_prlsrvctl('net', 'set', options[:network_id], *args) + + # Return the details + { + name: options[:network_id], + ip: options[:adapter_ip], + netmask: options[:netmask], + dhcp: options[:dhcp] + } + end end end end diff --git a/test/unit/action/network_test.rb b/test/unit/action/network_test.rb index 45a57264..14b42080 100644 --- a/test/unit/action/network_test.rb +++ b/test/unit/action/network_test.rb @@ -139,6 +139,36 @@ ) end end + + context 'with static ipv6' do + let(:network_args) {{ ip: 'dead:beef::100' }} + + it 'creates a host-only interface with an IPv6 address :1' do + allow(driver).to receive(:create_host_only_network) {{ name: 'vagrant-vnet0' }} + interface_ip = 'dead:beef::1' + + subject.call(env) + + expect(driver).to have_received(:create_host_only_network).with( + { + network_id: 'vagrant-vnet0', + adapter_ip: interface_ip, + netmask: 64, + } + ) + + expect(guest).to have_received(:capability).with( + :configure_networks, [{ + type: :static6, + adapter_ip: interface_ip, + ip: 'dead:beef::100', + netmask: 64, + auto_config: true, + interface: nil + }] + ) + end + end end context 'with public network' do From c6f6d8381830a76eb08788f9377d49fbc2a2a5b1 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Tue, 9 Aug 2016 11:52:55 +0300 Subject: [PATCH 2/3] action/network: Simplify the calculation of IP and DHCP settings --- lib/vagrant-parallels/action/network.rb | 56 ++++++++++--------------- test/unit/action/network_test.rb | 36 +++++++++++++++- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index 89220120..58f0de70 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -2,7 +2,6 @@ require 'set' require 'log4r' -require 'vagrant/util/network_ip' require 'vagrant/util/scoped_hash_override' module VagrantPlugins @@ -14,7 +13,6 @@ module Action # # This handles all the `config.vm.network` configurations. class Network - include Vagrant::Util::NetworkIP include Vagrant::Util::ScopedHashOverride @@lock = Mutex.new @@ -282,43 +280,31 @@ def hostonly_config(options) 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? options[:netmask] ||= 64 - # Set adapter IP to ::1 - options[:adapter_ip] ||= (ip.mask(options[:netmask].to_i) | 1).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 + # Calculate the adapter IP which is the network address with the final + # bit group appended by 1. Usually it is "x.x.x.1" for IPv4 and + # "::1" for IPv6 + options[:adapter_ip] ||= (ip.mask(options[:netmask]) | 1).to_s + dhcp_options = {} if options[:type] == :dhcp - # Calculate the DHCP server IP, which is the network address - # with the final octet + 1. So "172.28.0.0" turns into "172.28.0.1" - dhcp_ip = ip_parts.dup - dhcp_ip[3] += 1 - dhcp_options[:dhcp_ip] = options[:dhcp_ip] || dhcp_ip.join('.') - - # Calculate the lower and upper bound for the DHCP server - dhcp_lower = ip_parts.dup - dhcp_lower[3] += 2 - dhcp_options[:dhcp_lower] = options[:dhcp_lower] || dhcp_lower.join('.') - - dhcp_upper = ip_parts.dup - dhcp_upper[3] = 254 - dhcp_options[:dhcp_upper] = options[:dhcp_upper] || dhcp_upper.join('.') + # Calculate the IP and lower & upper bound for the DHCP server + # Example: for "192.168.22.64/26" network range it wil be: + # dhcp_ip: "192.168.22.65", + # dhcp_lower: "192.168.22.66" + # dhcp_upper: "192.168.22.126" + ip_range = ip.mask(options[:netmask]).to_range + dhcp_options[:dhcp_ip] = options[:dhcp_ip] || (ip_range.first | 1).to_s + dhcp_options[:dhcp_lower] = options[:dhcp_lower] || (ip_range.first | 2).to_s + dhcp_options[:dhcp_upper] = options[:dhcp_upper] || (ip_range.last(2).first).to_s end { @@ -459,19 +445,19 @@ def hostonly_create_network(config) # This finds a matching host only network for the given configuration. def hostonly_find_matching_network(config) - this_netaddr = network_address(config[:ip], config[:netmask]) + this_netaddr = IPAddr.new("#{config[:ip]}/#{config[:netmask]}") @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| return interface if config[:name] && config[:name] == interface[:name] - if interface[:ip] - return interface if this_netaddr == \ - network_address(interface[:ip], interface[:netmask]) + if interface[:ip] && this_netaddr.ipv4? + netaddr = IPAddr.new("#{interface[:ip]}/#{interface[:netmask]}") + return interface if netaddr.include? this_netaddr end - if interface[:ipv6] - return interface if this_netaddr == \ - network_address(interface[:ipv6], interface[:ipv6_prefix]) + if interface[:ipv6] && this_netaddr.ipv6? + netaddr = IPAddr.new("#{interface[:ipv6]}/#{interface[:ipv6_prefix]}") + return interface if netaddr.include? this_netaddr end end diff --git a/test/unit/action/network_test.rb b/test/unit/action/network_test.rb index 14b42080..ee8b2f49 100644 --- a/test/unit/action/network_test.rb +++ b/test/unit/action/network_test.rb @@ -59,7 +59,7 @@ context 'with type dhcp' do let(:network_args) {{ type: 'dhcp' }} - it 'creates a host only interface and configures network in the guest' do + it 'creates a host-only interface with default IP and configures network in the guest' do allow(driver).to receive(:create_host_only_network) {{ name: 'vagrant-vnet0' }} subject.call(env) @@ -90,6 +90,40 @@ end end + context 'with type dhcp and defined network' do + let(:network_args) {{ type: 'dhcp', ip: '172.28.128.100', netmask: '26' }} + + it 'creates a host-only interface with dhcp and configures network in the guest' do + allow(driver).to receive(:create_host_only_network) {{ name: 'vagrant-vnet0' }} + + subject.call(env) + + expect(driver).to have_received(:create_host_only_network).with( + { + network_id: 'vagrant-vnet0', + adapter_ip: '172.28.128.65', + netmask: '26', + dhcp: { + ip: '172.28.128.65', + lower: '172.28.128.66', + upper: '172.28.128.126' + } + } + ) + + expect(guest).to have_received(:capability).with( + :configure_networks, [{ + type: :dhcp, + adapter_ip: '172.28.128.65', + ip: '172.28.128.100', + netmask: '26', + auto_config: true, + interface: nil + }] + ) + end + end + context 'with static ip' do let (:network_args) {{ ip: '172.28.128.3' }} From bef58e7edab4bdd16619087d87850be12b3e680b Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Tue, 9 Aug 2016 16:57:43 +0300 Subject: [PATCH 3/3] action/network: Validate IP settings, show human-readable error --- lib/vagrant-parallels/action/network.rb | 42 ++++++++++++++----------- lib/vagrant-parallels/errors.rb | 4 +++ locales/en.yml | 5 +++ test/unit/action/network_test.rb | 18 +++++++++++ 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/lib/vagrant-parallels/action/network.rb b/lib/vagrant-parallels/action/network.rb index 58f0de70..45d8ed4f 100644 --- a/lib/vagrant-parallels/action/network.rb +++ b/lib/vagrant-parallels/action/network.rb @@ -260,14 +260,27 @@ def hostonly_config(options) # Default IP is in the 20-bit private network block for DHCP based networks options[:ip] = '10.37.129.1' if options[:type] == :dhcp && !options[:ip] - # TODO: catch wrong type exception - ip = IPAddr.new(options[:ip]) - if ip.ipv4? - options[:netmask] ||= '255.255.255.0' + begin + ip = IPAddr.new(options[:ip]) + if ip.ipv4? + options[:netmask] ||= '255.255.255.0' + elsif ip.ipv6? + options[:netmask] ||= 64 + + # Append a 6 to the end of the type + options[:type] = "#{options[:type]}6".to_sym + else + raise IPAddr::AddressFamilyError, 'unknown address family' + end # Calculate our network address for the given IP/netmask - netaddr = network_address(options[:ip], options[:netmask]) + netaddr = IPAddr.new("#{options[:ip]}/#{options[:netmask]}") + rescue IPAddr::Error => e + raise VagrantPlugins::Parallels::Errors::NetworkInvalidAddress, + options: options, error: e.message + end + if ip.ipv4? # Verify that a host-only network subnet would not collide # with a bridged networking interface. # @@ -276,23 +289,16 @@ def hostonly_config(options) # traffic onto the real interface rather than the virtual # network 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' + next if interface[:status] == 'Down' + that_netaddr = IPAddr.new("#{interface[:ip]}/#{interface[:netmask]}") + raise Vagrant::Errors::NetworkCollision if netaddr.include? that_netaddr end - elsif ip.ipv6? - options[:netmask] ||= 64 - - # 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 # Calculate the adapter IP which is the network address with the final # bit group appended by 1. Usually it is "x.x.x.1" for IPv4 and # "::1" for IPv6 - options[:adapter_ip] ||= (ip.mask(options[:netmask]) | 1).to_s + options[:adapter_ip] ||= (netaddr | 1).to_s dhcp_options = {} if options[:type] == :dhcp @@ -301,8 +307,8 @@ def hostonly_config(options) # dhcp_ip: "192.168.22.65", # dhcp_lower: "192.168.22.66" # dhcp_upper: "192.168.22.126" - ip_range = ip.mask(options[:netmask]).to_range - dhcp_options[:dhcp_ip] = options[:dhcp_ip] || (ip_range.first | 1).to_s + ip_range = netaddr.to_range + dhcp_options[:dhcp_ip] = options[:dhcp_ip] || (ip_range.first | 1).to_s dhcp_options[:dhcp_lower] = options[:dhcp_lower] || (ip_range.first | 2).to_s dhcp_options[:dhcp_upper] = options[:dhcp_upper] || (ip_range.last(2).first).to_s end diff --git a/lib/vagrant-parallels/errors.rb b/lib/vagrant-parallels/errors.rb index f6c91ac9..5047cfd3 100644 --- a/lib/vagrant-parallels/errors.rb +++ b/lib/vagrant-parallels/errors.rb @@ -39,6 +39,10 @@ class MacOSXRequired < VagrantParallelsError error_key(:mac_os_x_required) end + class NetworkInvalidAddress < VagrantParallelsError + error_key(:network_invalid_address) + end + class ExecutionError < VagrantParallelsError error_key(:execution_error) end diff --git a/locales/en.yml b/locales/en.yml index f0b685a8..06336ca3 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -58,6 +58,11 @@ en: which are not supported by "prl_fs" file system. Invalid mount options: %{options} + network_invalid_address: |- + Network settings specified in your Vagrantfile are invalid: + + Network settings: %{options} + Error: %{error} mac_os_x_required: |- Parallels provider works only on OS X (Mac OS X) systems. parallels_install_incomplete: |- diff --git a/test/unit/action/network_test.rb b/test/unit/action/network_test.rb index ee8b2f49..4a50b7b0 100644 --- a/test/unit/action/network_test.rb +++ b/test/unit/action/network_test.rb @@ -255,4 +255,22 @@ end + context 'with invalid settings' 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: '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(VagrantPlugins::Parallels::Errors::NetworkInvalidAddress) + end + end + end + end \ No newline at end of file