Skip to main content

ios_core/
device.rs

1#[cfg(feature = "mdns")]
2use std::collections::HashMap;
3#[cfg(feature = "tunnel")]
4use std::net::Ipv6Addr;
5#[cfg(feature = "tunnel")]
6use std::path::Path;
7#[cfg(feature = "tunnel")]
8use std::str::FromStr;
9use std::sync::Arc;
10#[cfg(any(feature = "tunnel", feature = "mdns"))]
11use std::time::Duration;
12#[cfg(feature = "tunnel")]
13use std::{
14    pin::Pin,
15    task::{Context, Poll},
16    time::Instant,
17};
18
19#[cfg(feature = "mdns")]
20use crate::lockdown::pair_record::default_pair_record_dir;
21use crate::lockdown::pair_record::PairRecord;
22#[cfg(feature = "tunnel")]
23use crate::lockdown::pairing::{
24    build_verify_start_tlv, build_verify_step2_tlv, HostIdentity, VerifyPairSession,
25};
26use crate::lockdown::protocol::{recv_lockdown, send_lockdown};
27#[cfg(feature = "tunnel")]
28use crate::lockdown::session::CORE_DEVICE_PROXY;
29use crate::lockdown::session::{start_lockdown_session, start_service, wrap_service_tls};
30use crate::lockdown::LOCKDOWN_PORT;
31use crate::mux::MuxClient;
32#[cfg(feature = "tunnel")]
33use crate::proto::tlv::TlvBuffer;
34#[cfg(feature = "tunnel-kernel")]
35use crate::tunnel::forward::forward_packets;
36use crate::tunnel::manager::{TunMode, TunnelHandle};
37#[cfg(feature = "tunnel-kernel")]
38use crate::tunnel::tun::kernel::KernelTunDevice;
39#[cfg(feature = "tunnel-userspace")]
40use crate::tunnel::tun::userspace::UserspaceTunDevice;
41#[cfg(feature = "tunnel")]
42use crate::xpc::message::XpcValue;
43#[cfg(all(feature = "tunnel", feature = "mdns"))]
44use crate::xpc::rsd::handshake as rsd_handshake;
45use crate::xpc::rsd::{RsdHandshake, ServiceDescriptor};
46#[cfg(feature = "tunnel")]
47use crate::xpc::XpcClient;
48#[cfg(feature = "tunnel")]
49use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
50#[cfg(feature = "tunnel")]
51use chacha20poly1305::{aead::Aead, KeyInit};
52#[cfg(feature = "tunnel")]
53use indexmap::IndexMap;
54#[cfg(feature = "tunnel")]
55use rand::RngCore;
56#[cfg(feature = "tunnel")]
57use tokio::io::ReadBuf;
58use tokio::io::{AsyncRead, AsyncWrite};
59use tokio::net::TcpStream;
60#[cfg(feature = "tunnel")]
61use tokio_stream::StreamExt;
62
63#[cfg(feature = "tunnel")]
64use crate::credentials::{PersistedCredentials, RemotePairingRecord};
65use crate::discovery::DeviceInfo;
66#[cfg(feature = "mdns")]
67use crate::discovery::{
68    browse_mobdev2, browse_remotepairing, mobdev2_wifi_mac, BonjourService, MdnsDevice,
69};
70use crate::error::CoreError;
71
72// ── ConnectOptions ─────────────────────────────────────────────────────────────
73
74#[derive(Debug, Clone, Default)]
75pub struct ConnectOptions {
76    pub tun_mode: TunMode,
77    pub pair_record_path: Option<std::path::PathBuf>,
78    /// Skip tunnel; use direct lockdown (iOS <17 or service-only access).
79    pub skip_tunnel: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
83pub struct InternationalConfiguration {
84    pub language: String,
85    pub locale: String,
86    pub supported_locales: Vec<String>,
87    pub supported_languages: Vec<String>,
88}
89
90// ── ServiceStream ──────────────────────────────────────────────────────────────
91
92/// A boxed bidirectional async stream returned by `connect_service()`.
93pub type ServiceStream = Box<dyn ServiceStreamTrait>;
94
95pub trait ServiceStreamTrait: AsyncRead + AsyncWrite + Unpin + Send {}
96impl<T: AsyncRead + AsyncWrite + Unpin + Send> ServiceStreamTrait for T {}
97
98#[cfg(feature = "tunnel")]
99const TUNNEL_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
100#[cfg(feature = "mdns")]
101const MOBDEV2_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
102#[cfg(all(feature = "tunnel", feature = "mdns"))]
103const DIRECT_RSD_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
104// Direct pairing TLV type: public key exchange (X25519 ephemeral public key)
105#[cfg(feature = "tunnel")]
106const DIRECT_PAIRING_TYPE_PUBLIC_KEY: u8 = 0x03;
107// Direct pairing TLV type: error response from device (pairing rejected or failed)
108#[cfg(feature = "tunnel")]
109const DIRECT_PAIRING_TYPE_ERROR: u8 = 0x07;
110#[cfg(feature = "tunnel")]
111const DIRECT_CONTROL_CHANNEL_ENVELOPE_TYPE: &str = "RemotePairing.ControlChannelMessageEnvelope";
112#[cfg(feature = "tunnel")]
113const DIRECT_CONTROL_CHANNEL_ORIGIN: &str = "host";
114
115// ── ConnectedDevice ────────────────────────────────────────────────────────────
116
117pub struct ConnectedDevice {
118    pub info: DeviceInfo,
119    pub(crate) tunnel: Option<Arc<TunnelHandle>>,
120    /// RSD service directory (only available after tunnel is up on iOS 17+)
121    pub(crate) rsd: Option<RsdHandshake>,
122    pair_record: Option<Arc<PairRecord>>,
123    lockdown_transport: LockdownTransport,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct PairedMobdev2Device {
128    pub udid: String,
129    pub host: String,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133#[cfg(feature = "tunnel")]
134enum TunnelConnectionTarget {
135    UserspaceProxy {
136        proxy_port: u16,
137        remote_addr: Ipv6Addr,
138    },
139    DirectIpv6 {
140        remote_addr: Ipv6Addr,
141    },
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145enum LockdownTransport {
146    Usbmux { device_id: u32 },
147    Tcp { host: String },
148}
149
150fn should_strip_service_ssl(service_name: &str) -> bool {
151    matches!(
152        service_name,
153        "com.apple.instruments.remoteserver" | "com.apple.accessibility.axAuditDaemon.remoteserver"
154    )
155}
156
157impl ConnectedDevice {
158    /// The RSD handshake result, if available (iOS 17+ with tunnel).
159    pub fn rsd(&self) -> Option<&RsdHandshake> {
160        self.rsd.as_ref()
161    }
162
163    /// Take ownership of the RSD handshake, consuming it from the device.
164    pub fn into_rsd(self) -> Option<RsdHandshake> {
165        self.rsd
166    }
167
168    /// The tunnel handle, if a tunnel is active.
169    pub fn tunnel_handle(&self) -> Option<&Arc<TunnelHandle>> {
170        self.tunnel.as_ref()
171    }
172
173    pub fn server_address(&self) -> Option<&str> {
174        self.tunnel.as_ref().map(|t| t.info.server_address.as_str())
175    }
176
177    pub fn userspace_port(&self) -> Option<u16> {
178        self.tunnel.as_ref().and_then(|t| t.userspace_port)
179    }
180
181    pub fn rsd_port(&self) -> Option<u16> {
182        self.tunnel.as_ref().map(|t| t.info.server_rsd_port)
183    }
184
185    fn pair_record(&self) -> Result<&Arc<PairRecord>, CoreError> {
186        self.pair_record
187            .as_ref()
188            .ok_or_else(|| CoreError::Unsupported("no pair record loaded".into()))
189    }
190
191    async fn lockdown_client(&self) -> Result<crate::lockdown::LockdownClient, CoreError> {
192        let pair_record = self.pair_record()?;
193        let stream = connect_lockdown_port(
194            &self.info.udid,
195            &self.lockdown_transport,
196            LOCKDOWN_PORT,
197            true,
198        )
199        .await?;
200        crate::lockdown::LockdownClient::connect_with_stream(stream, pair_record)
201            .await
202            .map_err(CoreError::from)
203    }
204
205    /// Open a lockdown service stream (iOS <17 or iOS 17+ services also accessible via lockdown).
206    pub async fn connect_service(&self, service_name: &str) -> Result<ServiceStream, CoreError> {
207        let pair_record = self.pair_record()?;
208        let lockdown_stream = connect_lockdown_port(
209            &self.info.udid,
210            &self.lockdown_transport,
211            LOCKDOWN_PORT,
212            true,
213        )
214        .await?;
215
216        let (_session_id, mut tls_reader, mut tls_writer) =
217            start_lockdown_session(lockdown_stream, pair_record).await?;
218
219        let (port, enable_ssl) =
220            start_service(&mut tls_reader, &mut tls_writer, service_name).await?;
221
222        let svc_stream =
223            connect_lockdown_port(&self.info.udid, &self.lockdown_transport, port, false).await?;
224
225        if enable_ssl {
226            let tls = wrap_service_tls(svc_stream, pair_record).await?;
227            if should_strip_service_ssl(service_name) {
228                let stream = crate::lockdown::session::strip_service_tls(tls)?;
229                Ok(Box::new(stream))
230            } else {
231                Ok(Box::new(tls))
232            }
233        } else {
234            Ok(Box::new(svc_stream))
235        }
236    }
237
238    /// Get the device's iOS version via lockdown.
239    pub async fn product_version(&self) -> Result<semver::Version, CoreError> {
240        let mut client = self.lockdown_client().await?;
241        let ver = client.product_version().await?;
242        Ok(ver)
243    }
244
245    /// Get a lockdown value by key (domain=None for global domain).
246    pub async fn lockdown_get_value(&self, key: Option<&str>) -> Result<plist::Value, CoreError> {
247        self.lockdown_get_value_in_domain(None, key).await
248    }
249
250    /// Get a lockdown value by optional domain and key.
251    pub async fn lockdown_get_value_in_domain(
252        &self,
253        domain: Option<&str>,
254        key: Option<&str>,
255    ) -> Result<plist::Value, CoreError> {
256        let mut client = self.lockdown_client().await?;
257        client.get_value(domain, key).await.map_err(CoreError::from)
258    }
259
260    /// Set a lockdown value by key (domain=None for global domain).
261    pub async fn lockdown_set_value(
262        &self,
263        key: Option<&str>,
264        value: plist::Value,
265    ) -> Result<(), CoreError> {
266        self.lockdown_set_value_in_domain(None, key, value).await
267    }
268
269    /// Set a lockdown value by optional domain and key.
270    pub async fn lockdown_set_value_in_domain(
271        &self,
272        domain: Option<&str>,
273        key: Option<&str>,
274        value: plist::Value,
275    ) -> Result<(), CoreError> {
276        let mut client = self.lockdown_client().await?;
277        client
278            .set_value(domain, key, value)
279            .await
280            .map_err(CoreError::from)
281    }
282
283    /// Remove a lockdown value by key (domain=None for global domain).
284    pub async fn lockdown_remove_value(&self, key: Option<&str>) -> Result<(), CoreError> {
285        self.lockdown_remove_value_in_domain(None, key).await
286    }
287
288    /// Remove a lockdown value by optional domain and key.
289    pub async fn lockdown_remove_value_in_domain(
290        &self,
291        domain: Option<&str>,
292        key: Option<&str>,
293    ) -> Result<(), CoreError> {
294        let mut client = self.lockdown_client().await?;
295        client
296            .remove_value(domain, key)
297            .await
298            .map_err(CoreError::from)
299    }
300
301    /// Read language and locale metadata from `com.apple.international`.
302    pub async fn lockdown_international_configuration(
303        &self,
304    ) -> Result<InternationalConfiguration, CoreError> {
305        const INTERNATIONAL_DOMAIN: &str = "com.apple.international";
306
307        let mut client = self.lockdown_client().await?;
308        let language = client
309            .get_value(Some(INTERNATIONAL_DOMAIN), Some("Language"))
310            .await?;
311        let locale = client
312            .get_value(Some(INTERNATIONAL_DOMAIN), Some("Locale"))
313            .await?;
314        let supported_locales = client
315            .get_value(Some(INTERNATIONAL_DOMAIN), Some("SupportedLocales"))
316            .await?;
317        let supported_languages = client
318            .get_value(Some(INTERNATIONAL_DOMAIN), Some("SupportedLanguages"))
319            .await?;
320
321        Ok(InternationalConfiguration {
322            language: plist_value_to_string(&language, "Language")?,
323            locale: plist_value_to_string(&locale, "Locale")?,
324            supported_locales: plist_value_to_string_vec(&supported_locales, "SupportedLocales")?,
325            supported_languages: plist_value_to_string_vec(
326                &supported_languages,
327                "SupportedLanguages",
328            )?,
329        })
330    }
331
332    /// Connect to an RSD service as a raw TCP stream (no XPC/H2 framing).
333    ///
334    /// Suitable for DTX-based services like `com.apple.instruments.dtservicehub`.
335    /// Supports userspace proxy and direct IPv6/kernel tunnel connections.
336    /// Performs an on-demand RSD handshake if rsd is not already populated.
337    pub async fn connect_rsd_service(
338        &self,
339        service_name: &str,
340    ) -> Result<ServiceStream, CoreError> {
341        let (resolved_service_name, port) =
342            self.resolve_rsd_service_with_retry(service_name).await?;
343
344        let mut stream = self.connect_tunnel_port(port).await?;
345        if resolved_service_name.ends_with(".shim.remote") {
346            rsd_checkin(&mut stream).await?;
347        }
348        Ok(stream)
349    }
350
351    /// Connect to an iOS 17+ XPC service via RSD.
352    ///
353    /// Returns an XpcClient ready for method calls.
354    /// Performs an on-demand RSD handshake if rsd is not already populated.
355    #[cfg(feature = "tunnel")]
356    pub async fn connect_xpc_service(&self, service_name: &str) -> Result<XpcClient, CoreError> {
357        let (_resolved_service_name, port) =
358            self.resolve_rsd_service_with_retry(service_name).await?;
359        let stream = self.connect_tunnel_port(port).await?;
360
361        XpcClient::connect_stream(stream)
362            .await
363            .map_err(CoreError::from)
364    }
365
366    async fn resolve_rsd_service_with_retry(
367        &self,
368        service_name: &str,
369    ) -> Result<(String, u16), CoreError> {
370        if let Some(rsd) = self.rsd.as_ref() {
371            return resolve_rsd_service(rsd, service_name).ok_or_else(|| {
372                CoreError::Unsupported(format!(
373                    "service '{service_name}' not found in RSD directory"
374                ))
375            });
376        }
377
378        let rsd = self.resolve_rsd_with_retry().await?;
379        resolve_rsd_service(&rsd, service_name).ok_or_else(|| {
380            CoreError::Unsupported(format!(
381                "service '{service_name}' not found in RSD directory"
382            ))
383        })
384    }
385
386    async fn resolve_rsd_with_retry(&self) -> Result<RsdHandshake, CoreError> {
387        #[cfg(not(feature = "tunnel"))]
388        {
389            Err(tunnel_unavailable())
390        }
391
392        #[cfg(feature = "tunnel")]
393        {
394            const MAX_ATTEMPTS: usize = 5;
395
396            if self.tunnel.is_none() {
397                return Err(CoreError::Unsupported(
398                    "RSD not available (no tunnel or iOS <17)".into(),
399                ));
400            }
401
402            for attempt in 0..MAX_ATTEMPTS {
403                if attempt > 0 {
404                    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
405                }
406
407                if let Some(rsd) = self.attempt_rsd_from_tunnel().await? {
408                    return Ok(rsd);
409                }
410
411                tracing::debug!(
412                    "RSD handshake attempt {}/{} failed, retrying...",
413                    attempt + 1,
414                    MAX_ATTEMPTS
415                );
416            }
417
418            Err(CoreError::Unsupported(
419                "RSD handshake failed after retries".into(),
420            ))
421        }
422    }
423
424    #[cfg(feature = "tunnel")]
425    async fn attempt_rsd_from_tunnel(&self) -> Result<Option<RsdHandshake>, CoreError> {
426        let server_addr = self
427            .server_address()
428            .ok_or_else(|| CoreError::Unsupported("no server address".into()))?;
429        let rsd_port = self
430            .rsd_port()
431            .ok_or_else(|| CoreError::Unsupported("no RSD port from tunnel info".into()))?;
432
433        Ok(match self.userspace_port() {
434            Some(proxy_port) => attempt_rsd_via_proxy(proxy_port, server_addr, rsd_port).await,
435            None => attempt_rsd(server_addr, rsd_port).await,
436        })
437    }
438
439    #[cfg(feature = "tunnel")]
440    fn tunnel_connection_target(&self) -> Result<TunnelConnectionTarget, CoreError> {
441        let server_addr = self
442            .server_address()
443            .ok_or_else(|| CoreError::Unsupported("no server address".into()))?;
444
445        resolve_tunnel_connection_target(server_addr, self.userspace_port())
446    }
447
448    async fn connect_tunnel_port(&self, port: u16) -> Result<ServiceStream, CoreError> {
449        #[cfg(not(feature = "tunnel"))]
450        {
451            let _ = port;
452            Err(tunnel_unavailable())
453        }
454
455        #[cfg(feature = "tunnel")]
456        {
457            use tokio::io::AsyncWriteExt;
458            use tokio::net::TcpStream;
459
460            match self.tunnel_connection_target()? {
461                TunnelConnectionTarget::UserspaceProxy {
462                    proxy_port,
463                    remote_addr,
464                } => {
465                    let mut proxy = TcpStream::connect(format!("127.0.0.1:{proxy_port}")).await?;
466                    proxy.write_all(&remote_addr.octets()).await?;
467                    proxy.write_all(&(port as u32).to_le_bytes()).await?;
468                    Ok(Box::new(proxy))
469                }
470                TunnelConnectionTarget::DirectIpv6 { remote_addr } => {
471                    let addr = std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
472                        remote_addr,
473                        port,
474                        0,
475                        0,
476                    ));
477                    Ok(Box::new(TcpStream::connect(addr).await?))
478                }
479            }
480        }
481    }
482}
483
484#[derive(serde::Serialize)]
485#[serde(rename_all = "PascalCase")]
486struct RsdCheckinRequest {
487    label: &'static str,
488    protocol_version: &'static str,
489    request: &'static str,
490}
491
492fn resolve_rsd_service(rsd: &RsdHandshake, requested_service: &str) -> Option<(String, u16)> {
493    if let Some(ServiceDescriptor { port }) = rsd.services.get(requested_service) {
494        return Some((requested_service.to_string(), *port));
495    }
496
497    let shim_service = format!("{requested_service}.shim.remote");
498    rsd.services
499        .get(&shim_service)
500        .map(|ServiceDescriptor { port }| (shim_service, *port))
501}
502
503#[cfg(feature = "tunnel")]
504fn resolve_tunnel_connection_target(
505    server_addr: &str,
506    userspace_port: Option<u16>,
507) -> Result<TunnelConnectionTarget, CoreError> {
508    let remote_addr = Ipv6Addr::from_str(server_addr)
509        .map_err(|e| CoreError::Protocol(format!("invalid IPv6 addr: {e}")))?;
510
511    Ok(match userspace_port {
512        Some(proxy_port) => TunnelConnectionTarget::UserspaceProxy {
513            proxy_port,
514            remote_addr,
515        },
516        None => TunnelConnectionTarget::DirectIpv6 { remote_addr },
517    })
518}
519
520fn validate_rsd_checkin_response(
521    response: plist::Value,
522    expected_request: &str,
523    context: &str,
524) -> Result<(), CoreError> {
525    let response = response.as_dictionary().ok_or_else(|| {
526        CoreError::Protocol(format!(
527            "{context} expected plist dictionary response, got {:?}",
528            response
529        ))
530    })?;
531
532    let actual_request = response
533        .get("Request")
534        .and_then(plist::Value::as_string)
535        .ok_or_else(|| {
536            CoreError::Protocol(format!(
537                "{context} missing Request field in response: {:?}",
538                response
539            ))
540        })?;
541
542    if actual_request != expected_request {
543        return Err(CoreError::Protocol(format!(
544            "{context} expected Request={expected_request}, got {actual_request}"
545        )));
546    }
547
548    if let Some(error) = response.get("Error") {
549        return Err(CoreError::Protocol(format!(
550            "{context} failed with Error={:?}",
551            error
552        )));
553    }
554
555    Ok(())
556}
557
558async fn rsd_checkin<S>(stream: &mut S) -> Result<(), CoreError>
559where
560    S: AsyncRead + AsyncWrite + Unpin,
561{
562    send_lockdown(
563        stream,
564        &RsdCheckinRequest {
565            label: "ios-rs",
566            protocol_version: "2",
567            request: "RSDCheckin",
568        },
569    )
570    .await?;
571
572    let checkin_response: plist::Value = recv_lockdown(stream).await?;
573    validate_rsd_checkin_response(checkin_response, "RSDCheckin", "RSD check-in response")?;
574
575    let start_service_response: plist::Value = recv_lockdown(stream).await?;
576    validate_rsd_checkin_response(
577        start_service_response,
578        "StartService",
579        "RSD start-service response",
580    )?;
581    Ok(())
582}
583
584// ── connect() ─────────────────────────────────────────────────────────────────
585
586pub async fn connect(udid: &str, opts: ConnectOptions) -> Result<ConnectedDevice, CoreError> {
587    let mut mux = MuxClient::connect().await?;
588    let devices = mux.list_devices().await?;
589    let dev = select_mux_device(devices, udid)
590        .ok_or_else(|| CoreError::DeviceNotFound(udid.to_string()))?;
591
592    let info = DeviceInfo {
593        udid: dev.serial_number.clone(),
594        device_id: dev.device_id,
595        connection_type: dev.connection_type.clone(),
596        product_id: dev.product_id,
597    };
598
599    let pair_record = load_pair_record(udid, opts.pair_record_path.as_deref())?;
600    connect_via_lockdown_transport(
601        info,
602        pair_record,
603        LockdownTransport::Usbmux {
604            device_id: dev.device_id,
605        },
606        opts,
607    )
608    .await
609}
610
611pub async fn connect_direct_usb_tunnel(
612    udid: &str,
613    rsd_ip: Option<&str>,
614    opts: ConnectOptions,
615) -> Result<ConnectedDevice, CoreError> {
616    let mut mux = MuxClient::connect().await?;
617    let devices = mux.list_devices().await?;
618    let dev = select_mux_device(devices, udid)
619        .ok_or_else(|| CoreError::DeviceNotFound(udid.to_string()))?;
620    let pair_record = try_load_pair_record(udid, opts.pair_record_path.as_deref());
621    let info = DeviceInfo {
622        udid: dev.serial_number.clone(),
623        device_id: dev.device_id,
624        connection_type: dev.connection_type.clone(),
625        product_id: dev.product_id,
626    };
627    let lockdown_transport = LockdownTransport::Usbmux {
628        device_id: dev.device_id,
629    };
630
631    if opts.skip_tunnel {
632        let pair_record =
633            require_pair_record(pair_record, udid, "direct USB lockdown access requires")?;
634        return Ok(ConnectedDevice {
635            info,
636            tunnel: None,
637            rsd: None,
638            pair_record: Some(pair_record),
639            lockdown_transport,
640        });
641    }
642
643    #[cfg(not(all(feature = "tunnel", feature = "mdns")))]
644    {
645        let _ = rsd_ip;
646        Err(CoreError::Unsupported(
647            "direct USB tunnel support requires ios-core features 'tunnel' and 'mdns'".into(),
648        ))
649    }
650
651    #[cfg(all(feature = "tunnel", feature = "mdns"))]
652    {
653        let targets = discover_direct_rsd_targets(udid, rsd_ip).await?;
654        if targets.is_empty() {
655            return Err(CoreError::Unsupported(format!(
656                "no _remoted target matched udid={udid} ip={rsd_ip:?}"
657            )));
658        }
659
660        let mut last_error = None;
661        for target in targets {
662            match connect_via_direct_rsd_target(
663                info.clone(),
664                pair_record.clone(),
665                lockdown_transport.clone(),
666                opts.clone(),
667                target,
668            )
669            .await
670            {
671                Ok(device) => return Ok(device),
672                Err(err) => last_error = Some(err),
673            }
674        }
675
676        Err(last_error.unwrap_or_else(|| {
677            CoreError::Unsupported(format!(
678                "no direct RSD target produced a tunnel for udid={udid}"
679            ))
680        }))
681    }
682}
683
684pub async fn connect_remote_pairing_tunnel(
685    udid: &str,
686    host: Option<&str>,
687    opts: ConnectOptions,
688) -> Result<ConnectedDevice, CoreError> {
689    let pair_record = try_load_pair_record(udid, opts.pair_record_path.as_deref());
690    let info = DeviceInfo {
691        udid: udid.to_string(),
692        device_id: 0,
693        connection_type: "Network".into(),
694        product_id: 0,
695    };
696
697    if opts.skip_tunnel {
698        let pair_record =
699            require_pair_record(pair_record, udid, "remote pairing lockdown access requires")?;
700        return Ok(ConnectedDevice {
701            info,
702            tunnel: None,
703            rsd: None,
704            pair_record: Some(pair_record),
705            lockdown_transport: LockdownTransport::Tcp {
706                host: host.unwrap_or_default().to_string(),
707            },
708        });
709    }
710
711    #[cfg(not(all(feature = "tunnel", feature = "mdns")))]
712    {
713        let _ = host;
714        Err(CoreError::Unsupported(
715            "remote pairing tunnel support requires ios-core features 'tunnel' and 'mdns'".into(),
716        ))
717    }
718
719    #[cfg(all(feature = "tunnel", feature = "mdns"))]
720    {
721        let targets = discover_remote_pairing_targets(udid, host).await?;
722        if targets.is_empty() {
723            return Err(CoreError::Unsupported(format!(
724                "no _remotepairing target matched udid={udid} host={host:?}"
725            )));
726        }
727
728        let mut last_error = None;
729        for (remote_host, port) in targets {
730            match connect_via_remote_pairing_target(
731                info.clone(),
732                pair_record.clone(),
733                opts.clone(),
734                udid,
735                &remote_host,
736                port,
737            )
738            .await
739            {
740                Ok(device) => return Ok(device),
741                Err(err) => last_error = Some(err),
742            }
743        }
744
745        Err(last_error.unwrap_or_else(|| {
746            CoreError::Unsupported(format!(
747                "no remote pairing target produced a tunnel for udid={udid}"
748            ))
749        }))
750    }
751}
752
753pub async fn connect_tcp_lockdown_tunnel(
754    udid: &str,
755    host: &str,
756    opts: ConnectOptions,
757) -> Result<ConnectedDevice, CoreError> {
758    let pair_record = load_pair_record(udid, opts.pair_record_path.as_deref())?;
759    let info = DeviceInfo {
760        udid: udid.to_string(),
761        device_id: 0,
762        connection_type: "Network".into(),
763        product_id: 0,
764    };
765    connect_via_lockdown_transport(
766        info,
767        pair_record,
768        LockdownTransport::Tcp {
769            host: host.to_string(),
770        },
771        opts,
772    )
773    .await
774}
775
776#[cfg(feature = "mdns")]
777pub async fn discover_paired_mobdev2_devices() -> Result<Vec<PairedMobdev2Device>, CoreError> {
778    let wifi_mac_to_udid = tokio::task::spawn_blocking(load_wifi_mac_pairings)
779        .await
780        .map_err(|e| CoreError::Other(format!("join error: {e}")))??;
781    let services = browse_mobdev2(MOBDEV2_DISCOVERY_TIMEOUT).await?;
782    Ok(match_paired_mobdev2_targets(&services, &wifi_mac_to_udid))
783}
784
785fn select_mux_device(
786    devices: Vec<crate::mux::MuxDevice>,
787    udid: &str,
788) -> Option<crate::mux::MuxDevice> {
789    let mut fallback = None;
790
791    for device in devices {
792        if device.serial_number != udid {
793            continue;
794        }
795
796        let is_usb = device.connection_type.eq_ignore_ascii_case("USB");
797        fallback = Some(device);
798
799        if is_usb {
800            return fallback;
801        }
802    }
803
804    fallback
805}
806
807fn load_pair_record(
808    udid: &str,
809    pair_record_path: Option<&std::path::Path>,
810) -> Result<Arc<PairRecord>, CoreError> {
811    Ok(Arc::new(if let Some(path) = pair_record_path {
812        PairRecord::load_from_path(path, udid)?
813    } else {
814        PairRecord::load(udid)?
815    }))
816}
817
818fn try_load_pair_record(
819    udid: &str,
820    pair_record_path: Option<&std::path::Path>,
821) -> Option<Arc<PairRecord>> {
822    load_pair_record(udid, pair_record_path).ok()
823}
824
825fn require_pair_record(
826    pair_record: Option<Arc<PairRecord>>,
827    udid: &str,
828    context: &str,
829) -> Result<Arc<PairRecord>, CoreError> {
830    pair_record.ok_or_else(|| {
831        CoreError::Unsupported(format!("{context} a lockdown pair record for {udid}"))
832    })
833}
834
835async fn connect_lockdown_port(
836    udid: &str,
837    transport: &LockdownTransport,
838    port: u16,
839    read_pair_record: bool,
840) -> Result<ServiceStream, CoreError> {
841    match transport {
842        LockdownTransport::Usbmux { device_id } => {
843            let mut mux = MuxClient::connect().await?;
844            if read_pair_record {
845                mux.read_pair_record(udid).await?;
846            }
847            let stream = mux.connect_to_port(*device_id, port).await?;
848            Ok(Box::new(stream))
849        }
850        LockdownTransport::Tcp { host, .. } => {
851            let stream = TcpStream::connect((host.as_str(), port)).await?;
852            Ok(Box::new(stream))
853        }
854    }
855}
856
857async fn connect_via_lockdown_transport(
858    info: DeviceInfo,
859    pair_record: Arc<PairRecord>,
860    lockdown_transport: LockdownTransport,
861    opts: ConnectOptions,
862) -> Result<ConnectedDevice, CoreError> {
863    if opts.skip_tunnel {
864        return Ok(ConnectedDevice {
865            info,
866            tunnel: None,
867            rsd: None,
868            pair_record: Some(pair_record),
869            lockdown_transport,
870        });
871    }
872
873    #[cfg(not(feature = "tunnel"))]
874    {
875        let _ = (info, pair_record, lockdown_transport);
876        Err(CoreError::Unsupported(
877            "CoreDevice tunnel support requires ios-core feature 'tunnel'".into(),
878        ))
879    }
880
881    #[cfg(feature = "tunnel")]
882    {
883        let lockdown_stream =
884            connect_lockdown_port(&info.udid, &lockdown_transport, LOCKDOWN_PORT, true).await?;
885
886        tracing::info!("tunnel connect: starting lockdown session");
887        let (_session_id, mut tls_reader, mut tls_writer) =
888            start_lockdown_session(lockdown_stream, &pair_record).await?;
889        tracing::info!("tunnel connect: lockdown session established");
890
891        tracing::info!("tunnel connect: requesting CoreDeviceProxy");
892        let (service_port, enable_service_ssl) =
893            start_service(&mut tls_reader, &mut tls_writer, CORE_DEVICE_PROXY).await?;
894        tracing::info!(
895        "tunnel connect: CoreDeviceProxy started on port {service_port} (ssl={enable_service_ssl})"
896    );
897
898        let proxy_stream_raw =
899            connect_lockdown_port(&info.udid, &lockdown_transport, service_port, false).await?;
900
901        let mut proxy_stream = if enable_service_ssl {
902            tracing::info!("tunnel connect: wrapping CoreDeviceProxy with TLS");
903            ProxyStream::Tls(Box::new(
904                wrap_service_tls(proxy_stream_raw, &pair_record).await?,
905            ))
906        } else {
907            tracing::info!("tunnel connect: CoreDeviceProxy is plaintext");
908            ProxyStream::Plain(proxy_stream_raw)
909        };
910        tracing::info!("tunnel connect: CoreDeviceProxy stream ready");
911
912        tracing::info!(
913            "tunnel connect: exchanging CDTunnel parameters (timeout={} ms)",
914            TUNNEL_HANDSHAKE_TIMEOUT.as_millis()
915        );
916        let tunnel_info = crate::tunnel::handshake::exchange_tunnel_parameters_with_timeout(
917            &mut proxy_stream,
918            TUNNEL_HANDSHAKE_TIMEOUT,
919        )
920        .await
921        .map_err(CoreError::Tunnel)?;
922        tracing::info!("tunnel connect: CDTunnel parameters received");
923        tracing::info!(
924            "tunnel_info: server={} rsd_port={} client={} mtu={}",
925            tunnel_info.server_address,
926            tunnel_info.server_rsd_port,
927            tunnel_info.client_address,
928            tunnel_info.client_mtu
929        );
930
931        match opts.tun_mode {
932            TunMode::Kernel => {
933                #[cfg(not(feature = "tunnel-kernel"))]
934                {
935                    return Err(CoreError::Unsupported(
936                        "kernel TUN support requires ios-core feature 'tunnel-kernel'".into(),
937                    ));
938                }
939                #[cfg(feature = "tunnel-kernel")]
940                {
941                    let (handle, cancel_rx) =
942                        TunnelHandle::new(info.udid.clone(), tunnel_info.clone(), None);
943                    let tun = KernelTunDevice::create(
944                        &tunnel_info.client_address,
945                        tunnel_info.client_mtu,
946                    )
947                    .await
948                    .map_err(CoreError::Tunnel)?;
949                    let mtu = tunnel_info.client_mtu;
950                    tokio::spawn(async move {
951                        if let Err(e) = forward_packets(proxy_stream, tun, mtu, cancel_rx).await {
952                            tracing::error!("kernel TUN forward: {e}");
953                        }
954                    });
955                    let rsd =
956                        attempt_rsd(&tunnel_info.server_address, tunnel_info.server_rsd_port).await;
957                    Ok(ConnectedDevice {
958                        info,
959                        tunnel: Some(Arc::new(handle)),
960                        rsd,
961                        pair_record: Some(pair_record),
962                        lockdown_transport,
963                    })
964                }
965            }
966            TunMode::Userspace => {
967                #[cfg(not(feature = "tunnel-userspace"))]
968                {
969                    return Err(CoreError::Unsupported(
970                        "userspace tunnel support requires ios-core feature 'tunnel-userspace'"
971                            .into(),
972                    ));
973                }
974                #[cfg(feature = "tunnel-userspace")]
975                {
976                    let userspace = UserspaceTunDevice::start(
977                        &tunnel_info.client_address,
978                        &tunnel_info.server_address,
979                        tunnel_info.client_mtu,
980                        proxy_stream,
981                    )
982                    .await
983                    .map_err(CoreError::Tunnel)?;
984
985                    let proxy_port = userspace.local_port;
986                    let handle = TunnelHandle::new_userspace(
987                        info.udid.clone(),
988                        tunnel_info.clone(),
989                        userspace,
990                    );
991                    let rsd = attempt_rsd_via_proxy(
992                        proxy_port,
993                        &tunnel_info.server_address,
994                        tunnel_info.server_rsd_port,
995                    )
996                    .await;
997                    Ok(ConnectedDevice {
998                        info,
999                        tunnel: Some(Arc::new(handle)),
1000                        rsd,
1001                        pair_record: Some(pair_record),
1002                        lockdown_transport,
1003                    })
1004                }
1005            }
1006        }
1007    }
1008}
1009
1010#[cfg(not(feature = "tunnel"))]
1011fn tunnel_unavailable() -> CoreError {
1012    CoreError::Unsupported("CoreDevice tunnel support requires ios-core feature 'tunnel'".into())
1013}
1014
1015#[cfg(feature = "tunnel")]
1016struct GuardedTunnelStream<G> {
1017    stream: tokio_openssl::SslStream<TcpStream>,
1018    _guard: G,
1019}
1020
1021#[cfg(feature = "tunnel")]
1022impl<G> Unpin for GuardedTunnelStream<G> {}
1023
1024#[cfg(feature = "tunnel")]
1025impl<G> AsyncRead for GuardedTunnelStream<G> {
1026    fn poll_read(
1027        self: Pin<&mut Self>,
1028        cx: &mut Context<'_>,
1029        buf: &mut ReadBuf<'_>,
1030    ) -> Poll<std::io::Result<()>> {
1031        Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
1032    }
1033}
1034
1035#[cfg(feature = "tunnel")]
1036impl<G> AsyncWrite for GuardedTunnelStream<G> {
1037    fn poll_write(
1038        self: Pin<&mut Self>,
1039        cx: &mut Context<'_>,
1040        buf: &[u8],
1041    ) -> Poll<std::io::Result<usize>> {
1042        Pin::new(&mut self.get_mut().stream).poll_write(cx, buf)
1043    }
1044
1045    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
1046        Pin::new(&mut self.get_mut().stream).poll_flush(cx)
1047    }
1048
1049    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
1050        Pin::new(&mut self.get_mut().stream).poll_shutdown(cx)
1051    }
1052}
1053
1054#[cfg(feature = "tunnel")]
1055struct LoadedRemotePairingCredentials {
1056    host_identity: HostIdentity,
1057}
1058
1059#[cfg(feature = "tunnel")]
1060struct RemotePairingControlChannel {
1061    stream: TcpStream,
1062}
1063
1064#[cfg(feature = "tunnel")]
1065impl RemotePairingControlChannel {
1066    async fn connect(host: &str, port: u16) -> Result<Self, CoreError> {
1067        Ok(Self {
1068            stream: TcpStream::connect((host, port)).await?,
1069        })
1070    }
1071
1072    async fn send(&mut self, payload: &serde_json::Value) -> Result<(), CoreError> {
1073        use tokio::io::AsyncWriteExt;
1074
1075        let body = serde_json::to_vec(payload)?;
1076        if body.len() > u16::MAX as usize {
1077            return Err(CoreError::Protocol(format!(
1078                "remote pairing payload too large: {} bytes",
1079                body.len()
1080            )));
1081        }
1082
1083        self.stream.write_all(b"RPPairing").await?;
1084        self.stream
1085            .write_all(&(body.len() as u16).to_be_bytes())
1086            .await?;
1087        self.stream.write_all(&body).await?;
1088        self.stream.flush().await?;
1089        Ok(())
1090    }
1091
1092    async fn recv(&mut self) -> Result<serde_json::Value, CoreError> {
1093        use tokio::io::AsyncReadExt;
1094
1095        let mut magic = [0u8; 9];
1096        self.stream.read_exact(&mut magic).await?;
1097        if &magic != b"RPPairing" {
1098            return Err(CoreError::Protocol(format!(
1099                "invalid RPPairing magic: {magic:?}"
1100            )));
1101        }
1102
1103        let mut length = [0u8; 2];
1104        self.stream.read_exact(&mut length).await?;
1105        let body_len = u16::from_be_bytes(length) as usize;
1106        let mut body = vec![0u8; body_len];
1107        self.stream.read_exact(&mut body).await?;
1108        Ok(serde_json::from_slice(&body)?)
1109    }
1110}
1111
1112#[cfg(all(feature = "tunnel", feature = "mdns"))]
1113async fn discover_direct_rsd_targets(
1114    udid: &str,
1115    ip_filter: Option<&str>,
1116) -> Result<Vec<MdnsDevice>, CoreError> {
1117    let stream = crate::discovery::discover_mdns().await?;
1118    tokio::pin!(stream);
1119
1120    let deadline = Instant::now() + DIRECT_RSD_DISCOVERY_TIMEOUT;
1121    let mut targets = Vec::new();
1122    let mut seen = std::collections::HashSet::new();
1123
1124    loop {
1125        let remaining = deadline.saturating_duration_since(Instant::now());
1126        if remaining.is_zero() {
1127            break;
1128        }
1129
1130        match tokio::time::timeout(remaining, stream.next()).await {
1131            Ok(Some(device)) => {
1132                let ip = device.ipv6.to_string();
1133                if ip_filter.map(|filter| filter != ip).unwrap_or(false) {
1134                    continue;
1135                }
1136
1137                let key = (device.ipv6, device.rsd_port);
1138                if !seen.insert(key) {
1139                    continue;
1140                }
1141
1142                targets.push(device);
1143            }
1144            Ok(None) | Err(_) => break,
1145        }
1146    }
1147
1148    targets.sort_by_key(|device| {
1149        if device.udid == udid {
1150            0
1151        } else if device.udid.is_empty() {
1152            1
1153        } else {
1154            2
1155        }
1156    });
1157    Ok(targets)
1158}
1159
1160#[cfg(all(feature = "tunnel", feature = "mdns"))]
1161async fn discover_remote_pairing_targets(
1162    udid: &str,
1163    host_filter: Option<&str>,
1164) -> Result<Vec<(String, u16)>, CoreError> {
1165    let services = browse_remotepairing(MOBDEV2_DISCOVERY_TIMEOUT).await?;
1166    let mut targets = Vec::new();
1167    let mut seen = std::collections::HashSet::new();
1168
1169    for service in services {
1170        let Some(host) = preferred_lockdown_address(&service.addresses) else {
1171            continue;
1172        };
1173        if host_filter.map(|filter| filter != host).unwrap_or(false) {
1174            continue;
1175        }
1176
1177        let key = (host.to_string(), service.port);
1178        if seen.insert(key.clone()) {
1179            targets.push(key);
1180        }
1181    }
1182
1183    if targets.is_empty() {
1184        return Err(CoreError::Unsupported(format!(
1185            "no browse_remotepairing target matched udid={udid} host={host_filter:?}"
1186        )));
1187    }
1188
1189    Ok(targets)
1190}
1191
1192#[cfg(all(feature = "tunnel", feature = "mdns"))]
1193async fn connect_via_direct_rsd_target(
1194    info: DeviceInfo,
1195    pair_record: Option<Arc<PairRecord>>,
1196    lockdown_transport: LockdownTransport,
1197    opts: ConnectOptions,
1198    target: MdnsDevice,
1199) -> Result<ConnectedDevice, CoreError> {
1200    let rsd = rsd_handshake(target.ipv6, target.rsd_port).await?;
1201    if rsd.udid != info.udid {
1202        return Err(CoreError::Protocol(format!(
1203            "direct RSD target {} resolved to unexpected udid {}",
1204            target.ipv6, rsd.udid
1205        )));
1206    }
1207
1208    let service_port = rsd
1209        .get_port(crate::pairing_transport::UNTRUSTED_SERVICE_NAME)
1210        .ok_or_else(|| {
1211            CoreError::Unsupported(format!(
1212                "direct RSD target {} does not expose {}",
1213                target.ipv6,
1214                crate::pairing_transport::UNTRUSTED_SERVICE_NAME
1215            ))
1216        })?;
1217    let mut direct_stream = establish_direct_tunnel_stream(target.ipv6, service_port).await?;
1218
1219    let tunnel_info = crate::tunnel::handshake::exchange_tunnel_parameters_with_timeout(
1220        &mut direct_stream,
1221        TUNNEL_HANDSHAKE_TIMEOUT,
1222    )
1223    .await
1224    .map_err(CoreError::Tunnel)?;
1225
1226    match opts.tun_mode {
1227        TunMode::Kernel => {
1228            #[cfg(not(feature = "tunnel-kernel"))]
1229            {
1230                return Err(CoreError::Unsupported(
1231                    "kernel TUN support requires ios-core feature 'tunnel-kernel'".into(),
1232                ));
1233            }
1234            #[cfg(feature = "tunnel-kernel")]
1235            {
1236                let (handle, cancel_rx) =
1237                    TunnelHandle::new(info.udid.clone(), tunnel_info.clone(), None);
1238                let tun =
1239                    KernelTunDevice::create(&tunnel_info.client_address, tunnel_info.client_mtu)
1240                        .await
1241                        .map_err(CoreError::Tunnel)?;
1242                let mtu = tunnel_info.client_mtu;
1243                tokio::spawn(async move {
1244                    if let Err(err) = forward_packets(direct_stream, tun, mtu, cancel_rx).await {
1245                        tracing::error!("direct kernel TUN forward: {err}");
1246                    }
1247                });
1248                let rsd =
1249                    attempt_rsd(&tunnel_info.server_address, tunnel_info.server_rsd_port).await;
1250                Ok(ConnectedDevice {
1251                    info,
1252                    tunnel: Some(Arc::new(handle)),
1253                    rsd,
1254                    pair_record,
1255                    lockdown_transport,
1256                })
1257            }
1258        }
1259        TunMode::Userspace => {
1260            #[cfg(not(feature = "tunnel-userspace"))]
1261            {
1262                return Err(CoreError::Unsupported(
1263                    "userspace tunnel support requires ios-core feature 'tunnel-userspace'".into(),
1264                ));
1265            }
1266            #[cfg(feature = "tunnel-userspace")]
1267            {
1268                let userspace = UserspaceTunDevice::start(
1269                    &tunnel_info.client_address,
1270                    &tunnel_info.server_address,
1271                    tunnel_info.client_mtu,
1272                    direct_stream,
1273                )
1274                .await
1275                .map_err(CoreError::Tunnel)?;
1276
1277                let proxy_port = userspace.local_port;
1278                let handle =
1279                    TunnelHandle::new_userspace(info.udid.clone(), tunnel_info.clone(), userspace);
1280                let rsd = attempt_rsd_via_proxy(
1281                    proxy_port,
1282                    &tunnel_info.server_address,
1283                    tunnel_info.server_rsd_port,
1284                )
1285                .await;
1286                Ok(ConnectedDevice {
1287                    info,
1288                    tunnel: Some(Arc::new(handle)),
1289                    rsd,
1290                    pair_record,
1291                    lockdown_transport,
1292                })
1293            }
1294        }
1295    }
1296}
1297
1298#[cfg(all(feature = "tunnel", feature = "mdns"))]
1299async fn connect_via_remote_pairing_target(
1300    info: DeviceInfo,
1301    pair_record: Option<Arc<PairRecord>>,
1302    opts: ConnectOptions,
1303    remote_identifier: &str,
1304    host: &str,
1305    port: u16,
1306) -> Result<ConnectedDevice, CoreError> {
1307    let mut remote_stream =
1308        establish_remote_pairing_tunnel_stream(remote_identifier, host, port).await?;
1309
1310    let tunnel_info = crate::tunnel::handshake::exchange_tunnel_parameters_with_timeout(
1311        &mut remote_stream,
1312        TUNNEL_HANDSHAKE_TIMEOUT,
1313    )
1314    .await
1315    .map_err(CoreError::Tunnel)?;
1316
1317    match opts.tun_mode {
1318        TunMode::Kernel => {
1319            #[cfg(not(feature = "tunnel-kernel"))]
1320            {
1321                return Err(CoreError::Unsupported(
1322                    "kernel TUN support requires ios-core feature 'tunnel-kernel'".into(),
1323                ));
1324            }
1325            #[cfg(feature = "tunnel-kernel")]
1326            {
1327                let (handle, cancel_rx) =
1328                    TunnelHandle::new(info.udid.clone(), tunnel_info.clone(), None);
1329                let tun =
1330                    KernelTunDevice::create(&tunnel_info.client_address, tunnel_info.client_mtu)
1331                        .await
1332                        .map_err(CoreError::Tunnel)?;
1333                let mtu = tunnel_info.client_mtu;
1334                tokio::spawn(async move {
1335                    if let Err(err) = forward_packets(remote_stream, tun, mtu, cancel_rx).await {
1336                        tracing::error!("remote pairing kernel TUN forward: {err}");
1337                    }
1338                });
1339                let rsd =
1340                    attempt_rsd(&tunnel_info.server_address, tunnel_info.server_rsd_port).await;
1341                Ok(ConnectedDevice {
1342                    info,
1343                    tunnel: Some(Arc::new(handle)),
1344                    rsd,
1345                    pair_record,
1346                    lockdown_transport: LockdownTransport::Tcp {
1347                        host: host.to_string(),
1348                    },
1349                })
1350            }
1351        }
1352        TunMode::Userspace => {
1353            #[cfg(not(feature = "tunnel-userspace"))]
1354            {
1355                return Err(CoreError::Unsupported(
1356                    "userspace tunnel support requires ios-core feature 'tunnel-userspace'".into(),
1357                ));
1358            }
1359            #[cfg(feature = "tunnel-userspace")]
1360            {
1361                let userspace = UserspaceTunDevice::start(
1362                    &tunnel_info.client_address,
1363                    &tunnel_info.server_address,
1364                    tunnel_info.client_mtu,
1365                    remote_stream,
1366                )
1367                .await
1368                .map_err(CoreError::Tunnel)?;
1369
1370                let proxy_port = userspace.local_port;
1371                let handle =
1372                    TunnelHandle::new_userspace(info.udid.clone(), tunnel_info.clone(), userspace);
1373                let rsd = attempt_rsd_via_proxy(
1374                    proxy_port,
1375                    &tunnel_info.server_address,
1376                    tunnel_info.server_rsd_port,
1377                )
1378                .await;
1379                Ok(ConnectedDevice {
1380                    info,
1381                    tunnel: Some(Arc::new(handle)),
1382                    rsd,
1383                    pair_record,
1384                    lockdown_transport: LockdownTransport::Tcp {
1385                        host: host.to_string(),
1386                    },
1387                })
1388            }
1389        }
1390    }
1391}
1392
1393#[cfg(feature = "tunnel")]
1394async fn establish_direct_tunnel_stream(
1395    rsd_addr: Ipv6Addr,
1396    service_port: u16,
1397) -> Result<GuardedTunnelStream<XpcClient>, CoreError> {
1398    let mut client = XpcClient::connect(rsd_addr, service_port).await?;
1399    let mut sequence_number = 0u64;
1400
1401    client
1402        .send(build_direct_handshake_request(sequence_number))
1403        .await?;
1404    sequence_number += 1;
1405
1406    let handshake = client.recv().await?;
1407    let remote_identifier =
1408        extract_direct_remote_identifier(handshake.body.as_ref().ok_or_else(|| {
1409            CoreError::Protocol("direct handshake response missing body".into())
1410        })?)?;
1411
1412    let loaded = {
1413        let id = remote_identifier.clone();
1414        tokio::task::spawn_blocking(move || load_remote_pairing_credentials(&id))
1415            .await
1416            .map_err(|e| CoreError::Other(format!("spawn_blocking join error: {e}")))?
1417    }?;
1418
1419    let mut our_secret = [0u8; 32];
1420    rand::thread_rng().fill_bytes(&mut our_secret);
1421    let static_secret = x25519_dalek::StaticSecret::from(our_secret);
1422    let our_public = x25519_dalek::PublicKey::from(&static_secret).to_bytes();
1423
1424    client
1425        .send(build_direct_pairing_event(
1426            &build_verify_start_tlv(&our_public),
1427            "verifyManualPairing",
1428            true,
1429            None,
1430            sequence_number,
1431        ))
1432        .await?;
1433    sequence_number += 1;
1434
1435    let verify_start = client.recv().await?;
1436    let verify_start_tlv =
1437        extract_direct_pairing_tlv(verify_start.body.as_ref().ok_or_else(|| {
1438            CoreError::Protocol("verifyManualPairing start missing body".into())
1439        })?)?;
1440    let verify_start_fields = TlvBuffer::decode(&verify_start_tlv);
1441    if let Some(error) = verify_start_fields.get(&DIRECT_PAIRING_TYPE_ERROR) {
1442        send_pair_verify_failed(&mut client, sequence_number).await?;
1443        return Err(CoreError::Protocol(format!(
1444            "verifyManualPairing start rejected: {error:?}"
1445        )));
1446    }
1447
1448    let device_public: [u8; 32] = verify_start_fields
1449        .get(&DIRECT_PAIRING_TYPE_PUBLIC_KEY)
1450        .ok_or_else(|| {
1451            CoreError::Protocol("verifyManualPairing start missing device public key".into())
1452        })?
1453        .as_ref()
1454        .try_into()
1455        .map_err(|_| {
1456            CoreError::Protocol("verifyManualPairing device public key must be 32 bytes".into())
1457        })?;
1458
1459    let verify_session = build_verify_step2_tlv(
1460        our_secret,
1461        &our_public,
1462        &device_public,
1463        &loaded.host_identity,
1464    )
1465    .map_err(|e| CoreError::Other(format!("verifyManualPairing finish build failed: {e}")))?;
1466
1467    client
1468        .send(build_direct_pairing_event(
1469            &verify_session.tlv,
1470            "verifyManualPairing",
1471            false,
1472            None,
1473            sequence_number,
1474        ))
1475        .await?;
1476    sequence_number += 1;
1477
1478    let verify_finish = client.recv().await?;
1479    let verify_finish_tlv =
1480        extract_direct_pairing_tlv(verify_finish.body.as_ref().ok_or_else(|| {
1481            CoreError::Protocol("verifyManualPairing finish missing body".into())
1482        })?)?;
1483    let verify_finish_fields = TlvBuffer::decode(&verify_finish_tlv);
1484    if let Some(error) = verify_finish_fields.get(&DIRECT_PAIRING_TYPE_ERROR) {
1485        send_pair_verify_failed(&mut client, sequence_number).await?;
1486        return Err(CoreError::Protocol(format!(
1487            "verifyManualPairing finish rejected: {error:?}"
1488        )));
1489    }
1490
1491    let listener_port =
1492        create_direct_tcp_listener(&mut client, &verify_session, sequence_number).await?;
1493    let stream = crate::psk_tls::connect_psk_tls(
1494        &rsd_addr.to_string(),
1495        listener_port,
1496        &verify_session.encryption_key,
1497    )
1498    .await
1499    .map_err(|e| CoreError::Other(format!("direct TLS-PSK listener connect failed: {e}")))?;
1500
1501    Ok(GuardedTunnelStream {
1502        stream,
1503        _guard: client,
1504    })
1505}
1506
1507#[cfg(feature = "tunnel")]
1508async fn establish_remote_pairing_tunnel_stream(
1509    remote_identifier: &str,
1510    host: &str,
1511    port: u16,
1512) -> Result<GuardedTunnelStream<RemotePairingControlChannel>, CoreError> {
1513    let loaded = {
1514        let id = remote_identifier.to_owned();
1515        tokio::task::spawn_blocking(move || load_remote_pairing_credentials(&id))
1516            .await
1517            .map_err(|e| CoreError::Other(format!("spawn_blocking join error: {e}")))?
1518    }?;
1519    let mut control = RemotePairingControlChannel::connect(host, port).await?;
1520    let mut sequence_number = 0u64;
1521
1522    control
1523        .send(&build_remote_pairing_handshake_request(sequence_number))
1524        .await?;
1525    sequence_number += 1;
1526    let _handshake = control.recv().await?;
1527
1528    let mut our_secret = [0u8; 32];
1529    rand::thread_rng().fill_bytes(&mut our_secret);
1530    let static_secret = x25519_dalek::StaticSecret::from(our_secret);
1531    let our_public = x25519_dalek::PublicKey::from(&static_secret).to_bytes();
1532
1533    control
1534        .send(&build_remote_pairing_pairing_event(
1535            &build_verify_start_tlv(&our_public),
1536            "verifyManualPairing",
1537            true,
1538            None,
1539            sequence_number,
1540        ))
1541        .await?;
1542    sequence_number += 1;
1543
1544    let verify_start = control.recv().await?;
1545    let verify_start_tlv = extract_remote_pairing_tlv(&verify_start)?;
1546    let verify_start_fields = TlvBuffer::decode(&verify_start_tlv);
1547    if let Some(error) = verify_start_fields.get(&DIRECT_PAIRING_TYPE_ERROR) {
1548        control
1549            .send(&build_remote_pairing_pair_verify_failed_event(
1550                sequence_number,
1551            ))
1552            .await?;
1553        return Err(CoreError::Protocol(format!(
1554            "remote pairing verify start rejected: {error:?}"
1555        )));
1556    }
1557
1558    let device_public: [u8; 32] = verify_start_fields
1559        .get(&DIRECT_PAIRING_TYPE_PUBLIC_KEY)
1560        .ok_or_else(|| {
1561            CoreError::Protocol("remote pairing verify start missing device public key".into())
1562        })?
1563        .as_ref()
1564        .try_into()
1565        .map_err(|_| {
1566            CoreError::Protocol("remote pairing device public key must be 32 bytes".into())
1567        })?;
1568
1569    let verify_session = build_verify_step2_tlv(
1570        our_secret,
1571        &our_public,
1572        &device_public,
1573        &loaded.host_identity,
1574    )
1575    .map_err(|e| CoreError::Other(format!("remote pairing verify finish build failed: {e}")))?;
1576
1577    control
1578        .send(&build_remote_pairing_pairing_event(
1579            &verify_session.tlv,
1580            "verifyManualPairing",
1581            false,
1582            None,
1583            sequence_number,
1584        ))
1585        .await?;
1586    sequence_number += 1;
1587
1588    let verify_finish = control.recv().await?;
1589    let verify_finish_tlv = extract_remote_pairing_tlv(&verify_finish)?;
1590    let verify_finish_fields = TlvBuffer::decode(&verify_finish_tlv);
1591    if let Some(error) = verify_finish_fields.get(&DIRECT_PAIRING_TYPE_ERROR) {
1592        control
1593            .send(&build_remote_pairing_pair_verify_failed_event(
1594                sequence_number,
1595            ))
1596            .await?;
1597        return Err(CoreError::Protocol(format!(
1598            "remote pairing verify finish rejected: {error:?}"
1599        )));
1600    }
1601
1602    let listener_port =
1603        create_remote_pairing_tcp_listener(&mut control, &verify_session, sequence_number).await?;
1604    let stream =
1605        crate::psk_tls::connect_psk_tls(host, listener_port, &verify_session.encryption_key)
1606            .await
1607            .map_err(|e| {
1608                CoreError::Other(format!(
1609                    "remote pairing TLS-PSK listener connect failed: {e}"
1610                ))
1611            })?;
1612
1613    Ok(GuardedTunnelStream {
1614        stream,
1615        _guard: control,
1616    })
1617}
1618
1619#[cfg(feature = "tunnel")]
1620async fn send_pair_verify_failed(
1621    client: &mut XpcClient,
1622    sequence_number: u64,
1623) -> Result<(), CoreError> {
1624    client
1625        .send(build_direct_pair_verify_failed_event(sequence_number))
1626        .await
1627        .map_err(CoreError::from)
1628}
1629
1630#[cfg(feature = "tunnel")]
1631fn load_remote_pairing_credentials(
1632    remote_identifier: &str,
1633) -> Result<LoadedRemotePairingCredentials, CoreError> {
1634    load_remote_pairing_credentials_from_dirs(
1635        remote_identifier,
1636        &PersistedCredentials::default_dir(),
1637        &PersistedCredentials::pymobiledevice3_dir(),
1638        &current_hostname(),
1639    )
1640}
1641
1642#[cfg(feature = "tunnel")]
1643fn load_remote_pairing_credentials_from_dirs(
1644    remote_identifier: &str,
1645    ios_rs_dir: &Path,
1646    pymobiledevice3_dir: &Path,
1647    hostname: &str,
1648) -> Result<LoadedRemotePairingCredentials, CoreError> {
1649    if let Some(remote_pair_record) =
1650        RemotePairingRecord::load_for_identifier(ios_rs_dir, remote_identifier)
1651    {
1652        if let Some(persisted) = find_persisted_host_identity(ios_rs_dir, remote_identifier) {
1653            return load_ios_rs_remote_pairing_credentials(
1654                remote_identifier,
1655                remote_pair_record,
1656                persisted,
1657            );
1658        }
1659    }
1660
1661    if let Some(remote_pair_record) =
1662        RemotePairingRecord::load_for_identifier(pymobiledevice3_dir, remote_identifier)
1663    {
1664        return load_pymobiledevice3_remote_pairing_credentials(
1665            remote_identifier,
1666            hostname,
1667            remote_pair_record,
1668            pymobiledevice3_dir,
1669        );
1670    }
1671
1672    if RemotePairingRecord::load_for_identifier(ios_rs_dir, remote_identifier).is_some() {
1673        return Err(CoreError::Unsupported(format!(
1674            "missing persisted host identity for remote identifier {remote_identifier}"
1675        )));
1676    }
1677
1678    Err(CoreError::Unsupported(format!(
1679        "missing remote pairing record for {remote_identifier} in {} or {}",
1680        ios_rs_dir.display(),
1681        pymobiledevice3_dir.display()
1682    )))
1683}
1684
1685#[cfg(feature = "tunnel")]
1686fn find_persisted_host_identity(
1687    creds_dir: &Path,
1688    remote_identifier: &str,
1689) -> Option<PersistedCredentials> {
1690    PersistedCredentials::list(creds_dir)
1691        .into_iter()
1692        .find(|creds| creds.remote_identifier.as_deref() == Some(remote_identifier))
1693}
1694
1695#[cfg(feature = "tunnel")]
1696fn load_ios_rs_remote_pairing_credentials(
1697    remote_identifier: &str,
1698    remote_pair_record: RemotePairingRecord,
1699    persisted: PersistedCredentials,
1700) -> Result<LoadedRemotePairingCredentials, CoreError> {
1701    let host_private_key = remote_pair_record.private_key.clone();
1702    let host_identity =
1703        HostIdentity::from_private_key_bytes(persisted.host_identifier, &host_private_key)
1704            .map_err(|e| CoreError::Other(format!("invalid persisted host identity: {e}")))?;
1705
1706    if host_identity.public_key_bytes() != remote_pair_record.public_key {
1707        return Err(CoreError::Protocol(format!(
1708            "persisted host key mismatch for remote identifier {remote_identifier}"
1709        )));
1710    }
1711
1712    if let Some(host_private_key_hex) = persisted.host_private_key_hex {
1713        let persisted_private_key = hex::decode(host_private_key_hex)
1714            .map_err(|e| CoreError::Other(format!("invalid host private key hex: {e}")))?;
1715        if persisted_private_key != remote_pair_record.private_key {
1716            return Err(CoreError::Protocol(format!(
1717                "persisted host private key mismatch for remote identifier {remote_identifier}"
1718            )));
1719        }
1720    }
1721
1722    Ok(LoadedRemotePairingCredentials { host_identity })
1723}
1724
1725#[cfg(feature = "tunnel")]
1726fn load_pymobiledevice3_remote_pairing_credentials(
1727    remote_identifier: &str,
1728    hostname: &str,
1729    remote_pair_record: RemotePairingRecord,
1730    creds_dir: &Path,
1731) -> Result<LoadedRemotePairingCredentials, CoreError> {
1732    let host_identifier = pymobiledevice3_host_identifier(hostname);
1733    let host_identity =
1734        HostIdentity::from_private_key_bytes(host_identifier, &remote_pair_record.private_key)
1735            .map_err(|e| {
1736                CoreError::Other(format!(
1737                    "invalid pymobiledevice3 remote pairing identity for {remote_identifier}: {e}"
1738                ))
1739            })?;
1740
1741    if host_identity.public_key_bytes() != remote_pair_record.public_key {
1742        return Err(CoreError::Protocol(format!(
1743            "pymobiledevice3 host key mismatch for remote identifier {remote_identifier} in {}",
1744            creds_dir.display()
1745        )));
1746    }
1747
1748    Ok(LoadedRemotePairingCredentials { host_identity })
1749}
1750
1751#[cfg(feature = "tunnel")]
1752fn current_hostname() -> String {
1753    std::env::var_os("COMPUTERNAME")
1754        .or_else(|| std::env::var_os("HOSTNAME"))
1755        .unwrap_or_default()
1756        .to_string_lossy()
1757        .into_owned()
1758}
1759
1760#[cfg(feature = "tunnel")]
1761fn pymobiledevice3_host_identifier(hostname: &str) -> String {
1762    const NAMESPACE_DNS: [u8; 16] = [
1763        0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
1764        0xc8,
1765    ];
1766
1767    let mut input = Vec::with_capacity(NAMESPACE_DNS.len() + hostname.len());
1768    input.extend_from_slice(&NAMESPACE_DNS);
1769    input.extend_from_slice(hostname.as_bytes());
1770
1771    let mut bytes = md5::compute(&input).0.to_vec();
1772    bytes[6] = (bytes[6] & 0x0f) | 0x30;
1773    bytes[8] = (bytes[8] & 0x3f) | 0x80;
1774
1775    format!(
1776        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1777        bytes[0],
1778        bytes[1],
1779        bytes[2],
1780        bytes[3],
1781        bytes[4],
1782        bytes[5],
1783        bytes[6],
1784        bytes[7],
1785        bytes[8],
1786        bytes[9],
1787        bytes[10],
1788        bytes[11],
1789        bytes[12],
1790        bytes[13],
1791        bytes[14],
1792        bytes[15]
1793    )
1794    .to_uppercase()
1795}
1796
1797#[cfg(feature = "tunnel")]
1798fn build_direct_handshake_request(sequence_number: u64) -> XpcValue {
1799    build_direct_control_envelope(
1800        xpc_dict(&[(
1801            "plain",
1802            xpc_dict(&[(
1803                "_0",
1804                xpc_dict(&[(
1805                    "request",
1806                    xpc_dict(&[(
1807                        "_0",
1808                        xpc_dict(&[(
1809                            "handshake",
1810                            xpc_dict(&[(
1811                                "_0",
1812                                xpc_dict(&[
1813                                    (
1814                                        "hostOptions",
1815                                        xpc_dict(&[("attemptPairVerify", XpcValue::Bool(true))]),
1816                                    ),
1817                                    ("wireProtocolVersion", XpcValue::Int64(19)),
1818                                ]),
1819                            )]),
1820                        )]),
1821                    )]),
1822                )]),
1823            )]),
1824        )]),
1825        sequence_number,
1826    )
1827}
1828
1829#[cfg(feature = "tunnel")]
1830fn build_direct_pairing_event(
1831    tlv_data: &[u8],
1832    kind: &str,
1833    start_new_session: bool,
1834    sending_host: Option<&str>,
1835    sequence_number: u64,
1836) -> XpcValue {
1837    let mut pairs = vec![
1838        (
1839            "data",
1840            XpcValue::Data(bytes::Bytes::copy_from_slice(tlv_data)),
1841        ),
1842        ("kind", XpcValue::String(kind.to_string())),
1843        ("startNewSession", XpcValue::Bool(start_new_session)),
1844    ];
1845    if let Some(host) = sending_host {
1846        pairs.push(("sendingHost", XpcValue::String(host.to_string())));
1847    }
1848
1849    build_direct_control_envelope(
1850        xpc_dict(&[(
1851            "plain",
1852            xpc_dict(&[(
1853                "_0",
1854                xpc_dict(&[(
1855                    "event",
1856                    xpc_dict(&[(
1857                        "_0",
1858                        xpc_dict(&[("pairingData", xpc_dict(&[("_0", xpc_dict(&pairs))]))]),
1859                    )]),
1860                )]),
1861            )]),
1862        )]),
1863        sequence_number,
1864    )
1865}
1866
1867#[cfg(feature = "tunnel")]
1868fn build_direct_pair_verify_failed_event(sequence_number: u64) -> XpcValue {
1869    build_direct_control_envelope(
1870        xpc_dict(&[(
1871            "plain",
1872            xpc_dict(&[(
1873                "_0",
1874                xpc_dict(&[(
1875                    "event",
1876                    xpc_dict(&[("_0", xpc_dict(&[("pairVerifyFailed", xpc_dict(&[]))]))]),
1877                )]),
1878            )]),
1879        )]),
1880        sequence_number,
1881    )
1882}
1883
1884#[cfg(feature = "tunnel")]
1885fn build_direct_control_envelope(message: XpcValue, sequence_number: u64) -> XpcValue {
1886    xpc_dict(&[
1887        (
1888            "mangledTypeName",
1889            XpcValue::String(DIRECT_CONTROL_CHANNEL_ENVELOPE_TYPE.to_string()),
1890        ),
1891        (
1892            "value",
1893            xpc_dict(&[
1894                ("message", message),
1895                (
1896                    "originatedBy",
1897                    XpcValue::String(DIRECT_CONTROL_CHANNEL_ORIGIN.to_string()),
1898                ),
1899                ("sequenceNumber", XpcValue::Uint64(sequence_number)),
1900            ]),
1901        ),
1902    ])
1903}
1904
1905#[cfg(feature = "tunnel")]
1906async fn create_direct_tcp_listener(
1907    client: &mut XpcClient,
1908    session: &VerifyPairSession,
1909    sequence_number: u64,
1910) -> Result<u16, CoreError> {
1911    let nonce = make_direct_encrypted_nonce(0);
1912    let request = serde_json::json!({
1913        "request": {
1914            "_0": {
1915                "createListener": {
1916                    "key": BASE64_STANDARD.encode(session.encryption_key),
1917                    "peerConnectionsInfo": [{
1918                        "owningPID": std::process::id(),
1919                        "owningProcessName": "CoreDeviceService",
1920                    }],
1921                    "transportProtocolType": "tcp",
1922                }
1923            }
1924        }
1925    });
1926    let client_cipher = chacha20poly1305::ChaCha20Poly1305::new((&session.client_key).into());
1927    let encrypted = client_cipher
1928        .encrypt((&nonce).into(), request.to_string().as_bytes())
1929        .map_err(|e| CoreError::Other(format!("createListener encrypt failed: {e}")))?;
1930
1931    client
1932        .send(build_direct_control_envelope(
1933            xpc_dict(&[(
1934                "streamEncrypted",
1935                xpc_dict(&[("_0", XpcValue::Data(bytes::Bytes::from(encrypted)))]),
1936            )]),
1937            sequence_number,
1938        ))
1939        .await?;
1940
1941    let response = client.recv().await?;
1942    let encrypted_response = extract_direct_stream_encrypted(
1943        response
1944            .body
1945            .as_ref()
1946            .ok_or_else(|| CoreError::Protocol("createListener response missing body".into()))?,
1947    )?;
1948    let server_cipher = chacha20poly1305::ChaCha20Poly1305::new((&session.server_key).into());
1949    let plaintext = server_cipher
1950        .decrypt((&nonce).into(), encrypted_response.as_ref())
1951        .map_err(|e| CoreError::Other(format!("createListener decrypt failed: {e}")))?;
1952    let response: serde_json::Value = serde_json::from_slice(&plaintext)?;
1953    let response_body = response
1954        .get("response")
1955        .and_then(|value| value.get("_1"))
1956        .ok_or_else(|| CoreError::Protocol("createListener response missing response._1".into()))?;
1957
1958    if let Some(message) = extract_direct_error_extended_message(response_body) {
1959        return Err(CoreError::Protocol(format!(
1960            "createListener returned errorExtended: {message}"
1961        )));
1962    }
1963
1964    let port = response_body
1965        .get("createListener")
1966        .and_then(|value| value.get("port"))
1967        .and_then(serde_json::Value::as_u64)
1968        .ok_or_else(|| CoreError::Protocol("createListener response missing port".into()))?;
1969    u16::try_from(port)
1970        .ok()
1971        .filter(|port| *port != 0)
1972        .ok_or_else(|| CoreError::Protocol(format!("invalid createListener port {port}")))
1973}
1974
1975#[cfg(feature = "tunnel")]
1976async fn create_remote_pairing_tcp_listener(
1977    control: &mut RemotePairingControlChannel,
1978    session: &VerifyPairSession,
1979    sequence_number: u64,
1980) -> Result<u16, CoreError> {
1981    let nonce = make_direct_encrypted_nonce(0);
1982    let request = serde_json::json!({
1983        "request": {
1984            "_0": {
1985                "createListener": {
1986                    "key": BASE64_STANDARD.encode(session.encryption_key),
1987                    "peerConnectionsInfo": [{
1988                        "owningPID": std::process::id(),
1989                        "owningProcessName": "CoreDeviceService",
1990                    }],
1991                    "transportProtocolType": "tcp",
1992                }
1993            }
1994        }
1995    });
1996    let client_cipher = chacha20poly1305::ChaCha20Poly1305::new((&session.client_key).into());
1997    let encrypted = client_cipher
1998        .encrypt((&nonce).into(), request.to_string().as_bytes())
1999        .map_err(|e| {
2000            CoreError::Other(format!("remote pairing createListener encrypt failed: {e}"))
2001        })?;
2002
2003    control
2004        .send(&serde_json::json!({
2005            "message": {
2006                "streamEncrypted": {
2007                    "_0": BASE64_STANDARD.encode(encrypted),
2008                }
2009            },
2010            "originatedBy": DIRECT_CONTROL_CHANNEL_ORIGIN,
2011            "sequenceNumber": sequence_number,
2012        }))
2013        .await?;
2014
2015    let response = control.recv().await?;
2016    let encrypted_response = extract_remote_pairing_stream_encrypted(&response)?;
2017    let server_cipher = chacha20poly1305::ChaCha20Poly1305::new((&session.server_key).into());
2018    let plaintext = server_cipher
2019        .decrypt((&nonce).into(), encrypted_response.as_ref())
2020        .map_err(|e| {
2021            CoreError::Other(format!("remote pairing createListener decrypt failed: {e}"))
2022        })?;
2023    let response: serde_json::Value = serde_json::from_slice(&plaintext)?;
2024    let response_body = response
2025        .get("response")
2026        .and_then(|value| value.get("_1"))
2027        .ok_or_else(|| {
2028            CoreError::Protocol("remote pairing createListener response missing response._1".into())
2029        })?;
2030
2031    if let Some(message) = extract_direct_error_extended_message(response_body) {
2032        return Err(CoreError::Protocol(format!(
2033            "remote pairing createListener returned errorExtended: {message}"
2034        )));
2035    }
2036
2037    let port = response_body
2038        .get("createListener")
2039        .and_then(|value| value.get("port"))
2040        .and_then(serde_json::Value::as_u64)
2041        .ok_or_else(|| {
2042            CoreError::Protocol("remote pairing createListener response missing port".into())
2043        })?;
2044    u16::try_from(port)
2045        .ok()
2046        .filter(|port| *port != 0)
2047        .ok_or_else(|| {
2048            CoreError::Protocol(format!("invalid remote pairing createListener port {port}"))
2049        })
2050}
2051
2052#[cfg(feature = "tunnel")]
2053fn xpc_dict(pairs: &[(&str, XpcValue)]) -> XpcValue {
2054    let mut map = IndexMap::new();
2055    for (key, value) in pairs {
2056        map.insert((*key).to_string(), value.clone());
2057    }
2058    XpcValue::Dictionary(map)
2059}
2060
2061#[cfg(feature = "tunnel")]
2062fn extract_direct_remote_identifier(body: &XpcValue) -> Result<String, CoreError> {
2063    direct_plain_message(body)?
2064        .get("response")
2065        .and_then(XpcValue::as_dict)
2066        .and_then(|response| response.get("_1"))
2067        .and_then(XpcValue::as_dict)
2068        .and_then(|response| response.get("handshake"))
2069        .and_then(XpcValue::as_dict)
2070        .and_then(|handshake| handshake.get("_0"))
2071        .and_then(XpcValue::as_dict)
2072        .and_then(|handshake| handshake.get("peerDeviceInfo"))
2073        .and_then(XpcValue::as_dict)
2074        .and_then(|peer| peer.get("identifier"))
2075        .and_then(XpcValue::as_str)
2076        .map(ToOwned::to_owned)
2077        .ok_or_else(|| CoreError::Protocol("handshake missing peerDeviceInfo.identifier".into()))
2078}
2079
2080#[cfg(feature = "tunnel")]
2081fn build_remote_pairing_handshake_request(sequence_number: u64) -> serde_json::Value {
2082    serde_json::json!({
2083        "message": {
2084            "plain": {
2085                "_0": {
2086                    "request": {
2087                        "_0": {
2088                            "handshake": {
2089                                "_0": {
2090                                    "hostOptions": {
2091                                        "attemptPairVerify": true,
2092                                    },
2093                                    "wireProtocolVersion": 19,
2094                                }
2095                            }
2096                        }
2097                    }
2098                }
2099            }
2100        },
2101        "originatedBy": DIRECT_CONTROL_CHANNEL_ORIGIN,
2102        "sequenceNumber": sequence_number,
2103    })
2104}
2105
2106#[cfg(feature = "tunnel")]
2107fn build_remote_pairing_pairing_event(
2108    tlv_data: &[u8],
2109    kind: &str,
2110    start_new_session: bool,
2111    sending_host: Option<&str>,
2112    sequence_number: u64,
2113) -> serde_json::Value {
2114    let mut body = serde_json::Map::new();
2115    body.insert(
2116        "data".into(),
2117        serde_json::Value::String(BASE64_STANDARD.encode(tlv_data)),
2118    );
2119    body.insert("kind".into(), serde_json::Value::String(kind.to_string()));
2120    body.insert(
2121        "startNewSession".into(),
2122        serde_json::Value::Bool(start_new_session),
2123    );
2124    if let Some(host) = sending_host {
2125        body.insert(
2126            "sendingHost".into(),
2127            serde_json::Value::String(host.to_string()),
2128        );
2129    }
2130
2131    serde_json::json!({
2132        "message": {
2133            "plain": {
2134                "_0": {
2135                    "event": {
2136                        "_0": {
2137                            "pairingData": {
2138                                "_0": serde_json::Value::Object(body),
2139                            }
2140                        }
2141                    }
2142                }
2143            }
2144        },
2145        "originatedBy": DIRECT_CONTROL_CHANNEL_ORIGIN,
2146        "sequenceNumber": sequence_number,
2147    })
2148}
2149
2150#[cfg(feature = "tunnel")]
2151fn build_remote_pairing_pair_verify_failed_event(sequence_number: u64) -> serde_json::Value {
2152    serde_json::json!({
2153        "message": {
2154            "plain": {
2155                "_0": {
2156                    "event": {
2157                        "_0": {
2158                            "pairVerifyFailed": {}
2159                        }
2160                    }
2161                }
2162            }
2163        },
2164        "originatedBy": DIRECT_CONTROL_CHANNEL_ORIGIN,
2165        "sequenceNumber": sequence_number,
2166    })
2167}
2168
2169#[cfg(feature = "tunnel")]
2170fn extract_direct_pairing_tlv(body: &XpcValue) -> Result<Vec<u8>, CoreError> {
2171    let event = direct_plain_message(body)?
2172        .get("event")
2173        .and_then(XpcValue::as_dict)
2174        .and_then(|event| event.get("_0"))
2175        .and_then(XpcValue::as_dict)
2176        .ok_or_else(|| CoreError::Protocol("pairing response missing event._0".into()))?;
2177
2178    if let Some(message) = event
2179        .get("pairingRejectedWithError")
2180        .and_then(extract_direct_rejection_message)
2181    {
2182        return Err(CoreError::Protocol(format!("pairing rejected: {message}")));
2183    }
2184
2185    event
2186        .get("pairingData")
2187        .and_then(XpcValue::as_dict)
2188        .and_then(|pairing| pairing.get("_0"))
2189        .and_then(XpcValue::as_dict)
2190        .and_then(|pairing| pairing.get("data"))
2191        .and_then(|value| match value {
2192            XpcValue::Data(bytes) => Some(bytes.to_vec()),
2193            _ => None,
2194        })
2195        .ok_or_else(|| CoreError::Protocol("pairing response missing pairingData._0.data".into()))
2196}
2197
2198#[cfg(feature = "tunnel")]
2199fn extract_remote_pairing_tlv(body: &serde_json::Value) -> Result<Vec<u8>, CoreError> {
2200    let event = body
2201        .get("message")
2202        .and_then(|value| value.get("plain"))
2203        .and_then(|value| value.get("_0"))
2204        .and_then(|value| value.get("event"))
2205        .and_then(|value| value.get("_0"))
2206        .ok_or_else(|| {
2207            CoreError::Protocol("remote pairing response missing message.plain._0.event._0".into())
2208        })?;
2209
2210    if let Some(message) = event
2211        .get("pairingRejectedWithError")
2212        .and_then(extract_remote_pairing_rejection_message)
2213    {
2214        return Err(CoreError::Protocol(format!(
2215            "remote pairing rejected: {message}"
2216        )));
2217    }
2218
2219    let data = event
2220        .get("pairingData")
2221        .and_then(|value| value.get("_0"))
2222        .and_then(|value| value.get("data"))
2223        .and_then(serde_json::Value::as_str)
2224        .ok_or_else(|| {
2225            CoreError::Protocol("remote pairing response missing pairingData._0.data".into())
2226        })?;
2227    BASE64_STANDARD
2228        .decode(data)
2229        .map_err(|e| CoreError::Other(format!("invalid remote pairing TLV base64: {e}")))
2230}
2231
2232#[cfg(feature = "tunnel")]
2233fn extract_direct_stream_encrypted(body: &XpcValue) -> Result<Vec<u8>, CoreError> {
2234    direct_control_value(body)?
2235        .get("message")
2236        .and_then(XpcValue::as_dict)
2237        .and_then(|message| message.get("streamEncrypted"))
2238        .and_then(XpcValue::as_dict)
2239        .and_then(|encrypted| encrypted.get("_0"))
2240        .and_then(|value| match value {
2241            XpcValue::Data(bytes) => Some(bytes.to_vec()),
2242            _ => None,
2243        })
2244        .ok_or_else(|| {
2245            CoreError::Protocol("encrypted response missing message.streamEncrypted._0".into())
2246        })
2247}
2248
2249#[cfg(feature = "tunnel")]
2250fn extract_remote_pairing_stream_encrypted(body: &serde_json::Value) -> Result<Vec<u8>, CoreError> {
2251    let encoded = body
2252        .get("message")
2253        .and_then(|value| value.get("streamEncrypted"))
2254        .and_then(|value| value.get("_0"))
2255        .and_then(serde_json::Value::as_str)
2256        .ok_or_else(|| {
2257            CoreError::Protocol(
2258                "remote pairing encrypted response missing message.streamEncrypted._0".into(),
2259            )
2260        })?;
2261    BASE64_STANDARD.decode(encoded).map_err(|e| {
2262        CoreError::Other(format!(
2263            "invalid remote pairing encrypted payload base64: {e}"
2264        ))
2265    })
2266}
2267
2268#[cfg(feature = "tunnel")]
2269fn direct_control_value(body: &XpcValue) -> Result<&IndexMap<String, XpcValue>, CoreError> {
2270    let envelope = body.as_dict().ok_or_else(|| {
2271        CoreError::Protocol("direct control message body must be a dictionary".into())
2272    })?;
2273    let mangled_type = envelope
2274        .get("mangledTypeName")
2275        .and_then(XpcValue::as_str)
2276        .ok_or_else(|| {
2277            CoreError::Protocol("direct control message missing mangledTypeName".into())
2278        })?;
2279    if mangled_type != DIRECT_CONTROL_CHANNEL_ENVELOPE_TYPE {
2280        return Err(CoreError::Protocol(format!(
2281            "unexpected direct control channel type {mangled_type}"
2282        )));
2283    }
2284    envelope
2285        .get("value")
2286        .and_then(XpcValue::as_dict)
2287        .ok_or_else(|| CoreError::Protocol("direct control message missing value".into()))
2288}
2289
2290#[cfg(feature = "tunnel")]
2291fn direct_plain_message(body: &XpcValue) -> Result<&IndexMap<String, XpcValue>, CoreError> {
2292    direct_control_value(body)?
2293        .get("message")
2294        .and_then(XpcValue::as_dict)
2295        .and_then(|message| message.get("plain"))
2296        .and_then(XpcValue::as_dict)
2297        .and_then(|plain| plain.get("_0"))
2298        .and_then(XpcValue::as_dict)
2299        .ok_or_else(|| {
2300            CoreError::Protocol("direct control message missing message.plain._0".into())
2301        })
2302}
2303
2304#[cfg(feature = "tunnel")]
2305fn extract_direct_rejection_message(value: &XpcValue) -> Option<String> {
2306    value
2307        .as_dict()
2308        .and_then(|wrapped| wrapped.get("wrappedError"))
2309        .and_then(XpcValue::as_dict)
2310        .and_then(|wrapped| wrapped.get("userInfo"))
2311        .and_then(XpcValue::as_dict)
2312        .and_then(|user_info| user_info.get("NSLocalizedDescription"))
2313        .and_then(XpcValue::as_str)
2314        .map(ToOwned::to_owned)
2315}
2316
2317#[cfg(feature = "tunnel")]
2318fn extract_remote_pairing_rejection_message(value: &serde_json::Value) -> Option<String> {
2319    value
2320        .get("wrappedError")
2321        .and_then(|wrapped| wrapped.get("userInfo"))
2322        .and_then(|user_info| user_info.get("NSLocalizedDescription"))
2323        .and_then(serde_json::Value::as_str)
2324        .map(ToOwned::to_owned)
2325}
2326
2327#[cfg(feature = "tunnel")]
2328fn extract_direct_error_extended_message(value: &serde_json::Value) -> Option<String> {
2329    value
2330        .get("errorExtended")
2331        .and_then(|value| value.get("_0"))
2332        .and_then(|value| value.get("userInfo"))
2333        .and_then(|value| value.get("NSLocalizedDescription"))
2334        .and_then(serde_json::Value::as_str)
2335        .map(ToOwned::to_owned)
2336}
2337
2338#[cfg(feature = "tunnel")]
2339fn make_direct_encrypted_nonce(sequence_number: u64) -> [u8; 12] {
2340    let mut nonce = [0u8; 12];
2341    nonce[..8].copy_from_slice(&sequence_number.to_le_bytes());
2342    nonce
2343}
2344
2345#[cfg(feature = "mdns")]
2346fn load_wifi_mac_pairings() -> Result<HashMap<String, String>, CoreError> {
2347    let mut wifi_mac_to_udid = HashMap::new();
2348    let pair_record_dir = default_pair_record_dir();
2349
2350    for entry in std::fs::read_dir(pair_record_dir)? {
2351        let entry = entry?;
2352        let path = entry.path();
2353        if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some("plist") {
2354            continue;
2355        }
2356
2357        let Some(udid) = path.file_stem().and_then(|stem| stem.to_str()) else {
2358            continue;
2359        };
2360        if udid.starts_with("remote_") {
2361            continue;
2362        }
2363
2364        let record = PairRecord::load_from_path(&path, udid)?;
2365        let Some(mac) = record.wifi_mac_address else {
2366            continue;
2367        };
2368        wifi_mac_to_udid.insert(mac.to_ascii_lowercase(), udid.to_string());
2369    }
2370
2371    Ok(wifi_mac_to_udid)
2372}
2373
2374#[cfg(feature = "mdns")]
2375fn match_paired_mobdev2_targets(
2376    services: &[BonjourService],
2377    wifi_mac_to_udid: &HashMap<String, String>,
2378) -> Vec<PairedMobdev2Device> {
2379    let mut targets = Vec::new();
2380    let mut seen = std::collections::HashSet::<(String, String)>::new();
2381
2382    for service in services {
2383        let Some(mac) = mobdev2_wifi_mac(&service.instance) else {
2384            continue;
2385        };
2386        let Some(udid) = wifi_mac_to_udid.get(&mac.to_ascii_lowercase()) else {
2387            continue;
2388        };
2389        let Some(host) = preferred_lockdown_address(&service.addresses) else {
2390            continue;
2391        };
2392
2393        let key = (udid.clone(), host.to_string());
2394        if seen.insert(key.clone()) {
2395            targets.push(PairedMobdev2Device {
2396                udid: key.0,
2397                host: key.1,
2398            });
2399        }
2400    }
2401
2402    targets
2403}
2404
2405#[cfg(feature = "mdns")]
2406fn preferred_lockdown_address(addresses: &[String]) -> Option<&str> {
2407    addresses
2408        .iter()
2409        .find(|address| address.parse::<std::net::Ipv4Addr>().is_ok())
2410        .map(String::as_str)
2411        .or_else(|| {
2412            addresses
2413                .iter()
2414                .find(|address| {
2415                    !address.contains('%') && !address.to_ascii_lowercase().starts_with("fe80:")
2416                })
2417                .map(String::as_str)
2418        })
2419        .or_else(|| addresses.first().map(String::as_str))
2420}
2421
2422/// Attempt RSD handshake; returns None on failure (e.g. iOS <17).
2423#[cfg(feature = "tunnel")]
2424async fn attempt_rsd(server_addr: &str, rsd_port: u16) -> Option<RsdHandshake> {
2425    let addr = Ipv6Addr::from_str(server_addr).ok()?;
2426    match rsd_handshake(addr, rsd_port).await {
2427        Ok(h) => {
2428            tracing::info!(
2429                "RSD: {} services discovered for {}",
2430                h.services.len(),
2431                h.udid
2432            );
2433            Some(h)
2434        }
2435        Err(e) => {
2436            tracing::debug!("RSD handshake failed (may be iOS <17): {e}");
2437            None
2438        }
2439    }
2440}
2441
2442/// Attempt RSD via go-ios-compatible userspace proxy.
2443#[cfg(feature = "tunnel")]
2444async fn attempt_rsd_via_proxy(
2445    proxy_port: u16,
2446    server_addr: &str,
2447    rsd_port: u16,
2448) -> Option<RsdHandshake> {
2449    tracing::info!(
2450        "RSD via proxy: probing [{server_addr}]:{rsd_port} through proxy port {proxy_port}"
2451    );
2452
2453    let mut framer = match open_rsd_proxy_framer(proxy_port, server_addr, rsd_port).await {
2454        Some(framer) => framer,
2455        None => return None,
2456    };
2457
2458    match tokio::time::timeout(
2459        Duration::from_secs(3),
2460        crate::xpc::rsd::queue_rsd_handshake_bootstrap_on_framer(&mut framer),
2461    )
2462    .await
2463    {
2464        Ok(Ok(())) => match tokio::time::timeout(
2465            Duration::from_secs(4),
2466            crate::xpc::rsd::handshake_on_framer(&mut framer),
2467        )
2468        .await
2469        {
2470            Ok(Ok(handshake)) => {
2471                tracing::info!(
2472                    "RSD via proxy: queued bootstrap succeeded with {} services for {}",
2473                    handshake.services.len(),
2474                    handshake.udid
2475                );
2476                return Some(handshake);
2477            }
2478            Ok(Err(e)) => {
2479                tracing::warn!(
2480                    "RSD via proxy: queued bootstrap handshake failed: {e}; trying legacy bootstrap"
2481                );
2482            }
2483            Err(_) => {
2484                tracing::warn!(
2485                    "RSD via proxy: queued bootstrap handshake timed out; trying legacy bootstrap"
2486                );
2487            }
2488        },
2489        Ok(Err(e)) => {
2490            tracing::warn!("RSD via proxy: queued bootstrap failed: {e}; trying legacy bootstrap");
2491        }
2492        Err(_) => {
2493            tracing::warn!("RSD via proxy: queued bootstrap timed out; trying legacy bootstrap");
2494        }
2495    }
2496
2497    let mut framer = match open_rsd_proxy_framer(proxy_port, server_addr, rsd_port).await {
2498        Some(framer) => framer,
2499        None => return None,
2500    };
2501
2502    match tokio::time::timeout(
2503        Duration::from_secs(3),
2504        crate::xpc::rsd::initialize_xpc_connection_on_framer(&mut framer),
2505    )
2506    .await
2507    {
2508        Ok(Ok(())) => match tokio::time::timeout(
2509            Duration::from_secs(3),
2510            crate::xpc::rsd::handshake_on_framer(&mut framer),
2511        )
2512        .await
2513        {
2514            Ok(Ok(h)) => {
2515                tracing::info!(
2516                    "RSD via proxy: legacy bootstrap succeeded with {} services for {}",
2517                    h.services.len(),
2518                    h.udid
2519                );
2520                Some(h)
2521            }
2522            Ok(Err(e)) => {
2523                tracing::warn!(
2524                    "RSD handshake via proxy after legacy bootstrap: {e}; trying passive fallback"
2525                );
2526                match tokio::time::timeout(
2527                    Duration::from_secs(2),
2528                    crate::xpc::rsd::handshake_on_framer(&mut framer),
2529                )
2530                .await
2531                {
2532                    Ok(Ok(h)) => {
2533                        tracing::info!(
2534                            "RSD via proxy (passive fallback): {} services for {}",
2535                            h.services.len(),
2536                            h.udid
2537                        );
2538                        Some(h)
2539                    }
2540                    Ok(Err(e)) => {
2541                        tracing::warn!("RSD passive fallback failed: {e}");
2542                        None
2543                    }
2544                    Err(_) => {
2545                        tracing::warn!("RSD passive fallback timed out");
2546                        None
2547                    }
2548                }
2549            }
2550            Err(_) => {
2551                tracing::warn!("RSD handshake via proxy timed out after legacy bootstrap");
2552                None
2553            }
2554        },
2555        Ok(Err(e)) => {
2556            tracing::warn!("RSD legacy bootstrap failed: {e}; trying passive fallback");
2557            match tokio::time::timeout(
2558                Duration::from_secs(2),
2559                crate::xpc::rsd::handshake_on_framer(&mut framer),
2560            )
2561            .await
2562            {
2563                Ok(Ok(h)) => {
2564                    tracing::info!(
2565                        "RSD via proxy (passive fallback): {} services for {}",
2566                        h.services.len(),
2567                        h.udid
2568                    );
2569                    Some(h)
2570                }
2571                Ok(Err(e)) => {
2572                    tracing::warn!("RSD passive fallback failed: {e}");
2573                    None
2574                }
2575                Err(_) => {
2576                    tracing::warn!("RSD passive fallback timed out");
2577                    None
2578                }
2579            }
2580        }
2581        Err(_) => {
2582            tracing::warn!("RSD legacy bootstrap timed out; trying passive fallback");
2583            match tokio::time::timeout(
2584                Duration::from_secs(2),
2585                crate::xpc::rsd::handshake_on_framer(&mut framer),
2586            )
2587            .await
2588            {
2589                Ok(Ok(h)) => {
2590                    tracing::info!(
2591                        "RSD via proxy (passive fallback): {} services for {}",
2592                        h.services.len(),
2593                        h.udid
2594                    );
2595                    Some(h)
2596                }
2597                Ok(Err(e)) => {
2598                    tracing::warn!("RSD passive fallback failed: {e}");
2599                    None
2600                }
2601                Err(_) => {
2602                    tracing::warn!("RSD passive fallback timed out");
2603                    None
2604                }
2605            }
2606        }
2607    }
2608}
2609
2610#[cfg(feature = "tunnel")]
2611async fn open_rsd_proxy_framer(
2612    proxy_port: u16,
2613    server_addr: &str,
2614    rsd_port: u16,
2615) -> Option<crate::xpc::h2_raw::H2Framer<tokio::net::TcpStream>> {
2616    use tokio::io::AsyncWriteExt;
2617    use tokio::net::TcpStream;
2618
2619    tracing::info!("RSD via proxy: connecting to 127.0.0.1:{proxy_port}");
2620    let mut proxy = match TcpStream::connect(format!("127.0.0.1:{proxy_port}")).await {
2621        Ok(stream) => {
2622            tracing::info!("RSD via proxy: connected to proxy");
2623            stream
2624        }
2625        Err(e) => {
2626            tracing::warn!("RSD proxy connect failed: {e}");
2627            return None;
2628        }
2629    };
2630
2631    let addr_bytes = match Ipv6Addr::from_str(server_addr) {
2632        Ok(addr) => addr.octets(),
2633        Err(e) => {
2634            tracing::warn!("RSD bad server addr '{server_addr}': {e}");
2635            return None;
2636        }
2637    };
2638
2639    if let Err(e) = proxy.write_all(&addr_bytes).await {
2640        tracing::warn!("RSD write addr: {e}");
2641        return None;
2642    }
2643    if let Err(e) = proxy.write_all(&(rsd_port as u32).to_le_bytes()).await {
2644        tracing::warn!("RSD write port: {e}");
2645        return None;
2646    }
2647    if let Err(e) = proxy.flush().await {
2648        tracing::warn!("RSD flush header: {e}");
2649        return None;
2650    }
2651
2652    tracing::info!(
2653        "RSD via proxy: connecting to [{server_addr}]:{rsd_port} through proxy port {proxy_port}"
2654    );
2655    tracing::info!("RSD via proxy: starting H2 framer connect");
2656    match crate::xpc::h2_raw::H2Framer::connect(proxy).await {
2657        Ok(framer) => {
2658            tracing::info!("RSD via proxy: H2 framer connected");
2659            Some(framer)
2660        }
2661        Err(e) => {
2662            tracing::warn!("RSD H2 framer: {e}");
2663            None
2664        }
2665    }
2666}
2667
2668// ── ProxyStream ───────────────────────────────────────────────────────────────
2669
2670#[cfg(feature = "tunnel")]
2671pub(crate) enum ProxyStream {
2672    Plain(ServiceStream),
2673    Tls(Box<tokio_rustls::client::TlsStream<ServiceStream>>),
2674}
2675
2676#[cfg(feature = "tunnel")]
2677impl Unpin for ProxyStream {}
2678
2679#[cfg(feature = "tunnel")]
2680impl AsyncRead for ProxyStream {
2681    fn poll_read(
2682        mut self: Pin<&mut Self>,
2683        cx: &mut Context<'_>,
2684        buf: &mut ReadBuf<'_>,
2685    ) -> Poll<std::io::Result<()>> {
2686        match &mut *self {
2687            ProxyStream::Plain(s) => Pin::new(s).poll_read(cx, buf),
2688            ProxyStream::Tls(s) => Pin::new(s).poll_read(cx, buf),
2689        }
2690    }
2691}
2692
2693#[cfg(feature = "tunnel")]
2694impl AsyncWrite for ProxyStream {
2695    fn poll_write(
2696        mut self: Pin<&mut Self>,
2697        cx: &mut Context<'_>,
2698        buf: &[u8],
2699    ) -> Poll<std::io::Result<usize>> {
2700        match &mut *self {
2701            ProxyStream::Plain(s) => Pin::new(s).poll_write(cx, buf),
2702            ProxyStream::Tls(s) => Pin::new(s).poll_write(cx, buf),
2703        }
2704    }
2705    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
2706        match &mut *self {
2707            ProxyStream::Plain(s) => Pin::new(s).poll_flush(cx),
2708            ProxyStream::Tls(s) => Pin::new(s).poll_flush(cx),
2709        }
2710    }
2711    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
2712        match &mut *self {
2713            ProxyStream::Plain(s) => Pin::new(s).poll_shutdown(cx),
2714            ProxyStream::Tls(s) => Pin::new(s).poll_shutdown(cx),
2715        }
2716    }
2717}
2718
2719fn plist_value_to_string(value: &plist::Value, field: &str) -> Result<String, CoreError> {
2720    value.as_string().map(ToOwned::to_owned).ok_or_else(|| {
2721        CoreError::Protocol(format!("{field} expected string value, got {:?}", value))
2722    })
2723}
2724
2725fn plist_value_to_string_vec(value: &plist::Value, field: &str) -> Result<Vec<String>, CoreError> {
2726    let values = value.as_array().ok_or_else(|| {
2727        CoreError::Protocol(format!(
2728            "{field} expected string array value, got {:?}",
2729            value
2730        ))
2731    })?;
2732
2733    values
2734        .iter()
2735        .map(|item| {
2736            item.as_string().map(ToOwned::to_owned).ok_or_else(|| {
2737                CoreError::Protocol(format!("{field} expected string entries, got {:?}", item))
2738            })
2739        })
2740        .collect()
2741}
2742
2743#[cfg(test)]
2744mod tests {
2745    use std::collections::HashMap;
2746    use std::path::PathBuf;
2747
2748    use tokio::io::duplex;
2749
2750    use super::*;
2751
2752    fn temp_test_dir(label: &str) -> PathBuf {
2753        let unique = std::time::SystemTime::now()
2754            .duration_since(std::time::UNIX_EPOCH)
2755            .expect("system time should be after unix epoch")
2756            .as_nanos();
2757        std::env::temp_dir().join(format!("ios_core_device_{label}_{unique}"))
2758    }
2759
2760    #[cfg(feature = "tunnel")]
2761    fn make_remote_pair_record(identity: &HostIdentity) -> RemotePairingRecord {
2762        RemotePairingRecord {
2763            public_key: identity.public_key_bytes(),
2764            private_key: identity.private_key_bytes(),
2765            remote_unlock_host_key: None,
2766        }
2767    }
2768
2769    #[test]
2770    fn try_load_pair_record_returns_none_for_missing_pair_record() {
2771        let missing_dir = temp_test_dir("missing_pair_record");
2772
2773        let loaded = try_load_pair_record("missing-udid", Some(&missing_dir));
2774
2775        assert!(loaded.is_none());
2776
2777        let _ = std::fs::remove_dir_all(missing_dir);
2778    }
2779
2780    #[test]
2781    fn require_pair_record_rejects_missing_lockdown_pair_record() {
2782        let err = require_pair_record(None, "test-udid", "remote pairing lockdown access requires")
2783            .expect_err("missing pair record should fail");
2784
2785        assert!(err
2786            .to_string()
2787            .contains("remote pairing lockdown access requires"));
2788        assert!(err.to_string().contains("test-udid"));
2789    }
2790
2791    #[test]
2792    #[cfg(feature = "tunnel")]
2793    fn load_remote_pairing_credentials_accepts_legacy_ios_rs_without_private_key_hex() {
2794        let base_dir = temp_test_dir("legacy_ios_rs");
2795        let ios_rs_dir = base_dir.join("ios-rs");
2796        let pymobiledevice3_dir = base_dir.join(".pymobiledevice3");
2797        let remote_identifier = "test-remote";
2798        let identity = HostIdentity::generate();
2799
2800        make_remote_pair_record(&identity)
2801            .save_for_identifier(&ios_rs_dir, remote_identifier)
2802            .unwrap();
2803        PersistedCredentials {
2804            remote_identifier: Some(remote_identifier.into()),
2805            host_identifier: identity.identifier.clone(),
2806            host_public_key_hex: hex::encode(identity.public_key_bytes()),
2807            host_private_key_hex: None,
2808            remote_unlock_host_key: None,
2809            device_address: "fd00::1".into(),
2810            rsd_port: 58783,
2811        }
2812        .save(&ios_rs_dir)
2813        .unwrap();
2814
2815        let loaded = load_remote_pairing_credentials_from_dirs(
2816            remote_identifier,
2817            &ios_rs_dir,
2818            &pymobiledevice3_dir,
2819            "unused-hostname",
2820        )
2821        .expect("legacy ios-rs credentials should load from remote pair record");
2822
2823        assert_eq!(loaded.host_identity.identifier, identity.identifier);
2824        assert_eq!(
2825            loaded.host_identity.public_key_bytes(),
2826            identity.public_key_bytes()
2827        );
2828
2829        let _ = std::fs::remove_dir_all(base_dir);
2830    }
2831
2832    #[test]
2833    #[cfg(feature = "tunnel")]
2834    fn load_remote_pairing_credentials_prefers_ios_rs_over_pymobiledevice3() {
2835        let base_dir = temp_test_dir("prefers_ios_rs");
2836        let ios_rs_dir = base_dir.join("ios-rs");
2837        let pymobiledevice3_dir = base_dir.join(".pymobiledevice3");
2838        let remote_identifier = "test-remote";
2839        let ios_rs_identity = HostIdentity::generate();
2840        let fallback_identity = HostIdentity::from_private_key_bytes(
2841            pymobiledevice3_host_identifier("example-host"),
2842            &[0x44; 32],
2843        )
2844        .unwrap();
2845
2846        make_remote_pair_record(&ios_rs_identity)
2847            .save_for_identifier(&ios_rs_dir, remote_identifier)
2848            .unwrap();
2849        PersistedCredentials {
2850            remote_identifier: Some(remote_identifier.into()),
2851            host_identifier: ios_rs_identity.identifier.clone(),
2852            host_public_key_hex: hex::encode(ios_rs_identity.public_key_bytes()),
2853            host_private_key_hex: Some(hex::encode(ios_rs_identity.private_key_bytes())),
2854            remote_unlock_host_key: None,
2855            device_address: "fd00::1".into(),
2856            rsd_port: 58783,
2857        }
2858        .save(&ios_rs_dir)
2859        .unwrap();
2860        make_remote_pair_record(&fallback_identity)
2861            .save_for_identifier(&pymobiledevice3_dir, remote_identifier)
2862            .unwrap();
2863
2864        let loaded = load_remote_pairing_credentials_from_dirs(
2865            remote_identifier,
2866            &ios_rs_dir,
2867            &pymobiledevice3_dir,
2868            "example-host",
2869        )
2870        .expect("ios-rs credentials should take precedence");
2871
2872        assert_eq!(loaded.host_identity.identifier, ios_rs_identity.identifier);
2873        assert_eq!(
2874            loaded.host_identity.public_key_bytes(),
2875            ios_rs_identity.public_key_bytes()
2876        );
2877
2878        let _ = std::fs::remove_dir_all(base_dir);
2879    }
2880
2881    #[test]
2882    #[cfg(feature = "tunnel")]
2883    fn load_remote_pairing_credentials_falls_back_to_pymobiledevice3_remote_record() {
2884        let base_dir = temp_test_dir("pymobiledevice3_fallback");
2885        let ios_rs_dir = base_dir.join("ios-rs");
2886        let pymobiledevice3_dir = base_dir.join(".pymobiledevice3");
2887        let remote_identifier = "test-remote";
2888        let hostname = "example-host";
2889        let expected_identity = HostIdentity::from_private_key_bytes(
2890            pymobiledevice3_host_identifier(hostname),
2891            &[0x22; 32],
2892        )
2893        .unwrap();
2894
2895        make_remote_pair_record(&expected_identity)
2896            .save_for_identifier(&pymobiledevice3_dir, remote_identifier)
2897            .unwrap();
2898
2899        let loaded = load_remote_pairing_credentials_from_dirs(
2900            remote_identifier,
2901            &ios_rs_dir,
2902            &pymobiledevice3_dir,
2903            hostname,
2904        )
2905        .expect("pymobiledevice3 remote record should be usable as fallback");
2906
2907        assert_eq!(
2908            loaded.host_identity.identifier,
2909            pymobiledevice3_host_identifier(hostname)
2910        );
2911        assert_eq!(
2912            loaded.host_identity.public_key_bytes(),
2913            expected_identity.public_key_bytes()
2914        );
2915
2916        let _ = std::fs::remove_dir_all(base_dir);
2917    }
2918
2919    #[test]
2920    #[cfg(feature = "tunnel")]
2921    fn direct_handshake_request_carries_attempt_pair_verify() {
2922        let request = build_direct_handshake_request(7);
2923        let envelope = request.as_dict().expect("envelope dict");
2924        assert_eq!(
2925            envelope.get("mangledTypeName").and_then(XpcValue::as_str),
2926            Some(DIRECT_CONTROL_CHANNEL_ENVELOPE_TYPE)
2927        );
2928
2929        let handshake = envelope
2930            .get("value")
2931            .and_then(XpcValue::as_dict)
2932            .and_then(|value| value.get("message"))
2933            .and_then(XpcValue::as_dict)
2934            .and_then(|message| message.get("plain"))
2935            .and_then(XpcValue::as_dict)
2936            .and_then(|plain| plain.get("_0"))
2937            .and_then(XpcValue::as_dict)
2938            .and_then(|plain| plain.get("request"))
2939            .and_then(XpcValue::as_dict)
2940            .and_then(|request| request.get("_0"))
2941            .and_then(XpcValue::as_dict)
2942            .and_then(|request| request.get("handshake"))
2943            .and_then(XpcValue::as_dict)
2944            .and_then(|handshake| handshake.get("_0"))
2945            .and_then(XpcValue::as_dict)
2946            .expect("handshake dict");
2947
2948        assert_eq!(
2949            handshake
2950                .get("hostOptions")
2951                .and_then(XpcValue::as_dict)
2952                .and_then(|options| options.get("attemptPairVerify")),
2953            Some(&XpcValue::Bool(true))
2954        );
2955        assert_eq!(
2956            handshake.get("wireProtocolVersion"),
2957            Some(&XpcValue::Int64(19))
2958        );
2959    }
2960
2961    #[test]
2962    #[cfg(feature = "tunnel")]
2963    fn remote_pairing_handshake_request_starts_at_plain_message_root() {
2964        let request = build_remote_pairing_handshake_request(0);
2965        assert_eq!(request["originatedBy"], "host");
2966        assert_eq!(request["sequenceNumber"], 0);
2967        assert_eq!(
2968            request["message"]["plain"]["_0"]["request"]["_0"]["handshake"]["_0"]["hostOptions"]
2969                ["attemptPairVerify"],
2970            true
2971        );
2972        assert_eq!(
2973            request["message"]["plain"]["_0"]["request"]["_0"]["handshake"]["_0"]
2974                ["wireProtocolVersion"],
2975            19
2976        );
2977    }
2978
2979    #[test]
2980    #[cfg(feature = "tunnel")]
2981    fn extract_direct_remote_identifier_reads_peer_device_info() {
2982        let body = build_direct_control_envelope(
2983            xpc_dict(&[(
2984                "plain",
2985                xpc_dict(&[(
2986                    "_0",
2987                    xpc_dict(&[(
2988                        "response",
2989                        xpc_dict(&[(
2990                            "_1",
2991                            xpc_dict(&[(
2992                                "handshake",
2993                                xpc_dict(&[(
2994                                    "_0",
2995                                    xpc_dict(&[(
2996                                        "peerDeviceInfo",
2997                                        xpc_dict(&[(
2998                                            "identifier",
2999                                            XpcValue::String("test-remote".into()),
3000                                        )]),
3001                                    )]),
3002                                )]),
3003                            )]),
3004                        )]),
3005                    )]),
3006                )]),
3007            )]),
3008            1,
3009        );
3010
3011        let identifier = extract_direct_remote_identifier(&body).expect("identifier should parse");
3012        assert_eq!(identifier, "test-remote");
3013    }
3014
3015    #[test]
3016    #[cfg(feature = "tunnel")]
3017    fn extract_direct_pairing_tlv_surfaces_rejection_message() {
3018        let body = build_direct_control_envelope(
3019            xpc_dict(&[(
3020                "plain",
3021                xpc_dict(&[(
3022                    "_0",
3023                    xpc_dict(&[(
3024                        "event",
3025                        xpc_dict(&[(
3026                            "_0",
3027                            xpc_dict(&[(
3028                                "pairingRejectedWithError",
3029                                xpc_dict(&[(
3030                                    "wrappedError",
3031                                    xpc_dict(&[(
3032                                        "userInfo",
3033                                        xpc_dict(&[(
3034                                            "NSLocalizedDescription",
3035                                            XpcValue::String("Trust denied".into()),
3036                                        )]),
3037                                    )]),
3038                                )]),
3039                            )]),
3040                        )]),
3041                    )]),
3042                )]),
3043            )]),
3044            2,
3045        );
3046
3047        let err = extract_direct_pairing_tlv(&body).expect_err("rejection should error");
3048        assert!(err.to_string().contains("Trust denied"));
3049    }
3050
3051    #[test]
3052    #[cfg(feature = "tunnel")]
3053    fn extract_remote_pairing_tlv_decodes_base64_payload() {
3054        let body = serde_json::json!({
3055            "message": {
3056                "plain": {
3057                    "_0": {
3058                        "event": {
3059                            "_0": {
3060                                "pairingData": {
3061                                    "_0": {
3062                                        "data": BASE64_STANDARD.encode([0x01, 0x02, 0x03]),
3063                                        "kind": "verifyManualPairing",
3064                                        "startNewSession": true
3065                                    }
3066                                }
3067                            }
3068                        }
3069                    }
3070                }
3071            }
3072        });
3073
3074        let tlv = extract_remote_pairing_tlv(&body).expect("payload should decode");
3075        assert_eq!(tlv, vec![0x01, 0x02, 0x03]);
3076    }
3077
3078    #[test]
3079    #[cfg(feature = "tunnel")]
3080    fn extract_remote_pairing_tlv_surfaces_rejection_message() {
3081        let body = serde_json::json!({
3082            "message": {
3083                "plain": {
3084                    "_0": {
3085                        "event": {
3086                            "_0": {
3087                                "pairingRejectedWithError": {
3088                                    "wrappedError": {
3089                                        "userInfo": {
3090                                            "NSLocalizedDescription": "Pair denied"
3091                                        }
3092                                    }
3093                                }
3094                            }
3095                        }
3096                    }
3097                }
3098            }
3099        });
3100
3101        let err = extract_remote_pairing_tlv(&body).expect_err("rejection should error");
3102        assert!(err.to_string().contains("Pair denied"));
3103    }
3104
3105    #[test]
3106    #[cfg(feature = "tunnel")]
3107    fn make_direct_encrypted_nonce_uses_little_endian_sequence() {
3108        let nonce = make_direct_encrypted_nonce(0x0102_0304_0506_0708);
3109        assert_eq!(
3110            nonce,
3111            [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0, 0, 0, 0]
3112        );
3113    }
3114
3115    #[test]
3116    fn select_mux_device_prefers_usb_when_multiple_transports_match() {
3117        let selected = select_mux_device(
3118            vec![
3119                crate::mux::MuxDevice {
3120                    device_id: 7,
3121                    serial_number: "test-udid".into(),
3122                    connection_type: "Network".into(),
3123                    product_id: 0,
3124                },
3125                crate::mux::MuxDevice {
3126                    device_id: 8,
3127                    serial_number: "test-udid".into(),
3128                    connection_type: "USB".into(),
3129                    product_id: 0,
3130                },
3131            ],
3132            "test-udid",
3133        )
3134        .expect("matching device should be selected");
3135
3136        assert_eq!(selected.device_id, 8);
3137        assert_eq!(selected.connection_type, "USB");
3138    }
3139
3140    #[test]
3141    fn select_mux_device_falls_back_to_non_usb_match() {
3142        let selected = select_mux_device(
3143            vec![crate::mux::MuxDevice {
3144                device_id: 9,
3145                serial_number: "test-udid".into(),
3146                connection_type: "Network".into(),
3147                product_id: 0,
3148            }],
3149            "test-udid",
3150        )
3151        .expect("network-only match should still be selected");
3152
3153        assert_eq!(selected.device_id, 9);
3154        assert_eq!(selected.connection_type, "Network");
3155    }
3156
3157    #[test]
3158    fn strip_ssl_selection_matches_legacy_dtx_services() {
3159        assert!(should_strip_service_ssl(
3160            "com.apple.accessibility.axAuditDaemon.remoteserver"
3161        ));
3162        assert!(should_strip_service_ssl(
3163            "com.apple.instruments.remoteserver"
3164        ));
3165        assert!(!should_strip_service_ssl(
3166            "com.apple.instruments.remoteserver.DVTSecureSocketProxy"
3167        ));
3168        assert!(!should_strip_service_ssl("com.apple.mobile.screenshotr"));
3169        assert!(!should_strip_service_ssl("com.apple.webinspector"));
3170    }
3171
3172    #[test]
3173    fn parses_string_array_values_for_international_configuration() {
3174        let value = plist::Value::Array(vec![
3175            plist::Value::String("en-US".into()),
3176            plist::Value::String("zh-Hans".into()),
3177        ]);
3178
3179        let parsed = plist_value_to_string_vec(&value, "SupportedLanguages")
3180            .expect("string array should parse");
3181
3182        assert_eq!(parsed, vec!["en-US".to_string(), "zh-Hans".to_string()]);
3183    }
3184
3185    #[test]
3186    fn rejects_non_string_entries_in_international_configuration_arrays() {
3187        let value = plist::Value::Array(vec![plist::Value::Integer(1i64.into())]);
3188
3189        let err = plist_value_to_string_vec(&value, "SupportedLocales")
3190            .expect_err("non-string entry should fail");
3191
3192        let rendered = err.to_string();
3193        assert!(rendered.contains("SupportedLocales"));
3194        assert!(rendered.contains("string"));
3195    }
3196
3197    #[test]
3198    fn resolve_rsd_service_reports_actual_shim_match() {
3199        let rsd = RsdHandshake {
3200            udid: "test-udid".into(),
3201            services: HashMap::from([(
3202                "com.apple.mobile.notification_proxy.shim.remote".into(),
3203                ServiceDescriptor { port: 1234 },
3204            )]),
3205        };
3206
3207        let resolved = resolve_rsd_service(&rsd, "com.apple.mobile.notification_proxy")
3208            .expect("shim fallback should resolve");
3209
3210        assert_eq!(
3211            resolved,
3212            (
3213                "com.apple.mobile.notification_proxy.shim.remote".into(),
3214                1234
3215            )
3216        );
3217    }
3218
3219    #[test]
3220    #[cfg(feature = "tunnel")]
3221    fn resolve_tunnel_connection_target_uses_userspace_proxy_when_available() {
3222        let target =
3223            resolve_tunnel_connection_target("fd00::1", Some(60105)).expect("valid proxy target");
3224
3225        assert_eq!(
3226            target,
3227            TunnelConnectionTarget::UserspaceProxy {
3228                proxy_port: 60105,
3229                remote_addr: Ipv6Addr::from_str("fd00::1").expect("valid IPv6"),
3230            }
3231        );
3232    }
3233
3234    #[test]
3235    #[cfg(feature = "tunnel")]
3236    fn resolve_tunnel_connection_target_falls_back_to_direct_ipv6() {
3237        let target =
3238            resolve_tunnel_connection_target("fd00::2", None).expect("valid direct target");
3239
3240        assert_eq!(
3241            target,
3242            TunnelConnectionTarget::DirectIpv6 {
3243                remote_addr: Ipv6Addr::from_str("fd00::2").expect("valid IPv6"),
3244            }
3245        );
3246    }
3247
3248    #[test]
3249    #[cfg(feature = "tunnel")]
3250    fn resolve_tunnel_connection_target_rejects_invalid_ipv6() {
3251        let err = resolve_tunnel_connection_target("not-an-ipv6", Some(60105))
3252            .expect_err("invalid IPv6 should fail");
3253
3254        assert!(err.to_string().contains("invalid IPv6 addr"));
3255    }
3256
3257    #[test]
3258    #[cfg(feature = "mdns")]
3259    fn preferred_lockdown_address_prefers_ipv4() {
3260        let addresses = vec![
3261            "fe80::1%Ethernet".to_string(),
3262            "192.168.31.247".to_string(),
3263            "fd00::1".to_string(),
3264        ];
3265
3266        assert_eq!(
3267            preferred_lockdown_address(&addresses),
3268            Some("192.168.31.247")
3269        );
3270    }
3271
3272    #[test]
3273    #[cfg(feature = "mdns")]
3274    fn match_paired_mobdev2_targets_uses_wifi_mac_and_dedupes() {
3275        let services = vec![
3276            BonjourService {
3277                instance: "34:10:be:1b:a6:4c@fe80::1._apple-mobdev2._tcp.local.".into(),
3278                port: 32498,
3279                addresses: vec!["192.168.31.247".into()],
3280                properties: HashMap::new(),
3281            },
3282            BonjourService {
3283                instance: "34:10:be:1b:a6:4c@fe80::1._apple-mobdev2._tcp.local.".into(),
3284                port: 32498,
3285                addresses: vec!["192.168.31.247".into()],
3286                properties: HashMap::new(),
3287            },
3288        ];
3289        let wifi_mac_to_udid =
3290            HashMap::from([("34:10:be:1b:a6:4c".to_string(), "test-udid".to_string())]);
3291
3292        let targets = match_paired_mobdev2_targets(&services, &wifi_mac_to_udid);
3293
3294        assert_eq!(
3295            targets,
3296            vec![PairedMobdev2Device {
3297                udid: "test-udid".into(),
3298                host: "192.168.31.247".into(),
3299            }]
3300        );
3301    }
3302
3303    #[tokio::test]
3304    async fn rsd_checkin_sends_request_and_consumes_two_responses() {
3305        let (mut client, mut server) = duplex(4096);
3306        let task = tokio::spawn(async move { rsd_checkin(&mut client).await });
3307
3308        let request: plist::Value = recv_lockdown(&mut server).await.expect("request frame");
3309        let dict = request
3310            .into_dictionary()
3311            .expect("RSDCheckin request should be a plist dictionary");
3312        assert_eq!(
3313            dict.get("Request").and_then(plist::Value::as_string),
3314            Some("RSDCheckin")
3315        );
3316        assert_eq!(
3317            dict.get("ProtocolVersion")
3318                .and_then(plist::Value::as_string),
3319            Some("2")
3320        );
3321
3322        send_lockdown(
3323            &mut server,
3324            &plist::Value::Dictionary(plist::Dictionary::from_iter([
3325                (
3326                    String::from("Request"),
3327                    plist::Value::String("RSDCheckin".into()),
3328                ),
3329                (
3330                    String::from("Status"),
3331                    plist::Value::String("Acknowledged".into()),
3332                ),
3333            ])),
3334        )
3335        .await
3336        .expect("checkin response");
3337        send_lockdown(
3338            &mut server,
3339            &plist::Value::Dictionary(plist::Dictionary::from_iter([
3340                (
3341                    String::from("Request"),
3342                    plist::Value::String("StartService".into()),
3343                ),
3344                (String::from("Service"), plist::Value::String("shim".into())),
3345            ])),
3346        )
3347        .await
3348        .expect("start service response");
3349
3350        task.await
3351            .expect("join")
3352            .expect("rsd checkin should succeed");
3353    }
3354
3355    #[tokio::test]
3356    async fn rsd_checkin_rejects_unexpected_first_response() {
3357        let (mut client, mut server) = duplex(4096);
3358        let task = tokio::spawn(async move { rsd_checkin(&mut client).await });
3359
3360        let _: plist::Value = recv_lockdown(&mut server).await.expect("request frame");
3361
3362        send_lockdown(
3363            &mut server,
3364            &plist::Value::Dictionary(plist::Dictionary::from_iter([(
3365                String::from("Request"),
3366                plist::Value::String("StartService".into()),
3367            )])),
3368        )
3369        .await
3370        .expect("unexpected first response");
3371        send_lockdown(
3372            &mut server,
3373            &plist::Value::Dictionary(plist::Dictionary::from_iter([(
3374                String::from("Request"),
3375                plist::Value::String("StartService".into()),
3376            )])),
3377        )
3378        .await
3379        .expect("second response");
3380
3381        let err = task
3382            .await
3383            .expect("join")
3384            .expect_err("rsd checkin should reject mismatched first response");
3385        let rendered = err.to_string();
3386        assert!(rendered.contains("RSD check-in response"));
3387        assert!(rendered.contains("Request=RSDCheckin"));
3388    }
3389
3390    #[tokio::test]
3391    async fn rsd_checkin_rejects_start_service_error() {
3392        let (mut client, mut server) = duplex(4096);
3393        let task = tokio::spawn(async move { rsd_checkin(&mut client).await });
3394
3395        let _: plist::Value = recv_lockdown(&mut server).await.expect("request frame");
3396
3397        send_lockdown(
3398            &mut server,
3399            &plist::Value::Dictionary(plist::Dictionary::from_iter([(
3400                String::from("Request"),
3401                plist::Value::String("RSDCheckin".into()),
3402            )])),
3403        )
3404        .await
3405        .expect("checkin response");
3406        send_lockdown(
3407            &mut server,
3408            &plist::Value::Dictionary(plist::Dictionary::from_iter([
3409                (
3410                    String::from("Request"),
3411                    plist::Value::String("StartService".into()),
3412                ),
3413                (
3414                    String::from("Error"),
3415                    plist::Value::String("ServiceProhibited".into()),
3416                ),
3417            ])),
3418        )
3419        .await
3420        .expect("start service error response");
3421
3422        let err = task
3423            .await
3424            .expect("join")
3425            .expect_err("rsd checkin should surface start service errors");
3426        let rendered = err.to_string();
3427        assert!(rendered.contains("RSD start-service response"));
3428        assert!(rendered.contains("ServiceProhibited"));
3429    }
3430}