Building a Port Scanner

CybersecurityBeginner
Practice Now

Introduction

Welcome to the Building a Port Scanner lab.

A port scanner is a fundamental tool in network security used to identify open ports and services running on a target machine. Tools like Nmap are incredibly powerful, but understanding how they work under the hood is essential for any aspiring cybersecurity professional or network engineer.

In this lab, you will build your own custom TCP port scanner from scratch using Python. You will start by parsing command-line arguments, then implement the core socket connection logic to check if a port is open. After that, you will add exception handling to make your tool robust, and finally, you will implement multithreading to drastically speed up the scanning process. By the end of this lab, you will have a functional, fast port scanner and a solid understanding of network programming in Python.

The educational goal is not to replace mature tools like Nmap. It is to expose the moving parts those tools automate: input handling, connection attempts, timeouts, result interpretation, and performance tradeoffs. Once you build a basic scanner yourself, professional scanners become much less mysterious.

Parse Command-Line Arguments

In this step, you will set up the foundation of your port scanner by creating a Python script that can accept command-line arguments. Command-line arguments allow users to specify the target IP address and the range of ports they want to scan without modifying the code.

This is an example of separating tool logic from user input. Good security tools are reusable because the code stays the same while the operator changes parameters from the command line.

We will use Python's built-in argparse module, which makes it easy to write user-friendly command-line interfaces.

First, ensure you are in your project directory. This is where you will create and run your Python script.

cd ~/project

Create a new file named scanner.py using the nano text editor:

nano scanner.py

Copy and paste the following code into the editor. This code imports the argparse module and defines a function to handle the inputs for the target IP and the port range.

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}")

Save the file by pressing Ctrl+O, then Enter. If you want to continue directly to the next step, you can stay inside nano and keep editing the same file. Use Ctrl+X only when you want to return to the terminal.

Now, test your script by running it with Python. We will pass the localhost IP (127.0.0.1) and a small port range (7990-8010).

python3 scanner.py -t 127.0.0.1 -p 7990-8010

You should see an output similar to this:

[*] Target set to: 127.0.0.1
[*] Port range set to: 7990-8010

If you try running the script without the -t argument, argparse will automatically generate an error message and show the correct usage, making your tool easier to use from the terminal.

Implement Socket Connection Logic

In this step, you will implement the core functionality of the port scanner: the socket connection.

At the protocol level, a basic TCP connect scan is simple: try to open a TCP connection to a target port and observe whether it succeeds. That one idea is enough to explain the behavior of a large class of port scanners.

A socket is an endpoint for sending or receiving data across a computer network. To check if a port is open, your scanner will attempt to establish a TCP connection to the target IP address on a specific port. If the connection is successful, the port is open; if it fails or times out, the port is closed or filtered.

If scanner.py is still open in nano, continue editing it. Otherwise, reopen it:

nano ~/project/scanner.py

Update your code to include the socket module and add the scanning logic. Replace the entire contents of the file with the following code:

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.")

Save the file with Ctrl+O and Enter. You can remain in nano and continue to the next step, because the script does not need to be tested yet unless you want to check your progress.

In this code, socket.socket(socket.AF_INET, socket.SOCK_STREAM) creates a TCP socket. The connect_ex() method is particularly useful for scanners because it returns an error indicator (like 0 for success) instead of raising an exception when a connection fails, which keeps our code clean.

Run your updated script against your local machine. The lab environment has a hidden web server running on port 8000, so let's scan a range that includes it:

python3 scanner.py -t 127.0.0.1 -p 7990-8010

You should see output similar to the following, successfully identifying port 8000 as open:

[*] Scanning target 127.0.0.1 from port 7990 to 8010...

[+] Port 8000 is OPEN

[*] Scan completed.

Depending on the lab environment, you may also see other open ports in the output. The important result for this step is that port 8000 appears as open.

Handle Connection Exceptions

In this step, you will make your scanner more robust by adding exception handling.

Security tools spend much of their life in imperfect conditions: bad input, unreachable hosts, slow networks, and interrupted runs. Exception handling is not decoration. It is part of making the tool usable during real operations.

Network operations are unpredictable. The user might provide an invalid hostname, the network might drop, or the user might want to cancel the scan midway by pressing Ctrl+C. Without proper exception handling, your Python script will crash and display an ugly traceback error to the user.

We will use try-except blocks to catch these specific errors and handle them gracefully.

If you are still editing scanner.py, keep working in the same nano session. Otherwise, reopen it:

nano ~/project/scanner.py

Update your code to include exception handling. Replace the contents with the following updated version:

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.")

Save the file with Ctrl+O and Enter. If you want to test the exception handling immediately, exit with Ctrl+X and run the commands below. If not, you can continue directly into the final step and leave nano only when you are ready to run the finished scanner.

Now, let's test the exception handling. Try running the script with an invalid port format to trigger the ValueError:

python3 scanner.py -t 127.0.0.1 -p 8000,8001

You should see a clean error message instead of a Python crash:

[-] Invalid port format. Please use start-end (e.g., 1-1024)

Next, start a scan on a large range of ports, and while it is running, press Ctrl+C on your keyboard to cancel it:

python3 scanner.py -t 127.0.0.1 -p 1-5000

When you press Ctrl+C, the script will catch the KeyboardInterrupt and exit gracefully:

[*] Scanning target 127.0.0.1 from port 1 to 5000...

[-] Scan canceled by user. Exiting...

If your scan completes before you can interrupt it, run it again with a wider range such as 1-9000, then press Ctrl+C while it is still running.

Add Multithreading for Speed

In this final step, you will solve the biggest problem with your current scanner: speed.

Right now, your scanner checks one port at a time. If a port is filtered and takes 1 second to timeout, scanning 1,000 ports will take 1,000 seconds. To fix this, we will use multithreading. Multithreading allows your program to run multiple tasks concurrently. Instead of waiting for one port check to finish before starting the next, we will spawn a new thread for every port.

If you kept scanner.py open, continue editing it. Otherwise, reopen it:

nano ~/project/scanner.py

Import the threading module and update the main loop to use threads. Replace the contents with this final version:

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.")

Save the file (Ctrl+O, Enter) and exit (Ctrl+X).

Notice how we added threading.Thread(target=scan_port, args=(target_ip, port)). This tells Python to run the scan_port function in the background. The thread.join() loop ensures the main program waits for all background scans to finish before printing "Scan completed."

Now, run a scan on a large range of ports. You will notice it finishes much faster than the previous sequential version.

python3 scanner.py -t 127.0.0.1 -p 1-9000

Output will look similar to this:

[*] Scanning target 127.0.0.1 from port 1 to 9000...

[+] Port 8000 is OPEN

[*] Scan completed.

Depending on the services running in the lab environment, you may see additional open ports besides 8000. That is normal.

You have successfully built a fast, multithreaded TCP port scanner in Python.

Summary

In this lab, you successfully built a custom, multithreaded TCP port scanner from scratch using Python. You started by learning how to parse command-line arguments using the argparse module to make your tool flexible. You then dove into network programming by implementing socket connections to detect open ports. To ensure your tool was robust, you added exception handling to gracefully manage network errors and user interruptions. Finally, you implemented multithreading, transforming a slow, sequential script into a high-speed concurrent scanner.

This hands-on experience demystifies how professional network scanning tools operate under the hood and provides a strong foundation for further exploration into network security and Python development.