Ruby - See if a port is open
Asked Answered
W

9

49

I need a quick way to find out if a given port is open with Ruby. I currently am fiddling around with this:

require 'socket'

def is_port_open?(ip, port)
  begin
    TCPSocket.new(ip, port)
  rescue Errno::ECONNREFUSED
    return false
  end
  return true
end

It works great if the port is open, but the downside of this is that occasionally it will just sit and wait for 10-20 seconds and then eventually time out, throwing a ETIMEOUT exception (if the port is closed). My question is thus:

Can this code be amended to only wait for a second (and return false if we get nothing back by then) or is there a better way to check if a given port is open on a given host?

Edit: Calling bash code is acceptable also as long as it works cross-platform (e.g., Mac OS X, *nix, and Cygwin), although I do prefer Ruby code.

Wadi answered 5/2, 2009 at 18:24 Comment(0)
I
54

Something like the following might work:

require 'socket'
require 'timeout'

def is_port_open?(ip, port)
  begin
    Timeout::timeout(1) do
      begin
        s = TCPSocket.new(ip, port)
        s.close
        return true
      rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
        return false
      end
    end
  rescue Timeout::Error
  end

  return false
end
Incomputable answered 5/2, 2009 at 19:55 Comment(5)
I had some trouble with this blocking (I think). Basically the timeout wouldn't actually time out. Not sure why, but the netcat solution worked well in its place.Lisandralisbeth
This answer has a solution that also works on windows: https://mcmap.net/q/356141/-shortening-socket-timeout-using-timeout-timeout-n-does-not-seem-to-work-for-meEmmittemmons
Shouldn't true/false be swapped?Burke
Add in there some command line options, and you've got a functional program!Halfblooded
Please watch out with this code. Timeout::timeout is really bad, as much as it can "work" in smaller scripts, it will introduce hard to track errors in bigger applications. Some reading: jvns.ca/blog/2015/11/27/…Cyanine
O
33

All other existing answer are undesirable. Using Timeout is discouraged. Perhaps things depend on ruby version. At least since 2.0 one can simply use:

Socket.tcp("www.ruby-lang.org", 10567, connect_timeout: 5) {}

For older ruby the best method I could find is using non-blocking mode and then select. Described here:

Orientate answered 8/7, 2016 at 12:2 Comment(1)
Worked perfectly for me: port_is_open = Socket.tcp(host, port, connect_timeout: 5) { true } rescue false. It's easy to expand from a one-liner to rescue the specific exceptions needed.Dragonet
C
27

More Ruby idiomatic syntax:

require 'socket'
require 'timeout'

def port_open?(ip, port, seconds=1)
  Timeout::timeout(seconds) do
    begin
      TCPSocket.new(ip, port).close
      true
    rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
      false
    end
  end
rescue Timeout::Error
  false
