lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

relay.rs (27287B)


      1 //! Nostr relay identifiers and network policy.
      2 
      3 use crate::{Error, RelayEndpoint};
      4 use async_wsocket::Message;
      5 use async_wsocket::futures_util::stream::SplitSink;
      6 use async_wsocket::futures_util::{Sink, SinkExt, StreamExt};
      7 use core::fmt;
      8 use core::pin::Pin;
      9 use nostr_relay_pool::ConnectionMode;
     10 use nostr_relay_pool::transport::error::TransportError;
     11 use nostr_relay_pool::transport::websocket::{WebSocketSink, WebSocketStream, WebSocketTransport};
     12 use radroots_transport::{BoxFuture, Target, TargetNetworkPolicy, TransportId};
     13 use std::collections::BTreeMap;
     14 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
     15 use std::sync::Arc;
     16 use std::task::{Context, Poll};
     17 use std::time::Duration;
     18 use tokio::net::TcpStream;
     19 use tokio_tungstenite::tungstenite::Message as WireMessage;
     20 use tokio_tungstenite::{MaybeTlsStream, WebSocketStream as TokioWebSocketStream};
     21 use url::Url;
     22 
     23 const MAX_RESOLVED_ADDRESSES: usize = 32;
     24 pub(crate) const MAX_WIRE_MESSAGE_BYTES: usize = 512 * 1024;
     25 
     26 /// Validated canonical Nostr relay URL.
     27 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     28 pub struct RelayUrl(String);
     29 
     30 impl RelayUrl {
     31     /// Parses, canonicalizes, and applies an explicit destination policy.
     32     pub fn parse(value: impl AsRef<str>, policy: RelayUrlPolicy) -> Result<Self, Error> {
     33         let original = value.as_ref();
     34         let target = match policy {
     35             RelayUrlPolicy::PrivateNetwork => {
     36                 Target::nostr_relay_with_policy(original, TargetNetworkPolicy::PrivateDevice)
     37             }
     38             RelayUrlPolicy::Public | RelayUrlPolicy::Local => Target::nostr_relay(original),
     39         }
     40         .map_err(|_| Error::InvalidRelayUrl)?;
     41         let canonical = target.uri().as_str();
     42         let parsed = Url::parse(canonical).map_err(|_| Error::InvalidRelayUrl)?;
     43         let host = parsed.host_str().ok_or(Error::InvalidRelayUrl)?;
     44         validate_scheme(canonical, parsed.scheme(), policy)?;
     45         validate_host(canonical, host, policy)?;
     46         Ok(Self(canonical.to_owned()))
     47     }
     48 
     49     /// Converts a validated relay URL into the generic Nostr target model.
     50     pub fn to_target(&self) -> Result<Target, Error> {
     51         Target::nostr_relay(self.as_str())
     52             .or_else(|_| {
     53                 Target::nostr_relay_with_policy(self.as_str(), TargetNetworkPolicy::PrivateDevice)
     54             })
     55             .map_err(|_| Error::Target)
     56     }
     57 
     58     /// Validates and converts a generic target under the selected policy.
     59     pub fn from_target(target: &Target, policy: RelayUrlPolicy) -> Result<Self, Error> {
     60         if *target.kind() != TransportId::NOSTR {
     61             return Err(Error::UnexpectedTransport);
     62         }
     63         Self::parse(target.uri().as_str(), policy)
     64     }
     65 
     66     /// Revalidates every address returned by DNS before a connection is made.
     67     pub fn validate_resolved_addresses(
     68         &self,
     69         policy: RelayUrlPolicy,
     70         addresses: impl IntoIterator<Item = IpAddr>,
     71     ) -> Result<(), Error> {
     72         let mut resolved = false;
     73         for address in addresses {
     74             resolved = true;
     75             if !policy.accepts_address(address) {
     76                 return Err(Error::ResolvedAddressDenied);
     77             }
     78         }
     79         if !resolved {
     80             return Err(Error::EmptyResolution);
     81         }
     82         Ok(())
     83     }
     84 
     85     /// Returns the canonical relay URL.
     86     pub fn as_str(&self) -> &str {
     87         self.0.as_str()
     88     }
     89 }
     90 
     91 impl fmt::Display for RelayUrl {
     92     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     93         formatter.write_str(self.as_str())
     94     }
     95 }
     96 
     97 /// WebSocket connector that validates and pins DNS results before opening a
     98 /// socket while retaining the original host name for TLS verification.
     99 #[derive(Clone, Debug)]
    100 pub(crate) struct HardenedWebsocketTransport {
    101     policies: Arc<BTreeMap<String, RelayUrlPolicy>>,
    102     pub(crate) writers: crate::socket_write::WriterRegistry,
    103     pub(crate) ingress: crate::source_ingress::IngressRegistry,
    104 }
    105 
    106 impl HardenedWebsocketTransport {
    107     pub(crate) fn new(endpoints: &[RelayEndpoint]) -> Self {
    108         Self {
    109             ingress: crate::source_ingress::IngressRegistry::new(
    110                 endpoints
    111                     .iter()
    112                     .map(|endpoint| endpoint.url().as_str().to_owned()),
    113             ),
    114             writers: crate::socket_write::WriterRegistry::new(
    115                 endpoints
    116                     .iter()
    117                     .map(|endpoint| endpoint.url().as_str().to_owned()),
    118             ),
    119             policies: Arc::new(
    120                 endpoints
    121                     .iter()
    122                     .map(|endpoint| (endpoint.url().as_str().to_owned(), endpoint.policy()))
    123                     .collect(),
    124             ),
    125         }
    126     }
    127 }
    128 
    129 impl WebSocketTransport for HardenedWebsocketTransport {
    130     fn support_ping(&self) -> bool {
    131         true
    132     }
    133 
    134     // Direct DNS/socket/TLS behavior is verified by the network-hardening
    135     // integration suite; deterministic coverage owns the surrounding policy.
    136     #[cfg_attr(coverage_nightly, coverage(off))]
    137     fn connect<'a>(
    138         &'a self,
    139         url: &'a Url,
    140         mode: &'a ConnectionMode,
    141         timeout: Duration,
    142     ) -> BoxFuture<'a, Result<(WebSocketSink, WebSocketStream), TransportError>> {
    143         Box::pin(async move {
    144             if !matches!(mode, ConnectionMode::Direct) {
    145                 return Err(policy_error(
    146                     "proxy and Tor connection modes are not configured",
    147                 ));
    148             }
    149             let policy = self
    150                 .policies
    151                 .get(configured_policy_key(url))
    152                 .copied()
    153                 .ok_or_else(|| policy_error("relay URL is not configured"))?;
    154             let relay = RelayUrl::parse(url.as_str(), policy)
    155                 .map_err(|_| policy_error("relay URL is denied by network policy"))?;
    156             let parsed =
    157                 Url::parse(relay.as_str()).map_err(|_| policy_error("relay URL is invalid"))?;
    158             let host = parsed
    159                 .host_str()
    160                 .ok_or_else(|| policy_error("relay URL host is missing"))?;
    161             let port = parsed
    162                 .port_or_known_default()
    163                 .ok_or_else(|| policy_error("relay URL port is missing"))?;
    164 
    165             let connect = async {
    166                 let ingress = self
    167                     .ingress
    168                     .connection(relay.as_str())
    169                     .ok_or_else(|| policy_error("relay ingress unavailable"))?;
    170                 let addresses = resolve_bounded(host, port).await?;
    171                 relay
    172                     .validate_resolved_addresses(policy, addresses.iter().map(SocketAddr::ip))
    173                     .map_err(|_| policy_error("relay DNS result is denied by network policy"))?;
    174                 let tcp = connect_pinned(addresses.as_slice()).await?;
    175                 let plaintext = if parsed.scheme() == "wss" {
    176                     let mut roots = rustls::RootCertStore::empty();
    177                     roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
    178                     let tls_config = rustls::ClientConfig::builder()
    179                         .with_root_certificates(roots)
    180                         .with_no_client_auth();
    181                     let server_name = rustls::pki_types::ServerName::try_from(
    182                         host.trim_matches(['[', ']']).to_owned(),
    183                     )
    184                     .map_err(|_| policy_error("relay TLS server name is invalid"))?;
    185                     let tls = tokio_rustls::TlsConnector::from(Arc::new(tls_config))
    186                         .connect(server_name, tcp)
    187                         .await
    188                         .map_err(TransportError::backend)?;
    189                     MaybeTlsStream::Rustls(tls)
    190                 } else {
    191                     MaybeTlsStream::Plain(tcp)
    192                 };
    193                 let metered = crate::source_wire::MeteredIo::new(plaintext, ingress);
    194                 let config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
    195                     .max_message_size(Some(MAX_WIRE_MESSAGE_BYTES))
    196                     .max_frame_size(Some(MAX_WIRE_MESSAGE_BYTES));
    197                 let (stream, _) = tokio_tungstenite::client_async_with_config(
    198                     relay.as_str(),
    199                     metered,
    200                     Some(config),
    201                 )
    202                 .await
    203                 .map_err(TransportError::backend)?;
    204                 let (tx, rx) = stream.split();
    205                 let writer =
    206                     crate::socket_write::SocketWriter::new(Box::new(HardenedTransportSink(tx)));
    207                 self.writers.install(relay.as_str(), &writer)?;
    208                 let sink: WebSocketSink =
    209                     Box::new(crate::socket_write::SharedSocketSink::new(writer));
    210                 let stream: WebSocketStream = Box::pin(
    211                     rx.map(|result| result.map_err(TransportError::backend).and_then(from_wire)),
    212                 );
    213                 Ok((sink, stream))
    214             };
    215 
    216             tokio::time::timeout(timeout, connect)
    217                 .await
    218                 .map_err(|_| policy_error("relay connection deadline elapsed"))?
    219         })
    220     }
    221 }
    222 
    223 fn configured_policy_key(url: &Url) -> &str {
    224     if url.path() == "/" && url.query().is_none() && url.fragment().is_none() {
    225         url.as_str().strip_suffix('/').unwrap_or(url.as_str())
    226     } else {
    227         url.as_str()
    228     }
    229 }
    230 
    231 #[cfg_attr(coverage_nightly, coverage(off))]
    232 async fn resolve_bounded(host: &str, port: u16) -> Result<Vec<SocketAddr>, TransportError> {
    233     let mut addresses = tokio::net::lookup_host((host, port))
    234         .await
    235         .map_err(|_| policy_error("relay DNS resolution failed"))?;
    236     let mut bounded = Vec::new();
    237     for address in addresses.by_ref().take(MAX_RESOLVED_ADDRESSES + 1) {
    238         bounded.push(address);
    239     }
    240     if bounded.is_empty() {
    241         return Err(policy_error("relay DNS resolution returned no addresses"));
    242     }
    243     if bounded.len() > MAX_RESOLVED_ADDRESSES {
    244         return Err(policy_error(
    245             "relay DNS resolution exceeded its address limit",
    246         ));
    247     }
    248     Ok(bounded)
    249 }
    250 
    251 #[cfg_attr(coverage_nightly, coverage(off))]
    252 async fn connect_pinned(addresses: &[SocketAddr]) -> Result<TcpStream, TransportError> {
    253     for address in addresses {
    254         if let Ok(stream) = TcpStream::connect(address).await {
    255             return Ok(stream);
    256         }
    257     }
    258     Err(policy_error("relay connection failed"))
    259 }
    260 
    261 #[derive(Debug)]
    262 struct NetworkPolicyError(&'static str);
    263 
    264 impl fmt::Display for NetworkPolicyError {
    265     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    266         formatter.write_str(self.0)
    267     }
    268 }
    269 
    270 impl std::error::Error for NetworkPolicyError {}
    271 
    272 pub(crate) fn policy_error(message: &'static str) -> TransportError {
    273     TransportError::backend(NetworkPolicyError(message))
    274 }
    275 
    276 type MeteredSocket = TokioWebSocketStream<crate::source_wire::MeteredIo<MaybeTlsStream<TcpStream>>>;
    277 
    278 struct HardenedTransportSink(SplitSink<MeteredSocket, WireMessage>);
    279 
    280 fn from_wire(message: WireMessage) -> Result<Message, TransportError> {
    281     Ok(match message {
    282         WireMessage::Text(text) => Message::Text(text.to_string()),
    283         WireMessage::Binary(bytes) => Message::Binary(bytes.to_vec()),
    284         WireMessage::Ping(bytes) => Message::Ping(bytes.to_vec()),
    285         WireMessage::Pong(bytes) => Message::Pong(bytes.to_vec()),
    286         WireMessage::Close(frame) => Message::Close(frame.map(Into::into)),
    287         WireMessage::Frame(_) => return Err(policy_error("unexpected raw WebSocket frame")),
    288     })
    289 }
    290 
    291 impl Sink<Message> for HardenedTransportSink {
    292     type Error = TransportError;
    293 
    294     #[cfg_attr(coverage_nightly, coverage(off))]
    295     fn poll_ready(
    296         mut self: Pin<&mut Self>,
    297         context: &mut Context<'_>,
    298     ) -> Poll<Result<(), Self::Error>> {
    299         Pin::new(&mut self.0)
    300             .poll_ready_unpin(context)
    301             .map_err(TransportError::backend)
    302     }
    303 
    304     #[cfg_attr(coverage_nightly, coverage(off))]
    305     fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
    306         Pin::new(&mut self.0)
    307             .start_send_unpin(item.into())
    308             .map_err(TransportError::backend)
    309     }
    310 
    311     #[cfg_attr(coverage_nightly, coverage(off))]
    312     fn poll_flush(
    313         mut self: Pin<&mut Self>,
    314         context: &mut Context<'_>,
    315     ) -> Poll<Result<(), Self::Error>> {
    316         Pin::new(&mut self.0)
    317             .poll_flush_unpin(context)
    318             .map_err(TransportError::backend)
    319     }
    320 
    321     #[cfg_attr(coverage_nightly, coverage(off))]
    322     fn poll_close(
    323         mut self: Pin<&mut Self>,
    324         context: &mut Context<'_>,
    325     ) -> Poll<Result<(), Self::Error>> {
    326         Pin::new(&mut self.0)
    327             .poll_close_unpin(context)
    328             .map_err(TransportError::backend)
    329     }
    330 }
    331 
    332 /// Destination class authorized for relay connections.
    333 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    334 #[non_exhaustive]
    335 pub enum RelayUrlPolicy {
    336     /// TLS-only public Internet endpoints; resolved addresses must be global.
    337     Public,
    338     /// Exact loopback endpoints; plaintext WebSocket is allowed.
    339     Local,
    340     /// Exact RFC1918 IPv4 or ULA IPv6 device endpoints.
    341     PrivateNetwork,
    342 }
    343 
    344 impl RelayUrlPolicy {
    345     fn accepts_address(self, address: IpAddr) -> bool {
    346         match self {
    347             Self::Public => public_address(address),
    348             Self::Local => address.is_loopback(),
    349             Self::PrivateNetwork => trusted_network_address(address),
    350         }
    351     }
    352 }
    353 
    354 fn validate_scheme(_url: &str, scheme: &str, policy: RelayUrlPolicy) -> Result<(), Error> {
    355     if scheme == "wss"
    356         || scheme == "ws"
    357             && matches!(
    358                 policy,
    359                 RelayUrlPolicy::Local | RelayUrlPolicy::PrivateNetwork
    360             )
    361     {
    362         return Ok(());
    363     }
    364     Err(Error::RelaySchemeDenied)
    365 }
    366 
    367 fn validate_host(_url: &str, host: &str, policy: RelayUrlPolicy) -> Result<(), Error> {
    368     let address = host.trim_matches(['[', ']']).parse::<IpAddr>().ok();
    369     let accepted = match (policy, address) {
    370         (RelayUrlPolicy::Public, Some(address)) => public_address(address),
    371         (RelayUrlPolicy::Public, None) => public_hostname(host),
    372         (RelayUrlPolicy::Local, Some(address)) => address.is_loopback(),
    373         (RelayUrlPolicy::Local, None) => host.eq_ignore_ascii_case("localhost"),
    374         (RelayUrlPolicy::PrivateNetwork, Some(address)) => trusted_network_address(address),
    375         (RelayUrlPolicy::PrivateNetwork, None) => false,
    376     };
    377     if accepted {
    378         Ok(())
    379     } else {
    380         Err(Error::RelayDestinationDenied)
    381     }
    382 }
    383 
    384 fn public_hostname(host: &str) -> bool {
    385     let host = host.trim_end_matches('.').to_ascii_lowercase();
    386     host.contains('.')
    387         && host != "localhost"
    388         && !host.ends_with(".localhost")
    389         && !host.ends_with(".local")
    390         && !host.ends_with(".home.arpa")
    391 }
    392 
    393 fn public_address(address: IpAddr) -> bool {
    394     match address {
    395         IpAddr::V4(address) => public_ipv4(address),
    396         IpAddr::V6(address) => public_ipv6(address),
    397     }
    398 }
    399 
    400 fn trusted_network_address(address: IpAddr) -> bool {
    401     match address {
    402         IpAddr::V4(address) => address.is_private(),
    403         IpAddr::V6(address) => address.segments()[0] & 0xfe00 == 0xfc00,
    404     }
    405 }
    406 
    407 fn public_ipv4(address: Ipv4Addr) -> bool {
    408     let octets = address.octets();
    409     !(octets[0] == 0
    410         || address.is_loopback()
    411         || address.is_private()
    412         || address.is_link_local()
    413         || address.is_multicast()
    414         || address.is_documentation()
    415         || octets[0] == 100 && (64..=127).contains(&octets[1])
    416         || octets[0] == 192 && octets[1] == 0 && octets[2] == 0
    417         || octets[0] == 192 && octets[1] == 88 && octets[2] == 99
    418         || octets[0] == 198 && matches!(octets[1], 18 | 19)
    419         || octets[0] >= 240)
    420 }
    421 
    422 fn public_ipv6(address: Ipv6Addr) -> bool {
    423     if let Some(mapped) = address.to_ipv4_mapped() {
    424         return public_ipv4(mapped);
    425     }
    426     let segments = address.segments();
    427     (segments[0] & 0xe000) == 0x2000
    428         && !(segments[0] == 0x2001 && segments[1] <= 0x01ff)
    429         && !(segments[0] == 0x2001 && segments[1] == 0x0db8)
    430         && segments[0] != 0x2002
    431         && !(segments[0] == 0x3fff && (segments[1] & 0xf000) == 0)
    432 }
    433 
    434 #[cfg(test)]
    435 mod tests {
    436     use super::*;
    437 
    438     #[test]
    439     fn metered_socket_conversion_preserves_data_control_and_close_messages() {
    440         for message in [
    441             WireMessage::Text("text".into()),
    442             WireMessage::Binary(vec![0, 255].into()),
    443             WireMessage::Ping(vec![1, 2].into()),
    444             WireMessage::Pong(vec![3].into()),
    445             WireMessage::Close(None),
    446             WireMessage::Close(Some(tokio_tungstenite::tungstenite::protocol::CloseFrame {
    447                 code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Normal,
    448                 reason: "finished".into(),
    449             })),
    450         ] {
    451             let restored: WireMessage = from_wire(message.clone()).expect("conversion").into();
    452             assert_eq!(restored, message);
    453         }
    454     }
    455 
    456     #[tokio::test]
    457     async fn secure_relay_requires_tls_before_any_http_upgrade() {
    458         use tokio::io::{AsyncReadExt, AsyncWriteExt};
    459         let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
    460             .await
    461             .expect("listener");
    462         let endpoint = format!("wss://{}", listener.local_addr().expect("address"));
    463         let server = tokio::spawn(async move {
    464             let (mut socket, _) = listener.accept().await.expect("accept");
    465             let mut prefix = [0; 5];
    466             socket
    467                 .read_exact(&mut prefix)
    468                 .await
    469                 .expect("TLS record prefix");
    470             socket
    471                 .write_all(b"HTTP/1.1 101 Switching Protocols\r\n\r\n")
    472                 .await
    473                 .expect("plaintext response");
    474             prefix
    475         });
    476         let profile = crate::profile::test_profile(
    477             crate::RelayProfileKind::Simulator,
    478             RelayUrlPolicy::Local,
    479             [endpoint.as_str()],
    480         )
    481         .expect("local TLS profile");
    482         let transport = HardenedWebsocketTransport::new(profile.endpoints());
    483         let url = Url::parse(&endpoint).expect("URL");
    484         let result = transport
    485             .connect(&url, &ConnectionMode::Direct, Duration::from_secs(5))
    486             .await;
    487         assert!(
    488             result.is_err(),
    489             "plaintext peer must not complete a secure relay connection"
    490         );
    491         let prefix = tokio::time::timeout(Duration::from_secs(5), server)
    492             .await
    493             .expect("server deadline")
    494             .expect("server task");
    495         assert_eq!(
    496             prefix[0], 0x16,
    497             "client must send a TLS handshake, never an HTTP request"
    498         );
    499         assert_eq!(prefix[1], 0x03, "TLS record version family");
    500     }
    501 
    502     #[test]
    503     fn policies_classify_literal_and_named_destinations() {
    504         assert!(RelayUrl::parse("wss://relay.example.com", RelayUrlPolicy::Public).is_ok());
    505         assert!(RelayUrl::parse("wss://10.0.0.1", RelayUrlPolicy::Public).is_err());
    506         assert!(RelayUrl::parse("wss://10.0.0.1", RelayUrlPolicy::PrivateNetwork).is_ok());
    507         assert!(RelayUrl::parse("ws://127.0.0.1", RelayUrlPolicy::Local).is_ok());
    508         assert!(RelayUrl::parse("ws://relay.example.com", RelayUrlPolicy::Public).is_err());
    509         assert!(RelayUrl::parse("wss://localhost", RelayUrlPolicy::Local).is_ok());
    510         assert!(RelayUrl::parse("wss://localhost", RelayUrlPolicy::Public).is_err());
    511         assert!(!public_hostname("localhost."));
    512         assert!(!public_hostname("intranet"));
    513         assert!(!public_hostname("host.localhost"));
    514         assert!(RelayUrl::parse("wss://host.local", RelayUrlPolicy::Public).is_err());
    515         assert!(RelayUrl::parse("wss://host.home.arpa", RelayUrlPolicy::Public).is_err());
    516         assert!(RelayUrl::parse("wss://private.example", RelayUrlPolicy::PrivateNetwork).is_err());
    517         assert!(RelayUrl::parse("wss://localhost", RelayUrlPolicy::PrivateNetwork).is_err());
    518         assert!(RelayUrl::parse("ws://10.0.0.1", RelayUrlPolicy::PrivateNetwork).is_ok());
    519         assert!(RelayUrl::parse("ws://8.8.8.8", RelayUrlPolicy::PrivateNetwork).is_err());
    520 
    521         let relay =
    522             RelayUrl::parse("wss://relay.example.com", RelayUrlPolicy::Public).expect("relay");
    523         assert_eq!(relay.to_string(), relay.as_str());
    524         assert_eq!(
    525             RelayUrl::from_target(&relay.to_target().expect("target"), RelayUrlPolicy::Public),
    526             Ok(relay)
    527         );
    528         let local = Target::local("local:device").expect("local target");
    529         assert!(matches!(
    530             RelayUrl::from_target(&local, RelayUrlPolicy::Public),
    531             Err(Error::UnexpectedTransport)
    532         ));
    533         let profile = crate::profile::test_profile(
    534             crate::RelayProfileKind::Public,
    535             RelayUrlPolicy::Public,
    536             ["wss://relay.example.com"],
    537         )
    538         .expect("profile");
    539         assert!(HardenedWebsocketTransport::new(profile.endpoints()).support_ping());
    540         assert!(!policy_error("denied").to_string().is_empty());
    541 
    542         let root = Url::parse("wss://relay.example/").expect("root URL");
    543         let path = Url::parse("wss://relay.example/path/").expect("path URL");
    544         let query = Url::parse("wss://relay.example/?scope=write").expect("query URL");
    545         let fragment = Url::parse("wss://relay.example/#status").expect("fragment URL");
    546         assert_eq!(configured_policy_key(&root), "wss://relay.example");
    547         assert_eq!(configured_policy_key(&path), "wss://relay.example/path/");
    548         assert_eq!(
    549             configured_policy_key(&query),
    550             "wss://relay.example/?scope=write"
    551         );
    552         assert_eq!(
    553             configured_policy_key(&fragment),
    554             "wss://relay.example/#status"
    555         );
    556     }
    557 
    558     #[test]
    559     fn device_network_policy_matches_the_shared_conformance_vectors() {
    560         let document: serde_json::Value = serde_json::from_str(include_str!(
    561             "../../../contracts/conformance/vectors/transport/device_network_policy.v1.json"
    562         ))
    563         .expect("device network policy vectors");
    564         for vector in document["vectors"].as_array().expect("vectors") {
    565             let input = &vector["input"];
    566             if input["surface"] != "relay" {
    567                 continue;
    568             }
    569             let policy = match input["policy"].as_str().expect("policy") {
    570                 "public" => RelayUrlPolicy::Public,
    571                 "loopback" => RelayUrlPolicy::Local,
    572                 "private_device" => RelayUrlPolicy::PrivateNetwork,
    573                 other => panic!("unknown relay policy {other}"),
    574             };
    575             let accepted =
    576                 RelayUrl::parse(input["endpoint"].as_str().expect("endpoint"), policy).is_ok();
    577             assert_eq!(
    578                 accepted,
    579                 vector["expected"]["accepted"].as_bool().expect("accepted"),
    580                 "{}",
    581                 vector["id"].as_str().expect("id")
    582             );
    583         }
    584     }
    585 
    586     #[test]
    587     fn patched_url_parser_rejects_ascii_masking_punycode() {
    588         for denied in ["wss://xn--example-.org", "wss://example.org.xn--"] {
    589             assert!(RelayUrl::parse(denied, RelayUrlPolicy::Public).is_err());
    590         }
    591     }
    592 
    593     #[test]
    594     fn resolved_addresses_are_revalidated() {
    595         let relay = RelayUrl::parse("wss://relay.example.com", RelayUrlPolicy::Public)
    596             .expect("public relay");
    597         assert!(
    598             relay
    599                 .validate_resolved_addresses(
    600                     RelayUrlPolicy::Public,
    601                     [IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34))],
    602                 )
    603                 .is_ok()
    604         );
    605         assert!(
    606             relay
    607                 .validate_resolved_addresses(
    608                     RelayUrlPolicy::Public,
    609                     [IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))],
    610                 )
    611                 .is_err()
    612         );
    613         assert!(
    614             relay
    615                 .validate_resolved_addresses(
    616                     RelayUrlPolicy::Public,
    617                     [
    618                         IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)),
    619                         IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
    620                     ],
    621                 )
    622                 .is_err()
    623         );
    624         assert!(
    625             relay
    626                 .validate_resolved_addresses(RelayUrlPolicy::Public, [])
    627                 .is_err()
    628         );
    629     }
    630 
    631     #[test]
    632     fn address_policies_fail_closed_for_special_use_ranges() {
    633         for denied in [
    634             Ipv4Addr::new(0, 0, 0, 0),
    635             Ipv4Addr::new(0, 1, 1, 1),
    636             Ipv4Addr::new(10, 0, 0, 1),
    637             Ipv4Addr::new(100, 64, 0, 1),
    638             Ipv4Addr::new(127, 0, 0, 1),
    639             Ipv4Addr::new(169, 254, 1, 1),
    640             Ipv4Addr::new(192, 0, 2, 1),
    641             Ipv4Addr::new(198, 18, 0, 1),
    642             Ipv4Addr::new(224, 0, 0, 1),
    643             Ipv4Addr::new(240, 0, 0, 1),
    644         ] {
    645             assert!(!RelayUrlPolicy::Public.accepts_address(denied.into()));
    646         }
    647         for denied in [Ipv6Addr::UNSPECIFIED, Ipv6Addr::LOCALHOST] {
    648             assert!(!RelayUrlPolicy::Public.accepts_address(denied.into()));
    649         }
    650         assert!(RelayUrlPolicy::Local.accepts_address(Ipv4Addr::LOCALHOST.into()));
    651         assert!(!RelayUrlPolicy::Local.accepts_address(Ipv4Addr::new(10, 0, 0, 1).into()));
    652         assert!(RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv4Addr::new(10, 0, 0, 1).into()));
    653         assert!(!RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv4Addr::UNSPECIFIED.into()));
    654         assert!(!RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv4Addr::LOCALHOST.into()));
    655         assert!(!RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv4Addr::BROADCAST.into()));
    656         assert!(
    657             !RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv4Addr::new(224, 0, 0, 1).into())
    658         );
    659         assert!(
    660             RelayUrlPolicy::PrivateNetwork
    661                 .accepts_address("fd00::1".parse::<Ipv6Addr>().expect("private v6").into())
    662         );
    663         assert!(!RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv6Addr::UNSPECIFIED.into()));
    664         assert!(!RelayUrlPolicy::PrivateNetwork.accepts_address(Ipv6Addr::LOCALHOST.into()));
    665         assert!(
    666             !RelayUrlPolicy::PrivateNetwork
    667                 .accepts_address("ff02::1".parse::<Ipv6Addr>().expect("multicast").into())
    668         );
    669 
    670         for denied in [
    671             "192.0.0.1",
    672             "192.88.99.1",
    673             "198.19.0.1",
    674             "255.0.0.1",
    675             "::ffff:10.0.0.1",
    676             "2001:db8::1",
    677             "2002::1",
    678             "3fff::1",
    679             "fc00::1",
    680             "fe80::1",
    681             "ff02::1",
    682         ] {
    683             let address = denied.parse::<IpAddr>().expect("address");
    684             assert!(
    685                 !RelayUrlPolicy::Public.accepts_address(address),
    686                 "accepted {denied}"
    687             );
    688         }
    689         for allowed in ["8.8.8.8", "2606:4700:4700::1111"] {
    690             assert!(
    691                 RelayUrlPolicy::Public.accepts_address(allowed.parse::<IpAddr>().expect("address"))
    692             );
    693         }
    694     }
    695 }