Skip to main content

scion_stack/
resolver.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! DNS resolution helpers for SCION addresses.
15
16pub mod txt;
17
18use async_trait::async_trait;
19use sciparse::address::ip_addr::ScionIpAddr;
20use thiserror::Error;
21
22/// DNS resolver trait for SCION address discovery.
23///
24/// Implementations return zero or more `ScionAddr` values for a given domain
25/// name. The resolver is expected to be async and safe to share across tasks.
26///
27/// # Error handling
28///
29/// Implementations SHOULD return `ResolveError::NoValidEntries` when a lookup
30/// completes but yields no valid SCION TXT entries, which includes a name that
31/// has no TXT records at all and a name that does not exist. Lookups that never
32/// complete (timeout, network error, server failure) SHOULD return
33/// `ResolveError::DnsLookup`, the only outcome a retry can change; callers
34/// branch on that with [`ResolveError::is_transient`] rather than on the
35/// variants. Partial failures SHOULD return the valid addresses and log
36/// warnings for invalid entries.
37#[async_trait]
38pub trait ScionDnsResolver: Send + Sync {
39    /// Resolve a domain into SCION addresses.
40    ///
41    /// Implementations SHOULD return only valid addresses and log warnings for
42    /// invalid TXT entries. Errors are reserved for lookup failures or when no
43    /// valid addresses can be produced.
44    async fn resolve(&self, domain: &str) -> Result<Vec<ScionIpAddr>, ResolveError>;
45}
46
47/// Errors returned by SCION DNS resolution.
48// Derives `PartialEq` for testability; as a consequence, `DnsLookup` carries a formatted message
49// rather than the underlying (non-`PartialEq`) DNS error. See API_CONVENTIONS.md.
50#[derive(Debug, Error, PartialEq)]
51#[non_exhaustive]
52pub enum ResolveError {
53    /// The lookup itself failed, for example on a timeout, a network error, or
54    /// a server failure. A retry may succeed.
55    #[error("dns lookup failed: {0}")]
56    DnsLookup(String),
57    /// The lookup completed without producing a usable SCION address: the TXT
58    /// records that exist do not parse as TSAR entries, the name has no TXT
59    /// records, or the name does not exist. A retry does not help for as long
60    /// as the answer stays valid.
61    #[error("no valid TSAR TXT entries for {domain}")]
62    NoValidEntries {
63        /// Domain name that was looked up.
64        domain: String,
65        /// Invalid entries encountered during parsing or TXT decoding. Empty
66        /// when the lookup returned no TXT records to parse.
67        invalid_entries: Vec<InvalidEntry>,
68    },
69}
70
71impl ResolveError {
72    /// Returns whether the failure is transient, so that a retry may help.
73    ///
74    /// Prefer this over matching the variants: the enum is `#[non_exhaustive]`,
75    /// and a new variant would silently fall into a caller's wildcard arm.
76    #[must_use]
77    pub fn is_transient(&self) -> bool {
78        match self {
79            Self::DnsLookup(_) => true,
80            Self::NoValidEntries { .. } => false,
81        }
82    }
83}
84
85/// Metadata for a TXT entry that could not be parsed.
86#[derive(Debug, Clone, PartialEq)]
87pub struct InvalidEntry {
88    raw: String,
89    reason: String,
90}
91
92impl InvalidEntry {
93    pub(crate) fn new(raw: impl Into<String>, reason: impl Into<String>) -> Self {
94        Self {
95            raw: raw.into(),
96            reason: reason.into(),
97        }
98    }
99
100    /// Return the raw TXT entry that failed parsing.
101    pub fn raw(&self) -> &str {
102        &self.raw
103    }
104
105    /// Return the reason this TXT entry failed parsing.
106    pub fn reason(&self) -> &str {
107        &self.reason
108    }
109}