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}