Quel est le moyen le plus simple de trouver un port local inutilisé?

52

Quel est le moyen le plus simple de trouver un port local inutilisé?

Actuellement, j'utilise quelque chose de similaire à ceci:

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

Il fait terriblement demi-tour, alors je me demande s’il existe un chemin plus simple, tel qu’un chemin intégré, que j’ai manqué.

mybuddymichael
la source
2
Pourquoi voulez-vous faire cela? Il est intrinsèquement racé (et inefficace - et ajoute le moins -nà netstat et un grep plus sélectif). Pour ce faire, essayez d'ouvrir un port dans le mode dont vous avez besoin et essayez-en un autre s'il n'est pas disponible.
Mat
1
@Mat J'essaie de trouver automatiquement un port ouvert à utiliser avec ssh -Dun serveur SOCKS.
mybuddymichael
Question similaire: stackoverflow.com/questions/13308144/… || POSIX: stackoverflow.com/questions/913501/…
Ciro Santilli a écrit:

Réponses:

24

Si votre application le prend en charge, vous pouvez essayer de transmettre le port 0 à l’application. Si votre application transmet cela au noyau, le port sera alloué de manière dynamique au moment de la demande et il sera garanti qu'il ne sera pas utilisé (l'allocation échouera si tous les ports sont déjà utilisés).

Sinon, vous pouvez le faire manuellement. Le script dans votre réponse a une condition de concurrence critique, le seul moyen de l'éviter est de vérifier de manière atomique s'il est ouvert en essayant de l'ouvrir. Si le port est en cours d'utilisation, le programme doit être arrêté avec une incapacité à ouvrir le port.

Par exemple, supposons que vous essayez d'écouter avec 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
Chris Down
la source
1
@Lekensteyn: Où voyez-vous une condition de concurrence ici?
Chris Down
1
Ce port tente d'utiliser le premier port disponible. Lorsque vous avez deux processus simultanés, le port qui vient d'être vérifié peut être réutilisé. Relisant votre réponse, il semble que vous suggériez de relier la liaison sur un port disponible jusqu'à ce que tous les ports soient épuisés. En supposant que le programme en question puisse faire la distinction entre le "port utilisé" et les autres erreurs, cela devrait aller (même si la randomisation le rendrait encore meilleur en cas d'imprévisibilité).
Lekensteyn
1
@Lekensteyn Une liaison de port réussie a pour résultat que le noyau renvoie EADDRINUSE si vous essayez de l'utiliser à nouveau, il est impossible que "le port qui vient d'être vérifié soit réutilisé".
Chris Down
Oui, j’ai supposé à tort que vous quitteriez la boucle et que vous utiliseriez $portle programme tel quel while ...; done; program --port $port.
Lekensteyn
Depuis la page de manuel: -p source_port Spécifie le port source que nc doit utiliser, sous réserve des restrictions de privilèges et de la disponibilité. C'est une erreur d'utiliser cette option en conjonction avec l'option -l.
Monksy
54

Ma solution est de se connecter au port 0, qui demande au noyau d'allouer un port à partir de son ip_local_port_range. Fermez ensuite le socket et utilisez ce numéro de port dans votre configuration.

Cela fonctionne parce que le noyau ne semble pas réutiliser les numéros de port avant que ce ne soit absolument nécessaire. Les liaisons ultérieures au port 0 attribueront un numéro de port différent. Code python:

import socket

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

Cela donne juste un numéro de port, par exemple. 60123.

Exécutez ce programme 10 000 fois (vous devriez les exécuter simultanément) et vous obtiendrez 10 000 numéros de port différents. Par conséquent, je pense que l’utilisation des ports est assez sûre.

