xref: /aosp_15_r20/tools/netsim/rust/hostapd-rs/src/hostapd.rs (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
1 // Copyright 2024 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Controller interface for the `hostapd` C library.
16 //!
17 //! This module allows interaction with `hostapd` to manage WiFi access point and perform various wireless networking tasks directly from Rust code.
18 //!
19 //! The main `hostapd` process is managed by a separate thread while responses from the `hostapd` process are handled
20 //! by another thread, ensuring efficient and non-blocking communication.
21 //!
22 //! `hostapd` configuration consists of key-value pairs. The default configuration file is generated in the discovery directory.
23 //!
24 //! ## Features
25 //!
26 //! * **Asynchronous operation:** The module utilizes `tokio` for asynchronous communication with the `hostapd` process,
27 //!   allowing for efficient and non-blocking operations.
28 //! * **Platform support:** Supports Linux, macOS, and Windows.
29 //! * **Configuration management:** Provides functionality to generate and manage `hostapd` configuration files.
30 //! * **Easy integration:** Offers a high-level API to simplify interaction with `hostapd`, abstracting away
31 //!   low-level details.
32 //!
33 //! ## Usage
34 //!
35 //! Here's a basic example of how to create a `Hostapd` instance and start the `hostapd` process:
36 //!
37 //! ```
38 //! use hostapd_rs::hostapd::Hostapd;
39 //! use std::path::PathBuf;
40 //! use std::sync::mpsc;
41 //!
42 //! fn main() {
43 //!     // Create a channel for receiving data from hostapd
44 //!     let (tx, _) = mpsc::channel();
45 //!
46 //!     // Create a new Hostapd instance
47 //!     let mut hostapd = Hostapd::new(
48 //!         tx,                                 // Sender for receiving data
49 //!         true,                               // Verbose mode (optional)
50 //!         PathBuf::from("/tmp/hostapd.conf"), // Path to the configuration file
51 //!     );
52 //!
53 //!     // Start the hostapd process
54 //!     hostapd.run();
55 //! }
56 //! ```
57 //!
58 //! This starts `hostapd` in a separate thread, allowing interaction with it using the `Hostapd` struct's methods.
59 
60 use anyhow::bail;
61 use bytes::Bytes;
62 use log::{info, warn};
63 use netsim_packets::ieee80211::{Ieee80211, MacAddress};
64 use std::collections::HashMap;
65 use std::ffi::{c_char, c_int, CStr, CString};
66 use std::fs::File;
67 use std::io::{BufWriter, Write};
68 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
69 #[cfg(unix)]
70 use std::os::fd::IntoRawFd;
71 #[cfg(windows)]
72 use std::os::windows::io::IntoRawSocket;
73 use std::path::PathBuf;
74 use std::sync::{mpsc, Arc, RwLock};
75 use std::thread::{self, sleep};
76 use std::time::Duration;
77 use tokio::io::{AsyncReadExt, AsyncWriteExt};
78 use tokio::net::{
79     tcp::{OwnedReadHalf, OwnedWriteHalf},
80     TcpListener, TcpStream,
81 };
82 use tokio::runtime::Runtime;
83 use tokio::sync::Mutex;
84 
85 use crate::hostapd_sys::{
86     run_hostapd_main, set_virtio_ctrl_sock, set_virtio_sock, VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG,
87     VIRTIO_WIFI_CTRL_CMD_TERMINATE,
88 };
89 
90 /// Alias for RawFd on Unix or RawSocket on Windows (converted to i32)
91 type RawDescriptor = i32;
92 
93 /// Hostapd process interface.
94 ///
95 /// This struct provides methods for interacting with the `hostapd` process,
96 /// such as starting and stopping the process, configuring the access point,
97 /// and sending and receiving data.
98 pub struct Hostapd {
99     // TODO: update to tokio based RwLock when usages are async
100     handle: RwLock<Option<thread::JoinHandle<()>>>,
101     verbose: bool,
102     config: HashMap<String, String>,
103     config_path: PathBuf,
104     data_writer: Option<Mutex<OwnedWriteHalf>>,
105     ctrl_writer: Option<Mutex<OwnedWriteHalf>>,
106     tx_bytes: mpsc::Sender<Bytes>,
107     runtime: Arc<Runtime>,
108     // MAC address of the access point.
109     bssid: MacAddress,
110 }
111 
112 impl Hostapd {
113     /// Creates a new `Hostapd` instance.
114     ///
115     /// # Arguments
116     ///
117     /// * `tx_bytes`: Sender for transmitting data received from `hostapd`.
118     /// * `verbose`: Whether to run `hostapd` in verbose mode.
119     /// * `config_path`: Path to the `hostapd` configuration file.
120 
new(tx_bytes: mpsc::Sender<Bytes>, verbose: bool, config_path: PathBuf) -> Self121     pub fn new(tx_bytes: mpsc::Sender<Bytes>, verbose: bool, config_path: PathBuf) -> Self {
122         // Default Hostapd conf entries
123         let config_data = [
124             ("ssid", "AndroidWifi"),
125             ("interface", "wlan1"),
126             ("driver", "virtio_wifi"),
127             ("bssid", "00:13:10:95:fe:0b"),
128             ("country_code", "US"),
129             ("hw_mode", "g"),
130             ("channel", "8"),
131             ("beacon_int", "1000"),
132             ("dtim_period", "2"),
133             ("max_num_sta", "255"),
134             ("rts_threshold", "2347"),
135             ("fragm_threshold", "2346"),
136             ("macaddr_acl", "0"),
137             ("auth_algs", "3"),
138             ("ignore_broadcast_ssid", "0"),
139             ("wmm_enabled", "0"),
140             ("ieee80211n", "1"),
141             ("eapol_key_index_workaround", "0"),
142         ];
143         let mut config: HashMap<String, String> = HashMap::new();
144         config.extend(config_data.iter().map(|(k, v)| (k.to_string(), v.to_string())));
145 
146         // TODO(b/381154253): Allow configuring BSSID in hostapd.conf.
147         // Currently, the BSSID is hardcoded in external/wpa_supplicant_8/src/drivers/driver_virtio_wifi.c. This should be configured by hostapd.conf and allow to be set by `Hostapd`.
148         let bssid_bytes: [u8; 6] = [0x00, 0x13, 0x10, 0x85, 0xfe, 0x01];
149         let bssid = MacAddress::from(&bssid_bytes);
150         Hostapd {
151             handle: RwLock::new(None),
152             verbose,
153             config,
154             config_path,
155             data_writer: None,
156             ctrl_writer: None,
157             tx_bytes,
158             runtime: Arc::new(Runtime::new().unwrap()),
159             bssid,
160         }
161     }
162 
163     /// Starts the `hostapd` main process and response thread.
164     ///
165     /// The "hostapd" thread manages the C `hostapd` process by running `run_hostapd_main`.
166     /// The "hostapd_response" thread manages traffic between `hostapd` and netsim.
167     ///
168     /// TODO:
169     /// * update as async fn.
run(&mut self) -> bool170     pub fn run(&mut self) -> bool {
171         // Check if already running
172         assert!(!self.is_running(), "hostapd is already running!");
173         // Setup config file
174         self.gen_config_file().unwrap_or_else(|_| {
175             panic!("Failed to generate config file: {:?}.", self.config_path.display())
176         });
177 
178         // Setup Sockets
179         let (ctrl_listener, _ctrl_reader, ctrl_writer) =
180             self.create_pipe().expect("Failed to create ctrl pipe");
181         self.ctrl_writer = Some(Mutex::new(ctrl_writer));
182         let (data_listener, data_reader, data_writer) =
183             self.create_pipe().expect("Failed to create data pipe");
184         self.data_writer = Some(Mutex::new(data_writer));
185 
186         // Start hostapd thread
187         let verbose = self.verbose;
188         let config_path = self.config_path.to_string_lossy().into_owned();
189         *self.handle.write().unwrap() = Some(
190             thread::Builder::new()
191                 .name("hostapd".to_string())
192                 .spawn(move || Self::hostapd_thread(verbose, config_path))
193                 .expect("Failed to spawn Hostapd thread"),
194         );
195 
196         // Start hostapd response thread
197         let tx_bytes = self.tx_bytes.clone();
198         let runtime = Arc::clone(&self.runtime);
199         let _ = thread::Builder::new()
200             .name("hostapd_response".to_string())
201             .spawn(move || {
202                 Self::hostapd_response_thread(
203                     data_listener,
204                     ctrl_listener,
205                     data_reader,
206                     tx_bytes,
207                     runtime,
208                 );
209             })
210             .expect("Failed to spawn hostapd_response thread");
211 
212         true
213     }
214 
215     /// Reconfigures `Hostapd` with the specified SSID (and password).
216     ///
217     /// TODO:
218     /// * implement password & encryption support
219     /// * update as async fn.
set_ssid( &mut self, ssid: impl Into<String>, password: impl Into<String>, ) -> anyhow::Result<()>220     pub fn set_ssid(
221         &mut self,
222         ssid: impl Into<String>,
223         password: impl Into<String>,
224     ) -> anyhow::Result<()> {
225         let ssid = ssid.into();
226         let password = password.into();
227         if ssid.is_empty() {
228             bail!("set_ssid must have a non-empty SSID");
229         }
230 
231         if !password.is_empty() {
232             bail!("set_ssid with password is not yet supported.");
233         }
234 
235         if ssid == self.get_ssid() && password == self.get_config_val("password") {
236             info!("SSID and password matches current configuration.");
237             return Ok(());
238         }
239 
240         // Update the config
241         self.config.insert("ssid".to_string(), ssid);
242         if !password.is_empty() {
243             let password_config = [
244                 ("wpa", "2"),
245                 ("wpa_key_mgmt", "WPA-PSK"),
246                 ("rsn_pairwise", "CCMP"),
247                 ("wpa_passphrase", &password),
248             ];
249             self.config.extend(password_config.iter().map(|(k, v)| (k.to_string(), v.to_string())));
250         }
251 
252         // Update the config file.
253         self.gen_config_file()?;
254 
255         // Send command for Hostapd to reload config file
256         if let Err(e) = self.runtime.block_on(Self::async_write(
257             self.ctrl_writer.as_ref().unwrap(),
258             c_string_to_bytes(VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG),
259         )) {
260             bail!("Failed to send VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG to hostapd to reload config: {:?}", e);
261         }
262 
263         Ok(())
264     }
265 
266     /// Retrieves the current SSID in the `Hostapd` configuration.
get_ssid(&self) -> String267     pub fn get_ssid(&self) -> String {
268         self.get_config_val("ssid")
269     }
270 
271     /// Retrieves the `Hostapd`'s BSSID.
get_bssid(&self) -> MacAddress272     pub fn get_bssid(&self) -> MacAddress {
273         self.bssid
274     }
275 
276     /// Attempt to encrypt the given IEEE 802.11 frame.
try_encrypt(&self, _ieee80211: &Ieee80211) -> Option<Ieee80211>277     pub fn try_encrypt(&self, _ieee80211: &Ieee80211) -> Option<Ieee80211> {
278         // TODO
279         None
280     }
281 
282     /// Attempt to decrypt the given IEEE 802.11 frame.
try_decrypt(&self, _ieee80211: &Ieee80211) -> Option<Ieee80211>283     pub fn try_decrypt(&self, _ieee80211: &Ieee80211) -> Option<Ieee80211> {
284         // TODO
285         None
286     }
287 
288     /// Inputs data packet bytes from netsim to `hostapd`.
289     ///
290     /// TODO:
291     /// * update as async fn.
input(&self, bytes: Bytes) -> anyhow::Result<()>292     pub fn input(&self, bytes: Bytes) -> anyhow::Result<()> {
293         // Make sure hostapd is already running
294         assert!(self.is_running(), "Failed to send input. Hostapd is not running.");
295         self.runtime.block_on(Self::async_write(self.data_writer.as_ref().unwrap(), &bytes))
296     }
297 
298     /// Checks whether the `hostapd` thread is running.
is_running(&self) -> bool299     pub fn is_running(&self) -> bool {
300         let handle_lock = self.handle.read().unwrap();
301         handle_lock.is_some() && !handle_lock.as_ref().unwrap().is_finished()
302     }
303 
304     /// Terminates the `Hostapd` process thread by sending a control command.
terminate(&self)305     pub fn terminate(&self) {
306         if !self.is_running() {
307             warn!("hostapd terminate() called when hostapd thread is not running");
308             return;
309         }
310 
311         // Send terminate command to hostapd
312         if let Err(e) = self.runtime.block_on(Self::async_write(
313             self.ctrl_writer.as_ref().unwrap(),
314             c_string_to_bytes(VIRTIO_WIFI_CTRL_CMD_TERMINATE),
315         )) {
316             warn!("Failed to send VIRTIO_WIFI_CTRL_CMD_TERMINATE to hostapd to terminate: {:?}", e);
317         }
318     }
319 
320     /// Generates the `hostapd.conf` file in the discovery directory.
gen_config_file(&self) -> anyhow::Result<()>321     fn gen_config_file(&self) -> anyhow::Result<()> {
322         let conf_file = File::create(self.config_path.clone())?; // Create or overwrite the file
323         let mut writer = BufWriter::new(conf_file);
324 
325         for (key, value) in &self.config {
326             writeln!(&mut writer, "{}={}", key, value)?;
327         }
328 
329         Ok(writer.flush()?) // Ensure all data is written to the file
330     }
331 
332     /// Gets the value of the given key in the config.
333     ///
334     /// Returns an empty String if the key is not found.
get_config_val(&self, key: &str) -> String335     fn get_config_val(&self, key: &str) -> String {
336         self.config.get(key).cloned().unwrap_or_default()
337     }
338 
339     /// Creates a pipe of two connected `TcpStream` objects.
340     ///
341     /// Extracts the first stream's raw descriptor and splits the second stream
342     /// into `OwnedReadHalf` and `OwnedWriteHalf`.
343     ///
344     /// # Returns
345     ///
346     /// * `Ok((listener, read_half, write_half))` if the pipe creation is successful.
347     /// * `Err(std::io::Error)` if an error occurs during pipe creation.
create_pipe( &self, ) -> anyhow::Result<(RawDescriptor, OwnedReadHalf, OwnedWriteHalf), std::io::Error>348     fn create_pipe(
349         &self,
350     ) -> anyhow::Result<(RawDescriptor, OwnedReadHalf, OwnedWriteHalf), std::io::Error> {
351         let (listener, stream) = self.runtime.block_on(Self::async_create_pipe())?;
352         let listener = into_raw_descriptor(listener);
353         let (read_half, write_half) = stream.into_split();
354         Ok((listener, read_half, write_half))
355     }
356 
357     /// Creates a pipe asynchronously.
async_create_pipe() -> anyhow::Result<(TcpStream, TcpStream), std::io::Error>358     async fn async_create_pipe() -> anyhow::Result<(TcpStream, TcpStream), std::io::Error> {
359         let listener = match TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await {
360             Ok(listener) => listener,
361             Err(e) => {
362                 // Support hosts that only have IPv6
363                 info!("Failed to bind to 127.0.0.1:0. Try to bind to [::1]:0 next. Err: {:?}", e);
364                 TcpListener::bind(SocketAddr::from((Ipv6Addr::LOCALHOST, 0))).await?
365             }
366         };
367         let addr = listener.local_addr()?;
368         let stream = TcpStream::connect(addr).await?;
369         let (listener, _) = listener.accept().await?;
370         Ok((listener, stream))
371     }
372 
373     /// Writes data to a writer asynchronously.
async_write(writer: &Mutex<OwnedWriteHalf>, data: &[u8]) -> anyhow::Result<()>374     async fn async_write(writer: &Mutex<OwnedWriteHalf>, data: &[u8]) -> anyhow::Result<()> {
375         let mut writer_guard = writer.lock().await;
376         writer_guard.write_all(data).await?;
377         writer_guard.flush().await?;
378         Ok(())
379     }
380 
381     /// Runs the C `hostapd` process with `run_hostapd_main`.
382     ///
383     /// This function is meant to be spawned in a separate thread.
hostapd_thread(verbose: bool, config_path: String)384     fn hostapd_thread(verbose: bool, config_path: String) {
385         let mut args = vec![CString::new("hostapd").unwrap()];
386         if verbose {
387             args.push(CString::new("-dddd").unwrap());
388         }
389         args.push(
390             CString::new(config_path.clone()).unwrap_or_else(|_| {
391                 panic!("CString::new error on config file path: {}", config_path)
392             }),
393         );
394         let argv: Vec<*const c_char> = args.iter().map(|arg| arg.as_ptr()).collect();
395         let argc = argv.len() as c_int;
396         // Safety: we ensure that argc is length of argv and argv.as_ptr() is a valid pointer of hostapd args
397         unsafe { run_hostapd_main(argc, argv.as_ptr()) };
398     }
399 
400     /// Sets the virtio (driver) data and control sockets.
set_virtio_driver_socket( data_descriptor: RawDescriptor, ctrl_descriptor: RawDescriptor, ) -> bool401     fn set_virtio_driver_socket(
402         data_descriptor: RawDescriptor,
403         ctrl_descriptor: RawDescriptor,
404     ) -> bool {
405         // Safety: we ensure that data_descriptor and ctrl_descriptor are valid i32 raw file descriptor or socket
406         unsafe {
407             set_virtio_sock(data_descriptor) == 0 && set_virtio_ctrl_sock(ctrl_descriptor) == 0
408         }
409     }
410 
411     /// Manages reading `hostapd` responses and sending them via `tx_bytes`.
412     ///
413     /// The thread first attempts to set virtio driver sockets with retries until success.
414     /// Next, the thread reads `hostapd` responses and writes them to netsim.
hostapd_response_thread( data_listener: RawDescriptor, ctrl_listener: RawDescriptor, mut data_reader: OwnedReadHalf, tx_bytes: mpsc::Sender<Bytes>, runtime: Arc<Runtime>, )415     fn hostapd_response_thread(
416         data_listener: RawDescriptor,
417         ctrl_listener: RawDescriptor,
418         mut data_reader: OwnedReadHalf,
419         tx_bytes: mpsc::Sender<Bytes>,
420         runtime: Arc<Runtime>,
421     ) {
422         let mut buf: [u8; 1500] = [0u8; 1500];
423         loop {
424             if !Self::set_virtio_driver_socket(data_listener, ctrl_listener) {
425                 warn!("Unable to set virtio driver socket. Retrying...");
426                 sleep(Duration::from_millis(250));
427                 continue;
428             };
429             break;
430         }
431         loop {
432             let size = match runtime.block_on(async { data_reader.read(&mut buf[..]).await }) {
433                 Ok(size) => size,
434                 Err(e) => {
435                     warn!("Failed to read hostapd response: {:?}", e);
436                     break;
437                 }
438             };
439 
440             if let Err(e) = tx_bytes.send(Bytes::copy_from_slice(&buf[..size])) {
441                 warn!("Failed to send hostapd packet response: {:?}", e);
442                 break;
443             };
444         }
445     }
446 }
447 
448 impl Drop for Hostapd {
449     /// Terminates the `hostapd` process when the `Hostapd` instance is dropped.
drop(&mut self)450     fn drop(&mut self) {
451         self.terminate();
452     }
453 }
454 
455 /// Converts a `TcpStream` to a `RawDescriptor` (i32).
into_raw_descriptor(stream: TcpStream) -> RawDescriptor456 fn into_raw_descriptor(stream: TcpStream) -> RawDescriptor {
457     let std_stream = stream.into_std().expect("into_raw_descriptor's into_std() failed");
458     // hostapd fd expects blocking, but rust set non-blocking for async
459     std_stream.set_nonblocking(false).expect("non-blocking");
460 
461     // Use into_raw_fd for Unix to pass raw file descriptor to C
462     #[cfg(unix)]
463     return std_stream.into_raw_fd();
464 
465     // Use into_raw_socket for Windows to pass raw socket to C
466     #[cfg(windows)]
467     std_stream.into_raw_socket().try_into().expect("Failed to convert Raw Socket value into i32")
468 }
469 
470 /// Converts a null-terminated c-string slice into `&[u8]` bytes without the null terminator.
c_string_to_bytes(c_string: &[u8]) -> &[u8]471 fn c_string_to_bytes(c_string: &[u8]) -> &[u8] {
472     CStr::from_bytes_with_nul(c_string).unwrap().to_bytes()
473 }
474