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

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 would take apart the function step by step in the following paragraphs.

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

ss -Htan

ss is a tool for investigating sockets, which prints out ports currently used in the form of a table:

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 . 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 prints out the second part (e.g., 17500). sort -u sorts the items and removes duplicated ones.

Now we’ve got the list of unavailable ports. We might name it as LIST2 for simplicity. The next step is to create another list LIST1 by “inversing” LIST2. By “inversing” we mean LIST1 (disjointly) unioning LIST2 would be equal to 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 known as 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 incorporate 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