An Open Source Rust SNMP Simulator
Tags: rust, opensource
Published: 2023-01-10 09:35:01

In much of the work we carry out for communication service providers (<a href="https://en.wikipedia.org/wiki/Telephone_company" target="blank">CSPs</a>), either as part of <a href="https://sonalake.com/telecom-solutions/visimetrix" target="blank">VisiMetrix</a> integrations or when developing custom software products, we need to integrate with IP networking devices that power the packet core, circuit-switched core, backhaul, transmission, <a href="https://en.wikipedia.org/wiki/Radio_access_network" target="blank">radio access</a>, and <a href="https://en.wikipedia.org/wiki/IP_Multimedia_Subsystem" target="blank">IMS</a> networks. While many of these integrations use proprietary management protocols, others conveniently use <a href="https://en.wikipedia.org/wiki/Simple_Network_Management_Protocol" target="blank">SNMP</a>. 

SNMP defines a standard protocol to collect status data, configuration information, and asynchronous notifications from such devices. Slightly less convenient is the fact that during development, we often don’t have access to the actual devices we need to integrate.

Developing and testing against production devices is not an option. While sometimes clients may have test devices, they’re often in high demand for exclusive access by other departments within the CSP. They can also suffer from sporadic downtime or are hidden behind remote desktop solutions rendering them unreachable from development and continuous integration environments.

For these reasons, a very long time ago, we developed an SNMP agent simulator. We used <a href="https://dev.java/" target="blank">Java</a> because it was our go-to language at the time. Cut to today, and while we still do most of our server-side work in <a href="https://dev.java/" target="blank">Java</a> and <a href="https://kotlinlang.org/" target="blank">Kotlin</a>, we also have a growing number of teams developing products for our clients in <a href="https://www.rust-lang.org/" target="blank">Rust</a>.

Recently when looking for a way to provide more of our engineers an opportunity to develop and hone their Rust skills and hit on the idea of rewriting the agent simulator in Rust. Not only that, we decided to develop it in the <a href="https://github.com/sonalake/snmp-sim-rust" target="blank">open</a>.

We’ll be sharing more about our progress in future posts, but in this one I’m going to talk about a particular challenge we hit early on.


## The Problem

Our goal is to implement a service that can simulate one or more SNMP agents concurrently and allow such agents to be created, deleted, started, or stopped at runtime.

Our functional requirements can be summarised as:

1. Expose REST endpoints that allow clients to control the simulator
2. Support multiple UDP listeners, each on their own port
3. Support dynamically creating and destroying UDP listeners

We also want to avoid excessive thread creation.

One of the first problems we hit was how to approach handling a dynamic set of UDP listeners in addition to a HTTP listener for REST API requests. Here’s what the (very) high level architecture looks like.

![SNMP simulator diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bwt6r0f931d2p5p33ytu.png)

While there are plenty of Rust HTTP server frameworks [1], none support our UDP requirement. What to do?


## Standing on the Shoulders of Giants

Before we did anything, we needed to make some key technical decisions. Based on previous experience and in some cases research, we settled on using the following crates [2].

### Actix

<a href="https://github.com/actix/actix" target="blank">Actix</a> is an <a href="https://en.wikipedia.org/wiki/Actor_model" target="blank">actor framework</a> for developing concurrent applications built on top of the <a href="https://tokio.rs/" target="blank">Tokio</a> asynchronous runtime. It allows multiple actors to run on a single thread, but also allows actors to run on multiple threads via <a href="https://actix.rs/book/actix/sec-5-arbiter.html" target="blank">Arbiters</a>. Actors can communicate with each other by sequentially exchanging typed messages.

The Actix framework is stable and mature and will ultimately help us create a scalable solution.


## Actix Web

To handle REST API requests, we chose the <a href="https://actix.rs/)" target="blank">Actix Web framework</a>. By default, its <a href="https://actix.rs/docs/server/" target="blank">HttpServer</a> automatically starts a number of HTTP workers equal to the number of logical CPUs in the system. Once created, each receives a separate application instance to handle requests. 

