Tutorial: Building a Port Scanner in Rust 🦀
Tags: rust
Published: 2024-04-19 17:17:07

---
title: Tutorial: Building a Port Scanner in Rust 🦀
published: true
date: 2024-04-19 12:42:49 UTC
tags: rust
canonical_url: https://eleftheriabatsou.hashnode.dev/tutorial-building-a-port-scanner-in-rust
---

### **Introduction**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lgnu2nyxr7ajubkpr4k.jpeg)

Hello amazing people, welcome to our Rust programming tutorial on creating an IP sniffer! Let's learn how to build a basic network tool that can scan ports on a specified IP address to see which ones are open.

This is a practical project that can help you understand network programming, asynchronous Rust with Tokio, and handling command-line arguments using Bpaf. By the end of this tutorial, you will have a clearer insight into network operations and Rust's powerful asynchronous features.

{% embed https://twitter.com/BatsouElef/status/1781277854711378230 %}


### **The Code**

The code for our IP sniffer is structured in a single `main.rs` file and utilizes dependencies from `cargo.toml`. Let's break down each part of the code to understand its purpose and functionality.

#### **Dependencies and Imports**

```
use bpaf::Bpaf;
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::mpsc::{channel, Sender};
use tokio::net::TcpStream;use tokio::task;
```

Here, we import the necessary modules and crates:

- `bpaf` for parsing command-line arguments.

- `std::io` and `std::net` for input/output operations and networking.

- `std::sync::mpsc` for message passing between threads.

- `tokio` for asynchronous programming.

#### **Constants and CLI Arguments**

This part of the code defines the structure for handling command-line arguments in your Rust application. It uses the `bpaf` crate to parse and validate these arguments efficiently. Here's what each component does:

```
const MAX: u16 = 65535;
const IPFALLBACK: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));

#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub struct Arguments {
    #[bpaf(long, short, argument("Address"), fallback(IPFALLBACK))]
    pub address: IpAddr,
    #[bpaf(long("start"), short('s'), guard(start_port_guard, "Must be greater than 0"), fallback(1u16))]
    pub start_port: u16,
    #[bpaf(long("end"), short('e'), guard(end_port_guard, "Must be less than or equal to 65535"), fallback(MAX))]
    pub end_port: u16,
}

```

- **Constants (`MAX` and `IPFALLBACK`)**: These are predefined values used as defaults. `MAX` sets the maximum value for the end port, ensuring it does not exceed the maximum allowable port number (65535). `IPFALLBACK` provides a default IP address (127.0.0.1, which is the localhost) in case the user does not specify one.

- **`Arguments` struct**: This structure defines the types and constraints of the command-line arguments your program will accept.

`address`: This field accepts an IP address as an input. If the user does not provide an address, it defaults to `IPFALLBACK`.

`start_port` and `end_port`: These fields define the range of ports to scan. The `start_port` must be greater than 0, and `end_port` must be less than or equal to 65535. Default values are provided for both, with `start_port` starting from 1 and `end_port` using the maximum port number possible.

The use of `bpaf` for argument parsing helps in making the command-line interface of your application robust, user-friendly, and less prone to errors, as it handles validation and defaults gracefully.

#### **Port Scanning Function**

The `scan` function in the provided code snippet is an asynchronous function designed to check if a specific port on a given IP address is open. Here's a detailed breakdown of what each part of the function does and why its necessary:

```
async fn scan(tx: Sender<u16>, start_port: u16, addr: IpAddr) {
    match TcpStream::connect(format!("{}:{}", addr, start_port)).await {
        Ok(_) => {
            print!(".");
            io::stdout().flush().unwrap();
            tx.send(start_port).unwrap();
        }
        Err(_) => {}
    }
}

```

**Function Signature:**
`async fn scan(tx: Sender<u16>, start_port: u16, addr: IpAddr)`: This defines an asynchronous function named scan that takes three parameters:

- `tx`: A sender channel of type Sender<u16> which is used to send data (in this case, port numbers) to another part of your program.

- `start_port`: The port number to check.

- `add`r: The IP address on which to check the port.

**TCP Connection Attempt:**
`TcpStream::connect(format!("{}:{}", addr, start_port)).await`: This line attempts to establish a TCP connection to the specified `addr` and `start_port`. The await keyword is used because `TcpStream::connect` is an asynchronous operation, and you need to wait for it to complete before moving on.

**Handling the Connection Result:**
`match TcpStream::connect(...)`: The match statement is used to handle the different outcomes of the connection attempt:

- `Ok(_)`: If the connection is successful (indicating the port is open):

- `print!(".")`: Prints a dot (.) to the standard output as a visual indication of a successful connection.

- `io::stdout().flush().unwrap()`: Ensures that the dot is immediately displayed on the screen by flushing the standard output buffer.

- `tx.send(start_port).unwrap()`: Sends the open port number back through the `tx` channel to be processed or recorded by another part of your program.

- `Err(_)`: If the connection fails (indicating the port is closed), nothing happens ({}).

This function is essential for performing port scanning by checking each port in a specified range to determine if it is open. It utilizes asynchronous programming to handle potentially long-running network operations efficiently, without blocking the execution of other parts of your program. This allows for scanning multiple ports concurrently, significantly speeding up the process.

#### **Main Function**

The `main` function sets up the asynchronous environment, collects arguments, and spawns tasks for scanning each port within the specified range. Results are collected, sorted, and printed.

```
async fn main() {
    let opts = arguments().run();
    let (tx, rx) = channel();
    for i in opts.start_port..opts.end_port {
        let tx = tx.clone();
        task::spawn(async move { scan(tx, i, opts.address).await });
    }
    drop(tx);
    let mut out = vec![];
    for p in rx {
        out.push(p);
    }
    println!("");
    out.sort();
    for v in out {
        println!("{} is open", v);
    }
}
```


**Argument Parsing:**
`let opts = arguments().run();`: This line calls the arguments() function (presumed to be defined elsewhere in your code) which constructs and parses command-line arguments, returning an instance of the Arguments struct stored in opts.

**Channel Setup:**
`let (tx, rx) = channel();`: Here, a multi-producer, single-consumer (MPSC) channel is created. tx is the transmitter or sender, and rx is the receiver. This channel is used to communicate between asynchronous tasks.

**Port Scanning Loop:**
`for i in opts.start_port..opts.end_port`: This loop iterates over the range of ports from start_port to end_port as specified in the command-line arguments.

Inside the loop:
- `let tx = tx.clone();`: Clones the sender part of the channel. This is necessary because the sender will be moved into the asynchronous task.

- `task::spawn(async move { scan(tx, i, opts.address).await });`: Spawns a new asynchronous task for each port. The scan function is called with the current port number i, the cloned sender tx, and the target IP address opts.address. Each task will attempt to connect to its assigned port and send the results back through the channel.

**Closing the Sender:**
`drop(tx);`: Explicitly drops the original sender. This is important because it signals that no more messages will be sent on this channel, allowing the receiver to exit its loop once all sent messages are processed.

**Collecting Results:**
`let mut out = vec![];`: Initializes a vector to store the results.

`for p in rx { out.push(p); }`: This loop receives messages from the channel. Each message represents an open port number, which is added to the `out` vector.

**Output Results:**
- `println!("");`: Prints a newline for better formatting before outputting the results.
- `out.sort();`: Sorts the vector of open ports.
- `for v in out { println!("{} is open", v); }`: Iterates through the sorted list of open ports and prints each one.

Overall, this function orchestrates the entire port scanning operation using asynchronous programming to handle potentially large numbers of ports efficiently and concurrently.

You can find the code [here](https://github.com/EleftheriaBatsou/port-sniffer-cli-rust/tree/main):

{% embed https://github.com/EleftheriaBatsou/port-sniffer-cli-rust 
 %}

### **Build and Run the Program**
Use `cargo run` to build and run your program.

Provide Command-Line Arguments

`cargo run -- --address 192.168.1.1 --start 1 --end 1000`

Replace `192.168.1.1` with the IP address you want to scan, and adjust the `--start` and `--end` arguments to specify the range of ports to scan.

### **Conclusion**

In this tutorial, you learned how to construct a simple IP sniffer using Rust. This project covered handling command-line arguments, performing network operations, and using asynchronous programming with Tokio. Such tools are not only useful for network diagnostics but also serve as great learning exercises for understanding the underlying principles of network communications and concurrent programming in Rust.

Happy Rust Coding! 🤞🦀

* * *

👋 Hello, I'm Eleftheria, **Community Manager,** developer, public speaker, and content creator.

🥰 If you liked this article, consider sharing it.

🔗 [**All links**](https://limey.io/batsouelef) | [**X**](https://twitter.com/BatsouElef) | [**LinkedIn**](https://www.linkedin.com/in/eleftheriabatsou/)