52

What's the easiest way to find an unused local port?

Currently I'm using something similar to this:

port=$RANDOM
quit=0

while [ "$quit" -ne 1 ]; do
  netstat -a | grep $port >> /dev/null
  if [ $? -gt 0 ]; then
    quit=1
  else
    port=`expr $port + 1`
  fi
done

It feels awfully roundabout, so I'm wondering if there's a more simple path such as a builtin that I've missed.

14 Answers 14

24

If your application supports it, you can try passing port 0 to the application. If your application passes this to the kernel, the port will be dynamically allocated at request time, and is guaranteed not to be in use (allocation will fail if all ports are already in use).

Otherwise, you can do this manually. The script in your answer has a race condition, the only way to avoid it is to atomically check if it is open by trying to open it. If the port is in use, the program should quit with a failure to open the port.

For example, say you're trying to listen with GNU netcat.

#!/bin/bash
read lower_port upper_port < /proc/sys/net/ipv4/ip_local_port_range
while :; do
    for (( port = lower_port ; port <= upper_port ; port++ )); do
        nc -l -p "$port" 2>/dev/null && break 2
    done
done
  • 1
    @Lekensteyn: Where do you see a race condition here? – Chris Down Nov 2 '14 at 23:23
  • 1
    This port tries to use the first port available. When you have two concurrent processes, then the port which just got checked might be reused. Re-reading your answer, it seems that you suggest to retry binding on an available port until all ports are exhausted. Assuming that the program in question can distinguish between "port in use" and other errors, it should be fine (though randomization would still make it better for unpredictability). – Lekensteyn Nov 2 '14 at 23:44
  • 1
    @Lekensteyn Successful port binding results in the kernel returning EADDRINUSE if you try and use it again, it's not possible that "the port which just got checked might be reused". – Chris Down Nov 3 '14 at 0:56
  • Yes, I wrongly assumed that you would exit the loop and use $port in the actual program as in while ...; done; program --port $port. – Lekensteyn Nov 3 '14 at 9:11
  • From the man page: -p source_port Specifies the source port nc should use, subject to privilege restrictions and availability. It is an error to use this option in conjunction with the -l option. – monksy Nov 17 '16 at 16:34
54

My solution is to bind to port 0, which asks the kernel to allocate a port from it's ip_local_port_range. Then, close the socket and use that port number in your configuration.

This works because the kernel doesn't seem to reuse port numbers until it absolutely has to. Subsequent binds to port 0 will allocate a different port number. Python code:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 0))
addr = s.getsockname()
print addr[1]
s.close()

This gives just a number of a port, eg. 60123.

Run this program 10 000 times (you should run these concurrently), and you'll get 10 000 different port numbers. Therefore, I think it's pretty safe to use the ports.

  • 20
    Here is a one-liner (valid with Python 2 and Python 3): python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()' – Lekensteyn Nov 2 '14 at 22:28
  • 4
    I ran the mentioned experiment, and not all results were unique. My histogram was: { 1: 7006, 2: 1249, 3: 151, 4: 8, 5: 1, 6: 1} – bukzor Jan 31 '15 at 2:00
  • 2
    Is there an easy way to add in a check that the port is not blocked by a firewall, or rather only searches open ports? – Mark Lakata Mar 12 '15 at 20:25
  • 1
    @dshepherd I believe you'll get different ports if you do not close the previous one (and close them all at once at last). – Franklin Yu Aug 23 '16 at 6:38
  • 1
    One liner for Ruby 2.3.1: ruby -e 'puts Addrinfo.tcp("", 0).bind { |s| s.local_address.ip_port }' – Franklin Yu Aug 23 '16 at 6:39
12

One-liner

I've put together a nice one-liner that quickly serves the purpose, allowing to grab an arbitrary number of ports in an arbitrary range (here it's divided in 4 lines for readability):

comm -23 \
<(seq "$FROM" "$TO" | sort) \
<(ss -tan | awk '{print $4}' | cut -d':' -f2 | grep '[0-9]\{1,5\}' | sort -u) \
| shuf | head -n "$HOWMANY"

Line by line

comm is a utility that compares lines in two files that must appear sorted alphabetically. It outputs three columns: lines that appear only in the first file, lines that only appear in the second one and common lines. By specifying -23 we suppress the latter columns and only keep the first one. We can use this to obtain the difference of two sets, expressed as a sequence of text lines. I learned about comm here.

The first file is the range of ports that we can select from. seq produces a sorted sequence of numbers from $FROM to $TO. The result is sorted alphabetically (instead of numerically) and piped to comm as the first file using process substitution.