To handle REST API requests, we chose the Actix Web framework. By default, its HttpServer automatically starts a number of HTTP workers equal to the number of logical CPUs in the system. Once created, each receives a separate application instance to handle requests. Application state is not shared between threads, and handlers are free to manipulate their copy of the state with no concurrency concerns.


### rasn ASN.1

We also need to read and write SNMP messages. The <a href="https://github.com/XAMPPRocky/rasn" target="blank">rasn</a> ASN.1 codec framework supports handling ASN.1 data types according to different encoding rules.

Conveniently, it also provides implementations for a bunch of relevant SNMP IETF RFCs:

* SNMP v1: [RFC 1157](https://www.rfc-editor.org/rfc/rfc1157)
* SNMP v2: [RFC 3416](https://www.rfc-editor.org/rfc/rfc3416)
* SNMP v2c: [RFC 1901](<RFC 1901>)
* SNMP v3: [RFC 3412](https://www.rfc-editor.org/rfc/rfc3412), [RFC 3414](https://www.rfc-editor.org/rfc/rfc3414)


## Application Architecture

On startup, the SNMP Agent Simulator creates a number of <a href="https://github.com/sonalake/snmp-sim-rust/blob/8aa1d8ad4098bcd672861098192597d686445e36/src/udp_server/udp_server_provider.rs" target="blank">UdpServerProvider</a> actors to handle incoming requests. The number of actors is determined by the host environment CPU resources. The UdpStreamHandler actor is responsible for serializing and deserializing UDP messages and for forwarding the decoded SNMP messages to the SNMPAgentEngine for further processing.

![SNMP Simulator architecture diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1zuibnipgcyj9c852i9f.png)


### UDP Socket Creation

So how do we go about creating a UDP socket for each agent? There are a couple of potential options. We can either use <a href="https://doc.rust-lang.org/stable/std/net/struct.UdpSocket.html" target="blank">UdpSocket</a> from the Rust standard library, or <a href="https://docs.rs/async-std/latest/async_std/net/struct.UdpSocket.html" target="blank">UdpSocket</a> from the Rust async standard library, or UdpSocket from Tokio. Which to choose?


#### Rust Standard Library

The standard net <a href="https://doc.rust-lang.org/std/net/struct.UdpSocket.html" target="blank">UdpSocket</a> represents a raw UDP socket bound to a socket address.

```rust
let std_socket = std::net::UdpSocket::bind(binding_address)
    .map_err(|error| UdpServerError::StartFailed(error.to_string()))?;
```

Standard net UdpSockets are created in blocking mode by default, and adding a stream backed by a socket created in blocking mode to an Actix stream handler actor leads the actor to end up in deadlock state.

To work around this, we need to ensure we create the socket in non-blocking mode before adding it to the Actix stream handler actor.

```rust
std_socket
    .set_nonblocking(true)
    .map_err(|error| UdpServerError::StartFailed(error.to_string()))?;
```

From here we can take that socket and wrap it in a Tokio <a href="https://docs.rs/tokio-util/latest/tokio_util/udp/struct.UdpFramed.html" target="blank">UdpFramed</a> object.

```rust
let socket = TokioUdpSocket::from_std(std_socket)
    .map_err(|error| UdpServerError::StartFailed(error.to_string()))?;
```

This works, but we have to explicitly set the socket to no-blocking mode. Perhaps the Rust async standard library might offer a solution.


#### Rust Async Standard Library

Unfortunately this isn’t an option. Tokio does not support <a href="https://docs.rs/async-std/latest/async_std/" target="blank">async_std</a> sockets so we can’t wrap the socket in a <a href="https://docs.rs/tokio-util/latest/tokio_util/udp/struct.UdpFramed.html" target="blank">UdpFramed</a> object.

The last option to explore is the Tokio UdpSocket.


#### Tokio UdpSocket

The Tokio UdpSocket is created in non-blocking mode directly, so we don’t need to change that property manually.

```rust
let socket = TokioUdpSocket::bind(binding_address)
    .await
    .map_err(|error| UdpServerError::StartFailed(error.to_string()))?;
```

This does the job and is succinct. Now that we’ve settled on how to create sockets, the next challenge is to figure out how to convert data to and from SNMP messages.


### UDP data to SNMP Messages

Raw UDP sockets work with <a href="https://en.wikipedia.org/wiki/Datagram" target="blank">datagrams</a> (byte buffers), but it is more convenient for us to work with domain specific types (messages). Tokio <a href="https://docs.rs/tokio-util/latest/tokio_util/udp/struct.UdpFramed.html" target="blank">UdpFramed</a> is an interface to an underlying UDP socket that returns a single object that is both a <a href="https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html" target="blank">Stream</a> for reading messages and a <a href="https://docs.rs/futures-sink/latest/futures_sink/trait.Sink.html" target="blank">Sink</a> for writing messages. However, the StreamHandler actor expects the Stream part of the UdpFramed object only. Conveniently, the split method allows us to <a href="https://docs.rs/futures-util/latest/futures_util/stream/trait.StreamExt.html#method.split" target="blank">split</a> the UdpFramed object into separate Stream and Sink objects as in the following code snippet.

```rust
// create a UdpFramed object using the SnmpCodec to work with the encoded/decoded frames directly, instead of raw UDP data
let (sink, stream) = UdpFramed::new(socket, SnmpCodec::default()).split();
```

The UdpFramed constructor must be supplied by an instance of a codec object, which implements the <a href="https://docs.rs/tokio-util/latest/tokio_util/codec/trait.Encoder.html" target="blank">Encoder</a> and <a href="https://docs.rs/tokio-util/latest/tokio_util/codec/trait.Decoder.html" target="blank">Decoder</a> Tokio crate traits. It’s used on top of the socket to handle frames. SnmpCodec is used to decode the received UDP socket data into a stream of SNMP messages, and to encode the SNMP messages into ASN.1 encoded UDP socket data. The SnmpCodec is a wrapper of <a href="https://github.com/XAMPPRocky/rasn" target="blank">rasn</a> ASN.1 codec framework, which implements the Encoder and Decoder traits to encode and decode UDP frames. The decoder and encoder implementations leverage the rasn ASN.1 codec framework.

In the Actix <a href="https://actix.rs/actix/actix/trait.StreamHandler.html#" target="blank">StreamHandler</a> actor, the stream handler helper trait is used to implement a UDP receiver data handler in a similar way to standard actor messages. The UdpStreamHandler represents an UDP socket receiver. The actor UdpMessage handler is called, when an SNMP message is received from the socket and successfully decoded.

```rust
// Add the stream to the actor's context.
// Stream item will be treated as a concurrent message and the actor's handle will be called.
ctx.add_stream(stream.map(|a| UdpMessage(a.map_err(|e| e.to_string()))));
```

The StreamHandler actor has no method to deregister the stream from the actor’s context. The solution here is to simply close the socket representing the stream.


## What next?

We love Rust! It’s a great language. However, it’s still possible for developers to write code that results in deadlocks. This often happens when the synchronous and asynchronous worlds accidentally collide. Perhaps we’ll write more about that in a future post.

In the meantime there’s lots more work to do on the SNMP simulator. If you’re interested in contributing, please let us know on <a href="https://github.com/sonalake/snmp-sim-rust" target="blank">Github</a>!


---
[1]: For example [Actix Web](https://actix.rs/), [Rocket](https://rocket.rs/), [Axum](https://github.com/tokio-rs/axum), [Warp](https://github.com/seanmonstar/warp), [Tide](https://github.com/http-rs/tide). 

[2]: 'Library' or 'package' for readers new to Rust