Linux Network Namespaces in Rust

Linux Network Namespaces are a feature within the Linux kernel that allows for the isolation and virtualization of network resources. This…

Linux Network Namespaces in Rust

Linux Network Namespaces are a feature within the Linux kernel that allows for the isolation and virtualization of network resources. This means you can have multiple instances of network interfaces, IP addresses, routing tables, and firewall rules within the same physical machine but operating independently as if they were on separate machines.

Think of Network Namespaces like creating separate, distinct rooms in a house, where each room has its own set of networking utilities (like its own Wi-Fi, its own phone line, etc.), completely unaware and unaffected by what’s happening in the other rooms. This allows for processes and applications to run in isolated network environments, making it incredibly useful for testing, development, security, and containerization purposes.

Each Network Namespace functions like a standalone network environment, with its own:

  • Network interfaces (both virtual and physical)
  • Routing tables that define how to route packets within and outside the namespace
  • Firewall rules
  • Network sockets and port numbers

This isolation ensures that actions or configurations within one namespace do not impact the network settings or traffic in another namespace or the default network namespace of the host system.

Working with Network Namespace in Rust

Let’s dive into Linux network namespaces, bridges, and veth pairs. Think of this as a hands-on guide to setting up isolated network environments on your Linux machine using Rust.

First up, we’ve got some Rust standard library imports. These are your tools for running commands, working with IP addresses, pausing with sleep, and timing things just right:

use std::process::{Command, Output}; 
use std::net::Ipv4Addr; 
use std::thread; 
use std::time::Duration;

Then, we bring in some special guests: rtnetlink for the nitty-gritty network interface stuff, and futures to keep things smooth and asynchronous:

use rtnetlink::{new_connection}; 
use futures::stream::TryStreamExt;

Step 1: Creating a Bridge

Establish a New Netlink Connection: The function starts by calling new_connection(), which establishes a new netlink connection for networking operations. This connection is essential for sending commands to and receiving responses from the kernel regarding network configurations.

let (connection, handle, _) = new_connection().map_err(|e| e.to_string())?;
  • connection: Represents the netlink socket connection.
  • handle: A handle to perform network operations.
  • The third value is ignored with _.

The connection is spawned as an asynchronous task using tokio::spawn(connection), ensuring that it runs concurrently with other tasks.

With the netlink handle, the function creates a new network bridge by specifying the bridge name. This is done using a fluent interface provided by rtnetlink, chaining methods to construct the command.

handle.link().add().bridge(bridge_name.to_string()).execute().await.map_err(|e| e.to_string())?;

This command effectively sends a request to the kernel to add a new link (in this case, a bridge) with the specified name.

Bring the Bridge Up: Once the bridge is created, the function needs to bring it “up” to make it operational. This is done by first retrieving the link (bridge) created earlier using a filter by the bridge name, and then setting its state to “up”.

let mut links = handle.link().get().set_name_filter(bridge_name.into()).execute(); 
 
if let Some(link) = links.try_next().await.map_err(|e| e.to_string())? { 
    handle.link().set(link.header.index).up().execute().await.map_err(|e| e.to_string())?; 
}

The try_next() method awaits the next link matching the filter, which should be the newly created bridge. The bridge is then brought up using handle.link().set().up().

Assign an IP Address to the Bridge: The final step is to assign an IP address to the bridge. This makes the bridge a functional network interface that can participate in IP networking. The function assigns a static IP address 192.168.1.1/24 to the bridge.

let addr = Ipv4Addr::new(192, 168, 1, 1); 
 
let prefix = 24; 
let mut links = handle.link().get().set_name_filter(bridge_name.into()).execute(); 
if let Some(link) = links.try_next().await.map_err(|e| e.to_string())? { 
    handle.address().add(link.header.index, addr.into(), prefix).execute().await.map_err(|e| e.to_string())?; 
}

Similar to bringing the bridge up, it finds the bridge link again and then uses the handle.address().add() method to set the IP address and subnet prefix.

By the end of this function, a new network bridge is created, activated, and assigned an IP address, ready for use in networking configurations such as connecting network namespaces or virtual machines.

Step 2. Create the Network Namespace

This part of the function involves using the ip netns add command to create a new network namespace with the specified name.

let output = Command::new("ip") 
    .args(&["netns", "add", name]) 
    .output() 
    .expect("Failed to execute command");

Step 3. Activate the Loopback Interface

After creating the namespace, the function activates the loopback interface within it using the ip link set lo up command executed within the context of the new namespace.

let output = Command::new("ip") 
    .args(&["netns", "exec", &name, "ip", "link", "set", "lo", "up"]) 
    .output() 
    .expect("Failed to execute command");

Step 4. Create a Virtual Ethernet (veth) Pair

