How to specify source IP address and port for a TCPStream?

From what I recall about C, one can use the sock_addr_in type to specify a source IP address and non-ephemeral source port number for a TCP connection. This enables a more fine-grained control over source IP and port number determination. I'm looking for a way to do the same thing in Rust.

I looked at the std::netTcpStream, tokio::net::TcpStream, and async_std::net::TcpStream APIs and could not find a way to replicate the same behavior in Rust through these APIs. It looks like they rely on the standard way of using ephemeral port numbers.

Is there an API available at a lower level that lets me achieve the intended behavior? I would ideally like an async API as I'm relying on the async-std library, but I'll take what I can get!

Somehow came across the crate socket2, which appears to be supported by the Rust language developers on this Github repo. I'm not sure why this isn't a part of the standard library, but oh well.

It addresses my requirements, and the resulting custom sockets can also be wrapped into async TcpStreams if necessary.

For posterity, I'm also including example code below for creating sockets with custom addresses, wrapping them into async counterparts, and consolidating connections and a listener under one address.

main.rs
use async_std::net::{TcpListener, TcpStream};
use async_std::stream::StreamExt;
use socket2::{Domain, Protocol, Socket, Type};
use std::net::{Ipv4Addr, SocketAddrV4};

const CUSTOM_PORT: u16 = 0; // use 0 if you want the system to generate an ephemeral port number

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    // Start a typical async TcpListener at port 3456
    // Verifies that a listener sees the custom address as the peer address
    let server_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3456);
    let server1 = TcpListener::bind(&server_addr).await?;
    let _t1 = async_std::task::spawn(async move {
        println!("Server1 bound to {}", server1.local_addr().unwrap());
        let mut incoming = server1.incoming();

        while let Some(Ok(conn)) = incoming.next().await {
            println!("Server1 received connection from {}", conn.peer_addr().unwrap())
        }
    });

    // Setup custom socket
    let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp()))?;
    socket
        .set_nonblocking(true)?;
    socket
        .set_reuse_address(true)?;
    socket
        .set_reuse_port(true)?; // must enable `reuseport` feature flag on crate

    // Bind socket to a custom IP address and port
    let sock_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), CUSTOM_PORT);
    socket
        .bind(&sock_addr.into())?;

    // Get custom address
    // Cannot use `sock_addr` again if port is an ephemeral number
    let custom_addr = socket.local_addr()?.as_std().unwrap();

    // Bind a traditional async TcpListener to the same socket address
    // Check to see if you can see still listen on an already used address
    let server2 = TcpListener::bind(&custom_addr).await?;
    let t2 = async_std::task::spawn(async move {
        println!("Server2 bound to {}", server2.local_addr().unwrap());
        let mut incoming = server2.incoming();

        if let Some(Ok(conn)) = incoming.next().await {
            println!("Server2 received connection from {}", conn.peer_addr().unwrap())
        }
    });

    // Establish a TCP connection to `server1`
    // Requires a timeout due to nonblocking being enabled
    socket
        .connect_timeout(&server_addr.into(), std::time::Duration::from_secs(1))?;

    // Can build a traditional async TcpStream from custom socket
    let _stream1 = TcpStream::from(socket.into_tcp_stream());

    // Verify that the listener on shared custom socket address is able to handle connections properly
    // Will use traditional random ephemeral port
    let _stream2 = TcpStream::connect(custom_addr).await?;

    t2.await;
    Ok(())
}
Cargo.toml dependencies
[dependencies]

anyhow = "1.0.38"

async-std = { version = "1.9.0", features = ["attributes"] }

socket2 = { version = "0.3.19", features = ["reuseport"] }
1 Like

With Tokio, I believe you could go through the TcpSocket type, which provides e.g. a bind method.

1 Like

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.