1use std::io::Write as _;
8use std::path::PathBuf;
9use std::str::FromStr as _;
10
11use futures::StreamExt as _;
12use futures::stream::FuturesOrdered;
13
14use flex_client::ProxyHasDomain;
15use flex_fuchsia_ebpf as febpf;
16use flex_fuchsia_io as fio;
17use flex_fuchsia_net as fnet;
18use flex_fuchsia_net_debug as fnet_debug;
19use flex_fuchsia_net_interfaces as fnet_interfaces;
20use zx_status;
21
22#[cfg(not(feature = "fdomain"))]
23use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
24#[cfg(feature = "fdomain")]
25use fidl_fuchsia_net_interfaces_ext_fdomain as fnet_interfaces_ext;
26
27use anyhow::Context as _;
28use writer::ToolIO as _;
29
30use crate::CaptureDeps;
31use crate::opts::capture::{StartRollingCommand, StopRollingCommand};
32
33pub const DEFAULT_CAPTURE_NAME: &str = "capture";
35const DEFAULT_OUTPUT_SUBDIR: &str = "pcap";
36
37#[derive(Debug, PartialEq, Eq)]
39enum CaptureInterfaceSpec {
40 Id(std::num::NonZeroU64),
42 Name(String),
44 Ip(std::net::IpAddr),
46}
47
48impl CaptureInterfaceSpec {
49 fn parse(s: &str) -> Result<Self, anyhow::Error> {
51 if let Some(id_str) = s.strip_prefix("id:") {
52 let id = id_str
53 .parse::<std::num::NonZeroU64>()
54 .context("failed to parse interface ID as non-zero integer")?;
55 return Ok(Self::Id(id));
56 }
57 if let Some(name_str) = s.strip_prefix("name:") {
58 return Ok(Self::Name(name_str.to_string()));
59 }
60 if let Some(ip_str) = s.strip_prefix("ip:") {
61 let ip = std::net::IpAddr::from_str(ip_str).context("failed to parse IP address")?;
62 return Ok(Self::Ip(ip));
63 }
64 anyhow::bail!(
65 "Invalid interface format. Interface must start with 'id:', 'name:', or 'ip:' prefix \
66 (e.g. 'name:lo')"
67 )
68 }
69}
70
71pub async fn start_rolling<C>(connector: &C, cmd: StartRollingCommand) -> Result<(), anyhow::Error>
73where
74 C: crate::NetCliDepsConnector,
75{
76 let StartRollingCommand { name, interface, pcap_filter, snap_len, capture_size } = cmd;
77 let name = name.unwrap_or_else(|| DEFAULT_CAPTURE_NAME.to_string());
78
79 let spec = CaptureInterfaceSpec::parse(&interface).map_err(|e| {
80 crate::user_facing_error(format!("Failed to parse interface specifier: {e:?}"))
81 })?;
82
83 let get_interfaces = async {
84 let state_proxy = crate::connect_with_context::<fnet_interfaces::StateMarker, _>(connector)
85 .await
86 .context("Failed to connect to interfaces state")?;
87 let stream =
88 fnet_interfaces_ext::event_stream_from_state::<fnet_interfaces_ext::AllInterest>(
89 &state_proxy,
90 fnet_interfaces_ext::WatchOptions {
91 included_addresses: fnet_interfaces_ext::IncludedAddresses::All,
92 ..Default::default()
93 },
94 )
95 .context("Failed to get watcher stream")?;
96 fnet_interfaces_ext::existing(
97 stream,
98 std::collections::HashMap::<
99 std::num::NonZeroU64,
100 fnet_interfaces_ext::PropertiesAndState<(), fnet_interfaces_ext::AllInterest>,
101 >::new(),
102 )
103 .await
104 .context("Failed to list interfaces")
105 };
106 let interface_id = match spec {
107 CaptureInterfaceSpec::Id(id) => id.get(),
108 CaptureInterfaceSpec::Name(name) => {
109 let id = get_interfaces
110 .await?
111 .values()
112 .find_map(|p| (&p.properties.name == &name).then_some(p.properties.id))
113 .ok_or_else(|| {
114 crate::user_facing_error(format!("No interface found with name '{name}'"))
115 })?;
116 id.get()
117 }
118 CaptureInterfaceSpec::Ip(ip) => {
119 let id = get_interfaces
120 .await?
121 .values()
122 .find_map(|p| {
123 let has_ip = p.properties.addresses.iter().any(|a| match a.addr.addr {
124 fnet::IpAddress::Ipv4(v4) => std::net::IpAddr::V4(v4.addr.into()) == ip,
125 fnet::IpAddress::Ipv6(v6) => std::net::IpAddr::V6(v6.addr.into()) == ip,
126 });
127 has_ip.then_some(p.properties.id)
128 })
129 .ok_or_else(|| {
130 crate::user_facing_error(format!(
131 "No interface found with IP address '{interface}'"
132 ))
133 })?;
134 id.get()
135 }
136 };
137
138 let interfaces = fnet_debug::InterfaceSpecifier::InterfaceIds(vec![interface_id]);
142
143 let bpf_program = pcap_filter
144 .map(|filter_str| {
145 let fidl_fuchsia_ebpf::VerifiedProgram {
146 code,
147 struct_access_instructions,
148 maps,
149 __source_breaking,
150 } = pcap::compile::compile_filter(&filter_str).map_err(|e| {
151 crate::user_facing_error(format!(
152 "Failed to compile pcap filter '{filter_str}': {e:?}"
153 ))
154 })?;
155 assert!(maps.unwrap().is_empty());
156 assert!(struct_access_instructions.unwrap().is_empty());
157 Ok::<_, anyhow::Error>(febpf::VerifiedProgram {
158 code,
159 struct_access_instructions: Some(Vec::new()),
160 maps: Some(Vec::new()),
161 ..Default::default()
162 })
163 })
164 .transpose()?;
165
166 let common_params = fnet_debug::CommonPacketCaptureParams {
167 interfaces: Some(interfaces),
168 bpf_program,
169 snap_len,
170 ..Default::default()
171 };
172
173 let provider =
174 crate::connect_with_context::<fnet_debug::PacketCaptureProviderMarker, _>(connector)
175 .await
176 .context("Failed to connect to packet capture provider")?;
177 let rolling_params =
178 fnet_debug::RollingPacketCaptureParams { capture_size, ..Default::default() };
179 let rolling_client = provider
180 .start_rolling(common_params, &rolling_params)
181 .await
182 .context("FIDL error calling StartRolling")?
183 .map_err(|e| crate::user_facing_error(format!("StartRolling error: {e:?}")))?
184 .into_proxy();
185
186 rolling_client
187 .detach(&name)
188 .await
189 .context("FIDL error calling Detach")?
190 .map_err(|e| crate::user_facing_error(format!("Detach failed with error: {e:?}")))?;
191
192 Ok(())
193}
194
195pub async fn stop_rolling<C, D>(
197 connector: &C,
198 deps: &D,
199 cmd: StopRollingCommand,
200) -> Result<Option<std::path::PathBuf>, anyhow::Error>
201where
202 C: crate::NetCliDepsConnector,
203 D: CaptureDeps,
204{
205 let StopRollingCommand { name, output, skip_download } = cmd;
206
207 if skip_download && output.is_some() {
208 return Err(crate::user_facing_error("Cannot specify both --skip-download and --output"));
209 }
210
211 let name = name.unwrap_or_else(|| DEFAULT_CAPTURE_NAME.to_string());
212
213 let provider =
214 crate::connect_with_context::<fnet_debug::PacketCaptureProviderMarker, _>(connector)
215 .await
216 .context("Failed to connect to packet capture provider")?;
217 let rolling_client = provider
218 .reconnect_rolling(&name)
219 .await
220 .context("FIDL error calling ReconnectRolling")?
221 .map_err(|e| {
222 crate::user_facing_error(format!("ReconnectRolling to name '{name}' failed: {e:?}"))
223 })?
224 .into_proxy();
225
226 if skip_download {
227 rolling_client.discard().await.context("Failed to discard rolling pcap")?;
228 return Ok(None);
229 }
230
231 let output_path = match output {
232 Some(path) => PathBuf::from(path),
233 None => {
234 let dir = std::env::temp_dir().join(DEFAULT_OUTPUT_SUBDIR);
235 dir.join(format!("{name}.pcapng"))
236 }
237 };
238
239 let download_res = async {
240 let client = rolling_client.domain();
241 let (file_proxy, file_server) = client.create_proxy::<fio::FileMarker>();
242 rolling_client.stop_and_download(file_server).context("Failed to call StopAndDownload")?;
243
244 let mut file = deps.create_output_writer(&output_path).map_err(|e| {
245 crate::user_facing_error(format!(
246 "Failed to create output file {}: {e:?}",
247 output_path.display()
248 ))
249 })?;
250
251 let (_mutable_attributes, immutable_attributes) = file_proxy
252 .get_attributes(fio::NodeAttributesQuery::CONTENT_SIZE)
253 .await
254 .context("Failed get_attributes wire call")?
255 .map_err(zx_status::Status::err_from_raw)
256 .context("Failed to get attributes of file")?;
257 let content_size = immutable_attributes
258 .content_size
259 .ok_or_else(|| anyhow::anyhow!("Failed to get content size of file"))?;
260
261 let mut queue = FuturesOrdered::new();
262 const CONCURRENT_READS: usize = 16;
263
264 for _ in 0..CONCURRENT_READS {
265 queue.push_back(file_proxy.read(fio::MAX_BUF));
266 }
267
268 let mut bytes_written = 0;
269 loop {
270 let data = queue
271 .next()
272 .await
273 .expect("read queue should never exhaust")
274 .context("FIDL error reading packet capture")?
275 .map_err(zx_status::Status::err_from_raw)
276 .context("Failed to read packet capture")?;
277 if data.is_empty() {
278 file.flush().context("Failed to flush packet capture to file")?;
279 break;
280 }
281 file.write_all(&data).context("Failed to write packet capture to file")?;
282 bytes_written += data.len();
283 queue.push_back(file_proxy.read(fio::MAX_BUF));
284 }
285
286 if u64::try_from(bytes_written).expect("bytes written does not fit into u64")
287 != content_size
288 {
289 return Err(anyhow::anyhow!(
290 "Download mismatch: Expected {} bytes, but instead read {} bytes",
291 content_size,
292 bytes_written
293 ));
294 }
295 Ok(())
296 }
297 .await;
298
299 if let Err(e) = download_res {
300 if let Err(e) = std::fs::remove_file(&output_path) {
301 log::warn!("Failed to delete partial file {output_path:?}: {e:?}");
302 }
303 return Err(e);
304 }
305
306 rolling_client.discard().await.context("Failed to discard rolling pcap")?;
307
308 Ok(Some(output_path))
309}
310
311pub async fn do_capture<C, D>(
312 mut out: writer::JsonWriter<serde_json::Value>,
313 crate::opts::capture::Capture { capture_cmd }: crate::opts::capture::Capture,
314 connector: &C,
315 deps: &D,
316) -> Result<(), anyhow::Error>
317where
318 C: crate::NetCliDepsConnector,
319 D: CaptureDeps,
320{
321 match capture_cmd {
322 crate::opts::capture::CaptureEnum::StartRolling(cmd) => start_rolling(connector, cmd).await,
323 crate::opts::capture::CaptureEnum::StopRolling(cmd) => {
324 let path = stop_rolling(connector, deps, cmd).await?;
325 if let Some(path) = path {
326 if out.is_machine() {
327 out.machine(&serde_json::json!({ "path": path.display().to_string() }))?;
328 } else {
329 out.line(format!("{}", path.display()))?;
330 }
331 } else {
332 if out.is_machine() {
333 out.machine(&serde_json::json!({ "path": null }))?;
334 }
335 }
336 Ok(())
337 }
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use assert_matches::assert_matches;
345
346 #[test]
347 fn test_parse_interface_spec() {
348 assert_eq!(
349 CaptureInterfaceSpec::parse("id:12").unwrap(),
350 CaptureInterfaceSpec::Id(std::num::NonZeroU64::new(12).unwrap())
351 );
352 assert_matches!(CaptureInterfaceSpec::parse("id:0"), Err(_));
353 assert_matches!(CaptureInterfaceSpec::parse("id:abc"), Err(_));
354
355 assert_eq!(
356 CaptureInterfaceSpec::parse("name:loopback").unwrap(),
357 CaptureInterfaceSpec::Name("loopback".to_string())
358 );
359
360 assert_eq!(
361 CaptureInterfaceSpec::parse("ip:127.0.0.1").unwrap(),
362 CaptureInterfaceSpec::Ip(net_declare::std_ip!("127.0.0.1"))
363 );
364 assert_eq!(
365 CaptureInterfaceSpec::parse("ip:fe80::1").unwrap(),
366 CaptureInterfaceSpec::Ip(net_declare::std_ip!("fe80::1"))
367 );
368
369 assert_matches!(CaptureInterfaceSpec::parse("ip:invalid_ip"), Err(_));
370
371 assert_matches!(CaptureInterfaceSpec::parse("loopback"), Err(_));
372 }
373}