This step involves creating a veth pair with one end in the host namespace and the other in the newly created namespace. This is achieved with the ip link add command, specifying the type as veth and naming the ends of the pair.

let veth_host = format!("veth-host-{}", name); 
let veth_ns = format!("veth-ns-{}", name); 
 
let output = Command::new("ip") 
    .args(&["link", "add", &veth_host, "type", "veth", "peer", &veth_ns]) 
    .output() 
    .expect("Failed to execute command");

Additional Steps

Move one end of the veth pair to the bridge

This snippet moves the host end of the veth pair to the bridge, effectively connecting the namespace to the bridge.

let output = Command::new("ip") 
    .args(&["link", "set", &veth_host, "master", "br0"]) 
    .output() 
    .expect("Failed to execute command");

Bring the bridge end up

This command activates the host end of the veth pair, ensuring it’s up and ready to transmit data.

let output = Command::new("ip") 
    .args(&["link", "set", &veth_host, "up"]) 
    .output() 
    .expect("Failed to execute command");

Move the other end of the veth pair to the namespace

This step moves the other end of the veth pair into the namespace, allowing communication between the host and the namespace.

let output = Command::new("ip") 
    .args(&["link", "set", &veth_ns, "netns", &name]) 
    .output() 
    .expect("Failed to execute command");

Enable the veth interface in the namespace

This command brings up the namespace end of the veth pair, completing the connection setup.

let output = Command::new("ip") 
    .args(&["netns", "exec", &name, "ip", "link", "set", "dev", &veth_ns, "up"]) 
    .output() 
    .expect("Failed to execute command");

Configure the veth interface in the namespace

Finally, this snippet assigns an IP address to the namespace end of the veth pair, allowing network communication.

let output = Command::new("ip") 
    .args(&["netns", "exec", &name, "ip", "addr", "add", "192.168.1.10/24", "dev", &veth_ns]) 
    .output() 
    .expect("Failed to execute command");

Each of these steps involves executing system commands using Rust’s Command struct from the std::process module, with error handling to ensure each step completes successfully.

Putting it all together

Here the final implementation:

use std::process::{Command, Output}; 
use std::net::Ipv4Addr; 
use std::thread; 
use std::time::Duration; 
use rtnetlink::{new_connection}; 
use futures::stream::TryStreamExt; 
use text_io::read; 
 
// Extension trait to convert command output to String easily 
trait OutputExt { 
    fn stdout_as_string(&self) -> String; 
} 
 
impl OutputExt for Output { 
    fn stdout_as_string(&self) -> String { 
        String::from_utf8_lossy(&self.stdout).to_string() 
    } 
} 
 
async fn create_network_ns(name: &str) -> Result<(), Box<dyn std::error::Error>> { 
 
    //Create a bridge in the host 
    let _ = create_bridge("br0").await; 
 
    //Create the network namespace 
    let output = Command::new("ip") 
        .args(&["netns", "add", name]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Network namespace {} created successfully.", name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error creating network namespace {}: {}", name, stderr); 
        return Err("Failed to create network namespace".into()); 
    } 
 
    //Bring the namespace up 
    let output = Command::new("ip") 
        .args(&["netns", "exec", &name, "ip", "link", "set", "lo", "up"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Namespace {} link is Up.", name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error creating network namespace {}: {}", name, stderr); 
        return Err("Failed to create network namespace".into()); 
    } 
 
    //Create a veth pair, with one end in the namespace and the other on the host 
    let veth_host = format!("veth-host-{}", name); 
    let veth_ns = format!("veth-ns-{}", name); 
 
    let output = Command::new("ip") 
        .args(&["link", "add", &veth_host, "type", "veth", "peer", &veth_ns]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Veth pair created: {} <--> {}", veth_host, veth_ns); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error creating veth pair: {}", stderr); 
        return Err("Failed to create veth pair".into()); 
    } 
 
    //Move one end of the veth pair to the bridge 
    let output = Command::new("ip") 
        .args(&["link", "set", &veth_host, "master", "br0"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Linked {} to the {} bridge.", veth_host, "br0"); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error moving veth to namespace {}: {}", name, stderr); 
        return Err("Failed to move veth to namespace".into()); 
    } 
 
    //Bring the bridge end up 
    let output = Command::new("ip") 
        .args(&["link", "set", &veth_host, "up"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Enabled bridge in {}.", veth_host); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error enabling veth in namespace {}: {}", name, stderr); 
        return Err("Failed to enable veth in namespace".into()); 
    } 
 
    // Move the other end of the veth pair to the namespace 
    let output = Command::new("ip") 
        .args(&["link", "set", &veth_ns, "netns", &name]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Moved {} to network namespace {}.", veth_ns, name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error moving veth to namespace {}: {}", name, stderr); 
        return Err("Failed to move veth to namespace".into()); 
    } 
 
    // Enable the veth interface in the namespace 
    let output = Command::new("ip") 
        .args(&["netns", "exec", &name, "ip", "link", "set", "dev", &veth_ns, "up"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Enabled {} in namespace {}.", veth_ns, name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error enabling veth in namespace {}: {}", name, stderr); 
        return Err("Failed to enable veth in namespace".into()); 
    } 
 
    // Configure the veth interface in the namespace 
    let output = Command::new("ip") 
        .args(&["netns", "exec", &name, "ip", "addr", "add", "192.168.1.10/24", "dev", &veth_ns]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Configured IP address for {} in namespace {}.", veth_ns, name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error configuring IP address in namespace {}: {}", name, stderr); 
        return Err("Failed to configure IP address in namespace".into()); 
    } 
 
 
    // Set default route inside the network namespace 
    Command::new("ip") 
        .args(&["netns", "exec", name, "ip", "route", "add", "default", "via", "192.168.1.1"]) 
        .output() 
        .map_err(|e| e.to_string())?; 
 
    if let Err(e) = test_connectivity(&name) { 
        eprintln!("Error testing connectivity from namespace to host: {}", e); 
        return Err("Failed to test connectivity from namespace to host".into()); 
    } 
 
    println!("Network namespace {} created and tested successfully.", name); 
 
    Ok(()) 
} 
 
