Add infrastructure for handling `DNSSECError` onion messages
What changed, and why it matters
This commit adds plumbing for a new Lightning onion message type called DNSSECError. It lets a DNS resolver tell a payer that a name could not be resolved, so the payer can fail or retry faster. The change is mostly new message framing and empty/default handlers; it does not appear to fix an active security bug or introduce a clear vulnerability.
Treat as routine protocol-support code. Review future implementations of handle_dnssec_error for proper validation of DNSSECError::definitely_unresolvable and safe handling of untrusted resolver responses. No immediate security patch is indicated by this commit alone.
Security signals we found
New network message type added with serialization/deserialization code
Default handler is a no-op, so security relevance depends on future implementations
Message is only processed when a valid DNSResolverContext is present
No input-length or format validation changes beyond existing Hostname/Readable patterns
Evidence from the diff
The patch extends the DNSResolverMessage enum with a DNSSECError variant (TLV type 65550), adds a handle_dnssec_error method to the DNSResolverMessageHandler trait, implements serialization/deserialization, and wires it through OnionMessenger. The real handling logic is left to downstream implementers; the built-in IgnoringMessageHandler and test stubs do nothing. There is no evidence in the commit of memory corruption, authentication bypass, or cryptographic weakness.
Changed components
lightning/src/onion_message/dns_resolution.rslightning/src/onion_message/messenger.rslightning/src/ln/peer_handler.rslightning-dns-resolver/src/lib.rslightning/src/onion_message/functional_tests.rsInspect captured patch +81 / −4
diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs
index 90f1ac0..c802692 100644
--- a/lightning-dns-resolver/src/lib.rs
+++ b/lightning-dns-resolver/src/lib.rs
@@ -14,7 +14,7 @@ use dnssec_prover::query::build_txt_proof_async;
use lightning::blinded_path::message::DNSResolverContext;
use lightning::ln::peer_handler::IgnoringMessageHandler;
use lightning::onion_message::dns_resolution::{
- DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery,
+ DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery,
};
use lightning::onion_message::messenger::{
MessageSendInstructions, Responder, ResponseInstruction,
@@ -103,6 +103,12 @@ impl<PH: DNSResolverMessageHandler> DNSResolverMessageHandler for OMDomainResolv
}
}
+ fn handle_dnssec_error(&self, error: DNSSECError, context: DNSResolverContext) {
+ if let Some(proof_handler) = &self.proof_handler {
+ proof_handler.handle_dnssec_error(error, context);
+ }
+ }
+
fn handle_dnssec_query(
&self, q: DNSSECQuery, responder_opt: Option<Responder>,
) -> Option<(DNSResolverMessage, ResponseInstruction)> {
@@ -229,6 +235,9 @@ mod test {
core::mem::swap(&mut *self.resolved_uri.lock().unwrap(), &mut result);
assert!(result.is_none());
}
+ fn handle_dnssec_error(&self, msg: DNSSECError, context: DNSResolverContext) {
+ // TODO
+ }
fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> {
core::mem::take(&mut *self.pending_messages.lock().unwrap())
}
diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs
index 2cc2b9c..8a983c6 100644
--- a/lightning/src/ln/peer_handler.rs
+++ b/lightning/src/ln/peer_handler.rs
@@ -35,7 +35,7 @@ use crate::onion_message::async_payments::{
ServeStaticInvoice, StaticInvoicePersisted,
};
use crate::onion_message::dns_resolution::{
- DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery,
+ DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery,
};
use crate::onion_message::messenger::{
CustomOnionMessageHandler, MessageSendInstructions, Responder, ResponseInstruction,
@@ -273,6 +273,7 @@ impl DNSResolverMessageHandler for IgnoringMessageHandler {
None
}
fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {}
+ fn handle_dnssec_error(&self, _message: DNSSECError, _context: DNSResolverContext) {}
}
impl CustomOnionMessageHandler for IgnoringMessageHandler {
type CustomMessage = Infallible;
diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs
index 67d91bc..494b1bc 100644
--- a/lightning/src/onion_message/dns_resolution.rs
+++ b/lightning/src/onion_message/dns_resolution.rs
@@ -77,6 +77,19 @@ pub trait DNSResolverMessageHandler {
/// [`OnionMessenger`]: crate::onion_message::messenger::OnionMessenger
fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext);
+ /// Handle a [`DNSSECError`] message (in response to a [`DNSSECQuery`] we presumably sent),
+ /// indicating that the resolver was unable to resolve the requested name.
+ ///
+ /// The provided [`DNSResolverContext`] was authenticated by the [`OnionMessenger`] as coming from
+ /// a blinded path that we created.
+ ///
+ /// Receiving this lets us avoid waiting for a [`DNSSECProof`] which will never come, failing the
+ /// pending operation early instead (at least if the name is
+ /// [definitely unresolvable](DNSSECError::definitely_unresolvable)).
+ ///
+ /// [`OnionMessenger`]: crate::onion_message::messenger::OnionMessenger
+ fn handle_dnssec_error(&self, message: DNSSECError, context: DNSResolverContext);
+
/// Gets the node feature flags which this handler itself supports. Useful for setting the
/// `dns_resolver` flag if this handler supports returning [`DNSSECProof`] messages in response
/// to [`DNSSECQuery`] messages.
@@ -99,6 +112,9 @@ impl<T: DNSResolverMessageHandler + ?Sized, D: Deref<Target = T>> DNSResolverMes
fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) {
self.deref().handle_dnssec_proof(message, context)
}
+ fn handle_dnssec_error(&self, message: DNSSECError, context: DNSResolverContext) {
+ self.deref().handle_dnssec_error(message, context)
+ }
fn provided_node_features(&self) -> NodeFeatures {
self.deref().provided_node_features()
}
@@ -115,10 +131,14 @@ pub enum DNSResolverMessage {
DNSSECQuery(DNSSECQuery),
/// A response containing a DNSSEC proof
DNSSECProof(DNSSECProof),
+ /// An error in response to a [`DNSSECQuery`], indicating that the requested name could not be
+ /// resolved into a [`DNSSECProof`].
+ DNSSECError(DNSSECError),
}
const DNSSEC_QUERY_TYPE: u64 = 65536;
const DNSSEC_PROOF_TYPE: u64 = 65538;
+const DNSSEC_ERROR_TYPE: u64 = 65550;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
/// A message which is sent to a DNSSEC prover requesting a DNSSEC proof for the given name.
@@ -136,11 +156,30 @@ pub struct DNSSECProof {
pub proof: Vec<u8>,
}
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+/// A message which is sent in response to a [`DNSSECQuery`] when the resolver was unable to build a
+/// [`DNSSECProof`] for the requested name.
+///
+/// This lets the recipient stop waiting for a [`DNSSECProof`] which will not be forthcoming.
+pub struct DNSSECError {
+ /// The name which the [`DNSSECQuery`] was for and which we were unable to resolve.
+ pub name: Name,
+ /// Whether the name is known to be permanently unresolvable, as opposed to having failed due to
+ /// some transient error.
+ ///
+ /// This is set if the requested name does not exist (i.e. the resolver received an NXDOMAIN
+ /// response) or if the name is not in a DNSSEC-signed zone, in which case retrying or querying a
+ /// different resolver will not help. It is not set for transient failures (e.g. a timeout
+ /// communicating with an upstream DNS server), where a retry or a different resolver may yet
+ /// succeed.
+ pub definitely_unresolvable: bool,
+}
+
impl DNSResolverMessage {
/// Returns whether `tlv_type` corresponds to a TLV record for DNS Resolvers.
pub fn is_known_type(tlv_type: u64) -> bool {
match tlv_type {
- DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE => true,
+ DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE | DNSSEC_ERROR_TYPE => true,
_ => false,
}
}
@@ -158,6 +197,11 @@ impl Writeable for DNSResolverMessage {
w.write_all(&name.as_str().as_bytes())?;
proof.write(w)
},
+ Self::DNSSECError(DNSSECError { name, definitely_unresolvable }) => {
+ (name.as_str().len() as u8).write(w)?;
+ w.write_all(&name.as_str().as_bytes())?;
+ definitely_unresolvable.write(w)
+ },
}
}
}
@@ -176,6 +220,12 @@ impl ReadableArgs<u64> for DNSResolverMessage {
let proof = Readable::read(r)?;
Ok(DNSResolverMessage::DNSSECProof(DNSSECProof { name, proof }))
},
+ DNSSEC_ERROR_TYPE => {
+ let s = Hostname::read(r)?;
+ let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?;
+ let definitely_unresolvable = Readable::read(r)?;
+ Ok(DNSResolverMessage::DNSSECError(DNSSECError { name, definitely_unresolvable }))
+ },
_ => Err(DecodeError::InvalidValue),
}
}
@@ -187,6 +237,7 @@ impl OnionMessageContents for DNSResolverMessage {
match self {
DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query".to_string(),
DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof".to_string(),
+ DNSResolverMessage::DNSSECError(_) => "DNSSEC Error".to_string(),
}
}
#[cfg(not(c_bindings))]
@@ -194,12 +245,14 @@ impl OnionMessageContents for DNSResolverMessage {
match self {
DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query",
DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof",
+ DNSResolverMessage::DNSSECError(_) => "DNSSEC Error",
}
}
fn tlv_type(&self) -> u64 {
match self {
DNSResolverMessage::DNSSECQuery(_) => DNSSEC_QUERY_TYPE,
DNSResolverMessage::DNSSECProof(_) => DNSSEC_PROOF_TYPE,
+ DNSResolverMessage::DNSSECError(_) => DNSSEC_ERROR_TYPE,
}
}
}
diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs
index 4adc126..94536e0 100644
--- a/lightning/src/onion_message/functional_tests.rs
+++ b/lightning/src/onion_message/functional_tests.rs
@@ -14,7 +14,7 @@ use super::async_payments::{
ServeStaticInvoice, StaticInvoicePersisted,
};
use super::dns_resolution::{
- DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery,
+ DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery,
};
use super::messenger::{
CustomOnionMessageHandler, DefaultMessageRouter, Destination, MessageSendInstructions,
@@ -155,6 +155,7 @@ impl DNSResolverMessageHandler for TestDNSResolverMessageHandler {
None
}
fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {}
+ fn handle_dnssec_error(&self, _message: DNSSECError, _context: DNSResolverContext) {}
}
#[derive(Clone, Debug, PartialEq)]
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index 04697d9..a434e57 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -2309,6 +2309,19 @@ impl<
};
self.dns_resolver_handler.handle_dnssec_proof(msg, context);
},
+ DNSResolverMessage::DNSSECError(msg) => {
+ let context = match context {
+ Some(ctx) => ctx,
+ None => {
+ log_trace!(
+ logger,
+ "Ignoring DNSSECError onion message due to missing context"
+ );
+ return;
+ },
+ };
+ self.dns_resolver_handler.handle_dnssec_error(msg, context);
+ },
}
},
Ok(PeeledOnion::Custom(message, context, reply_path)) => {
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.