Publi

¿Cómo cerrar un puerto TCP ocupado por una aplicación en GNU/Linux?

Cuando vamos a establecer una comunicación entre dos máquinas a través de una red TCP/IP, vamos lo que estamos haciendo a diario miles de veces con nuestro ordenador mientras navegamos por Internet, lo hacemos a través de un puerto. Imaginemos que tenemos un sistema de comunicación rudimentario entre 10 amigos, con 5 cables y, cada cable, nos permite hablar con uno de nuestros amigos, pero claro, como sólo tenemos 5 cables, sólo podemos hablar con 5 amigos al mismo tiempo, así que en algún punto del recorrido deberá haber alguien con la capacidad de enchufar y desenchufar esos cables. El caso es que esos cables serían los canales, o puertos de nuestro sistema, alguien deberá enchufar y desenchufar cables para asegurarse de que nos conectemos con quien queremos.
Por lo tanto, yo tengo 5 cables que corresponden con las 5 conexiones que puedo tener simultáneas, ahora, por seguir un orden, si quiero conectarme con Alberto, éste se conectará con mi canal 1, aunque nadie me dice a mí que yo también esté en su canal 1. Ahora bien, conecto con Bruno por el canal 2 y cierro a Alberto, para conectar con Carla, que podemos enchufarla en el 1, ya que está libre… ahora Bruno y Alberto contactarán entre ellos, cada uno por alguno de los canales que tenga libre. Bueno, ¿se capta la idea?

Hablando de TCP/IP tenemos algunos más de 5 puertos (podríamos tener 65535 puertos) y serían las conexiones que podemos tener abiertas al mismo tiempo desde una máquina, o al menos desde una misma dirección IP. Y, hablando de nuestro ordenador, cada programa que tenemos en ejecución y que hace conexiones de red puede estar utilizando determinados puertos para establecer las comunicaciones. Por ejemplo, nuestro navegador, programas de chat, correo, música online, sincronización de información y demás tendrán ciertos puertos abiertos para utilizar dichos servicios. Algunos puertos estarán abiertos en modo escucha (para que otros se conecten con nosotros) y otros puertos serán las conexiones establecidas (nos hemos conectado a un puerto en modo escucha, en nuestro equipo, o en un equipo de la red).

Entonces, las aplicaciones que corren en nuestro sistema pueden tener determinados puertos abiertos para realizar su trabajo.

Nos gusta el cacharreo

Ahora bien, puede que si estamos desarrollando un programa que haga uso de la red, nuestro programa deje un puerto abierto y se haya quedado bloqueado. O también puede ser que estemos intentando poner a prueba la gestión de errores de un software, o estemos haciendo algo de ingeniería inversa. En cualquier modo, lo que queremos es cerrar sin piedad un puerto en nuestro ordenador.
Tenemos que tener cuidado porque, si lo hacemos sin control, puede que algún programa haga cosas muy raras. Además, necesitaremos permisos de root para algunas cosas, por lo que serán acciones privilegiadas.
En mi caso, casi siempre que hago esto es para experimentar. Por ejemplo, ¿cómo se comportaría mi programa si cierro la conexión de repente? Sin que ninguna de las dos partes se lo espere. Como también está bien utilizar iptables para restringir las futuras comunicaciones de mi aplicación (sería algo así como tirar del cable). El objetivo es que nuestro software sea más rubusto y sepa qué hacer en este tipo de casos.

Ver qué programa utiliza qué puerto

Lo primero que debemos hacer es averiguar qué proceso está utilizando el puerto que queremos. Con netstat.

