Какой самый простой способ найти неиспользуемый локальный порт?

Вопрос или проблема

Какой самый простой способ найти неиспользуемый локальный порт?

В данный момент я использую что-то подобное:

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

Это кажется довольно запутанным, так что я думаю, есть ли более простой способ, например, встроенная функция, которую я пропустил.

Мое решение – привязаться к порту 0, что означает запрос к ядру на выделение порта из диапазона ip_local_port_range. Затем закройте сокет и используйте этот номер порта в вашей конфигурации.

Это работает, потому что ядро, похоже, не переиспользует номера портов, пока в этом нет абсолютной необходимости. Последующие привязки к порту 0 будут выделять другой номер порта. Код на Python:

import socket

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

Это возвращает просто номер порта, например, 60123.

Запустите эту программу 10000 раз (вы должны запускать их одновременно), и вы получите 10000 различных номеров портов. Поэтому, я думаю, довольно безопасно использовать эти порты.

Если ваше приложение поддерживает это, вы можете попробовать передать порт 0 приложению. Если ваше приложение передаст это ядру, порт будет динамически выделен во время запроса и гарантированно не будет использоваться (выделение не удастся, если все порты уже используются).

В противном случае вы можете сделать это вручную. Скрипт в вашем ответе имеет состоявшуюся гонку, и единственный способ избежать этого – атомарно проверить, открыт ли он, попытавшись открыть его. Если порт используется, программа должна завершиться с ошибкой открытия порта.

Например, скажем, вы пытаетесь слушать с помощью 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

Однострочник

Я составил хороший однострочник, который быстро выполняет задачу, позволяя получить произвольное количество портов в произвольном диапазоне (здесь он разбит на 4 строки для удобства чтения):

comm -23 \
<(seq "$FROM" "$TO" | sort) \
<(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) \
| shuf | head -n "$HOWMANY"

Построчно

comm – это утилита, которая сравнивает строки в двух файлах, которые должны быть отсортированы в алфавитном порядке. Она выводит три колонки: строки, которые появляются только в первом файле, строки, которые появляются только во втором и общие строки. Указывая -23, мы подавляем последние колонки и сохраняем только первую. Мы можем использовать это для получения разности двух множеств, выраженной как последовательность текстовых строк. Я узнал о comm здесь.

Первый файл – это диапазон портов, из которых мы можем выбирать. seq генерирует отсортированную последовательность чисел от $FROM до $TO. Результат сортируется в алфавитном порядке (вместо числового, чтобы соответствовать требованиям comm) и передается comm как первый файл с помощью подстановки процесса.

Второй файл – это отсортированный список портов, который мы получаем, вызывая команду ss-t, означающим TCP порты, -a, означающим все – установленные и слушающие – и -n, числовой – не пытаться разрешить, например, 22 в ssh). Мы затем берем только четвертую колонку с awk, которая содержит локальный адрес и порт. Мы используем cut, чтобы разделить адрес и порт с помощью разделителя : и оставить только последний (-f2). Затем мы соответствуем требованию comm, сортируя без дубликатов -u.

Теперь у нас есть отсортированный список открытых портов, который мы можем shuf сбросить, чтобы затем взять первые "$HOWMANY" с помощью head -n.

Пример

Получите три случайных открытых порта в частном диапазоне (49152-65535)

comm -23 <(seq 49152 65535 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 3

может вернуть, например

54930
57937
51399

Заметки

  • замените -t на -u в ss, чтобы получить свободные UDP порты.
  • замените shuf на sort -n, если вы хотите получить доступные порты, отсортированные по числовому значению, вместо случайных.

Очевидно, что TCP соединения могут использоваться как дескрипторы файлов на Linux из Bash/Zsh. Следующая функция использует эту технику и должна работать быстрее, чем вызов netcat/telnet.

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

Инструкции по использованию: Привяжите вывод к переменной и используйте в скриптах. Протестировано на Ubuntu 16.04

root@ubuntu:~> EPHEMERAL_PORT
59453
root@ubuntu:~> PORT=$(EPHEMERAL_PORT)

#!/bin/bash
read LOWERPORT UPPERPORT < /proc/sys/net/ipv4/ip_local_port_range
while :
do
        PORT="`shuf -i $LOWERPORT-$UPPERPORT -n 1`"
        ss -lpna | grep -q ":$PORT " || break
done
echo $PORT

“ss -lpn” – покажет только порты с установленными соединениями. Мы должны использовать

“ss -lpna”, чтобы учитывать порты, которые слушают, тоже.

Автор: Chris Down

Вот кросс-платформенный, эффективный “однострочник”, который собирает все используемые порты и дает вам первый доступный с 3000 и выше:

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
  }
'

Вы можете просто соединить все строки, чтобы получить это в одной строке. Если вы хотите получить первый доступный от другого номера порта, измените присвоение i в цикле for.

Это работает как на Mac, так и на Linux, поэтому необходима регулярка [:.].

Вот версия, которую я использую:

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

echo "$port"

