Skip to content

Conversation

@thedevbirb
Copy link
Contributor

@thedevbirb thedevbirb commented Jan 4, 2026

Supersedes #72.

This PR is a complete re-work of the msg-sim crate. First, MacOS support has been dropped. This allows to focus more on the networking stack we're the most interested in and maximise the feature completeness of the crate itself. Then, compared #72, this approach doesn't rely on wrappers over the tc binary but creates appropriate rtnetlink requests to manipulate the networking stack of the host. Moreover, the library offers an API to create a network of veth-linked devices where impairments can be added to individual links. Each "peer" in the network has a dedicated network device which lives in a completely isolated network namespace, so we can guarantee no interferences with the host environment.

The network follows a central hub topology, where there is a namespace with a single bridge/switch where all peers veth devices attach to. This is the simplest design to allow discovery and network impairments between any two peers.

The Network abstraction is flexible enough to allow running arbitrary code tasks in the network namespace of the selected peer, without the need to create additional processes, runtimes etc each time.
Here is an example of how it looks like in action, with a 1s latency impairment (from a test):

    #[tokio::test(flavor = "multi_thread")]
    async fn simulate_reqrep_netem_delay_works() {
        let _ = tracing_subscriber::fmt::try_init();

        let subnet = Subnet::new(Ipv4Addr::new(14, 0, 0, 0).into(), 16);
        let mut network = Network::new(subnet).await.unwrap();

        let peer_1 = network.add_peer().await.unwrap();
        let peer_2 = network.add_peer().await.unwrap();

        // 1s latency.
        let sec_in_us = 1_000_000;
        let impairment = LinkImpairment { latency: sec_in_us, ..Default::default() };
        network.apply_impairment(Link::new(peer_1, peer_2), impairment).await.unwrap();

        let address_2 = peer_2.veth_address(subnet);
        let port_2 = 12345;

        let task1 = network
            .run_in_namespace(peer_2, move |_ctx| {
                Box::pin(async move {
                    let mut rep_socket = RepSocket::new(Tcp::default());
                    rep_socket.bind(SocketAddr::new(address_2, port_2)).await.unwrap();

                    // Given the delay in peer1-peer2 link, this should hit timeout
                    tokio::time::timeout(Duration::from_micros((sec_in_us / 2).into()), async {
                        if let Some(request) = rep_socket.next().await {
                            let msg = request.msg().clone();
                            request.respond(msg).unwrap();
                        }
                    })
                    .await
                    .unwrap_err();

                    if let Some(request) = rep_socket.next().await {
                        let msg = request.msg().clone();
                        request.respond(msg).unwrap();
                    }
                })
            })
            .await
            .unwrap();

        let task2 = network
            .run_in_namespace(peer_1, move |_ctx| {
                Box::pin(async move {
                    let mut req_socket = ReqSocket::new(Tcp::default());

                    req_socket.connect_sync(SocketAddr::new(address_2, port_2));
                    req_socket.request("hello".into()).await.unwrap();
                })
            })
            .await
            .unwrap();

        tokio::try_join!(task1, task2).unwrap();
    }

As of now, the impairments supported are the one provided by netem(8). That includes latency, limit, loss, gap, duplicate and jitter.

@thedevbirb thedevbirb mentioned this pull request Jan 4, 2026
6 tasks
sudo ifconfig lo0 mtu 16384
# Remove the dummynet pipes
sudo dnctl pipe delete 1
sudo HOME=$HOME $(which cargo) test # add your arguments here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super liking this - isn't there a better way? Maybe asking capabilities from the kernel? Or elevate priviledges inside the binary

Copy link
Contributor Author

@thedevbirb thedevbirb Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear that. This has been a forever pain point since I started using namespaces. Using plain sudo is just bad and I know. But you need the capabilities to fiddle with the networking stack, like CAP_NET_ADMIN (and something more). So an approach I tried briefly is to first compile the test binaries (--no-run flag), identify them, grant privileges with sudo, and then run them.

Still not incredible DX, so I'm postponing the problem for now until I think of something better.

@thedevbirb thedevbirb force-pushed the lore/feat/msg-sim branch 2 times, most recently from db13e1c to 36632e9 Compare January 9, 2026 14:15
@thedevbirb thedevbirb marked this pull request as ready for review January 9, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants