I just had this quick idea to write a tcp port scanner in bash. Bash supports the special /dev/tcp/host/port file that you can read/write. Writing to this special file makes bash open a tcp connection to host:port. If writing to the port succeeds, the port is open, else the port is closed.

So at first I wrote this quick script:

for port in {1..65535}; do
  echo >/dev/tcp/google.com/$port &&
    echo "port $port is open" ||
    echo "port $port is closed"
done

This loops over ports 1-65535 and tries to open google.com:$port. However this doesn't work that well because if the port is closed, it takes bash like 2 minutes to realize that.

To solve this I needed something like alarm(2) to interrupt bash. Bash doesn't have a built-in alarm function, so I had to write my own using Perl:

alarm() {
  perl -e '
    eval {
      $SIG{ALRM} = sub { die };
      alarm shift;
      system(@ARGV);
    };
    if ($@) { exit 1 }
  ' "$@";
}

This alarm function takes two args: seconds for the alarm call, and the code to execute. If the code doesn't execute in the given time, the function fails.

Once I had this, I could take my earlier code and just call it through alarm:

for port in {1..65535}; do
  alarm 1 "echo >/dev/tcp/google.com/$port" &&
    echo "port $port is open" ||
    echo "port $port is closed"
done

This is working! Now if bash freezes because of a closed port, the alarm 1 will kill the probe in 1 second, and the script will move to the next port.

I went ahead and turned this into a proper scan function:

scan() {
  if [[ -z $1 || -z $2 ]]; then
    echo "Usage: $0 <host> <port, ports, or port-range>"
    return
  fi

  local host=$1
  local ports=()
  case $2 in
    *-*)
      IFS=- read start end <<< "$2"
      for ((port=start; port <= end; port++)); do
        ports+=($port)
      done
      ;;
    *,*)
      IFS=, read -ra ports <<< "$2"
      ;;
    *)
      ports+=($2)
      ;;
  esac


  for port in "${ports[@]}"; do
    alarm 1 "echo >/dev/tcp/$host/$port" &&
      echo "port $port is open" ||
      echo "port $port is closed"
  done
}

You can run the scan function from your shell. It takes two arguments: the host to scan, and a list of ports to scan (such as 22,80,443), or a range of ports to scan (such as 1-1024), or an individual port to scan (such as 80).

Here is what happens when I run scan google.com 78-82:

$ scan google.com 78-82 
port 78 is closed
port 79 is closed
port 80 is open
port 81 is closed
port 82 is closed

Similarly you can write an udp port scanner. Just replace /dev/tcp/ with /dev/udp/.

Update

It turns out GNU's coreutils include timeout utility that runs a command with a time limit. Using timeout we can rewrite the tcp proxy without using Perl for SIGALRM:

$ timeout 1 bash -c "echo >/dev/tcp/$host/$port" &&
    echo "port $port is open" ||
    echo "port $port is closed"

Comments

@kjellski Permalink
August 28, 2012, 13:07

beautiful idea as well as realization Peteris, as always

piyush Permalink
August 28, 2012, 13:08

1 liner:

for i in {1..100}; do nc -v -z www.google.com $i; done

sandra Permalink
August 28, 2012, 15:36

you depend on nc....

meh Permalink
August 28, 2012, 16:37

Debian doesn't support /dev/tcp, but has netcat installed by default.

Martin Permalink
August 28, 2012, 23:35

vs... depending on perl?

pxs Permalink
August 29, 2012, 06:42

OP's script relies on perl

Keith Brown Permalink
August 28, 2012, 15:21

Works but I get a ton of 'connection refused' that I am unable to grep/filter out. I tried:
scan localhost 1-100

Coder.C Permalink
August 28, 2012, 17:28

This is how to write alarm in bash:

function alarm() {
    timeout=$1; shift;
    bash -c "$@" &
    pid=$!
    {
      sleep $timeout
      kill $pid 2> /dev/null
    } &
    wait $pid 2> /dev/null
    return $?
  }

alarm 1 "echo >/dev/tcp/google.com/230" && echo "Y" || echo "N"

//this prints 'N' in ~1s

alarm 60 "echo >/dev/tcp/google.com/80" && echo "Y" || echo "N"

//this still returns almost immediately and prints "Y"
sean Permalink
August 28, 2012, 21:23

