1//! `snowbound-relay`: see `crates/relay/README.md`.
2
3use relay::server::{self, Config};
4use std::{net::TcpListener, process::ExitCode, time::Duration};
5
6const USAGE: &str = "\
7usage: snowbound-relay [OPTION VALUE]...
8
9Each option may also come from the environment as SNOWBOUND_RELAY_<OPTION>, upper case with
10underscores: SNOWBOUND_RELAY_LISTEN=127.0.0.1:23592. Flags win.
11
12 --listen ADDRESS where to listen (127.0.0.1:23592)
13 --trust-forwarded true|false count peers by X-Forwarded-For, behind a proxy (false)
14 --max-connections N (256)
15 --max-connections-per-address N per IPv4 address or IPv6 /64 (128)
16 --max-rooms N (128)
17 --max-room-peers N (64)
18 --max-message BYTES (262144)
19 --queue BYTES waiting to go to one peer before it is dropped (1048576)
20 --idle SECONDS silence before a connection is closed (600)
21 --room-bytes-per-second BYTES (4194304)
22 --joins-per-minute N per address (240)
23 --room-joins-per-minute N (120)
24 --failures-per-minute N wrong codes per address before a lockout (10)
25 --burn-after N wrong codes before a code admits no one new (5)
26 --pending SECONDS for a peer joining a code to meet its owner (20)
27";
28
29fn main() -> ExitCode {
30 let mut listen = String::from("127.0.0.1:23592");
31 let mut config = Config::default();
32 let from_environment = std::env::vars().filter_map(|(key, value)| {
33 let option = key.strip_prefix("SNOWBOUND_RELAY_")?;
34 Some((option.to_lowercase().replace('_', "-"), value))
35 });
36 let mut arguments = std::env::args().skip(1);
37 let mut from_flags = Vec::new();
38 while let Some(flag) = arguments.next() {
39 if flag == "--help" {
40 print!("{USAGE}");
41 return ExitCode::SUCCESS;
42 }
43 let Some(option) = flag.strip_prefix("--") else {
44 eprint!("{USAGE}");
45 return ExitCode::from(2);
46 };
47 let (option, value) = match option.split_once('=') {
48 Some((option, value)) => (option.to_owned(), value.to_owned()),
49 None => match arguments.next() {
50 Some(value) => (option.to_owned(), value),
51 None => {
52 eprintln!("--{option} needs a value\n\n{USAGE}");
53 return ExitCode::from(2);
54 }
55 },
56 };
57 from_flags.push((option, value));
58 }
59 for (option, value) in from_environment.chain(from_flags) {
60 let set = match option.as_str() {
61 "listen" => {
62 listen = value.clone();
63 Ok(())
64 }
65 _ => set(&mut config, &option, &value),
66 };
67 if let Err(error) = set {
68 eprintln!("--{option} {value}: {error}\n\n{USAGE}");
69 return ExitCode::from(2);
70 }
71 }
72 let listener = match TcpListener::bind(&listen) {
73 Ok(listener) => listener,
74 Err(error) => {
75 eprintln!("Cannot listen on {listen}: {error}");
76 return ExitCode::FAILURE;
77 }
78 };
79 match listener.local_addr() {
80 Ok(address) => println!(
81 "snowbound-relay {} listening on {address}",
82 env!("CARGO_PKG_VERSION")
83 ),
84 Err(error) => eprintln!("{error}"),
85 }
86 match server::serve(listener, config) {
87 Ok(()) => ExitCode::SUCCESS,
88 Err(error) => {
89 eprintln!("{error}");
90 ExitCode::FAILURE
91 }
92 }
93}
94
95fn set(config: &mut Config, option: &str, value: &str) -> Result<(), String> {
96 let number = || value.parse::<u64>().map_err(|error| error.to_string());
97 let count = || number().map(|number| number as usize);
98 let small =
99 || number().and_then(|number| u32::try_from(number).map_err(|error| error.to_string()));
100 match option {
101 "trust-forwarded" => {
102 config.trust_forwarded = value.parse().map_err(|_| "true or false".to_owned())?
103 }
104 "max-connections" => config.max_connections = count()?,
105 "max-connections-per-address" => config.max_connections_per_address = count()?,
106 "max-rooms" => config.max_rooms = count()?,
107 "max-room-peers" => config.max_room_peers = count()?,
108 "max-message" => config.max_message = count()?,
109 "queue" => config.queue = count()?,
110 "idle" => config.idle = Duration::from_secs(number()?),
111 "room-bytes-per-second" => config.room_bytes_per_second = number()?.max(1),
112 "joins-per-minute" => config.joins_per_minute = small()?.max(1),
113 "room-joins-per-minute" => config.room_joins_per_minute = small()?.max(1),
114 "failures-per-minute" => config.failures_per_minute = small()?.max(1),
115 "burn-after" => config.burn_after = small()?.max(1),
116 "pending" => config.pending = Duration::from_secs(number()?),
117 _ => return Err("no such option".into()),
118 }
119 Ok(())
120}