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 use protos;
177use sha2::Sha256;
178
179use std::{net::SocketAddr, ops::Deref};
180
181use md5::{Digest, Md5};
182use tonic::{
183 Request, Response, Status,
184 transport::{Channel, Server},
185};
186
187use protos::{
188 GetTurnPasswordRequest, GetTurnPasswordResponse, Identifier, PasswordAlgorithm,
189 TurnAllocatedEvent, TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent,
190 TurnRefreshEvent, TurnServerInfo, TurnSession, TurnSessionStatistics,
191 turn_hooks_service_server::{TurnHooksService, TurnHooksServiceServer},
192 turn_service_client::TurnServiceClient,
193};
194
195/// turn service client
196///
197/// This struct is used to interact with the turn service.
198pub struct TurnService(TurnServiceClient<Channel>);
199
200impl TurnService {
201 /// create a new turn service client
202 pub fn new(channel: Channel) -> Self {
203 Self(TurnServiceClient::new(channel))
204 }
205
206 /// get the server info
207 pub async fn get_info(&mut self) -> Result<TurnServerInfo, Status> {
208 Ok(self.0.get_info(Request::new(())).await?.into_inner())
209 }
210
211 /// get the session
212 pub async fn get_session(&mut self, id: Identifier) -> Result<TurnSession, Status> {
213 Ok(self.0.get_session(Request::new(id)).await?.into_inner())
214 }
215
216 /// get the session statistics
217 pub async fn get_session_statistics(
218 &mut self,
219 id: Identifier,
220 ) -> Result<TurnSessionStatistics, Status> {
221 Ok(self
222 .0
223 .get_session_statistics(Request::new(id))
224 .await?
225 .into_inner())
226 }
227
228 /// destroy the session
229 pub async fn destroy_session(&mut self, id: Identifier) -> Result<(), Status> {
230 Ok(self.0.destroy_session(Request::new(id)).await?.into_inner())
231 }
232}
233
234/// credential
235///
236/// This struct is used to store the credential for the turn hooks server.
237pub struct Credential {
238 pub password: String,
239 pub realm: String,
240}
241
242struct TurnHooksServerInner<T>(T);
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum Password {
246 Md5([u8; 16]),
247 Sha256([u8; 32]),
248}
249
250impl Deref for Password {
251 type Target = [u8];
252
253 fn deref(&self) -> &Self::Target {
254 match self {
255 Password::Md5(it) => it,
256 Password::Sha256(it) => it,
257 }
258 }
259}
260
261pub fn generate_password(
262 username: &str,
263 password: &str,
264 realm: &str,
265 algorithm: PasswordAlgorithm,
266) -> Password {
267 match algorithm {
268 PasswordAlgorithm::Md5 => {
269 let mut hasher = Md5::new();
270
271 hasher.update([username, realm, password].join(":"));
272
273 Password::Md5(hasher.finalize().into())
274 }
275 PasswordAlgorithm::Sha256 => {
276 let mut hasher = Sha256::new();
277
278 hasher.update([username, realm, password].join(":").as_bytes());
279
280 let mut result = [0u8; 32];
281 result.copy_from_slice(&hasher.finalize());
282 Password::Sha256(result)
283 }
284 PasswordAlgorithm::Unspecified => {
285 panic!("Invalid password algorithm");
286 }
287 }
288}
289
290#[tonic::async_trait]
291impl<T: TurnHooksServer + 'static> TurnHooksService for TurnHooksServerInner<T> {
292 async fn get_password(
293 &self,
294 request: Request<GetTurnPasswordRequest>,
295 ) -> Result<Response<GetTurnPasswordResponse>, Status> {
296 let request = request.into_inner();
297 let algorithm = request.algorithm();
298
299 if let Ok(credential) = self
300 .0
301 .get_password(
302 request
303 .id
304 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
305 &request.realm,
306 &request.username,
307 algorithm,
308 )
309 .await
310 {
311 Ok(Response::new(GetTurnPasswordResponse {
312 password: generate_password(
313 &request.username,
314 &credential.password,
315 &credential.realm,
316 algorithm,
317 )
318 .to_vec(),
319 }))
320 } else {
321 Err(Status::not_found("Message integrity not found"))
322 }
323 }
324
325 async fn on_allocated_event(
326 &self,
327 request: Request<TurnAllocatedEvent>,
328 ) -> Result<Response<()>, Status> {
329 let request = request.into_inner();
330 self.0
331 .on_allocated(
332 request
333 .id
334 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
335 request.username,
336 request.port as u16,
337 )
338 .await;
339
340 Ok(Response::new(()))
341 }
342
343 async fn on_channel_bind_event(
344 &self,
345 request: Request<TurnChannelBindEvent>,
346 ) -> Result<Response<()>, Status> {
347 let request = request.into_inner();
348 self.0
349 .on_channel_bind(
350 request
351 .id
352 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
353 request.username,
354 request.channel as u16,
355 )
356 .await;
357
358 Ok(Response::new(()))
359 }
360
361 async fn on_create_permission_event(
362 &self,
363 request: Request<TurnCreatePermissionEvent>,
364 ) -> Result<Response<()>, Status> {
365 let request = request.into_inner();
366 self.0
367 .on_create_permission(
368 request
369 .id
370 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
371 request.username,
372 request.ports.iter().map(|p| *p as u16).collect(),
373 )
374 .await;
375
376 Ok(Response::new(()))
377 }
378
379 async fn on_refresh_event(
380 &self,
381 request: Request<TurnRefreshEvent>,
382 ) -> Result<Response<()>, Status> {
383 let request = request.into_inner();
384 self.0
385 .on_refresh(
386 request
387 .id
388 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
389 request.username,
390 request.lifetime as u32,
391 )
392 .await;
393
394 Ok(Response::new(()))
395 }
396
397 async fn on_destroy_event(
398 &self,
399 request: Request<TurnDestroyEvent>,
400 ) -> Result<Response<()>, Status> {
401 let request = request.into_inner();
402 self.0
403 .on_destroy(
404 request
405 .id
406 .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
407 request.username,
408 )
409 .await;
410
411 Ok(Response::new(()))
412 }
413}
414
415#[tonic::async_trait]
416pub trait TurnHooksServer: Send + Sync {
417 #[allow(unused_variables)]
418 async fn get_password(
419 &self,
420 id: Identifier,
421 realm: &str,
422 username: &str,
423 algorithm: PasswordAlgorithm,
424 ) -> Result<Credential, Status> {
425 Err(Status::unimplemented("get_password is not implemented"))
426 }
427
428 /// allocate request
429 ///
430 /// [rfc8489](https://tools.ietf.org/html/rfc8489)
431 ///
432 /// In all cases, the server SHOULD only allocate ports from the range
433 /// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]),
434 /// unless the TURN server application knows, through some means not
435 /// specified here, that other applications running on the same host as
436 /// the TURN server application will not be impacted by allocating ports
437 /// outside this range. This condition can often be satisfied by running
438 /// the TURN server application on a dedicated machine and/or by
439 /// arranging that any other applications on the machine allocate ports
440 /// before the TURN server application starts. In any case, the TURN
441 /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well-
442 /// Known Port range) to discourage clients from using TURN to run
443 /// standard services.
444 #[allow(unused_variables)]
445 async fn on_allocated(&self, id: Identifier, username: String, port: u16) {}
446
447 /// channel bind request
448 ///
449 /// [rfc8489](https://tools.ietf.org/html/rfc8489)
450 ///
451 /// If the request is valid, but the server is unable to fulfill the
452 /// request due to some capacity limit or similar, the server replies
453 /// with a 508 (Insufficient Capacity) error.
454 ///
455 /// Otherwise, the server replies with a ChannelBind success response.
456 /// There are no required attributes in a successful ChannelBind
457 /// response.
458 #[allow(unused_variables)]
459 async fn on_channel_bind(&self, id: Identifier, username: String, channel: u16) {}
460
461 /// create permission request
462 ///
463 /// [rfc8489](https://tools.ietf.org/html/rfc8489)
464 ///
465 /// If the request is valid, but the server is unable to fulfill the
466 /// request due to some capacity limit or similar, the server replies
467 /// with a 508 (Insufficient Capacity) error.
468 ///
469 /// Otherwise, the server replies with a ChannelBind success response.
470 /// There are no required attributes in a successful ChannelBind
471 /// response.
472 #[allow(unused_variables)]
473 async fn on_create_permission(&self, id: Identifier, username: String, ports: Vec<u16>) {}
474
475 /// refresh request
476 ///
477 /// [rfc8489](https://tools.ietf.org/html/rfc8489)
478 ///
479 /// If the request is valid, but the server is unable to fulfill the
480 /// request due to some capacity limit or similar, the server replies
481 /// with a 508 (Insufficient Capacity) error.
482 ///
483 /// Otherwise, the server replies with a ChannelBind success response.
484 /// There are no required attributes in a successful ChannelBind
485 /// response.
486 #[allow(unused_variables)]
487 async fn on_refresh(&self, id: Identifier, username: String, lifetime: u32) {}
488
489 /// session closed
490 ///
491 /// Triggered when the session leaves from the turn. Possible reasons: the
492 /// session life cycle has expired, external active deletion, or active
493 /// exit of the session.
494 #[allow(unused_variables)]
495 async fn on_destroy(&self, id: Identifier, username: String) {}
496
497 /// start the turn hooks server
498 ///
499 /// This function will start the turn hooks server on the given server and listen address.
500 async fn start_with_server(
501 self,
502 server: &mut Server,
503 listen: SocketAddr,
504 ) -> Result<(), tonic::transport::Error>
505 where
506 Self: Sized + 'static,
507 {
508 server
509 .add_service(TurnHooksServiceServer::<TurnHooksServerInner<Self>>::new(
510 TurnHooksServerInner(self),
511 ))
512 .serve(listen)
513 .await?;
514
515 Ok(())
516 }
517}