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