/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! FFI glue layer between happy-eyeballs Rust crate and Firefox's C++ networking stack. mod profiler; use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_ERROR_UNEXPECTED, NS_OK}; use nsstring::{nsACString, nsCString}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::ptr; use std::time::Instant; use thin_vec::ThinVec; use xpcom::{AtomicRefcnt, RefCounted}; #[cfg(not(windows))] use libc::{AF_INET, AF_INET6}; #[cfg(windows)] use winapi::shared::ws2def::{AF_INET, AF_INET6}; #[repr(C)] pub enum IpPreference { DualStackPreferV6 = 0, DualStackPreferV4 = 1, } impl From for happy_eyeballs::IpPreference { fn from(v: IpPreference) -> Self { match v { IpPreference::DualStackPreferV6 => Self::DualStackPreferV6, IpPreference::DualStackPreferV4 => Self::DualStackPreferV4, } } } #[no_mangle] pub extern "C" fn happy_eyeballs_create( result: &mut *const HappyEyeballs, origin: *const nsACString, port: u16, alt_svc: *const ThinVec, ip_preference: IpPreference, ) -> nsresult { *result = ptr::null_mut(); let Some(origin) = (unsafe { origin.as_ref() }) else { debug_assert!(false, "unexpected null origin pointer"); return NS_ERROR_INVALID_ARG; }; let Some(alt_svc) = (unsafe { alt_svc.as_ref() }) else { debug_assert!(false, "unexpected null alt_svc pointer"); return NS_ERROR_INVALID_ARG; }; let origin_str = origin.to_utf8().to_string(); let alt_svc_vec: Vec<_> = alt_svc .iter() .map(|a| happy_eyeballs::AltSvc { host: None, port: None, http_version: a.http_version.into(), }) .collect(); let network_config = happy_eyeballs::NetworkConfig { alt_svc: alt_svc_vec, ip: ip_preference.into(), ..Default::default() }; let raw_ptr = match happy_eyeballs::HappyEyeballs::new_with_network_config( origin_str.as_str(), port, network_config, ) { Ok(he) => { let mut boxed = Box::new(HappyEyeballs { refcnt: unsafe { AtomicRefcnt::new() }, inner: he, profiler: profiler::Profiler::new(0, origin_str), }); boxed .profiler .set_flow_id(std::ptr::from_ref(&boxed.refcnt) as u64); Box::into_raw(boxed) } Err(_) => return NS_ERROR_UNEXPECTED, }; unsafe { happy_eyeballs_addref(raw_ptr as *const _) }; *result = raw_ptr; NS_OK } #[no_mangle] pub extern "C" fn happy_eyeballs_process_dns_response_a( he: *mut HappyEyeballs, id: u64, addrs: *const ThinVec, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); return NS_ERROR_INVALID_ARG; }; let Some(addrs) = (unsafe { addrs.as_ref() }) else { debug_assert!(false, "unexpected null addrs pointer"); return NS_ERROR_INVALID_ARG; }; he.process_dns_response_a(id, addrs) } #[no_mangle] pub extern "C" fn happy_eyeballs_process_dns_response_aaaa( he: *mut HappyEyeballs, id: u64, addrs: *const ThinVec, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); return NS_ERROR_INVALID_ARG; }; let Some(addrs) = (unsafe { addrs.as_ref() }) else { debug_assert!(false, "unexpected null addrs pointer"); return NS_ERROR_INVALID_ARG; }; he.process_dns_response_aaaa(id, addrs) } #[no_mangle] pub extern "C" fn happy_eyeballs_process_dns_response_https( he: *mut HappyEyeballs, id: u64, service_infos: *const ThinVec, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); return NS_ERROR_INVALID_ARG; }; let Some(service_infos) = (unsafe { service_infos.as_ref() }) else { debug_assert!(false, "unexpected null service_infos pointer"); return NS_ERROR_INVALID_ARG; }; he.process_dns_response_https(id, service_infos) } #[no_mangle] pub extern "C" fn happy_eyeballs_process_connection_result( he: *mut HappyEyeballs, id: u64, status: nsresult, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); return NS_ERROR_INVALID_ARG; }; he.process_connection_result(id, status) } #[no_mangle] pub extern "C" fn happy_eyeballs_process_output( he: *mut HappyEyeballs, ret_event: *mut Output, ech_config: *mut ThinVec, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); return NS_ERROR_INVALID_ARG; }; let Some(ret_event) = (unsafe { ret_event.as_mut() }) else { debug_assert!(false, "unexpected null ret_event pointer"); return NS_ERROR_INVALID_ARG; }; let Some(ech_config) = (unsafe { ech_config.as_mut() }) else { debug_assert!(false, "unexpected null ech_config pointer"); return NS_ERROR_INVALID_ARG; }; he.process_output(ret_event, ech_config) } #[repr(C)] pub struct HappyEyeballs { refcnt: AtomicRefcnt, inner: happy_eyeballs::HappyEyeballs, profiler: profiler::Profiler, } impl HappyEyeballs { fn process_dns_response_a(&mut self, id: u64, net_addrs: &ThinVec) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut addrs = Vec::with_capacity(net_addrs.len()); for na in net_addrs.iter() { let family = i32::from(unsafe { moz_netaddr_get_family((na as *const NetAddr).cast()) }); if family != AF_INET { debug_assert!(false, "got {} instead of AF_INET in A record", family); return NS_ERROR_UNEXPECTED; } let ip_be = unsafe { moz_netaddr_get_network_order_ip((na as *const NetAddr).cast()) }; let ipv4 = Ipv4Addr::from(u32::from_be(ip_be)); addrs.push(ipv4); } self.profiler.dns_response_a(id, &addrs); let result = happy_eyeballs::DnsResult::A(Ok(addrs)); let input = happy_eyeballs::Input::DnsResult { id, result }; self.inner.process_input(input, Instant::now()); NS_OK } fn process_dns_response_aaaa(&mut self, id: u64, net_addrs: &ThinVec) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut addrs = Vec::with_capacity(net_addrs.len()); for na in net_addrs.iter() { let family = i32::from(unsafe { moz_netaddr_get_family((na as *const NetAddr).cast()) }); if family != AF_INET6 { debug_assert!(false, "got {} instead of AF_INET6 in AAAA record", family); return NS_ERROR_UNEXPECTED; } let p = unsafe { moz_netaddr_get_ipv6((na as *const NetAddr).cast()) }; let octs: [u8; 16] = unsafe { std::slice::from_raw_parts(p, 16).try_into().unwrap() }; let ipv6 = Ipv6Addr::from(octs); addrs.push(ipv6); } self.profiler.dns_response_aaaa(id, &addrs); let result = happy_eyeballs::DnsResult::Aaaa(Ok(addrs)); let input = happy_eyeballs::Input::DnsResult { id, result }; self.inner.process_input(input, Instant::now()); NS_OK } fn process_dns_response_https( &mut self, id: u64, service_infos: &ThinVec, ) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut infos = Vec::new(); for svc_info in service_infos { let target_str = svc_info.target_name.to_utf8(); let target = if target_str.is_empty() { todo!() } else { happy_eyeballs::TargetName::from(target_str.as_ref()) }; let mut alpn_set = std::collections::HashSet::new(); for http_version in &svc_info.alpn_http_versions { alpn_set.insert((*http_version).into()); } let ech = if svc_info.ech_config.is_empty() { None } else { Some(svc_info.ech_config.to_vec()) }; let mut ipv4_vec = Vec::new(); for na in &svc_info.ipv4_hints { let family = i32::from(unsafe { moz_netaddr_get_family((na as *const NetAddr).cast()) }); debug_assert_eq!(family, AF_INET, "Expected IPv4 address in IPv4 hints"); if family != AF_INET { return NS_ERROR_UNEXPECTED; } let ip_be = unsafe { moz_netaddr_get_network_order_ip((na as *const NetAddr).cast()) }; let ipv4 = Ipv4Addr::from(u32::from_be(ip_be)); ipv4_vec.push(ipv4); } let mut ipv6_vec = Vec::new(); for na in &svc_info.ipv6_hints { let family = i32::from(unsafe { moz_netaddr_get_family((na as *const NetAddr).cast()) }); debug_assert_eq!(family, AF_INET6, "Expected IPv6 address in IPv6 hints"); if family != AF_INET6 { return NS_ERROR_UNEXPECTED; } let p = unsafe { moz_netaddr_get_ipv6((na as *const NetAddr).cast()) }; let octs: [u8; 16] = unsafe { std::slice::from_raw_parts(p, 16).try_into().unwrap() }; let ipv6 = Ipv6Addr::from(octs); ipv6_vec.push(ipv6); } let port = if svc_info.port == 0 { None } else { Some(svc_info.port) }; infos.push(happy_eyeballs::ServiceInfo { priority: svc_info.priority, target_name: target, alpn_http_versions: alpn_set, ech_config: ech, ipv4_hints: ipv4_vec, ipv6_hints: ipv6_vec, port, }); } self.profiler.dns_response_https(id, &infos); let result = happy_eyeballs::DnsResult::Https(Ok(infos)); let input = happy_eyeballs::Input::DnsResult { id, result }; self.inner.process_input(input, Instant::now()); NS_OK } fn process_connection_result(&mut self, id: u64, status: nsresult) -> nsresult { let id: happy_eyeballs::Id = id.into(); self.profiler.connection_result(id, status == NS_OK); let result = if status == NS_OK { Ok(()) } else { Err(format!("connection failed: 0x{:08x}", status.0)) }; let input = happy_eyeballs::Input::ConnectionResult { id, result }; self.inner.process_input(input, Instant::now()); NS_OK } fn process_output(&mut self, ret_event: &mut Output, ech_config: &mut ThinVec) -> nsresult { let out = self.inner.process_output(std::time::Instant::now()); ech_config.clear(); match out { Some(happy_eyeballs::Output::SendDnsQuery { id, hostname: _hostname, record_type, }) => { self.profiler.dns_query_started(id); *ret_event = Output::SendDnsQuery { id: id.into(), record_type: record_type.into(), }; } Some(happy_eyeballs::Output::Timer { duration, .. }) => { let duration_ms = match duration.as_millis() { 0 => 1, ms => ms.try_into().unwrap_or_else(|_| { debug_assert!(false, "duration > u64::MAX"); u64::MAX }), }; *ret_event = Output::Timer { duration_ms }; } Some(happy_eyeballs::Output::AttemptConnection { id, endpoint }) => { self.profiler.connection_attempt_started(id, &endpoint); if let Some(ref ech) = endpoint.ech_config { ech_config.extend_from_slice(ech); } *ret_event = Output::AttemptConnection { id: id.into(), http_version: endpoint.http_version.into(), addr: endpoint.address.ip().into(), port: endpoint.address.port(), }; } Some(happy_eyeballs::Output::CancelConnection { id }) => { *ret_event = Output::CancelConnection { id: id.into() }; } Some(happy_eyeballs::Output::Succeeded) => { *ret_event = Output::Succeeded; } Some(happy_eyeballs::Output::Failed) => { *ret_event = Output::Failed; } None => { *ret_event = Output::None; } } NS_OK } } // TODO: Expose ip and port. #[repr(C)] pub struct AltSvc { pub http_version: HttpVersion, } #[repr(C)] pub enum DnsRecordType { Https = 0, Aaaa = 1, A = 2, } #[repr(C)] #[derive(Clone, Copy)] pub enum HttpVersion { H3 = 0, H2 = 1, H1 = 2, } #[repr(C)] pub enum ConnectionAttemptHttpVersions { H3 = 0, H2OrH1 = 1, H2 = 2, H1 = 3, } impl From for happy_eyeballs::HttpVersion { fn from(v: HttpVersion) -> Self { match v { HttpVersion::H3 => Self::H3, HttpVersion::H2 => Self::H2, HttpVersion::H1 => Self::H1, } } } impl From for HttpVersion { fn from(v: happy_eyeballs::HttpVersion) -> Self { match v { happy_eyeballs::HttpVersion::H3 => Self::H3, happy_eyeballs::HttpVersion::H2 => Self::H2, happy_eyeballs::HttpVersion::H1 => Self::H1, } } } impl From for DnsRecordType { fn from(v: happy_eyeballs::DnsRecordType) -> Self { match v { happy_eyeballs::DnsRecordType::Https => Self::Https, happy_eyeballs::DnsRecordType::Aaaa => Self::Aaaa, happy_eyeballs::DnsRecordType::A => Self::A, } } } impl From for ConnectionAttemptHttpVersions { fn from(v: happy_eyeballs::ConnectionAttemptHttpVersions) -> Self { match v { happy_eyeballs::ConnectionAttemptHttpVersions::H3 => Self::H3, happy_eyeballs::ConnectionAttemptHttpVersions::H2OrH1 => Self::H2OrH1, happy_eyeballs::ConnectionAttemptHttpVersions::H2 => Self::H2, happy_eyeballs::ConnectionAttemptHttpVersions::H1 => Self::H1, } } } impl From for happy_eyeballs::ConnectionAttemptHttpVersions { fn from(v: ConnectionAttemptHttpVersions) -> Self { match v { ConnectionAttemptHttpVersions::H3 => Self::H3, ConnectionAttemptHttpVersions::H2OrH1 => Self::H2OrH1, ConnectionAttemptHttpVersions::H2 => Self::H2, ConnectionAttemptHttpVersions::H1 => Self::H1, } } } impl From for ConnectionAttemptHttpVersions { fn from(v: happy_eyeballs::HttpVersion) -> Self { match v { happy_eyeballs::HttpVersion::H3 => Self::H3, happy_eyeballs::HttpVersion::H2 => Self::H2, happy_eyeballs::HttpVersion::H1 => Self::H1, } } } #[repr(C)] pub struct ServiceInfo { pub priority: u16, pub port: u16, pub target_name: nsCString, pub alpn_http_versions: ThinVec, pub ech_config: ThinVec, pub ipv4_hints: ThinVec, pub ipv6_hints: ThinVec, } #[repr(C)] pub enum IpAddr { V4([u8; 4]), V6([u8; 16]), } impl From for IpAddr { fn from(ip: std::net::IpAddr) -> Self { match ip { std::net::IpAddr::V4(ipv4) => IpAddr::V4(ipv4.octets()), std::net::IpAddr::V6(ipv6) => IpAddr::V6(ipv6.octets()), } } } #[repr(C)] pub enum Output { SendDnsQuery { id: u64, record_type: DnsRecordType, }, Timer { duration_ms: u64, }, AttemptConnection { id: u64, http_version: ConnectionAttemptHttpVersions, addr: IpAddr, port: u16, }, CancelConnection { id: u64, }, Succeeded, Failed, None, } #[no_mangle] pub unsafe extern "C" fn happy_eyeballs_release(happy_eyeballs: *const HappyEyeballs) { let Some(happy_eyeballs) = (unsafe { happy_eyeballs.as_ref() }) else { debug_assert!(false, "unexpected null happy_eyeballs pointer"); return; }; let rc = happy_eyeballs.refcnt.dec(); if rc == 0 { drop(Box::from_raw(ptr::from_ref(happy_eyeballs).cast_mut())); } } #[no_mangle] pub unsafe extern "C" fn happy_eyeballs_addref(happy_eyeballs: *const HappyEyeballs) { let Some(happy_eyeballs) = (unsafe { happy_eyeballs.as_ref() }) else { debug_assert!(false, "unexpected null happy_eyeballs pointer"); return; }; happy_eyeballs.refcnt.inc(); } // xpcom::RefPtr support unsafe impl RefCounted for HappyEyeballs { unsafe fn addref(&self) { happy_eyeballs_addref(self); } unsafe fn release(&self) { happy_eyeballs_release(self); } } // Opaque interface to mozilla::net::NetAddr defined in DNS.h #[repr(C)] pub union NetAddr { _private: [u8; 0], } extern "C" { fn moz_netaddr_get_family(arg: *const NetAddr) -> u16; fn moz_netaddr_get_network_order_ip(arg: *const NetAddr) -> u32; fn moz_netaddr_get_ipv6(arg: *const NetAddr) -> *const u8; }