Port Forwarding doesn't work on MacOS 26.7+
Port forwarding doesn't work on MacOS 26.7+ and MacOS 27. It is due to a change in MacOS behavior that breaks is_port_open?.
Debug output
See Actual Behavior below
Expected behavior
This line should cause guest port 80 to be forwarded to host port 10080 since the host does not have that port open. Note that the problem occurs with any host port.
config.vm.network :forwarded_port, guest: 80, host: 10080
Actual behavior
The bug causes Vagrant to report an error similar to the following:
Vagrant cannot forward the specified ports on this VM, since they
would collide with some other application that is already listening
on these ports. The forwarded port to 10080 is already in use
on the host machine.
To fix this, modify your current project's Vagrantfile to use another
port. Example, where '1234' would be replaced by a unique host port:
config.vm.network :forwarded_port, guest: 80, host: 1234
Reproduction information
Upgrade to MacOS Tahoe 26.7 or later. Or upgrade to MacOS Golden Gate.
Run vagrant up on a Vagrantfile with a line similar to:
config.vm.network :forwarded_port, guest: 80, host: 10080
Vagrant version
vagrant 2.4.9
Host operating system
MacOS 26.7 or later
Guest operating system
Doesn't matter but I was using Ubuntu 24.
Steps to reproduce
- Upgrade to MacOS Tahoe 26.7 or later. Or upgrade to MacOS Golden Gate.
- Run
vagrant upon theVagrantfilebelow:
config.vm.network :forwarded_port, guest: 80, host: 10080
Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "luminositylabsllc/bento-ubuntu-24.04"
config.vm.define "test" do |s|
s.vm.hostname = "test"
s.vm.network "forwarded_port", guest: 80, host: 10080
end
end
Solution
We have worked around it with the following patch by adding require_relative 'port_check_patch' to the top of our Vagrantfile. However, this should be fixed in Vagrant itself.
# port_check_patch.rb
# Workaround for a host-OS socket behaviour change, not a Vagrant or vagrant-parallels bug.
#
# macOS 26.7+ reports a refused non-blocking connect by making the socket writable and recording the
# error in SO_ERROR, rather than raising an error. Ruby's Socket.tcp treats writability as
# success and returns a dead socket, so Vagrant's is_port_open? reports every port as in use.
#
# TCPSocket.new still reports refusals correctly, so use it instead, bounded by an explicit timeout.
# This only patches hosts that actually exhibit the bug: it is a no-op elsewhere and stops applying
# once macOS or Ruby fixes this. Delete this file and its require when that happens.
require 'socket'
require 'timeout'
require 'vagrant/util/is_port_open'
# True if Socket.tcp fails to raise on a port nothing is listening on.
def connect_timeout_reports_false_positive?
probe = TCPServer.new('127.0.0.1', 0)
free_port = probe.addr[1]
probe.close
Socket.tcp('127.0.0.1', free_port, connect_timeout: 0.1).close
true
rescue StandardError
false
end
if connect_timeout_reports_false_positive?
module Vagrant
module Util
module IsPortOpen
def is_port_open?(host, port)
Timeout.timeout(1) { TCPSocket.new(host, port).close }
true
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH,
Errno::EACCES, Errno::ENOTCONN, Errno::EALREADY, Timeout::Error
false
end
extend self
end
end
end
end
Source: hashicorp/vagrant