Skip to main content

dvb_gse/
cli.rs

1//! CLI application.
2//!
3//! This module implements a CLI application that receives UDP or TCP packets
4//! containing BBFRAMEs from an external DVB-S2 receiver such as
5//! [`Longmynd`](https://github.com/BritishAmateurTelevisionClub/longmynd). It
6//! obtains IP packets from a continous-mode GSE stream, and sends the IP
7//! packets to a TUN device.
8
9use crate::{
10    bbframe::{BBFrameDefrag, BBFrameReceiver, BBFrameRecv, BBFrameStream},
11    gseheader::Label,
12    gsepacket::{GSEPacketDefrag, PDU},
13    metrics::Metrics as MetricsTrait,
14};
15use anyhow::{Context, Result};
16use bytes::Bytes;
17use clap::Parser;
18use std::{
19    net::{SocketAddr, TcpListener, UdpSocket},
20    os::unix::io::AsRawFd,
21    sync::{Arc, Mutex, mpsc},
22    thread,
23    time::Duration,
24};
25
26/// dvb-gse CLI application.
27///
28/// This struct contains everything that is needed by the dvb-gse application.
29///
30/// The `AppArgs` type argument defines the type of the CLI arguments of the
31/// application. It can either be [`Args`] if no custom CLI arguments are
32/// needed, or a custom type that extends [`Args`] by implementing
33/// [`clap::Parser`] and `AsRef<Args>`.
34///
35/// The `Metrics` type argument defines the type of the custom metrics. The
36/// default type `()` is used for no metrics. It is possible to provide a type
37/// here that implements the [`Metrics`](MetricsTrait) trait to add custom
38/// metrics to the application.
39pub struct App<AppArgs = Args, Metrics = ()> {
40    args: AppArgs,
41    metrics: AppMetrics<Metrics>,
42}
43
44/// Receive DVB-GSE and send PDUs into a TUN device.
45#[derive(Parser, Debug)]
46#[command(author, version, about, long_about = None)]
47pub struct Args {
48    /// IP address and port to listen on to receive DVB-S2 BBFRAMEs.
49    #[arg(long)]
50    pub listen: SocketAddr,
51    /// TUN interface name.
52    #[arg(long)]
53    pub tun: String,
54    /// Input format: "UDP fragments", "UDP complete", or "TCP".
55    #[arg(long, default_value_t)]
56    pub input: InputFormat,
57    /// Input header length (the header is discarded).
58    #[arg(long, default_value_t = 0)]
59    pub header_length: usize,
60    /// ISI to process in MIS mode (if this option is not specified, run in SIS mode).
61    #[arg(long)]
62    pub isi: Option<u8>,
63    /// Time interval used to log statistics (in seconds).
64    #[arg(long, default_value_t = 100.0)]
65    pub stats_interval: f64,
66    /// Skip checking the GSE total length field.
67    #[arg(long)]
68    pub skip_total_length: bool,
69    /// Allow GSE packets addressed to the broadcast label (no label).
70    ///
71    /// If this argument is used, all the GSE packets not explicitly allowed via
72    /// CLI arguments are dropped.
73    #[arg(long)]
74    pub allow_broadcast: bool,
75    /// Allow GSE packets addressed to this 3-byte or 6-byte label.
76    ///
77    /// If this argument is used, all the GSE packets not explicitly allowed via
78    /// CLI arguments are dropped. This argument can be used multiple times to
79    /// allow multiple labels.
80    #[arg(long, value_parser = Label::from_hex)]
81    pub allow_label: Vec<Label>,
82}
83
84impl AsRef<Args> for Args {
85    fn as_ref(&self) -> &Args {
86        self
87    }
88}
89
90#[derive(Debug, Clone)]
91struct AppMetrics<Metrics> {
92    stats: Arc<Mutex<Stats>>,
93    metrics: Metrics,
94}
95
96impl<Metrics: MetricsTrait> MetricsTrait for AppMetrics<Metrics> {
97    fn bbframe_received(&mut self, bbframe: &Bytes) {
98        self.stats.lock().unwrap().bbframes += 1;
99        self.metrics.bbframe_received(bbframe);
100    }
101
102    fn bbframe_error(&mut self, error: &std::io::Error) {
103        self.stats.lock().unwrap().bbframe_errors += 1;
104        self.metrics.bbframe_error(error);
105    }
106
107    fn gse_pdu_received(&mut self, pdu: &PDU) {
108        self.stats.lock().unwrap().gse_pdus += 1;
109        self.metrics.gse_pdu_received(pdu);
110    }
111
112    fn gse_pdu_dropped_label_filtering(&mut self, pdu: &PDU) {
113        self.stats.lock().unwrap().gse_pdus_dropped_by_label += 1;
114        self.metrics.gse_pdu_dropped_label_filtering(pdu);
115    }
116
117    fn tcp_client_connected(&mut self, stream: &std::net::TcpStream) {
118        self.metrics.tcp_client_connected(stream);
119    }
120
121    fn tcp_client_finished(&mut self) {
122        self.metrics.tcp_client_finished();
123    }
124
125    fn tun_error(&mut self, error: &std::io::Error, pdu: &PDU) {
126        self.stats.lock().unwrap().tun_errors += 1;
127        self.metrics.tun_error(error, pdu);
128    }
129}
130
131impl<AppArgs: Parser, Metrics> App<AppArgs, Metrics> {
132    /// Creates a dvb-gse application by parsing arguments and doing some
133    /// initialization.
134    ///
135    /// The [`App::run`] method needs to be called to run the application.
136    ///
137    /// The `build_metrics` closure is used to build a `Metrics` object from the
138    /// parsed CLI arguments.
139    pub fn new<F: FnOnce(&AppArgs) -> Result<Metrics>>(build_metrics: F) -> Result<Self> {
140        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
141        let args = AppArgs::parse();
142        let metrics = build_metrics(&args)?;
143        let stats = Arc::new(Mutex::new(Stats::default()));
144        let metrics = AppMetrics { stats, metrics };
145        Ok(App { args, metrics })
146    }
147}
148
149impl<AppArgs: Parser, Metrics: Default> App<AppArgs, Metrics> {
150    /// Creates a dvb-gse application with default metrics by parsing arguments
151    /// and doing some initialization.
152    ///
153    /// The [`App::run`] method needs to be called to run the application.
154    ///
155    /// The `Metrics` object is constructed by calling its [`Default`]
156    /// implementation.
157    pub fn with_default_metrics() -> Result<Self> {
158        Self::new(|_| Ok(Metrics::default()))
159    }
160}
161
162impl<AppArgs: AsRef<Args>, Metrics: MetricsTrait + Clone + Send + 'static> App<AppArgs, Metrics> {
163    /// Runs the dvb-gse application.
164    ///
165    /// This function only returns if there is a fatal error.
166    pub fn run(self) -> Result<()> {
167        let tun = tun_tap::Iface::without_packet_info(&self.args.as_ref().tun, tun_tap::Mode::Tun)
168            .context("failed to open TUN device")?;
169        log::info!("dvb-gse v{} started", env!("CARGO_PKG_VERSION"));
170        let stats_interval = self.args.as_ref().stats_interval;
171        if stats_interval != 0.0 {
172            std::thread::spawn({
173                let stats = Arc::clone(&self.metrics.stats);
174                move || {
175                    report_stats(&stats, Duration::from_secs_f64(stats_interval));
176                }
177            });
178        }
179        match self.args.as_ref().input {
180            InputFormat::UdpFragments | InputFormat::UdpComplete => self.run_udp_input(tun),
181            InputFormat::Tcp => self.run_tcp_input(tun),
182        }
183    }
184
185    fn run_udp_input(self, tun: tun_tap::Iface) -> Result<()> {
186        let gsepacket_defrag = gsepacket_defragmenter(self.args.as_ref());
187        let socket =
188            UdpSocket::bind(self.args.as_ref().listen).context("failed to bind to UDP socket")?;
189        setup_multicast(&socket, &self.args.as_ref().listen)?;
190        let allow_settings = AllowSettings::from(self.args.as_ref());
191        match self.args.as_ref().input {
192            InputFormat::UdpFragments => {
193                let mut bbframe_recv = BBFrameDefrag::new(socket);
194                bbframe_recv.set_isi(self.args.as_ref().isi);
195                bbframe_recv.set_header_bytes(self.args.as_ref().header_length)?;
196                let mut app = AppLoop {
197                    bbframe_recv,
198                    gsepacket_defrag,
199                    tun,
200                    bbframe_recv_errors_fatal: true,
201                    metrics: self.metrics,
202                    allow_settings,
203                };
204                app.app_loop()?;
205            }
206            InputFormat::UdpComplete => {
207                let mut bbframe_recv = BBFrameRecv::new(socket);
208                bbframe_recv.set_isi(self.args.as_ref().isi);
209                bbframe_recv.set_header_bytes(self.args.as_ref().header_length)?;
210                let mut app = AppLoop {
211                    bbframe_recv,
212                    gsepacket_defrag,
213                    tun,
214                    bbframe_recv_errors_fatal: false,
215                    metrics: self.metrics,
216                    allow_settings,
217                };
218                app.app_loop()?;
219            }
220            // This function is only called for UDP input types
221            InputFormat::Tcp => unreachable!(),
222        }
223        Ok(())
224    }
225
226    fn run_tcp_input(mut self, mut tun: tun_tap::Iface) -> Result<()> {
227        let listener =
228            TcpListener::bind(self.args.as_ref().listen).context("failed to bind to TCP socket")?;
229        // For TCP, the application runs each TCP connection in a dedicated
230        // thread. There is another thread that owns the TUN. The TCP
231        // connection threads are connected to the TUN thread by an mpsc
232        // channel.
233        let channel_capacity = 64;
234        let (tun_tx, tun_rx) = mpsc::sync_channel(channel_capacity);
235        let allow_settings = AllowSettings::from(self.args.as_ref());
236        thread::spawn({
237            let mut metrics = self.metrics.clone();
238            move || {
239                for pdu in tun_rx.iter() {
240                    write_pdu_tun(&pdu, &mut tun, &mut metrics, &allow_settings);
241                }
242            }
243        });
244        // use thread scope to pass args by reference
245        thread::scope(|s| {
246            for stream in listener.incoming() {
247                let stream = match stream {
248                    Ok(s) => s,
249                    Err(e) => {
250                        log::error!("connection error {e}");
251                        continue;
252                    }
253                };
254                match stream.peer_addr() {
255                    Ok(addr) => log::info!("TCP client connected from {addr}"),
256                    Err(err) => log::error!(
257                        "TCP client connected (but could not retrieve peer address): {err}"
258                    ),
259                }
260                self.metrics.tcp_client_connected(&stream);
261                s.spawn({
262                    let args = self.args.as_ref();
263                    let tun_tx = tun_tx.clone();
264                    let mut metrics = self.metrics.clone();
265                    move || {
266                        let mut gsepacket_defrag = gsepacket_defragmenter(args);
267                        let mut bbframe_recv = BBFrameStream::new(stream);
268                        bbframe_recv.set_isi(args.isi);
269                        if let Err(err) = bbframe_recv.set_header_bytes(args.header_length) {
270                            eprintln!("could not set header length: {err}");
271                            std::process::exit(1);
272                        }
273                        loop {
274                            let bbframe = bbframe_recv.get_bbframe();
275                            let bbframe = {
276                                match bbframe {
277                                    Ok(b) => {
278                                        metrics.bbframe_received(&b);
279                                        b
280                                    }
281                                    Err(err) => {
282                                        log::error!("failed to receive BBFRAME; terminating connection: {err}");
283                                        metrics.bbframe_error(&err);
284                                        metrics.tcp_client_finished();
285                                        return;
286                                    }
287                                }
288                            };
289                            // the BBFRAME was validated by bbframe_recv, so we can unwrap here
290                            for pdu in gsepacket_defrag.defragment(&bbframe).unwrap() {
291                                tun_tx.send(pdu).unwrap();
292                            }
293                        }
294                    }
295                });
296            }
297        });
298        Ok(())
299    }
300}
301
302#[derive(Debug, Clone)]
303struct AllowSettings {
304    allow_broadcast: bool,
305    allow_label: Vec<Label>,
306}
307
308impl AllowSettings {
309    fn is_label_allowed(&self, label: &Label) -> bool {
310        if !self.allow_broadcast && self.allow_label.is_empty() {
311            // no 'allow' arguments used; allow everything
312            return true;
313        }
314        if label.is_broadcast() {
315            return self.allow_broadcast;
316        }
317        self.allow_label.iter().any(|allowed| label == allowed)
318    }
319}
320
321impl From<&Args> for AllowSettings {
322    fn from(args: &Args) -> AllowSettings {
323        AllowSettings {
324            allow_broadcast: args.allow_broadcast,
325            allow_label: args.allow_label.clone(),
326        }
327    }
328}
329
330/// Input format for the dvb-gse CLI application.
331#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
332pub enum InputFormat {
333    /// BBFRAME fragments in UDP datagrams.
334    #[default]
335    UdpFragments,
336    /// Complete BBFRAMEs in UDP datagrams.
337    UdpComplete,
338    /// BBFRAMEs in a TCP stream.
339    Tcp,
340}
341
342impl std::str::FromStr for InputFormat {
343    type Err = String;
344
345    fn from_str(s: &str) -> Result<Self, Self::Err> {
346        Ok(match s {
347            "UDP" | "UDP fragments" => InputFormat::UdpFragments,
348            "UDP complete" => InputFormat::UdpComplete,
349            "TCP" => InputFormat::Tcp,
350            _ => return Err(format!("invalid input format {s}")),
351        })
352    }
353}
354
355impl std::fmt::Display for InputFormat {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
357        write!(
358            f,
359            "{}",
360            match self {
361                InputFormat::UdpFragments => "UDP fragments",
362                InputFormat::UdpComplete => "UDP complete",
363                InputFormat::Tcp => "TCP",
364            }
365        )
366    }
367}
368
369fn setup_multicast(socket: &UdpSocket, addr: &SocketAddr) -> Result<()> {
370    match addr.ip() {
371        std::net::IpAddr::V4(addr) if addr.is_multicast() => {
372            set_reuseaddr(socket)?;
373            log::info!("joining multicast address {}", addr);
374            socket.join_multicast_v4(&addr, &std::net::Ipv4Addr::UNSPECIFIED)?;
375        }
376        std::net::IpAddr::V6(addr) if addr.is_multicast() => {
377            set_reuseaddr(socket)?;
378            log::info!("joining multicast address {}", addr);
379            socket.join_multicast_v6(&addr, 0)?;
380        }
381        _ => (),
382    }
383    Ok(())
384}
385
386fn set_reuseaddr(socket: &UdpSocket) -> Result<()> {
387    let optval: libc::c_int = 1;
388    if unsafe {
389        libc::setsockopt(
390            socket.as_raw_fd(),
391            libc::SOL_SOCKET,
392            libc::SO_REUSEADDR,
393            &optval as *const _ as *const libc::c_void,
394            libc::socklen_t::try_from(std::mem::size_of::<libc::c_int>()).unwrap(),
395        )
396    } != 0
397    {
398        let err = std::io::Error::last_os_error();
399        anyhow::bail!("could not set SO_REUSEADDR: {err}")
400    }
401    Ok(())
402}
403
404#[derive(Debug)]
405struct AppLoop<D, Metrics> {
406    bbframe_recv: D,
407    gsepacket_defrag: GSEPacketDefrag,
408    tun: tun_tap::Iface,
409    bbframe_recv_errors_fatal: bool,
410    metrics: AppMetrics<Metrics>,
411    allow_settings: AllowSettings,
412}
413
414fn write_pdu_tun<Metrics: MetricsTrait>(
415    pdu: &PDU,
416    tun: &mut tun_tap::Iface,
417    metrics: &mut AppMetrics<Metrics>,
418    allow_settings: &AllowSettings,
419) {
420    metrics.gse_pdu_received(pdu);
421    if !allow_settings.is_label_allowed(pdu.label()) {
422        metrics.gse_pdu_dropped_label_filtering(pdu);
423        return;
424    }
425    if let Err(err) = tun.send(pdu.data()) {
426        log::error!("could not write packet to TUN device: {err}");
427        metrics.tun_error(&err, pdu);
428    }
429}
430
431impl<D: BBFrameReceiver, Metrics: MetricsTrait> AppLoop<D, Metrics> {
432    fn app_loop(&mut self) -> Result<()> {
433        loop {
434            let bbframe = self.bbframe_recv.get_bbframe();
435            let bbframe = match bbframe {
436                Ok(b) => {
437                    self.metrics.bbframe_received(&b);
438                    b
439                }
440                Err(err) => {
441                    self.metrics.bbframe_error(&err);
442                    if self.bbframe_recv_errors_fatal {
443                        return Err(err).context("failed to receive BBFRAME");
444                    } else {
445                        continue;
446                    }
447                }
448            };
449            // the BBFRAME was validated by bbframe_recv, so we can unwrap here
450            for pdu in self.gsepacket_defrag.defragment(&bbframe).unwrap() {
451                write_pdu_tun(&pdu, &mut self.tun, &mut self.metrics, &self.allow_settings);
452            }
453        }
454    }
455}
456
457fn gsepacket_defragmenter(args: &Args) -> GSEPacketDefrag {
458    let mut defrag = GSEPacketDefrag::new();
459    defrag.set_skip_total_length_check(args.skip_total_length);
460    defrag
461}
462
463#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
464struct Stats {
465    bbframes: u64,
466    bbframe_errors: u64,
467    gse_pdus: u64,
468    gse_pdus_dropped_by_label: u64,
469    tun_errors: u64,
470}
471
472fn report_stats(stats: &Mutex<Stats>, interval: Duration) {
473    loop {
474        {
475            let stats = stats.lock().unwrap();
476            log::info!(
477                "BBFRAMES: {}, BBFRAME errors: {}, GSE PDUs: {}, GSE PDUs dropped by label: {}, TUN errors: {}",
478                stats.bbframes,
479                stats.bbframe_errors,
480                stats.gse_pdus,
481                stats.gse_pdus_dropped_by_label,
482                stats.tun_errors
483            );
484        }
485        std::thread::sleep(interval);
486    }
487}