1use std::path::Path;
2
3use anyhow::{Context, Result, bail};
4use serde::Deserialize;
5
6#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum ProtocolKind {
9 Tcp,
10 Udp,
11 #[default]
12 Both,
13}
14
15fn default_udp_idle_secs() -> u64 {
16 60
17}
18
19fn default_reconnect_min_secs() -> u64 {
20 1
21}
22
23fn default_reconnect_max_secs() -> u64 {
24 30
25}
26
27#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
32#[serde(rename_all = "lowercase")]
33pub enum Transport {
34 #[default]
40 Tls,
41 Noise,
45}
46
47#[derive(Debug, Clone, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct ForwardRule {
50 pub name: Option<String>,
51 pub listen: String,
52 pub target: String,
53 #[serde(default)]
54 pub protocol: ProtocolKind,
55 #[serde(default = "default_udp_idle_secs")]
56 pub udp_idle_secs: u64,
57 #[serde(default)]
62 pub transport: Option<Transport>,
63 #[serde(default)]
66 pub token: Option<String>,
67}
68
69impl ForwardRule {
70 pub fn label(&self) -> String {
71 self.name
72 .clone()
73 .unwrap_or_else(|| format!("{}->{}", self.listen, self.target))
74 }
75
76 pub fn validate(&self) -> Result<()> {
79 if self.transport == Some(Transport::Noise) && self.token.is_none() {
80 bail!(
81 "forward[{}]: transport = \"noise\" requires a token",
82 self.label()
83 );
84 }
85 Ok(())
86 }
87}
88
89#[derive(Debug, Clone, Deserialize)]
90pub struct ServeTunnel {
91 pub name: String,
92 pub external: String,
93}
94
95#[derive(Debug, Clone, Default, Deserialize)]
96pub struct ServeConfig {
97 pub listen: String,
99 #[serde(default)]
102 pub token: String,
103 #[serde(default)]
104 pub tunnel: Vec<ServeTunnel>,
105 #[serde(default)]
108 pub tenants_file: Option<String>,
109 #[serde(default)]
112 pub sni_listen: Option<String>,
113}
114
115impl ServeConfig {
116 pub fn is_multi_tenant(&self) -> Result<bool> {
119 match (&self.tenants_file, &self.sni_listen) {
120 (Some(_), sni) => {
121 if !self.token.is_empty() || !self.tunnel.is_empty() {
122 bail!(
123 "serve: `tenants_file` cannot be combined with `token` or `[[serve.tunnel]]`"
124 );
125 }
126 if sni.is_none() {
127 bail!("serve: `tenants_file` requires `sni_listen`");
128 }
129 Ok(true)
130 }
131 (None, Some(_)) => bail!("serve: `sni_listen` requires `tenants_file`"),
132 (None, None) => Ok(false),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Deserialize)]
138pub struct ConnectTunnel {
139 pub name: String,
140 pub target: String,
141}
142
143#[derive(Debug, Clone, Deserialize)]
144pub struct ConnectConfig {
145 pub server: String,
147 pub token: String,
148 #[serde(default)]
149 pub tunnel: Vec<ConnectTunnel>,
150 #[serde(default = "default_reconnect_min_secs")]
151 pub reconnect_min_secs: u64,
152 #[serde(default = "default_reconnect_max_secs")]
153 pub reconnect_max_secs: u64,
154}
155
156#[derive(Debug, Clone, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct ClientRule {
159 pub name: Option<String>,
160 pub listen: String,
162 pub server: String,
164 #[serde(default)]
168 pub transport: Transport,
169 #[serde(default)]
171 pub token: Option<String>,
172}
173
174impl ClientRule {
175 pub fn label(&self) -> String {
176 self.name
177 .clone()
178 .unwrap_or_else(|| format!("{}->{}", self.listen, self.server))
179 }
180
181 pub fn validate(&self) -> Result<()> {
182 if self.transport == Transport::Noise && self.token.is_none() {
183 bail!(
184 "client[{}]: transport = \"noise\" requires a token",
185 self.label()
186 );
187 }
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, Deserialize, Default)]
193pub struct FileConfig {
194 #[serde(default)]
195 pub forward: Vec<ForwardRule>,
196 pub serve: Option<ServeConfig>,
197 pub connect: Option<ConnectConfig>,
198 #[serde(default)]
199 pub client: Vec<ClientRule>,
200}
201
202impl FileConfig {
203 pub fn load(path: &Path) -> Result<Self> {
204 let text = std::fs::read_to_string(path)
205 .with_context(|| format!("reading config file {}", path.display()))?;
206 let cfg: FileConfig = toml::from_str(&text)
207 .with_context(|| format!("parsing config file {}", path.display()))?;
208 Ok(cfg)
209 }
210}
211
212pub fn parse_map_flag(raw: &str) -> Result<ForwardRule> {
218 let (rest, (protocol, transport)) = match raw.rsplit_once('/') {
219 Some((rest, proto)) => (rest, parse_protocol(proto)?),
220 None => (raw, (ProtocolKind::Both, None)),
221 };
222 let parts: Vec<&str> = rest.splitn(2, "->").map(str::trim).collect();
223 let (listen_raw, target_raw) = match parts.as_slice() {
224 [listen, target] => (*listen, *target),
225 _ => bail!("--map must look like LISTEN->TARGET[/proto], got: {raw}"),
226 };
227 let listen = normalize_listen(listen_raw)?;
228 Ok(ForwardRule {
229 name: None,
230 listen,
231 target: target_raw.to_string(),
232 protocol,
233 udp_idle_secs: default_udp_idle_secs(),
234 transport,
235 token: None,
236 })
237}
238
239pub fn parse_client_map_flag(raw: &str) -> Result<ClientRule> {
246 let (rest, transport) = match raw.rsplit_once('/') {
247 Some((rest, modifier)) => (rest, parse_client_transport(modifier)?),
248 None => (raw, Transport::Tls),
249 };
250 let parts: Vec<&str> = rest.splitn(2, "->").map(str::trim).collect();
251 let (listen_raw, server_raw) = match parts.as_slice() {
252 [listen, server] => (*listen, *server),
253 _ => bail!("--map must look like LOCAL_LISTEN->SERVER_ADDR[/proto], got: {raw}"),
254 };
255 let listen = normalize_client_listen(listen_raw)?;
256 Ok(ClientRule {
257 name: None,
258 listen,
259 server: server_raw.to_string(),
260 transport,
261 token: None,
262 })
263}
264
265fn parse_client_transport(modifier: &str) -> Result<Transport> {
266 match modifier.to_ascii_lowercase().as_str() {
267 "tls" => Ok(Transport::Tls),
268 "noise" => Ok(Transport::Noise),
269 other => bail!("unknown transport '{other}', expected tls or noise"),
270 }
271}
272
273fn normalize_client_listen(raw: &str) -> Result<String> {
276 if raw.parse::<u16>().is_ok() {
277 Ok(format!("127.0.0.1:{raw}"))
278 } else {
279 Ok(raw.to_string())
280 }
281}
282
283fn parse_protocol(s: &str) -> Result<(ProtocolKind, Option<Transport>)> {
284 let is_transport =
285 |part: &&str| part.eq_ignore_ascii_case("tls") || part.eq_ignore_ascii_case("noise");
286 let transport_parts: Vec<&str> = s.split('+').filter(is_transport).collect();
287 let transport = match transport_parts.as_slice() {
288 [] => None,
289 [t] if t.eq_ignore_ascii_case("tls") => Some(Transport::Tls),
290 [t] if t.eq_ignore_ascii_case("noise") => Some(Transport::Noise),
291 _ => bail!("'{s}' names more than one transport, expected at most one of +tls/+noise"),
292 };
293 let without_transport: Vec<&str> = s.split('+').filter(|p| !is_transport(p)).collect();
294 let protocol = match without_transport.join("+").to_ascii_lowercase().as_str() {
295 "" if transport.is_some() => ProtocolKind::Tcp,
296 "tcp" => ProtocolKind::Tcp,
297 "udp" => ProtocolKind::Udp,
298 "both" | "tcp+udp" | "udp+tcp" => ProtocolKind::Both,
299 other => bail!(
300 "unknown protocol '{other}', expected tcp, udp, both, optionally with a +tls/+noise modifier"
301 ),
302 };
303 if transport.is_some() && protocol != ProtocolKind::Tcp {
304 bail!(
305 "+tls/+noise only support the tcp protocol, got '{s}' (UDP forwarding cannot be wrapped in a transport)"
306 );
307 }
308 Ok((protocol, transport))
309}
310
311fn normalize_listen(raw: &str) -> Result<String> {
313 if raw.parse::<u16>().is_ok() {
314 Ok(format!("0.0.0.0:{raw}"))
315 } else {
316 Ok(raw.to_string())
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn map_flag_without_proto_is_plaintext_both() {
326 let rule = parse_map_flag("8080->10.0.0.5:80").unwrap();
327 assert_eq!(rule.protocol, ProtocolKind::Both);
328 assert_eq!(rule.transport, None);
329 }
330
331 #[test]
332 fn map_flag_bare_tls_means_tls_tcp() {
333 let rule = parse_map_flag("8080->10.0.0.5:80/tls").unwrap();
334 assert_eq!(rule.protocol, ProtocolKind::Tcp);
335 assert_eq!(rule.transport, Some(Transport::Tls));
336 }
337
338 #[test]
339 fn map_flag_tcp_plus_tls_means_tls_tcp() {
340 let rule = parse_map_flag("8080->10.0.0.5:80/tcp+tls").unwrap();
341 assert_eq!(rule.protocol, ProtocolKind::Tcp);
342 assert_eq!(rule.transport, Some(Transport::Tls));
343 }
344
345 #[test]
346 fn map_flag_bare_noise_means_noise_tcp() {
347 let rule = parse_map_flag("8080->10.0.0.5:80/noise").unwrap();
348 assert_eq!(rule.protocol, ProtocolKind::Tcp);
349 assert_eq!(rule.transport, Some(Transport::Noise));
350 }
351
352 #[test]
353 fn map_flag_both_transports_is_rejected() {
354 assert!(parse_map_flag("8080->10.0.0.5:80/tcp+tls+noise").is_err());
355 }
356
357 #[test]
358 fn map_flag_udp_plus_tls_is_rejected() {
359 assert!(parse_map_flag("5353->10.0.0.5:53/udp+tls").is_err());
360 }
361
362 #[test]
363 fn map_flag_both_plus_tls_is_rejected() {
364 assert!(parse_map_flag("8080->10.0.0.5:80/both+tls").is_err());
365 }
366
367 #[test]
368 fn client_map_flag_without_modifier_defaults_to_tls() {
369 let rule = parse_client_map_flag("1080->10.0.0.5:8443").unwrap();
370 assert_eq!(rule.transport, Transport::Tls);
371 }
372
373 #[test]
374 fn client_map_flag_noise_modifier_selects_noise() {
375 let rule = parse_client_map_flag("1080->10.0.0.5:8443/noise").unwrap();
376 assert_eq!(rule.transport, Transport::Noise);
377 }
378
379 #[test]
380 fn client_map_flag_unknown_modifier_is_rejected() {
381 assert!(parse_client_map_flag("1080->10.0.0.5:8443/quic").is_err());
382 }
383}