Introducción
Bienvenido al laboratorio de Construcción de un escáner de puertos.
Un escáner de puertos es una herramienta fundamental en la seguridad de redes, utilizada para identificar puertos abiertos y servicios que se ejecutan en una máquina objetivo. Herramientas como Nmap son increíblemente potentes, pero comprender cómo funcionan internamente es esencial para cualquier aspirante a profesional de la ciberseguridad o ingeniero de redes.
En este laboratorio, construirás tu propio escáner de puertos TCP personalizado desde cero utilizando Python. Comenzarás analizando los argumentos de la línea de comandos, luego implementarás la lógica central de conexión de sockets para verificar si un puerto está abierto. Después, añadirás manejo de excepciones para hacer que tu herramienta sea robusta y, finalmente, implementarás el multiproceso (multithreading) para acelerar drásticamente el proceso de escaneo. Al finalizar este laboratorio, tendrás un escáner de puertos funcional y rápido, además de una sólida comprensión de la programación de redes en Python.
El objetivo educativo no es reemplazar herramientas maduras como Nmap, sino exponer las piezas móviles que esas herramientas automatizan: manejo de entradas, intentos de conexión, tiempos de espera (timeouts), interpretación de resultados y compensaciones de rendimiento. Una vez que construyas tu propio escáner básico, las herramientas profesionales dejarán de ser un misterio.
Analizar argumentos de la línea de comandos
En este paso, establecerás la base de tu escáner de puertos creando un script de Python que pueda aceptar argumentos desde la línea de comandos. Los argumentos de línea de comandos permiten a los usuarios especificar la dirección IP objetivo y el rango de puertos que desean escanear sin necesidad de modificar el código.
Este es un ejemplo de cómo separar la lógica de la herramienta de la entrada del usuario. Las buenas herramientas de seguridad son reutilizables porque el código permanece igual mientras el operador cambia los parámetros desde la línea de comandos.
Utilizaremos el módulo integrado de Python argparse, que facilita la creación de interfaces de línea de comandos fáciles de usar.
Primero, asegúrate de estar en el directorio de tu proyecto. Aquí es donde crearás y ejecutarás tu script de Python.
cd ~/project
Crea un nuevo archivo llamado scanner.py usando el editor de texto nano:
nano scanner.py
Copia y pega el siguiente código en el editor. Este código importa el módulo argparse y define una función para manejar las entradas de la IP objetivo y el rango de puertos.
import argparse
import sys
def get_arguments():
parser = argparse.ArgumentParser(description="A simple TCP port scanner")
parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
options = parser.parse_args()
return options
if __name__ == "__main__":
options = get_arguments()
## Extract the target and ports from the user's input
target_ip = options.target
ports_input = options.ports
print(f"[*] Target set to: {target_ip}")
print(f"[*] Port range set to: {ports_input}")
Guarda el archivo presionando Ctrl+O y luego Enter. Si deseas continuar directamente al siguiente paso, puedes permanecer dentro de nano y seguir editando el mismo archivo. Usa Ctrl+X solo cuando quieras volver a la terminal.
Ahora, prueba tu script ejecutándolo con Python. Pasaremos la IP local (127.0.0.1) y un rango pequeño de puertos (7990-8010).
python3 scanner.py -t 127.0.0.1 -p 7990-8010
Deberías ver una salida similar a esta:
[*] Target set to: 127.0.0.1
[*] Port range set to: 7990-8010
Si intentas ejecutar el script sin el argumento -t, argparse generará automáticamente un mensaje de error y mostrará el uso correcto, haciendo que tu herramienta sea más fácil de usar desde la terminal.
Implementar la lógica de conexión de sockets
En este paso, implementarás la funcionalidad principal del escáner de puertos: la conexión de socket.
A nivel de protocolo, un escaneo de conexión TCP básico es simple: intenta abrir una conexión TCP a un puerto objetivo y observa si tiene éxito. Esa idea es suficiente para explicar el comportamiento de una gran clase de escáneres de puertos.
Un socket es un punto final para enviar o recibir datos a través de una red informática. Para verificar si un puerto está abierto, tu escáner intentará establecer una conexión TCP a la dirección IP objetivo en un puerto específico. Si la conexión es exitosa, el puerto está abierto; si falla o se agota el tiempo de espera, el puerto está cerrado o filtrado.
Si scanner.py sigue abierto en nano, continúa editándolo. De lo contrario, ábrelo de nuevo:
nano ~/project/scanner.py
Actualiza tu código para incluir el módulo socket y añadir la lógica de escaneo. Reemplaza todo el contenido del archivo con el siguiente código:
import argparse
import sys
import socket
def get_arguments():
parser = argparse.ArgumentParser(description="A simple TCP port scanner")
parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
return parser.parse_args()
def scan_port(ip, port):
## Create a new socket using the IPv4 address family and TCP protocol
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
## Set a timeout of 1 second so the scanner doesn't hang on closed ports
sock.settimeout(1)
## connect_ex returns 0 if the connection was successful
result = sock.connect_ex((ip, port))
if result == 0:
print(f"[+] Port {port} is OPEN")
## Always close the socket after using it
sock.close()
if __name__ == "__main__":
options = get_arguments()
target_ip = options.target
## Parse the start and end ports from the input string
start_port, end_port = map(int, options.ports.split('-'))
print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")
## Loop through the specified range and scan each port sequentially
for port in range(start_port, end_port + 1):
scan_port(target_ip, port)
print("\n[*] Scan completed.")
Guarda el archivo con Ctrl+O y Enter. Puedes permanecer en nano y continuar al siguiente paso, ya que el script no necesita ser probado todavía a menos que quieras verificar tu progreso.
En este código, socket.socket(socket.AF_INET, socket.SOCK_STREAM) crea un socket TCP. El método connect_ex() es particularmente útil para los escáneres porque devuelve un indicador de error (como 0 para éxito) en lugar de lanzar una excepción cuando una conexión falla, lo que mantiene nuestro código limpio.
Ejecuta tu script actualizado contra tu máquina local. El entorno de laboratorio tiene un servidor web oculto ejecutándose en el puerto 8000, así que escaneemos un rango que lo incluya:
python3 scanner.py -t 127.0.0.1 -p 7990-8010
Deberías ver una salida similar a la siguiente, identificando con éxito el puerto 8000 como abierto:
[*] Scanning target 127.0.0.1 from port 7990 to 8010...
[+] Port 8000 is OPEN
[*] Scan completed.
Dependiendo del entorno de laboratorio, es posible que también veas otros puertos abiertos en la salida. El resultado importante para este paso es que el puerto 8000 aparezca como abierto.
Manejar excepciones de conexión
En este paso, harás que tu escáner sea más robusto añadiendo manejo de excepciones.
Las herramientas de seguridad pasan gran parte de su vida en condiciones imperfectas: entradas incorrectas, hosts inalcanzables, redes lentas y ejecuciones interrumpidas. El manejo de excepciones no es un adorno; es parte de hacer que la herramienta sea utilizable durante operaciones reales.
Las operaciones de red son impredecibles. El usuario podría proporcionar un nombre de host no válido, la red podría caerse o el usuario podría querer cancelar el escaneo a mitad de camino presionando Ctrl+C. Sin un manejo de excepciones adecuado, tu script de Python se bloqueará y mostrará un error de rastreo (traceback) poco elegante al usuario.
Utilizaremos bloques try-except para capturar estos errores específicos y manejarlos de manera elegante.
Si todavía estás editando scanner.py, sigue trabajando en la misma sesión de nano. De lo contrario, ábrelo de nuevo:
nano ~/project/scanner.py
Actualiza tu código para incluir el manejo de excepciones. Reemplaza el contenido con la siguiente versión actualizada:
import argparse
import sys
import socket
def get_arguments():
parser = argparse.ArgumentParser(description="A simple TCP port scanner")
parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
return parser.parse_args()
def scan_port(ip, port):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((ip, port))
if result == 0:
print(f"[+] Port {port} is OPEN")
sock.close()
except socket.gaierror:
## This exception is raised for address-related errors, like an invalid hostname
print("\n[-] Hostname could not be resolved.")
sys.exit(1)
except socket.error:
## This catches general socket errors
print("\n[-] Could not connect to server.")
sys.exit(1)
if __name__ == "__main__":
options = get_arguments()
target_ip = options.target
try:
start_port, end_port = map(int, options.ports.split('-'))
except ValueError:
print("[-] Invalid port format. Please use start-end (e.g., 1-1024)")
sys.exit(1)
print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")
try:
for port in range(start_port, end_port + 1):
scan_port(target_ip, port)
except KeyboardInterrupt:
## This catches the Ctrl+C signal from the user
print("\n[-] Scan canceled by user. Exiting...")
sys.exit(1)
print("\n[*] Scan completed.")
Guarda el archivo con Ctrl+O y Enter. Si deseas probar el manejo de excepciones inmediatamente, sal con Ctrl+X y ejecuta los comandos a continuación. Si no, puedes continuar directamente al paso final y dejar nano solo cuando estés listo para ejecutar el escáner terminado.
Ahora, probemos el manejo de excepciones. Intenta ejecutar el script con un formato de puerto no válido para activar el ValueError:
python3 scanner.py -t 127.0.0.1 -p 8000,8001
Deberías ver un mensaje de error limpio en lugar de un bloqueo de Python:
[-] Invalid port format. Please use start-end (e.g., 1-1024)
A continuación, inicia un escaneo en un rango amplio de puertos y, mientras se ejecuta, presiona Ctrl+C en tu teclado para cancelarlo:
python3 scanner.py -t 127.0.0.1 -p 1-5000
Cuando presiones Ctrl+C, el script capturará el KeyboardInterrupt y saldrá de forma elegante:
[*] Scanning target 127.0.0.1 from port 1 to 5000...
[-] Scan canceled by user. Exiting...
Si tu escaneo se completa antes de que puedas interrumpirlo, ejecútalo de nuevo con un rango más amplio, como 1-9000, y luego presiona Ctrl+C mientras aún se está ejecutando.
Añadir multiproceso para mayor velocidad
En este paso final, resolverás el mayor problema de tu escáner actual: la velocidad.
En este momento, tu escáner verifica un puerto a la vez. Si un puerto está filtrado y tarda 1 segundo en agotar el tiempo de espera, escanear 1,000 puertos tomará 1,000 segundos. Para solucionar esto, utilizaremos el multiproceso (multithreading). El multiproceso permite que tu programa ejecute múltiples tareas simultáneamente. En lugar de esperar a que termine una verificación de puerto antes de comenzar la siguiente, generaremos un nuevo hilo para cada puerto.
Si mantuviste scanner.py abierto, continúa editándolo. De lo contrario, ábrelo de nuevo:
nano ~/project/scanner.py
Importa el módulo threading y actualiza el bucle principal para usar hilos. Reemplaza el contenido con esta versión final:
import argparse
import sys
import socket
import threading
def get_arguments():
parser = argparse.ArgumentParser(description="A simple TCP port scanner")
parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
return parser.parse_args()
def scan_port(ip, port):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((ip, port))
if result == 0:
print(f"[+] Port {port} is OPEN")
sock.close()
except (socket.gaierror, socket.error):
pass
if __name__ == "__main__":
options = get_arguments()
target_ip = options.target
try:
start_port, end_port = map(int, options.ports.split('-'))
except ValueError:
print("[-] Invalid port format. Please use start-end (e.g., 1-1024)")
sys.exit(1)
print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")
try:
## Create a list to keep track of active threads
threads = []
for port in range(start_port, end_port + 1):
## Create a new thread for each port scan
thread = threading.Thread(target=scan_port, args=(target_ip, port))
threads.append(thread)
thread.start()
## Wait for all threads to complete before exiting
for thread in threads:
thread.join()
except KeyboardInterrupt:
print("\n[-] Scan canceled by user. Exiting...")
sys.exit(1)
print("\n[*] Scan completed.")
Guarda el archivo (Ctrl+O, Enter) y sal (Ctrl+X).
Observa cómo añadimos threading.Thread(target=scan_port, args=(target_ip, port)). Esto le indica a Python que ejecute la función scan_port en segundo plano. El bucle thread.join() asegura que el programa principal espere a que todos los escaneos en segundo plano finalicen antes de imprimir "Scan completed."
Ahora, ejecuta un escaneo en un rango amplio de puertos. Notarás que termina mucho más rápido que la versión secuencial anterior.
python3 scanner.py -t 127.0.0.1 -p 1-9000
La salida se verá similar a esto:
[*] Scanning target 127.0.0.1 from port 1 to 9000...
[+] Port 8000 is OPEN
[*] Scan completed.
Dependiendo de los servicios que se ejecuten en el entorno de laboratorio, es posible que veas puertos abiertos adicionales además del 8000. Eso es normal.
Has construido con éxito un escáner de puertos TCP rápido y multiproceso en Python.
Resumen
En este laboratorio, construiste con éxito un escáner de puertos TCP multiproceso personalizado desde cero utilizando Python. Comenzaste aprendiendo a analizar argumentos de línea de comandos usando el módulo argparse para hacer que tu herramienta fuera flexible. Luego, te adentraste en la programación de redes implementando conexiones de socket para detectar puertos abiertos. Para asegurar que tu herramienta fuera robusta, añadiste manejo de excepciones para gestionar de forma elegante los errores de red y las interrupciones del usuario. Finalmente, implementaste el multiproceso, transformando un script secuencial lento en un escáner concurrente de alta velocidad.
Esta experiencia práctica desmitifica cómo operan las herramientas profesionales de escaneo de red bajo el capó y proporciona una base sólida para una mayor exploración en la seguridad de redes y el desarrollo en Python.



