Skip to main content

spvirit_client/
search.rs

1use std::collections::HashSet;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3use std::sync::Arc;
4use std::time::Duration;
5
6use dns_lookup::lookup_host;
7use get_if_addrs::{IfAddr, get_if_addrs};
8use socket2::{Domain, Protocol, Socket, Type};
9use tokio::io::AsyncWriteExt;
10use tokio::net::UdpSocket;
11use tracing::debug;
12
13use crate::auth::{default_authnz_host, default_authnz_user};
14use crate::transport::read_packet;
15use crate::types::{PvGetError, PvGetOptions};
16use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
17use spvirit_codec::spvirit_encode::{
18    encode_client_connection_validation, encode_search_request, ip_to_bytes,
19    socket_addr_from_pva_bytes,
20};
21
22#[derive(Clone, Copy, Debug)]
23pub struct SearchTarget {
24    pub target: IpAddr,
25    pub bind: IpAddr,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub struct DiscoveredServer {
30    pub guid: [u8; 12],
31    pub tcp_addr: SocketAddr,
32}
33
34pub fn parse_addr_list(env: &str) -> Vec<IpAddr> {
35    env.split(|c| c == ',' || c == ' ' || c == '\t')
36        .filter(|s| !s.trim().is_empty())
37        .filter_map(|s| parse_search_target_ip(s.trim()))
38        .collect()
39}
40
41fn parse_search_target_ip(token: &str) -> Option<IpAddr> {
42    if token.is_empty() {
43        return None;
44    }
45
46    if let Ok(ip) = token.parse::<IpAddr>() {
47        return Some(ip);
48    }
49    if let Ok(sock) = token.parse::<SocketAddr>() {
50        return Some(sock.ip());
51    }
52
53    // Accept host:port where host may be a name or an IP literal.
54    // For IPv6 bracket notation [::1]:port, SocketAddr::parse above already handles it.
55    if let Some((host, port_str)) = token.rsplit_once(':') {
56        if !host.is_empty()
57            && !port_str.is_empty()
58            && port_str.chars().all(|c| c.is_ascii_digit())
59            && !host.contains(']')
60        {
61            if let Ok(ip) = host.parse::<IpAddr>() {
62                return Some(ip);
63            }
64            if let Ok(addrs) = lookup_host(host) {
65                // Prefer IPv4 for backward compat, fall back to first IPv6
66                let addrs: Vec<IpAddr> = addrs.collect();
67                if let Some(ip) = addrs
68                    .iter()
69                    .find(|ip| ip.is_ipv4())
70                    .copied()
71                    .or_else(|| addrs.into_iter().next())
72                {
73                    return Some(ip);
74                }
75            }
76        }
77    }
78
79    if let Ok(addrs) = lookup_host(token) {
80        // Prefer IPv4, fall back to first IPv6
81        let addrs: Vec<IpAddr> = addrs.collect();
82        if let Some(ip) = addrs
83            .iter()
84            .find(|ip| ip.is_ipv4())
85            .copied()
86            .or_else(|| addrs.into_iter().next())
87        {
88            return Some(ip);
89        }
90    }
91
92    None
93}
94
95/// Return a default unspecified bind address matching the target's address family.
96fn unspecified_for(ip: IpAddr) -> IpAddr {
97    match ip {
98        IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
99        IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
100    }
101}
102
103pub fn build_search_targets(
104    search_addr: Option<IpAddr>,
105    bind_addr: Option<IpAddr>,
106) -> Vec<SearchTarget> {
107    // Explicit --search-addr overrides everything (single target).
108    if let Some(ip) = search_addr {
109        return vec![SearchTarget {
110            target: ip,
111            bind: bind_addr.unwrap_or_else(|| unspecified_for(ip)),
112        }];
113    }
114
115    let mut targets = Vec::new();
116    let mut seen = HashSet::new();
117
118    // Addresses from EPICS_PVA_ADDR_LIST.
119    if let Ok(env) = std::env::var("EPICS_PVA_ADDR_LIST") {
120        for ip in parse_addr_list(&env) {
121            if seen.insert(ip) {
122                targets.push(SearchTarget {
123                    target: ip,
124                    bind: bind_addr.unwrap_or_else(|| unspecified_for(ip)),
125                });
126            }
127        }
128    }
129
130    // Merge auto-discovered broadcast addresses unless explicitly disabled.
131    // This matches EPICS Base behaviour: ADDR_LIST + auto-broadcast combined.
132    if is_auto_addr_list_enabled() {
133        for t in build_auto_broadcast_targets() {
134            if seen.insert(t.target) {
135                targets.push(SearchTarget {
136                    target: t.target,
137                    bind: bind_addr.unwrap_or(t.bind),
138                });
139            }
140        }
141    }
142
143    targets
144}
145
146pub fn is_auto_addr_list_enabled() -> bool {
147    match std::env::var("EPICS_PVA_AUTO_ADDR_LIST") {
148        Ok(v) => {
149            let v = v.trim().to_ascii_uppercase();
150            v == "YES" || v == "Y" || v == "1" || v == "TRUE"
151        }
152        Err(_) => true,
153    }
154}
155
156fn ipv4_is_link_local(ip: Ipv4Addr) -> bool {
157    let octets = ip.octets();
158    octets[0] == 169 && octets[1] == 254
159}
160
161fn choose_default_bind_v4() -> Option<Ipv4Addr> {
162    let ifaces = get_if_addrs().ok()?;
163    for iface in ifaces {
164        if let IfAddr::V4(v4) = iface.addr {
165            let ip = v4.ip;
166            if ip.is_loopback() || ipv4_is_link_local(ip) {
167                continue;
168            }
169            return Some(ip);
170        }
171    }
172    None
173}
174
175fn choose_default_bind_v6() -> Option<Ipv6Addr> {
176    let ifaces = get_if_addrs().ok()?;
177    for iface in ifaces {
178        if let IfAddr::V6(v6) = iface.addr {
179            let ip = v6.ip;
180            if ip.is_loopback() {
181                continue;
182            }
183            // Skip link-local (fe80::/10) — not routable without scope id
184            let segs = ip.segments();
185            if segs[0] & 0xffc0 == 0xfe80 {
186                continue;
187            }
188            return Some(ip);
189        }
190    }
191    None
192}
193
194fn broadcast_for(ip: Ipv4Addr, netmask: Ipv4Addr) -> Ipv4Addr {
195    let ip_u = u32::from(ip);
196    let mask_u = u32::from(netmask);
197    Ipv4Addr::from(ip_u | !mask_u)
198}
199
200fn discovery_target_for(ip: Ipv4Addr, netmask: Ipv4Addr) -> Ipv4Addr {
201    let limited_broadcast = Ipv4Addr::new(255, 255, 255, 255);
202    if netmask == Ipv4Addr::new(255, 255, 255, 255) || netmask.is_unspecified() {
203        return limited_broadcast;
204    }
205    let directed = broadcast_for(ip, netmask);
206    if directed == ip {
207        limited_broadcast
208    } else {
209        directed
210    }
211}
212
213pub fn build_auto_broadcast_targets() -> Vec<SearchTarget> {
214    let mut targets = Vec::new();
215    let mut fallback_targets = Vec::new();
216    let mut fallback_seen = HashSet::new();
217    let mut added_v4_multicast = false;
218    let mut added_v6_multicast = false;
219    let ifaces = match get_if_addrs() {
220        Ok(v) => v,
221        Err(_) => return targets,
222    };
223    for iface in &ifaces {
224        if let IfAddr::V4(v4) = &iface.addr {
225            let ip = v4.ip;
226            if ip.is_loopback() || ipv4_is_link_local(ip) {
227                continue;
228            }
229            let bcast = discovery_target_for(ip, v4.netmask);
230            targets.push(SearchTarget {
231                target: IpAddr::V4(bcast),
232                bind: IpAddr::V4(ip),
233            });
234            // Also send to IPv4 multicast group (matching PVXS behaviour).
235            // Docker overlay networks may block broadcast but allow multicast.
236            targets.push(SearchTarget {
237                target: IpAddr::V4(PVA_MULTICAST_V4),
238                bind: IpAddr::V4(ip),
239            });
240            if fallback_seen.insert(IpAddr::V4(bcast)) {
241                fallback_targets.push(SearchTarget {
242                    target: IpAddr::V4(bcast),
243                    bind: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
244                });
245            }
246            if !added_v4_multicast {
247                added_v4_multicast = true;
248                fallback_targets.push(SearchTarget {
249                    target: IpAddr::V4(PVA_MULTICAST_V4),
250                    bind: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
251                });
252            }
253        }
254    }
255    // Add IPv6 multicast targets for each non-loopback, non-link-local v6 iface.
256    for iface in &ifaces {
257        if let IfAddr::V6(v6) = &iface.addr {
258            let ip = v6.ip;
259            if ip.is_loopback() {
260                continue;
261            }
262            let segs = ip.segments();
263            if segs[0] & 0xffc0 == 0xfe80 {
264                continue; // skip link-local
265            }
266            let multicast_target = IpAddr::V6(PVA_MULTICAST_V6);
267            targets.push(SearchTarget {
268                target: multicast_target,
269                bind: IpAddr::V6(ip),
270            });
271            if !added_v6_multicast {
272                added_v6_multicast = true;
273                fallback_targets.push(SearchTarget {
274                    target: multicast_target,
275                    bind: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
276                });
277            }
278        }
279    }
280    targets.extend(fallback_targets);
281    targets
282}
283
284/// PVA multicast group (IPv4).
285const PVA_MULTICAST_V4: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 128);
286
287/// PVA multicast group (IPv6 link-local, ff02::42:1).
288const PVA_MULTICAST_V6: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0x42, 1);
289
290/// Best-effort join the PVA multicast group appropriate for the bind address.
291fn join_multicast_any(socket: &std::net::UdpSocket, bind: IpAddr) {
292    match bind {
293        IpAddr::V4(iface) => {
294            let _ = socket.join_multicast_v4(&PVA_MULTICAST_V4, &iface);
295        }
296        IpAddr::V6(_) => {
297            // interface index 0 = OS picks the default interface
298            let _ = socket.join_multicast_v6(&PVA_MULTICAST_V6, 0);
299        }
300    }
301}
302
303fn decode_search_response_addr(addr: [u8; 16], port: u16, src: SocketAddr) -> SocketAddr {
304    socket_addr_from_pva_bytes(addr, port)
305        .filter(|a| !a.ip().is_unspecified())
306        .unwrap_or_else(|| SocketAddr::new(src.ip(), port))
307}
308
309fn normalize_discovered_servers(items: Vec<DiscoveredServer>) -> Vec<DiscoveredServer> {
310    let mut seen = HashSet::new();
311    let mut out = Vec::new();
312    for item in items {
313        if seen.insert((item.guid, item.tcp_addr)) {
314            out.push(item);
315        }
316    }
317    out.sort_by(|a, b| a.tcp_addr.to_string().cmp(&b.tcp_addr.to_string()));
318    out
319}
320
321/// Create a UDP socket with SO_REUSEADDR set (matching PVXS behaviour),
322/// allowing multiple processes to share the search port.
323///
324/// On Windows SO_REUSEADDR has different (unsafe) semantics — it allows
325/// a second socket to steal an actively-used port — so we only enable it
326/// on Unix where it merely permits rebinding during TIME_WAIT.
327fn bind_udp_reuse(addr: SocketAddr) -> std::io::Result<std::net::UdpSocket> {
328    let domain = if addr.is_ipv4() {
329        Domain::IPV4
330    } else {
331        Domain::IPV6
332    };
333    let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
334    #[cfg(unix)]
335    sock.set_reuse_address(true)?;
336    sock.set_nonblocking(true)?;
337    sock.bind(&addr.into())?;
338    Ok(sock.into())
339}
340
341pub async fn search_pv(
342    pv_name: &str,
343    udp_port: u16,
344    timeout_dur: Duration,
345    targets: &[SearchTarget],
346    debug_enabled: bool,
347) -> Result<SocketAddr, PvGetError> {
348    if targets.is_empty() {
349        return Err(PvGetError::Search("no search targets"));
350    }
351
352    let now = std::time::SystemTime::now()
353        .duration_since(std::time::UNIX_EPOCH)
354        .unwrap_or_default();
355    let seq = (now.as_nanos() as u32).wrapping_add(std::process::id());
356    let cid = seq ^ 0x9E37_79B9;
357
358    let mut last_io_error: Option<std::io::Error> = None;
359    let deadline = tokio::time::Instant::now() + timeout_dur;
360
361    // Group targets by bind address so we can share a socket per bind.
362    let mut bind_groups: Vec<(IpAddr, Vec<IpAddr>)> = Vec::new();
363    for t in targets {
364        if let Some(group) = bind_groups.iter_mut().find(|(b, _)| *b == t.bind) {
365            group.1.push(t.target);
366        } else {
367            bind_groups.push((t.bind, vec![t.target]));
368        }
369    }
370
371    // Open sockets and send to all targets first, then collect responses.
372    // Store (socket, message, destinations) for retransmission.
373    let mut socket_info: Vec<(Arc<UdpSocket>, Vec<u8>, Vec<SocketAddr>)> = Vec::new();
374
375    for (bind_ip, group_targets) in &bind_groups {
376        // Always use an ephemeral port for the search client socket.
377        // We only receive unicast replies, so sharing the server's search
378        // port is unnecessary — and on Linux with SO_REUSEPORT the kernel
379        // would route our own outbound packet back to us instead of the
380        // server.
381        let bind_addr = SocketAddr::new(*bind_ip, 0);
382        let (std_sock, actual_bind_addr) = match bind_udp_reuse(bind_addr) {
383            Ok(sock) => {
384                let actual = sock.local_addr().unwrap_or(bind_addr);
385                (sock, actual)
386            }
387            Err(err) => {
388                if debug_enabled {
389                    debug!(
390                        "pva search skipping bind={} step=bind kind={:?} err={}",
391                        bind_addr,
392                        err.kind(),
393                        err
394                    );
395                }
396                last_io_error = Some(err);
397                continue;
398            }
399        };
400        if let Err(err) = std_sock.set_broadcast(true) {
401            if debug_enabled {
402                debug!(
403                    "pva search skipping bind={} step=set_broadcast kind={:?} err={}",
404                    bind_addr,
405                    err.kind(),
406                    err
407                );
408            }
409            last_io_error = Some(err);
410            continue;
411        }
412
413        join_multicast_any(&std_sock, *bind_ip);
414
415        let reply_addr = ip_to_bytes(*bind_ip);
416        let reply_port = match std_sock.local_addr() {
417            Ok(addr) => addr.port(),
418            Err(err) => {
419                if debug_enabled {
420                    debug!(
421                        "pva search skipping bind={} step=local_addr kind={:?} err={}",
422                        bind_addr,
423                        err.kind(),
424                        err
425                    );
426                }
427                last_io_error = Some(err);
428                continue;
429            }
430        };
431        let requests = [(cid, pv_name)];
432        let msg = encode_search_request(seq, 0x81, reply_port, reply_addr, &requests, 2, false);
433
434        let socket = match UdpSocket::from_std(std_sock) {
435            Ok(socket) => socket,
436            Err(err) => {
437                if debug_enabled {
438                    debug!(
439                        "pva search skipping bind={} step=from_std kind={:?} err={}",
440                        bind_addr,
441                        err.kind(),
442                        err
443                    );
444                }
445                last_io_error = Some(err);
446                continue;
447            }
448        };
449
450        let dests: Vec<SocketAddr> = group_targets
451            .iter()
452            .map(|ip| SocketAddr::new(*ip, udp_port))
453            .collect();
454
455        // Send to every target in this bind group immediately.
456        for dest in &dests {
457            if debug_enabled {
458                debug!(
459                    "pva search bind={} target={} server_port={} reply_port={}",
460                    actual_bind_addr,
461                    dest.ip(),
462                    udp_port,
463                    reply_port
464                );
465                debug!("pva search seq={} cid={}", seq, cid);
466                debug!("pva search send {} bytes to {}", msg.len(), dest);
467            }
468            if let Err(err) = socket.send_to(&msg, dest).await {
469                if debug_enabled {
470                    debug!(
471                        "pva search send_to target={} kind={:?} err={}",
472                        dest,
473                        err.kind(),
474                        err
475                    );
476                }
477                last_io_error = Some(err);
478            }
479        }
480
481        socket_info.push((Arc::new(socket), msg, dests));
482    }
483
484    if socket_info.is_empty() {
485        if let Some(err) = last_io_error {
486            return Err(PvGetError::Io(err));
487        }
488        return Err(PvGetError::Timeout("search response"));
489    }
490
491    // Spawn a receiver task per socket that forwards packets into a shared channel.
492    let (tx, mut rx) = tokio::sync::mpsc::channel::<(Vec<u8>, SocketAddr)>(64);
493    for (sock, _, _) in &socket_info {
494        let sock = Arc::clone(sock);
495        let tx = tx.clone();
496        tokio::spawn(async move {
497            loop {
498                let mut buf = vec![0u8; 2048];
499                match sock.recv_from(&mut buf).await {
500                    Ok((len, src)) => {
501                        buf.truncate(len);
502                        if tx.send((buf, src)).await.is_err() {
503                            break;
504                        }
505                    }
506                    Err(_) => break,
507                }
508            }
509        });
510    }
511    drop(tx); // Only spawned tasks hold senders; channel closes when they exit.
512
513    // Retransmit schedule: exponential backoff from start.
514    let retransmit_offsets = [100u64, 500, 1000, 2000];
515    let start = tokio::time::Instant::now();
516    let mut next_retransmit = 0usize;
517
518    loop {
519        // Compute the next wake-up: either the next retransmit or the deadline.
520        let next_retransmit_at = if next_retransmit < retransmit_offsets.len() {
521            start + Duration::from_millis(retransmit_offsets[next_retransmit])
522        } else {
523            deadline
524        };
525        let wake_at = next_retransmit_at.min(deadline);
526
527        tokio::select! {
528            recv = rx.recv() => {
529                let Some((buf, src)) = recv else { break };
530                let mut pkt = PvaPacket::new(&buf);
531                let cmd = pkt
532                    .decode_payload()
533                    .ok_or(PvGetError::Search("failed to decode search response"))?;
534                if let PvaPacketCommand::SearchResponse(payload) = cmd {
535                    if debug_enabled {
536                        debug!(
537                            "pva search response found={} cids={:?} addr={:?} port={}",
538                            payload.found, payload.cids, payload.addr, payload.port
539                        );
540                    }
541                    if payload.seq != seq {
542                        continue;
543                    }
544                    if !payload.protocol.is_empty() && !payload.protocol.eq_ignore_ascii_case("tcp") {
545                        continue;
546                    }
547                    if !payload.found {
548                        continue;
549                    }
550                    if !payload.cids.is_empty() && !payload.cids.contains(&cid) {
551                        continue;
552                    }
553
554                    let addr = decode_search_response_addr(payload.addr, payload.port, src);
555                    if debug_enabled {
556                        debug!("pva search response from {}", addr);
557                    }
558                    return Ok(addr);
559                }
560            }
561            _ = tokio::time::sleep_until(wake_at) => {
562                if tokio::time::Instant::now() >= deadline {
563                    break;
564                }
565                // Retransmit to all targets on all sockets.
566                if next_retransmit < retransmit_offsets.len() {
567                    if debug_enabled {
568                        debug!("pva search retransmit round {}", next_retransmit + 1);
569                    }
570                    for (sock, msg, dests) in &socket_info {
571                        for dest in dests {
572                            let _ = sock.send_to(msg, dest).await;
573                        }
574                    }
575                    next_retransmit += 1;
576                }
577            }
578        }
579    }
580
581    Err(PvGetError::Timeout("search response"))
582}
583
584pub fn default_bind_ip() -> Option<IpAddr> {
585    choose_default_bind_v4()
586        .map(IpAddr::V4)
587        .or_else(|| choose_default_bind_v6().map(IpAddr::V6))
588}
589
590/// Parse `EPICS_PVA_NAME_SERVERS` value into socket addresses.
591/// Accepts space/comma separated entries: `host:port`, `ip`, `hostname`
592/// (port defaults to 5075).
593pub fn parse_name_servers(env_val: &str) -> Vec<SocketAddr> {
594    let mut out = Vec::new();
595    for token in env_val.split(|c| c == ',' || c == ' ' || c == '\t') {
596        let token = token.trim();
597        if token.is_empty() {
598            continue;
599        }
600        if let Ok(addr) = token.parse::<SocketAddr>() {
601            out.push(addr);
602            continue;
603        }
604        if let Ok(ip) = token.parse::<IpAddr>() {
605            out.push(SocketAddr::new(ip, 5075));
606            continue;
607        }
608        use std::net::ToSocketAddrs;
609        if let Ok(mut addrs) = token.to_socket_addrs() {
610            if let Some(addr) = addrs.next() {
611                out.push(addr);
612                continue;
613            }
614        }
615        let with_port = format!("{}:5075", token);
616        if let Ok(mut addrs) = with_port.to_socket_addrs() {
617            if let Some(addr) = addrs.next() {
618                out.push(addr);
619            }
620        }
621    }
622    out
623}
624
625/// Build a minimal PVA ConnectionValidation response for name server search.
626fn encode_search_validation(version: u8, is_be: bool) -> Vec<u8> {
627    let user = default_authnz_user();
628    let host = default_authnz_host();
629    encode_client_connection_validation(87_040, 32_767, 0, "ca", &user, &host, version, is_be)
630}
631
632/// Search for a PV via a TCP connection to a PVA name server.
633///
634/// Connects to the name server, performs the PVA handshake, sends a search
635/// request over TCP, and returns the server address from the search response.
636pub async fn search_pv_tcp(
637    pv_name: &str,
638    name_server: SocketAddr,
639    timeout_dur: Duration,
640    debug_enabled: bool,
641) -> Result<SocketAddr, PvGetError> {
642    let deadline = tokio::time::Instant::now() + timeout_dur;
643
644    let mut stream = tokio::time::timeout(timeout_dur, tokio::net::TcpStream::connect(name_server))
645        .await
646        .map_err(|_| PvGetError::Timeout("name server connect"))??;
647
648    let mut version = 2u8;
649    let mut is_be = false;
650
651    // Read SET_BYTE_ORDER + ConnectionValidation from name server.
652    for _ in 0..2 {
653        let now = tokio::time::Instant::now();
654        if now >= deadline {
655            return Err(PvGetError::Timeout("name server handshake"));
656        }
657        let remaining = deadline - now;
658        if let Ok(bytes) = read_packet(&mut stream, remaining).await {
659            let mut pkt = PvaPacket::new(&bytes);
660            if let Some(cmd) = pkt.decode_payload() {
661                match cmd {
662                    PvaPacketCommand::Control(payload) => {
663                        if payload.command == 2 {
664                            is_be = pkt.header.flags.is_msb;
665                        }
666                    }
667                    PvaPacketCommand::ConnectionValidation(_) => {
668                        version = pkt.header.version;
669                        is_be = pkt.header.flags.is_msb;
670                    }
671                    _ => {}
672                }
673            }
674        }
675    }
676
677    let validation = encode_search_validation(version, is_be);
678    stream.write_all(&validation).await?;
679
680    // Wait for ConnectionValidated.
681    loop {
682        let now = tokio::time::Instant::now();
683        if now >= deadline {
684            return Err(PvGetError::Timeout("name server validated"));
685        }
686        let remaining = deadline - now;
687        let bytes = read_packet(&mut stream, remaining).await?;
688        let mut pkt = PvaPacket::new(&bytes);
689        if let Some(cmd) = pkt.decode_payload() {
690            if matches!(cmd, PvaPacketCommand::ConnectionValidated(_)) {
691                break;
692            }
693        }
694    }
695
696    // Send search request over TCP.
697    let now_ts = std::time::SystemTime::now()
698        .duration_since(std::time::UNIX_EPOCH)
699        .unwrap_or_default();
700    let seq = (now_ts.as_nanos() as u32).wrapping_add(std::process::id());
701    let cid = seq ^ 0x9E37_79B9;
702    let requests = [(cid, pv_name)];
703    let msg = encode_search_request(seq, 0x80, 0, [0u8; 16], &requests, version, is_be);
704    stream.write_all(&msg).await?;
705
706    if debug_enabled {
707        debug!(
708            "pva tcp search sent to name_server={} pv={}",
709            name_server, pv_name
710        );
711    }
712
713    // Read search response.
714    loop {
715        let now = tokio::time::Instant::now();
716        if now >= deadline {
717            return Err(PvGetError::Timeout("name server search response"));
718        }
719        let remaining = deadline - now;
720        let bytes = read_packet(&mut stream, remaining).await?;
721        let mut pkt = PvaPacket::new(&bytes);
722        if let Some(cmd) = pkt.decode_payload() {
723            if let PvaPacketCommand::SearchResponse(payload) = cmd {
724                if !payload.found {
725                    continue;
726                }
727                if !payload.cids.is_empty() && !payload.cids.contains(&cid) {
728                    continue;
729                }
730                let addr = decode_search_response_addr(payload.addr, payload.port, name_server);
731                if debug_enabled {
732                    debug!(
733                        "pva tcp search response from name_server={}: {}",
734                        name_server, addr
735                    );
736                }
737                return Ok(addr);
738            }
739        }
740    }
741}
742
743/// Resolve the PVA server for a PV using name servers (TCP) and/or UDP search.
744///
745/// - If `opts.server_addr` is set, returns it directly.
746/// - Tries each name server from `opts.name_servers` and `EPICS_PVA_NAME_SERVERS`
747///   via TCP search.
748/// - Falls back to UDP search using `build_search_targets()`.
749pub async fn resolve_pv_server(opts: &PvGetOptions) -> Result<SocketAddr, PvGetError> {
750    if let Some(addr) = opts.server_addr {
751        return Ok(addr);
752    }
753
754    let mut name_servers = opts.name_servers.clone();
755    if let Ok(env) = std::env::var("EPICS_PVA_NAME_SERVERS") {
756        name_servers.extend(parse_name_servers(&env));
757    }
758
759    let no_broadcast = opts.no_broadcast;
760
761    // Fail fast when no search strategy is available.
762    if no_broadcast && name_servers.is_empty() {
763        return Err(PvGetError::Search(
764            "no search strategy: specify --name-server or --server when using --no-broadcast",
765        ));
766    }
767
768    // Launch all search strategies concurrently — TCP name servers + UDP broadcast.
769    // Return the first successful result.
770    let targets = build_search_targets(opts.search_addr, opts.bind_addr);
771
772    let pv = opts.pv_name.clone();
773    let timeout_dur = opts.timeout;
774    let debug_enabled = opts.debug;
775    let udp_port = opts.udp_port;
776
777    let mut set = tokio::task::JoinSet::new();
778
779    for ns in name_servers {
780        let pv = pv.clone();
781        set.spawn(async move {
782            let addr = search_pv_tcp(&pv, ns, timeout_dur, debug_enabled).await?;
783            Ok::<SocketAddr, PvGetError>(addr)
784        });
785    }
786
787    if !no_broadcast {
788        let pv = pv.clone();
789        let targets = targets.clone();
790        set.spawn(async move {
791            let addr = search_pv(&pv, udp_port, timeout_dur, &targets, debug_enabled).await?;
792            Ok(addr)
793        });
794    }
795
796    let mut last_err = None;
797    while let Some(result) = set.join_next().await {
798        match result {
799            Ok(Ok(addr)) => {
800                set.abort_all();
801                return Ok(addr);
802            }
803            Ok(Err(e)) => {
804                if debug_enabled {
805                    debug!("pva search strategy failed: {}", e);
806                }
807                last_err = Some(e);
808            }
809            Err(join_err) => {
810                if debug_enabled {
811                    debug!("pva search task panicked: {}", join_err);
812                }
813            }
814        }
815    }
816
817    Err(last_err.unwrap_or(PvGetError::Timeout("search response")))
818}
819
820pub async fn discover_servers(
821    udp_port: u16,
822    timeout_dur: Duration,
823    targets: &[SearchTarget],
824    debug_enabled: bool,
825) -> Result<Vec<DiscoveredServer>, PvGetError> {
826    if targets.is_empty() {
827        return Err(PvGetError::Search("no search targets"));
828    }
829
830    let now = std::time::SystemTime::now()
831        .duration_since(std::time::UNIX_EPOCH)
832        .unwrap_or_default();
833    let seq = (now.as_nanos() as u32).wrapping_add(std::process::id());
834
835    let mut found: Vec<DiscoveredServer> = Vec::new();
836    let mut last_io_error: Option<std::io::Error> = None;
837    let deadline = tokio::time::Instant::now() + timeout_dur;
838
839    // Group targets by bind address so we can share a socket per bind.
840    let mut bind_groups: Vec<(IpAddr, Vec<IpAddr>)> = Vec::new();
841    for t in targets {
842        if let Some(group) = bind_groups.iter_mut().find(|(b, _)| *b == t.bind) {
843            group.1.push(t.target);
844        } else {
845            bind_groups.push((t.bind, vec![t.target]));
846        }
847    }
848
849    // Open sockets and send to all targets first, then collect responses.
850    // Store (socket, message, destinations) for retransmission.
851    let mut socket_info: Vec<(Arc<UdpSocket>, Vec<u8>, Vec<SocketAddr>)> = Vec::new();
852
853    for (bind_ip, group_targets) in &bind_groups {
854        // Always use an ephemeral port for the discovery client socket.
855        // We only receive unicast replies, so sharing the server's search
856        // port is unnecessary — and on Linux with SO_REUSEPORT the kernel
857        // would route our own outbound packet back to us instead of the
858        // server.
859        let bind_addr = SocketAddr::new(*bind_ip, 0);
860        let (std_sock, actual_bind_addr) = match bind_udp_reuse(bind_addr) {
861            Ok(sock) => {
862                let actual = sock.local_addr().unwrap_or(bind_addr);
863                (sock, actual)
864            }
865            Err(err) => {
866                if debug_enabled {
867                    debug!(
868                        "pva discover skipping bind={} step=bind kind={:?} err={}",
869                        bind_addr,
870                        err.kind(),
871                        err
872                    );
873                }
874                last_io_error = Some(err);
875                continue;
876            }
877        };
878        if let Err(err) = std_sock.set_broadcast(true) {
879            if debug_enabled {
880                debug!(
881                    "pva discover skipping bind={} step=set_broadcast kind={:?} err={}",
882                    bind_addr,
883                    err.kind(),
884                    err
885                );
886            }
887            last_io_error = Some(err);
888            continue;
889        }
890
891        join_multicast_any(&std_sock, *bind_ip);
892
893        let reply_addr = ip_to_bytes(*bind_ip);
894        let reply_port = match std_sock.local_addr() {
895            Ok(addr) => addr.port(),
896            Err(err) => {
897                if debug_enabled {
898                    debug!(
899                        "pva discover skipping bind={} step=local_addr kind={:?} err={}",
900                        bind_addr,
901                        err.kind(),
902                        err
903                    );
904                }
905                last_io_error = Some(err);
906                continue;
907            }
908        };
909        let msg = encode_search_request(seq, 0x81, reply_port, reply_addr, &[], 2, false);
910
911        let socket = match UdpSocket::from_std(std_sock) {
912            Ok(socket) => socket,
913            Err(err) => {
914                if debug_enabled {
915                    debug!(
916                        "pva discover skipping bind={} step=from_std kind={:?} err={}",
917                        bind_addr,
918                        err.kind(),
919                        err
920                    );
921                }
922                last_io_error = Some(err);
923                continue;
924            }
925        };
926
927        let dests: Vec<SocketAddr> = group_targets
928            .iter()
929            .map(|ip| SocketAddr::new(*ip, udp_port))
930            .collect();
931
932        // Send to every target in this bind group immediately.
933        for dest in &dests {
934            if debug_enabled {
935                debug!(
936                    "pva discover bind={} target={} server_port={} reply_port={} seq={}",
937                    actual_bind_addr,
938                    dest.ip(),
939                    udp_port,
940                    reply_port,
941                    seq
942                );
943            }
944            if let Err(err) = socket.send_to(&msg, dest).await {
945                if debug_enabled {
946                    debug!(
947                        "pva discover send_to target={} kind={:?} err={}",
948                        dest,
949                        err.kind(),
950                        err
951                    );
952                }
953                last_io_error = Some(err);
954            }
955        }
956
957        socket_info.push((Arc::new(socket), msg, dests));
958    }
959
960    if socket_info.is_empty() {
961        if let Some(err) = last_io_error {
962            return Err(PvGetError::Io(err));
963        }
964        return Err(PvGetError::Search("no search targets"));
965    }
966
967    // Spawn a receiver task per socket that forwards packets into a shared channel.
968    let (tx, mut rx) = tokio::sync::mpsc::channel::<(Vec<u8>, SocketAddr)>(64);
969    for (sock, _, _) in &socket_info {
970        let sock = Arc::clone(sock);
971        let tx = tx.clone();
972        tokio::spawn(async move {
973            loop {
974                let mut buf = vec![0u8; 2048];
975                match sock.recv_from(&mut buf).await {
976                    Ok((len, src)) => {
977                        buf.truncate(len);
978                        if tx.send((buf, src)).await.is_err() {
979                            break;
980                        }
981                    }
982                    Err(_) => break,
983                }
984            }
985        });
986    }
987    drop(tx); // Only spawned tasks hold senders; channel closes when they exit.
988
989    // Retransmit schedule: exponential backoff from start.
990    let retransmit_offsets = [100u64, 500, 1000, 2000];
991    let start = tokio::time::Instant::now();
992    let mut next_retransmit = 0usize;
993
994    loop {
995        // Compute the next wake-up: either the next retransmit or the deadline.
996        let next_retransmit_at = if next_retransmit < retransmit_offsets.len() {
997            start + Duration::from_millis(retransmit_offsets[next_retransmit])
998        } else {
999            deadline
1000        };
1001        let wake_at = next_retransmit_at.min(deadline);
1002
1003        tokio::select! {
1004            recv = rx.recv() => {
1005                let Some((buf, src)) = recv else { break };
1006                let mut pkt = PvaPacket::new(&buf);
1007                let Some(cmd) = pkt.decode_payload() else {
1008                    continue;
1009                };
1010                if let PvaPacketCommand::SearchResponse(payload) = cmd {
1011                    if payload.seq != seq {
1012                        continue;
1013                    }
1014                    if !payload.protocol.is_empty() && !payload.protocol.eq_ignore_ascii_case("tcp") {
1015                        continue;
1016                    }
1017                    let tcp_addr = decode_search_response_addr(payload.addr, payload.port, src);
1018                    found.push(DiscoveredServer {
1019                        guid: payload.guid,
1020                        tcp_addr,
1021                    });
1022                }
1023            }
1024            _ = tokio::time::sleep_until(wake_at) => {
1025                if tokio::time::Instant::now() >= deadline {
1026                    break;
1027                }
1028                // Retransmit to all targets on all sockets.
1029                if next_retransmit < retransmit_offsets.len() {
1030                    if debug_enabled {
1031                        debug!("pva discover retransmit round {}", next_retransmit + 1);
1032                    }
1033                    for (sock, msg, dests) in &socket_info {
1034                        for dest in dests {
1035                            let _ = sock.send_to(msg, dest).await;
1036                        }
1037                    }
1038                    next_retransmit += 1;
1039                }
1040            }
1041        }
1042    }
1043
1044    Ok(normalize_discovered_servers(found))
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::*;
1050    use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
1051
1052    #[test]
1053    fn encode_decode_search_request_roundtrip() {
1054        let seq = 1234;
1055        let cid = 42;
1056        let port = 5076;
1057        let pv_name = "TEST:PV";
1058        let reply_addr = ip_to_bytes(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)));
1059        let requests = [(cid, pv_name)];
1060        let msg = encode_search_request(seq, 0x81, port, reply_addr, &requests, 2, false);
1061        let mut pkt = PvaPacket::new(&msg);
1062        let cmd = pkt.decode_payload().expect("decoded");
1063        match cmd {
1064            PvaPacketCommand::Search(payload) => {
1065                assert_eq!(payload.seq, seq);
1066                assert_eq!(payload.mask, 0x81);
1067                assert_eq!(payload.addr, reply_addr);
1068                assert_eq!(payload.port, port);
1069                assert_eq!(payload.protocols, vec!["tcp".to_string()]);
1070                assert_eq!(payload.pv_requests.len(), 1);
1071                assert_eq!(payload.pv_requests[0].0, cid);
1072                assert_eq!(payload.pv_requests[0].1, pv_name.to_string());
1073            }
1074            other => panic!("unexpected decode: {:?}", other),
1075        }
1076    }
1077
1078    #[test]
1079    fn encode_decode_server_discovery_request_roundtrip() {
1080        let seq = 4321;
1081        let port = 5076;
1082        let reply_addr = ip_to_bytes(IpAddr::V4(Ipv4Addr::new(10, 20, 30, 40)));
1083        let msg = encode_search_request(seq, 0x81, port, reply_addr, &[], 2, false);
1084        let mut pkt = PvaPacket::new(&msg);
1085        let cmd = pkt.decode_payload().expect("decoded");
1086        match cmd {
1087            PvaPacketCommand::Search(payload) => {
1088                assert_eq!(payload.seq, seq);
1089                assert_eq!(payload.pv_requests.len(), 0);
1090                assert_eq!(payload.protocols, vec!["tcp".to_string()]);
1091            }
1092            other => panic!("unexpected decode: {:?}", other),
1093        }
1094    }
1095
1096    #[test]
1097    fn normalize_discovered_servers_deduplicates_by_guid_and_addr() {
1098        let guid = [1u8; 12];
1099        let s1 = DiscoveredServer {
1100            guid,
1101            tcp_addr: "127.0.0.1:5075".parse().unwrap(),
1102        };
1103        let s2 = DiscoveredServer {
1104            guid,
1105            tcp_addr: "127.0.0.1:5075".parse().unwrap(),
1106        };
1107        let s3 = DiscoveredServer {
1108            guid: [2u8; 12],
1109            tcp_addr: "127.0.0.1:5075".parse().unwrap(),
1110        };
1111        let normalized = normalize_discovered_servers(vec![s1, s2, s3]);
1112        assert_eq!(normalized.len(), 2);
1113    }
1114
1115    #[test]
1116    fn parse_addr_list_accepts_ip_and_ip_port() {
1117        let items = parse_addr_list("192.168.1.10 10.0.0.1:5076");
1118        assert!(items.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))));
1119        assert!(items.contains(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
1120    }
1121
1122    #[test]
1123    fn discovery_target_falls_back_to_limited_broadcast_for_invalid_netmask() {
1124        let ip = Ipv4Addr::new(130, 246, 90, 92);
1125        assert_eq!(
1126            discovery_target_for(ip, Ipv4Addr::new(255, 255, 255, 255)),
1127            Ipv4Addr::new(255, 255, 255, 255)
1128        );
1129        assert_eq!(
1130            discovery_target_for(ip, Ipv4Addr::new(0, 0, 0, 0)),
1131            Ipv4Addr::new(255, 255, 255, 255)
1132        );
1133    }
1134
1135    #[test]
1136    fn discovery_target_uses_directed_broadcast_for_normal_subnet() {
1137        let ip = Ipv4Addr::new(192, 168, 56, 1);
1138        let netmask = Ipv4Addr::new(255, 255, 255, 0);
1139        assert_eq!(
1140            discovery_target_for(ip, netmask),
1141            Ipv4Addr::new(192, 168, 56, 255)
1142        );
1143    }
1144
1145    #[test]
1146    fn parse_name_servers_ip_with_port() {
1147        let addrs = parse_name_servers("192.168.1.10:5075");
1148        assert_eq!(
1149            addrs,
1150            vec!["192.168.1.10:5075".parse::<SocketAddr>().unwrap()]
1151        );
1152    }
1153
1154    #[test]
1155    fn parse_name_servers_ip_without_port_defaults_to_5075() {
1156        let addrs = parse_name_servers("10.0.0.1");
1157        assert_eq!(
1158            addrs,
1159            vec![SocketAddr::new(
1160                IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1161                5075
1162            )]
1163        );
1164    }
1165
1166    #[test]
1167    fn parse_name_servers_multiple_comma_separated() {
1168        let addrs = parse_name_servers("10.0.0.1:5075,10.0.0.2:9876");
1169        assert_eq!(addrs.len(), 2);
1170        assert_eq!(addrs[0], "10.0.0.1:5075".parse::<SocketAddr>().unwrap());
1171        assert_eq!(addrs[1], "10.0.0.2:9876".parse::<SocketAddr>().unwrap());
1172    }
1173
1174    #[test]
1175    fn parse_name_servers_multiple_space_separated() {
1176        let addrs = parse_name_servers("10.0.0.1 10.0.0.2:5075");
1177        assert_eq!(addrs.len(), 2);
1178        assert_eq!(
1179            addrs[0],
1180            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 5075)
1181        );
1182        assert_eq!(addrs[1], "10.0.0.2:5075".parse::<SocketAddr>().unwrap());
1183    }
1184
1185    #[test]
1186    fn parse_name_servers_empty_string() {
1187        let addrs = parse_name_servers("");
1188        assert!(addrs.is_empty());
1189    }
1190
1191    #[test]
1192    fn parse_name_servers_whitespace_only() {
1193        let addrs = parse_name_servers("  \t  ");
1194        assert!(addrs.is_empty());
1195    }
1196
1197    #[test]
1198    fn parse_name_servers_mixed_separators() {
1199        let addrs = parse_name_servers("10.0.0.1:5075, 10.0.0.2  ,  10.0.0.3:9999");
1200        assert_eq!(addrs.len(), 3);
1201        assert_eq!(addrs[0], "10.0.0.1:5075".parse::<SocketAddr>().unwrap());
1202        assert_eq!(
1203            addrs[1],
1204            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 5075)
1205        );
1206        assert_eq!(addrs[2], "10.0.0.3:9999".parse::<SocketAddr>().unwrap());
1207    }
1208
1209    #[test]
1210    fn parse_name_servers_ipv6_with_port() {
1211        let addrs = parse_name_servers("[::1]:5075");
1212        assert_eq!(
1213            addrs,
1214            vec![SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5075)]
1215        );
1216    }
1217
1218    #[test]
1219    fn parse_name_servers_ipv6_without_port() {
1220        let addrs = parse_name_servers("::1");
1221        assert_eq!(
1222            addrs,
1223            vec![SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5075)]
1224        );
1225    }
1226
1227    #[test]
1228    fn decode_search_response_addr_falls_back_to_udp_source_when_unspecified() {
1229        let src: SocketAddr = "192.168.1.20:5076".parse().unwrap();
1230        let decoded = decode_search_response_addr([0u8; 16], 5075, src);
1231        assert_eq!(decoded, "192.168.1.20:5075".parse().unwrap());
1232    }
1233}