1pub mod actions;
47pub mod wire;
48
49pub use actions::NetbiosNsClientProtocol;
50
51use crate::utils::clock::Instant;
52use std::collections::VecDeque;
53use std::net::{IpAddr, SocketAddr};
54use std::sync::Arc;
55use std::time::Duration;
56
57use anyhow::{Context, Result};
58use tokio::net::UdpSocket;
59use tokio::sync::{mpsc, Mutex};
60use tracing::{debug, error, info, trace, warn};
61
62use crate::client::llm_budget::call_llm_for_client;
63use crate::llm::actions::client_trait::{Client, ClientActionResult};
64use crate::llm::actions::protocol_trait::Protocol;
65use crate::llm::ollama_client::OllamaClient;
66use crate::llm::ClientLlmResult;
67use crate::protocol::{ConnectContext, Event, EventType};
68use crate::server::netbios_ns::packet as pkt;
69use crate::state::app_state::AppState;
70use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
71use crate::state::{AccessLogOwner, ClientId, ClientStatus};
72
73use self::actions::{
74 NETBIOS_NAME_RESPONSE_EVENT, NETBIOS_NEGATIVE_RESPONSE_EVENT,
75 NETBIOS_NODE_STATUS_RESPONSE_EVENT, NETBIOS_NS_CONNECTED_EVENT, NETBIOS_QUERY_TIMEOUT_EVENT,
76};
77
78const DEFAULT_NBNS_PORT: u16 = 137;
81
82const DEFAULT_QUERY_TIMEOUT_SECS: u64 = 3;
84
85const MAX_FOLLOWUP_DEPTH: usize = 6;
89
90struct Transport {
92 socket: Mutex<UdpSocket>,
95 default_target: SocketAddr,
96 query_timeout: Duration,
97}
98
99enum Applied {
101 Answered {
105 event_type: &'static EventType,
106 event_data: serde_json::Value,
107 detail: String,
108 },
109 Nothing(String),
111 Disconnect,
113}
114
115enum QueryOutcome {
117 Reply {
118 response: wire::NbnsResponse,
119 from: SocketAddr,
120 },
121 TimedOut { ignored: usize },
125}
126
127pub struct NetbiosNsClient;
128
129impl NetbiosNsClient {
130 pub async fn connect_with_llm_actions(ctx: ConnectContext) -> Result<SocketAddr> {
133 let ConnectContext {
134 remote_addr,
135 llm_client,
136 state: app_state,
137 status_tx,
138 client_id,
139 startup_params,
140 } = ctx;
141
142 let port_override = match &startup_params {
144 Some(params) => params.get_optional_u32("port")?,
145 None => None,
146 };
147 let port_override = match port_override {
148 Some(p) => Some(u16::try_from(p).map_err(|_| {
149 anyhow::anyhow!("startup parameter 'port' must be between 0 and 65535, got {p}")
150 })?),
151 None => None,
152 };
153 let query_timeout_secs = match &startup_params {
154 Some(params) => params
155 .get_optional_u64("query_timeout_secs")?
156 .unwrap_or(DEFAULT_QUERY_TIMEOUT_SECS),
157 None => DEFAULT_QUERY_TIMEOUT_SECS,
158 };
159 if query_timeout_secs == 0 {
160 anyhow::bail!(
161 "startup parameter 'query_timeout_secs' must be at least 1: a zero deadline \
162 discards every reply before it can arrive"
163 );
164 }
165
166 let default_target = resolve_target(&remote_addr, port_override).await?;
167
168 let socket = UdpSocket::bind(("0.0.0.0", 0))
170 .await
171 .context("failed to bind the NetBIOS-NS client's UDP socket")?;
172 let local_addr = socket
173 .local_addr()
174 .context("bound NetBIOS-NS socket has no local address")?;
175
176 info!(
177 "NetBIOS-NS client {} querying from {} to {}",
178 client_id, local_addr, default_target
179 );
180 app_state
181 .update_client_status(client_id, ClientStatus::Connected)
182 .await;
183 let _ = status_tx.send(format!(
184 "[CLIENT] NetBIOS-NS client {} ready ({} -> {})",
185 client_id, local_addr, default_target
186 ));
187 let _ = status_tx.send("__UPDATE_UI__".to_string());
188
189 let transport = Arc::new(Transport {
190 socket: Mutex::new(socket),
191 default_target,
192 query_timeout: Duration::from_secs(query_timeout_secs),
193 });
194 let protocol = Arc::new(NetbiosNsClientProtocol::new());
195
196 let command_rx =
200 crate::client::command_support::register_command_channel(&app_state, client_id).await;
201 let cmd_task = tokio::spawn(Self::command_loop(
202 command_rx,
203 protocol.clone(),
204 transport.clone(),
205 client_id,
206 app_state.clone(),
207 llm_client.clone(),
208 status_tx.clone(),
209 ));
210 app_state.register_client_task(client_id, cmd_task).await;
211
212 let conversation = tokio::spawn(Self::conversation(
215 protocol.clone(),
216 transport.clone(),
217 client_id,
218 app_state.clone(),
219 llm_client,
220 status_tx,
221 default_target,
222 local_addr,
223 ));
224 app_state
225 .register_client_task(client_id, conversation)
226 .await;
227
228 Ok(local_addr)
229 }
230
231 #[allow(clippy::too_many_arguments)]
233 async fn conversation(
234 protocol: Arc<NetbiosNsClientProtocol>,
235 transport: Arc<Transport>,
236 client_id: ClientId,
237 app_state: Arc<AppState>,
238 llm_client: OllamaClient,
239 status_tx: mpsc::UnboundedSender<String>,
240 default_target: SocketAddr,
241 local_addr: SocketAddr,
242 ) {
243 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
244 debug!(
245 "NetBIOS-NS client {} has no instruction; nothing to drive",
246 client_id
247 );
248 return;
249 };
250
251 let event = Event::new(
252 &NETBIOS_NS_CONNECTED_EVENT,
253 serde_json::json!({
254 "remote_addr": default_target.to_string(),
255 "local_addr": local_addr.to_string(),
256 }),
257 );
258
259 let memory = app_state
260 .get_memory_for_client(client_id)
261 .await
262 .unwrap_or_default();
263
264 match call_llm_for_client(
265 &llm_client,
266 &app_state,
267 client_id.to_string(),
268 &instruction,
269 &memory,
270 Some(&event),
271 protocol.as_ref(),
272 &status_tx,
273 )
274 .await
275 {
276 Ok(ClientLlmResult {
277 actions,
278 memory_updates,
279 }) => {
280 if let Some(mem) = memory_updates {
281 app_state.set_memory_for_client(client_id, mem).await;
282 }
283 Self::run_actions(
284 &protocol,
285 &transport,
286 actions,
287 0,
288 client_id,
289 &app_state,
290 &llm_client,
291 &status_tx,
292 )
293 .await;
294 }
295 Err(e) => {
296 error!("LLM error for NetBIOS-NS client {}: {}", client_id, e);
297 let _ = status_tx.send(format!(
298 "[CLIENT] ✖ NetBIOS-NS client {} could not start: {}",
299 client_id, e
300 ));
301 }
302 }
303
304 debug!("NetBIOS-NS client {} conversation task finished", client_id);
305 }
306
307 #[allow(clippy::too_many_arguments)]
313 async fn run_actions(
314 protocol: &Arc<NetbiosNsClientProtocol>,
315 transport: &Arc<Transport>,
316 initial_actions: Vec<serde_json::Value>,
317 depth: usize,
318 client_id: ClientId,
319 app_state: &Arc<AppState>,
320 llm_client: &OllamaClient,
321 status_tx: &mpsc::UnboundedSender<String>,
322 ) {
323 let mut pending: VecDeque<(serde_json::Value, usize)> =
324 initial_actions.into_iter().map(|a| (a, depth)).collect();
325
326 while let Some((action, depth)) = pending.pop_front() {
327 let applied =
328 match Self::apply_action(protocol, transport, action, client_id, status_tx).await {
329 Ok(applied) => applied,
330 Err(e) => {
331 error!("NetBIOS-NS client {} action error: {}", client_id, e);
332 let _ = status_tx.send(format!(
333 "[WARN] NetBIOS-NS client {} action failed: {}",
334 client_id, e
335 ));
336 continue;
337 }
338 };
339
340 match applied {
341 Applied::Disconnect => {
342 Self::finish(client_id, app_state, status_tx).await;
343 return;
344 }
345 Applied::Nothing(detail) => {
346 debug!("NetBIOS-NS client {}: {}", client_id, detail);
347 }
348 Applied::Answered {
349 event_type,
350 event_data,
351 ..
352 } => {
353 if depth >= MAX_FOLLOWUP_DEPTH {
354 warn!(
355 "NetBIOS-NS client {} reached the follow-up depth cap ({}); the \
356 '{}' answer is not being handed back to the model",
357 client_id, MAX_FOLLOWUP_DEPTH, event_type.id
358 );
359 let _ = status_tx.send(format!(
360 "[CLIENT] ⚠ NetBIOS-NS client {} stopped following up at depth {}",
361 client_id, MAX_FOLLOWUP_DEPTH
362 ));
363 continue;
364 }
365 let follow_ups = Self::report(
366 event_type, event_data, protocol, client_id, app_state, llm_client,
367 status_tx,
368 )
369 .await;
370 pending.extend(follow_ups.into_iter().map(|a| (a, depth + 1)));
371 }
372 }
373 }
374 }
375
376 async fn report(
382 event_type: &'static EventType,
383 event_data: serde_json::Value,
384 protocol: &Arc<NetbiosNsClientProtocol>,
385 client_id: ClientId,
386 app_state: &Arc<AppState>,
387 llm_client: &OllamaClient,
388 status_tx: &mpsc::UnboundedSender<String>,
389 ) -> Vec<serde_json::Value> {
390 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
391 return Vec::new();
392 };
393 let event = Event::new(event_type, event_data);
394 let memory = app_state
395 .get_memory_for_client(client_id)
396 .await
397 .unwrap_or_default();
398
399 match call_llm_for_client(
400 llm_client,
401 app_state,
402 client_id.to_string(),
403 &instruction,
404 &memory,
405 Some(&event),
406 protocol.as_ref(),
407 status_tx,
408 )
409 .await
410 {
411 Ok(ClientLlmResult {
412 actions,
413 memory_updates,
414 }) => {
415 if let Some(mem) = memory_updates {
416 app_state.set_memory_for_client(client_id, mem).await;
417 }
418 actions
419 }
420 Err(e) => {
421 error!("LLM error for NetBIOS-NS client {}: {}", client_id, e);
422 Vec::new()
423 }
424 }
425 }
426
427 async fn apply_action(
430 protocol: &Arc<NetbiosNsClientProtocol>,
431 transport: &Arc<Transport>,
432 action: serde_json::Value,
433 client_id: ClientId,
434 status_tx: &mpsc::UnboundedSender<String>,
435 ) -> Result<Applied> {
436 match protocol.execute_action(action)? {
437 ClientActionResult::Custom { name, data } if name == "netbios_name_query" => {
438 Self::query(transport, &data, pkt::QTYPE_NB, client_id, status_tx).await
439 }
440 ClientActionResult::Custom { name, data } if name == "netbios_node_status_query" => {
441 Self::query(transport, &data, pkt::QTYPE_NBSTAT, client_id, status_tx).await
442 }
443 ClientActionResult::Disconnect => Ok(Applied::Disconnect),
444 ClientActionResult::WaitForMore => {
445 Ok(Applied::Nothing("wait_for_more: no query sent".to_string()))
446 }
447 ClientActionResult::Custom { name, .. } => Ok(Applied::Nothing(format!(
448 "custom result '{name}' is not a NetBIOS-NS query; nothing sent"
449 ))),
450 other => Ok(Applied::Nothing(format!(
451 "action result {other:?} produced no datagram"
452 ))),
453 }
454 }
455
456 async fn query(
458 transport: &Arc<Transport>,
459 data: &serde_json::Value,
460 qtype: u16,
461 client_id: ClientId,
462 status_tx: &mpsc::UnboundedSender<String>,
463 ) -> Result<Applied> {
464 let name = data
465 .get("name")
466 .and_then(|v| v.as_str())
467 .context("query is missing 'name'")?
468 .to_string();
469 let suffix = u8::try_from(data.get("suffix").and_then(|v| v.as_u64()).unwrap_or(0))
470 .context("'suffix' must be between 0 and 255")?;
471 let broadcast = data
472 .get("broadcast")
473 .and_then(|v| v.as_bool())
474 .unwrap_or(false);
475 let target = target_for(transport.default_target, data)?;
476
477 let trn_id: u16 = rand::random();
480
481 let datagram = if qtype == pkt::QTYPE_NBSTAT {
482 wire::encode_node_status_query(trn_id, &name, suffix)?
483 } else {
484 wire::encode_name_query(trn_id, &name, suffix, broadcast)?
485 };
486 let question_type = pkt::qtype_name(qtype);
487
488 debug!(
489 "NetBIOS-NS client {} sending {} query for {}<0x{:02x}> to {} (trn_id 0x{:04x}, \
490 {} octets)",
491 client_id,
492 question_type,
493 name,
494 suffix,
495 target,
496 trn_id,
497 datagram.len()
498 );
499
500 let outcome = Self::run_query(transport, &datagram, target, trn_id, client_id).await?;
501
502 match outcome {
503 QueryOutcome::TimedOut { ignored } => {
504 info!(
505 "NetBIOS-NS client {} got no matching answer for {}<0x{:02x}> from {} in {}s \
506 ({} datagram(s) discarded)",
507 client_id,
508 name,
509 suffix,
510 target,
511 transport.query_timeout.as_secs(),
512 ignored
513 );
514 let _ = status_tx.send(format!(
515 "[CLIENT] NetBIOS-NS {} query for {} timed out (normal when the node does \
516 not hold the name)",
517 question_type, name
518 ));
519 Ok(Applied::Answered {
520 event_type: &NETBIOS_QUERY_TIMEOUT_EVENT,
521 event_data: serde_json::json!({
522 "name": name,
523 "suffix": suffix,
524 "suffix_label": wire::suffix_label(suffix),
525 "question_type": question_type,
526 "target": target.to_string(),
527 "timeout_secs": transport.query_timeout.as_secs(),
528 "ignored_datagrams": ignored,
529 }),
530 detail: format!(
531 "{question_type} query for {name} timed out after {}s ({ignored} \
532 datagram(s) discarded)",
533 transport.query_timeout.as_secs()
534 ),
535 })
536 }
537 QueryOutcome::Reply { response, from } => {
538 Ok(Self::describe_reply(response, from, client_id, status_tx))
539 }
540 }
541 }
542
543 fn describe_reply(
545 response: wire::NbnsResponse,
546 from: SocketAddr,
547 client_id: ClientId,
548 status_tx: &mpsc::UnboundedSender<String>,
549 ) -> Applied {
550 let responder = from.to_string();
551 match response.answer {
552 wire::NbnsAnswer::Name(answer) => {
553 let addresses: Vec<String> =
554 answer.addresses.iter().map(|a| a.to_string()).collect();
555 info!(
556 "NetBIOS-NS client {} resolved {}<0x{:02x}> to {} (ttl {}, group {})",
557 client_id,
558 answer.name,
559 answer.suffix,
560 addresses.join(", "),
561 answer.ttl,
562 answer.group
563 );
564 let _ = status_tx.send(format!(
565 "[CLIENT] NetBIOS-NS {}<0x{:02x}> = {}",
566 answer.name,
567 answer.suffix,
568 addresses.join(", ")
569 ));
570 let detail = format!(
571 "{}<0x{:02x}> resolved to {}",
572 answer.name,
573 answer.suffix,
574 addresses.join(", ")
575 );
576 Applied::Answered {
577 event_type: &NETBIOS_NAME_RESPONSE_EVENT,
578 event_data: serde_json::json!({
579 "name": answer.name,
580 "suffix": answer.suffix,
581 "suffix_label": wire::suffix_label(answer.suffix),
582 "addresses": addresses,
583 "ttl": answer.ttl,
584 "group": answer.group,
585 "node_type": answer.node_type,
586 "responder": responder,
587 }),
588 detail,
589 }
590 }
591 wire::NbnsAnswer::NodeStatus(answer) => {
592 let names: Vec<serde_json::Value> = answer
593 .names
594 .iter()
595 .map(|entry| {
596 serde_json::json!({
597 "name": entry.name,
598 "suffix": entry.suffix,
599 "suffix_label": wire::suffix_label(entry.suffix),
600 "group": entry.group,
601 "active": entry.active,
602 })
603 })
604 .collect();
605 info!(
606 "NetBIOS-NS client {} node status from {}: {} name(s), adapter {}",
607 client_id,
608 responder,
609 names.len(),
610 answer.mac_address
611 );
612 let _ = status_tx.send(format!(
613 "[CLIENT] NetBIOS-NS node status: {} name(s), adapter {}",
614 names.len(),
615 answer.mac_address
616 ));
617 let detail = format!(
618 "node status listed {} name(s), adapter {}",
619 names.len(),
620 answer.mac_address
621 );
622 Applied::Answered {
623 event_type: &NETBIOS_NODE_STATUS_RESPONSE_EVENT,
624 event_data: serde_json::json!({
625 "name": answer.name,
626 "suffix": answer.suffix,
627 "suffix_label": wire::suffix_label(answer.suffix),
628 "names": names,
629 "mac_address": answer.mac_address,
630 "responder": responder,
631 }),
632 detail,
633 }
634 }
635 wire::NbnsAnswer::Negative(answer) => {
636 info!(
637 "NetBIOS-NS client {} refused for {}<0x{:02x}>: rcode {} ({})",
638 client_id, answer.name, answer.suffix, answer.rcode, answer.rcode_name
639 );
640 let _ = status_tx.send(format!(
641 "[CLIENT] NetBIOS-NS {} -> {}",
642 answer.name, answer.rcode_name
643 ));
644 let detail = format!(
645 "{}<0x{:02x}> refused: {}",
646 answer.name, answer.suffix, answer.rcode_name
647 );
648 Applied::Answered {
649 event_type: &NETBIOS_NEGATIVE_RESPONSE_EVENT,
650 event_data: serde_json::json!({
651 "name": answer.name,
652 "suffix": answer.suffix,
653 "suffix_label": wire::suffix_label(answer.suffix),
654 "rcode": answer.rcode,
655 "rcode_name": answer.rcode_name,
656 "responder": responder,
657 }),
658 detail,
659 }
660 }
661 }
662 }
663
664 async fn run_query(
673 transport: &Arc<Transport>,
674 datagram: &[u8],
675 target: SocketAddr,
676 trn_id: u16,
677 client_id: ClientId,
678 ) -> Result<QueryOutcome> {
679 let deadline = Instant::now() + transport.query_timeout;
680 let mut ignored = 0usize;
681 let mut buffer = vec![0u8; wire::RECV_BUFFER];
682
683 let socket = transport.socket.lock().await;
687
688 socket
689 .send_to(datagram, target)
690 .await
691 .with_context(|| format!("failed to send a NetBIOS-NS query to {target}"))?;
692
693 loop {
694 let remaining = deadline.saturating_duration_since(Instant::now());
695 if remaining.is_zero() {
696 return Ok(QueryOutcome::TimedOut { ignored });
697 }
698
699 let received =
700 match tokio::time::timeout(remaining, socket.recv_from(&mut buffer)).await {
701 Err(_) => return Ok(QueryOutcome::TimedOut { ignored }),
702 Ok(Ok(received)) => received,
703 Ok(Err(e)) => {
704 return Err(anyhow::anyhow!(
705 "NetBIOS-NS client {client_id} socket read failed: {e}"
706 ))
707 }
708 };
709 let (n, from) = received;
710 trace!(
711 "NetBIOS-NS client {} read {} octets from {}",
712 client_id,
713 n,
714 from
715 );
716
717 match wire::parse_response(&buffer[..n]) {
718 Ok(response) if response.trn_id == trn_id => {
719 return Ok(QueryOutcome::Reply { response, from })
720 }
721 Ok(response) => {
722 ignored += 1;
723 warn!(
728 "NetBIOS-NS client {} discarded a datagram from {}: transaction id \
729 0x{:04x} does not match the outstanding 0x{:04x}",
730 client_id, from, response.trn_id, trn_id
731 );
732 }
733 Err(e) => {
734 ignored += 1;
735 debug!(
736 "NetBIOS-NS client {} discarded a datagram from {}: {}",
737 client_id, from, e
738 );
739 }
740 }
741 }
742 }
743
744 #[allow(clippy::too_many_arguments)]
756 async fn command_loop(
757 mut command_rx: mpsc::Receiver<ClientCommand>,
758 protocol: Arc<NetbiosNsClientProtocol>,
759 transport: Arc<Transport>,
760 client_id: ClientId,
761 app_state: Arc<AppState>,
762 llm_client: OllamaClient,
763 status_tx: mpsc::UnboundedSender<String>,
764 ) {
765 while let Some(command) = command_rx.recv().await {
766 let action = command.action.clone();
767
768 let mut answered: Option<(&'static EventType, serde_json::Value)> = None;
769 let outcome = match Self::apply_action(
770 &protocol,
771 &transport,
772 action.clone(),
773 client_id,
774 &status_tx,
775 )
776 .await
777 {
778 Ok(Applied::Answered {
779 event_type,
780 event_data,
781 detail,
782 }) => {
783 answered = Some((event_type, event_data));
784 Ok(ClientSendOutcome::Executed { detail })
785 }
786 Ok(Applied::Nothing(detail)) => Ok(ClientSendOutcome::Executed { detail }),
787 Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
788 Err(e)
791 if e.to_string()
792 .starts_with("Unknown NetBIOS-NS client action") =>
793 {
794 Ok(ClientSendOutcome::Rejected {
795 error: e.to_string(),
796 })
797 }
798 Err(e) => Err(e),
799 };
800
801 let outcome_json = match &outcome {
802 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
803 Err(e) => serde_json::json!({"error": e.to_string()}),
804 };
805 app_state
806 .record_access_log(
807 AccessLogOwner::Client(client_id.as_u32()),
808 protocol.protocol_name(),
809 None,
810 "injected_action",
811 action,
812 vec![outcome_json],
813 )
814 .await;
815
816 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
817 if let Err(e) = &outcome {
818 error!(
819 "NetBIOS-NS client {} injected action failed: {}",
820 client_id, e
821 );
822 let _ = status_tx.send(format!(
823 "[WARN] Client {} injected action failed: {}",
824 client_id, e
825 ));
826 }
827 let _ = status_tx.send("__UPDATE_UI__".to_string());
828 crate::client::command_support::reply(command, outcome);
829
830 if disconnect {
831 Self::finish(client_id, &app_state, &status_tx).await;
832 break;
833 }
834
835 if let Some((event_type, event_data)) = answered {
838 let follow_ups = Self::report(
839 event_type,
840 event_data,
841 &protocol,
842 client_id,
843 &app_state,
844 &llm_client,
845 &status_tx,
846 )
847 .await;
848 Self::run_actions(
849 &protocol,
850 &transport,
851 follow_ups,
852 1,
853 client_id,
854 &app_state,
855 &llm_client,
856 &status_tx,
857 )
858 .await;
859 }
860 }
861 }
862
863 async fn finish(
866 client_id: ClientId,
867 app_state: &Arc<AppState>,
868 status_tx: &mpsc::UnboundedSender<String>,
869 ) {
870 info!("NetBIOS-NS client {} disconnecting", client_id);
871 app_state.remove_client_handle(client_id).await;
872 app_state
873 .update_client_status(client_id, ClientStatus::Disconnected)
874 .await;
875 let _ = status_tx.send("__UPDATE_UI__".to_string());
876 }
877}
878
879async fn resolve_target(remote_addr: &str, port_override: Option<u16>) -> Result<SocketAddr> {
885 let remote_addr = remote_addr.trim();
886
887 if let Ok(addr) = remote_addr.parse::<SocketAddr>() {
888 return Ok(match port_override {
889 Some(port) => SocketAddr::new(addr.ip(), port),
890 None => addr,
891 });
892 }
893 if let Ok(ip) = remote_addr.parse::<IpAddr>() {
894 return Ok(SocketAddr::new(
895 ip,
896 port_override.unwrap_or(DEFAULT_NBNS_PORT),
897 ));
898 }
899
900 let (host, port) = match remote_addr.rsplit_once(':') {
902 Some((host, port)) if port.chars().all(|c| c.is_ascii_digit()) && !port.is_empty() => (
903 host,
904 port.parse::<u16>()
905 .with_context(|| format!("'{port}' is not a port number"))?,
906 ),
907 _ => (remote_addr, DEFAULT_NBNS_PORT),
908 };
909 let port = port_override.unwrap_or(port);
910
911 tokio::net::lookup_host((host, port))
912 .await
913 .with_context(|| format!("could not resolve NetBIOS-NS target '{remote_addr}'"))?
914 .find(|addr| addr.is_ipv4())
916 .with_context(|| format!("'{remote_addr}' has no IPv4 address; NetBIOS-NS is IPv4 only"))
917}
918
919fn target_for(default_target: SocketAddr, data: &serde_json::Value) -> Result<SocketAddr> {
921 let ip = match data.get("target_address").and_then(|v| v.as_str()) {
922 Some(text) => text.trim().parse::<IpAddr>().with_context(|| {
923 format!("'target_address' must be a dotted-quad IPv4 address, got '{text}'")
924 })?,
925 None => default_target.ip(),
926 };
927 let port = match data.get("target_port").and_then(|v| v.as_u64()) {
928 Some(port) => u16::try_from(port)
929 .with_context(|| format!("'target_port' must be between 0 and 65535, got {port}"))?,
930 None => default_target.port(),
931 };
932 Ok(SocketAddr::new(ip, port))
933}