Команда shuf -n 1 -i 49152-65535 дает вам “случайный” порт в динамическом диапазоне. Если он уже используется, пробуется другой порт из этого диапазона.

Команда netstat -atun перечисляет все (-a) TCP (-t) и UDP (-u) порты, не тратя время на определение имен хостов (-n).

На Linux вы могли бы сделать что-то вроде этого:

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}'

Чтобы найти первый свободный порт выше 1080. Обратите внимание, что ssh -D будет связываться с интерфейсом петли, так что теоретически вы можете повторно использовать порт 1080, если сокет связан с другим адресом. Другой способ – действительно попытаться привязаться к нему:

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'

Если у вас есть Python, я бы сделал так:

port="$(python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1])')";
echo "Неиспользуемый порт: $port"

Еще один вариант, хотя и запоздалый. Этот однострочник даже работает с busybox:

for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1

Измените seq 3000 4000 на shuf -i 3000-4000, чтобы получить случайный порт.

Увеличьте число после head -n, чтобы вернуть более одного свободного порта.

Это требует, чтобы ss был установлен. Если он недоступен, легко изменить ss -tlnH на netstat -tln | tail -n +3, чтобы использовать netstat от busybox.

Еще один подход к этой старой проблеме:

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[@]}"
}

Этот код делает чисто переносимый способ использования netstat, egrep, awk и т.д. Обратите внимание, что всего один вызов делается к внешним командам, чтобы в начале получить список используемых портов. Можно запросить один или несколько свободных портов:

:;  random_free_tcp_port
2070
:;  random_free_tcp_port 2
2073
2114

и начинать с произвольного порта:

:;  random_free_tcp_port 2 10240
10245
10293

Это часть функции в моем .bashrc, которая динамически создает SSH туннели и пытается использовать любой порт из диапазона:

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

   # найти свободный прослушивающий порт
   for port in ${lps[@]}; do
      lsof -i -n -P |grep LISTEN |grep -q ":${port}"
      [ $? -eq 1 ] && { lp=$port; break; }
   done
   [ "$lp" = "null" ] && { echo "нет доступных локальных портов"; return 2; }
   return $port

Ваши результаты могут различаться

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

Моя комбинация из других ответов выше. Получите это:

С помощью shuf -n1 мы берем одно случайное число из диапазона (-i) в /proc/sys/net/ipv4/ip_local_port_range. shuf требует синтаксиса с дефисом, поэтому мы используем tr, чтобы изменить табуляцию на дефис.

