dittolive_ditto/transport/transport_config.rs
1use std::{collections::HashSet, path::PathBuf, time::Duration};
2
3use serde::{Deserialize, Serialize};
4use serde_with::serde_as;
5
6pub const NO_PREFERRED_ROUTE_HINT: u32 = 0;
7
8/// A configuration object specifying which network transports Ditto should use
9/// to sync data.
10///
11/// A `Ditto` object comes with a default transport configuration where the
12/// stable peer-to-peer transports supported on the current platform are
13/// enabled. Reliable UDP multicast is excluded from this default and must be
14/// enabled explicitly. You can customize the configuration by initializing a
15/// `TransportConfig`, adjusting its properties, and supplying it to
16/// `set_transport_config()` on `Ditto`.
17///
18/// When you initialize a `TransportConfig` yourself it starts with all
19/// transports disabled. You must enable each one directly.
20///
21/// Peer-to-peer transports will automatically discover peers in the vicinity
22/// and create connections without any configuration. These are configured
23/// inside the `peer_to_peer` property. To turn each one on, set its `enabled`
24/// property to `true`.
25///
26/// To connect to a peer at a known location, such as a Ditto Big Peer, add its
27/// address inside the `connect` configuration. These are either `host:port`
28/// strings for raw TCP sync, or a `wss://…` URL for websockets.
29///
30/// The `listen` configurations are for specific less common data sync
31/// scenarios. Please read the documentation on the Ditto website for examples.
32/// Incorrect use of `listen` can result in insecure configurations.
33#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
34#[serde(default)]
35pub struct TransportConfig {
36 /// Configure Bluetooth and LAN transports.
37 pub peer_to_peer: PeerToPeer,
38
39 /// Configure TCP and WebSocket outbound transports.
40 pub connect: Connect,
41
42 /// Configure TCP and HTTP inbound transports.
43 pub listen: Listen,
44
45 /// Set peer-global transport settings.
46 pub global: Global,
47}
48
49impl TransportConfig {
50 /// Create a new [`TransportConfig`] with default settings.
51 pub fn new() -> Self {
52 Self {
53 peer_to_peer: PeerToPeer {
54 bluetooth_le: BluetoothLEConfig::new(),
55 lan: LanConfig::new(),
56 awdl: AwdlConfig::new(),
57 wifi_aware: WifiAwareConfig::new(),
58 multicast_beta: MulticastBetaConfig::new(),
59 },
60 connect: Connect {
61 tcp_servers: HashSet::new(),
62 websocket_urls: HashSet::new(),
63 retry_interval: Duration::from_secs(5),
64 },
65 listen: Listen {
66 tcp: TcpListenConfig::new(),
67 http: HttpListenConfig::new(),
68 },
69 global: Global {
70 sync_group: 0,
71 routing_hint: NO_PREFERRED_ROUTE_HINT,
72 },
73 }
74 }
75
76 /// Enables all stable peer-to-peer transports in this config.
77 ///
78 /// This method does not affect the reliable UDP multicast transport.
79 /// Configure [`PeerToPeer::multicast_beta`] separately.
80 pub fn enable_all_peer_to_peer(&mut self) {
81 self.peer_to_peer.bluetooth_le.enabled = true;
82 self.peer_to_peer.lan.enabled = true;
83 self.peer_to_peer.awdl.enabled = true;
84 self.peer_to_peer.wifi_aware.enabled = true;
85 }
86}
87
88/// Configure peer-to-peer transports such as Bluetooth and LAN.
89#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
90#[serde(default)]
91pub struct PeerToPeer {
92 /// Bluetooth transport configurations.
93 pub bluetooth_le: BluetoothLEConfig,
94
95 /// LAN transport configurations.
96 pub lan: LanConfig,
97
98 /// AWDL transport configurations.
99 pub awdl: AwdlConfig,
100
101 /// Wi-Fi Aware transport configurations.
102 pub wifi_aware: WifiAwareConfig,
103
104 /// Private beta configuration for reliable UDP multicast transport. See
105 /// [`MulticastBetaConfig`].
106 #[serde(rename = "multicast")]
107 pub multicast_beta: MulticastBetaConfig,
108}
109
110/// Configure outbound transports such as TCP and WebSocket dialing.
111#[serde_as]
112#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
113#[serde(default)]
114pub struct Connect {
115 /// A set of TCP servers to attempt connection to.
116 pub tcp_servers: HashSet<String>,
117
118 /// A set of websocket servers to attempt connection to.
119 pub websocket_urls: HashSet<String>,
120
121 /// The retry interval between failed connection attempts. For
122 /// cross-compatibility, this must be less than 2^32 - 1 milliseconds.
123 #[serde_as(as = "::serde_with::DurationMilliSeconds<u64>")]
124 pub retry_interval: Duration,
125}
126
127/// Configure inbound transports such as TCP and HTTP servers.
128#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
129#[serde(default)]
130pub struct Listen {
131 /// Configure inbound TCP transports.
132 pub tcp: TcpListenConfig,
133
134 /// Configure inbound HTTP transports.
135 pub http: HttpListenConfig,
136}
137
138/// Settings not associated with any specific type of transport.
139#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
140#[serde(default)]
141pub struct Global {
142 /// The sync group for this device.
143 ///
144 /// When peer-to-peer transports are enabled, all devices with the same App
145 /// ID will normally form an interconnected mesh network. In some
146 /// situations it may be desirable to have distinct groups of devices
147 /// within the same app, so that connections will only be formed within
148 /// each group. The `sync_group` parameter changes that group
149 /// membership. A device can only ever be in one sync group, which
150 /// by default is group 0. Up to 2^32 distinct group numbers can be used in
151 /// an app.
152 ///
153 /// This is an optimization, not a security control. If a connection is
154 /// created manually, such as by specifying a `connect` transport, then
155 /// devices from different sync groups will still sync as normal. If
156 /// two groups of devices are intended to have access to different data
157 /// sets, this must be enforced using Ditto's permissions system.
158 pub sync_group: u32,
159
160 /// The routing hint for this device.
161 ///
162 /// A routing hint is a performance tuning option which can improve the performance of
163 /// applications that use large collections. Ditto will make a best effort to co-locate data
164 /// for the same routing key. In most circumstances, this should substantially improve
165 /// responsiveness of the Ditto Cloud.
166 ///
167 /// The value of the routing hint is application specific - you are free to choose any value.
168 /// Devices which you expect to operate on much the same data should be configured to
169 /// use the same value.
170 ///
171 /// A routing hint does not partition data. The value of the routing hint will not affect the
172 /// data returned for a query. The routing hint only improves the efficiency of the Cloud's
173 /// ability to satisfy the query.
174 pub routing_hint: u32,
175}
176
177/// Configure inbound HTTP transports.
178#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
179#[serde(default)]
180pub struct HttpListenConfig {
181 /// Whether inbound HTTP is enabled.
182 pub enabled: bool,
183
184 /// The IP address on which to bind a listening HTTP server.
185 pub interface_ip: String,
186
187 /// The port on which to bind a listening HTTP server.
188 pub port: u16,
189
190 /// Whether to enable Websocket sync on this transport.
191 pub websocket_sync: bool,
192
193 /// Optional path to a TLS key file.
194 pub tls_key_path: Option<PathBuf>,
195
196 /// Optional path to a TLS certificate file.
197 pub tls_certificate_path: Option<PathBuf>,
198}
199
200impl HttpListenConfig {
201 /// Create a new [`HttpListenConfig`] with default settings.
202 pub fn new() -> Self {
203 Self {
204 enabled: false,
205 interface_ip: "[::]".to_string(),
206 port: 80,
207 websocket_sync: true,
208 tls_key_path: None,
209 tls_certificate_path: None,
210 }
211 }
212}
213
214/// Configure inbound TCP transports.
215#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
216#[serde(default)]
217pub struct TcpListenConfig {
218 /// Whether inbound TCP is enabled.
219 pub enabled: bool,
220
221 /// The address to bind a TCP listener, such as `0.0.0.0`.
222 pub interface_ip: String,
223
224 /// The port to bind the TCP listener to.
225 pub port: u16,
226}
227
228impl TcpListenConfig {
229 /// Create a new [`TcpListenConfig`] with default settings.
230 pub fn new() -> Self {
231 Self {
232 enabled: false,
233 interface_ip: "[::]".to_string(),
234 port: 4040,
235 }
236 }
237}
238
239/// Configure the Bluetooth Low-Energy transport.
240#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
241#[serde(default)]
242pub struct BluetoothLEConfig {
243 /// Whether to enable the Bluetooth Low-Energy transport.
244 pub enabled: bool,
245}
246
247impl BluetoothLEConfig {
248 /// Create a new [`BluetoothLEConfig`] with default settings.
249 pub fn new() -> Self {
250 Self { enabled: false }
251 }
252}
253
254/// Configure the Local Area Network (LAN) transport.
255#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
256#[serde(default)]
257pub struct LanConfig {
258 /// Whether to enable the LAN transport.
259 pub enabled: bool,
260
261 /// Whether to enable mDNS peer discovery.
262 pub mdns_enabled: bool,
263
264 /// Whether to enable multicast.
265 pub multicast_enabled: bool,
266}
267
268impl LanConfig {
269 /// Create a new [`LanConfig`] with default settings.
270 pub fn new() -> Self {
271 Self {
272 enabled: false,
273 mdns_enabled: true,
274 multicast_enabled: true,
275 }
276 }
277}
278
279/// Configuration for Apple Wireless Direct Link (AWDL) network transport.
280/// Not supported on all platforms.
281#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
282#[serde(default)]
283pub struct AwdlConfig {
284 /// Whether to enable the AWDL transport.
285 pub enabled: bool,
286}
287
288impl AwdlConfig {
289 /// Create a new [`AwdlConfig`] with default settings.
290 pub fn new() -> Self {
291 Self { enabled: false }
292 }
293}
294
295/// Configuration for Wi-Fi Aware network transport.
296/// Not supported on all platforms.
297#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
298#[serde(default)]
299pub struct WifiAwareConfig {
300 /// Whether to enable the Wi-Fi Aware transport.
301 pub enabled: bool,
302}
303
304impl WifiAwareConfig {
305 /// Create a new [`WifiAwareConfig`] with default settings.
306 pub fn new() -> Self {
307 Self { enabled: false }
308 }
309}
310
311/// Configuration for reliable UDP multicast transport.
312///
313/// Use this configuration to enable peer-to-peer data synchronization over a
314/// multicast group.
315///
316/// This does not configure multicast-based LAN discovery. Use
317/// [`LanConfig::multicast_enabled`] for that setting.
318///
319/// On iOS and Android, configure this transport while sync is stopped. Changes
320/// requested while sync is active take effect after sync is stopped and
321/// successfully started again, when Ditto validates platform prerequisites.
322///
323/// This feature is in private beta and should only be used in coordination
324/// with Ditto support.
325#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
326#[serde(default)]
327pub struct MulticastBetaConfig {
328 /// Whether reliable UDP multicast transport is enabled. This private beta
329 /// feature should only be enabled in coordination with Ditto support.
330 pub enabled: bool,
331 /// IPv4 multicast group address (e.g. `224.1.2.3`). All peers in the
332 /// mesh must use the same address.
333 pub group_address: String,
334 /// UDP port. All peers must use the same port.
335 pub port: u16,
336 /// Network interface name to bind to (e.g. `en0`, `wlan0`). When `None`,
337 /// the OS picks one — typically the default route interface.
338 pub interface: Option<String>,
339}
340
341impl MulticastBetaConfig {
342 /// Create a new [`MulticastBetaConfig`] with default settings.
343 pub fn new() -> Self {
344 Self::default()
345 }
346}
347
348impl Default for MulticastBetaConfig {
349 fn default() -> Self {
350 Self {
351 enabled: false,
352 group_address: "224.1.2.3".to_string(),
353 port: 6003,
354 interface: None,
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn awdl_config_default_disabled() {
365 assert!(!AwdlConfig::default().enabled);
366 assert!(!AwdlConfig::new().enabled);
367 }
368
369 #[test]
370 fn wifi_aware_config_default_disabled() {
371 assert!(!WifiAwareConfig::default().enabled);
372 assert!(!WifiAwareConfig::new().enabled);
373 }
374
375 #[test]
376 fn enable_all_peer_to_peer_enables_all_transports() {
377 let mut config = TransportConfig::new();
378 config.enable_all_peer_to_peer();
379 assert!(config.peer_to_peer.bluetooth_le.enabled);
380 assert!(config.peer_to_peer.lan.enabled);
381 assert!(config.peer_to_peer.awdl.enabled);
382 assert!(config.peer_to_peer.wifi_aware.enabled);
383 }
384
385 #[test]
386 fn enable_all_peer_to_peer_does_not_affect_multicast() {
387 for multicast_enabled in [false, true] {
388 let mut config = TransportConfig::new();
389 config.peer_to_peer.multicast_beta.enabled = multicast_enabled;
390
391 config.enable_all_peer_to_peer();
392
393 assert_eq!(
394 config.peer_to_peer.multicast_beta.enabled,
395 multicast_enabled
396 );
397 }
398 }
399
400 #[test]
401 fn awdl_config_serde_roundtrip() {
402 let original = AwdlConfig { enabled: true };
403 let json = serde_json::to_string(&original).unwrap();
404 let decoded: AwdlConfig = serde_json::from_str(&json).unwrap();
405 assert_eq!(original, decoded);
406 }
407
408 #[test]
409 fn wifi_aware_config_serde_roundtrip() {
410 let original = WifiAwareConfig { enabled: true };
411 let json = serde_json::to_string(&original).unwrap();
412 let decoded: WifiAwareConfig = serde_json::from_str(&json).unwrap();
413 assert_eq!(original, decoded);
414 }
415
416 #[test]
417 fn peer_to_peer_missing_fields_default_to_disabled() {
418 let json = r#"{"bluetooth_le":{"enabled":true},"lan":{"enabled":true,"mdns_enabled":true,"multicast_enabled":true}}"#;
419 let peer_to_peer: PeerToPeer = serde_json::from_str(json).unwrap();
420 assert!(!peer_to_peer.awdl.enabled);
421 assert!(!peer_to_peer.wifi_aware.enabled);
422 }
423
424 #[test]
425 fn multicast_beta_config_default_disabled() {
426 let cfg = MulticastBetaConfig::default();
427 assert!(!cfg.enabled);
428 assert_eq!(cfg.group_address, "224.1.2.3");
429 assert_eq!(cfg.port, 6003);
430 assert!(cfg.interface.is_none());
431 }
432
433 #[test]
434 fn multicast_beta_config_new_equals_default() {
435 assert_eq!(MulticastBetaConfig::new(), MulticastBetaConfig::default());
436 }
437
438 #[test]
439 fn multicast_beta_config_serde_roundtrip() {
440 let original = MulticastBetaConfig {
441 enabled: true,
442 group_address: "224.1.2.3".to_string(),
443 port: 6003,
444 interface: Some("en0".to_string()),
445 };
446 let json = serde_json::to_string(&original).unwrap();
447 let decoded: MulticastBetaConfig = serde_json::from_str(&json).unwrap();
448 assert_eq!(original, decoded);
449 }
450
451 #[test]
452 fn transport_config_peer_to_peer_exposes_multicast_beta() {
453 let mut config = TransportConfig::new();
454 config.peer_to_peer.multicast_beta.enabled = true;
455 config.peer_to_peer.multicast_beta.group_address = "224.5.6.7".to_string();
456 config.peer_to_peer.multicast_beta.port = 7000;
457 assert!(config.peer_to_peer.multicast_beta.enabled);
458 assert_eq!(
459 config.peer_to_peer.multicast_beta.group_address,
460 "224.5.6.7"
461 );
462 assert_eq!(config.peer_to_peer.multicast_beta.port, 7000);
463
464 let json = serde_json::to_value(&config.peer_to_peer).unwrap();
465 assert!(json.get("multicast").is_some());
466 assert!(json.get("multicast_beta").is_none());
467 }
468}