Skip to main content

donut_lib/
opts.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use clap::{Parser, Subcommand};
6use eui48::MacAddress;
7use flex_fuchsia_wlan_common as wlan_common;
8use flex_fuchsia_wlan_policy as wlan_policy;
9
10#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
11pub enum RoleArg {
12    Client,
13    Ap,
14}
15
16#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
17pub enum ScanTypeArg {
18    Active,
19    Passive,
20}
21
22#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
23pub enum SecurityTypeArg {
24    None,
25    Wep,
26    Wpa,
27    Wpa2,
28    Wpa3,
29}
30
31#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
32pub enum CredentialTypeArg {
33    None,
34    Psk,
35    Password,
36}
37
38impl From<RoleArg> for wlan_common::WlanMacRole {
39    fn from(arg: RoleArg) -> Self {
40        match arg {
41            RoleArg::Client => wlan_common::WlanMacRole::Client,
42            RoleArg::Ap => wlan_common::WlanMacRole::Ap,
43        }
44    }
45}
46
47impl From<ScanTypeArg> for wlan_common::ScanType {
48    fn from(arg: ScanTypeArg) -> Self {
49        match arg {
50            ScanTypeArg::Active => wlan_common::ScanType::Active,
51            ScanTypeArg::Passive => wlan_common::ScanType::Passive,
52        }
53    }
54}
55
56impl From<SecurityTypeArg> for wlan_policy::SecurityType {
57    fn from(arg: SecurityTypeArg) -> Self {
58        match arg {
59            SecurityTypeArg::r#None => wlan_policy::SecurityType::None,
60            SecurityTypeArg::Wep => wlan_policy::SecurityType::Wep,
61            SecurityTypeArg::Wpa => wlan_policy::SecurityType::Wpa,
62            SecurityTypeArg::Wpa2 => wlan_policy::SecurityType::Wpa2,
63            SecurityTypeArg::Wpa3 => wlan_policy::SecurityType::Wpa3,
64        }
65    }
66}
67
68impl From<PolicyNetworkConfig> for wlan_policy::NetworkConfig {
69    fn from(arg: PolicyNetworkConfig) -> Self {
70        let credential = match arg.credential_type {
71            Some(CredentialTypeArg::r#None) => wlan_policy::Credential::None(wlan_policy::Empty),
72            Some(CredentialTypeArg::Psk) => {
73                wlan_policy::Credential::Psk(parse_psk_string(arg.credential.unwrap()))
74            }
75            Some(CredentialTypeArg::Password) => {
76                wlan_policy::Credential::Password(arg.credential.unwrap().as_bytes().to_vec())
77            }
78            None => {
79                // If credential type is not provided, infer it from the credential value.
80                credential_from_string(arg.credential.unwrap_or_else(|| "".to_string()))
81            }
82        };
83
84        let security_type = security_type_from_args(arg.security_type, &credential);
85
86        let network_id = wlan_policy::NetworkIdentifier {
87            ssid: arg.ssid.as_bytes().to_vec(),
88            type_: security_type,
89        };
90        wlan_policy::NetworkConfig {
91            id: Some(network_id),
92            credential: Some(credential),
93            ..Default::default()
94        }
95    }
96}
97
98/// Parse the hexadecimal characters to bytes if a valid PSK is provided, or panic with an error
99/// message if the format is invalid.
100fn parse_psk_string(credential: String) -> Vec<u8> {
101    let psk_arg = credential.as_bytes().to_vec();
102    hex::decode(psk_arg).expect(
103        "Error: PSK must be 64 hexadecimal characters.\
104        Example: \"123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234\"",
105    )
106}
107
108/// Build a WLAN policy FIDL type credential from a string. PSK will be given in hexadecimal.
109/// This panics if the string does not represent a valid credential.
110fn credential_from_string(credential: String) -> wlan_policy::Credential {
111    match credential.len() {
112        0 => wlan_policy::Credential::None(wlan_policy::Empty),
113        0..=63 => wlan_policy::Credential::Password(credential.into_bytes()),
114        64 => wlan_policy::Credential::Psk(parse_psk_string(credential)),
115        65..=usize::MAX => {
116            panic!(
117                "Provided credential is too long. A password must be between 0 and 63 \
118                characters and a PSK must be 64 hexadecimal characters. Provided \
119                credential is {} characters.",
120                credential.len()
121            );
122        }
123        _ => {
124            // This shouldn't happen; all possible lengths should be handled above.
125            panic!("Invalid credential of length {}", credential.len())
126        }
127    }
128}
129
130/// Convert the security type provided as an argument, or use a default type that matches the
131/// provided credential.
132fn security_type_from_args(
133    security_arg: Option<SecurityTypeArg>,
134    credential: &wlan_policy::Credential,
135) -> wlan_policy::SecurityType {
136    if let Some(arg) = security_arg {
137        match arg {
138            SecurityTypeArg::Wep => wlan_policy::SecurityType::Wep,
139            SecurityTypeArg::Wpa => wlan_policy::SecurityType::Wpa,
140            SecurityTypeArg::Wpa2 => wlan_policy::SecurityType::Wpa2,
141            SecurityTypeArg::Wpa3 => wlan_policy::SecurityType::Wpa3,
142            SecurityTypeArg::r#None => wlan_policy::SecurityType::None,
143        }
144    } else {
145        match credential {
146            wlan_policy::Credential::None(_) => wlan_policy::SecurityType::None,
147            _ => wlan_policy::SecurityType::Wpa2,
148        }
149    }
150}
151
152#[derive(clap::Args, Clone, Debug)]
153pub struct PolicyNetworkConfig {
154    #[arg(long)]
155    pub ssid: String,
156    #[arg(long = "security-type", value_enum, ignore_case = true)]
157    pub security_type: Option<SecurityTypeArg>,
158    #[arg(long = "credential-type", value_enum, ignore_case = true)]
159    pub credential_type: Option<CredentialTypeArg>,
160    #[arg(long)]
161    pub credential: Option<String>,
162}
163
164#[derive(clap::Args, Clone, Debug)]
165pub struct ForgetArgs {
166    #[arg(long)]
167    pub ssid: String,
168    #[arg(long = "security-type", value_enum, ignore_case = true)]
169    pub security_type: Option<SecurityTypeArg>,
170}
171
172impl ForgetArgs {
173    pub fn parse_security(&self) -> Option<wlan_policy::SecurityType> {
174        self.security_type.map(|s| s.into())
175    }
176}
177
178#[derive(clap::Args, Clone, Debug)]
179pub struct SaveNetworkArgs {
180    #[arg(long)]
181    pub ssid: String,
182    #[arg(long = "security-type", value_enum, ignore_case = true)]
183    pub security_type: SecurityTypeArg,
184    #[arg(long = "credential-type", value_enum, ignore_case = true)]
185    pub credential_type: Option<CredentialTypeArg>,
186    #[arg(long)]
187    pub credential: String,
188}
189
190#[derive(clap::Args, Clone, Debug)]
191pub struct ConnectArgs {
192    #[arg(long)]
193    pub ssid: String,
194    #[arg(long = "security-type", value_enum, ignore_case = true)]
195    pub security_type: Option<SecurityTypeArg>,
196}
197
198#[derive(Subcommand, Clone, Debug)]
199pub enum PolicyClientCmd {
200    #[command(name = "connect")]
201    Connect(ConnectArgs),
202    #[command(name = "list-saved-networks")]
203    GetSavedNetworks,
204    #[command(name = "listen")]
205    Listen,
206    #[command(name = "forget-network")]
207    ForgetNetwork(ForgetArgs),
208    #[command(name = "save-network")]
209    SaveNetwork(PolicyNetworkConfig),
210    #[command(name = "scan")]
211    ScanForNetworks,
212    #[command(name = "start-client-connections")]
213    StartClientConnections,
214    #[command(name = "stop-client-connections")]
215    StopClientConnections,
216    #[command(name = "dump-config")]
217    DumpConfig,
218    #[command(name = "restore-config")]
219    RestoreConfig { serialized_config: String },
220    #[command(name = "status")]
221    Status,
222}
223
224#[derive(Subcommand, Clone, Debug)]
225pub enum PolicyAccessPointCmd {
226    // TODO(sakuma): Allow users to specify connectivity mode and operating band.
227    #[command(name = "start")]
228    Start(PolicyNetworkConfig),
229    #[command(name = "stop")]
230    Stop(PolicyNetworkConfig),
231    #[command(name = "stop-all")]
232    StopAllAccessPoints,
233    #[command(name = "listen")]
234    Listen,
235    #[command(name = "status")]
236    Status,
237}
238
239#[derive(Subcommand, Clone, Debug)]
240pub enum DeprecatedConfiguratorCmd {
241    #[command(name = "suggest-mac")]
242    SuggestAccessPointMacAddress {
243        #[arg(required = true)]
244        mac: MacAddress,
245    },
246}
247
248#[derive(Parser, Clone, Debug)]
249pub enum Opt {
250    #[command(subcommand, name = "client")]
251    Client(PolicyClientCmd),
252    #[command(subcommand, name = "ap")]
253    AccessPoint(PolicyAccessPointCmd),
254    #[command(subcommand, name = "deprecated")]
255    Deprecated(DeprecatedConfiguratorCmd),
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    /// Tests that a WEP network config will be correctly translated for save and remove network.
263    #[fuchsia::test]
264    fn test_construct_config_wep() {
265        test_construct_config_security(wlan_policy::SecurityType::Wep, SecurityTypeArg::Wep);
266    }
267
268    /// Tests that a WPA network config will be correctly translated for save and remove network.
269    #[fuchsia::test]
270    fn test_construct_config_wpa() {
271        test_construct_config_security(wlan_policy::SecurityType::Wpa, SecurityTypeArg::Wpa);
272    }
273
274    /// Tests that a WPA2 network config will be correctly translated for save and remove network.
275    #[fuchsia::test]
276    fn test_construct_config_wpa2() {
277        test_construct_config_security(wlan_policy::SecurityType::Wpa2, SecurityTypeArg::Wpa2);
278    }
279
280    /// Tests that a WPA3 network config will be correctly translated for save and remove network.
281    #[fuchsia::test]
282    fn test_construct_config_wpa3() {
283        test_construct_config_security(wlan_policy::SecurityType::Wpa3, SecurityTypeArg::Wpa3);
284    }
285
286    /// Tests that a config for an open network will be correctly translated to FIDL values for
287    /// save and remove network when no security type and credential type are omitted.
288    #[fuchsia::test]
289    fn test_construct_config_open() {
290        let open_config = PolicyNetworkConfig {
291            ssid: "some_ssid".to_string(),
292            security_type: None,
293            credential_type: None,
294            credential: Some("".to_string()),
295        };
296        let expected_cfg = wlan_policy::NetworkConfig {
297            id: Some(wlan_policy::NetworkIdentifier {
298                ssid: "some_ssid".as_bytes().to_vec(),
299                type_: wlan_policy::SecurityType::None,
300            }),
301            credential: Some(wlan_policy::Credential::None(wlan_policy::Empty {})),
302            ..Default::default()
303        };
304        let result_cfg = wlan_policy::NetworkConfig::from(open_config);
305        assert_eq!(expected_cfg, result_cfg);
306    }
307
308    /// Tests that a config for an open network will be correctly translated to FIDL values for
309    /// save and remove network when credential type and security type are specified.
310    #[fuchsia::test]
311    fn test_construct_config_open_with_omitted_args() {
312        let open_config = PolicyNetworkConfig {
313            ssid: "some_ssid".to_string(),
314            security_type: Some(SecurityTypeArg::None),
315            credential_type: Some(CredentialTypeArg::None),
316            credential: Some("".to_string()),
317        };
318        let expected_cfg = wlan_policy::NetworkConfig {
319            id: Some(wlan_policy::NetworkIdentifier {
320                ssid: "some_ssid".as_bytes().to_vec(),
321                type_: wlan_policy::SecurityType::None,
322            }),
323            credential: Some(wlan_policy::Credential::None(wlan_policy::Empty {})),
324            ..Default::default()
325        };
326        let result_cfg = wlan_policy::NetworkConfig::from(open_config);
327        assert_eq!(expected_cfg, result_cfg);
328    }
329
330    /// Test the case where a config is saved with SSID and password, but no security type or
331    /// credential type provided. This is a common usage of the tool.
332    #[fuchsia::test]
333    fn test_construct_config_password_provided_no_security() {
334        let password = "mypassword";
335        let ssid = "some_ssid";
336        let arg_config = PolicyNetworkConfig {
337            ssid: ssid.to_string(),
338            security_type: None,
339            credential_type: None,
340            credential: Some(password.to_string()),
341        };
342        let expected_cfg = wlan_policy::NetworkConfig {
343            id: Some(wlan_policy::NetworkIdentifier {
344                ssid: ssid.as_bytes().to_vec(),
345                type_: wlan_policy::SecurityType::Wpa2,
346            }),
347            credential: Some(wlan_policy::Credential::Password(password.as_bytes().to_vec())),
348            ..Default::default()
349        };
350        let result_cfg = wlan_policy::NetworkConfig::from(arg_config);
351        assert_eq!(expected_cfg, result_cfg);
352    }
353
354    /// Test the case where a config is saved with SSID and psk, but no security type or
355    /// credential type provided.
356    #[fuchsia::test]
357    fn test_construct_config_psk_provided_no_security() {
358        let psk = "123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234".to_string();
359        let psk_bytes = hex::decode(psk.as_bytes().to_vec()).unwrap();
360        let ssid = "some_ssid";
361        let arg_config = PolicyNetworkConfig {
362            ssid: ssid.to_string(),
363            security_type: None,
364            credential_type: None,
365            credential: Some(psk),
366        };
367        let expected_cfg = wlan_policy::NetworkConfig {
368            id: Some(wlan_policy::NetworkIdentifier {
369                ssid: ssid.as_bytes().to_vec(),
370                type_: wlan_policy::SecurityType::Wpa2,
371            }),
372            credential: Some(wlan_policy::Credential::Psk(psk_bytes)),
373            ..Default::default()
374        };
375        let result_cfg = wlan_policy::NetworkConfig::from(arg_config);
376        assert_eq!(expected_cfg, result_cfg);
377    }
378
379    /// Test that a config with a PSK will be translated correctly, including a transfer from a
380    /// hex string to bytes.
381    #[fuchsia::test]
382    fn test_construct_config_psk() {
383        // Test PSK separately since it has a unique credential
384        const ASCII_ZERO: u8 = 49;
385        let psk =
386            String::from_utf8([ASCII_ZERO; 64].to_vec()).expect("Failed to create PSK test value");
387        let wpa_config = PolicyNetworkConfig {
388            ssid: "some_ssid".to_string(),
389            security_type: Some(SecurityTypeArg::Wpa2),
390            credential_type: Some(CredentialTypeArg::Psk),
391            credential: Some(psk),
392        };
393        let expected_cfg = wlan_policy::NetworkConfig {
394            id: Some(wlan_policy::NetworkIdentifier {
395                ssid: "some_ssid".as_bytes().to_vec(),
396                type_: wlan_policy::SecurityType::Wpa2,
397            }),
398            credential: Some(wlan_policy::Credential::Psk([17; 32].to_vec())),
399            ..Default::default()
400        };
401        let result_cfg = wlan_policy::NetworkConfig::from(wpa_config);
402        assert_eq!(expected_cfg, result_cfg);
403    }
404
405    /// Test that the given variant of security type with a password works when constructing
406    /// network configs as used by save and remove network.
407    fn test_construct_config_security(
408        fidl_type: wlan_policy::SecurityType,
409        tool_type: SecurityTypeArg,
410    ) {
411        let args_config = PolicyNetworkConfig {
412            ssid: "some_ssid".to_string(),
413            security_type: Some(tool_type),
414            credential_type: Some(CredentialTypeArg::Password),
415            credential: Some("some_password_here".to_string()),
416        };
417        let expected_cfg = wlan_policy::NetworkConfig {
418            id: Some(wlan_policy::NetworkIdentifier {
419                ssid: "some_ssid".as_bytes().to_vec(),
420                type_: fidl_type,
421            }),
422            credential: Some(wlan_policy::Credential::Password(
423                "some_password_here".as_bytes().to_vec(),
424            )),
425            ..Default::default()
426        };
427        let result_cfg = wlan_policy::NetworkConfig::from(args_config);
428        assert_eq!(expected_cfg, result_cfg);
429    }
430}