netstat -tanp
(No todos los procesos pueden ser identificados, no hay información de propiedad del proceso
no se mostrarán, necesita ser superusuario para verlos todos.)
Conexiones activas de Internet (servidores y establecidos)
Proto  Recib Enviad Dirección local         Dirección remota       Estado       PID/Program name
tcp        0      0 127.0.0.1:631           0.0.0.0:*               ESCUCHAR    --
tcp        0      0 0.0.0.0:45847           0.0.0.0:*               ESCUCHAR    18904/nc
tcp        0      0 0.0.0.0:17500           0.0.0.0:*               ESCUCHAR    19556/dropbox
tcp        0      0 127.0.0.1:4381          0.0.0.0:*               ESCUCHAR    30420/spotify
tcp        0      0 192.168.0.7:60892       64.233.167.188:5228     ESTABLECIDO 22144/libpepflashpl
tcp        0      0 192.168.0.7:42090       157.55.235.157:40008    ESTABLECIDO 1992/skype
tcp        0      0 192.168.0.7:39342       xx.xxx.xxx.xx:28         ESTABLECIDO 7428/ssh
tcp     3775      0 127.0.0.1:35381         127.0.0.1:37516         ESTABLECIDO --
tcp        0      0 192.168.0.7:48130       192.30.253.124:443      ESTABLECIDO 24441/firefox
tcp        0      1 192.168.0.7:33380       92.189.149.89:35148     FIN_WAIT1   --
tcp        0      1 192.168.0.7:33988       92.189.149.89:35148     FIN_WAIT1   --
tcp        0      1 192.168.0.7:39694       37.35.181.204:63905     FIN_WAIT1   --
tcp        0      0 192.168.0.7:60244       xx.xxx.xxx.xx:143        ESTABLECIDO 3708/thunderbird
tcp        0      0 192.168.0.7:60544       138.68.115.83:443       TIME_WAIT   --
tcp        0      0 127.0.0.1:50542         127.0.0.1:35381         ESTABLECIDO 7345/emacs
tcp        0      0 192.168.0.7:53492       72.21.206.121:443       ESTABLECIDO 24441/firefox
tcp        0      0 127.0.0.1:37516         127.0.0.1:35381         ESTABLECIDO 7345/emacs

Sólo he puesto una pequeña muestra. Con los argumentos tanp conseguimos:
  • -t: Sólo muestra conexiones TCP.
  • -a: Muestra también los puertos que estamos escuchando
  • -n: No resuelve las direcciones. (Es mucho más rápido, pero veremos sólo IPs y no nombres)
  • -p: Muestra los procesos que originan dichas conexiones. (pid y nombre, si es posible)

Vemos que muchos programas no aparecen y eso puede ser debido a los privilegios con los que hemos ejecutado el programa, por lo que si utilizamos sudo deberíamos ver todos o casi todos. Por supuesto, la salida de netstat la podemos pasar por grep, para realizar un filtrado de lo que nos interesa:

netstat -tanp | grep  40008
tcp        0      0 192.168.0.7:42090       157.55.235.157:40008    ESTABLECIDO 1992/skype

Si queremos más información sobre las conexiones abiertas y su asociación con procesos, en lugar de netstat podremos utilizar lsof. Aunque con lsof veremos muchísima información, ya que no sólo se centra en conexiones de red, sino que veremos cada descriptor de archivo abierto en el sistema. Y como las conexiones de red suelen tener un descriptor de archivo asociado, las veremos también . Por lo que, con lsof, podremos visualizar todos los descriptores abiertos por todos los procesos. Por ejemplo:

sudo lsof -n | grep emacs | grep TCP
emacs      7345                     gaspy   17u     IPv4           40529162       0t0        TCP 127.0.0.1:49778->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   18u     IPv4           40538567       0t0        TCP 127.0.0.1:50542->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   19u     IPv4           40585112       0t0        TCP 127.0.0.1:53658->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   20u     IPv4           40595540       0t0        TCP 127.0.0.1:53728->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   21u     IPv4           41279086       0t0        TCP 127.0.0.1:37436->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   22u     IPv4           41283195       0t0        TCP 127.0.0.1:37516->127.0.0.1:35381 (ESTABLISHED)
emacs      7345                     gaspy   23u     IPv4           41284210       0t0        TCP 127.0.0.1:37578->127.0.0.1:35381 (ESTABLISHED)

