Skip to main content

turn_server_sdk/
lib.rs

1//! # Turn Server SDK
2//!
3//! A Rust client SDK for interacting with the `turn-server` gRPC API exposed by the `turn-rs` project.
4//! This crate provides both client and server utilities for TURN server integration.
5//!
6//! ## Features
7//!
8//! - **TurnService Client**: Query server information, session details, and manage TURN sessions
9//! - **TurnHooksServer**: Implement custom authentication and event handling for TURN server hooks
10//! - **Password Generation**: Generate STUN/TURN authentication passwords using MD5 or SHA256
11//!
12//! ## Client Usage
13//!
14//! The `TurnService` client allows you to interact with a running TURN server's gRPC API:
15//!
16//! ```no_run
17//! use tonic::transport::Channel;
18//! use turn_server_sdk::{TurnService, protos::{Identifier, Transport}};
19//!
20//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21//! // Connect to the TURN server gRPC endpoint
22//! let channel = Channel::from_static("http://127.0.0.1:3000")
23//!     .connect()
24//!     .await?;
25//!
26//! // Create a client
27//! let mut client = TurnService::new(channel);
28//!
29//! // Get server information
30//! let info = client.get_info().await?;
31//! println!("Server software: {}", info.software);
32//!
33//! let id = Identifier {
34//!     source: "127.0.0.1".to_string(),
35//!     external: "127.0.0.1".to_string(),
36//!     interface: "127.0.0.1".to_string(),
37//!     transport: Transport::Udp as i32,
38//! };
39//!
40//! // Query a session by ID
41//! let session = client.get_session(id.clone()).await?;
42//! println!("Session username: {}", session.username);
43//!
44//! // Get session statistics
45//! let stats = client.get_session_statistics(id.clone()).await?;
46//! println!("Bytes sent: {}", stats.send_bytes);
47//!
48//! // Destroy a session
49//! client.destroy_session(id).await?;
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ## Server Usage (Hooks Implementation)
55//!
56//! Implement the `TurnHooksServer` trait to provide custom authentication and handle TURN events:
57//!
58//! ```no_run
59//! use tonic::transport::Server;
60//! use turn_server_sdk::{
61//!     TurnHooksServer, Credential, protos::{PasswordAlgorithm, Identifier},
62//! };
63//!
64//! use std::net::SocketAddr;
65//!
66//! struct MyHooksServer;
67//!
68//! #[tonic::async_trait]
69//! impl TurnHooksServer for MyHooksServer {
70//!     async fn get_password(
71//!         &self,
72//!         _id: Identifier,
73//!         realm: &str,
74//!         username: &str,
75//!         algorithm: PasswordAlgorithm,
76//!     ) -> Result<Credential, tonic::Status> {
77//!         // Implement your authentication logic here
78//!         // For example, look up the user in a database
79//!         Ok(Credential {
80//!             password: "user-password".to_string(),
81//!             realm: realm.to_string(),
82//!         })
83//!     }
84//!
85//!     async fn on_allocated(&self, id: Identifier, username: String, port: u16) {
86//!         println!("Session allocated: id={:?}, username={}, port={}", id, username, port);
87//!         // Handle allocation event (e.g., log to database, update metrics)
88//!     }
89//!
90//!     async fn on_destroy(&self, id: Identifier, username: String) {
91//!         println!("Session destroyed: id={:?}, username={}", id, username);
92//!         // Handle session destruction (e.g., cleanup resources)
93//!     }
94//! }
95//!
96//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
97//! // Start the hooks server
98//! let mut server = Server::builder();
99//! let hooks = MyHooksServer;
100//!
101//! hooks.start_with_server(
102//!     &mut server,
103//!     "127.0.0.1:8080".parse()?,
104//! ).await?;
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! ## Password Generation
110//!
111//! Generate STUN/TURN authentication passwords for long-term credentials:
112//!
113//! ```no_run
114//! use turn_server_sdk::{generate_password, protos::PasswordAlgorithm};
115//!
116//! // Generate MD5 password (RFC 5389)
117//! let md5_password = generate_password(
118//!     "username",
119//!     "password",
120//!     "realm",
121//!     PasswordAlgorithm::Md5,
122//! );
123//!
124//! // Generate SHA256 password (RFC 8489)
125//! let sha256_password = generate_password(
126//!     "username",
127//!     "password",
128//!     "realm",
129//!     PasswordAlgorithm::Sha256,
130//! );
131//!
132//! // Access the password bytes
133//! match md5_password {
134//!     turn_server_sdk::Password::Md5(bytes) => {
135//!         println!("MD5 password: {:?}", bytes);
136//!     }
137//!     turn_server_sdk::Password::Sha256(bytes) => {
138//!         println!("SHA256 password: {:?}", bytes);
139//!     }
140//! }
141//! ```
142//!
143//! ## Event Handling
144//!
145//! The `TurnHooksServer` trait provides hooks for various TURN server events:
146//!
147//! - `on_allocated`: Called when a client allocates a relay port
148//! - `on_channel_bind`: Called when a channel is bound to a peer
149//! - `on_create_permission`: Called when permissions are created for peers
150//! - `on_refresh`: Called when a session is refreshed
151//! - `on_destroy`: Called when a session is destroyed
152//!
153//! All event handlers are optional and have default no-op implementations.
154//!
155//! ## Error Handling
156//!
157//! Most operations return `Result<T, Status>` where `Status` is a gRPC status code.
158//! Common error scenarios:
159//!
160//! - `Status::not_found`: Session or resource not found
161//! - `Status::unavailable`: Server is not available
162//! - `Status::unauthenticated`: Authentication failed
163//!
164//! ## Re-exports
165//!
166//! This crate re-exports:
167//! - `tonic`: The gRPC framework used for communication
168//! - `protos`: The generated protobuf bindings for TURN server messages
169//!
170//! ## See Also
171//!
172//! - [TURN Server Documentation](../README.md)
173//! - [RFC 8489](https://tools.ietf.org/html/rfc8489) - Session Traversal Utilities for NAT (STUN)
174//! - [RFC 8656](https://tools.ietf.org/html/rfc8656) - Traversal Using Relays around NAT (TURN)
175
176pub mod protos {
177    tonic::include_proto!("turn.server");
178}
179
180use std::{net::SocketAddr, ops::Deref};
181
182use md5::{Digest, Md5};
183use sha2::Sha256;
184use tonic::{
185    Request, Response, Status,
186    transport::{Channel, Server},
187};
188
189use self::protos::{
190    GetTurnPasswordRequest, GetTurnPasswordResponse, Identifier, PasswordAlgorithm,
191    TurnAllocatedEvent, TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent,
192    TurnRefreshEvent, TurnServerInfo, TurnSession, TurnSessionStatistics,
193    turn_hooks_service_server::{TurnHooksService, TurnHooksServiceServer},
194    turn_service_client::TurnServiceClient,
195};
196
197/// turn service client
198///
199/// This struct is used to interact with the turn service.
200pub struct TurnService(TurnServiceClient<Channel>);
201
202impl TurnService {
203    /// create a new turn service client
204    pub fn new(channel: Channel) -> Self {
205        Self(TurnServiceClient::new(channel))
206    }
207
208    /// get the server info
209    pub async fn get_info(&mut self) -> Result<TurnServerInfo, Status> {
210        Ok(self.0.get_info(Request::new(())).await?.into_inner())
211    }
212
213    /// get the session
214    pub async fn get_session(&mut self, id: Identifier) -> Result<TurnSession, Status> {
215        Ok(self.0.get_session(Request::new(id)).await?.into_inner())
216    }
217
218    /// get the session statistics
219    pub async fn get_session_statistics(
220        &mut self,
221        id: Identifier,
222    ) -> Result<TurnSessionStatistics, Status> {
223        Ok(self
224            .0
225            .get_session_statistics(Request::new(id))
226            .await?
227            .into_inner())
228    }
229
230    /// destroy the session
231    pub async fn destroy_session(&mut self, id: Identifier) -> Result<(), Status> {
232        Ok(self.0.destroy_session(Request::new(id)).await?.into_inner())
233    }
234}
235
236/// credential
237///
238/// This struct is used to store the credential for the turn hooks server.
239pub struct Credential {
240    pub password: String,
241    pub realm: String,
242}
243
244struct TurnHooksServerInner<T>(T);
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum Password {
248    Md5([u8; 16]),
249    Sha256([u8; 32]),
250}
251
252impl Deref for Password {
253    type Target = [u8];
254
255    fn deref(&self) -> &Self::Target {
256        match self {
257            Password::Md5(it) => it,
258            Password::Sha256(it) => it,
259        }
260    }
261}
262
263pub fn generate_password(
264    username: &str,
265    password: &str,
266    realm: &str,
267    algorithm: PasswordAlgorithm,
268) -> Password {
269    match algorithm {
270        PasswordAlgorithm::Md5 => {
271            let mut hasher = Md5::new();
272
273            hasher.update([username, realm, password].join(":"));
274
275            Password::Md5(hasher.finalize().into())
276        }
277        PasswordAlgorithm::Sha256 => {
278            let mut hasher = Sha256::new();
279
280            hasher.update([username, realm, password].join(":").as_bytes());
281
282            let mut result = [0u8; 32];
283            result.copy_from_slice(&hasher.finalize());
284            Password::Sha256(result)
285        }
286        PasswordAlgorithm::Unspecified => {
287            panic!("Invalid password algorithm");
288        }
289    }
290}
291
292#[tonic::async_trait]
293impl<T: TurnHooksServer + 'static> TurnHooksService for TurnHooksServerInner<T> {
294    async fn get_password(
295        &self,
296        request: Request<GetTurnPasswordRequest>,
297    ) -> Result<Response<GetTurnPasswordResponse>, Status> {
298        let request = request.into_inner();
299        let algorithm = request.algorithm();
300
301        if let Ok(credential) = self
302            .0
303            .get_password(
304                request
305                    .id
306                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
307                &request.realm,
308                &request.username,
309                algorithm,
310            )
311            .await
312        {
313            Ok(Response::new(GetTurnPasswordResponse {
314                password: generate_password(
315                    &request.username,
316                    &credential.password,
317                    &credential.realm,
318                    algorithm,
319                )
320                .to_vec(),
321            }))
322        } else {
323            Err(Status::not_found("Message integrity not found"))
324        }
325    }
326
327    async fn on_allocated_event(
328        &self,
329        request: Request<TurnAllocatedEvent>,
330    ) -> Result<Response<()>, Status> {
331        let request = request.into_inner();
332        self.0
333            .on_allocated(
334                request
335                    .id
336                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
337                request.username,
338                request.port as u16,
339            )
340            .await;
341
342        Ok(Response::new(()))
343    }
344
345    async fn on_channel_bind_event(
346        &self,
347        request: Request<TurnChannelBindEvent>,
348    ) -> Result<Response<()>, Status> {
349        let request = request.into_inner();
350        self.0
351            .on_channel_bind(
352                request
353                    .id
354                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
355                request.username,
356                request.channel as u16,
357            )
358            .await;
359
360        Ok(Response::new(()))
361    }
362
363    async fn on_create_permission_event(
364        &self,
365        request: Request<TurnCreatePermissionEvent>,
366    ) -> Result<Response<()>, Status> {
367        let request = request.into_inner();
368        self.0
369            .on_create_permission(
370                request
371                    .id
372                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
373                request.username,
374                request.ports.iter().map(|p| *p as u16).collect(),
375            )
376            .await;
377
378        Ok(Response::new(()))
379    }
380
381    async fn on_refresh_event(
382        &self,
383        request: Request<TurnRefreshEvent>,
384    ) -> Result<Response<()>, Status> {
385        let request = request.into_inner();
386        self.0
387            .on_refresh(
388                request
389                    .id
390                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
391                request.username,
392                request.lifetime as u32,
393            )
394            .await;
395
396        Ok(Response::new(()))
397    }
398
399    async fn on_destroy_event(
400        &self,
401        request: Request<TurnDestroyEvent>,
402    ) -> Result<Response<()>, Status> {
403        let request = request.into_inner();
404        self.0
405            .on_destroy(
406                request
407                    .id
408                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
409                request.username,
410            )
411            .await;
412
413        Ok(Response::new(()))
414    }
415}
416
417#[tonic::async_trait]
418pub trait TurnHooksServer: Send + Sync {
419    #[allow(unused_variables)]
420    async fn get_password(
421        &self,
422        id: Identifier,
423        realm: &str,
424        username: &str,
425        algorithm: PasswordAlgorithm,
426    ) -> Result<Credential, Status> {
427        Err(Status::unimplemented("get_password is not implemented"))
428    }
429
430    /// allocate request
431    ///
432    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
433    ///
434    /// In all cases, the server SHOULD only allocate ports from the range
435    /// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]),
436    /// unless the TURN server application knows, through some means not
437    /// specified here, that other applications running on the same host as
438    /// the TURN server application will not be impacted by allocating ports
439    /// outside this range.  This condition can often be satisfied by running
440    /// the TURN server application on a dedicated machine and/or by
441    /// arranging that any other applications on the machine allocate ports
442    /// before the TURN server application starts.  In any case, the TURN
443    /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well-
444    /// Known Port range) to discourage clients from using TURN to run
445    /// standard services.
446    #[allow(unused_variables)]
447    async fn on_allocated(&self, id: Identifier, username: String, port: u16) {}
448
449    /// channel bind request
450    ///
451    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
452    ///
453    /// If the request is valid, but the server is unable to fulfill the
454    /// request due to some capacity limit or similar, the server replies
455    /// with a 508 (Insufficient Capacity) error.
456    ///
457    /// Otherwise, the server replies with a ChannelBind success response.
458    /// There are no required attributes in a successful ChannelBind
459    /// response.
460    #[allow(unused_variables)]
461    async fn on_channel_bind(&self, id: Identifier, username: String, channel: u16) {}
462
463    /// create permission request
464    ///
465    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
466    ///
467    /// If the request is valid, but the server is unable to fulfill the
468    /// request due to some capacity limit or similar, the server replies
469    /// with a 508 (Insufficient Capacity) error.
470    ///
471    /// Otherwise, the server replies with a ChannelBind success response.
472    /// There are no required attributes in a successful ChannelBind
473    /// response.
474    #[allow(unused_variables)]
475    async fn on_create_permission(&self, id: Identifier, username: String, ports: Vec<u16>) {}
476
477    /// refresh request
478    ///
479    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
480    ///
481    /// If the request is valid, but the server is unable to fulfill the
482    /// request due to some capacity limit or similar, the server replies
483    /// with a 508 (Insufficient Capacity) error.
484    ///
485    /// Otherwise, the server replies with a ChannelBind success response.
486    /// There are no required attributes in a successful ChannelBind
487    /// response.
488    #[allow(unused_variables)]
489    async fn on_refresh(&self, id: Identifier, username: String, lifetime: u32) {}
490
491    /// session closed
492    ///
493    /// Triggered when the session leaves from the turn. Possible reasons: the
494    /// session life cycle has expired, external active deletion, or active
495    /// exit of the session.
496    #[allow(unused_variables)]
497    async fn on_destroy(&self, id: Identifier, username: String) {}
498
499    /// start the turn hooks server
500    ///
501    /// This function will start the turn hooks server on the given server and listen address.
502    async fn start_with_server(
503        self,
504        server: &mut Server,
505        listen: SocketAddr,
506    ) -> Result<(), tonic::transport::Error>
507    where
508        Self: Sized + 'static,
509    {
510        server
511            .add_service(TurnHooksServiceServer::<TurnHooksServerInner<Self>>::new(
512                TurnHooksServerInner(self),
513            ))
514            .serve(listen)
515            .await?;
516
517        Ok(())
518    }
519}