backgrounding a job to sleep/kill a pid isn't necessarily a great idea imho. here, if the port in question is open then then you will pass the wait relatively quickly, but you still have a timer set to kill the pid. A short timer probably won't cause an issue, but the longer you wait the more likely it becomes that the kernel will assign that pid to a new process that you probably don't want killed. especially if you are launching two new pids for each possible port.

June 12, 2013, 01:14

You can't wait for a PID that isn't a child process, so the PID reuse isn't a problem. There's a lot of other good info on managing and waiting for (multiple) sub-processes here: http://stackoverflow.com/questions/356100/how-to-wait-in-bash-for-several-subprocesses-to-finish-and-return-exit-code-0

jamesjon Permalink
March 24, 2014, 09:27

It's just that the pass4sure MB6-871 pesky reality of limited time and resources has a way of worming pass4sure MB6-869 its way into your organization's grand dreams and ambitious plans. pass4sure MB6-700 The more you can help your team realize that their drop in morale

Jason Dusek Permalink
August 28, 2012, 19:19

Thanks for posting the pure Bash version, Coder.C!

Simon Stroh Permalink
August 28, 2012, 22:38

Here's a pure bash version I came up with a while ago, abusing the timeout in read to wait less than a second while still only using bash builtins :)

function portscan() {
  for p in {0..65535};do((bash -c "(>/dev/tcp/$1/$p)" 2> /dev/null && echo open: $p)&read -t0.1;kill $! 2>/dev/null)2>/dev/null;done;
}
August 30, 2012, 17:59

Good job!

I'll reformat this for better viewing:

function portscan() {
  for p in {0..65535}; do
    (
      (
         bash -c "(>/dev/tcp/$1/$p)" 2> /dev/null && echo open: $p
      ) &
      read -t0.1
      kill $! 2>/dev/null
    ) 2>/dev/null
  done
}
Keith Brown Permalink
August 30, 2012, 23:20

Please explain the use of the '&' outside of the subshell '( )' as opposed to within the subshell in the context of being within a script:

(
bash -c "(>/dev/tcp/$1/$p)" 2> /dev/null && echo open: $p
) &

instead of

(
bash -c "(>/dev/tcp/$1/$p)" 2> /dev/null && echo open: $p
& )

I have seen both on the web but never have seen an explanation of the difference.

Thanks

Alexander Permalink
August 29, 2012, 15:18

Just do this in Python instead.

Warren Rattan Permalink
August 29, 2012, 18:25

Is it possible to capture a UDP datagram and convert it to TCP?

Casey Permalink
August 30, 2012, 09:01

Ok, so why not just use nmap?

August 30, 2012, 17:57

Because I can.

September 07, 2012, 02:21

Any idea when /dev/tcp came in? Or what kernel options might be required to make it work?

I'm running 2.6.32 (Ubuntu Lucid), Bash 4.1.5 and the tcp & udp dev files are not present :(

asd Permalink
November 12, 2012, 21:48

ubuntu's not a real operating system, just a non-functional failed prototype, so that is expected.

Charles Permalink
March 07, 2013, 18:36

There are no /dev files; The trickery is a feature built into BASH.

July 11, 2013, 23:37

Nice! thank you very much for sharing this.

July 13, 2013, 17:13

Thank you on behalf of one more essential article. Where also may perhaps any person acquire to kind of in rank in such a complete way of text ? I suffer a presentation incoming week, and I am on the pay attention on behalf of such in rank.

August 15, 2013, 18:40

This is this very primary measure to i personally visit now. I uncovered numerous entertaining goods inside your blog, specifically its chat. Through the a intense deal of opinions on the content articles, Perhaps I am not motto abandoned having all of the leisure listed now.

October 23, 2013, 15:51

Many appreciation designed for taking this likelihood to tell concerning this, Personally i think strongly regarding it and I turn out manipulate of learning concerning this field of study. When probable, as you advance data, please inform this website with newborn in order. I’ve found it very convenient.

Leave a new comment

(why do I need your e-mail?)

(Your twitter name, if you have one. (I'm @pkrumins, btw.))

Type the first letter of your name: (just to make sure you're a human)

Please preview the comment before submitting to make sure it's OK.

Advertisements