Con lo que veremos las conexiones TCP que ha establecido mi aplicación Emacs.

También podemos conocer con fuser, el proceso que origina la conexión. Conociendo el puerto (por ejemplo 35381):

fuser -n tcp 35381
35381/tcp:            7359

Matando procesos

Si queremos ser rápidos, lo más fácil para cerrar la conexión de manera abrupta, y una buena forma para probar nuestros programas es matarlos. Como muchos sabréis podemos hacerlo con kill.

kill 7345

O con killall, pero tenemos que tener cuidado si hay varios procesos con el mismo nombre.
killall emacs

Aunque tanto kill como killall, por defecto intentan cerrar la aplicación educadamente, y la aplicación puede elegir cerrarse o no. Incluso puede estar bloqueada, por lo que si queremos podemos utilizar el argumento -9 o -SIGKILL , con lo que el programa no tendrá más remedio que terminar, es más, no tiene elección, ya que es el sistema operativo el que lo mata.

De todas formas, una forma algo más segura es matar al proceso que tiene un determinado puerto. Igual que antes veíamos la información con fuser. Este comando también mata aplicaciones, directamente enviando SIGKILL, es decir, matando, sin pedir permiso, las aplicaciones (a no ser que lo especifiquemos en los argumentos):

fuser -k -n tcp 8080
8080/tcp:            23003

Método artesano que sólo cierra conexiones

Me encanta este método. Con él hacemos que el proceso entre en depuración y lo matamos. Una cosa importante a tener en cuenta, como dije antes, son los descriptores. Cada conexión, o fichero abierto tiene un descriptor único para la aplicación en la que se encuentra. Y suelen ser números pequeños y secuenciales. Así como la salida estándar por pantalla suele ser el 1, la salida de error el 2, y la entrada por teclado el 0, cuando abrimos el primer fichero, se le asignará el 3 y cuando creamos una conexión después el 4, y así sucesivamente. Entonces, veamos cómo al ejecutar lsof, vemos en mi cuarta columna, generalmente unos números. Para hacer el ejemplo más legible, he ejecutado netcat para abrir un puerto, así:

nc -l 54321

Luego, para averiguar el ID de proceso, lo podemos hacer con fuser, por ejemplo, aunque podemos utilizar cualquier comando de los explicados anteriormente:
fuser -n tcp 54321
54321/tcp:           23101

Ya que tenemos nuestro PID, 23101, vemos los descriptores abiertos por ese proceso:
lsof -np 23101
COMMAND   PID  USER   FD   TYPE    DEVICE SIZE/OFF    NODE NAME
nc      23101 gaspy  cwd    DIR      8,22    12288 7602177 /home/gaspy
nc      23101 gaspy  rtd    DIR      8,18     4096       2 /
nc      23101 gaspy  txt    REG      8,18    31248  129397 /bin/nc.openbsd
nc      23101 gaspy  mem    REG      8,18  1868984  426046 /lib/x86_64-linux-gnu/libc-2.23.so
nc      23101 gaspy  mem    REG      8,18   101200  400728 /lib/x86_64-linux-gnu/libresolv-2.23.so
nc      23101 gaspy  mem    REG      8,18    81040  393967 /lib/x86_64-linux-gnu/libbsd.so.0.8.2
nc      23101 gaspy  mem    REG      8,18   162632  396890 /lib/x86_64-linux-gnu/ld-2.23.so
nc      23101 gaspy    0u   CHR    136,14      0t0      17 /dev/pts/14
nc      23101 gaspy    1u   CHR    136,14      0t0      17 /dev/pts/14
nc      23101 gaspy    2u   CHR    136,14      0t0      17 /dev/pts/14
nc      23101 gaspy    3u  IPv4 139144920      0t0     TCP *:54321 (LISTEN)

