diff --git a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb index 82fe2891b..e0ada3243 100644 --- a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb +++ b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb @@ -1,6 +1,7 @@ require "set" require "log4r" +require 'socket' require "vagrant/util/is_port_open" @@ -242,20 +243,35 @@ module Vagrant return extra_in_use.fetch(hostport).include?(hostip) end - def port_check(host_ip, host_port) - # If no host_ip is specified, intention taken to be list on all interfaces. - # If platform is windows, default back to localhost only - test_host_ip = host_ip || "0.0.0.0" - begin - is_port_open?(test_host_ip, host_port) - rescue Errno::EADDRNOTAVAIL - if !host_ip && test_host_ip == "0.0.0.0" - test_host_ip = "127.0.0.1" - retry - else - raise + def ipv4_addresses + ip_addresses = [] + Socket.getifaddrs.each do |ifaddr| + if ifaddr.addr.ipv4? + ip_addresses << ifaddr.addr.ip_address end end + ip_addresses + end + + def port_check(host_ip, host_port) + # If no host_ip is specified, intention taken to be listen on all interfaces. + if host_ip.nil? || host_ip == "0.0.0.0" + @logger.debug("Checking port #{host_port} on all IPv4 addresses...") + available_ips = ipv4_addresses.select do |test_host_ip| + @logger.debug("Host IP: #{test_host_ip}, port: #{host_port}") + !is_port_open?(test_host_ip, host_port) + end + if available_ips.empty? + @logger.debug("Cannot forward port #{host_port} on any interfaces.") + true + else + @logger.debug("These IP addresses will forward to the guest: #{available_ips.join(', ')}") + false + end + else + # Do a regular check + is_port_open?(host_ip, host_port) + end end def with_forwarded_ports(env) diff --git a/test/unit/vagrant/action/builtin/handle_forwarded_port_collisions_test.rb b/test/unit/vagrant/action/builtin/handle_forwarded_port_collisions_test.rb index 865c35248..ffdc3f37f 100644 --- a/test/unit/vagrant/action/builtin/handle_forwarded_port_collisions_test.rb +++ b/test/unit/vagrant/action/builtin/handle_forwarded_port_collisions_test.rb @@ -72,6 +72,7 @@ describe Vagrant::Action::Builtin::HandleForwardedPortCollisions do let(:port_options){ {guest: 80, host: 8080} } before do expect(vm_config).to receive(:networks).and_return([[:forwarded_port, port_options]]).twice + allow(instance).to receive(:ipv4_addresses).and_return(["127.0.0.1"]) end it "should check if host port is in use" do @@ -144,6 +145,23 @@ describe Vagrant::Action::Builtin::HandleForwardedPortCollisions do describe "#recover" do end + describe "#ipv4_addresses" do + let(:ipv4_ifaddr) { double("ipv4_ifaddr") } + let(:ipv6_ifaddr) { double("ipv6_ifaddr") } + let(:ifaddrs) { [ ipv4_ifaddr, ipv6_ifaddr ] } + + before do + allow(ipv4_ifaddr).to receive_message_chain(:addr, :ipv4?).and_return(true) + allow(ipv4_ifaddr).to receive_message_chain(:addr, :ip_address).and_return("127.0.0.1") + allow(ipv6_ifaddr).to receive_message_chain(:addr, :ipv4?).and_return(false) + end + + it "returns a list of all IPv4 addresses" do + allow(Socket).to receive(:getifaddrs).and_return(ifaddrs) + expect(instance.send(:ipv4_addresses)).to eq([ "127.0.0.1" ]) + end + end + describe "#port_check" do let(:host_ip){ "127.0.0.1" } let(:host_port){ 8080 } @@ -153,18 +171,34 @@ describe Vagrant::Action::Builtin::HandleForwardedPortCollisions do instance.send(:port_check, host_ip, host_port) end - context "when host_ip is not set" do - let(:host_ip){ nil } + context "when host_ip is 0.0.0.0" do + let(:host_ip) { "0.0.0.0" } + let(:test_ips) { [ "127.0.0.1", "192.168.1.7" ] } - it "should set host_ip to 0.0.0.0 when unset" do - expect(instance).to receive(:is_port_open?).with("0.0.0.0", host_port).and_return(true) + before do + allow(instance).to receive(:ipv4_addresses).and_return(test_ips) + end + + it "should check the port on every IPv4 interface" do + expect(instance).to receive(:is_port_open?).with(test_ips.first, host_port) + expect(instance).to receive(:is_port_open?).with(test_ips.last, host_port) instance.send(:port_check, host_ip, host_port) end - it "should set host_ip to 127.0.0.1 when 0.0.0.0 is not available" do - expect(instance).to receive(:is_port_open?).with("0.0.0.0", host_port).and_raise(Errno::EADDRNOTAVAIL) - expect(instance).to receive(:is_port_open?).with("127.0.0.1", host_port).and_return(true) - instance.send(:port_check, host_ip, host_port) + it "should return false if the port is closed on any IPv4 interfaces" do + expect(instance).to receive(:is_port_open?).with(test_ips.first, host_port). + and_return(true) + expect(instance).to receive(:is_port_open?).with(test_ips.last, host_port). + and_return(false) + expect(instance.send(:port_check, host_ip, host_port)).to be(false) + end + + it "should return true if the port is open on all IPv4 interfaces" do + expect(instance).to receive(:is_port_open?).with(test_ips.first, host_port). + and_return(true) + expect(instance).to receive(:is_port_open?).with(test_ips.last, host_port). + and_return(true) + expect(instance.send(:port_check, host_ip, host_port)).to be(true) end end end