fn test_connectivity(namespace_name: &str) -> Result<(), Box<dyn std::error::Error>> { 
    // Wait for a moment to allow network configurations to take effect 
    thread::sleep(Duration::from_secs(2)); 
 
    // Test network connectivity from the namespace to the host 
    let output = Command::new("ip") 
        .args(&["netns", "exec", namespace_name, "ping", "-c", "3", "192.168.1.2"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Network namespace {} can ping host (192.168.1.2).", namespace_name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error testing connectivity from namespace to host: {}", stderr); 
        return Err("Failed to test connectivity from namespace to host".into()); 
    } 
 
    // Test network connectivity from the host to the namespace 
    let output = Command::new("ping") 
        .args(&["-c", "3", "-I", "veth-host-ns1", "192.168.1.1"]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Host can ping network namespace {} (192.168.1.1).", namespace_name); 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error testing connectivity from host to namespace: {}", stderr); 
        return Err("Failed to test connectivity from host to namespace".into()); 
    } 
 
    Ok(()) 
} 
 
async fn create_bridge(bridge_name: &str) -> Result<(), String> { 
    let (connection, handle, _) = new_connection().map_err(|e| e.to_string())?; 
    tokio::spawn(connection); 
 
    // Create bridge 
    handle.link().add().bridge(bridge_name.to_string()).execute().await.map_err(|e| e.to_string())?; 
 
    // Bring bridge up 
    let mut links = handle.link().get().set_name_filter(bridge_name.into()).execute(); 
    if let Some(link) = links.try_next().await.map_err(|e| e.to_string())? { 
        handle.link().set(link.header.index).up().execute().await.map_err(|e| e.to_string())?; 
    } 
 
    // Assign IP address 
    let addr = Ipv4Addr::new(192, 168, 1, 1); 
    let prefix = 24; 
    let mut links = handle.link().get().set_name_filter(bridge_name.into()).execute(); 
    if let Some(link) = links.try_next().await.map_err(|e| e.to_string())? { 
        handle.address().add(link.header.index, addr.into(), prefix).execute().await.map_err(|e| e.to_string())?; 
    } 
 
    Ok(()) 
} 
 
#[derive(Debug)] 
struct Container { 
    namespace: String, 
} 
 
#[tokio::main] 
async fn main() { 
    let mut containers: Vec<Container> = Vec::new(); 
    let mut counter_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new(); 
 
    loop { 
        println!("Menu:"); 
        println!("1 - Create containers"); 
        println!("2 - Rollback containers (Not implemented yet)"); 
        println!("3 - List all containers"); 
        println!("4 - Exit"); 
 
        let choice: i32 = read!("{}\n"); 
 
        match choice { 
            1 => { 
                let namespace = format!("ns{}", get_next_container_number(&mut counter_map, "ns")); 
 
                if let Err(e) = create_network_ns(&namespace).await { 
                    eprintln!("Error creating network namespace: {}", e); 
                } 
 
                containers.push(Container { 
                    namespace: namespace.clone() 
                }); 
 
                println!("Container created: namespace={}", namespace); 
            } 
            2 => { 
                println!("Option 2 - Rollback containers"); 
                for container in containers.iter() { 
                    if let Err(e) = remove_network_ns(&container.namespace).await { 
                        eprintln!("Error removing network namespace: {}", e); 
                    } 
 
                    //Remove bridge 
                    if let Err(e) = remove_veth_interface(&container.namespace).await { 
                        eprintln!("Error removing bridge: {}", e); 
                    } 
 
                    //Remove bridge 
                    if let Err(e) = remove_bridge("br0").await { 
                        eprintln!("Error removing bridge: {}", e); 
                    } 
                } 
                containers.clear(); 
                println!("Containers rolled back."); 
            } 
            3 => { 
                println!("Option 3 - List all containers"); 
                for (index, container) in containers.iter().enumerate() { 
                    println!( 
                        "Container {}: namespace={}", 
                        index + 1, 
                        container.namespace 
                    ); 
                } 
            } 
            4 => { 
                println!("Option 4 - Exit"); 
                break; // Exit the loop to terminate the program 
            } 
            _ => { 
                println!("Invalid option. Please choose a valid option."); 
            } 
        } 
    } 
} 
 