Mark Theunissen
la source
20
Voici un one-liner (valide avec Python 2 et Python 3):python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()'
Lekensteyn
4
J'ai couru l'expérience mentionnée et tous les résultats n'étaient pas uniques. Mon histogramme était:{ 1: 7006, 2: 1249, 3: 151, 4: 8, 5: 1, 6: 1}
bukzor le
2
Existe-t-il un moyen simple d’ajouter dans une vérification que le port n’est pas bloqué par un pare-feu ou recherche uniquement les ports ouverts?
Mark Lakata
1
@dshepherd Je pense que vous obtiendrez des ports différents si vous ne fermez pas le précédent (et fermez-les tous en même temps).
Franklin Yu
1
Une doublure pour Ruby 2.3.1:ruby -e 'puts Addrinfo.tcp("", 0).bind { |s| s.local_address.ip_port }'
Franklin Yu
12

Bon mot

J'ai mis au point une ligne simple qui répond rapidement à cet objectif, permettant de saisir un nombre arbitraire de ports dans une plage quelconque (ici, il est divisé en 4 lignes pour la lisibilité):

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"

Ligne par ligne

commest un utilitaire qui compare les lignes de deux fichiers devant apparaître triés par ordre alphabétique. Il génère trois colonnes: les lignes qui apparaissent uniquement dans le premier fichier, les lignes qui apparaissent uniquement dans le second et les lignes communes. En spécifiant, -23nous supprimons ces dernières colonnes et ne conservons que la première. Nous pouvons utiliser ceci pour obtenir la différence de deux ensembles, exprimés sous la forme d'une séquence de lignes de texte. J'ai appris à propos de comm ici .

Le premier fichier est la gamme de ports que nous pouvons sélectionner. seqproduit une séquence triée de nombres allant de $FROMà $TO. Le résultat est trié par ordre alphabétique (au lieu de numérique) et dirigé vers commle premier fichier en utilisant la substitution de processus .

Le deuxième fichier est la liste triée des ports, obtenue en appelant la sscommande (ce qui -tsignifie ports TCP, -asignifiant tout établi et à l'écoute - et -nnumérique - n'essayez pas de résoudre, par exemple, 22en ssh). Nous sélectionnons ensuite uniquement la quatrième colonne awkcontenant l’adresse locale et le port. Nous avons l'habitude cutde scinder l'adresse et le port avec le :délimiteur et de ne garder que celui-ci ( -f2). ssgénère également un en-tête, que nous éliminons par grepping pour les séquences non vides de nombres ne dépassant pas 5. Nous nous conformons ensuite à comml'exigence de s sorten dupliquant -u.

Maintenant , nous avons une liste triée des ports ouverts, que nous pouvons shufFLE pour saisir ensuite les premiers "$HOWMANY"ceux avec head -n.

Exemple

Saisissez les trois ports ouverts aléatoires dans la plage privée (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

pourrait revenir par exemple

54930
57937
51399

Remarques

  • passer -tavec -udans sspour obtenir des ports UDP libres à la place.
  • remplacez shufpar sort -nsi vous préférez avoir les ports disponibles triés numériquement plutôt que de façon aléatoire
Stefanobaghino
la source
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

Crédits à Chris Down

Stefan Leisten
la source
6

Apparemment, les connexions TCP peuvent être utilisées comme descripteurs de fichiers sous Linux à partir de avec bash / zsh. La fonction suivante utilise cette technique et devrait être plus rapide que d'appeler 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
}

Instructions d'utilisation: Liez le résultat à une variable et utilisez-le dans des scripts. Testé sur Ubuntu 16.04

root@ubuntu:~> EPHEMERAL_PORT
59453
root@ubuntu:~> PORT=$(EPHEMERAL_PORT)
Sandeep
la source
Fonctionne avec ksh93aussi.
fpmurphy le
Si vous modifiez UPORT en 32768, vous pouvez toujours obtenir EG 35835. RANDOM renvoie un nombre dans [0,32767]. Modding ceci par un nombre supérieur au maximum n'a aucun effet. Vous voulez quelque chose comme $[$LPORT + ($RANDOM % ($UPORT-$LPORT))].
Lxs
Sinon assez cool quand même!
Lxs
Cela envoie \nà n’importe quel port d’écoute :) Je suggère d’ajouter -n. Ceci essayera toujours d'ouvrir une connexion mais n'enverra rien mais déconnectera immédiatement.
Stefanct
4

