1#![forbid(unsafe_code)]
10#![allow(
11 clippy::doc_markdown,
12 clippy::missing_errors_doc,
13 clippy::module_name_repetitions,
14 reason = "HNS, DNS, ODoH, and P2P are protocol names"
15)]
16
17use hns_resolution_policy::{PolicySnapshot, ResolutionTransport, TransportPlan};
18use std::sync::atomic::{AtomicU64, Ordering};
19use thiserror::Error;
20
21static NEXT_GATEWAY_ID: AtomicU64 = AtomicU64::new(1);
22
23pub const DEFAULT_ATTEMPT_TIMEOUT_SECONDS: u64 = 10;
25pub const DEFAULT_MAXIMUM_RESPONSE_BYTES: usize = 65_535;
27pub const DEFAULT_MAXIMUM_IDENTITY_BYTES: usize = 256;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct GatewayLimits {
33 pub attempt_timeout_seconds: u64,
35 pub maximum_response_bytes: usize,
37 pub maximum_identity_bytes: usize,
39}
40
41impl Default for GatewayLimits {
42 fn default() -> Self {
43 Self {
44 attempt_timeout_seconds: DEFAULT_ATTEMPT_TIMEOUT_SECONDS,
45 maximum_response_bytes: DEFAULT_MAXIMUM_RESPONSE_BYTES,
46 maximum_identity_bytes: DEFAULT_MAXIMUM_IDENTITY_BYTES,
47 }
48 }
49}
50
51impl GatewayLimits {
52 fn validate(self) -> Result<Self, GatewayError> {
53 if self.attempt_timeout_seconds == 0
54 || self.attempt_timeout_seconds > 300
55 || self.maximum_response_bytes == 0
56 || self.maximum_response_bytes > DEFAULT_MAXIMUM_RESPONSE_BYTES
57 || self.maximum_identity_bytes == 0
58 || self.maximum_identity_bytes > 4_096
59 {
60 return Err(GatewayError::InvalidLimits);
61 }
62 Ok(self)
63 }
64}
65
66#[repr(u8)]
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum TransportFailure {
70 Unreachable = 0,
72 Timeout = 1,
74 Unsupported = 2,
76 Truncated = 3,
78 Malformed = 4,
80 AuthenticationFailed = 5,
82 Cancelled = 6,
84}
85
86impl TransportFailure {
87 const fn permits_fallback_from(self, transport: ResolutionTransport) -> bool {
88 matches!(self, Self::Unreachable | Self::Timeout | Self::Unsupported)
89 || (matches!(self, Self::Truncated)
90 && matches!(transport, ResolutionTransport::DirectAuthoritativeUdp))
91 }
92}
93
94#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub struct GatewayIdentities {
97 pub peer: Option<String>,
99 pub proxy: Option<String>,
101 pub target: Option<String>,
103}
104
105#[derive(Clone, Debug, Eq, PartialEq)]
107pub enum AttemptOutcome {
108 Response {
110 bytes: Vec<u8>,
112 identities: GatewayIdentities,
114 },
115 Failure(TransportFailure),
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct GatewayAttempt {
122 instance_id: u64,
123 policy_generation: u64,
124 sequence: u64,
125 candidate_index: usize,
126 transport: ResolutionTransport,
127 deadline: u64,
128}
129
130impl GatewayAttempt {
131 #[must_use]
133 pub const fn policy_generation(self) -> u64 {
134 self.policy_generation
135 }
136
137 #[must_use]
139 pub const fn sequence(self) -> u64 {
140 self.sequence
141 }
142
143 #[must_use]
145 pub const fn transport(self) -> ResolutionTransport {
146 self.transport
147 }
148
149 #[must_use]
151 pub const fn deadline(self) -> u64 {
152 self.deadline
153 }
154}
155
156#[derive(Debug, Eq, PartialEq)]
158pub struct GatewaySelection {
159 policy_generation: u64,
160 transport: ResolutionTransport,
161 response: Vec<u8>,
162 identities: GatewayIdentities,
163 direct_relay_fallback: bool,
164}
165
166impl GatewaySelection {
167 #[must_use]
169 pub const fn policy_generation(&self) -> u64 {
170 self.policy_generation
171 }
172
173 #[must_use]
175 pub const fn transport(&self) -> ResolutionTransport {
176 self.transport
177 }
178
179 #[must_use]
181 pub fn response(&self) -> &[u8] {
182 &self.response
183 }
184
185 #[must_use]
187 pub const fn identities(&self) -> &GatewayIdentities {
188 &self.identities
189 }
190
191 #[must_use]
193 pub const fn direct_relay_fallback(&self) -> bool {
194 self.direct_relay_fallback
195 }
196
197 #[must_use]
199 pub fn into_parts(self) -> (u64, ResolutionTransport, Vec<u8>, GatewayIdentities, bool) {
200 (
201 self.policy_generation,
202 self.transport,
203 self.response,
204 self.identities,
205 self.direct_relay_fallback,
206 )
207 }
208}
209
210#[derive(Debug, Eq, PartialEq)]
212pub enum GatewayStep {
213 RetryAvailable,
215 Selected(GatewaySelection),
217 Unavailable,
219}
220
221#[derive(Debug)]
223pub struct Gateway {
224 instance_id: u64,
225 limits: GatewayLimits,
226 policy: PolicySnapshot,
227 plan: TransportPlan,
228 next_candidate: usize,
229 sequence: u64,
230 active: Option<GatewayAttempt>,
231 terminal: bool,
232 odoh_attempted: bool,
233}
234
235impl Gateway {
236 pub fn new(policy: PolicySnapshot, limits: GatewayLimits) -> Result<Self, GatewayError> {
238 let limits = limits.validate()?;
239 let instance_id = NEXT_GATEWAY_ID
240 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
241 current.checked_add(1)
242 })
243 .map_err(|_| GatewayError::InstanceSequenceExhausted)?;
244 Ok(Self {
245 instance_id,
246 limits,
247 policy,
248 plan: TransportPlan::for_policy(policy.config()),
249 next_candidate: 0,
250 sequence: 0,
251 active: None,
252 terminal: false,
253 odoh_attempted: false,
254 })
255 }
256
257 #[must_use]
259 pub const fn plan(&self) -> &TransportPlan {
260 &self.plan
261 }
262
263 pub fn next_attempt(
265 &mut self,
266 current_policy: PolicySnapshot,
267 now: u64,
268 ) -> Result<GatewayAttempt, GatewayError> {
269 self.ensure_current_policy(current_policy)?;
270 if self.terminal {
271 return Err(GatewayError::Terminal);
272 }
273 if self.active.is_some() {
274 return Err(GatewayError::AttemptAlreadyActive);
275 }
276 let transport = self
277 .plan
278 .as_slice()
279 .get(self.next_candidate)
280 .copied()
281 .ok_or(GatewayError::NoCandidate)?;
282 let sequence = self
283 .sequence
284 .checked_add(1)
285 .ok_or(GatewayError::SequenceExhausted)?;
286 let deadline = now
287 .checked_add(self.limits.attempt_timeout_seconds)
288 .ok_or(GatewayError::TimeOverflow)?;
289 let attempt = GatewayAttempt {
290 instance_id: self.instance_id,
291 policy_generation: self.policy.generation(),
292 sequence,
293 candidate_index: self.next_candidate,
294 transport,
295 deadline,
296 };
297 self.next_candidate += 1;
298 self.sequence = sequence;
299 self.odoh_attempted |= transport == ResolutionTransport::HandshakeP2pOdoh;
300 self.active = Some(attempt);
301 Ok(attempt)
302 }
303
304 pub fn complete(
306 &mut self,
307 current_policy: PolicySnapshot,
308 attempt: GatewayAttempt,
309 outcome: AttemptOutcome,
310 now: u64,
311 ) -> Result<GatewayStep, GatewayError> {
312 self.ensure_current_policy(current_policy)?;
313 let active = self.active.ok_or(GatewayError::NoActiveAttempt)?;
314 if active != attempt {
315 return Err(GatewayError::AttemptMismatch);
316 }
317 self.active = None;
318
319 if now > attempt.deadline {
320 return self.retry_or_unavailable(TransportFailure::Timeout, attempt.transport);
321 }
322 match outcome {
323 AttemptOutcome::Failure(failure) => {
324 self.retry_or_unavailable(failure, attempt.transport)
325 }
326 AttemptOutcome::Response { bytes, identities } => {
327 if let Err(error) = self.validate_response(&bytes, &identities, attempt.transport) {
328 self.terminal = true;
329 return Err(error);
330 }
331 self.terminal = true;
332 Ok(GatewayStep::Selected(GatewaySelection {
333 policy_generation: self.policy.generation(),
334 transport: attempt.transport,
335 response: bytes,
336 identities,
337 direct_relay_fallback: attempt.transport
338 == ResolutionTransport::HandshakeP2pDnsRelay
339 && self.odoh_attempted,
340 }))
341 }
342 }
343 }
344
345 pub fn revoke(&mut self) {
347 self.active = None;
348 self.terminal = true;
349 }
350
351 fn retry_or_unavailable(
352 &mut self,
353 failure: TransportFailure,
354 transport: ResolutionTransport,
355 ) -> Result<GatewayStep, GatewayError> {
356 if !failure.permits_fallback_from(transport) {
357 self.terminal = true;
358 return Err(GatewayError::TerminalTransportFailure(failure));
359 }
360 if self.next_candidate < self.plan.as_slice().len() {
361 Ok(GatewayStep::RetryAvailable)
362 } else {
363 self.terminal = true;
364 Ok(GatewayStep::Unavailable)
365 }
366 }
367
368 fn ensure_current_policy(&mut self, current: PolicySnapshot) -> Result<(), GatewayError> {
369 if current.generation() != self.policy.generation()
370 || current.config() != self.policy.config()
371 {
372 self.revoke();
373 return Err(GatewayError::StalePolicy);
374 }
375 Ok(())
376 }
377
378 fn validate_response(
379 &self,
380 bytes: &[u8],
381 identities: &GatewayIdentities,
382 transport: ResolutionTransport,
383 ) -> Result<(), GatewayError> {
384 if bytes.is_empty() || bytes.len() > self.limits.maximum_response_bytes {
385 return Err(GatewayError::InvalidResponseSize);
386 }
387 for identity in [
388 identities.peer.as_deref(),
389 identities.proxy.as_deref(),
390 identities.target.as_deref(),
391 ]
392 .into_iter()
393 .flatten()
394 {
395 if identity.is_empty() || identity.len() > self.limits.maximum_identity_bytes {
396 return Err(GatewayError::InvalidIdentity);
397 }
398 }
399 match transport {
400 ResolutionTransport::HandshakeP2pOdoh => {
401 let proxy = identities
402 .proxy
403 .as_deref()
404 .ok_or(GatewayError::MissingIdentity)?;
405 let target = identities
406 .target
407 .as_deref()
408 .ok_or(GatewayError::MissingIdentity)?;
409 if identities.peer.is_some() || proxy == target {
410 return Err(GatewayError::InvalidIdentityTopology);
411 }
412 }
413 ResolutionTransport::HandshakeP2pDnsRelay => {
414 if identities.peer.is_none()
415 || identities.proxy.is_some()
416 || identities.target.is_some()
417 {
418 return Err(GatewayError::InvalidIdentityTopology);
419 }
420 }
421 ResolutionTransport::DirectAuthoritativeUdp
422 | ResolutionTransport::DirectAuthoritativeTcp
423 | ResolutionTransport::AuthenticatedAuthoritativeDoh
424 | ResolutionTransport::UserConfiguredRecursiveHnsDoh => {
425 if identities != &GatewayIdentities::default() {
426 return Err(GatewayError::InvalidIdentityTopology);
427 }
428 }
429 ResolutionTransport::Unavailable
430 | ResolutionTransport::ValidatingIcannDoh
431 | ResolutionTransport::LocalHnsProof => {
432 return Err(GatewayError::UnavailableTransport);
433 }
434 }
435 Ok(())
436 }
437}
438
439#[derive(Debug, Error)]
441#[non_exhaustive]
442pub enum GatewayError {
443 #[error("invalid HNS gateway limits")]
445 InvalidLimits,
446 #[error("HNS gateway policy is stale")]
448 StalePolicy,
449 #[error("HNS gateway attempt is already active")]
451 AttemptAlreadyActive,
452 #[error("HNS gateway has no active attempt")]
454 NoActiveAttempt,
455 #[error("HNS gateway completion does not match the active attempt")]
457 AttemptMismatch,
458 #[error("HNS gateway has no remaining transport candidate")]
460 NoCandidate,
461 #[error("HNS gateway is terminal")]
463 Terminal,
464 #[error("HNS gateway attempt sequence exhausted")]
466 SequenceExhausted,
467 #[error("HNS gateway instance sequence exhausted")]
469 InstanceSequenceExhausted,
470 #[error("HNS gateway deadline overflow")]
472 TimeOverflow,
473 #[error("HNS gateway transport failed closed: {0:?}")]
475 TerminalTransportFailure(TransportFailure),
476 #[error("HNS gateway response size is invalid")]
478 InvalidResponseSize,
479 #[error("HNS gateway intermediary identity is missing")]
481 MissingIdentity,
482 #[error("HNS gateway intermediary identity is invalid")]
484 InvalidIdentity,
485 #[error("HNS gateway intermediary topology is invalid")]
487 InvalidIdentityTopology,
488 #[error("unavailable cannot carry a gateway response")]
490 UnavailableTransport,
491}
492
493#[cfg(test)]
494#[allow(
495 clippy::unwrap_used,
496 reason = "tests fail immediately on invalid deterministic gateway fixtures"
497)]
498mod tests {
499 use hns_resolution_policy::{ObliviousDnsPolicy, PolicyConfig, ProviderPolicy, WireProfile};
500
501 use super::*;
502
503 fn policy() -> PolicySnapshot {
504 PolicySnapshot::default()
505 }
506
507 fn failure_then_next(
508 gateway: &mut Gateway,
509 snapshot: PolicySnapshot,
510 now: u64,
511 ) -> GatewayAttempt {
512 let attempt = gateway.next_attempt(snapshot, now).unwrap();
513 assert_eq!(
514 gateway
515 .complete(
516 snapshot,
517 attempt,
518 AttemptOutcome::Failure(TransportFailure::Unreachable),
519 now
520 )
521 .unwrap(),
522 GatewayStep::RetryAvailable
523 );
524 attempt
525 }
526
527 #[test]
528 fn follows_direct_first_plan_and_derives_relay_downgrade() {
529 let snapshot = policy();
530 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
531 assert_eq!(
532 gateway.plan().as_slice(),
533 [
534 ResolutionTransport::DirectAuthoritativeUdp,
535 ResolutionTransport::DirectAuthoritativeTcp,
536 ResolutionTransport::AuthenticatedAuthoritativeDoh,
537 ResolutionTransport::HandshakeP2pOdoh,
538 ResolutionTransport::HandshakeP2pDnsRelay,
539 ]
540 );
541 for expected in [
542 ResolutionTransport::DirectAuthoritativeUdp,
543 ResolutionTransport::DirectAuthoritativeTcp,
544 ResolutionTransport::AuthenticatedAuthoritativeDoh,
545 ResolutionTransport::HandshakeP2pOdoh,
546 ] {
547 assert_eq!(
548 failure_then_next(&mut gateway, snapshot, 100).transport(),
549 expected
550 );
551 }
552 let relay = gateway.next_attempt(snapshot, 100).unwrap();
553 assert_eq!(relay.transport(), ResolutionTransport::HandshakeP2pDnsRelay);
554 let selected = gateway
555 .complete(
556 snapshot,
557 relay,
558 AttemptOutcome::Response {
559 bytes: vec![1, 2, 3],
560 identities: GatewayIdentities {
561 peer: Some("relay-peer".to_owned()),
562 ..GatewayIdentities::default()
563 },
564 },
565 100,
566 )
567 .unwrap();
568 let selected = match selected {
569 GatewayStep::Selected(selected) => Some(selected),
570 GatewayStep::RetryAvailable | GatewayStep::Unavailable => None,
571 }
572 .unwrap();
573 assert!(selected.direct_relay_fallback());
574 assert_eq!(
575 selected.transport(),
576 ResolutionTransport::HandshakeP2pDnsRelay
577 );
578 }
579
580 #[test]
581 fn malformed_and_authentication_failures_are_terminal() {
582 for failure in [
583 TransportFailure::Malformed,
584 TransportFailure::AuthenticationFailed,
585 TransportFailure::Cancelled,
586 ] {
587 let snapshot = policy();
588 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
589 let attempt = gateway.next_attempt(snapshot, 100).unwrap();
590 assert!(matches!(
591 gateway.complete(
592 snapshot,
593 attempt,
594 AttemptOutcome::Failure(failure),
595 100
596 ),
597 Err(GatewayError::TerminalTransportFailure(actual)) if actual == failure
598 ));
599 assert!(matches!(
600 gateway.next_attempt(snapshot, 100),
601 Err(GatewayError::Terminal)
602 ));
603 }
604 }
605
606 #[test]
607 fn enforces_response_bounds_and_transport_identity_topology() {
608 let mut config = PolicyConfig {
609 authenticated_authoritative_doh: false,
610 oblivious_dns: ObliviousDnsPolicy::Required,
611 providers: ProviderPolicy::default(),
612 wire_profile: WireProfile::DenuoV1,
613 ..PolicyConfig::default()
614 };
615 let snapshot = PolicySnapshot::new(9, config).unwrap();
616 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
617 failure_then_next(&mut gateway, snapshot, 100);
618 failure_then_next(&mut gateway, snapshot, 100);
619 let odoh = gateway.next_attempt(snapshot, 100).unwrap();
620 assert!(matches!(
621 gateway.complete(
622 snapshot,
623 odoh,
624 AttemptOutcome::Response {
625 bytes: vec![1],
626 identities: GatewayIdentities {
627 proxy: Some("same".to_owned()),
628 target: Some("same".to_owned()),
629 ..GatewayIdentities::default()
630 },
631 },
632 100
633 ),
634 Err(GatewayError::InvalidIdentityTopology)
635 ));
636 assert!(matches!(
637 gateway.next_attempt(snapshot, 100),
638 Err(GatewayError::Terminal)
639 ));
640
641 config.oblivious_dns = ObliviousDnsPolicy::Disabled;
642 let direct = PolicySnapshot::new(10, config).unwrap();
643 let mut gateway = Gateway::new(
644 direct,
645 GatewayLimits {
646 maximum_response_bytes: 2,
647 ..GatewayLimits::default()
648 },
649 )
650 .unwrap();
651 let attempt = gateway.next_attempt(direct, 100).unwrap();
652 assert!(matches!(
653 gateway.complete(
654 direct,
655 attempt,
656 AttemptOutcome::Response {
657 bytes: vec![1, 2, 3],
658 identities: GatewayIdentities::default(),
659 },
660 100
661 ),
662 Err(GatewayError::InvalidResponseSize)
663 ));
664 }
665
666 #[test]
667 fn rejects_stale_policy_mismatched_tokens_and_late_responses() {
668 let snapshot = policy();
669 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
670 let attempt = gateway.next_attempt(snapshot, 100).unwrap();
671 assert!(matches!(
672 gateway.next_attempt(snapshot, 100),
673 Err(GatewayError::AttemptAlreadyActive)
674 ));
675
676 let mut other = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
677 let foreign = other.next_attempt(snapshot, 100).unwrap();
678 assert!(matches!(
679 gateway.complete(
680 snapshot,
681 foreign,
682 AttemptOutcome::Failure(TransportFailure::Timeout),
683 100
684 ),
685 Err(GatewayError::AttemptMismatch)
686 ));
687 assert_eq!(
688 gateway
689 .complete(
690 snapshot,
691 attempt,
692 AttemptOutcome::Response {
693 bytes: vec![1],
694 identities: GatewayIdentities::default(),
695 },
696 111
697 )
698 .unwrap(),
699 GatewayStep::RetryAvailable
700 );
701
702 let changed = PolicySnapshot::new(snapshot.generation() + 1, snapshot.config()).unwrap();
703 assert!(matches!(
704 gateway.next_attempt(changed, 112),
705 Err(GatewayError::StalePolicy)
706 ));
707 assert!(matches!(
708 gateway.next_attempt(snapshot, 112),
709 Err(GatewayError::Terminal)
710 ));
711 }
712
713 #[test]
714 fn valid_udp_truncation_advances_only_to_direct_tcp() {
715 let snapshot = policy();
716 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
717 let udp = gateway.next_attempt(snapshot, 100).unwrap();
718 assert_eq!(udp.transport(), ResolutionTransport::DirectAuthoritativeUdp);
719 assert_eq!(
720 gateway
721 .complete(
722 snapshot,
723 udp,
724 AttemptOutcome::Failure(TransportFailure::Truncated),
725 100
726 )
727 .unwrap(),
728 GatewayStep::RetryAvailable
729 );
730 let tcp = gateway.next_attempt(snapshot, 100).unwrap();
731 assert_eq!(tcp.transport(), ResolutionTransport::DirectAuthoritativeTcp);
732 let selected = gateway
733 .complete(
734 snapshot,
735 tcp,
736 AttemptOutcome::Response {
737 bytes: vec![1],
738 identities: GatewayIdentities::default(),
739 },
740 100,
741 )
742 .unwrap();
743 assert!(matches!(
744 selected,
745 GatewayStep::Selected(selection)
746 if selection.transport() == ResolutionTransport::DirectAuthoritativeTcp
747 && !selection.direct_relay_fallback()
748 ));
749 }
750
751 #[test]
752 fn recursive_hns_doh_is_terminal_and_requires_policy_consent() {
753 let config = PolicyConfig {
754 user_configured_recursive_hns_doh: true,
755 ..PolicyConfig::default()
756 };
757 let snapshot = PolicySnapshot::new(2, config).unwrap();
758 let gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
759
760 assert_eq!(
761 gateway.plan().as_slice().last(),
762 Some(&ResolutionTransport::UserConfiguredRecursiveHnsDoh)
763 );
764 assert!(
765 gateway
766 .validate_response(
767 &[1],
768 &GatewayIdentities::default(),
769 ResolutionTransport::UserConfiguredRecursiveHnsDoh,
770 )
771 .is_ok()
772 );
773 }
774
775 #[test]
776 fn unsupported_final_candidate_reports_unavailable() {
777 let config = PolicyConfig {
778 authenticated_authoritative_doh: false,
779 oblivious_dns: ObliviousDnsPolicy::Disabled,
780 dns_relay_requester: hns_resolution_policy::DnsRelayRequesterPolicy::Disabled,
781 ..PolicyConfig::default()
782 };
783 let snapshot = PolicySnapshot::new(3, config).unwrap();
784 let mut gateway = Gateway::new(snapshot, GatewayLimits::default()).unwrap();
785 let first = gateway.next_attempt(snapshot, 100).unwrap();
786 assert_eq!(
787 gateway
788 .complete(
789 snapshot,
790 first,
791 AttemptOutcome::Failure(TransportFailure::Unsupported),
792 100
793 )
794 .unwrap(),
795 GatewayStep::RetryAvailable
796 );
797 let second = gateway.next_attempt(snapshot, 100).unwrap();
798 assert_eq!(
799 gateway
800 .complete(
801 snapshot,
802 second,
803 AttemptOutcome::Failure(TransportFailure::Unsupported),
804 100
805 )
806 .unwrap(),
807 GatewayStep::Unavailable
808 );
809 }
810}