/// Tests for HTTPS/SVCB DNS record handling including ECH, port SvcParams, /// multiple ServiceInfo records, and SVC1 target name resolution. mod common; use common::*; use std::{ collections::HashSet, net::{IpAddr, Ipv4Addr, SocketAddr}, }; use happy_eyeballs::{ ConnectionAttemptHttpVersions, DnsRecordType, DnsResult, Endpoint, HttpVersion, Id, Input, Output, ServiceInfo, }; #[test] fn ech_config_propagated_to_endpoint() { let (now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), ( Some(in_dns_aaaa_negative(Id::from(1))), Some(out_resolution_delay()), ), ( Some(in_dns_a_negative(Id::from(2))), Some(out_resolution_delay()), ), ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![V6_ADDR], ipv4_hints: vec![], ech_config: Some(ECH_CONFIG.to_vec()), port: None, }])), }), Some(Output::AttemptConnection { id: Id::from(3), endpoint: Endpoint { address: SocketAddr::new(V6_ADDR.into(), PORT), protocol: ConnectionAttemptHttpVersions::H3, ech_config: Some(ECH_CONFIG.to_vec()), }, }), ), ], now, ); } #[test] fn ech_config_from_https_applies_to_aaaa() { let (now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: Some(ECH_CONFIG.to_vec()), port: None, }])), }), Some(out_resolution_delay()), ), ( Some(in_dns_aaaa_positive(Id::from(1))), Some(Output::AttemptConnection { id: Id::from(3), endpoint: Endpoint { address: SocketAddr::new(V6_ADDR.into(), PORT), protocol: ConnectionAttemptHttpVersions::H3, ech_config: Some(ECH_CONFIG.to_vec()), }, }), ), ], now, ); } #[test] fn multiple_target_names() { let (now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), // HTTPS response with a different target name ( Some(in_dns_https_positive_svc1(Id::from(0))), Some(out_send_dns_svc1(Id::from(3))), ), // Now we have queries for both "example.com" and "svc1.example.com." // Getting a positive AAAA for the main host ( Some(in_dns_aaaa_positive(Id::from(1))), Some(Output::AttemptConnection { id: Id::from(4), endpoint: Endpoint { address: SocketAddr::new(V6_ADDR_2.into(), PORT), protocol: ConnectionAttemptHttpVersions::H3, ech_config: None, }, }), ), ], now, ); } mod https_port_svcparam_overrides_port_for { use super::*; fn check(ipv4_hints: Vec) { let (now, mut he) = setup(); // constructed with PORT (443) he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), ( Some(in_dns_aaaa_negative(Id::from(1))), Some(out_resolution_delay()), ), ( Some(in_dns_a_negative(Id::from(2))), Some(out_resolution_delay()), ), // HTTPS record carries port=8443; the connection attempt must use // 8443, not the authority port 443. IPv6 is preferred. ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![V6_ADDR], ipv4_hints, ech_config: None, port: Some(CUSTOM_PORT), }])), }), Some(out_attempt_v6_h3_custom_port(Id::from(3))), ), ], now, ); } #[test] fn v6_hints() { check(vec![]); } /// HTTPS record with both IPv4 and IPv6 hints and a `port` SvcParam: both /// families use the overridden port. #[test] fn v4_and_v6_hints() { check(vec![V4_ADDR]); } } #[test] fn https_port_svcparam_applies_to_resolved_a_and_aaaa() { let (now, mut he) = setup(); // constructed with PORT (443) he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), // HTTPS record with port=8443, no hints ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: Some(CUSTOM_PORT), }])), }), Some(out_resolution_delay()), ), // Positive AAAA: connection attempt must use port 8443, not 443 ( Some(in_dns_aaaa_positive(Id::from(1))), Some(out_attempt_v6_h3_custom_port(Id::from(3))), ), ( Some(in_dns_a_positive(Id::from(2))), Some(out_connection_attempt_delay()), ), // Positive A: connection attempt must use port 8443, not 443 ( Some(in_connection_result_negative(Id::from(3))), Some(out_attempt_v4_h3_custom_port(Id::from(4))), ), ], now, ); } #[test] fn https_port_svcparam_applies_but_fallbacks_follow() { let (mut now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), // HTTPS record with port=8443, no hints ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: Some(CUSTOM_PORT), }])), }), Some(out_resolution_delay()), ), // Positive AAAA: connection attempt must use port 8443, not 443 ( Some(in_dns_aaaa_positive(Id::from(1))), Some(Output::AttemptConnection { id: Id::from(3), endpoint: Endpoint { address: SocketAddr::new(V6_ADDR.into(), CUSTOM_PORT), protocol: ConnectionAttemptHttpVersions::H3, ech_config: None, }, }), ), ( Some(in_dns_a_positive(Id::from(2))), Some(out_connection_attempt_delay()), ), ], now, ); // Connection attempts using custom port: V6:H3, V4:H3, V6:H2, V4:H2, then // fallback on port 443. he.expect_connection_attempts( &mut now, vec![ out_attempt_v4_h3_custom_port(Id::from(4)), out_attempt_v6_h2_custom_port(Id::from(5)), out_attempt_v4_h2_custom_port(Id::from(6)), out_attempt_v6_h3(Id::from(7)), out_attempt_v4_h3(Id::from(8)), out_attempt_v6_h2(Id::from(9)), out_attempt_v4_h2(Id::from(10)), ], ); } /// Two HTTPS ServiceInfo records with different priorities and `port` SvcParams. /// /// ```dns /// example.com HTTPS 1 . alpn="h2,h3" port=20007 /// example.com HTTPS 2 . alpn="h2,h3" port=20008 /// ``` /// /// Connection attempts are grouped by port in priority order, then the /// authority port as a final fallback: /// /// priority-1 bucket (port 20007): V6:H3, V4:H3, V6:H2, V4:H2 /// priority-2 bucket (port 20008): V6:H3, V4:H3, V6:H2, V4:H2 /// fallback bucket (port 443): V6:H3, V4:H3, V6:H2, V4:H2 #[test] fn https_two_service_infos_with_different_ports() { const PORT_1: u16 = 20007; const PORT_2: u16 = 20008; let (mut now, mut he) = setup(); // PORT = 443 let attempt = |id: u64, addr: IpAddr, port: u16, protocol: ConnectionAttemptHttpVersions| { Output::AttemptConnection { id: Id::from(id), endpoint: Endpoint { address: SocketAddr::new(addr, port), protocol, ech_config: None, }, } }; he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), // Two ServiceInfo records; the lower priority number wins first. ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: Some(PORT_1), }, ServiceInfo { priority: 2, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H3, HttpVersion::H2]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: Some(PORT_2), }, ])), }), Some(out_resolution_delay()), ), // AAAA arrives; move-on criteria met. First bucket is PORT_1. ( Some(in_dns_aaaa_positive(Id::from(1))), Some(attempt( 3, V6_ADDR.into(), PORT_1, ConnectionAttemptHttpVersions::H3, )), ), (None, Some(out_connection_attempt_delay())), ( Some(in_dns_a_positive(Id::from(2))), Some(out_connection_attempt_delay()), ), ], now, ); he.expect_connection_attempts( &mut now, vec![ // Priority-1 bucket (port 20007): V4:H3, V6:H2, V4:H2. attempt(4, V4_ADDR.into(), PORT_1, ConnectionAttemptHttpVersions::H3), attempt(5, V6_ADDR.into(), PORT_1, ConnectionAttemptHttpVersions::H2), attempt(6, V4_ADDR.into(), PORT_1, ConnectionAttemptHttpVersions::H2), // Priority-2 bucket (port 20008). attempt(7, V6_ADDR.into(), PORT_2, ConnectionAttemptHttpVersions::H3), attempt(8, V4_ADDR.into(), PORT_2, ConnectionAttemptHttpVersions::H3), attempt(9, V6_ADDR.into(), PORT_2, ConnectionAttemptHttpVersions::H2), attempt( 10, V4_ADDR.into(), PORT_2, ConnectionAttemptHttpVersions::H2, ), // Fallback bucket (port 443). out_attempt_v6_h3(Id::from(11)), out_attempt_v4_h3(Id::from(12)), out_attempt_v6_h2(Id::from(13)), out_attempt_v4_h2(Id::from(14)), ], ); } /// Website with HTTPS record with `noDefaultAlpn` set. /// /// See e.g. . #[test] fn no_default_alpn() { let (now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), ( Some(in_dns_https_positive(Id::from(0))), Some(out_resolution_delay()), ), ( Some(in_dns_aaaa_positive(Id::from(1))), Some(out_attempt_v6_h3(Id::from(3))), ), ( Some(in_dns_a_positive(Id::from(2))), Some(out_connection_attempt_delay()), ), ( Some(in_connection_result_negative(Id::from(3))), Some(out_attempt_v4_h3(Id::from(4))), ), ( Some(in_connection_result_negative(Id::from(4))), Some(out_attempt_v6_h2(Id::from(5))), ), ( Some(in_connection_result_negative(Id::from(5))), Some(out_attempt_v4_h2(Id::from(6))), ), ( Some(in_connection_result_negative(Id::from(6))), Some(Output::Failed), ), ], now, ); } #[test] fn https_svc1_addresses_trigger_additional_attempts() { let (mut now, mut he) = setup(); he.expect( vec![ (None, Some(out_send_dns_https(Id::from(0)))), (None, Some(out_send_dns_aaaa(Id::from(1)))), (None, Some(out_send_dns_a(Id::from(2)))), ( Some(Input::DnsResult { id: Id::from(0), result: DnsResult::Https(Ok(vec![ ServiceInfo { priority: 1, target_name: HOSTNAME.into(), alpn_protocols: HashSet::from([HttpVersion::H2, HttpVersion::H3]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: None, }, ServiceInfo { priority: 2, target_name: SVC1.into(), alpn_protocols: HashSet::from([HttpVersion::H2, HttpVersion::H3]), ipv6_hints: vec![], ipv4_hints: vec![], ech_config: None, port: None, }, ])), }), Some(Output::SendDnsQuery { id: Id::from(3), hostname: SVC1.into(), record_type: DnsRecordType::Aaaa, }), ), ( None, Some(Output::SendDnsQuery { id: Id::from(4), hostname: SVC1.into(), record_type: DnsRecordType::A, }), ), (None, Some(out_resolution_delay())), ( Some(in_dns_aaaa_positive(Id::from(1))), Some(out_attempt_v6_h3(Id::from(5))), ), ( Some(in_dns_a_positive(Id::from(2))), Some(out_connection_attempt_delay()), ), ( Some(Input::DnsResult { id: Id::from(3), result: DnsResult::Aaaa(Ok(vec![V6_ADDR_2])), }), Some(out_connection_attempt_delay()), ), ( Some(Input::DnsResult { id: Id::from(4), result: DnsResult::A(Ok(vec![V4_ADDR_2])), }), Some(out_connection_attempt_delay()), ), ], now, ); let attempt = |id: u64, addr: IpAddr, protocol: ConnectionAttemptHttpVersions| { Output::AttemptConnection { id: Id::from(id), endpoint: Endpoint { address: SocketAddr::new(addr, PORT), protocol, ech_config: None, }, } }; // Addresses respect HTTPS record priority: P1 (HOSTNAME, priority=1) endpoints // come before P2 (SVC1, priority=2) endpoints. V6_ADDR:H3 was already // attempted (id=5); the remaining 7 follow in priority order. he.expect_connection_attempts( &mut now, vec![ attempt(6, V4_ADDR.into(), ConnectionAttemptHttpVersions::H3), // priority=1 attempt(7, V6_ADDR.into(), ConnectionAttemptHttpVersions::H2), // priority=1 attempt(8, V4_ADDR.into(), ConnectionAttemptHttpVersions::H2), // priority=1 attempt(9, V6_ADDR_2.into(), ConnectionAttemptHttpVersions::H3), // priority=2 attempt(10, V4_ADDR_2.into(), ConnectionAttemptHttpVersions::H3), // priority=2 attempt(11, V6_ADDR_2.into(), ConnectionAttemptHttpVersions::H2), // priority=2 attempt(12, V4_ADDR_2.into(), ConnectionAttemptHttpVersions::H2), // priority=2 ], ); }