Advice regarding increasing performance of network packet processing

Hi All,

I am new to Rust and am in the process of learning the language. I program mostly in Golang these days and was exploring Rust as an alternative for writing networking applications.

I have a packet processing program that transports packets over a QUIC connection from client to server where the server forwards it to the destination with appropriate NAT'ing applied. The program is functional though the performance of the Rust based program is lower than a comparative Go program and I was hoping this would be higher. I think I might be missing something in my implementation which may improve performance even though I have tried to use async based constructs.

Any advice on how to make this better would be greatly appreciated.

The relevant part of the code is copy-pasted below.

#[macro_use]
extern crate log;

use std::{net::SocketAddr, error::Error, fs::File, io::BufReader};
use std::sync::Arc;
use quinn::{Endpoint, ServerConfig, Connection};
use ctrlc;
use tokio::io::{WriteHalf, ReadHalf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::net::Ipv4Addr;
use std::path::Path;
use std::fs;
use libc;
use iptables;
use pnet::packet::{ipv4::{MutableIpv4Packet, checksum}, ip::IpNextHeaderProtocols, tcp::{MutableTcpPacket}, tcp, udp, Packet, udp::MutableUdpPacket};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();
    debug!("Starting server!");

    // start server loop
    server().await?;
    Ok(())
}

fn server_addr() -> SocketAddr {
    "0.0.0.0:443".parse::<SocketAddr>().unwrap()
}

async fn server() -> Result<(), Box<dyn Error>> {
    const ALPN_QUIC_TUNNEL: &[&[u8]] = &[b"quic-tunnel"];
    let (certs, key) = read_certs_from_file().unwrap();
    let mut server_crypto = rustls::ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(certs, key)?;
    server_crypto.alpn_protocols = ALPN_QUIC_TUNNEL.iter().map(|&x| x.into()).collect();

    let server_config = ServerConfig::with_crypto(Arc::new(server_crypto));
    let endpoint = Endpoint::server(server_config, server_addr())?;

    // Start iterating over incoming connections.
    while let Some(conn) = endpoint.accept().await {
        let connection = conn.await?;
        debug!("Received connection!");

        let tunnel_subnet = String::from("172.0.0.10");
        let tunnel_ip = tunnel_subnet.parse::<Ipv4Addr>().unwrap();
        let iface: tokio_tun::Tun = tokio_tun::TunBuilder::new()
            .name("tun0")
            .tap(false)
            .packet_info(false)
            .mtu(1100)
            .up()
            .address(tunnel_ip)
            .netmask(Ipv4Addr::new(255, 255, 255, 254))
            .try_build().unwrap();

        let (reader, writer) = tokio::io::split(iface);
        let connection2 = connection.clone();

        let receive_fut = receive_datagram(connection, writer);
        tokio::spawn(async move {
            receive_fut.await;                
        });
        
        let send_fut = send_datagram(connection2, reader);
        tokio::spawn(async move {
            send_fut.await;
        });
    }

    Ok(())
}

async fn receive_datagram(connection: Connection, mut tun_writer: WriteHalf<tokio_tun::Tun>) {
    let srcip = "172.0.0.11".parse::<Ipv4Addr>().unwrap();
    while let Ok(received_bytes) = connection.read_datagram().await {
        let mut data = received_bytes.to_vec();

        // decode packet and replace src ip
        let mut ipv4 = MutableIpv4Packet::new(&mut data).unwrap();
        ipv4.set_source(srcip);

        match ipv4.get_next_level_protocol() {
            IpNextHeaderProtocols::Tcp => {
                let destip = ipv4.get_destination();
                let mut tcp = MutableTcpPacket::owned(ipv4.payload().to_owned()).unwrap();
                tcp.set_checksum(tcp::ipv4_checksum(&tcp.to_immutable(), &srcip, &destip));
                ipv4.set_payload(tcp.packet());
            }
            IpNextHeaderProtocols::Udp => {
                let destip = ipv4.get_destination();
                let mut udp = MutableUdpPacket::owned(ipv4.payload().to_owned()).unwrap();
                udp.set_checksum(udp::ipv4_checksum(&udp.to_immutable(), &srcip, &destip));
                ipv4.set_payload(udp.packet());
            },
            IpNextHeaderProtocols::Icmp => (),
            _ => {
                debug!("Unknown packet type!");
                continue;
            },
        }

        ipv4.set_checksum(checksum(&ipv4.to_immutable()));
        let _num_written = tun_writer.write(&ipv4.packet()).await;
    }
}

