1use crate::cli::config::resolve_workers;
4use crate::custom_module::manycastr::{
5 Address, CliMessage, Configuration, LiveTarget, ProtocolType, TargetBatch, cli_message,
6};
7use crate::{ALL_ORIGINS, ALL_WORKERS};
8use bimap::BiHashMap;
9use futures_core::Stream;
10use log::warn;
11use std::collections::HashMap;
12use std::io::BufRead;
13use std::pin::Pin;
14use std::task::{Context, Poll};
15use tokio::sync::mpsc;
16
17pub const FEED_CHANNEL_SIZE: usize = 1024;
19
20pub struct FeedOrigin {
22 pub is_v6: bool,
24 pub p_type: ProtocolType,
26}
27
28impl FeedOrigin {
29 fn supports_sessions(&self) -> bool {
31 matches!(self.p_type, ProtocolType::Icmp | ProtocolType::ADns)
32 }
33}
34
35pub struct FeedOrigins {
37 by_id: HashMap<u32, FeedOrigin>,
39 default_v4: Option<u32>,
41 default_v6: Option<u32>,
43}
44
45impl FeedOrigins {
46 pub fn new(configurations: &[Configuration]) -> Self {
49 let mut by_id = HashMap::new();
50 let mut default_v4 = None;
51 let mut default_v6 = None;
52 for origin in configurations.iter().filter_map(|c| c.origin) {
53 by_id.entry(origin.origin_id).or_insert(FeedOrigin {
54 is_v6: origin.is_v6(),
55 p_type: origin.p_type(),
56 });
57 let default = if origin.is_v6() {
58 &mut default_v6
59 } else {
60 &mut default_v4
61 };
62 default.get_or_insert(origin.origin_id);
63 }
64 Self {
65 by_id,
66 default_v4,
67 default_v6,
68 }
69 }
70
71 fn default_for(&self, is_v6: bool) -> Option<u32> {
74 let default = if is_v6 {
75 self.default_v6
76 } else {
77 self.default_v4
78 };
79 if default.is_none() {
80 warn!(
81 "[CLI] No {} origin is configured.",
82 if is_v6 { "IPv6" } else { "IPv4" }
83 );
84 }
85 default
86 }
87
88 fn has_version(&self, is_v6: bool) -> bool {
90 self.by_id.values().any(|origin| origin.is_v6 == is_v6)
91 }
92}
93
94pub struct FeedStream {
96 pub(crate) inner: mpsc::Receiver<CliMessage>,
97}
98
99impl Stream for FeedStream {
101 type Item = CliMessage;
102
103 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
104 self.inner.poll_recv(cx)
105 }
106}
107
108pub fn read_stdin_feed(
126 feed_tx: mpsc::Sender<CliMessage>,
127 worker_map: BiHashMap<u32, String>,
128 origins: FeedOrigins,
129 is_trace: bool, is_sessions: bool, ) {
132 let stdin = std::io::stdin();
133 for line in stdin.lock().lines() {
134 let Ok(line) = line else {
135 break;
136 };
137 let line = line.trim();
138 if line.is_empty() {
139 continue;
140 }
141
142 let Some(target) = parse_feed_line(line, &worker_map, &origins, is_trace, is_sessions)
143 else {
144 warn!("[CLI] Skipping invalid feed line: {line}");
145 continue;
146 };
147
148 let addr = target.dst.expect("parsed target always has a dst");
149 if !origins.has_version(addr.is_v6()) {
151 warn!(
152 "[CLI] Skipping target {addr}: no {} origin is configured",
153 if addr.is_v6() { "IPv6" } else { "IPv4" }
154 );
155 continue;
156 }
157
158 let msg = CliMessage {
159 message: Some(cli_message::Message::Targets(TargetBatch {
160 targets: vec![target],
161 })),
162 };
163 if feed_tx.blocking_send(msg).is_err() {
164 break; }
166 }
167}
168
169fn parse_feed_line(
174 line: &str,
175 worker_map: &BiHashMap<u32, String>,
176 origins: &FeedOrigins,
177 is_trace: bool,
178 is_sessions: bool,
179) -> Option<LiveTarget> {
180 if !line.starts_with('{') {
182 let dst = line.parse::<Address>().ok()?;
183 return Some(LiveTarget {
184 dst: Some(dst),
185 worker_ids: Vec::new(), origin_id: origins.default_for(dst.is_v6())?,
187 nprobes: 1, ttl: 0, session_id: 0, });
191 }
192
193 parse_feed_object(line, worker_map, origins, is_trace, is_sessions)
195}
196
197fn parse_feed_object(
200 line: &str,
201 worker_map: &BiHashMap<u32, String>,
202 origins: &FeedOrigins,
203 is_trace: bool,
204 is_sessions: bool,
205) -> Option<LiveTarget> {
206 let value: serde_json::Value = serde_json::from_str(line).ok()?;
207 let dst = value.get("dst")?.as_str()?.parse::<Address>().ok()?;
208 let worker_ids = match value.get("worker") {
209 None => Vec::new(), Some(worker) => parse_worker(worker, worker_map)?,
211 };
212 let origin_id = match value.get("origin") {
213 None => origins.default_for(dst.is_v6())?,
214 Some(origin) => parse_origin(origin, origins, dst.is_v6())?,
215 };
216 let nprobes = match value.get("nprobes") {
217 None => 1,
218 Some(nprobes) => parse_nprobes(nprobes)?,
219 };
220 let ttl = match value.get("ttl") {
221 None => 0, Some(_) if !is_trace => {
223 warn!("[CLI] The 'ttl' field requires a feed-trace measurement (-m feed-trace).");
224 return None;
225 }
226 Some(ttl) => parse_ttl(ttl)?,
227 };
228 let session_id = match value.get("session") {
229 None => 0, Some(_) if !is_sessions => {
231 warn!("[CLI] Ignoring 'session': sessions are not enabled (start with --sessions).");
232 0
233 }
234 Some(session) => parse_session(session, origin_id, origins, dst.is_v6())?,
235 };
236
237 Some(LiveTarget {
238 dst: Some(dst),
239 worker_ids,
240 origin_id,
241 nprobes,
242 ttl,
243 session_id,
244 })
245}
246
247fn parse_session(
253 session: &serde_json::Value,
254 origin_id: u32,
255 origins: &FeedOrigins,
256 dst_is_v6: bool,
257) -> Option<u32> {
258 let n = match session {
259 serde_json::Value::Number(n) => n.as_u64()?,
261 serde_json::Value::String(s) => s.parse::<u64>().ok()?,
263 _ => return None,
264 };
265
266 if n > u16::MAX as u64 {
267 warn!("[CLI] '{n}' is not a valid session value (0-65535).");
268 return None;
269 }
270 if n == 0 {
271 return Some(0); }
273
274 let unattributable = match origin_id {
276 ALL_ORIGINS => origins
277 .by_id
278 .values()
279 .any(|origin| origin.is_v6 == dst_is_v6 && !origin.supports_sessions()),
280 id => origins
281 .by_id
282 .get(&id)
283 .is_some_and(|o| !o.supports_sessions()),
284 };
285 if unattributable {
286 warn!(
287 "[CLI] Session {n}: TCP/CHAOS replies cannot echo the session ID; their rows will report session 0."
288 );
289 }
290
291 Some(n as u32)
292}
293
294fn parse_ttl(ttl: &serde_json::Value) -> Option<u32> {
296 let n = match ttl {
297 serde_json::Value::Number(n) => n.as_u64()?,
299 serde_json::Value::String(s) => s.parse::<u64>().ok()?,
301 _ => return None,
302 };
303
304 if (1..=u8::MAX as u64).contains(&n) {
305 Some(n as u32)
306 } else {
307 warn!("[CLI] '{n}' is not a valid ttl value (1-255).");
308 None
309 }
310}
311
312fn parse_nprobes(nprobes: &serde_json::Value) -> Option<u32> {
314 let n = match nprobes {
315 serde_json::Value::Number(n) => n.as_u64()?,
317 serde_json::Value::String(s) => s.parse::<u64>().ok()?,
319 _ => return None,
320 };
321
322 if (1..=u8::MAX as u64).contains(&n) {
323 Some(n as u32)
324 } else {
325 warn!("[CLI] '{n}' is not a valid nprobes value (1-255).");
326 None
327 }
328}
329
330fn parse_origin(origin: &serde_json::Value, origins: &FeedOrigins, dst_is_v6: bool) -> Option<u32> {
334 let id = match origin {
335 serde_json::Value::Number(n) => u32::try_from(n.as_u64()?).ok()?,
337 serde_json::Value::String(s) if s == "all" => return Some(ALL_ORIGINS),
338 serde_json::Value::String(s) => match s.parse::<u32>() {
340 Ok(id) => id,
341 Err(_) => {
342 warn!("[CLI] '{s}' is not a valid origin ID.");
343 return None;
344 }
345 },
346 _ => return None,
347 };
348
349 match origins.by_id.get(&id) {
351 Some(origin) if origin.is_v6 == dst_is_v6 => Some(id),
352 Some(origin) => {
353 warn!(
354 "[CLI] Origin {id} is {} but the target is {}.",
355 if origin.is_v6 { "IPv6" } else { "IPv4" },
356 if dst_is_v6 { "IPv6" } else { "IPv4" }
357 );
358 None
359 }
360 None => {
361 warn!("[CLI] Origin ID '{id}' is not a configured origin.");
362 None
363 }
364 }
365}
366
367fn parse_worker(
373 worker: &serde_json::Value,
374 worker_map: &BiHashMap<u32, String>,
375) -> Option<Vec<u32>> {
376 if let Some(id) = worker.as_u64() {
378 let id = u32::try_from(id).ok()?;
379 if worker_map.contains_left(&id) {
380 return Some(vec![id]);
381 }
382 warn!("[CLI] Worker ID '{id}' is not a known worker.");
383 return None;
384 }
385
386 let worker = worker.as_str()?;
387 match worker {
388 "any" => Some(Vec::new()),
389 "all" => Some(vec![ALL_WORKERS]),
390 _ => {
392 let ids = resolve_workers(worker, worker_map);
393 if ids.is_empty() {
394 warn!("[CLI] '{worker}' did not match any known worker ID or hostname.");
395 return None;
396 }
397 Some(ids)
398 }
399 }
400}