We might sometimes want to randomly choose an unused TCP port on Linux. This occurs from time to time on a server, when the administrator wants to expose an HTTP port for each user. Or, you just need an available port for IPC. Let’s make it happen with bash only.

The function below does the right job:

function unused_port() {
N=${1:-1}
comm -23 \
<(seq "1025" "65535" | sort) \
<(ss -Htan |
awk '{print $4}' |
cut -d':' -f2 |
sort -u) |
shuf |
head -n "$N"
}

We are going to step through the code next.

Our first step is to obtain a list of occupied ports, which can be accomplished by command

ss -Htan

ss is a tool for investigating sockets, printing out currently used ports as a table like:

State        Recv-Q   Send-Q                               Local Address:Port                              Peer Address:Port               Process
ESTAB 0 0 127.0.0.1:45342 127.0.0.1:1081
...

Argument -Htan controls the printing style of ss. -H removes table header; -t lists only TCP ports; -a lists all ports, including listening and unlistening ones; -n enforces ports to be printed numerically (otherwise 80 will be resolved as http). The command produces an output like:

LISTEN       0        128                                        0.0.0.0:17500                                            0.0.0.0:*
LISTEN 0 128 127.0.0.1:17600 0.0.0.0:*

We then use some string manipulation tools to extract the port numbers:

ss -Htan |
awk '{print $4}' |
cut -d':' -f2 |
sort -u

Here, awk '{print $4}' selects the 4th item (e.g., 0.0.0.0:17500) from each line. cut -d':' -f2 splits each item with :, and then prints out the second part (e.g., 17500). sort -u sorts the items and removes duplicated ones.

Now we get the list, say LIST2. Our next step is to create another list LIST1 by “inversing” LIST2, such that LIST1 (disjointly) unioning LIST2 equals FULLLIST (all legal ports).

FULLLIST can be obtained easily with seq "1025" "65535" | sort. The “inverse” operation, meanwhile, can be achieved using comm.

Simply, comm takes two files FILE1 and FILE2 as input, and produces output with three columns:

  • Column 1 contains lines unique to FILE1;
  • Column 2 contains lines unique to FILE2;
  • Column 3 contains lines common to both files.

Apparently we only needs the first column, so use -23 to suppress the other two ones. Put them together as:

comm -23 \
<(seq "1025" "65535" | sort) \
<(ss -Htan |
awk '{print $4}' |
cut -d':' -f2 |
sort -u)

The syntax <(command) is called process substitution. It is equivalent to:

  • Spawn command in current shell and pipe its stdout to /dev/fd/<some number>;
  • Substitute <(...) with /dev/fd/<some number>.

The last step, we further extend the command into a convenient function.

function unused_port() {
N=${1:-1}
comm -23 \
<(seq "1025" "65535" | sort) \
<(ss -Htan |
awk '{print $4}' |
cut -d':' -f2 |
sort -u) |
shuf |
head -n "$N"
}

The function receives an argument N, shuffles the available port numbers, and choose N ports from the list.

Code is available at hsfzxjy/bashi on Github.

References