async fn send_datagram(connection: Connection, mut tun_reader: ReadHalf<tokio_tun::Tun>) {
    let destip = "10.0.0.2".parse::<Ipv4Addr>().unwrap();
    let mut buffer = vec![0; 1100];
    loop {
        let num_read = tun_reader.read(&mut buffer).await.unwrap();
        let mut data = buffer[..num_read].to_owned();

        // decode packet and replace src ip
        let mut ipv4 = MutableIpv4Packet::new(&mut data).unwrap();
        ipv4.set_destination(destip);

        match ipv4.get_next_level_protocol() {
            IpNextHeaderProtocols::Tcp => {
                let srcip = ipv4.get_source();
                let mut tcp = MutableTcpPacket::owned(ipv4.payload().to_owned()).unwrap();
                tcp.set_checksum(tcp::ipv4_checksum(&tcp.to_immutable(), &srcip, &destip));
                ipv4.set_payload(tcp.packet());
            }
            IpNextHeaderProtocols::Udp => {
                let srcip = ipv4.get_source();
                let mut udp = MutableUdpPacket::owned(ipv4.payload().to_owned()).unwrap();
                udp.set_checksum(udp::ipv4_checksum(&udp.to_immutable(), &srcip, &destip));
                ipv4.set_payload(udp.packet());
            },
            IpNextHeaderProtocols::Icmp => (),
            _ => {
                debug!("Unknown packet type!");
                continue
            },
        }

        ipv4.set_checksum(checksum(&ipv4.to_immutable()));
        let out_data = ipv4.packet().to_owned();

        if let Err(e) = connection.send_datagram(out_data.into()) {
            error!("Error sending datagram back to client: {}", e.to_string());
        }
    }
}

Please read the code formatting guidelines, it's much harder to read code that hasn't been formatted properly

Are you building your Rust program in release mode when you compare performance?

1 Like

Thanks for your response. I edited the post to add the code formatting backticks. Hopefully it is more readable now.

I did compile in release mode as well to measure. The throughput as measured by iperf3 was lesser than the equivalent Go program. The Rust program did use less CPU and memory so that was encouraging which is why I am hopeful that I can get this to run faster as well. Perhaps it might be difference in the libraries being used for QUIC connection, tunnel devices and packet parsing (although these seem like well used implementations in the community).

Try replace most of your to_owned() call with a borrow instead. For example

- let mut data = buffer[..num_read].to_owned();
+ let data = &mut buffer[..num_read]; 

// ...

- let mut tcp = MutableTcpPacket::owned(ipv4.payload().to_owned()).unwrap();
+ let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).unwrap();
tcp.set_checksum(tcp::ipv4_checksum(&tcp.to_immutable(), &srcip, &destip));
- ipv4.set_payload(tcp.packet());

3 Likes

Thank you for the suggestions. I am still figuring out how to use borrowing correctly and your advice helped. The performance has increased a bit after the changes but alas not enough. I will keep plugging at this at my end. Thanks again for looking through the code and the helpful suggestions!

Out of curiosity, can you post the benchmark result of various implementations (Rust/Go)? What's the benchmark method? (I'm not familiar with iperf3 so maybe you need to explain a bit.)

The both the servers are running in Linux containers and so is the iperf server. Client is on macOS.

Rust code

iperf3 -i 0 -c <iperf-server> -t 10 -V
iperf 3.13
Darwin 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:42:11 PST 2023; root:xnu-8792.81.3~2/RELEASE_X86_64 x86_64
Control connection MSS 1048
Time: Fri, 24 Mar 2023 08:12:53 UTC
Connecting to host 192.168.x.x, port 5201
      Cookie: ka3pv3lrbjdot6knv2p7ev3aioe5fhilsyuy
      TCP MSS: 1048 (default)
[  5] local 192.168.5.2 port 59226 connected to 192.168.8.187 port 5201
Starting Test: protocol: TCP, 1 streams, 131072 byte blocks, omitting 0 seconds, 10 second test, tos 0
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  39.9 MBytes  33.5 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  39.9 MBytes  33.5 Mbits/sec                  sender
[  5]   0.00-10.03  sec  39.8 MBytes  33.3 Mbits/sec                  receiver
CPU Utilization: local/sender 3.4% (0.8%u/2.6%s), remote/receiver 0.3% (0.1%u/0.2%s)
rcv_tcp_congestion cubic

iperf Done.

Go code

iperf3 -i 0 -c <iperf-server> -t 10 -V
iperf 3.13
Darwin  22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:42:11 PST 2023; root:xnu-8792.81.3~2/RELEASE_X86_64 x86_64
Control connection MSS 1048
Time: Fri, 24 Mar 2023 08:08:07 UTC
Connecting to host 192.168.x.x, port 5201
      Cookie: of74k2jsc5ongtleg4eoqjrry2mhlyl263b6
      TCP MSS: 1048 (default)
[  5] local 192.168.5.2 port 59164 connected to 192.168.8.187 port 5201
Starting Test: protocol: TCP, 1 streams, 131072 byte blocks, omitting 0 seconds, 10 second test, tos 0
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  52.2 MBytes  43.8 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
Test Complete. Summary Results:
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  52.2 MBytes  43.8 Mbits/sec                  sender
[  5]   0.00-10.03  sec  52.0 MBytes  43.5 Mbits/sec                  receiver
CPU Utilization: local/sender 3.7% (0.7%u/3.0%s), remote/receiver 5.8% (1.2%u/4.6%s)
rcv_tcp_congestion cubic

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.