Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPv6 support for Host-Only networks (PD 12+) #273

Merged
merged 3 commits into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 55 additions & 43 deletions lib/vagrant-parallels/action/network.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
require 'ipaddr'
require 'set'

require 'log4r'

require 'vagrant/util/network_ip'
require 'vagrant/util/scoped_hash_override'

module VagrantPlugins
Expand All @@ -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

Expand Down Expand Up @@ -253,7 +251,6 @@ def hostonly_config(options)
mac: nil,
name: nil,
nic_type: nil,
netmask: '255.255.255.0',
type: :static
}.merge(options)

Expand All @@ -263,47 +260,57 @@ 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'
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 = IPAddr.new("#{options[:ip]}/#{options[:netmask]}")
rescue IPAddr::Error => e
raise VagrantPlugins::Parallels::Errors::NetworkInvalidAddress,
options: options, error: e.message
end

# Split the IP address into its components
ip_parts = netaddr.split('.').map { |i| i.to_i }
if ip.ipv4?
# 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|
next if interface[:status] == 'Down'
that_netaddr = IPAddr.new("#{interface[:ip]}/#{interface[:netmask]}")
raise Vagrant::Errors::NetworkCollision if netaddr.include? that_netaddr
end
end

# 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('.')
# 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
# "<prefix>::1" for IPv6
options[:adapter_ip] ||= (netaddr | 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 = 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

{
Expand Down Expand Up @@ -444,14 +451,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] && this_netaddr.ipv6?
netaddr = IPAddr.new("#{interface[:ipv6]}/#{interface[:ipv6_prefix]}")
return interface if netaddr.include? this_netaddr
end
end

Expand Down
5 changes: 5 additions & 0 deletions lib/vagrant-parallels/driver/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions lib/vagrant-parallels/driver/pd_12.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/vagrant-parallels/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |-
Expand Down
84 changes: 83 additions & 1 deletion test/unit/action/network_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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' }}

Expand Down Expand Up @@ -139,6 +173,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 <prefix>: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
Expand Down Expand Up @@ -191,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