fn get_next_container_number(counter_map: &mut std::collections::HashMap<String, u32>, prefix: &str) -> u32 { 
    let counter = counter_map.entry(prefix.to_string()).or_insert(0); 
    *counter += 1; 
    *counter 
} 
 
async fn remove_network_ns(name: &str) -> Result<(), Box<dyn std::error::Error>> { 
    let output = Command::new("ip") 
        .args(&["netns", "delete", name]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Network namespace {} removed successfully.", name); 
        Ok(()) 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error removing network namespace {}: {}", name, stderr); 
        Err("Failed to remove network namespace".into()) 
    } 
} 
 
async fn remove_bridge(name: &str) -> Result<(), Box<dyn std::error::Error>> { 
    let output = Command::new("ip") 
        .args(&["link", "del", name]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Bridge {} removed successfully.", name); 
        Ok(()) 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error removing bridge {}: {}", name, stderr); 
        Err("Failed to remove bridge".into()) 
    } 
} 
 
async fn remove_veth_interface(name: &str) -> Result<(), Box<dyn std::error::Error>> { 
 
    let veth_host = format!("veth-host-{}", name); 
 
    let output = Command::new("ip") 
        .args(&["link", "del", &veth_host]) 
        .output() 
        .expect("Failed to execute command"); 
 
    if output.status.success() { 
        println!("Veth interface {} removed successfully.", name); 
        Ok(()) 
    } else { 
        let stderr = String::from_utf8_lossy(&output.stderr); 
        eprintln!("Error removing veth interface {}: {}", name, stderr); 
        Err("Failed to remove veth interface".into()) 
    } 
}

Running the Project

Execute the Binary: Once the project is compiled, you can run the binary directly using Cargo by executing:

  • cargo run

Interact with the CLI: The project likely provides a Command Line Interface (CLI) for interaction. Follow the prompts or instructions output to the terminal to create network namespaces, bridges, and more. Typical commands might include creating containers, listing them, or exiting the program.

Testing Network Isolation and Connectivity

List Network Namespaces: After creating network namespaces using your Rust application, verify their creation by listing all available namespaces:

  • ip netns list

You should see the namespaces you’ve created listed.

Execute Commands Inside a Namespace: To run commands inside a specific network namespace, use ip netns exec. For example, to list the network interfaces in a namespace named ns1, run:

  • ip netns exec ns1 ip addr

Test Connectivity: To ensure that your network setup is functioning as intended, you can test connectivity from within a network namespace to the host machine.

  • ip netns exec ns1 ping -c 4 <your_local_host_ip>

🚀 Explore a Wealth of Resources in Software Development and More by Luis Soares

📚 Learning Hub: Expand your knowledge in various tech domains, including Rust, Software Development, Cloud Computing, Cyber Security, Blockchain, and Linux, through my extensive resource collection:

  • Hands-On Tutorials with GitHub Repos: Gain practical skills across different technologies with step-by-step tutorials, complemented by dedicated GitHub repositories. Access Tutorials
  • In-Depth Guides & Articles: Deep dive into core concepts of Rust, Software Development, Cloud Computing, and more, with detailed guides and articles filled with practical examples. Read More
  • E-Books Collection: Enhance your understanding of various tech fields with a series of free e-Books, including titles like “Mastering Rust Ownership” and “Application Security Guide” Download eBook
  • Project Showcases: Discover a range of fully functional projects across different domains, such as an API Gateway, Blockchain Network, Cyber Security Tools, Cloud Services, and more. View Projects
  • LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here

🔗 Connect with Me:

  • Medium: Read my articles on Medium and give claps if you find them helpful. It motivates me to keep writing and sharing Rust content. Follow on Medium
  • Personal Blog: Discover more on my personal blog, a hub for all my Rust-related content. Visit Blog
  • LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
  • Twitter: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter

Wanna talk? Leave a comment or drop me a message!

All the best,

Luis Soares
luis.soares@linux.com

Senior Software Engineer | Cloud Engineer | SRE | Tech Lead | Rust | Golang | Java | ML AI & Statistics | Web3 & Blockchain

Read more