Fix #11373: Check all interfaces for port collisions

This commit changes the behavior of the port check to check all possible
IPv4 network interfaces when the host IP is `nil` or `0.0.0.0`. This
means that if the desired port is available on any network interfaces, a
forward from 0.0.0.0 will use that interface.

If the port is open (in use) on all interfaces, then it's treated as a
collision and will either throw an error or auto-correct the port, based
on the Vagrantfile configuration.
This commit is contained in:
Jeff Bonhag 2020-03-13 16:30:01 -04:00
parent 94c1c605cd
commit d4acdd06ec
No known key found for this signature in database
GPG Key ID: 32966F3FB5AC1129
2 changed files with 70 additions and 20 deletions

View File

@ -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)

View File

@ -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