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