1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//! config types.
/// Configuration for running a BootstrapSrv.
#[derive(Debug)]
pub struct Config {
/// Worker thread count.
///
/// This server is currently built using blocking io and filesystem
/// storage. It is therefore beneficial to have more worker threads
/// than system cpus, since the workers will be bound on io, not
/// on cpu. On the other hand, increasing this will also increase
/// memory overhead and tempfile handle count, so we don't want to
/// set it too high.
///
/// Defaults:
/// - `testing = 2`
/// - `production = 4 * cpu_count`
pub worker_thread_count: usize,
/// The maximum agent info entry count per space.
///
/// All entries will be returned in a get space request, so
/// this count should be low enough to reasonably send this response
/// over http without needing pagination.
///
/// Defaults:
/// - `testing = 32`
/// - `production = 32`
pub max_entries_per_space: usize,
/// The duration worker threads will block waiting for incoming connections
/// before checking to see if the server is shutting down.
///
/// Setting this very high will cause ctrl-c / server shutdown to be slow.
/// Setting this very low will increase cpu overhead (and in extreme
/// conditions, could cause a lack of responsiveness in the server).
///
/// Defaults:
/// - `testing = 10ms`
/// - `production = 2s`
pub request_listen_duration: std::time::Duration,
/// The address(es) at which to listen.
///
/// Defaults:
/// - `testing = "[127.0.0.1:0]"`
/// - `production = "[0.0.0.0:443, [::]:443]"`
pub listen_address_list: Vec<std::net::SocketAddr>,
/// The interval at which expired agents are purged from the cache.
///
/// This is a fairly expensive operation that requires iterating
/// through every registered space and loading all the infos off the disk,
/// so it should not be undertaken too frequently.
///
/// Defaults:
/// - `testing = 10s`
/// - `production = 60s`
pub prune_interval: std::time::Duration,
/// The path to a TLS certificate file.
///
/// Must be provided when `tls_cert` is provided.
///
/// Default:
/// - `None`
pub tls_cert: Option<std::path::PathBuf>,
/// The path to a TLS key file.
///
/// Must be provided when `tls_cert` is provided.
///
/// Default:
/// - `None`
pub tls_key: Option<std::path::PathBuf>,
/// The socket address on which the QUIC Address Discovery (QAD) server
/// should bind. When set (along with TLS cert/key), a QUIC endpoint is
/// started that lets iroh clients discover their public address.
///
/// Default: `None` (disabled)
#[cfg(feature = "iroh-relay")]
pub quic_bind_addr: Option<std::net::SocketAddr>,
/// Sustained inbound byte rate per relay client connection, in bytes
/// per second.
///
/// `None` disables the limiter entirely. Setting this without
/// [`Self::relay_client_rx_burst_bytes`] derives the burst as
/// `(bytes_per_second / 10).max(1)` to match iroh's own default.
///
/// Default: `None` in both `testing()` and `production()`.
#[cfg(feature = "iroh-relay")]
pub relay_client_rx_bytes_per_second: Option<std::num::NonZeroU32>,
/// Maximum allowed inbound burst per relay client connection, in bytes.
///
/// Has effect only when [`Self::relay_client_rx_bytes_per_second`] is
/// also `Some`. Setting this alone is rejected at `BootstrapSrv::new`
/// time.
///
/// Default: `None` in both `testing()` and `production()`.
#[cfg(feature = "iroh-relay")]
pub relay_client_rx_burst_bytes: Option<std::num::NonZeroU32>,
/// Disable the relay server.
pub no_relay_server: bool,
/// The allowed origins for CORS requests.
///
/// If `None`, defaults to allowing any origin.
pub allowed_origins: Option<Vec<String>>,
/// Authentication configuration.
///
/// This is independent of the relay implementation.
pub auth: crate::auth::AuthConfig,
}
impl Config {
/// Get a boot_srv config suitable for testing.
pub fn testing() -> Self {
Self {
worker_thread_count: 2,
max_entries_per_space: 32,
request_listen_duration: std::time::Duration::from_millis(10),
listen_address_list: vec![
(std::net::Ipv4Addr::LOCALHOST, 0).into(),
],
prune_interval: std::time::Duration::from_secs(10),
tls_cert: None,
tls_key: None,
#[cfg(feature = "iroh-relay")]
quic_bind_addr: Some((std::net::Ipv4Addr::LOCALHOST, 0).into()),
#[cfg(feature = "iroh-relay")]
relay_client_rx_bytes_per_second: None,
#[cfg(feature = "iroh-relay")]
relay_client_rx_burst_bytes: None,
no_relay_server: false,
allowed_origins: None,
auth: crate::auth::AuthConfig::default(),
}
}
/// Get a boot_srv config suitable for production.
pub fn production() -> Self {
Self {
worker_thread_count: num_cpus::get() * 4,
max_entries_per_space: 32,
request_listen_duration: std::time::Duration::from_secs(2),
listen_address_list: vec![
(std::net::Ipv4Addr::UNSPECIFIED, 443).into(),
(std::net::Ipv6Addr::UNSPECIFIED, 443).into(),
],
prune_interval: std::time::Duration::from_secs(60),
tls_cert: None,
tls_key: None,
#[cfg(feature = "iroh-relay")]
quic_bind_addr: Some(
(std::net::Ipv6Addr::UNSPECIFIED, 7842).into(),
),
#[cfg(feature = "iroh-relay")]
relay_client_rx_bytes_per_second: None,
#[cfg(feature = "iroh-relay")]
relay_client_rx_burst_bytes: None,
no_relay_server: false,
allowed_origins: None,
auth: crate::auth::AuthConfig::default(),
}
}
}
#[cfg(feature = "iroh-relay")]
impl Config {
/// Resolve the per-connection inbound byte rate limit from the two
/// `relay_client_rx_*` fields.
///
/// Returns:
/// - `Ok(None)` when no limit is configured.
/// - `Ok(Some(_))` with the resolved [`crate::iroh_relay_axum::RelayClientRxRateLimit`]
/// when a sustained rate is set; burst defaults to `(bps / 10).max(1)` if
/// not set explicitly.
/// - `Err(_)` when only the burst is set but not the sustained rate.
pub fn resolve_relay_rate_limit(
&self,
) -> Result<
Option<crate::iroh_relay_axum::RelayClientRxRateLimit>,
std::io::Error,
> {
match (
self.relay_client_rx_bytes_per_second,
self.relay_client_rx_burst_bytes,
) {
(None, None) => Ok(None),
(None, Some(_)) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"relay_client_rx_burst_bytes is set but \
relay_client_rx_bytes_per_second is not; both required \
(burst alone has no meaning)",
)),
// The token bucket uses a 100ms refill window, so
// refill = bytes_per_second * 100 / 1000. Rates below 10 B/s
// produce refill = 0, which iroh's Bucket::new rejects with
// InvalidBucketConfig and would panic the connection task.
(Some(bps), _) if bps.get() < 10 => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"relay_client_rx_bytes_per_second must be >= 10 \
(the 100ms refill window requires at least 1 byte per tick)",
)),
(Some(bps), Some(burst)) => {
Ok(Some(crate::iroh_relay_axum::RelayClientRxRateLimit {
bytes_per_second: bps,
burst_bytes: burst,
}))
}
(Some(bps), None) => {
let derived = bps.get() / 10;
let burst = std::num::NonZeroU32::new(derived)
.expect("bps >= 10 ensures derived burst >= 1");
Ok(Some(crate::iroh_relay_axum::RelayClientRxRateLimit {
bytes_per_second: bps,
burst_bytes: burst,
}))
}
}
}
}
#[cfg(all(test, feature = "iroh-relay"))]
mod resolve_rate_limit_tests {
use super::*;
use std::num::NonZeroU32;
fn cfg() -> Config {
Config::testing()
}
#[test]
fn none_when_unset() {
let c = cfg();
assert!(c.resolve_relay_rate_limit().unwrap().is_none());
}
#[test]
fn err_when_only_burst_set() {
let mut c = cfg();
c.relay_client_rx_burst_bytes = NonZeroU32::new(1024);
assert!(c.resolve_relay_rate_limit().is_err());
}
#[test]
fn explicit_burst_used_as_is() {
let mut c = cfg();
c.relay_client_rx_bytes_per_second = NonZeroU32::new(1000);
c.relay_client_rx_burst_bytes = NonZeroU32::new(2048);
let r = c.resolve_relay_rate_limit().unwrap().unwrap();
assert_eq!(r.bytes_per_second.get(), 1000);
assert_eq!(r.burst_bytes.get(), 2048);
}
#[test]
fn burst_defaults_to_one_tenth_when_unset() {
let mut c = cfg();
c.relay_client_rx_bytes_per_second = NonZeroU32::new(1000);
let r = c.resolve_relay_rate_limit().unwrap().unwrap();
assert_eq!(r.bytes_per_second.get(), 1000);
assert_eq!(r.burst_bytes.get(), 100);
}
#[test]
fn err_for_rate_below_refill_window_minimum() {
let mut c = cfg();
c.relay_client_rx_bytes_per_second = NonZeroU32::new(1);
assert!(c.resolve_relay_rate_limit().is_err());
c.relay_client_rx_bytes_per_second = NonZeroU32::new(9);
assert!(c.resolve_relay_rate_limit().is_err());
}
#[test]
fn min_valid_rate_yields_burst_of_one() {
let mut c = cfg();
c.relay_client_rx_bytes_per_second = NonZeroU32::new(10);
let r = c.resolve_relay_rate_limit().unwrap().unwrap();
assert_eq!(r.burst_bytes.get(), 1);
}
}