Skip to main content

moblink_rust/
relay_service.rs

1use std::collections::HashMap;
2use std::net::Ipv4Addr;
3use std::path::PathBuf;
4use std::sync::{Arc, Weak};
5use std::time::Duration;
6
7use log::{error, info};
8use mdns_sd::{ServiceDaemon, ServiceEvent};
9use network_interface::{NetworkInterface, NetworkInterfaceConfig};
10use regex::Regex;
11use serde::{Deserialize, Serialize};
12use tokio::fs::File;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::sync::Mutex;
15use tokio::task::JoinHandle;
16use url::Url;
17use uuid::Uuid;
18
19use crate::MDNS_SERVICE_TYPE;
20use crate::relay::{GetStatusClosure, Relay, Status};
21use crate::utils::{any_address_belongs_to_this_machine, get_first_ipv4_address};
22
23#[derive(Serialize, Deserialize, Default)]
24struct DatabaseContent {
25    relay_ids: HashMap<String, Uuid>,
26}
27
28struct Database {
29    path: PathBuf,
30    content: DatabaseContent,
31}
32
33impl Database {
34    async fn new(path: PathBuf) -> Self {
35        let content = Self::load(&path).await;
36        Self { path, content }
37    }
38
39    async fn load(path: &PathBuf) -> DatabaseContent {
40        let mut content = "".to_string();
41        if let Ok(mut file) = File::open(path).await {
42            let mut buffer = vec![];
43            if file.read_to_end(&mut buffer).await.is_ok() {
44                content = String::from_utf8(buffer).unwrap_or_default();
45            }
46        }
47        serde_json::from_str(&content).unwrap_or_default()
48    }
49
50    async fn store(&self) {
51        let content = serde_json::to_string(&self.content).unwrap_or_default();
52        if let Ok(mut file) = File::create(&self.path).await {
53            file.write_all(content.as_bytes()).await.ok();
54        }
55    }
56
57    async fn get_relay_id(&mut self, name: &str) -> Uuid {
58        if !self.content.relay_ids.contains_key(name) {
59            self.content
60                .relay_ids
61                .insert(name.to_string(), Uuid::new_v4());
62            self.store().await;
63        }
64        *self.content.relay_ids.get(name).unwrap()
65    }
66}
67
68struct ServiceRelay {
69    interface_name: String,
70    interface_address: Ipv4Addr,
71    streamer_name: String,
72    streamer_url: String,
73    relay: Relay,
74}
75
76impl ServiceRelay {
77    async fn new(
78        interface_name: String,
79        interface_address: Ipv4Addr,
80        relay_name: String,
81        streamer: Streamer,
82        password: String,
83        get_status: Option<GetStatusClosure>,
84        database: Arc<Mutex<Database>>,
85    ) -> Self {
86        let relay = Relay::new();
87        relay.set_bind_address(interface_address.to_string()).await;
88        relay
89            .setup(
90                streamer.url.clone(),
91                password,
92                database.lock().await.get_relay_id(&interface_name).await,
93                relay_name,
94                |_| {},
95                get_status,
96            )
97            .await;
98        relay.start().await;
99        Self {
100            interface_name,
101            interface_address,
102            streamer_name: streamer.name,
103            streamer_url: streamer.url,
104            relay,
105        }
106    }
107}
108
109#[derive(Clone)]
110struct Streamer {
111    name: String,
112    url: String,
113}
114
115#[derive(Serialize)]
116struct RuntimeStreamerStatus {
117    name: String,
118    url: String,
119    host: String,
120}
121
122#[derive(Serialize)]
123struct RuntimeRelayStatus {
124    interface_name: String,
125    interface_address: String,
126    streamer_name: String,
127    streamer_url: String,
128    streamer_host: String,
129}
130
131#[derive(Serialize)]
132struct RuntimeStatus {
133    connected: bool,
134    manual_streamer: bool,
135    streamers: Vec<RuntimeStreamerStatus>,
136    relays: Vec<RuntimeRelayStatus>,
137}
138
139// Parses "interface=label" pairs used to give a relay a friendly name in the
140// Moblin app instead of the raw interface name (e.g. "eth0=WAN").
141fn parse_interface_name_overrides(values: Vec<String>) -> HashMap<String, String> {
142    let mut overrides = HashMap::new();
143
144    for value in values {
145        let Some((interface, label)) = value.split_once('=') else {
146            continue;
147        };
148        let interface = interface.trim();
149        let label = label.trim();
150
151        if !interface.is_empty() && !label.is_empty() {
152            overrides.insert(interface.to_string(), label.to_string());
153        }
154    }
155
156    overrides
157}
158
159fn host_from_url(url: &str) -> String {
160    Url::parse(url)
161        .ok()
162        .and_then(|parsed| parsed.host_str().map(|host| host.to_string()))
163        .unwrap_or_default()
164}
165
166// Builds the streamer list from explicit URLs, bypassing mDNS discovery. Used
167// where discovery is unreliable (e.g. across router uplinks behind NAT).
168fn parse_manual_streamers(values: Vec<String>) -> Vec<Streamer> {
169    values
170        .into_iter()
171        .filter_map(|value| match Url::parse(&value) {
172            Ok(url) => Some(Streamer {
173                name: url
174                    .host_str()
175                    .map(|host| host.to_string())
176                    .unwrap_or_else(|| value.clone()),
177                url: value,
178            }),
179            Err(error) => {
180                error!("Invalid manual streamer URL {}: {}", value, error);
181                None
182            }
183        })
184        .collect()
185}
186
187struct NetworkInterfaceFilter {
188    patterns_to_allow: Option<Regex>,
189    patterns_to_ignore: Option<Regex>,
190}
191
192impl NetworkInterfaceFilter {
193    fn new(patterns_to_allow: Vec<String>, patterns_to_ignore: Vec<String>) -> Self {
194        Self {
195            patterns_to_allow: Self::compile(patterns_to_allow),
196            patterns_to_ignore: Self::compile(patterns_to_ignore),
197        }
198    }
199
200    fn filter(&self, interfaces: &mut Vec<NetworkInterface>) {
201        if let Some(patterns_to_allow) = &self.patterns_to_allow {
202            interfaces.retain(|interface| patterns_to_allow.is_match(&interface.name));
203        }
204        if let Some(patterns_to_ignore) = &self.patterns_to_ignore {
205            interfaces.retain(|interface| !patterns_to_ignore.is_match(&interface.name));
206        }
207    }
208
209    fn compile(patterns: Vec<String>) -> Option<Regex> {
210        if !patterns.is_empty() {
211            let pattern = format!("^{}$", patterns.join("|"));
212            match Regex::new(&pattern) {
213                Ok(regex) => return Some(regex),
214                Err(error) => {
215                    error!("Failed to compile regex {} with error: {}", pattern, error);
216                }
217            }
218        }
219        None
220    }
221}
222
223struct RelayServiceInner {
224    me: Weak<Mutex<Self>>,
225    password: String,
226    network_interface_filter: NetworkInterfaceFilter,
227    manual_streamers: Vec<Streamer>,
228    interface_name_overrides: HashMap<String, String>,
229    runtime_status_file: Option<PathBuf>,
230    get_status: Option<GetStatusClosure>,
231    status: Status,
232    relays: Vec<ServiceRelay>,
233    network_interfaces: Vec<NetworkInterface>,
234    streamers: Vec<Streamer>,
235    network_interface_monitor: Option<JoinHandle<()>>,
236    streamers_monitor: Option<JoinHandle<()>>,
237    get_status_updater: Option<JoinHandle<()>>,
238    database: Arc<Mutex<Database>>,
239}
240
241impl RelayServiceInner {
242    async fn new(
243        config: RelayServiceConfig,
244        get_status: Option<GetStatusClosure>,
245    ) -> Arc<Mutex<Self>> {
246        let database = Arc::new(Mutex::new(Database::new(config.database).await));
247        Arc::new_cyclic(|me| {
248            Mutex::new(Self {
249                me: me.clone(),
250                password: config.password,
251                network_interface_filter: NetworkInterfaceFilter::new(
252                    config.network_interfaces_to_allow,
253                    config.network_interfaces_to_ignore,
254                ),
255                manual_streamers: parse_manual_streamers(config.streamer_urls),
256                interface_name_overrides: parse_interface_name_overrides(
257                    config.interface_name_overrides,
258                ),
259                runtime_status_file: config.runtime_status_file,
260                get_status,
261                status: Default::default(),
262                relays: Vec::new(),
263                network_interfaces: Vec::new(),
264                streamers: Vec::new(),
265                network_interface_monitor: None,
266                streamers_monitor: None,
267                get_status_updater: None,
268                database,
269            })
270        })
271    }
272
273    async fn start(&mut self) {
274        self.start_network_interfaces_monitor();
275        if self.manual_streamers.is_empty() {
276            self.start_streamers_monitor();
277        } else {
278            self.streamers = self.manual_streamers.clone();
279            self.updated().await;
280            info!(
281                "Using {} manually configured streamer URL(s)",
282                self.streamers.len()
283            );
284        }
285        self.start_get_status_updater();
286        self.write_runtime_status().await;
287    }
288
289    async fn stop(&mut self) {
290        if let Some(network_interface_monitor) = self.network_interface_monitor.take() {
291            network_interface_monitor.abort();
292            network_interface_monitor.await.ok();
293        }
294        if let Some(streamers_finder) = self.streamers_monitor.take() {
295            streamers_finder.abort();
296            streamers_finder.await.ok();
297        }
298    }
299
300    fn start_network_interfaces_monitor(&mut self) {
301        let relay_service = self.me.clone();
302        self.network_interface_monitor = Some(tokio::spawn(async move {
303            while let Ok(interfaces) = NetworkInterface::show() {
304                let Some(relay_service) = relay_service.upgrade() else {
305                    break;
306                };
307                {
308                    let mut relay_service = relay_service.lock().await;
309                    relay_service.update_network_interfaces(interfaces);
310                    relay_service.updated().await;
311                }
312                tokio::time::sleep(Duration::from_secs(3)).await;
313            }
314        }));
315    }
316
317    fn update_network_interfaces(&mut self, mut interfaces: Vec<NetworkInterface>) {
318        self.network_interface_filter.filter(&mut interfaces);
319        self.network_interfaces = interfaces;
320    }
321
322    fn start_streamers_monitor(&mut self) {
323        let relay_service = self.me.clone();
324        self.streamers_monitor = Some(tokio::spawn(async move {
325            loop {
326                let Ok(browser) = ServiceDaemon::new() else {
327                    return;
328                };
329                let Ok(receiver) = browser.browse(MDNS_SERVICE_TYPE) else {
330                    return;
331                };
332                while let Ok(event) = receiver.recv_async().await {
333                    if let ServiceEvent::ServiceResolved(info) = event {
334                        info!(
335                            "mDNS-SD: Found streamer {} {:?} {}",
336                            info.get_fullname(),
337                            info.get_addresses(),
338                            info.get_port()
339                        );
340                        let Some(name) = info.get_property_val_str("name") else {
341                            continue;
342                        };
343                        let addresses = info.get_addresses_v4();
344                        if any_address_belongs_to_this_machine(&addresses) {
345                            continue;
346                        }
347                        let Some(address) = addresses.iter().next().cloned() else {
348                            continue;
349                        };
350                        let Some(relay_service) = relay_service.upgrade() else {
351                            break;
352                        };
353                        {
354                            let mut relay_service = relay_service.lock().await;
355                            relay_service.add_streamer(name.to_string(), address, info.get_port());
356                            relay_service.updated().await;
357                        }
358                    }
359                }
360            }
361        }));
362    }
363
364    fn start_get_status_updater(&mut self) {
365        let relay_service = self.me.clone();
366        self.get_status_updater = Some(tokio::spawn(async move {
367            loop {
368                let Some(relay_service) = relay_service.upgrade() else {
369                    break;
370                };
371                relay_service.lock().await.update_status().await;
372                tokio::time::sleep(Duration::from_secs(5)).await;
373            }
374        }));
375    }
376
377    async fn update_status(&mut self) {
378        self.status = if let Some(get_status) = &self.get_status {
379            get_status().await
380        } else {
381            Status::default()
382        }
383    }
384
385    fn add_streamer(&mut self, name: String, address: Ipv4Addr, port: u16) {
386        let url = format!("ws://{}:{}", address, port);
387        self.streamers.retain(|streamer| streamer.url != url);
388        self.streamers.push(Streamer { name, url });
389    }
390
391    // Writes the current relay/streamer state to a JSON file so external UIs
392    // (e.g. the OpenWrt LuCI app) can show connection state and streamer IPs.
393    async fn write_runtime_status(&self) {
394        let Some(path) = &self.runtime_status_file else {
395            return;
396        };
397
398        let status = RuntimeStatus {
399            connected: !self.relays.is_empty(),
400            manual_streamer: !self.manual_streamers.is_empty(),
401            streamers: self
402                .streamers
403                .iter()
404                .map(|streamer| RuntimeStreamerStatus {
405                    name: streamer.name.clone(),
406                    url: streamer.url.clone(),
407                    host: host_from_url(&streamer.url),
408                })
409                .collect(),
410            relays: self
411                .relays
412                .iter()
413                .map(|relay| RuntimeRelayStatus {
414                    interface_name: relay.interface_name.clone(),
415                    interface_address: relay.interface_address.to_string(),
416                    streamer_name: relay.streamer_name.clone(),
417                    streamer_url: relay.streamer_url.clone(),
418                    streamer_host: host_from_url(&relay.streamer_url),
419                })
420                .collect(),
421        };
422
423        let Ok(content) = serde_json::to_string(&status) else {
424            return;
425        };
426
427        if let Some(parent) = path.parent() {
428            tokio::fs::create_dir_all(parent).await.ok();
429        }
430
431        // Write to a sibling temp file and atomically rename it over the target.
432        // Readers polling this file (e.g. the LuCI UI) then always observe either
433        // the previous or the new complete document, never a truncated or
434        // half-written one. Writes here are already serialized by the service
435        // mutex, so the fixed temp name cannot collide with itself.
436        let mut temp_path = path.clone();
437        temp_path.as_mut_os_string().push(".tmp");
438        if let Err(error) = tokio::fs::write(&temp_path, content).await {
439            error!("Failed to write runtime status file: {}", error);
440            return;
441        }
442        if let Err(error) = tokio::fs::rename(&temp_path, path).await {
443            error!("Failed to replace runtime status file: {}", error);
444            tokio::fs::remove_file(&temp_path).await.ok();
445        }
446    }
447
448    async fn updated(&mut self) {
449        let old_number_of_relays = self.relays.len();
450        self.add_relays().await;
451        self.remove_relays().await;
452        let new_number_of_relays = self.relays.len();
453        if new_number_of_relays != old_number_of_relays {
454            info!("Number of relays: {}", new_number_of_relays);
455        }
456        self.write_runtime_status().await;
457    }
458
459    async fn add_relays(&mut self) {
460        for interface in &self.network_interfaces {
461            let Some(interface_address) = get_first_ipv4_address(interface) else {
462                continue;
463            };
464            if interface_address.is_loopback() {
465                continue;
466            }
467            for streamer in &self.streamers {
468                if self.relay_already_added(interface_address, &streamer.url) {
469                    continue;
470                }
471                let relay_name = self
472                    .interface_name_overrides
473                    .get(&interface.name)
474                    .cloned()
475                    .unwrap_or_else(|| interface.name.clone());
476                info!(
477                    "Adding relay called {} with interface address {} for streamer name {} and \
478                     URL {}",
479                    relay_name, interface_address, streamer.name, streamer.url
480                );
481                self.relays.push(
482                    ServiceRelay::new(
483                        interface.name.clone(),
484                        interface_address,
485                        relay_name,
486                        streamer.clone(),
487                        self.password.clone(),
488                        self.create_get_status_closure(),
489                        self.database.clone(),
490                    )
491                    .await,
492                );
493            }
494        }
495    }
496
497    pub fn create_get_status_closure(&self) -> Option<GetStatusClosure> {
498        let relay_service = self.me.clone();
499        Some(Box::new(move || {
500            let relay_service = relay_service.clone();
501            Box::pin(async move {
502                if let Some(relay_service) = relay_service.upgrade() {
503                    relay_service.lock().await.status.clone()
504                } else {
505                    Status::default()
506                }
507            })
508        }))
509    }
510
511    fn relay_already_added(&self, interface_address: Ipv4Addr, streamer_url: &str) -> bool {
512        self.relays.iter().any(|relay| {
513            relay.interface_address == interface_address && relay.streamer_url == streamer_url
514        })
515    }
516
517    async fn remove_relays(&mut self) {
518        let mut relays_to_keep: Vec<ServiceRelay> = Vec::new();
519        let mut relays_to_remove: Vec<ServiceRelay> = Vec::new();
520        for relay in self.relays.drain(..) {
521            if Self::should_keep_relay(&self.network_interfaces, relay.interface_address) {
522                relays_to_keep.push(relay);
523            } else {
524                relays_to_remove.push(relay);
525            }
526        }
527        self.relays = relays_to_keep;
528        for relay in relays_to_remove {
529            info!(
530                "Removing relay called {} with interface address {} for streamer name {} and URL \
531                 {}",
532                relay.interface_name,
533                relay.interface_address,
534                relay.streamer_name,
535                relay.streamer_url
536            );
537            relay.relay.stop().await;
538        }
539    }
540
541    fn should_keep_relay(
542        network_interfaces: &[NetworkInterface],
543        interface_address: Ipv4Addr,
544    ) -> bool {
545        network_interfaces
546            .iter()
547            .any(|interface| get_first_ipv4_address(interface) == Some(interface_address))
548    }
549}
550
551/// Configuration for a [`RelayService`].
552pub struct RelayServiceConfig {
553    pub password: String,
554    /// Regex of interface names to allow (`^`/`$` anchors added automatically).
555    pub network_interfaces_to_allow: Vec<String>,
556    /// Regex of interface names to ignore (`^`/`$` anchors added
557    /// automatically).
558    pub network_interfaces_to_ignore: Vec<String>,
559    /// Streamer URLs to connect to directly instead of discovering over mDNS.
560    pub streamer_urls: Vec<String>,
561    /// `"interface=label"` pairs renaming a relay as shown in the Moblin app.
562    pub interface_name_overrides: Vec<String>,
563    /// File to write relay/streamer state as JSON for external UIs to read.
564    pub runtime_status_file: Option<PathBuf>,
565    /// File storing the per-interface relay identities.
566    pub database: PathBuf,
567}
568
569pub struct RelayService {
570    inner: Arc<Mutex<RelayServiceInner>>,
571}
572
573impl RelayService {
574    pub async fn new(config: RelayServiceConfig, get_status: Option<GetStatusClosure>) -> Self {
575        Self {
576            inner: RelayServiceInner::new(config, get_status).await,
577        }
578    }
579
580    pub async fn start(&self) {
581        self.inner.lock().await.start().await;
582    }
583
584    pub async fn stop(&self) {
585        self.inner.lock().await.stop().await;
586    }
587}