//! Optimistic DNS: the state machine uses an answer the resolver served from a //! stale (expired) cache entry, and revalidates it with a background query. //! //! //! mod common; use common::*; use std::time::Instant; use happy_eyeballs::{ CONNECTION_ATTEMPT_DELAY, ConnectionAttemptHttpVersions, DnsRecordType, DnsResult, FailureReason, HappyEyeballs, Id, Input, IpPreference, NetworkConfig, Output, }; /// Dual-stack (the default), with the HTTPS query answered negative so the AAAA /// and A queries (ids 1 and 2) drive the flow. Leaves the state machine having /// emitted the initial HTTPS/AAAA/A queries (ids 0, 1, 2). fn setup_dual() -> (Instant, HappyEyeballs) { let (now, mut he) = setup(); expect_initial_dns_queries(&mut he, now); he.input(in_dns_https_negative(Id::from(0)), now); he.expect(out_resolution_delay(), now); (now, he) } /// A record's first query permits a stale answer; the revalidation query /// forbids one. This is the wire contract the rest of the tests rely on. #[test] fn query_allow_stale_flags() { let (now, mut he) = setup(); // Every initial query (HTTPS, AAAA, A) permits a stale answer. for _ in 0..3 { match he.process_output(now) { Some(Output::SendDnsQuery { allow_stale, .. }) => assert!(allow_stale), other => panic!("expected SendDnsQuery, got {other:?}"), } } he.input(in_dns_https_negative(Id::from(0)), now); he.input(in_dns_a_negative(Id::from(2)), now); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); // The revalidation query forbids a stale answer. match he.process_output(now) { Some(Output::SendDnsQuery { allow_stale, record_type, .. }) => { assert!(!allow_stale); assert_eq!(record_type, DnsRecordType::Aaaa); } other => panic!("expected refresh SendDnsQuery, got {other:?}"), } } /// A stale AAAA answer is used at once to race a connection, and the state /// machine emits a background query that forbids a stale answer /// (`allow_stale: false`) to revalidate it. The A query is still in flight, /// as it would be when AAAA came straight from an expired cache entry. #[test] fn stale_answer_races_and_refreshes() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); } /// A stale A answer is revalidated the same way as a stale AAAA answer. #[test] fn stale_a_answer_races_and_refreshes() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_negative(Id::from(1)), now); he.expect(out_resolution_delay(), now); he.input(in_dns_a_stale(Id::from(2)), now); he.expect(out_attempt_v4_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::A), now, ); he.expect(out_connection_attempt_delay(), now); } /// Both address families answered stale: each is revalidated exactly once, and /// the preferred family is raced first. #[test] fn both_families_stale_each_refreshed() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.input(in_dns_a_stale(Id::from(2)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect( out_send_dns_refresh(Id::from(5), HOSTNAME, DnsRecordType::A), now, ); he.expect(out_connection_attempt_delay(), now); } /// A fresh (non-stale) answer is not revalidated: after the connection attempt /// the next output is the delay timer, not a refresh query. #[test] fn fresh_answer_is_not_refreshed() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_positive(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect(out_connection_attempt_delay(), now); } /// A stale negative answer is revalidated too, matching the draft's Optimistic /// Negative Answers. #[test] fn stale_negative_answer_is_refreshed() { let (now, mut he) = setup_dual(); he.input( Input::DnsResult { id: Id::from(1), result: DnsResult::Aaaa(Err(())), stale: true, }, now, ); // No address to race yet, but the stale negative answer is revalidated. he.expect( out_send_dns_refresh(Id::from(3), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_resolution_delay(), now); } /// A stale SVCB/HTTPS answer is revalidated too. #[test] fn stale_https_record_is_refreshed() { let (now, mut he) = setup(); expect_initial_dns_queries(&mut he, now); he.input(in_dns_https_stale_v6_hints(Id::from(0)), now); // The stale HTTPS record is revalidated. AAAA and A for the origin are still // in flight, so the machine waits out the resolution delay before using the // hint. he.expect( out_send_dns_refresh(Id::from(3), HOSTNAME, DnsRecordType::Https), now, ); he.expect(out_resolution_delay(), now); } /// A stale answer for a follow-up TargetName query (named by an HTTPS record) is /// revalidated as well. #[test] fn stale_target_name_record_is_refreshed() { let (now, mut he) = setup(); expect_initial_dns_queries(&mut he, now); // HTTPS names SVC1; the machine queries SVC1's AAAA and A. he.input(in_dns_https_positive_svc1(Id::from(0)), now); he.expect(out_send_dns(Id::from(3), SVC1, DnsRecordType::Aaaa), now); he.expect(out_send_dns(Id::from(4), SVC1, DnsRecordType::A), now); he.expect(out_resolution_delay(), now); // SVC1's AAAA comes back stale: it is raced (H3 from the record's ALPN) and // revalidated for SVC1. he.input(in_dns_aaaa_stale(Id::from(3)), now); he.expect( out_attempt( Id::from(5), V6_ADDR.into(), PORT, ConnectionAttemptHttpVersions::H3, ), now, ); he.expect( out_send_dns_refresh(Id::from(6), SVC1, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); } /// Optimistic refresh also works on a single-stack (IPv4-only) network. #[test] fn stale_answer_refreshed_single_stack() { let (now, mut he) = setup_with_config(NetworkConfig { ip: IpPreference::Ipv4Only, ..NetworkConfig::default() }); he.expect(out_send_dns_https(Id::from(0)), now); he.expect(out_send_dns_a(Id::from(1)), now); he.input(in_dns_https_negative(Id::from(0)), now); he.expect(out_resolution_delay(), now); he.input(in_dns_a_stale(Id::from(1)), now); he.expect(out_attempt_v4_h1_h2(Id::from(2)), now); he.expect( out_send_dns_refresh(Id::from(3), HOSTNAME, DnsRecordType::A), now, ); he.expect(out_connection_attempt_delay(), now); } /// When the revalidation answer confirms the same address, nothing new is /// attempted and no second refresh is emitted. #[test] fn refresh_confirming_same_address_is_quiet() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input(in_dns_aaaa_positive(Id::from(4)), now); he.expect(out_connection_attempt_delay(), now); } /// A refresh query is sent with `allow_stale: false`, so a stale response to one /// is a resolver contract violation. In debug builds it trips a debug assertion. /// In release builds the assertion compiles out and the answer is accepted as-is /// (the `Done` state still prevents a second revalidation). #[cfg(debug_assertions)] #[test] #[should_panic(expected = "stale response for refresh query")] fn stale_response_to_refresh_query_asserts() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); // The revalidation query forbade a stale answer; returning one violates the // contract. he.input(in_dns_aaaa_stale(Id::from(4)), now); } /// When the revalidation answer carries a changed address, the state machine /// adds it to the race once the connection-attempt delay elapses. #[test] fn refresh_with_changed_address_updates_candidates() { let (mut now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input( Input::DnsResult { id: Id::from(4), result: DnsResult::Aaaa(Ok(vec![V6_ADDR_2])), stale: false, }, now, ); he.expect(out_connection_attempt_delay(), now); now += CONNECTION_ATTEMPT_DELAY; he.expect( out_attempt( Id::from(5), V6_ADDR_2.into(), PORT, ConnectionAttemptHttpVersions::H2OrH1, ), now, ); } /// It is fine to finish on the stale address: a success is reported even while /// the revalidation is still in flight. #[test] fn success_finishes_despite_inflight_refresh() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input(in_connection_result_positive(Id::from(3)), now); he.expect(Output::Succeeded, now); } /// A revalidation answer that arrives after the connection already succeeded is /// handled gracefully; the machine stays succeeded. #[test] fn refresh_answer_after_success_is_ignored() { let (now, mut he) = setup_dual(); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input(in_connection_result_positive(Id::from(3)), now); he.expect(Output::Succeeded, now); // Late revalidation answer with a different address changes nothing. he.input( Input::DnsResult { id: Id::from(4), result: DnsResult::Aaaa(Ok(vec![V6_ADDR_2])), stale: false, }, now, ); he.expect(Output::Succeeded, now); } /// If the stale address fails to connect while its revalidation is still in /// flight, the state machine waits for the fresh answer rather than declaring /// failure, then races the address the fresh answer provides. /// /// The A query is answered negative up front, so the only thing keeping the /// state machine from failing after the stale connection dies is the in-flight /// revalidation. #[test] fn stale_failure_waits_for_refresh() { let (now, mut he) = setup_dual(); he.input(in_dns_a_negative(Id::from(2)), now); he.expect(out_resolution_delay(), now); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); // The stale address fails, but the refresh is still outstanding. he.input(in_connection_result_negative(Id::from(3)), now); he.expect_idle(now); // The fresh answer arrives with a usable address, which is raced. he.input( Input::DnsResult { id: Id::from(4), result: DnsResult::Aaaa(Ok(vec![V6_ADDR_2])), stale: false, }, now, ); he.expect( out_attempt( Id::from(5), V6_ADDR_2.into(), PORT, ConnectionAttemptHttpVersions::H2OrH1, ), now, ); } /// The refresh must not paper over a genuine failure: once the revalidation /// returns and it, too, cannot yield a working connection, the state machine /// finishes with a failure. #[test] fn failure_reported_after_refresh_also_fails() { let (now, mut he) = setup_dual(); he.input(in_dns_a_negative(Id::from(2)), now); he.expect(out_resolution_delay(), now); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input(in_connection_result_negative(Id::from(3)), now); he.expect_idle(now); // The refresh confirms the same address, which has already failed. No new // endpoint to try, so the state machine now reports the failure. he.input(in_dns_aaaa_positive(Id::from(4)), now); he.expect(Output::Failed(FailureReason::Connection), now); } /// A revalidation that returns a negative answer removes the candidate; with the /// stale connection already failed, the machine reports failure. #[test] fn refresh_negative_then_failure() { let (now, mut he) = setup_dual(); he.input(in_dns_a_negative(Id::from(2)), now); he.expect(out_resolution_delay(), now); he.input(in_dns_aaaa_stale(Id::from(1)), now); he.expect(out_attempt_v6_h1_h2(Id::from(3)), now); he.expect( out_send_dns_refresh(Id::from(4), HOSTNAME, DnsRecordType::Aaaa), now, ); he.expect(out_connection_attempt_delay(), now); he.input(in_connection_result_negative(Id::from(3)), now); he.expect_idle(now); // Revalidation says the address is gone. he.input( Input::DnsResult { id: Id::from(4), result: DnsResult::Aaaa(Err(())), stale: false, }, now, ); he.expect(Output::Failed(FailureReason::Connection), now); }