Al final, veremos 0u, 1u, 2u, 3u (la u indica que es de lectura/escritura; r, sería lectura y w, escritura. Aunque algunas veces tenemos descriptores de lectura/escritura que no son necesariamente así… pero eso es otra historia). Lo que nos interesa es que el descriptor 3 es el que corresponde con la conexión que queremos cerrar. Ahora, podemos utilizar gdb para depurar el programa e introducir un comando close() a dicho descriptor, para cerrarlo (salen muchas letras, pero es sencillo):

sudo gdb -p 23101
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type “show copying”
and “show warranty” for details.
This GDB was configured as “x86_64-linux-gnu”.
Type “show configuration” for configuration details.
Para las instrucciones de informe de errores, vea:
.
Find the GDB manual and other documentation resources online at:
.
For help, type “help”.
Type “apropos word” to search for commands related to “word”.
Adjuntando a process 23101
Leyendo símbolos desde /bin/nc.openbsd…(no se encontraron símbolos de depuración)hecho.
Leyendo símbolos desde /lib/x86_64-linux-gnu/libbsd.so.0…(no se encontraron símbolos de depuración)hecho.
Leyendo símbolos desde /lib/x86_64-linux-gnu/libresolv.so.2…Leyendo símbolos desde /usr/lib/debug//lib/x86_64-linux-gnu/libresolv-2.23.so…hecho.
hecho.
Leyendo símbolos desde /lib/x86_64-linux-gnu/libc.so.6…Leyendo símbolos desde /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.23.so…hecho.
hecho.
Leyendo símbolos desde /lib64/ld-linux-x86-64.so.2…Leyendo símbolos desde /usr/lib/debug//lib/x86_64-linux-gnu/ld-2.23.so…hecho.
hecho.
0x00007ff39bffa060 in __accept_nocancel () at ../sysdeps/unix/syscall-template.S:84
84 ../sysdeps/unix/syscall-template.S: No existe el archivo o el directorio.
call close(3)
1 = 0
quit
Una sesión de depuración está activa.
Inferior 1 [process 23355] will be detached.
¿Salir de cualquier modo? (y or n)
y
Separando desde el programa: /bin/nc.openbsd, process 23355

Eso sí, puede que el programa que estamos corriendo dependa de la conexión, o ese sea su único propósito, por ejemplo netcat o telnet, por lo que una vez cerrada la conexión se cierran. Aunque otros programas utilizan las conexiones de red realizan otras tareas como por ejemplo mantener un entorno gráfico, monitorizar el sistema, actualizar un fichero, etc; deberían poder gestionar correctamente ese cierre de conexión sin sufrir ningún desastre.

killcx, o cerrar conexiones educadamente

Ésta es una de esas herramientas (escrita en Perl) que deben estar en nuestra caja de herramientas de sysadmin. Y sirve para matar conexiones, aunque lo hace de un modo seguro. Es más, es capaz de cerrar una conexión que se ha quedado bloqueada en el sistema. Para utilizarla debemos hacer:

sudo ./killcx 127.0.0.1:54321 lo
killcx v1.0.3 -- (c)2009-2011 Jerome Bruandet -- http://killcx.sourceforge.net/
[PARENT] checking connection with [127.0.0.1:54321]
[PARENT] found connection with [127.0.0.1:44904] (ESTABLISHED)
[PARENT] forking child
[CHILD]  setting up filter to sniff ACK on [lo] for 5 seconds
[PARENT] sending spoofed SYN to [127.0.0.1:44904] with bogus SeqNum
[CHILD]  hooked ACK from [127.0.0.1:44904]
[CHILD]  found AckNum [3556219343] and SeqNum [477005426]
[CHILD]  sending spoofed RST to [127.0.0.1:44904] with SeqNum [3556219343]
[CHILD]  sending RST to remote host as well with SeqNum [477005426]
[CHILD]  all done, sending USR1 signal to parent [23768] and exiting
[PARENT] received child signal, checking results…
=> success : connection has been closed !

La IP y puerto variarán según nuestras necesidades, y lo es el dispositivo, que podrá ser eth0, wlan0, etc.
La herramienta podemos descargarla desde aquí.

Foto principal: Alex Lehner

También podría interesarte...

Leave a Reply