Skip to main content

ios_core/
device.rs

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