Building a Syn Scanner in Rust
Tags: rust, networking, synscan
Published: 2023-12-15 22:12:37

**Note:**
Port scanning on hosts that don't like it when people probe them. They could actually bring legal consequences for port scanning them. Scan at your own risk. That being said, I've never heard of anyone getting arrested for port scanning.
_i had to update some stuff bear with me_

**Thanks to chinoto for helping me.**

**Prereqs**: having Rust installed

**tldr** : [code](https://github.com/sigh-gone/syn-scan-practice)

I'll be as brief as I can, and I hope to do a good write-up.


We'll be making a syn scanner in Rust. I love Rust, its a great language and I've had a lot of fun programming in it. You might have issues understanding whats going on if you're a beginner (or maybe as a pro if you think my code is janky). This code doesn't handle potentially filtered ports.

Lets start at what a syn scan should do.
The TCP handshake is the initial packet is a Syn packet which is like a request to connect. From there if the port is open receiver sends back a Syn/Ack. Then the sender sends an Ack packet. From there the connection starts.

In the syn scan, we send a Syn packet, and if the port is open we receive a Syn/Ack as a response. We never send back the last Ack packet to confirm connect. Instead we then send back an Rst packet otherwise the receiver of our Syn packet will continually send Syn/Ack to a timeout. We don't want to waste bandwidth.

Now that sounds super simple, and on paper it is. However to implement it is a tad bit more complicated.

Let's start out by making the new project. In the directory you have Rust projects in run
```bash
cargo new <Project Name>
```
replace project name for whatever you want to name it. I named it "syn-scan-practice"

Now navigate to the project folder and open up the _Cargo.toml_ file and past the following dependencies in it:
```toml
pnet = "0.34.0"
rand = "0.8.5"
pnet_packet = "0.34.0"
```
As of 15/Dec/2023 these are the latest versions of the dependencies.

We are going to section this out in blocks, and I want it so we can do this with all the correct using statements right off the bat. So in _main.rs_ paste the following usings at the top:
```rust
use pnet::{
    datalink::{Channel, DataLinkReceiver, NetworkInterface},
    packet::{
        ethernet::{EtherTypes, EthernetPacket},
        ip::IpNextHeaderProtocols,
        ipv4::Ipv4Packet,
        ipv6::Ipv6Packet,
        tcp::{ipv4_checksum, ipv6_checksum, MutableTcpPacket, TcpFlags, TcpOption, TcpPacket},
        Packet,
    },
    transport::{self, TransportChannelType, TransportProtocol, TransportSender},
};
use rand::{thread_rng, Rng};
use std::{
    net::IpAddr,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    thread,
    time::{Duration, Instant},
};
```

We will use all of these, and its a good amount of stuff to perform what on paper is a simple task.

Now lets get the consts out of the way, in _main.rs_ copy paste the following into it under the using statements:
```rust
pub const IPV4_HEADER_LEN: usize = 20;
pub const IPV6_HEADER_LEN: usize = 40;
pub const ETHERNET_HEADER_LEN: usize = 14;
```
The length of the IPv4 header is 20, the IPv6 is double that. This gets added to the Ethernet header length which is 14. This all goes into the raw packet slice for building the packet.

Now lets set up the config struct, in _main.rs_ copypaste this into it under the usings:
```rust
pub struct Config {
    interface_ip: IpAddr,
    source_port: u16,
    destination_ip: IpAddr,
    ports_to_scan: Vec<u16>,
    wait_after_send: Duration,
    timeout: Duration,
    all_sent: Arc<AtomicBool>,
}
```
The config will be mostly reused variables in a struct, this will later go into an [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html) so we can pass it between threads. The meaning behind this is to potentially incorporate it in a larger project.

next, get the number generator up, this will be used for ephemeral the ephemeral port.
```rust
fn generate_random_port(min: u32, max: u32) -> u32 {
    let mut rng = rand::thread_rng();
    rng.gen_range(min..max)
}
```

Next we will want to implement _new_ method for config, so in _main.rs_ copypaste the following code:
```rust
impl Config {
    pub fn new(
        destination_ip: IpAddr,
        ports_to_scan: Vec<u16>,
        interface_ip: IpAddr,
        timeout: u64,
    ) -> Self {
        //get ephemeral port to use
        let source_port: u16 = generate_random_port(10024, 65535) as u16;
        Self {
            interface_ip,
            source_port,
            destination_ip,
            //set the kill flag, giving every syn packet a 500 millisecond roundtrip
            wait_after_send: Duration::from_millis(500 * ports_to_scan.len() as u64),
            ports_to_scan,
            //timeout just in case we need it.
            timeout: Duration::from_secs(timeout),
            //kill flag
            all_sent: Arc::new(AtomicBool::new(false)),
        }
    }
}
``` 
This is taking the destination IP and the ports to scan, and timeout just in case something happens receiving (something funky).

This is  also setting up a source port to receive packets on a port that is [Ephemeral](https://unix.stackexchange.com/questions/65475/ephemeral-port-what-is-it-and-what-does-it-do). This also encompasses the "timeout" or the _wait_afte_send_ (_since it really does sleep the thread after send, but acts like a timeout_). 

Lastly, it has the "kill" flag, which is really the "all packets are sent" bool.

now lets set up the ability to get an interface:
```rust
//get the interface to establish a datalink channel
fn get_interface(interface_name: Option<String>) -> (NetworkInterface, IpAddr) {
    let interfaces = pnet::datalink::interfaces();

    //checks if interface exists, and if not it lists out available interfaces
    let Some(interface_name) = interface_name else {
        println!("Interface names available:");
        { interfaces.iter() }.for_each(|iface| println!("{}", iface.name));
        std::process::exit(1);
    };

    let interfaces_name_match = |interface: &NetworkInterface| interface.name == interface_name;

    if let Some(interface) = interfaces.into_iter().find(interfaces_name_match) {
        match interface.ips.first() {
            Some(ip_network) => {
                let interface_ip = ip_network.ip();
                (interface, interface_ip)
            }
            None => {
                panic!(
                    "cant get ip for interface \n {} \n interface: \n {}",
                    interface_name, interface,
                );
            }
        }
    } else {
        panic!("no interface named {}", interface_name);
    }
}
```
This will later be used and the arg to it is a command line arg at the start. This will also return the ip of that interface that will be used later.

Now lets set up the ability to get a socket to send the Rst and Syn packets:
```rust
fn get_socket(destination: IpAddr) -> Result<TransportSender, String> {
    match destination {
        IpAddr::V4(_) => {
            match transport::transport_channel(
                4096,
                TransportChannelType::Layer4(TransportProtocol::Ipv4(IpNextHeaderProtocols::Tcp)),
            ) {
                Ok((ts, _)) => Ok(ts),
                Err(e) => Err(format!("{}", e)),
            }
        }
        IpAddr::V6(_) => {
            match transport::transport_channel(
                4096,
                TransportChannelType::Layer4(TransportProtocol::Ipv6(IpNextHeaderProtocols::Tcp)),
            ) {
                Ok((ts, _)) => Ok(ts),
                Err(e) => Err(format!("{}", e)),
            }
        }
    }
}
```
This takes in the destination Ip to return a socket (_or a TransportSender_). You can play around with buffer sizes if you so like.

Now lets build out the _build_packet_ function.
```rust
//build rst or syn packet
fn build_packet(
    tcp_packet: &mut MutableTcpPacket,
    source_ip: IpAddr,
    dest_ip: IpAddr,
    source_port: u16,
    dest_port: u16,
    syn: bool,
) {
    tcp_packet.set_source(source_port); //ephemeral port
    tcp_packet.set_destination(dest_port); //port being probed
    tcp_packet.set_sequence(0);
    tcp_packet.set_window(64240);
    tcp_packet.set_data_offset(8);

    //if syn, set syn flag, if rst set rst flag
    if syn {
        tcp_packet.set_flags(TcpFlags::SYN);
        tcp_packet.set_options(&[
            TcpOption::mss(1460),
            TcpOption::sack_perm(),
            TcpOption::nop(),
            TcpOption::nop(),
            TcpOption::wscale(7),
        ]);
        tcp_packet.set_urgent_ptr(0);
    } else {
        tcp_packet.set_flags(TcpFlags::RST);
    }

    let checksum = match (source_ip, dest_ip) {
        (IpAddr::V4(src), IpAddr::V4(dest)) => {
            ipv4_checksum(&tcp_packet.to_immutable(), &src, &dest)
        }
        (IpAddr::V6(src), IpAddr::V6(dest)) => {
            ipv6_checksum(&tcp_packet.to_immutable(), &src, &dest)
        }
        _ => panic!("cant create socket in get_socket"),
    };
    tcp_packet.set_checksum(checksum);
}
```
You are free to read up on the Tcp field values, the ones on this are tested and work, at least for me. If you have questions as to what they do, I suggest looking over this [TCP header explanation](https://www.inetdaemon.com/tutorials/internet/tcp/tcp_header.shtml).

now for the _get_buffer_ function
```rust
fn get_buffer(config: &Config) -> Vec<u8> {
    let header_length = match config.destination_ip {
        IpAddr::V4(_) => IPV4_HEADER_LEN,
        IpAddr::V6(_) => IPV6_HEADER_LEN,
    };
    let vec: Vec<u8> = vec![0; ETHERNET_HEADER_LEN + header_length + 86];
    vec
}
```
this is to reduce redundancy, it builds out a buffer in case of a IPv4 packet or an IPv6 packet


Now lets build out the functionality to handle the tcp packets:
```rust
fn handle_tcp<'a>(
    ip_payload: &[u8],
    config: &Config,
    ip_addr: IpAddr,
    buffer: &'a mut [u8],
) -> Result<Option<MutableTcpPacket<'a>>, String> {
    let tcp_packet =
        TcpPacket::new(ip_payload).ok_or_else(|| "Failed to create TCP packet".to_string())?;

    let mut rst_packet = MutableTcpPacket::new(buffer)
        .ok_or_else(|| "Failed to create mutable TCP packet".to_string())?;

    if tcp_packet.get_destination() != config.source_port {
        return Ok(None);
    }

    if tcp_packet.get_flags() == TcpFlags::SYN | TcpFlags::ACK {
        println!("port {} open on host {}", tcp_packet.get_source(), ip_addr);
        build_packet(
            &mut rst_packet,
            config.interface_ip,
            config.destination_ip,
            config.source_port,
            tcp_packet.get_source(),
            false,
        );
        Ok(Some(rst_packet))
    } else if tcp_packet.get_flags() == TcpFlags::RST {
        Err(format!(
            "misc flag {} on port {}",
            tcp_packet.get_flags(),
            tcp_packet.get_source()
        ))
    } else {
        Ok(None)
    }
}
```
This incorporates a lifetime tied to the input buffer as the output packet contains a reference to the input buffer

Now lets build out the _send_packets_ function:
```rust
fn send_packets(config: &Config, mut sender: TransportSender) {
    for destination_port in config.ports_to_scan.iter() {
        //get buffer to send
        let mut vec: Vec<u8> = get_buffer(config);
        //create packet
        let mut tcp_packet =
            MutableTcpPacket::new(&mut vec[..]).expect("Failed to create mutable TCP packet");
        //build packet
        build_packet(
            &mut tcp_packet,
            config.interface_ip,
            config.destination_ip,
            config.source_port,
            *destination_port,
            true,
        );

        //send packet
        if let Err(e) = sender.send_to(tcp_packet.to_immutable(), config.destination_ip) {
            eprintln!("Error sending packet: {}", e);
        } else {
            println!(
                "sent syn packet to port {} on host {}",
                destination_port, config.destination_ip
            );
        }
    }
    //sleep to change the all_sent flag
    thread::sleep(config.wait_after_send);
    //mark to true to kill receive_packets loop
    config.all_sent.store(true, Ordering::SeqCst);
}
``` 
This takes the config struct and a transport sender to send the packets. It builds a syn packet and sends it using the socket or the TransportSender. Look over the code and I will do my best to comment the source code on github to explain things that may not be apparent. Feel free to leave a comment if you don't understand whats going on in it.

Now the tough part, _receive_packets_ function.
```rust
fn receive_packets(
    config: &Config,
    mut sender: TransportSender,
    mut rx: Box<dyn DataLinkReceiver>,
) {
    let start = Instant::now();
    //loops over received packets, may not be all our packets
    loop {
        //we got a packet
        match rx.next() {
            //we got a packet
            Ok(packet) => {
                //Build from bottom up the stack, starting with ethernet
                let eth_packet = EthernetPacket::new(packet).unwrap();
                //get the type of packet riding on ethernet packet
                match eth_packet.get_ethertype() {
                    //type of IPv4, we want this
                    EtherTypes::Ipv4 => {
                        let ipv4_packet = Ipv4Packet::new(eth_packet.payload()).unwrap();
                        //if next layer up isn't tcp, we don't care
                        if ipv4_packet.get_next_level_protocol() == IpNextHeaderProtocols::Tcp {
                            //get buffer to have a lifetime outside the function, done for ownership reasons
                            let mut buffer = get_buffer(config);
                            //develop rst packet to send
                            let rst_packet = handle_tcp(
                                ipv4_packet.payload(),
                                config,
                                ipv4_packet.get_source().into(),
                                &mut buffer,
                            );
                            //rst_packet is a Result<Option> so this unwraps it all
                            match rst_packet {
                                Ok(rst_packet) => {
                                    if let Some(rst_packet) = rst_packet {
                                        let _ = sender.send_to(rst_packet, config.destination_ip);
                                    }
                                }
                                Err(e) => {
                                    //prints too much
                                    println!("error {:?}", e)
                                }
                            }
                        }
                    }
                    //type of IPv6, we want this
                    EtherTypes::Ipv6 => {
                        let ipv6_packet = Ipv6Packet::new(eth_packet.payload()).unwrap();

                        //if next layer up isn't tcp, we don't care
                        if ipv6_packet.get_next_header() == IpNextHeaderProtocols::Tcp {
                            //get buffer to have a lifetime outside the function, done for ownership reasons
                            let mut buffer = get_buffer(config);
                            let rst_packet = handle_tcp(
                                ipv6_packet.payload(),
                                config,
                                ipv6_packet.get_source().into(),
                                &mut buffer,
                            );
                            match rst_packet {
                                Ok(rst_packet) => {
                                    if let Some(rst_packet) = rst_packet {
                                        println!("{:?}", rst_packet.get_source());
                                        println!("{:?}", config.destination_ip);
                                        let _ = sender.send_to(rst_packet, config.destination_ip);
                                    }
                                }
                                Err(e) => {
                                    //oops
                                    println!("error: {:?}", e);
                                }
                            }
                        }
                    }
                    //we dont care, do nothing so we can check breaking condition
                    _ => {}
                }
            }
            //print out if there is an error receiving a packet
            Err(e) => {
                println!("error while receiving packet: {:?}", e)
            }
        }
        //check if all_sent flag is true
        if config.all_sent.load(Ordering::SeqCst)
            //check if timeout is hit
            || Instant::now().duration_since(start) > config.timeout
        {
            break;
        }
    }
}
```
I commented a lot on the code to clarify some things, let me know if it is unclear.

Now set up the _main_
```rust
 fn main() {
    //setting up values for new config and an interface to send into receive_packets
    let interface_name = std::env::args().nth(1);
    //set up var to use, make sure its in scope
    let destination_ip: IpAddr;

    if let Some(ip) = std::env::args().nth(2) {
        match ip.parse::<IpAddr>() {
            Ok(ip) => destination_ip = ip,
            Err(err) => {
                panic!("error parsing ip address {}\n error: \n {}", ip, err);
            }
        }
    } else {
        panic!("no ipv4 or ipv6 address provided")
    }

    //change to desired ports
    let ports_to_scan: Vec<u16> = vec![443];

    //get interface and the interface_ip
    let (interface, interface_ip): (NetworkInterface, IpAddr) = get_interface(interface_name);

    //timeout value if op isnt working correctly, going to be in secs.
    let timeout: u64 = 10;

    //config
    let config: Config = Config::new(destination_ip, ports_to_scan, interface_ip, timeout);

    //create the arcs to send among other threads
    let config_arc = Arc::new(config);
    let config_arc_clone = config_arc.clone();

    let rx_builder = thread::Builder::new().name("receive_thread".to_string());
    let tx_builder = thread::Builder::new().name("send_thread".to_string());

    //set up senders for sender syn packets (sender) and rst packets to close the connection (receiver)
    let syn_sender: TransportSender =
        get_socket(destination_ip).expect("Failed to create tx_sender");
    let rst_sender: TransportSender =
        get_socket(destination_ip).expect("Failed to create rx_sender");

    //get interface to receive syn packet responses
    let rx = match pnet::datalink::channel(&interface, Default::default()) {
        Ok(Channel::Ethernet(_, rx)) => rx,
        Ok(_) => panic!("Unknown channel type"),
        Err(e) => panic!("Error happened {}", e),
    };

    //receiving packet thread
    let rx_thread = rx_builder.spawn(move || {
        receive_packets(&config_arc, rst_sender, rx);
    });

    //sending packet thread
    let tx_thread = tx_builder.spawn(move || {
        send_packets(&config_arc_clone, syn_sender);
    });

    //joining the handles
    let _ = rx_thread.expect("receive failed").join();
    let _ = tx_thread.expect("send failed").join();
}
```
Again, I've attempted to comment it out to make it more clear. Some things you can change are the _destination_ip_ or the _ports_to_scan_.

So now do a cargo check to make sure if it is all there.
And if that's all okay, run it
(_for linux/mac_)
```bash
sudo cargo run <Interface Name> <IP>
```
Replace <Interface Name> with the name of the interface you want to use, same with IP. 
If you run
(_for linux/mac_)
```bash
sudo cargo run
```
you will get a list of interfaces available

**If your on windows**, I think you can run
```bash
cargo build
```
and right click the binary and "run as administrator" it. If that doesnt work let me know, I'm not a windows user so I can't trouble shoot issues on it at the moment.

Let me know if you have issues, I ran through the tutorial quick and I came up with no issues. But I can miss stuff.

This is how to build a syn scanner in Rust, if you have any ideas, link up on the comments or github.

Some ideas for future are some things like figuring out how to reuse the buffer, adding the ability to input ip from command line (can take v4 and v6), take in custom port ranges || subnets || single ports, etc.

Thanks