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