pub struct StunClient { /* private fields */ }
Expand description
A STUN client is an entity that sends STUN requests and receives STUN responses and STUN indications. A STUN client can also send indications.
§StunClient
This is the main entity used to interact with the STUN server.
The StunClient
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.
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);
Implementations§
Source§impl StunClient
impl StunClient
Sourcepub fn send_request(
&mut self,
method: MessageMethod,
attributes: StunAttributes,
buffer: Vec<u8>,
instant: Instant,
) -> Result<TransactionId, StunAgentError>
pub fn send_request( &mut self, method: MessageMethod, attributes: StunAttributes, buffer: Vec<u8>, instant: Instant, ) -> Result<TransactionId, StunAgentError>
Creates a STUN request.
§Arguments
method
- The STUNMessageMethod
to use.attributes
- TheStunAttributes
to include in the request.buffer
- The buffer to send with the request.instant
- The instant when the request is sent.
§Returns
The TransactionId
of the request on success. Otherwise, a StunAgentError
is returned.
After calling this method, the user should invoke events to retrieve the events generated by the agent.
Sourcepub fn send_indication(
&mut self,
method: MessageMethod,
attributes: StunAttributes,
buffer: Vec<u8>,
) -> Result<TransactionId, StunAgentError>
pub fn send_indication( &mut self, method: MessageMethod, attributes: StunAttributes, buffer: Vec<u8>, ) -> Result<TransactionId, StunAgentError>
Creates a STUN indication.
§Arguments
method
- The STUNMessageMethod
to use.attributes
- TheStunAttributes
to include in the indication.buffer
- The buffer to send with the indication.
§Returns
The TransactionId
of the indication on success. Otherwise, a StunAgentError
is returned.
After calling this method, the user should invoke events to retrieve the events generated by the agent.
Sourcepub fn on_buffer_recv(
&mut self,
buffer: &[u8],
instant: Instant,
) -> Result<(), StunAgentError>
pub fn on_buffer_recv( &mut self, buffer: &[u8], instant: Instant, ) -> Result<(), StunAgentError>
Called when a buffer is received from the server.
§Arguments
buffer
- The buffer received from the server.instant
- The instant when the buffer was received.
§Returns
A StunAgentError
if the buffer is invalid or the transaction is discarded.
In the case when STUN is being multiplexed with another protocol, an error
may indicate that this is not really a STUN message; in this case, the agent
should try to parse the message as a different protocol.
After calling this method, the user should invoke events to retrieve the events generated by the agent.
Sourcepub fn on_timeout(&mut self, instant: Instant)
pub fn on_timeout(&mut self, instant: Instant)
Sourcepub fn events(&mut self) -> Vec<StuntClientEvent>
pub fn events(&mut self) -> Vec<StuntClientEvent>
Returns the events generated by the agent. This method should be called after any interaction with the agent. The events notify the user about the status of the transactions. Note that no state is maintained between interactions with the agent. Therefore, the user should call this method to retrieve the events as soon as an operation is completed. Otherwise, the events may be lost if a new operation is performed.