Skip to main content

linuxutils_system/
rfkill.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::{Parser, Subcommand};
6use cols::{OutputMode, Table, WidthHint, print_table};
7use std::{
8    fs::{self, File, OpenOptions},
9    io::{self, Read, Write},
10    process::ExitCode,
11};
12
13// rfkill type constants (from linux/rfkill.h)
14const TYPE_ALL: u8 = 0;
15const TYPE_WLAN: u8 = 1;
16const TYPE_BLUETOOTH: u8 = 2;
17const TYPE_UWB: u8 = 3;
18const TYPE_WIMAX: u8 = 4;
19const TYPE_WWAN: u8 = 5;
20const TYPE_GPS: u8 = 6;
21const TYPE_FM: u8 = 7;
22const TYPE_NFC: u8 = 8;
23
24// rfkill operation constants (from linux/rfkill.h)
25const OP_ADD: u8 = 0;
26const OP_DEL: u8 = 1;
27const OP_CHANGE: u8 = 2;
28const OP_CHANGE_ALL: u8 = 3;
29
30#[derive(Parser)]
31#[command(name = "rfkill", about = "Enable and disable wireless devices")]
32pub struct Args {
33    /// Use JSON output format
34    #[arg(short = 'J', long)]
35    json: bool,
36
37    /// Don't print headings
38    #[arg(short = 'n', long)]
39    noheadings: bool,
40
41    /// Define which output columns to use
42    #[arg(short = 'o', long, value_delimiter = ',')]
43    output: Option<Vec<String>>,
44
45    /// Output all available columns
46    #[arg(long)]
47    output_all: bool,
48
49    /// Use the raw output format
50    #[arg(short = 'r', long)]
51    raw: bool,
52
53    #[command(subcommand)]
54    command: Option<Cmd>,
55}
56
57#[derive(Subcommand)]
58enum Cmd {
59    /// Listen for rfkill events and display them on stdout
60    Event,
61
62    /// List the current state of all available devices (deprecated format)
63    List {
64        /// Device identifiers (IDs or type names)
65        #[arg()]
66        identifiers: Vec<String>,
67    },
68
69    /// Disable the corresponding device
70    Block {
71        /// Device identifiers (IDs or type names)
72        #[arg(required = true)]
73        identifiers: Vec<String>,
74    },
75
76    /// Enable the corresponding device
77    Unblock {
78        /// Device identifiers (IDs or type names)
79        #[arg(required = true)]
80        identifiers: Vec<String>,
81    },
82
83    /// Enable or disable the corresponding device
84    Toggle {
85        /// Device identifiers (IDs or type names)
86        #[arg(required = true)]
87        identifiers: Vec<String>,
88    },
89}
90
91#[derive(Debug)]
92struct Device {
93    id: u32,
94    rf_type: u8,
95    name: String,
96    soft: bool,
97    hard: bool,
98}
99
100impl Device {
101    fn type_name(&self) -> &'static str {
102        type_to_name(self.rf_type)
103    }
104
105    fn type_desc(&self) -> &'static str {
106        type_to_desc(self.rf_type)
107    }
108}
109
110fn type_to_name(t: u8) -> &'static str {
111    match t {
112        TYPE_WLAN => "wlan",
113        TYPE_BLUETOOTH => "bluetooth",
114        TYPE_UWB => "uwb",
115        TYPE_WIMAX => "wimax",
116        TYPE_WWAN => "wwan",
117        TYPE_GPS => "gps",
118        TYPE_FM => "fm",
119        TYPE_NFC => "nfc",
120        _ => "unknown",
121    }
122}
123
124fn type_to_desc(t: u8) -> &'static str {
125    match t {
126        TYPE_WLAN => "Wireless LAN",
127        TYPE_BLUETOOTH => "Bluetooth",
128        TYPE_UWB => "Ultra-Wideband",
129        TYPE_WIMAX => "WiMAX",
130        TYPE_WWAN => "Wireless WAN",
131        TYPE_GPS => "GPS",
132        TYPE_FM => "FM",
133        TYPE_NFC => "NFC",
134        _ => "Unknown",
135    }
136}
137
138fn name_to_type(s: &str) -> Option<u8> {
139    match s {
140        "all" => Some(TYPE_ALL),
141        "wlan" | "wifi" => Some(TYPE_WLAN),
142        "bluetooth" => Some(TYPE_BLUETOOTH),
143        "uwb" | "ultrawideband" => Some(TYPE_UWB),
144        "wimax" => Some(TYPE_WIMAX),
145        "wwan" => Some(TYPE_WWAN),
146        "gps" => Some(TYPE_GPS),
147        "fm" => Some(TYPE_FM),
148        "nfc" => Some(TYPE_NFC),
149        _ => None,
150    }
151}
152
153enum Identifier {
154    Id(u32),
155    Type(u8),
156}
157
158fn parse_identifier(s: &str) -> Option<Identifier> {
159    if let Ok(id) = s.parse::<u32>() {
160        return Some(Identifier::Id(id));
161    }
162    name_to_type(s).map(Identifier::Type)
163}
164
165fn read_devices() -> Vec<Device> {
166    let mut devices = Vec::new();
167
168    let entries = match fs::read_dir("/sys/class/rfkill") {
169        Ok(e) => e,
170        Err(_) => return devices,
171    };
172
173    for entry in entries.flatten() {
174        let fname = entry.file_name();
175        let fname_str = match fname.to_str() {
176            Some(s) => s,
177            None => continue,
178        };
179
180        let id: u32 = match fname_str.strip_prefix("rfkill") {
181            Some(n) => match n.parse() {
182                Ok(v) => v,
183                Err(_) => continue,
184            },
185            None => continue,
186        };
187
188        let base = entry.path();
189
190        let type_str = sysfs_read_str(&base.join("type")).unwrap_or_default();
191        let dev_name = sysfs_read_str(&base.join("name")).unwrap_or_default();
192        let soft = sysfs_read_u8(&base.join("soft")).unwrap_or(0) != 0;
193        let hard = sysfs_read_u8(&base.join("hard")).unwrap_or(0) != 0;
194
195        let rf_type = name_to_type(&type_str).unwrap_or(0);
196
197        devices.push(Device {
198            id,
199            rf_type,
200            name: dev_name,
201            soft,
202            hard,
203        });
204    }
205
206    devices.sort_by_key(|d| d.id);
207    devices
208}
209
210fn sysfs_read_str(path: &std::path::Path) -> Option<String> {
211    fs::read_to_string(path).ok().map(|s| s.trim().to_string())
212}
213
214fn sysfs_read_u8(path: &std::path::Path) -> Option<u8> {
215    sysfs_read_str(path)?.parse().ok()
216}
217
218// Write a packed rfkill_event (8 bytes) to /dev/rfkill.
219// Layout: u32 idx, u8 type, u8 op, u8 soft, u8 hard (all in native byte order).
220fn write_event(idx: u32, rf_type: u8, op: u8, soft: u8) -> io::Result<()> {
221    let mut f = OpenOptions::new().write(true).open("/dev/rfkill")?;
222    let mut buf = [0u8; 8];
223    buf[0..4].copy_from_slice(&idx.to_ne_bytes());
224    buf[4] = rf_type;
225    buf[5] = op;
226    buf[6] = soft;
227    buf[7] = 0;
228    f.write_all(&buf)
229}
230
231fn device_matches(dev: &Device, ident: &Identifier) -> bool {
232    match ident {
233        Identifier::Id(id) => dev.id == *id,
234        Identifier::Type(TYPE_ALL) => true,
235        Identifier::Type(t) => dev.rf_type == *t,
236    }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240enum Col {
241    Id,
242    Type,
243    Device,
244    TypeDesc,
245    Soft,
246    Hard,
247}
248
249impl Col {
250    fn name(self) -> &'static str {
251        match self {
252            Col::Id => "ID",
253            Col::Type => "TYPE",
254            Col::Device => "DEVICE",
255            Col::TypeDesc => "TYPE-DESC",
256            Col::Soft => "SOFT",
257            Col::Hard => "HARD",
258        }
259    }
260
261    fn whint(self) -> WidthHint {
262        match self {
263            Col::Id => WidthHint::Fixed(2),
264            Col::Type => WidthHint::Fixed(9),
265            Col::Device => WidthHint::Fixed(10),
266            Col::TypeDesc => WidthHint::Fixed(12),
267            Col::Soft => WidthHint::Fixed(9),
268            Col::Hard => WidthHint::Fixed(9),
269        }
270    }
271
272    fn is_right(self) -> bool {
273        matches!(self, Col::Id)
274    }
275
276    fn from_name(s: &str) -> Option<Self> {
277        match s.to_uppercase().as_str() {
278            "ID" => Some(Col::Id),
279            "TYPE" => Some(Col::Type),
280            "DEVICE" => Some(Col::Device),
281            "TYPE-DESC" => Some(Col::TypeDesc),
282            "SOFT" => Some(Col::Soft),
283            "HARD" => Some(Col::Hard),
284            _ => None,
285        }
286    }
287
288    fn cell_value(self, dev: &Device) -> String {
289        match self {
290            Col::Id => dev.id.to_string(),
291            Col::Type => dev.type_name().to_string(),
292            Col::Device => dev.name.clone(),
293            Col::TypeDesc => dev.type_desc().to_string(),
294            Col::Soft => blocked_str(dev.soft).to_string(),
295            Col::Hard => blocked_str(dev.hard).to_string(),
296        }
297    }
298}
299
300const DEFAULT_COLUMNS: &[Col] =
301    &[Col::Id, Col::Type, Col::Device, Col::Soft, Col::Hard];
302
303const ALL_COLUMNS: &[Col] = &[
304    Col::Id,
305    Col::Type,
306    Col::Device,
307    Col::TypeDesc,
308    Col::Soft,
309    Col::Hard,
310];
311
312fn blocked_str(blocked: bool) -> &'static str {
313    if blocked { "blocked" } else { "unblocked" }
314}
315
316fn print_table_output(args: &Args, devices: &[Device]) -> ExitCode {
317    let columns = if args.output_all {
318        ALL_COLUMNS.to_vec()
319    } else if let Some(ref names) = args.output {
320        let mut cols = Vec::new();
321        for name in names {
322            match Col::from_name(name.trim()) {
323                Some(c) => cols.push(c),
324                None => {
325                    eprintln!("rfkill: unknown column: {name}");
326                    return ExitCode::FAILURE;
327                }
328            }
329        }
330        cols
331    } else {
332        DEFAULT_COLUMNS.to_vec()
333    };
334
335    let mut table = Table::new();
336    table.name_set("rfkilldevs");
337
338    if args.json {
339        table.output_mode_set(OutputMode::Json);
340    } else if args.raw {
341        table.output_mode_set(OutputMode::Raw);
342    }
343
344    if args.noheadings {
345        table.headings_set(false);
346    }
347
348    for col in &columns {
349        let idx = table.new_column(col.name());
350        table.column_mut(idx).unwrap().width_hint_set(col.whint());
351        if col.is_right() {
352            table.column_mut(idx).unwrap().right_set(true);
353        }
354    }
355
356    for dev in devices {
357        let line_id = table.new_line(None);
358        let line = table.line_mut(line_id);
359        for (ci, col) in columns.iter().enumerate() {
360            line.data_set(ci, &col.cell_value(dev));
361        }
362    }
363
364    let stdout = std::io::stdout();
365    let mut out = stdout.lock();
366    if let Err(e) = print_table(&table, &mut out) {
367        eprintln!("rfkill: {e}");
368        return ExitCode::FAILURE;
369    }
370
371    ExitCode::SUCCESS
372}
373
374fn print_legacy_list(devices: &[Device]) {
375    for dev in devices {
376        println!("{}: {}: {}", dev.id, dev.name, dev.type_desc());
377        println!("\tSoft blocked: {}", if dev.soft { "yes" } else { "no" });
378        println!("\tHard blocked: {}", if dev.hard { "yes" } else { "no" });
379    }
380}
381
382fn filter_devices<'a>(
383    devices: &'a [Device],
384    identifiers: &[String],
385) -> Vec<&'a Device> {
386    if identifiers.is_empty() {
387        return devices.iter().collect();
388    }
389
390    let mut seen = vec![false; devices.len()];
391    let mut result = Vec::new();
392    for ident_str in identifiers {
393        if let Some(ident) = parse_identifier(ident_str) {
394            for (i, dev) in devices.iter().enumerate() {
395                if device_matches(dev, &ident) && !seen[i] {
396                    seen[i] = true;
397                    result.push(dev);
398                }
399            }
400        }
401    }
402    result
403}
404
405fn do_block_op(identifiers: &[String], soft: u8) -> ExitCode {
406    for ident_str in identifiers {
407        let ident = match parse_identifier(ident_str) {
408            Some(i) => i,
409            None => {
410                eprintln!("rfkill: invalid identifier: {ident_str}");
411                return ExitCode::FAILURE;
412            }
413        };
414
415        let result = match ident {
416            Identifier::Type(t) => write_event(0, t, OP_CHANGE_ALL, soft),
417            Identifier::Id(id) => write_event(id, 0, OP_CHANGE, soft),
418        };
419
420        if let Err(e) = result {
421            eprintln!("rfkill: {e}");
422            return ExitCode::FAILURE;
423        }
424    }
425    ExitCode::SUCCESS
426}
427
428fn do_toggle(identifiers: &[String]) -> ExitCode {
429    let devices = read_devices();
430
431    for ident_str in identifiers {
432        let ident = match parse_identifier(ident_str) {
433            Some(i) => i,
434            None => {
435                eprintln!("rfkill: invalid identifier: {ident_str}");
436                return ExitCode::FAILURE;
437            }
438        };
439
440        for dev in &devices {
441            if device_matches(dev, &ident) {
442                let new_soft: u8 = if dev.soft { 0 } else { 1 };
443                if let Err(e) = write_event(dev.id, 0, OP_CHANGE, new_soft) {
444                    eprintln!("rfkill: {e}");
445                    return ExitCode::FAILURE;
446                }
447            }
448        }
449    }
450    ExitCode::SUCCESS
451}
452
453fn do_event() -> ExitCode {
454    let mut f = match File::open("/dev/rfkill") {
455        Ok(f) => f,
456        Err(e) => {
457            eprintln!("rfkill: cannot open /dev/rfkill: {e}");
458            return ExitCode::FAILURE;
459        }
460    };
461
462    let mut buf = [0u8; 8];
463    loop {
464        match f.read_exact(&mut buf) {
465            Ok(_) => {}
466            Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
467            Err(e) => {
468                eprintln!("rfkill: read error: {e}");
469                return ExitCode::FAILURE;
470            }
471        }
472
473        let idx = u32::from_ne_bytes(buf[0..4].try_into().unwrap());
474        let rf_type = buf[4];
475        let op = buf[5];
476        let soft = buf[6];
477        let hard = buf[7];
478
479        let op_name = match op {
480            OP_ADD => "add",
481            OP_DEL => "remove",
482            OP_CHANGE => "change",
483            OP_CHANGE_ALL => "change-all",
484            _ => "unknown",
485        };
486
487        println!(
488            "{idx}: {}: {op_name} soft={} hard={}",
489            type_to_name(rf_type),
490            blocked_str(soft != 0),
491            blocked_str(hard != 0),
492        );
493    }
494    ExitCode::SUCCESS
495}
496
497pub fn run(args: Args) -> ExitCode {
498    match &args.command {
499        None => {
500            let devices = read_devices();
501            print_table_output(&args, &devices)
502        }
503        Some(Cmd::List { identifiers }) => {
504            let devices = read_devices();
505            if args.output.is_some() || args.output_all || args.json || args.raw
506            {
507                let filtered: Vec<&Device> =
508                    filter_devices(&devices, identifiers);
509                let owned: Vec<Device> = filtered
510                    .into_iter()
511                    .map(|d| Device {
512                        id: d.id,
513                        rf_type: d.rf_type,
514                        name: d.name.clone(),
515                        soft: d.soft,
516                        hard: d.hard,
517                    })
518                    .collect();
519                print_table_output(&args, &owned)
520            } else {
521                let filtered = filter_devices(&devices, identifiers);
522                let refs: Vec<&Device> = filtered;
523                let owned: Vec<Device> = refs
524                    .into_iter()
525                    .map(|d| Device {
526                        id: d.id,
527                        rf_type: d.rf_type,
528                        name: d.name.clone(),
529                        soft: d.soft,
530                        hard: d.hard,
531                    })
532                    .collect();
533                print_legacy_list(&owned);
534                ExitCode::SUCCESS
535            }
536        }
537        Some(Cmd::Block { identifiers }) => do_block_op(identifiers, 1),
538        Some(Cmd::Unblock { identifiers }) => do_block_op(identifiers, 0),
539        Some(Cmd::Toggle { identifiers }) => do_toggle(identifiers),
540        Some(Cmd::Event) => do_event(),
541    }
542}