Voici un "oneliner" multi-plateforme et efficace qui sillonne tous les ports utilisés et vous donne le premier disponible à partir de 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
  }
'

Vous pouvez simplement joindre toutes les lignes pour l’avoir sur une seule ligne. Si vous souhaitez obtenir le premier disponible à partir d'un numéro de port différent, modifiez l'affectation en idans la forboucle.

Cela fonctionne à la fois sur Mac et Linux, c'est pourquoi la [:.]regex est nécessaire.

w00t
la source
Pourquoi -aet ne -tpas simplement regarder les sockets TCP (6)?
Stefanct
Et tant que nous y sommes, analyser le résultat de ss -Htnlpourrait être meilleur (et plus rapide! - pas que je me soucie de ça: P).
Stefanct
@stefanct BSD netstat ne possède pas -t, du moins celui fourni par Apple, et ssn'est pas présent non plus sur macOS. netstat -alnfonctionne même sur Solaris.
w00t
3

Sous Linux, vous pouvez faire quelque chose comme:

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

Pour trouver le premier port libre supérieur à 1080. Notez que la ssh -Dliaison serait liée à l'interface de bouclage. En théorie, vous pouvez donc réutiliser le port 1080 si un socket l'a lié à une autre adresse. Une autre façon serait d'essayer de le lier:

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'
Stéphane Chazelas
la source
Cela implique toutefois une situation de concurrence critique entre la tentative d'ouverture du port et son utilisation effective.
Chris Down
@ChrisDown, en effet, mais avec ssh -D, je ne vois pas de meilleure option. L' -O forwardoption de sshne renvoie pas d'erreur lorsque le transfert échoue.
Stéphane Chazelas
3

Ceci est la version que j'utilise:

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

echo "$port"

La commande shuf -n 1 -i 49152-65535vous donne un port "aléatoire" dans la plage dynamique. S'il est déjà utilisé, un autre port de cette gamme est essayé.

La commande netstat -atunrépertorie tous les ports (-a) TCP (-t) et UDP (-u) sans perdre de temps à déterminer les noms d'hôte (-n).

pfo
la source
1

Cela fait partie d'une fonction que j'ai dans mon .bashrc, qui crée dynamiquement des tunnels SSH et tente d'utiliser n'importe quel port dans une plage:

   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

mills013
la source
1

Encore une autre course à ce vieux cheval de bataille:

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

Ce code utilise purement portable netstat, egrep, awk, et al. Notez que seul l'appel est émis vers les commandes externes, pour obtenir une liste des ports pris au début. On peut demander un ou plusieurs ports libres:

:;  random_free_tcp_port
2070
:;  random_free_tcp_port 2
2073
2114

et commencez à un port arbitraire:

:;  random_free_tcp_port 2 10240
10245
10293
Solidsnack
la source
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

Ma combinaison d'autres réponses ci-dessus. Tu piges:

Avec shuf -n1, nous prenons un nombre aléatoire de la plage (-i) dans / proc / sys / net / ipv4 / ip_local_port_range. shuf a besoin de la syntaxe avec dash, nous utilisons donc tr pour changer de tabulation dans un tiret.

Nous utiliserons ensuite netstat pour nous montrer toutes les connexions (-a) tcp et udp (-u -t) en nombres (-n), si nous trouvons notre port aléatoire $ port dans cet emplacement (commençons par a: et finissons par w espaces blancs ( \ s) alors nous avons besoin d’un autre port et nous continuons ainsi. Sinon (grep -q a un code retour> 0 nous avons laissé la boucle while et $ port est défini

Winfried Münch
la source
1

Si vous avez du python, je le ferais:

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

Mon point de vue ... la fonction essaie de trouver ndes ports libres consécutifs:

#!/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
Stefanct
la source