end
Cistaceous answered 26/1, 2012 at 12:8 Comment(4)
This gave a false positive for the inputs '192.0.2.0', 80, 10 which should be invalid (according to #10456544). I got the same result with Ruby 1.9.3p448 and 2.0.0p195, both on Mac. In what situations does this method manage to return false? (I even tried writing to the socket before closing it, but that still returned true!)Standardize
I think this function def is missing a begin statement before timeout, or is that somehow optional? (rescue Timeout::Error should be in a begin block, shouldn't it?)Banner
@Banner Ruby permits a rescue clause to belong to the method itself. In effect, the method is an implicit block.Physical
wouldn't it be be even better to enclose the Timeout block in the inner begin...rescue one, and analyze the errors at the same indentation level!Syrup
D
17

I recently came up with this solution, making use of the unix lsof command:

def port_open?(port)
  !system("lsof -i:#{port}", out: '/dev/null')
end
Diluvium answered 31/3, 2014 at 1:11 Comment(4)
This was very nice for me. I wanted to introduce a system of assigning ports to virtual machines in vagrant and wrote this one-liner to check to see if the port I was about to assign was open or not: vms['port'] += 1 while ports.include? vms['port'] or system("lsof -i:#{vms['port']}")Archibold
This only works for the logged in user. To work across the board, use sudo lsof -i:<port>Owain
I had to remove the ! (not operator) to get this to work.Calabar
"open" here means "in use" so this will return false when the port is "free"Clutch
D
9

Just for completeness, the Bash would be something like this:

$ netcat $HOST $PORT -w 1 -q 0 </dev/null && do_something

-w 1 specifies a timeout of 1 second, and -q 0 says that, when connected, close the connection as soon as stdin gives EOF (which /dev/null will do straight away).

Bash also has its own built-in TCP/UDP services, but they are a compile-time option and I don't have a Bash compiled with them :P

Decalcify answered 5/2, 2009 at 23:20 Comment(3)
They're pretty simple: just pretend /dev/{tcp}/HOST/PORT are files :)Czarism
For future reference, I found this as nc on my system rather than netcatUropygium
Warning: On MacOS X, this gives the error nc: invalid option -- q. The following works on both MacOS X and Ubuntu, and seems simpler to me: nc -z $HOST $PORTAeschines
S
3

My solution is derived from the posted solutions.

require 'socket'
def is_port_open?(ip, port)
  begin
    s = Socket.tcp(ip, port, connect_timeout: 5)
    s.close
    return true
  rescue => e
    # possible exceptions:
    # - Errno::ECONNREFUSED
    # - Errno::EHOSTUNREACH
    # - Errno::ETIMEDOUT
    puts "#{e.class}: #{e.message}"
    return false
  end
end
Stink answered 14/12, 2020 at 19:29 Comment(1)
This is a better answer, thanks!Aciculate
S
1

My slight variation to Chris Rice's answer. Still handles timing out on a single attempt but also allows multiple retries until you give up.

    def is_port_open?(host, port, timeout, sleep_period)
      begin
        Timeout::timeout(timeout) do
          begin
            s = TCPSocket.new(host, port)
            s.close
            return true
          rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
            sleep(sleep_period)
            retry
          end
        end
      rescue Timeout::Error
        return false
      end
    end
Squinteyed answered 20/2, 2014 at 23:25 Comment(0)
J
1

All *nix platforms:

try nc / netcat command as follow.

`nc -z -w #{timeout_in_seconds} -G #{timeout_in_seconds} #{host} #{port}`
if $?.exitstatus == 0
  #port is open
else
  #refused, port is closed
end

The -z flag can be used to tell nc to report open ports, rather than initiate a connection.

The -w flag means Timeout for connects and final net reads

The -G flag is connection timeout in seconds

Use -n flag to work with IP address rather than hostname.

Examples:

# `nc -z -w 1 -G 1 google.com 80`
# `nc -z -w 1 -G 1 -n 123.234.1.18 80`
Junno answered 5/9, 2016 at 8:25 Comment(0)
I
0

Haven't paid attention to the question for a long time, but here is the script I use to see if a anything is listening on a port. Notice that the method I suggested in the accepted answer ins't used.


#!/usr/bin/env ruby
# encoding: ASCII-8bit
# warn_indent: true
# frozen_string_literal: true

# rubocop:disable Lint/MissingCopEnableDirective
# rubocop:disable Style/StderrPuts

# Test a connection to a port on a host. A host name, IPv4 address, or IPv6
# address is allowed for the host. A service name or a port number is allowed
# for the port.

# Trap a few signals and exit. No problem if the operating system doesn't
# support one of the signals. Rescuing ArgumentError takes care of that.
%w[HUP INT PIPE QUIT TERM].each do |signame|
  Signal.trap(signame) do
    $stdout.puts
    exit 1
  end
rescue ArgumentError
  # the operating system doesn't support the signal, try the next list element
end

require 'socket'

def usage
  me = File.basename($PROGRAM_NAME)
  $stderr.puts "usage: #{me} [-v] host_name_or_address service_name_or_port_number"
  $stderr.puts "   or: #{me} [-v] host_name_or_address:service_name_or_port_number"
  exit 1
end

# address family to string or nil
def af_to_s(addr)
  if addr.ipv4?
    'IPv4'
  elsif addr.ipv6?
    'IPv6'
  end
end

# VERY simple command line argument handling follows...

verbose = if ARGV[0] == '-v'
            ARGV.shift
            true
          else
            false
          end

case ARGV.size

when 1
  if ARGV[0] =~ /^(.*):([^:]+)$/
    host = Regexp.last_match(1)
    port = Regexp.last_match(2)
  else
    usage
  end

when 2
  host, port = *ARGV

else
  usage
end

host = host.strip  # get thawed copy of host with white space removed
port = port.strip  # get thawed copy of port with white space removed

host = Regexp.last_match(1) if host =~ /^\[(.*)\]$/

usage if host.empty? || port.empty?

begin
  addrs = Addrinfo.getaddrinfo(host, port, nil, :STREAM, nil, Socket::AI_ALL)
rescue SocketError => e
  $stderr.puts "error: host #{host}: #{e}"
  exit 2
end

if addrs.empty? # can this ever happen?
  $stderr.puts "error: host #{host}: getaddrinfo failed to return anything!"
  exit 3
end

num_conn = 0
num_addrs = 0

addrs.each do |addr|
  family = af_to_s(addr)
  next if family.nil? # not an IPv4 or IPv6 address

  num_addrs += 1

  # Get an IPv4 or IPv6 socket (which is determined by the address family).
  s = Socket.new(addr.afamily, Socket::SOCK_STREAM)

  # Connect in non-blocking mode. If the connection can be made immediately,
  # then skip the select() call. If the connection can't be made immediately,
  # then use select() to wait up to 10 seconds for the connection to be
  # allowed. If select() times out, then just go on to the next address.

  begin
    s.connect_nonblock(addr.to_sockaddr)
  rescue Errno::EINPROGRESS
    next if IO.select(nil, [s], nil, 10).nil?

    begin
      # Socket is ready. Attempt real connection.
      s.connect_nonblock(addr.to_sockaddr)
    rescue Errno::EISCONN
      # connected
    rescue Errno::ECONNREFUSED => e
      $stderr.puts "error: #{e}" if verbose
      next
    rescue SystemCallError => e
      $stderr.puts "error: #{e}" if verbose
      next
    end
  rescue Errno::ENETUNREACH => e
    $stderr.puts "error: #{e}" if verbose
    next
  end

  puts "connected #{family} #{host}"
  num_conn += 1
end

if num_addrs.zero?
  $stderr.puts "error: no IPv4 or IPv6 addresses found for #{host}"
  exit 4
elsif num_conn.zero?
  $stderr.puts 'error: all connections failed'
  exit 5
else
  exit 0
end
Incomputable answered 21/1 at 22:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.