The second file is the sorted list of ports, that we obtain by calling the ss command (with -t meaning TCP ports, -a meaning all - established and listening - and -n numeric - don't try to resolve, say, 22 to ssh). We then pick only the fourth column with awk, which contains the local address and port. We use cut to split address and port with the : delimiter and keep only the latter (-f2). ss also output an header, that we get rid of by grepping for non-empty sequences of numbers that are no longer than 5. We then comply with comm's requirement by sorting without duplicates -u.

Now we have a sorted list of open ports, that we can shuffle to then grab the first "$HOWMANY" ones with head -n.

Example

Grab the three random open ports in the private range (49152-65535)

comm -23 <(seq 49152 65535 | sort) <(ss -tan | awk '{print $4}' | cut -d':' -f2 | grep "[0-9]\{1,5\}" | sort -u) | shuf | head -n 3

could return for example

54930
57937
51399

Notes

  • switch -t with -u in ss to get free UDP ports instead.
  • replace shuf with sort -n if you prefer to get available ports numerically sorted instead of at random
11
#!/bin/bash
read LOWERPORT UPPERPORT < /proc/sys/net/ipv4/ip_local_port_range
while :
do
        PORT="`shuf -i $LOWERPORT-$UPPERPORT -n 1`"
        ss -lpn | grep -q ":$PORT " || break
done
echo $PORT

Credits to Chris Down

7

Apparently TCP connections can be used as file descriptors on linux from with in bash/zsh. The following function uses that technique and should be faster than invoking netcat/telnet.

function EPHEMERAL_PORT() {
    LOW_BOUND=49152
    RANGE=16384
    while true; do
        CANDIDATE=$[$LOW_BOUND + ($RANDOM % $RANGE)]
        (echo "" >/dev/tcp/127.0.0.1/${CANDIDATE}) >/dev/null 2>&1
        if [ $? -ne 0 ]; then
            echo $CANDIDATE
            break
        fi
    done
}

Usage instructions: Bind the output to a variable and use in scripts. Tested on Ubuntu 16.04

root@ubuntu:~> EPHEMERAL_PORT
59453
root@ubuntu:~> PORT=$(EPHEMERAL_PORT)
  • Works withksh93 also. – fpmurphy Apr 19 '18 at 11:50
  • If you change UPORT to 32768, you can still get EG 35835. RANDOM returns a number in [0,32767]. Modding this by a number greater than the max has no effect. You want something like $[$LPORT + ($RANDOM % ($UPORT-$LPORT))]. – lxs Jan 4 at 13:47
  • Otherwise pretty cool though! – lxs Jan 4 at 13:47
  • This sends \n to any listening port though :) I'd suggest adding -n. This will still try to open a connection but not send anything but immediately disconnect. – stefanct Feb 14 at 16:08
4

Here's a cross-platform, efficient "oneliner" that slurps up all in-use ports and gives you the first available one from 3000 onwards:

netstat -aln | awk '
  $6 == "LISTEN" {
    if ($4 ~ "[.:][0-9]+$") {
      split($4, a, /[:.]/);
      port = a[length(a)];
      p[port] = 1
    }
  }
  END {
    for (i = 3000; i < 65000 && p[i]; i++){};
    if (i == 65000) {exit 1};
    print i
  }
'

You can simply join all lines to have it on one line. If you want to get the first available from a different port number, change the assignment to i in the for loop.

It works on both Mac and Linux, which is why the [:.] regex is needed.

  • Why -a and not -t to just look at TCP(6) sockets? – stefanct Feb 14 at 16:10
  • And while we are at it, parsing the output of ss -Htnl might be better (and faster! - not that I care about that :P). – stefanct Feb 14 at 16:21
  • @stefanct BSD netstat doesn't have -t, at least the one that Apple ships with, and ss is also not present on macOS. netstat -aln even works on Solaris. – w00t Feb 25 at 20:54
3

On Linux, you could do something like:

ss -tln | 
  awk 'NR > 1{gsub(/.*:/,"",$4); print $4}' |
  sort -un |
  awk -v n=1080 '$0 < n {next}; $0 == n {n++; next}; {exit}; END {print n}'

To find the first free port above 1080. Note that ssh -D would bind on the loopback interface, so in theory you could reuse port 1080 if a socket has it bound on another address. Another way would be to actually try and bind it:

