3 releases
0.1.2 | Oct 22, 2024 |
---|---|
0.1.1 | Oct 22, 2024 |
0.1.0 | Oct 22, 2024 |
#514 in Network programming
685KB
13K
SLoC
Crate stun-agent
STUN Agent library for Rust.
This crate provides a STUN I/O-free protocol implementation. An I/O-free protocol implementation, often referred to as a Sans-IO implementation, is a network protocol implementation that contains no code for network I/O or asynchronous flow control. This means the protocol implementation is agnostic to the underlying networking stack and can be used in any environment that provides the necessary network I/O and asynchronous flow control.
These STUN agents are desi∫gned for use in a client-server architecture where the client sends a request and the server responds. This sans-IO protocol implementation is defined entirely in terms of synchronous functions returning synchronous results, without blocking or waiting for any form of I/O. This makes it suitable for a wide range of environments, enhancing testing, flexibility, correctness, re-usability and simplicity.
This library currently provides support for writing STUN clients. Support for writing servers is not yet implemented. The main element of this library is:
StunClient
A STUN client is an entity that sends STUN requests and receives STUN responses and STUN indications. A STUN client can also send indications. This is the main entity used to interact with the STUN server. This class provides the tools required to implement different STUN usages over the STUN protocol in an easy and efficient way.
API considerations
Since the StunClient abstains from performing any I/O operations, the controller assumes responsibility for managing input and output buffers, timeouts, and client-generated events. The implementation of this controller is entirely at the user’s discretion and does not enforce the use of any specific I/O stack or asynchronous framework. This abstraction imposes certain guidelines to ensure the protocol’s proper functioning. Consequently, users must consider the following technical aspects:
- The controller must capture and handle any events that the client may generate after interacting with the library.
- The controller must handle the input and output buffers that the client will use to send and receive data from the server.
- Timing management falls under the controller’s jurisdiction, as the client lacks internal time-handling mechanisms. The controller must define transaction timeouts and inform the client upon their expiration. For supporting timed events, the API exposes an Instant parameter to the controller, facilitating specification of event occurrence times.
Design considerations
Most Sans I/O implementations are structured around a state machine that responds to events generated by both the client and the server. Each event triggers the generation of output buffers, timers, or additional events. This foundational concept is illustrated in the following API:
let events = handle_data(&in_bytes);
let out_bytes = perform_action();
However, the STUN requirements introduce complexity to the API. The aforementioned API alone does not suffice to manage STUN intricacies. For instance, the handle_data function might fail and trigger events even in case of failures. The STUN client needs to manage these events and generate further events for the controller. This implementation could have been realized as follows:
fn handle_data(in_bytes: &[u8])
-> Result<Vec<StuntClientEvent>, (StunAgentError, Vec<StuntClientEvent>)> {
// Implementation
}
The design of this API necessitates that the caller manages both errors and the events they generate. This approach can lead to increased complexity and maintenance challenges in the caller’s code. For instance, the caller may employ a match expression when invoking the function to handle both success outcomes and the errors and resulting events in case of failure:
let response = handle_data(&in_bytes);
match response {
Ok(events) => {
handle_events(events);
},
Err((error, events)) => {
handle_error(error);
handle_events(events);
},
}
As observed, managing events in both success and failure scenarios indicates a sub-optimal design. Consequently, the STUN client API is structured to enable the caller to pull events generated by the client. While this approach offers a more ergonomic event handling mechanism, it requires the caller to actively retrieve and process events from the client.
fn handle_data(in_bytes: &[u8]) -> Result<ClientData, StunAgentError> {
// Implementation
}
And the controller’s code would look like this:
let data = handle_data(&in_bytes)?;
// Now we can pull events from the client
let events = pull_events();
Moreover, this type of API not only facilitates the retrieval of events but also allows for the retrieval of data generated by the client. For instance, the send_request method returns the TransactionId of the request, which the controller can use to manage outgoing transactions.
[!WARNING] Events are overwritten whenever a new operation is performed on the client. Therefore, the controller must ensure that all events are processed before initiating any new operations. In multi-threaded environments, the controller must also synchronize operations and event retrieval to maintain consistency and prevent data loss.
Input and Output
The STUN client does not perform any I/O operations. Instead, the controller is responsible for managing input and output buffers. Memory allocation is delegated to the controller, which must provide the buffers used by the client. This approach reduces the client’s memory footprint and enhances performance by enabling more sophisticated memory management strategies, such as memory pools, where buffers can be reused to minimize memory allocation overhead.
Timing Management
The STUN client does not manage timing internally. Instead, the controller is responsible for setting timeouts and managing transaction timing. The API provides an Instant parameter to the controller, allowing it to specify event occurrence times. Timing consistency across operations is crucial, meaning that time must monotonically increase to ensure the proper functioning of the client.
Exposing the Instant parameter in the API might seem counter intuitive, as it requires the controller to manage time. However, this design choice ensures that the client remains agnostic to time management, granting the controller full control over the internal state machine. This approach facilitates comprehensive testing of complex scenarios by enabling deterministic time control without the need to mock time.
Timeouts
Timeouts specify the maximum duration the client will wait for an event to occur. The STUN client uses timeouts to manage transactions and prevent indefinite waiting for responses. If a response is not received within the designated timeout period, the client generates a timeout event, marking the transaction as failed. Timeouts are also employed to manage re-transmissions of requests sent over unreliable transports. When the client needs to set a timeout for a re-transmission, it generates a RestransmissionTimeOut event, which is then notified to the controller when the events are pulled.
If multiple timeouts are scheduled, the client will only notify the controller of the most recent timeout. This approach allows the controller to manage timeouts more efficiently, ensuring that only one timeout needs to be handled at a time.
Managing timeouts is the responsibility of the controller; the STUN client will only provide the timeout duration. If the timeout is not canceled, the controller must call the on_timeout method to inform the client that the timeout has been reached.
Timeouts are identified by a TransactionId. When a timeout is canceled for any reason, the client will notify the controller either by setting a new timeout with a different TransactionId or by not setting any timeout event at all.
Usage
The following example demonstrates how to create a STUN client and send a BINDING indication to a STUN server.
// We use a client builder to create a STUN client, for this example,
// the client will be used over an unreliable transport such as UDP.
// This client will no use any credential mechanism nor the FINGERPRINT
// attributes. Besides, we configure the default parameters for the
// re-transmission timeout.
let mut client = StunClienteBuilder::new(
TransportReliability::Unreliable(RttConfig::default()))
.build()
.unwrap();
// We create a STUN BINDING indication to send to the server.
// According to the RFC8489, the BINDING indications does not require
// any attributes.
let mut attributes = StunAttributes::default();
// Since this is a library implementation without direct I/O operations,
// no input or output will be handled by the stack. Instead, we need to
// access the output buffer event provided by the client to send the data
// through the socket.
// Besides, no buffer allocations will be performed by the library, so the
// controller must provide the buffer that will be used to send the data.
// This allow the library to reduce the memory footprint and improve the
// performance, being flexible to allow more complex usages of memory such
// as memory pools where buffers can be reused to minimize the memory
// allocation overhead.
let buffer = vec![0; 1024];
client.send_indication(BINDING, attributes, buffer).unwrap();
// Pull events from the client
let events = client.events();
// Only one output packect event is expected. This event must contain the
// buffer that will be sent to the server. Because indications do not require
// a response, no timeouts will be set for this transaction.
assert_eq!(events.len(), 1);
let mut iter = events.iter();
// Next event already contains the buffer that needs to be send to the server.
let StuntClientEvent::OutputPacket(buffer) = iter
.next()
.expect("Expected event")
else {
panic!("Expected OutputBuffer event");
};
In the following example we are going to use the STUN client to send a BINDING request to a STUN server. Requests require a response from the server, so the client will set a timeout for the transaction. The response must arrive before the timeout is reached, otherwise the client will generate a timeout event and will mark the transaction as failed.
// We create a STUN BINDING request to send to the server.
// According to the RFC8489, the BINDING request does not require
// any attributes.
let instant = std::time::Instant::now();
let mut attributes = StunAttributes::default();
let buffer = vec![0; 1024];
let transaction_id = client
.send_request(BINDING, attributes, buffer, instant)
.unwrap();
// Pull events from the client
let events = client.events();
// Two events are expected, the first one is the output buffer event
// and the second one is the timeout event.
assert_eq!(events.len(), 2);
let mut iter = events.iter();
// Next event already contains the buffer that needs to be send to the server.
let StuntClientEvent::OutputPacket(buffer) = iter
.next()
.expect("Expected event")
else {
panic!("Expected OutputBuffer event");
};
// Next event indicates that the user must set a timeout for the transaction
// identified by the transaction_id.
let StuntClientEvent::RestransmissionTimeOut((id, duration)) = iter
.next()
.expect("Expected event")
else {
panic!("Expected RestransmissionTimeOut event");
};
assert_eq!(id, &transaction_id);
// Now the controller should set a timout of `duration` for the transaction
// identified by `id`. After the timeout is reached, the controller must call
// the `on_timeout` method to notify the client that the time has expired.
// We re going to simulate the timeout event by calling the `on_timeout` method.
let instant = instant + *duration;
client.on_timeout(instant);
// Pull events from the client
let events = client.events();
// Two events are expected, the first one is the retransmission of the requests,
// and the second one is the new timeout set for the transaction.
assert_eq!(events.len(), 2);
let mut iter = events.iter();
// Next event contains the buffer that needs to be retransmitted.
let StuntClientEvent::OutputPacket(buffer) = iter
.next()
.expect("Expected event")
else {
panic!("Expected OutputBuffer event");
};
let StuntClientEvent::RestransmissionTimeOut((id, duration)) = iter
.next()
.expect("Expected event")
else {
panic!("Expected RestransmissionTimeOut event");
};
assert_eq!(id, &transaction_id);
When sending over an unreliable transport, the client SHOULD re-transmit a STUN request message starting with an interval of RTO (“Re-transmission TimeOut”), doubling after each re-transmission until a final timeout is reached. By default, if the controller does not set a different value, the default timeout is 39500 ms for both, reliable and not reliable transports. If the client has not received a response after that time, the client will consider the transaction to have timed out, and an event of type TransactionFailed will be generated the next time that events were pulled with the error TimedOut for the transaction.
To finish, the next example shows how to handle buffers received from the server. Raw buffers will be processed by the client to generate events that can be pulled by the controller.
// Buffer received from the server
let buffer = [
0x00, 0x11, 0x00, 0x00, // BINDING Indication type and message length
0x21, 0x12, 0xA4, 0x42, // Magic cookie
0xB8, 0xC2, 0x8E, 0x1A, // }
0x41, 0x05, 0x18, 0x56, // } Transaction ID
0x3E, 0xFC, 0xCF, 0x5D, // }
];
// Process buffer
client.on_buffer_recv(&buffer, Instant::now()).unwrap();
// Pull events from the client
let events = client.events();
// There must be only one events with the STUN message received
assert_eq!(events.len(), 1);
let mut iter = events.iter();
let StuntClientEvent::StunMessageReceived(msg) = iter
.next()
.expect("Expected event")
else {
panic!("Expected StunMessageReceived event");
};
assert_eq!(msg.method(), BINDING);
assert_eq!(msg.class(), Indication);
// No attributes in the message
assert_eq!(msg.attributes().len(), 0);
Contributing
Patches and feedback are welcome.
Donations
If you find this project helpful, you may consider making a donation:
License
This project is licensed under either of
Dependencies
~6.5MB
~129K SLoC