Далее мы используем netstat, чтобы показать все (-a) tcp и udp (-u -t) соединения в числах (-n), если мы находим наш случайный порт $port в этом (начиная со знака : и заканчивая пробелом (\s), нам нужен другой порт и так продолжается. В противном случае (grep -q возвращает код > 0), мы выходим из цикла while, и $port установлен.

Моя версия… функция пытается найти n последовательных свободных портов:

#!/bin/bash

RANDOM=$$

# Основано на 
# 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
        # Спать от 100 до 499 мс
        sleep "0.$((100+$RANDOM%399))"
        local start="`shuf -i $lower_port-$(($upper_port-$n-1)) -n 1`"
        local end=$(($start+$n-1))
        # создаем фильтр ss для $n последовательных портов, начиная с $start
        local filter=""
        local ports=$(seq $start $end)
        for p in $ports ; do
            filter="$filter or sport = $p"
        done
        # если ни один из портов не используется, вернуть их
        if [ "$(ss -tHn "${filter# or}" | wc -l)" = "0" ] ; then
            echo "$ports"
            return 0
        fi
    done
    echo "Не удалось найти $n последовательных портов">&2
    return 1
}

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

Еще один ответ для macOS, основанный на этом и этом ответе:

lower_port=`sysctl -n net.inet.ip.portrange.first`
upper_port=`sysctl -n net.inet.ip.portrange.last`

comm -23 \
<(seq "$lower_port" "$upper_port" | sort) \
<(sudo lsof -iTCP -n -P -sTCP:LISTEN -t | sort -u) \
| gshuf | head -n 1

Вы можете изменить 1 в head -n 1 на другое число (или переменную аргумента скрипта), если хотите получить несколько портов.

shuf доступен как gshuf через пакет coreutils в Homebrew.

Код диапазона портов взят из этого ответа на этот вопрос на Stack Overflow:

Этот скрипт работает у меня на macOS

#!/usr/bin/env bash
set -u                    # выход при неопределенной переменной
bash -c 'set -o pipefail' # код возврата первой команды, которая завершится неудачей в конвейере

# Печатает случайный открытый порт. Вдохновлено
# https://superuser.com/a/1041677/537059

CHECK="do while"

while [[ ! -z $CHECK ]]; do
  PORT=$(((RANDOM % 60000) + 1025))
  # Совпадение с концом границы слова, чтобы избежать ложных срабатываний,
  # например, 2000 совпадает с 20000
  # https://stackoverflow.com/a/34074458/639133
  CHECK=$(netstat -an -ptcp | grep LISTEN | grep -r "${PORT}\b")
done

echo ${PORT}

Мне понравился ответ w00t, но я не хотел, чтобы один и тот же свободный порт выбирался каждый раз. Добавив инкрементное обновление, это будет случайным образом выбирать свободный порт между 3000 и 13000 (но если все они заняты, он будет поочередно искать с этого случайно выбранного порта):

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

Еще один ответ. Он очень похож на несколько предложенных, но некоторые из них действительно не работают для меня. Некоторые не говорят мне, какой свободный порт я мог бы использовать. Некоторые дают мне порт, который на самом деле не свободен. А другие используют перемешивание, которое мне не нужно.

read first last < /proc/sys/net/ipv4/ip_local_port_range
# или определите свой собственный диапазон, например:
# first=9000; last=10000

for port in $(seq $first $last); do
  ss -atun | grep -q ":$port " || break
done

echo $port

Или как длинный однострочник:

read first last < /proc/sys/net/ipv4/ip_local_port_range; for port in $(seq $first $last); do ss -atun | grep -q ":$port " || break; done; echo $port

Мой подход, похожий на несколько идей здесь. Случайным образом выбирает из диапазона и проверяет с помощью nc

# Пытается найти свободный порт случайным образом из заданного диапазона
random_free_port() {
  local max_attempts=5
  local low_range=8432
  local range=200
  for i in $(seq "${max_attempts}"); do
    # Выбираем случайный порт между 8432-8632
    local port="$(($low_range + $RANDOM % $range))"
    # Проверяем, не открыт ли порт
    if ! nc -z -w 0 localhost 8433; then
      echo "Найден свободный порт $port" 1>&2
      echo "${port}"
      return
    fi
  done
  echo "Не удалось найти свободный порт после 5 попыток" 1>&2
  return 1
}

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int
main (int argc, char** argv)
{
  int sock;
  struct sockaddr_in name;
  char *addr;

  if(argc<2) {
     addr="0.0.0.0";
  } else {
     addr=argv[1];
  }
  /* Создаем сокет. */
  sock = socket (PF_INET, SOCK_STREAM, 0);
  if (sock < 0)
    {
      perror ("socket");
      exit (EXIT_FAILURE);
    }

  /* Присваиваем сокету имя. */
  name.sin_family = AF_INET;
  name.sin_port = htons (0);
  name.sin_addr.s_addr = inet_addr(addr);
  if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
    {
      perror ("bind");
      exit (EXIT_FAILURE);
    }

  int len = sizeof(name);
  int sn = getsockname(sock, (struct sockaddr *)&name, &len);
  printf("Порт - %d\n",ntohs(name.sin_port));
  return sock;
}

Компиляция:
gcc getport.c -o getport

Использование: ./getport [адрес]

Вывод:
./getport
Порт - 31725
./getport 127.0.0.1
Порт - 32064
./getport 1.2.3.4
bind: Невозможно назначить запрашиваемый адрес

Ответ или решение

Создание приложения или сервиса, который требует использования свободного локального порта, может представлять определенные трудности, особенно если необходимо избежать конфликтов с уже занятой сетевой активностью. В данной ситуации мы рассмотрим несколько методов для нахождения неиспользуемого локального порта.

1. Использование порта 0

Наиболее простым и эффективным способом является использование порта 0. Это запрос к операционной системе, которая автоматически назначит свободный порт из диапазона, определенного в ip_local_port_range. После этого вы можете закрыть сокет и использовать полученный номер порта в вашей конфигурации.

Пример на Python:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 0))  # Привязываем сокет к порту 0
addr = s.getsockname()
print(addr[1])  # Выводим номер порта
s.close()

2. Использование утилиты ss или netstat

Если вы предпочитаете командную строку, можно воспользоваться утилитой ss (или netstat) для получения списка используемых портов и нахождения свободного.

Пример в Bash:

while true; do
    PORT=$(shuf -i 49152-65535 -n 1)  # Генерация случайного порта
    if ! ss -tln | grep -q ":$PORT"; then  # Проверка, свободен ли порт
        echo "Используйте порт: $PORT"
        break
    fi
done

3. Подборка свободного порта в заданном диапазоне

Вы можете самостоятельно перебрать порты в определенном диапазоне, чтобы найти свободный. Например, используем диапазон приписываемых портов 49152-65535:

Пример:

read lower upper < /proc/sys/net/ipv4/ip_local_port_range
for port in $(seq $lower $upper); do
    if ! ss -tln | grep -q ":$port"; then
        echo "Найден свободный порт: $port"
        break
    fi
done

4. Кроссплатформенное решение

Вот кроссплатформенное решение с использованием awk и netstat, которое найдет первый свободный порт:

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
  }
'

Заключение

Таким образом, для нахождения свободного локального порта вы можете использовать один из описанных выше методов — выбирать тот, который больше соответствует вашим требованиям и окружению. Использование ядром системы порта 0 является наиболее простым и надежным способом, а перебор портов — хорошим вариантом, если требуется более детальный контроль.

Оцените материал
Добавить комментарий

Капча загружается...