perl -MSocket -le 'socket S, PF_INET, SOCK_STREAM,getprotobyname("tcp");
  $port = 1080;
  ++$port until bind S, sockaddr_in($port,inet_aton("127.1"));
  print $port'
  • This does, however, involve a race condition between attempting to open the port and actually using it. – Chris Down Nov 17 '12 at 0:02
  • @ChrisDown, Indeed, but with ssh -D, I can't see any better option. The -O forward option of ssh doesn't return an error when the forward fail. – Stéphane Chazelas Nov 17 '12 at 9:00
3

This is the version I use:

while
  port=$(shuf -n 1 -i 49152-65535)
  netstat -atun | grep -q "$port"
do
  continue
done

echo "$port"

The command shuf -n 1 -i 49152-65535 gives you a "random" port in the dynamic range. If it is used already, another port in that range is tried.

The command netstat -atun lists all (-a) TCP (-t) and UDP (-u) ports without wasting time to determine hostnames (-n).

1

This is part of a function I have in my .bashrc, which dynamically creates SSH tunnels and tries to use any port in a range:

   lps=( 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 )
   lp=null

   # find a free listening port
   for port in ${lps[@]}; do
      lsof -i -n -P |grep LISTEN |grep -q ":${port}"
      [ $? -eq 1 ] && { lp=$port; break; }
   done
   [ "$lp" = "null" ] && { echo "no free local ports available"; return 2; }
   return $port

YMMV

1

Yet another run at this old hobby horse:

function random_free_tcp_port {
  local ports="${1:-1}" interim="${2:-2048}" spacing=32
  local free_ports=( )
  local taken_ports=( $( netstat -aln | egrep ^tcp | fgrep LISTEN |
                         awk '{print $4}' | egrep -o '[0-9]+$' |
                         sort -n | uniq ) )
  interim=$(( interim + (RANDOM % spacing) ))

  for taken in "${taken_ports[@]}" 65535
  do
    while [[ $interim -lt $taken && ${#free_ports[@]} -lt $ports ]]
    do
      free_ports+=( $interim )
      interim=$(( interim + spacing + (RANDOM % spacing) ))
    done
    interim=$(( interim > taken + spacing
                ? interim
                : taken + spacing + (RANDOM % spacing) ))
  done

  [[ ${#free_ports[@]} -ge $ports ]] || return 2

  printf '%d\n' "${free_ports[@]}"
}

This code makes purely portable use of netstat, egrep, awk, &al. Note that only call is issued to external commands, to get a list of taken ports at the beginning. One can request one or more free ports:

:;  random_free_tcp_port
2070
:;  random_free_tcp_port 2
2073
2114

and start at an arbitrary port:

:;  random_free_tcp_port 2 10240
10245
10293
1
while port=$(shuf -n1 -i $(cat /proc/sys/net/ipv4/ip_local_port_range | tr '\011' '-'))
netstat -atun | grep -q ":$port\s" ; do
    continue
done
echo $port

My combination from other answers above. Get it:

With shuf -n1 we take one random number from the range (-i) in /proc/sys/net/ipv4/ip_local_port_range. shuf need the syntax with dash, so we use tr to change the tab in a dash.

Next wie use netstat to show us all (-a) tcp and udp (-u -t) connections in numbers(-n), if we find our random port $port in this (start with a : and end with w whitespace (\s) then we need a other Port and so continue. Else (grep -q has a returncode > 0 we left the while loop and $port is set.

1

If you have python lying around, I'd do this:

port="$(python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1])')";
echo "Unused Port: $port"
0

I wrote this little Python2/3 script that might help you next time:

https://github.com/sdenel/find-unused-local-port

0

My take on it... the function tries to find n consecutive free ports:

#!/bin/bash

RANDOM=$$

# Based on 
# https://unix.stackexchange.com/a/55918/41065
# https://unix.stackexchange.com/a/248319/41065
find_n_ports () {
    local n=$1
    RANDOM=$$
    read lower_port upper_port < /proc/sys/net/ipv4/ip_local_port_range
    local tries=10
    while [ $((tries--)) -gt 0 ]; do
        # Sleep for 100 to 499 ms
        sleep "0.$((100+$RANDOM%399))"
        local start="`shuf -i $lower_port-$(($upper_port-$n-1)) -n 1`"
        local end=$(($start+$n-1))
        # create ss filter for $n consecutive ports starting with $start
        local filter=""
        local ports=$(seq $start $end)
        for p in $ports ; do
            filter="$filter or sport = $p"
        done
        # if none of the ports is in use return them
        if [ "$(ss -tHn "${filter# or}" | wc -l)" = "0" ] ; then
            echo "$ports"
            return 0
        fi
    done
    echo "Could not find $n consecutive ports">&2
    return 1
}

ports=$(find_n_ports 3)
[ $? -ne 0 ] && exit 1
exit 0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.