mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
Module reorganization, identity test coverage, design doc corrections
Module reorganization: - Split identity.rs (930 lines) into identity/ directory module: mod.rs, node_addr.rs, address.rs, peer.rs, local.rs, auth.rs, encoding.rs, tests.rs — following established bloom/, tree/, noise/ pattern - Group TUN, DNS, and ICMPv6 into upper/ module as the IPv6 adaptation layer: move tun.rs, icmp.rs, node/dns.rs into upper/ Identity test coverage (28 new tests, 52 total): - Encoding error paths: invalid npub/nsec length, bad hex input - NodeAddr: Debug, Display, as_slice, AsRef, Hash - FipsAddress: from_slice, From trait, Debug, Display, Eq+Hash - PeerIdentity: from_pubkey_full, pubkey_full parity paths, Debug - Identity: keypair, pubkey_full, Debug - AuthChallenge: from_bytes Design doc corrections (fips-software-architecture.md): - Identity struct: npub+nsec fields → keypair: Keypair with accessors - Node struct: TunInterface → TunState, Transport → TransportHandle, Peer → PeerSlot - Peer section: monolithic Peer → two-phase PeerSlot (PeerConnection + ActivePeer) with HandshakeState/ConnectivityState - ActivePeer: npub → identity: PeerIdentity, ancestry Vec → Option, declaration/inbound_filter wrapped in Option - BloomState: add 4 missing fields, fix update_debounce type - DiscoveredPeer: field name and type corrections
This commit is contained in:
-327
@@ -1,327 +0,0 @@
|
||||
//! FIPS DNS Responder
|
||||
//!
|
||||
//! Resolves `<npub>.fips` queries to FipsAddress IPv6 addresses.
|
||||
//! The resolution is pure computation: npub → PublicKey → NodeAddr → FipsAddress.
|
||||
//! As a side effect, resolved identities are sent to the Node for identity
|
||||
//! cache population, enabling subsequent TUN packet routing.
|
||||
|
||||
use crate::{NodeAddr, PeerIdentity};
|
||||
use simple_dns::rdata::{RData, AAAA};
|
||||
use simple_dns::{Packet, Name, ResourceRecord, CLASS, RCODE, PacketFlag, QTYPE, TYPE};
|
||||
use std::net::Ipv6Addr;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// Identity resolved by the DNS responder, sent to Node for cache population.
|
||||
pub struct DnsResolvedIdentity {
|
||||
pub node_addr: NodeAddr,
|
||||
pub pubkey: secp256k1::PublicKey,
|
||||
}
|
||||
|
||||
/// Channel sender for DNS → Node identity registration.
|
||||
pub type DnsIdentityTx = tokio::sync::mpsc::Sender<DnsResolvedIdentity>;
|
||||
|
||||
/// Channel receiver consumed by the Node RX event loop.
|
||||
pub type DnsIdentityRx = tokio::sync::mpsc::Receiver<DnsResolvedIdentity>;
|
||||
|
||||
/// Resolve a `.fips` domain name to an IPv6 address and identity.
|
||||
///
|
||||
/// The name should be `<npub>.fips` (with optional trailing dot).
|
||||
/// Returns the FipsAddress IPv6, NodeAddr, and full PublicKey on success.
|
||||
pub fn resolve_fips_query(name: &str) -> Option<(Ipv6Addr, NodeAddr, secp256k1::PublicKey)> {
|
||||
let name = name.strip_suffix('.').unwrap_or(name);
|
||||
let npub = name.strip_suffix(".fips")
|
||||
.or_else(|| name.strip_suffix(".FIPS"))
|
||||
.or_else(|| {
|
||||
// Case-insensitive check for .fips suffix
|
||||
let lower = name.to_ascii_lowercase();
|
||||
if lower.ends_with(".fips") {
|
||||
Some(&name[..name.len() - 5])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
let peer = PeerIdentity::from_npub(npub).ok()?;
|
||||
let ipv6 = peer.address().to_ipv6();
|
||||
let node_addr = *peer.node_addr();
|
||||
let pubkey = peer.pubkey_full();
|
||||
|
||||
Some((ipv6, node_addr, pubkey))
|
||||
}
|
||||
|
||||
/// Handle a raw DNS query packet and produce a response.
|
||||
///
|
||||
/// Returns the response bytes and an optional resolved identity (for AAAA queries
|
||||
/// that successfully resolved a `.fips` name).
|
||||
pub fn handle_dns_packet(query_bytes: &[u8], ttl: u32) -> Option<(Vec<u8>, Option<DnsResolvedIdentity>)> {
|
||||
let query = Packet::parse(query_bytes).ok()?;
|
||||
let question = query.questions.first()?;
|
||||
|
||||
let qname = question.qname.to_string();
|
||||
let is_aaaa = matches!(question.qtype, QTYPE::TYPE(TYPE::AAAA));
|
||||
|
||||
let mut response = query.into_reply();
|
||||
response.set_flags(PacketFlag::AUTHORITATIVE_ANSWER);
|
||||
|
||||
if is_aaaa
|
||||
&& let Some((ipv6, node_addr, pubkey)) = resolve_fips_query(&qname)
|
||||
{
|
||||
let name = Name::new_unchecked(&qname).into_owned();
|
||||
let record = ResourceRecord::new(
|
||||
name,
|
||||
CLASS::IN,
|
||||
ttl,
|
||||
RData::AAAA(AAAA::from(ipv6)),
|
||||
);
|
||||
response.answers.push(record);
|
||||
|
||||
let identity = DnsResolvedIdentity { node_addr, pubkey };
|
||||
let bytes = response.build_bytes_vec_compressed().ok()?;
|
||||
return Some((bytes, Some(identity)));
|
||||
}
|
||||
|
||||
// Query we can't answer: NXDOMAIN
|
||||
*response.rcode_mut() = RCODE::NameError;
|
||||
let bytes = response.build_bytes_vec_compressed().ok()?;
|
||||
Some((bytes, None))
|
||||
}
|
||||
|
||||
/// Run the DNS responder UDP server loop.
|
||||
///
|
||||
/// Listens for DNS queries, resolves `.fips` names, and sends resolved
|
||||
/// identities to the Node via the identity channel.
|
||||
pub async fn run_dns_responder(
|
||||
socket: tokio::net::UdpSocket,
|
||||
identity_tx: DnsIdentityTx,
|
||||
ttl: u32,
|
||||
) {
|
||||
let mut buf = [0u8; 512]; // Standard DNS UDP max
|
||||
|
||||
loop {
|
||||
let (len, src) = match socket.recv_from(&mut buf).await {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "DNS socket recv error");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let query_bytes = &buf[..len];
|
||||
|
||||
match handle_dns_packet(query_bytes, ttl) {
|
||||
Some((response_bytes, identity)) => {
|
||||
if let Some(id) = identity {
|
||||
debug!(
|
||||
node_addr = %id.node_addr,
|
||||
"DNS resolved .fips name, registering identity"
|
||||
);
|
||||
let _ = identity_tx.send(id).await;
|
||||
}
|
||||
|
||||
if let Err(e) = socket.send_to(&response_bytes, src).await {
|
||||
debug!(error = %e, "DNS send error");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!(len, "Failed to parse DNS query, dropping");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Identity;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_valid_npub() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
let expected_ipv6 = identity.address().to_ipv6();
|
||||
|
||||
let query = format!("{}.fips", npub);
|
||||
let result = resolve_fips_query(&query);
|
||||
|
||||
assert!(result.is_some(), "should resolve valid npub.fips");
|
||||
let (ipv6, node_addr, _pubkey) = result.unwrap();
|
||||
assert_eq!(ipv6, expected_ipv6);
|
||||
assert_eq!(node_addr, *identity.node_addr());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_trailing_dot() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
let expected_ipv6 = identity.address().to_ipv6();
|
||||
|
||||
let query = format!("{}.fips.", npub);
|
||||
let result = resolve_fips_query(&query);
|
||||
|
||||
assert!(result.is_some(), "should handle trailing dot");
|
||||
let (ipv6, _, _) = result.unwrap();
|
||||
assert_eq!(ipv6, expected_ipv6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_case_insensitive() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
|
||||
// .FIPS
|
||||
let result = resolve_fips_query(&format!("{}.FIPS", npub));
|
||||
assert!(result.is_some(), "should handle .FIPS");
|
||||
|
||||
// .Fips
|
||||
let result = resolve_fips_query(&format!("{}.Fips", npub));
|
||||
assert!(result.is_some(), "should handle .Fips");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_invalid_npub() {
|
||||
let result = resolve_fips_query("not-a-valid-npub.fips");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_wrong_suffix() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
|
||||
let result = resolve_fips_query(&format!("{}.com", npub));
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_empty_name() {
|
||||
assert!(resolve_fips_query("").is_none());
|
||||
assert!(resolve_fips_query(".fips").is_none());
|
||||
assert!(resolve_fips_query("fips").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_aaaa_query() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
let expected_ipv6 = identity.address().to_ipv6();
|
||||
|
||||
// Build a DNS AAAA query packet
|
||||
let query_name = format!("{}.fips", npub);
|
||||
let query_packet = build_test_query(&query_name, TYPE::AAAA);
|
||||
|
||||
let result = handle_dns_packet(&query_packet, 300);
|
||||
assert!(result.is_some(), "should handle AAAA query");
|
||||
|
||||
let (response_bytes, identity_opt) = result.unwrap();
|
||||
assert!(identity_opt.is_some(), "should produce identity");
|
||||
|
||||
// Parse the response
|
||||
let response = Packet::parse(&response_bytes).unwrap();
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
// Verify the AAAA record
|
||||
if let RData::AAAA(aaaa) = &response.answers[0].rdata {
|
||||
let addr = Ipv6Addr::from(aaaa.address);
|
||||
assert_eq!(addr, expected_ipv6);
|
||||
} else {
|
||||
panic!("expected AAAA record");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_nxdomain_for_unknown() {
|
||||
let query_packet = build_test_query("unknown.fips", TYPE::AAAA);
|
||||
|
||||
let result = handle_dns_packet(&query_packet, 300);
|
||||
assert!(result.is_some());
|
||||
|
||||
let (response_bytes, identity_opt) = result.unwrap();
|
||||
assert!(identity_opt.is_none(), "should not produce identity for unknown");
|
||||
|
||||
let response = Packet::parse(&response_bytes).unwrap();
|
||||
assert_eq!(response.rcode(), RCODE::NameError);
|
||||
assert!(response.answers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_non_aaaa_query() {
|
||||
let identity = Identity::generate();
|
||||
let query_name = format!("{}.fips", identity.npub());
|
||||
let query_packet = build_test_query(&query_name, TYPE::A);
|
||||
|
||||
let result = handle_dns_packet(&query_packet, 300);
|
||||
assert!(result.is_some());
|
||||
|
||||
let (response_bytes, identity_opt) = result.unwrap();
|
||||
assert!(identity_opt.is_none(), "A query should not resolve .fips");
|
||||
|
||||
let response = Packet::parse(&response_bytes).unwrap();
|
||||
assert_eq!(response.rcode(), RCODE::NameError);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dns_responder_udp() {
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
let expected_ipv6 = identity.address().to_ipv6();
|
||||
|
||||
// Bind responder on ephemeral port
|
||||
let server_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let server_addr = server_socket.local_addr().unwrap();
|
||||
|
||||
let (identity_tx, mut identity_rx) = tokio::sync::mpsc::channel(16);
|
||||
|
||||
// Spawn the responder
|
||||
let responder_handle = tokio::spawn(run_dns_responder(server_socket, identity_tx, 300));
|
||||
|
||||
// Send a query
|
||||
let client_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let query = build_test_query(&format!("{}.fips", npub), TYPE::AAAA);
|
||||
client_socket.send_to(&query, server_addr).await.unwrap();
|
||||
|
||||
// Receive response
|
||||
let mut buf = [0u8; 512];
|
||||
let (len, _) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
client_socket.recv_from(&mut buf),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let response = Packet::parse(&buf[..len]).unwrap();
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
if let RData::AAAA(aaaa) = &response.answers[0].rdata {
|
||||
assert_eq!(Ipv6Addr::from(aaaa.address), expected_ipv6);
|
||||
} else {
|
||||
panic!("expected AAAA record");
|
||||
}
|
||||
|
||||
// Verify identity was sent through channel
|
||||
let resolved = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
identity_rx.recv(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(resolved.node_addr, *identity.node_addr());
|
||||
|
||||
responder_handle.abort();
|
||||
}
|
||||
|
||||
/// Build a test DNS query packet for a given name and record type.
|
||||
fn build_test_query(name: &str, rtype: TYPE) -> Vec<u8> {
|
||||
use simple_dns::Question;
|
||||
|
||||
let mut packet = Packet::new_query(0x1234);
|
||||
let question = Question::new(
|
||||
Name::new_unchecked(name).into_owned(),
|
||||
QTYPE::TYPE(rtype),
|
||||
simple_dns::QCLASS::CLASS(CLASS::IN),
|
||||
false,
|
||||
);
|
||||
packet.questions.push(question);
|
||||
packet.build_bytes_vec().unwrap()
|
||||
}
|
||||
}
|
||||
@@ -569,7 +569,7 @@ impl Node {
|
||||
|
||||
/// Send ICMPv6 Destination Unreachable back through TUN.
|
||||
pub(in crate::node) fn send_icmpv6_dest_unreachable(&self, original_packet: &[u8]) {
|
||||
use crate::icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode};
|
||||
use crate::upper::icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode};
|
||||
use crate::FipsAddress;
|
||||
|
||||
if !should_send_icmp_error(original_packet) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{Node, NodeError, NodeState};
|
||||
use crate::peer::PeerConnection;
|
||||
use crate::protocol::{Disconnect, DisconnectReason};
|
||||
use crate::transport::{packet_channel, Link, LinkDirection, TransportAddr};
|
||||
use crate::tun::{run_tun_reader, shutdown_tun_interface, TunDevice, TunState};
|
||||
use crate::upper::tun::{run_tun_reader, shutdown_tun_interface, TunDevice, TunState};
|
||||
use crate::node::wire::build_msg1;
|
||||
use crate::{NodeAddr, PeerIdentity};
|
||||
use std::thread;
|
||||
@@ -319,7 +319,7 @@ impl Node {
|
||||
let dns_channel_size = self.config.node.buffers.dns_channel;
|
||||
let (identity_tx, identity_rx) = tokio::sync::mpsc::channel(dns_channel_size);
|
||||
let dns_ttl = self.config.dns.ttl();
|
||||
let handle = tokio::spawn(crate::node::dns::run_dns_responder(socket, identity_tx, dns_ttl));
|
||||
let handle = tokio::spawn(crate::upper::dns::run_dns_responder(socket, identity_tx, dns_ttl));
|
||||
self.dns_identity_rx = Some(identity_rx);
|
||||
self.dns_task = Some(handle);
|
||||
info!(bind = %bind, "DNS responder started for .fips domain");
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@
|
||||
//! Bloom filters, coordinate caches, transports, links, and peers.
|
||||
|
||||
mod bloom;
|
||||
pub(crate) mod dns;
|
||||
mod handlers;
|
||||
mod lifecycle;
|
||||
mod retry;
|
||||
@@ -27,7 +26,7 @@ use crate::transport::{
|
||||
};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use self::wire::build_encrypted;
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
@@ -297,7 +296,7 @@ pub struct Node {
|
||||
|
||||
// === DNS Responder ===
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
dns_identity_rx: Option<dns::DnsIdentityRx>,
|
||||
dns_identity_rx: Option<crate::upper::dns::DnsIdentityRx>,
|
||||
/// DNS responder task handle.
|
||||
dns_task: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user