turn_server/
config.rs

1use std::{collections::HashMap, fs::read_to_string, net::SocketAddr, str::FromStr};
2
3use anyhow::anyhow;
4use clap::Parser;
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7
8#[repr(C)]
9#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum Transport {
12    TCP = 0,
13    UDP = 1,
14}
15
16impl FromStr for Transport {
17    type Err = anyhow::Error;
18
19    fn from_str(value: &str) -> Result<Self, Self::Err> {
20        Ok(match value {
21            "udp" => Self::UDP,
22            "tcp" => Self::TCP,
23            _ => return Err(anyhow!("unknown transport: {value}")),
24        })
25    }
26}
27
28#[derive(Deserialize, Serialize, Debug, Clone)]
29pub struct Interface {
30    pub transport: Transport,
31    /// turn server listen address
32    pub bind: SocketAddr,
33    /// external address
34    ///
35    /// specify the node external address and port.
36    /// for the case of exposing the service to the outside,
37    /// you need to manually specify the server external IP
38    /// address and service listening port.
39    pub external: SocketAddr,
40}
41
42impl FromStr for Interface {
43    type Err = anyhow::Error;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        let (transport, addrs) = s
47            .split('@')
48            .collect_tuple()
49            .ok_or_else(|| anyhow!("invalid interface transport: {}", s))?;
50
51        let (bind, external) = addrs
52            .split('/')
53            .collect_tuple()
54            .ok_or_else(|| anyhow!("invalid interface address: {}", s))?;
55
56        Ok(Interface {
57            external: external.parse::<SocketAddr>()?,
58            bind: bind.parse::<SocketAddr>()?,
59            transport: transport.parse()?,
60        })
61    }
62}
63
64#[derive(Deserialize, Debug)]
65pub struct Turn {
66    /// turn server realm
67    ///
68    /// specify the domain where the server is located.
69    /// for a single node, this configuration is fixed,
70    /// but each node can be configured as a different domain.
71    /// this is a good idea to divide the nodes by namespace.
72    #[serde(default = "Turn::realm")]
73    pub realm: String,
74
75    /// turn server listen interfaces
76    ///
77    /// The address and port to which the UDP Server is bound. Multiple
78    /// addresses can be bound at the same time. The binding address supports
79    /// ipv4 and ipv6.
80    #[serde(default = "Turn::interfaces")]
81    pub interfaces: Vec<Interface>,
82}
83
84impl Turn {
85    pub fn get_externals(&self) -> Vec<SocketAddr> {
86        self.interfaces.iter().map(|item| item.external).collect()
87    }
88}
89
90impl Turn {
91    fn realm() -> String {
92        "localhost".to_string()
93    }
94
95    fn interfaces() -> Vec<Interface> {
96        vec![]
97    }
98}
99
100impl Default for Turn {
101    fn default() -> Self {
102        Self {
103            realm: Self::realm(),
104            interfaces: Self::interfaces(),
105        }
106    }
107}
108
109#[derive(Deserialize, Debug)]
110pub struct Api {
111    /// api bind
112    ///
113    /// This option specifies the http server binding address used to control
114    /// the turn server.
115    ///
116    /// Warn: This http server does not contain any means of authentication,
117    /// and sensitive information and dangerous operations can be obtained
118    /// through this service, please do not expose it directly to an unsafe
119    /// environment.
120    #[serde(default = "Api::bind")]
121    pub bind: SocketAddr,
122}
123
124impl Api {
125    fn bind() -> SocketAddr {
126        "127.0.0.1:3000".parse().unwrap()
127    }
128}
129
130impl Default for Api {
131    fn default() -> Self {
132        Self { bind: Self::bind() }
133    }
134}
135
136#[derive(Deserialize, Debug, Clone, Copy)]
137#[serde(rename_all = "lowercase")]
138pub enum LogLevel {
139    Error,
140    Warn,
141    Info,
142    Debug,
143    Trace,
144}
145
146impl FromStr for LogLevel {
147    type Err = String;
148
149    fn from_str(value: &str) -> Result<Self, Self::Err> {
150        Ok(match value {
151            "trace" => Self::Trace,
152            "debug" => Self::Debug,
153            "info" => Self::Info,
154            "warn" => Self::Warn,
155            "error" => Self::Error,
156            _ => return Err(format!("unknown log level: {value}")),
157        })
158    }
159}
160
161impl Default for LogLevel {
162    fn default() -> Self {
163        Self::Info
164    }
165}
166
167impl LogLevel {
168    pub fn as_level(&self) -> log::Level {
169        match *self {
170            Self::Error => log::Level::Error,
171            Self::Debug => log::Level::Debug,
172            Self::Trace => log::Level::Trace,
173            Self::Warn => log::Level::Warn,
174            Self::Info => log::Level::Info,
175        }
176    }
177}
178
179#[derive(Deserialize, Debug, Default)]
180pub struct Log {
181    /// log level
182    ///
183    /// An enum representing the available verbosity levels of the logger.
184    #[serde(default)]
185    pub level: LogLevel,
186}
187
188#[derive(Deserialize, Debug, Default)]
189pub struct Auth {
190    /// static user password
191    ///
192    /// This option can be used to specify the
193    /// static identity authentication information used by the turn server for
194    /// verification. Note: this is a high-priority authentication method, turn
195    /// The server will try to use static authentication first, and then use
196    /// external control service authentication.
197    #[serde(default)]
198    pub static_credentials: HashMap<String, String>,
199    /// Static authentication key value (string) that applies only to the TURN
200    /// REST API.
201    ///
202    /// If set, the turn server will not request external services via the HTTP
203    /// Hooks API to obtain the key.
204    pub static_auth_secret: Option<String>,
205}
206
207#[derive(Deserialize, Debug)]
208pub struct Config {
209    #[serde(default)]
210    pub turn: Turn,
211    #[serde(default)]
212    pub api: Api,
213    #[serde(default)]
214    pub log: Log,
215    #[serde(default)]
216    pub auth: Auth,
217}
218
219#[derive(Parser, Debug)]
220#[command(
221    about = env!("CARGO_PKG_DESCRIPTION"),
222    version = env!("CARGO_PKG_VERSION"),
223    author = env!("CARGO_PKG_AUTHORS"),
224)]
225struct Cli {
226    /// Specify the configuration file path
227    ///
228    /// Example: --config /etc/turn-rs/config.toml
229    #[arg(long, short)]
230    config: Option<String>,
231    /// Static user password
232    ///
233    /// Example: --auth-static-credentials test=test
234    #[arg(long, value_parser = Cli::parse_credential)]
235    auth_static_credentials: Option<Vec<(String, String)>>,
236    /// Static authentication key value (string) that applies only to the TURN
237    /// REST API
238    #[arg(long)]
239    auth_static_auth_secret: Option<String>,
240    /// An enum representing the available verbosity levels of the logger
241    #[arg(
242        long,
243        value_parser = clap::value_parser!(LogLevel),
244    )]
245    log_level: Option<LogLevel>,
246    /// This option specifies the http server binding address used to control
247    /// the turn server
248    #[arg(long)]
249    api_bind: Option<SocketAddr>,
250    /// TURN server realm
251    #[arg(long)]
252    turn_realm: Option<String>,
253    /// TURN server listen interfaces
254    ///
255    /// Example: --turn-interfaces udp@127.0.0.1:3478/127.0.0.1:3478
256    #[arg(long)]
257    turn_interfaces: Option<Vec<Interface>>,
258}
259
260impl Cli {
261    // [username]:[password]
262    fn parse_credential(s: &str) -> Result<(String, String), anyhow::Error> {
263        let (username, password) = s
264            .split('=')
265            .collect_tuple()
266            .ok_or_else(|| anyhow!("invalid credential str: {}", s))?;
267        Ok((username.to_string(), password.to_string()))
268    }
269}
270
271impl Config {
272    /// Load configure from config file and command line parameters.
273    ///
274    /// Load command line parameters, if the configuration file path is
275    /// specified, the configuration is read from the configuration file,
276    /// otherwise the default configuration is used.
277    pub fn load() -> anyhow::Result<Self> {
278        let cli = Cli::parse();
279        let mut config = toml::from_str::<Self>(
280            &cli.config
281                .and_then(|path| read_to_string(path).ok())
282                .unwrap_or("".to_string()),
283        )?;
284
285        // Command line arguments have a high priority and override configuration file
286        // options; here they are used to replace the configuration parsed out of the
287        // configuration file.
288        {
289            if let Some(credentials) = cli.auth_static_credentials {
290                for (k, v) in credentials {
291                    config.auth.static_credentials.insert(k, v);
292                }
293            }
294
295            if let Some(secret) = cli.auth_static_auth_secret {
296                config.auth.static_auth_secret.replace(secret);
297            }
298
299            if let Some(level) = cli.log_level {
300                config.log.level = level;
301            }
302
303            if let Some(bind) = cli.api_bind {
304                config.api.bind = bind;
305            }
306
307            if let Some(realm) = cli.turn_realm {
308                config.turn.realm = realm;
309            }
310
311            if let Some(interfaces) = cli.turn_interfaces {
312                for interface in interfaces {
313                    config.turn.interfaces.push(interface);
314                }
315            }
316        }
317
318        // Filters out transport protocols that are not enabled.
319        {
320            let mut interfaces = Vec::with_capacity(config.turn.interfaces.len());
321
322            {
323                for it in &config.turn.interfaces {
324                    #[cfg(feature = "udp")]
325                    if it.transport == Transport::UDP {
326                        interfaces.push(it.clone());
327                    }
328
329                    #[cfg(feature = "tcp")]
330                    if it.transport == Transport::TCP {
331                        interfaces.push(it.clone());
332                    }
333                }
334            }
335
336            config.turn.interfaces = interfaces;
337        }
338
339        Ok(config)
340    }
341}