From 6b5c3ead1eed6606df564dcbe860d8be074e0729 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:21:29 +0800 Subject: [PATCH 01/85] chore(deps): switch h3x dependency to endpoint branch --- Cargo.toml | 5 ++++- gmdns-server/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89332b4..0d62c09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ members = ["gmdns-server"] resolver = "2" +[patch."https://github.com/genmeta/h3x.git"] +h3x = { path = "../h3x" } + [package] name = "gmdns" version = "0.2.0" @@ -49,7 +52,7 @@ url = "2" x509-parser = "0.18" # Optional HTTP/3 publisher/resolver via h3x -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "main", default-features = false, features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = [ "dquic", ], optional = true } http = { version = "1", optional = true } diff --git a/gmdns-server/Cargo.toml b/gmdns-server/Cargo.toml index d18a63a..281b43e 100644 --- a/gmdns-server/Cargo.toml +++ b/gmdns-server/Cargo.toml @@ -9,7 +9,7 @@ path = "src/main.rs" [dependencies] gmdns = { path = "..", features = ["h3x-resolver"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "main", features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", features = [ "dquic", ] } From 73ac61f12d649481dc6d9719b60282b5409a0304 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:30:57 +0800 Subject: [PATCH 02/85] refactor(resolvers): migrate h3/http/mdns resolvers to h3x endpoint API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - qresolve → resolver + net modules - h3x::server:: → h3x::endpoint::server:: - boundAddr → h3x::dquic::net::BoundAddr - Client → H3Endpoint - ConnectServerError → ConnectError --- gmdns-server/src/lookup.rs | 2 +- gmdns-server/src/publish.rs | 2 +- src/mdns.rs | 2 +- src/parser/record/endpoint.rs | 2 +- src/resolvers.rs | 3 ++- src/resolvers/h3.rs | 27 +++++++++++++-------------- src/resolvers/http.rs | 8 ++++---- src/resolvers/mdns.rs | 5 +++-- 8 files changed, 26 insertions(+), 25 deletions(-) diff --git a/gmdns-server/src/lookup.rs b/gmdns-server/src/lookup.rs index 34fd1ba..c6e55b5 100644 --- a/gmdns-server/src/lookup.rs +++ b/gmdns-server/src/lookup.rs @@ -8,7 +8,7 @@ use gmdns::{ MdnsPacket, parser::{packet::be_packet, record::RData}, }; -use h3x::server::{Request, Response, Service}; +use h3x::endpoint::server::{Request, Response, Service}; use redis::AsyncCommands; use tracing::debug; diff --git a/gmdns-server/src/publish.rs b/gmdns-server/src/publish.rs index a7eed21..1a5a7c3 100644 --- a/gmdns-server/src/publish.rs +++ b/gmdns-server/src/publish.rs @@ -1,7 +1,7 @@ use futures::future::BoxFuture; use h3x::{ + endpoint::server::{Request, Response, Service}, quic::agent::RemoteAgent, - server::{Request, Response, Service}, }; use redis::AsyncCommands; use tokio::time::{Duration, Instant}; diff --git a/src/mdns.rs b/src/mdns.rs index b3fa2e3..f10ee4d 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -9,7 +9,7 @@ use std::{ use futures::{Stream, stream}; #[cfg(feature = "h3x-resolver")] -use h3x::dquic::qbase::net::addr::BoundAddr; +use h3x::dquic::net::BoundAddr; use h3x::dquic::qinterface::{Interface, component::Component, io::IO}; use tokio::{task::JoinSet, time}; use tracing::Instrument; diff --git a/src/parser/record/endpoint.rs b/src/parser/record/endpoint.rs index ea682aa..0e98d53 100644 --- a/src/parser/record/endpoint.rs +++ b/src/parser/record/endpoint.rs @@ -8,7 +8,7 @@ use std::{ use base64::Engine; use bytes::BufMut; -use h3x::dquic::qresolve::SocketEndpointAddr; +use h3x::dquic::net::SocketEndpointAddr; use nom::{ IResult, Parser, bytes::streaming::take, diff --git a/src/resolvers.rs b/src/resolvers.rs index fad5462..cdf6d3f 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -6,8 +6,9 @@ use std::{ use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream}; use h3x::dquic::{ + net::{EndpointAddr, Family}, qinterface::device::Devices, - qresolve::{EndpointAddr, Family, Publish, Resolve, ResolveFuture, Source}, + resolver::{Publish, Resolve, ResolveFuture, Source}, }; use snafu::Report; use tokio::io; diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 9b6a329..3df11ae 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -3,12 +3,11 @@ use std::{fmt, io, sync::Arc, time::Duration}; use dashmap::DashMap; use futures::{FutureExt, StreamExt, TryFutureExt, stream}; use h3x::{ - client::Client, + endpoint::H3Endpoint, dquic::{ - prelude::ConnectServerError, - qresolve::{ - EndpointAddr, Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source, - }, + ConnectError, + net::EndpointAddr, + resolver::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }, quic, }; @@ -21,7 +20,7 @@ use crate::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; // Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { - client: Client, + client: H3Endpoint, base_url: Url, cached_records: DashMap, negative_cache: DashMap, @@ -29,7 +28,7 @@ pub struct H3Resolver { #[derive(Debug)] struct Record { - addrs: Vec, + addrs: Vec, expire: Instant, } @@ -52,14 +51,14 @@ impl fmt::Display for H3Resolver { } #[derive(Debug, snafu::Snafu)] -pub enum Error { +pub enum Error { #[snafu(display("h3 stream error"))] H3Stream { - source: h3x::client::MessageStreamError, + source: h3x::endpoint::server::MessageStreamError, }, #[snafu(display("h3 request error"))] H3Request { - source: h3x::client::RequestError, + source: h3x::endpoint::client::RequestError, }, #[snafu(display("{status}"))] @@ -81,7 +80,7 @@ impl H3Resolver where C::Error: Send + Sync + 'static, { - pub fn new(base_url: impl IntoUrl, client: Client) -> io::Result { + pub fn new(base_url: impl IntoUrl, client: H3Endpoint) -> io::Result { let base_url = base_url .into_url() .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; @@ -110,8 +109,8 @@ where let endpoints = endpoints .iter() .filter_map(|ep| match *ep { - h3x::dquic::qresolve::EndpointAddr::Socket(ep) => ep.try_into().ok(), - h3x::dquic::qresolve::EndpointAddr::Ble(..) => None, + h3x::dquic::net::EndpointAddr::Socket(ep) => ep.try_into().ok(), + h3x::dquic::net::EndpointAddr::Ble(..) => None, }) .collect(); let mut hosts = std::collections::HashMap::new(); @@ -225,7 +224,7 @@ where record::RData::E(ep) => { let socket_ep = ep.clone().try_into().ok()?; trace!(?socket_ep, "parsed endpoint from record"); - Some(h3x::dquic::qresolve::EndpointAddr::Socket(socket_ep)) + Some(h3x::dquic::net::EndpointAddr::Socket(socket_ep)) } _ => { tracing::debug!(?answer, "ignored record"); diff --git a/src/resolvers/http.rs b/src/resolvers/http.rs index f2ab92b..a2ae2e3 100644 --- a/src/resolvers/http.rs +++ b/src/resolvers/http.rs @@ -6,7 +6,7 @@ use std::{ use dashmap::DashMap; use futures::{StreamExt, TryFutureExt, stream}; -use h3x::dquic::qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}; +use h3x::dquic::resolver::{Publish, PublishFuture, Resolve, ResolveFuture, Source}; use reqwest::{Client, IntoUrl, StatusCode, Url}; use tokio::time::Instant; @@ -14,7 +14,7 @@ use crate::parser::packet::be_packet; #[derive(Debug)] struct Record { - addrs: Vec, + addrs: Vec, expire: Instant, } @@ -131,7 +131,7 @@ impl Resolve for HttpResolver { let endpoint_addrs: Vec<_> = record .addrs .iter() - .map(|e: &h3x::dquic::qresolve::EndpointAddr| (soource.clone(), *e)) + .map(|e: &h3x::dquic::net::EndpointAddr| (soource.clone(), *e)) .collect(); return Ok(stream::iter(endpoint_addrs).boxed()); } @@ -154,7 +154,7 @@ impl Resolve for HttpResolver { .filter_map(|answer| match answer.data() { record::RData::E(ep) => { let socket_ep = ep.clone().try_into().ok()?; - Some(h3x::dquic::qresolve::EndpointAddr::Socket(socket_ep)) + Some(h3x::dquic::net::EndpointAddr::Socket(socket_ep)) } _ => { tracing::debug!(?answer, "ignored record"); diff --git a/src/resolvers/mdns.rs b/src/resolvers/mdns.rs index 19404f2..422c7cd 100644 --- a/src/resolvers/mdns.rs +++ b/src/resolvers/mdns.rs @@ -10,8 +10,9 @@ use futures::{ stream::{self, FuturesUnordered}, }; use h3x::dquic::{ + net::{EndpointAddr, Family, SocketEndpointAddr}, qinterface::{BindInterface, WeakInterface, bind_uri::BindUri, io::IO}, - qresolve::{EndpointAddr, Family, RecordStream, ResolveFuture, SocketEndpointAddr, Source}, + resolver::{RecordStream, ResolveFuture, Source}, }; use super::{Publish, Resolve}; @@ -41,7 +42,7 @@ impl Publish for MdnsResolver { &'a self, name: &'a str, packet: &'a [u8], - ) -> h3x::dquic::qresolve::PublishFuture<'a> { + ) -> h3x::dquic::resolver::PublishFuture<'a> { use crate::parser::{packet::be_packet, record::RData}; let endpoints = be_packet(packet) .map(|(_, pkt)| { From de62cb88cbf80de36455a95bfd0655fe6ad243fa Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:46:05 +0800 Subject: [PATCH 03/85] refactor(examples): migrate publish to h3x endpoint API, remove error anti-patterns --- examples/publish.rs | 58 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/examples/publish.rs b/examples/publish.rs index 0b24d76..d0272dd 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -7,8 +7,19 @@ use std::{ use clap::Parser; use gmdns::{parser::record::endpoint::EndpointAddr, resolvers::H3Publisher}; -use h3x::dquic::{H3Client, qresolve::Publish}; -use rustls::{RootCertStore, SignatureScheme, pki_types::PrivateKeyDer, sign::SigningKey}; +use h3x::dquic::{ + Identity, Network, QuicEndpoint, ServerName, + cert::handy::{ToCertificate, ToPrivateKey}, + client::{ClientQuicConfig, ServerCertVerifierChoice}, + resolver::{Publish, handy::SystemResolver}, +}; +use rustls::{ + RootCertStore, + SignatureScheme, + client::WebPkiServerVerifier, + pki_types::PrivateKeyDer, + sign::SigningKey, +}; use tracing::{Level, info}; #[derive(Parser, Debug)] @@ -143,18 +154,35 @@ async fn main() -> io::Result<()> { .transpose()?; let signer_scheme = signer.as_deref().map(pick_signature_scheme).transpose()?; - let client = H3Client::builder() - .with_root_certificates(Arc::new(root_store)) - .with_identity( - opt.client_name, - cert_chain_pem.as_slice(), - private_key_pem.as_slice(), - ) - .map_err(io::Error::other)? - .build(); + // Build WebPki server cert verifier from CA root store + let verifier = WebPkiServerVerifier::builder(Arc::new(root_store)) + .build() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // Build TLS identity from cert chain and private key PEM + let identity = Identity { + name: ServerName::new(&opt.client_name), + certs: Arc::new(cert_chain_pem.to_certificate()), + key: Arc::new(private_key_pem.to_private_key()), + ocsp: Arc::new(None), + }; + + // Build network and QuicEndpoint with client mTLS config + let network = Network::builder().build(); + let quic = QuicEndpoint::builder() + .network(network) + .identity(Arc::new(identity)) + .resolver(Arc::new(SystemResolver)) + .client(ClientQuicConfig { + verifier: ServerCertVerifierChoice::WebPki(verifier), + ..Default::default() + }) + .build() + .await; + let h3_endpoint = h3x::dquic::H3Endpoint::new(quic); // Uses H3Resolver which uses dquic internally aka HTTP/3 - let resolver = H3Publisher::new(opt.base_url.clone(), client)?; + let resolver = H3Publisher::new(opt.base_url.clone(), h3_endpoint)?; info!(host = %opt.host, addrs = ?opt.addr, base_url = %opt.base_url, "publish.start"); if let Some(scheme) = signer_scheme { @@ -173,7 +201,9 @@ async fn main() -> io::Result<()> { endpoint.set_sequence(opt.sequence); if let Some((key, scheme)) = signer.as_deref().zip(signer_scheme) { info!("Signing endpoint with scheme: {:?}", scheme); - endpoint.sign_with(key, scheme).map_err(io::Error::other)?; + endpoint + .sign_with(key, scheme) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } info!("Publishing endpoint: {:?}", endpoint); let mut hosts = std::collections::HashMap::new(); @@ -182,7 +212,7 @@ async fn main() -> io::Result<()> { resolver .publish(&opt.host, &packet) .await - .map_err(io::Error::other)?; + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; info!("Successfully published endpoint for {}", addr); } info!("publish.ok"); From 80eb8f033d932bb8aaf31eb0d23f54faebad8956 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:46:05 +0800 Subject: [PATCH 04/85] refactor(examples): migrate query to h3x endpoint API, remove error anti-patterns --- examples/query.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/examples/query.rs b/examples/query.rs index 5a2408c..e274618 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -6,8 +6,18 @@ use std::{ use clap::Parser; use gmdns::{MdnsPacket, parser::record::RData, wire::be_multi_response}; -use h3x::dquic::H3Client; -use rustls::RootCertStore; +use h3x::{ + dquic::{ + client::{ClientQuicConfig, ServerCertVerifierChoice}, + resolver::handy::SystemResolver, + Network, QuicEndpoint, + }, + endpoint::H3Endpoint, +}; +use rustls::{ + RootCertStore, + client::WebPkiServerVerifier, +}; use tracing::{Level, info}; #[derive(Parser, Debug)] @@ -95,11 +105,21 @@ async fn main() -> Result<(), Box> { let opt = Options::parse(); let server_ca = expand_tilde(&opt.server_ca)?; let root_store = load_root_store_from_pem(&server_ca)?; - let client = H3Client::builder() - .with_root_certificates(Arc::new(root_store)) - .without_identity() - .map_err(|e| io::Error::other(e.to_string()))? - .build(); + let verifier = WebPkiServerVerifier::builder(Arc::new(root_store)) + .build() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let client_config = ClientQuicConfig { + verifier: ServerCertVerifierChoice::WebPki(verifier), + ..Default::default() + }; + let network = Network::builder().build(); + let quic = QuicEndpoint::builder() + .network(network) + .resolver(Arc::new(SystemResolver)) + .client(client_config) + .build() + .await; + let client = H3Endpoint::new(quic); let url = format!("{}lookup?host={}", opt.base_url, opt.host); info!(url = %url, "lookup.start"); From 044c481064fdecbdc871afcdbeb0db6167f38dec Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:46:05 +0800 Subject: [PATCH 05/85] refactor(server): migrate gmdns-server to QuicEndpoint + H3Endpoint::serve_owned --- gmdns-server/Cargo.toml | 1 + gmdns-server/src/main.rs | 63 ++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/gmdns-server/Cargo.toml b/gmdns-server/Cargo.toml index 281b43e..9697b4f 100644 --- a/gmdns-server/Cargo.toml +++ b/gmdns-server/Cargo.toml @@ -33,6 +33,7 @@ idna = "1" x509-parser = "0.18" snafu = "0.8" tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2" diff --git a/gmdns-server/src/main.rs b/gmdns-server/src/main.rs index 57cde39..d34f393 100644 --- a/gmdns-server/src/main.rs +++ b/gmdns-server/src/main.rs @@ -5,19 +5,22 @@ mod policy; mod publish; mod storage; -use std::{collections::HashMap, io, net::SocketAddr, sync::Arc}; +use std::{collections::HashMap, io, net::SocketAddr, str::FromStr, sync::Arc}; use clap::Parser; use gmdns::{MdnsEndpoint, MdnsPacket}; use h3x::{ - dquic::prelude::{ - BindUri, - handy::{ToCertificate, ToPrivateKey}, + dquic::{ + Identity, Network, QuicEndpoint, ServerName, + binds::BindPattern, + cert::handy::{ToCertificate, ToPrivateKey}, + server::ServerQuicConfig, }, - server::{Router, Servers}, + endpoint::{server::Router, H3Endpoint}, }; use rustls::{RootCertStore, server::WebPkiClientVerifier}; -use tracing::{info, level_filters::LevelFilter}; +use tokio_util::task::AbortOnDropHandle; +use tracing::{Instrument, info, level_filters::LevelFilter}; use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt}; use crate::{ @@ -52,7 +55,7 @@ fn build_seed_records(seed_records: &[SeedRecordConfig]) -> io::Result Result<(), Box> { }, ); - let bind = { - let base = BindUri::from(format!("inet://{}", config.listen)); - if config.listen.port() == 0 { - base.alloc_port() - } else { - base - } + let identity = Arc::new(Identity { + name: ServerName::new(&config.server_name), + certs: Arc::new(cert_pem.to_certificate()), + key: Arc::new(key_pem.to_private_key()), + ocsp: Arc::new(None), + }); + let server_config = ServerQuicConfig { + client_cert_verifier: verifier, + ..Default::default() }; - - let mut servers = Servers::builder() - .with_client_cert_verifier(verifier)? - .listen()?; - - servers - .add_server( - config.server_name.clone(), - cert_pem.to_certificate(), - key_pem.to_private_key(), - None, - [bind], - router, - ) - .await?; - + let quic = QuicEndpoint::builder() + .network(Network::builder().build()) + .identity(identity) + .server(server_config) + .bind(Arc::new(vec![ + BindPattern::from_str(&format!("inet://{}", config.listen)).expect("valid bind pattern"), + ])) + .build() + .await; + let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - _ = servers.run().await; + let _serve = AbortOnDropHandle::new( + tokio::spawn(server.serve_owned(router).in_current_span()), + ); Ok(()) } From 963b0a57336c95c7cbc9444544ad546a31a9caf7 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 11 May 2026 17:48:21 +0800 Subject: [PATCH 06/85] fix: replace io::Error::other with explicit io::ErrorKind --- gmdns-server/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gmdns-server/src/main.rs b/gmdns-server/src/main.rs index d34f393..bb18f05 100644 --- a/gmdns-server/src/main.rs +++ b/gmdns-server/src/main.rs @@ -55,7 +55,7 @@ fn build_seed_records(seed_records: &[SeedRecordConfig]) -> io::Result Date: Wed, 13 May 2026 18:47:22 +0800 Subject: [PATCH 07/85] fix(gmdns): adapt to dquic v0.5.1 and h3x Request API changes - Remove BoundAddr usage (bound_addr() now returns SocketAddr directly) - Replace SocketEndpointAddr with DquicEndpointAddr in TryFrom conversions - Update EndpointAddr::Socket(..) -> EndpointAddr::Direct{..} - Migrate to h3x Request builder API: .get(uri)/.with_body().post() -> .method()+.uri()+.write()+.into_response() - Use std::thread::spawn + oneshot channel to bridge non-Send Request future for Publish/Resolve trait implementations - Fix examples/query.rs to use new Request API --- examples/publish.rs | 5 +- examples/query.rs | 14 +-- gmdns-server/src/main.rs | 9 +- src/mdns.rs | 11 +-- src/parser/record/endpoint.rs | 41 ++++---- src/resolvers/h3.rs | 171 ++++++++++++++++++++++++++++------ src/resolvers/http.rs | 4 +- src/resolvers/mdns.rs | 6 +- 8 files changed, 185 insertions(+), 76 deletions(-) diff --git a/examples/publish.rs b/examples/publish.rs index d0272dd..acc8059 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -14,10 +14,7 @@ use h3x::dquic::{ resolver::{Publish, handy::SystemResolver}, }; use rustls::{ - RootCertStore, - SignatureScheme, - client::WebPkiServerVerifier, - pki_types::PrivateKeyDer, + RootCertStore, SignatureScheme, client::WebPkiServerVerifier, pki_types::PrivateKeyDer, sign::SigningKey, }; use tracing::{Level, info}; diff --git a/examples/query.rs b/examples/query.rs index e274618..b4194ab 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -8,16 +8,14 @@ use clap::Parser; use gmdns::{MdnsPacket, parser::record::RData, wire::be_multi_response}; use h3x::{ dquic::{ + Network, QuicEndpoint, client::{ClientQuicConfig, ServerCertVerifierChoice}, resolver::handy::SystemResolver, - Network, QuicEndpoint, }, endpoint::H3Endpoint, }; -use rustls::{ - RootCertStore, - client::WebPkiServerVerifier, -}; +use http::Method; +use rustls::{RootCertStore, client::WebPkiServerVerifier}; use tracing::{Level, info}; #[derive(Parser, Debug)] @@ -125,7 +123,11 @@ async fn main() -> Result<(), Box> { info!(url = %url, "lookup.start"); let uri: http::Uri = url.parse()?; - let (_req, mut resp) = client.new_request().get(uri).await?; + let client = Arc::new(client); + let req = client.new_request_owned(); + req.method(Method::GET); + req.uri(uri); + let mut resp = req.into_response().await?; if resp.status().is_success() { let bytes = resp.read_to_bytes().await?; diff --git a/gmdns-server/src/main.rs b/gmdns-server/src/main.rs index bb18f05..7ab5300 100644 --- a/gmdns-server/src/main.rs +++ b/gmdns-server/src/main.rs @@ -16,7 +16,7 @@ use h3x::{ cert::handy::{ToCertificate, ToPrivateKey}, server::ServerQuicConfig, }, - endpoint::{server::Router, H3Endpoint}, + endpoint::{H3Endpoint, server::Router}, }; use rustls::{RootCertStore, server::WebPkiClientVerifier}; use tokio_util::task::AbortOnDropHandle; @@ -191,15 +191,14 @@ async fn main() -> Result<(), Box> { .identity(identity) .server(server_config) .bind(Arc::new(vec![ - BindPattern::from_str(&format!("inet://{}", config.listen)).expect("valid bind pattern"), + BindPattern::from_str(&format!("inet://{}", config.listen)) + .expect("valid bind pattern"), ])) .build() .await; let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - let _serve = AbortOnDropHandle::new( - tokio::spawn(server.serve_owned(router).in_current_span()), - ); + let _serve = AbortOnDropHandle::new(tokio::spawn(server.serve_owned(router).in_current_span())); Ok(()) } diff --git a/src/mdns.rs b/src/mdns.rs index f10ee4d..5d0170a 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -8,8 +8,6 @@ use std::{ }; use futures::{Stream, stream}; -#[cfg(feature = "h3x-resolver")] -use h3x::dquic::net::BoundAddr; use h3x::dquic::qinterface::{Interface, component::Component, io::IO}; use tokio::{task::JoinSet, time}; use tracing::Instrument; @@ -75,12 +73,7 @@ impl Mdns { "interface is not bound to internet address", )); }; - let BoundAddr::Internet(bound_addr) = iface.bound_addr()? else { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "interface is not bound to internet address", - )); - }; + let bound_addr = iface.bound_addr()?; Self::new(service_name, bound_addr.ip(), device) } @@ -92,7 +85,7 @@ impl Mdns { let Some((_family, device, _port)) = binding.as_iface_bind_uri() else { return; }; - let Ok(BoundAddr::Internet(bound_addr)) = iface.bound_addr() else { + let Ok(bound_addr) = iface.bound_addr() else { return; }; let ip = bound_addr.ip(); diff --git a/src/parser/record/endpoint.rs b/src/parser/record/endpoint.rs index 0e98d53..386aefd 100644 --- a/src/parser/record/endpoint.rs +++ b/src/parser/record/endpoint.rs @@ -8,7 +8,7 @@ use std::{ use base64::Engine; use bytes::BufMut; -use h3x::dquic::net::SocketEndpointAddr; +use h3x::dquic::net::EndpointAddr as DquicEndpointAddr; use nom::{ IResult, Parser, bytes::streaming::take, @@ -680,22 +680,22 @@ impl Display for EndpointAddr { } } -impl TryFrom for EndpointAddr { +impl TryFrom for EndpointAddr { type Error = (); - fn try_from(value: SocketEndpointAddr) -> Result { + fn try_from(value: DquicEndpointAddr) -> Result { match value { - SocketEndpointAddr::Direct { + DquicEndpointAddr::Direct { addr: SocketAddr::V4(addr), } => Ok(Self::direct_v4(addr)), - SocketEndpointAddr::Direct { + DquicEndpointAddr::Direct { addr: SocketAddr::V6(addr), } => Ok(Self::direct_v6(addr)), - SocketEndpointAddr::Agent { + DquicEndpointAddr::Agent { agent: SocketAddr::V4(agent), outer: SocketAddr::V4(outer), } => Ok(Self::nat_v4(outer, agent)), - SocketEndpointAddr::Agent { + DquicEndpointAddr::Agent { agent: SocketAddr::V6(agent), outer: SocketAddr::V6(outer), } => Ok(Self::nat_v6(outer, agent)), @@ -704,26 +704,30 @@ impl TryFrom for EndpointAddr { } } -impl TryFrom for SocketEndpointAddr { +impl TryFrom for DquicEndpointAddr { type Error = (); fn try_from(value: EndpointAddr) -> Result { if let Some(agent_addr) = value.agent { match (value.primary, agent_addr) { - (SocketAddr::V4(outer), SocketAddr::V4(agent)) => Ok(SocketEndpointAddr::Agent { - outer: outer.into(), - agent: agent.into(), + (SocketAddr::V4(outer), SocketAddr::V4(agent)) => Ok(DquicEndpointAddr::Agent { + outer: SocketAddr::V4(outer), + agent: SocketAddr::V4(agent), }), - (SocketAddr::V6(outer), SocketAddr::V6(agent)) => Ok(SocketEndpointAddr::Agent { - outer: outer.into(), - agent: agent.into(), + (SocketAddr::V6(outer), SocketAddr::V6(agent)) => Ok(DquicEndpointAddr::Agent { + outer: SocketAddr::V6(outer), + agent: SocketAddr::V6(agent), }), _ => Err(()), } } else { match value.primary { - SocketAddr::V4(addr) => Ok(SocketEndpointAddr::Direct { addr: addr.into() }), - SocketAddr::V6(addr) => Ok(SocketEndpointAddr::Direct { addr: addr.into() }), + SocketAddr::V4(addr) => Ok(DquicEndpointAddr::Direct { + addr: SocketAddr::V4(addr), + }), + SocketAddr::V6(addr) => Ok(DquicEndpointAddr::Direct { + addr: SocketAddr::V6(addr), + }), } } } @@ -733,17 +737,14 @@ impl TryFrom for SocketEndpointAddr { pub fn sign_endponit_address( server_id: u8, key: Option<(&(impl SigningKey + ?Sized), SignatureScheme)>, - endpoint: SocketEndpointAddr, + endpoint: DquicEndpointAddr, ) -> Option { let mut ep: EndpointAddr = endpoint.try_into().ok()?; - ep.set_main(server_id == 0); ep.set_sequence(server_id as u64); - if let Some((key, scheme)) = key { let _ = ep.sign_with(key, scheme); } - Some(ep) } diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 3df11ae..5f1c29c 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -1,16 +1,17 @@ use std::{fmt, io, sync::Arc, time::Duration}; use dashmap::DashMap; -use futures::{FutureExt, StreamExt, TryFutureExt, stream}; +use futures::{StreamExt, stream}; use h3x::{ - endpoint::H3Endpoint, dquic::{ ConnectError, net::EndpointAddr, resolver::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }, + endpoint::H3Endpoint, quic, }; +use http::Method; use reqwest::IntoUrl; use tokio::time::Instant; use tracing::trace; @@ -20,7 +21,7 @@ use crate::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; // Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { - client: H3Endpoint, + client: Arc>, base_url: Url, cached_records: DashMap, negative_cache: DashMap, @@ -76,9 +77,10 @@ pub enum Error { ParseMultiResponse, } -impl H3Resolver +impl H3Resolver where C::Error: Send + Sync + 'static, + C::Connection: Send + 'static, { pub fn new(base_url: impl IntoUrl, client: H3Endpoint) -> io::Result { let base_url = base_url @@ -92,7 +94,7 @@ where })?; Ok(Self { - client, + client: Arc::new(client), base_url, cached_records: DashMap::new(), negative_cache: DashMap::new(), @@ -108,10 +110,7 @@ where let bytes = { let endpoints = endpoints .iter() - .filter_map(|ep| match *ep { - h3x::dquic::net::EndpointAddr::Socket(ep) => ep.try_into().ok(), - h3x::dquic::net::EndpointAddr::Ble(..) => None, - }) + .filter_map(|ep| crate::parser::record::endpoint::EndpointAddr::try_from(*ep).ok()) .collect(); let mut hosts = std::collections::HashMap::new(); hosts.insert(name.to_string(), endpoints); @@ -127,11 +126,14 @@ where url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("h3x publishing packet for {} to {}", name, self.base_url); - let (_, resp) = self - .client - .new_request() - .with_body(bytes::Bytes::copy_from_slice(packet)) - .post(uri) + let req = self.client.new_request_owned(); + req.method(Method::POST); + req.uri(uri); + req.write(bytes::Bytes::copy_from_slice(packet)) + .await + .map_err(|source| Error::H3Request { source })?; + let resp = req + .into_response() .await .map_err(|source| Error::H3Request { source })?; @@ -183,10 +185,11 @@ where let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("sending lookup request to {}", self.base_url); - let (_req, mut resp) = self - .client - .new_request() - .get(uri) + let req = self.client.new_request_owned(); + req.method(Method::GET); + req.uri(uri); + let mut resp = req + .into_response() .await .map_err(|source| Error::H3Request { source })?; @@ -222,9 +225,11 @@ where .iter() .filter_map(|answer| match answer.data() { record::RData::E(ep) => { - let socket_ep = ep.clone().try_into().ok()?; - trace!(?socket_ep, "parsed endpoint from record"); - Some(h3x::dquic::net::EndpointAddr::Socket(socket_ep)) + let endpoint = + TryInto::::try_into(ep.clone()) + .ok()?; + trace!(?endpoint, "parsed endpoint from record"); + Some(endpoint) } _ => { tracing::debug!(?answer, "ignored record"); @@ -256,22 +261,134 @@ where pub type H3Publisher = H3Resolver; -impl Publish for H3Publisher +impl Publish for H3Publisher where C::Error: Send + Sync + 'static, + C::Connection: Send + 'static, { fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { - self.publish_packet(name, packet) - .map_err(io::Error::other) - .boxed() + let (tx, rx) = tokio::sync::oneshot::channel(); + let name = name.to_owned(); + let packed = bytes::Bytes::copy_from_slice(packet); + let base_url = self.base_url.clone(); + let client = self.client.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to build runtime"); + let result = rt.block_on(async { + let mut url = base_url.join("publish").expect("Invalid base URL"); + url.set_query(Some(&format!("host={name}"))); + let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); + let req = client.new_request_owned(); + req.method(Method::POST); + req.uri(uri); + req.write(packed) + .await + .map_err(|source| Error::H3Request { source })?; + let resp = req + .into_response() + .await + .map_err(|source| Error::H3Request { source })?; + if resp.status() != http::StatusCode::OK { + return Err(Error::Status { + status: resp.status(), + }); + } + Ok(()) + }); + let _ = tx.send(result); + }); + Box::pin(async move { + match rx.await { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(io::Error::other(e)), + Err(_) => Err(io::Error::other("task cancelled")), + } + }) } } -impl Resolve for H3Resolver +impl Resolve for H3Resolver where C::Error: Send + Sync + 'static, + C::Connection: Send + 'static, { fn lookup<'l>(&'l self, name: &'l str) -> ResolveFuture<'l> { - self.lookup(name).map_err(io::Error::other).boxed() + let (tx, rx) = tokio::sync::oneshot::channel(); + let name = name.to_owned(); + let base_url = self.base_url.clone(); + let client = self.client.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to build runtime"); + let result = rt.block_on(async { + let mut url = base_url.join("lookup").expect("Invalid URL"); + url.set_query(Some(&format!("host={name}"))); + let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); + let req = client.new_request_owned(); + req.method(Method::GET); + req.uri(uri); + let mut resp = req + .into_response() + .await + .map_err(|source| Error::H3Request { source })?; + match resp.status() { + http::StatusCode::OK => { + let response = resp + .read_to_bytes() + .await + .map_err(|source| Error::H3Stream { source })?; + let (_remain, multi) = be_multi_response(response.as_ref()) + .map_err(|_| Error::ParseMultiResponse)?; + let mut addrs = Vec::new(); + for r in multi.records { + let (_remain, mdns_pkt) = + be_packet(&r.dns).map_err(|source| Error::ParseRecords { + source: source.to_owned(), + })?; + addrs.extend(mdns_pkt.answers.iter().filter_map(|answer| { + match answer.data() { + crate::parser::record::RData::E(ep) => { + TryInto::::try_into( + ep.clone(), + ) + .ok() + } + _ => None, + } + })); + } + if addrs.is_empty() { + return Err(Error::NoRecordFound); + } + let server: Arc = + Arc::from(base_url.host_str().unwrap_or("")); + Ok(stream::iter(addrs.into_iter().map(move |ep| { + ( + Source::Http { + server: server.clone(), + }, + ep, + ) + })) + .boxed()) + } + http::StatusCode::NOT_FOUND => Err(Error::NoRecordFound), + status => Err(Error::Status { status }), + } + }); + let _ = tx.send(result); + }); + Box::pin(async move { + match rx.await { + Ok(Ok(stream)) => Ok(stream), + Ok(Err(e)) => Err(io::Error::other(e)), + Err(_) => Err(io::Error::other("task cancelled")), + } + }) } } diff --git a/src/resolvers/http.rs b/src/resolvers/http.rs index a2ae2e3..3692ee5 100644 --- a/src/resolvers/http.rs +++ b/src/resolvers/http.rs @@ -153,8 +153,8 @@ impl Resolve for HttpResolver { .iter() .filter_map(|answer| match answer.data() { record::RData::E(ep) => { - let socket_ep = ep.clone().try_into().ok()?; - Some(h3x::dquic::net::EndpointAddr::Socket(socket_ep)) + let endpoint = ep.clone().try_into().ok()?; + Some(endpoint) } _ => { tracing::debug!(?answer, "ignored record"); diff --git a/src/resolvers/mdns.rs b/src/resolvers/mdns.rs index 422c7cd..d1714f6 100644 --- a/src/resolvers/mdns.rs +++ b/src/resolvers/mdns.rs @@ -10,7 +10,7 @@ use futures::{ stream::{self, FuturesUnordered}, }; use h3x::dquic::{ - net::{EndpointAddr, Family, SocketEndpointAddr}, + net::Family, qinterface::{BindInterface, WeakInterface, bind_uri::BindUri, io::IO}, resolver::{RecordStream, ResolveFuture, Source}, }; @@ -66,7 +66,7 @@ impl Resolve for MdnsResolver { self.query(name.to_owned()) .map_ok(move |list| { stream::iter(list.into_iter().filter_map(move |ep| { - let ep = EndpointAddr::Socket(SocketEndpointAddr::try_from(ep).ok()?); + let ep = h3x::dquic::net::EndpointAddr::try_from(ep).ok()?; Some((source.clone(), ep)) })) .boxed() @@ -120,7 +120,7 @@ impl MdnsResolvers { let source = resolver.source(); lookup_futures.push(resolver.query(name.to_owned()).map_ok(move |eps| { stream::iter(eps.into_iter().filter_map(move |ep| { - let ep = EndpointAddr::Socket(SocketEndpointAddr::try_from(ep).ok()?); + let ep = h3x::dquic::net::EndpointAddr::try_from(ep).ok()?; Some((source.clone(), ep)) })) })); From 8e2d3e00b46d8b36f42e8ce98190476470381d06 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 14 May 2026 10:50:54 +0800 Subject: [PATCH 08/85] fix(gmdns): fix clippy warnings and clean up deprecated Request API usage - Fix clippy io_other_error and redundant_closure in publish.rs - Remove unused http::Method imports - Replace deprecated new_request_owned/method/uri API with H3Endpoint::get/post - Rename H3Resolver::client to endpoint for clarity --- examples/publish.rs | 2 +- examples/query.rs | 6 +----- src/resolvers/h3.rs | 47 +++++++++++++++------------------------------ 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/examples/publish.rs b/examples/publish.rs index acc8059..d25b009 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -209,7 +209,7 @@ async fn main() -> io::Result<()> { resolver .publish(&opt.host, &packet) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + .map_err(io::Error::other)?; info!("Successfully published endpoint for {}", addr); } info!("publish.ok"); diff --git a/examples/query.rs b/examples/query.rs index b4194ab..e17eed8 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -14,7 +14,6 @@ use h3x::{ }, endpoint::H3Endpoint, }; -use http::Method; use rustls::{RootCertStore, client::WebPkiServerVerifier}; use tracing::{Level, info}; @@ -124,10 +123,7 @@ async fn main() -> Result<(), Box> { let uri: http::Uri = url.parse()?; let client = Arc::new(client); - let req = client.new_request_owned(); - req.method(Method::GET); - req.uri(uri); - let mut resp = req.into_response().await?; + let mut resp = client.get(uri).await?; if resp.status().is_success() { let bytes = resp.read_to_bytes().await?; diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 5f1c29c..1e0dbd7 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -11,7 +11,6 @@ use h3x::{ endpoint::H3Endpoint, quic, }; -use http::Method; use reqwest::IntoUrl; use tokio::time::Instant; use tracing::trace; @@ -21,7 +20,7 @@ use crate::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; // Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { - client: Arc>, + endpoint: Arc>, base_url: Url, cached_records: DashMap, negative_cache: DashMap, @@ -94,7 +93,7 @@ where })?; Ok(Self { - client: Arc::new(client), + endpoint: Arc::new(client), base_url, cached_records: DashMap::new(), negative_cache: DashMap::new(), @@ -126,14 +125,10 @@ where url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("h3x publishing packet for {} to {}", name, self.base_url); - let req = self.client.new_request_owned(); - req.method(Method::POST); - req.uri(uri); - req.write(bytes::Bytes::copy_from_slice(packet)) - .await - .map_err(|source| Error::H3Request { source })?; - let resp = req - .into_response() + let resp = self + .endpoint + .post(uri) + .body(packet) .await .map_err(|source| Error::H3Request { source })?; @@ -185,11 +180,9 @@ where let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("sending lookup request to {}", self.base_url); - let req = self.client.new_request_owned(); - req.method(Method::GET); - req.uri(uri); - let mut resp = req - .into_response() + let mut resp = self + .endpoint + .get(uri) .await .map_err(|source| Error::H3Request { source })?; @@ -271,7 +264,7 @@ where let name = name.to_owned(); let packed = bytes::Bytes::copy_from_slice(packet); let base_url = self.base_url.clone(); - let client = self.client.clone(); + let client = self.endpoint.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -281,14 +274,9 @@ where let mut url = base_url.join("publish").expect("Invalid base URL"); url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); - let req = client.new_request_owned(); - req.method(Method::POST); - req.uri(uri); - req.write(packed) - .await - .map_err(|source| Error::H3Request { source })?; - let resp = req - .into_response() + let resp = client + .post(uri) + .body(packed) .await .map_err(|source| Error::H3Request { source })?; if resp.status() != http::StatusCode::OK { @@ -319,7 +307,7 @@ where let (tx, rx) = tokio::sync::oneshot::channel(); let name = name.to_owned(); let base_url = self.base_url.clone(); - let client = self.client.clone(); + let client = self.endpoint.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -329,11 +317,8 @@ where let mut url = base_url.join("lookup").expect("Invalid URL"); url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); - let req = client.new_request_owned(); - req.method(Method::GET); - req.uri(uri); - let mut resp = req - .into_response() + let mut resp = client + .get(uri) .await .map_err(|source| Error::H3Request { source })?; match resp.status() { From ade254500170567b92d87daa990b8040a410bd92 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 15 May 2026 01:04:44 +0800 Subject: [PATCH 09/85] fix(gmdns): adapt to ServerName FromStr and remove deprecated with_mdns_resolvers Co-Authored-By: Claude Opus 4.7 --- examples/publish.rs | 4 ++-- gmdns-server/src/main.rs | 4 ++-- src/resolvers.rs | 27 +-------------------------- 3 files changed, 5 insertions(+), 30 deletions(-) diff --git a/examples/publish.rs b/examples/publish.rs index d25b009..d81ef00 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -8,7 +8,7 @@ use std::{ use clap::Parser; use gmdns::{parser::record::endpoint::EndpointAddr, resolvers::H3Publisher}; use h3x::dquic::{ - Identity, Network, QuicEndpoint, ServerName, + Identity, Network, QuicEndpoint, cert::handy::{ToCertificate, ToPrivateKey}, client::{ClientQuicConfig, ServerCertVerifierChoice}, resolver::{Publish, handy::SystemResolver}, @@ -158,7 +158,7 @@ async fn main() -> io::Result<()> { // Build TLS identity from cert chain and private key PEM let identity = Identity { - name: ServerName::new(&opt.client_name), + name: opt.client_name.parse().unwrap(), certs: Arc::new(cert_chain_pem.to_certificate()), key: Arc::new(private_key_pem.to_private_key()), ocsp: Arc::new(None), diff --git a/gmdns-server/src/main.rs b/gmdns-server/src/main.rs index 7ab5300..c5d5804 100644 --- a/gmdns-server/src/main.rs +++ b/gmdns-server/src/main.rs @@ -11,7 +11,7 @@ use clap::Parser; use gmdns::{MdnsEndpoint, MdnsPacket}; use h3x::{ dquic::{ - Identity, Network, QuicEndpoint, ServerName, + Identity, Network, QuicEndpoint, binds::BindPattern, cert::handy::{ToCertificate, ToPrivateKey}, server::ServerQuicConfig, @@ -177,7 +177,7 @@ async fn main() -> Result<(), Box> { ); let identity = Arc::new(Identity { - name: ServerName::new(&config.server_name), + name: config.server_name.parse().unwrap(), certs: Arc::new(cert_pem.to_certificate()), key: Arc::new(key_pem.to_private_key()), ocsp: Arc::new(None), diff --git a/src/resolvers.rs b/src/resolvers.rs index cdf6d3f..ed1aaa9 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -6,8 +6,7 @@ use std::{ use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream}; use h3x::dquic::{ - net::{EndpointAddr, Family}, - qinterface::device::Devices, + net::EndpointAddr, resolver::{Publish, Resolve, ResolveFuture, Source}, }; use snafu::Report; @@ -89,30 +88,6 @@ impl Resolvers { self } - pub fn with_mdns_resolvers( - mut self, - service_name: &str, - mut filter: impl FnMut(&str, Family) -> bool, - ) -> Self { - let devices = Devices::global(); - self.resolvers.extend( - devices - .interfaces() - .iter() - .flat_map(|(device, iface)| { - Option::into_iter( - (!iface.ipv4.is_empty()).then_some((device.as_str(), Family::V4)), - ) - .chain((!iface.ipv6.is_empty()).then_some((device.as_str(), Family::V6))) - }) - .filter(|(device, family)| filter(device, *family)) - .filter_map(|(device, ip)| Some((device, devices.resolve(device, ip)?))) - .filter_map(|(device, ip)| MdnsResolver::new(service_name, ip, device).ok()) - .map(|resolver| Arc::new(resolver) as ArcResolver), - ); - self - } - pub async fn lookup( &self, name: &str, From 355c74c4e36652e34af8f1c8fe7f35aa3a2a6ca9 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 17 May 2026 20:49:36 +0800 Subject: [PATCH 10/85] refactor(gmdns): adapt H3Resolver to H3Endpoint dual-generic Update H3Endpoint to H3Endpoint in H3Resolver to match the new dual-generic signature. Co-Authored-By: Claude Opus 4.7 --- src/resolvers/h3.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 1e0dbd7..a95f8ba 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -20,7 +20,7 @@ use crate::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; // Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { - endpoint: Arc>, + endpoint: Arc>, base_url: Url, cached_records: DashMap, negative_cache: DashMap, @@ -81,7 +81,7 @@ where C::Error: Send + Sync + 'static, C::Connection: Send + 'static, { - pub fn new(base_url: impl IntoUrl, client: H3Endpoint) -> io::Result { + pub fn new(base_url: impl IntoUrl, client: H3Endpoint) -> io::Result { let base_url = base_url .into_url() .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; From aeeb3381725007816738cb534734a14e941e53f7 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:27:28 +0800 Subject: [PATCH 11/85] chore: repair gmdns server redis dependency baseline --- gmdns-server/Cargo.toml | 3 +-- gmdns-server/src/lookup.rs | 4 ++-- gmdns-server/src/publish.rs | 9 +++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gmdns-server/Cargo.toml b/gmdns-server/Cargo.toml index 9697b4f..60209b1 100644 --- a/gmdns-server/Cargo.toml +++ b/gmdns-server/Cargo.toml @@ -14,8 +14,7 @@ h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", feature ] } # server-specific deps(不再污染核心库) -deadpool-redis = "0.12" -redis = { version = "0.23", features = ["tokio-comp", "aio"] } +deadpool-redis = "0.23" serde = { version = "1", features = ["derive"] } toml = "0.8" dashmap = "6" diff --git a/gmdns-server/src/lookup.rs b/gmdns-server/src/lookup.rs index c6e55b5..45e40c5 100644 --- a/gmdns-server/src/lookup.rs +++ b/gmdns-server/src/lookup.rs @@ -9,7 +9,7 @@ use gmdns::{ parser::{packet::be_packet, record::RData}, }; use h3x::endpoint::server::{Request, Response, Service}; -use redis::AsyncCommands; +use deadpool_redis::redis::{self, AsyncCommands}; use tracing::debug; use crate::{ @@ -99,7 +99,7 @@ async fn perform_lookup_multi( .arg(&set_key) .arg("-inf") .arg(cutoff_score) - .query_async::<_, ()>(&mut *conn) + .query_async::<()>(&mut *conn) .await .unwrap_or(()); diff --git a/gmdns-server/src/publish.rs b/gmdns-server/src/publish.rs index 1a5a7c3..2580976 100644 --- a/gmdns-server/src/publish.rs +++ b/gmdns-server/src/publish.rs @@ -3,7 +3,7 @@ use h3x::{ endpoint::server::{Request, Response, Service}, quic::agent::RemoteAgent, }; -use redis::AsyncCommands; +use deadpool_redis::redis::{self, AsyncCommands}; use tokio::time::{Duration, Instant}; use tracing::{debug, info, warn}; @@ -165,7 +165,8 @@ pub async fn publish_record( return; } }; - let ttl_secs: usize = state.ttl_secs.try_into().unwrap_or(usize::MAX); + let ttl_secs = state.ttl_secs; + let expire_ttl_secs = i64::try_from(state.ttl_secs).unwrap_or(i64::MAX); let now_secs = unix_now_secs(); let expire_secs = now_secs + state.ttl_secs; @@ -216,7 +217,7 @@ pub async fn publish_record( } // Expire the ZSET key at max(ttl_secs) from now as a safety net. - let _: () = conn.expire(&set_key, ttl_secs).await.unwrap_or(()); + let _: bool = conn.expire(&set_key, expire_ttl_secs).await.unwrap_or(false); // Evict stale (score < now - ttl) entries. let cutoff = now_secs.saturating_sub(state.ttl_secs) as f64; @@ -224,7 +225,7 @@ pub async fn publish_record( .arg(&set_key) .arg("-inf") .arg(cutoff) - .query_async::<_, ()>(&mut *conn) + .query_async::<()>(&mut *conn) .await .unwrap_or(()); } From 2875756efd5a71ba7ea284ce00c82230d4b3c348 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:28:07 +0800 Subject: [PATCH 12/85] chore: create ddns workspace skeleton --- Cargo.toml | 88 ++------------------------------------------ ddns-core/Cargo.toml | 17 +++++++++ ddns-core/src/lib.rs | 7 ++++ ddns/Cargo.toml | 52 ++++++++++++++++++++++++++ ddns/src/lib.rs | 11 ++++++ gmdns/Cargo.toml | 16 ++++++++ gmdns/src/lib.rs | 7 ++++ 7 files changed, 114 insertions(+), 84 deletions(-) create mode 100644 ddns-core/Cargo.toml create mode 100644 ddns-core/src/lib.rs create mode 100644 ddns/Cargo.toml create mode 100644 ddns/src/lib.rs create mode 100644 gmdns/Cargo.toml create mode 100644 gmdns/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0d62c09..7dc97e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,90 +1,10 @@ [workspace] -members = ["gmdns-server"] +members = ["ddns-core", "gmdns", "ddns", "gmdns-server"] resolver = "2" -[patch."https://github.com/genmeta/h3x.git"] -h3x = { path = "../h3x" } - -[package] -name = "gmdns" +[workspace.package] version = "0.2.0" edition = "2024" -autoexamples = false - -[dependencies] -base64 = "0.22" -bitfield-struct = "0.10" -bytes = "1" -dashmap = "6" -flume = "0.12" -futures = "0.3" -libc = "0.2" -nom = "8" -rand = "0.9" -reqwest = { version = "0.12", default-features = false, features = [ - "charset", - "rustls-tls", - "http2", - "macos-system-configuration", - "json", -] } -ring = "0.17" -rustls = { version = "0.23", default-features = false, features = [ - "logging", - "ring", -] } -rustls-pemfile = "2" -serde = "1" -shellexpand = "3" -snafu = "0.8" -socket2 = { version = "0.5.8", features = ["all"] } -tokio = { version = "1", features = [ - "time", - "macros", - "net", - "sync", - "rt", - "rt-multi-thread", -] } -tokio-util = { version = "0.7", features = ["rt"] } -tracing = "0.1" -url = "2" -x509-parser = "0.18" - -# Optional HTTP/3 publisher/resolver via h3x -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = [ - "dquic", -], optional = true } -http = { version = "1", optional = true } - -[features] -default = ["h3x-resolver"] -h3x-resolver = ["dep:h3x", "dep:http"] -[dev-dependencies] -criterion = "0.5" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -rustls-pki-types = "1" -tracing-appender = "0.2" -# examples: publish / query -clap = { version = "4", features = ["derive"] } -idna = "1" -serde = { version = "1", features = ["derive"] } - -[[example]] -name = "mdns_discover" -path = "examples/mdns_discover.rs" - -[[example]] -name = "mdns_query" -path = "examples/mdns_query.rs" - -[[example]] -name = "publish" -path = "examples/publish.rs" -required-features = ["h3x-resolver"] - -[[example]] -name = "query" -path = "examples/query.rs" -required-features = ["h3x-resolver"] +[patch."https://github.com/genmeta/h3x.git"] +h3x = { path = "../h3x" } diff --git a/ddns-core/Cargo.toml b/ddns-core/Cargo.toml new file mode 100644 index 0000000..3af09b8 --- /dev/null +++ b/ddns-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ddns-core" +version.workspace = true +edition.workspace = true + +[dependencies] +base64 = "0.22" +bitfield-struct = "0.10" +bytes = "1" +dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +nom = "8" +rand = "0.9" +ring = "0.17" +rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } +rustls-pemfile = "2" +snafu = "0.8" +x509-parser = "0.18" diff --git a/ddns-core/src/lib.rs b/ddns-core/src/lib.rs new file mode 100644 index 0000000..a43ac4b --- /dev/null +++ b/ddns-core/src/lib.rs @@ -0,0 +1,7 @@ +pub mod parser; +pub mod wire; + +pub type MdnsEndpoint = crate::parser::record::endpoint::EndpointAddr; +pub type MdnsPacket = crate::parser::packet::Packet; + +pub use parser::record::endpoint::sign_endponit_address; diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml new file mode 100644 index 0000000..328fedd --- /dev/null +++ b/ddns/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "ddns" +version.workspace = true +edition.workspace = true +autoexamples = false + +[dependencies] +ddns-core = { path = "../ddns-core" } +dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +futures = "0.3" +gmdns = { path = "../gmdns" } +rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } +snafu = "0.8" +tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } +tracing = "0.1" + +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"], optional = true } +http = { version = "1", optional = true } +reqwest = { version = "0.12", default-features = false, features = ["charset", "rustls-tls", "http2", "macos-system-configuration", "json"], optional = true } +url = { version = "2", optional = true } + +[features] +default = [] +h3x-resolver = ["dep:h3x", "dep:http", "dep:url"] +http-resolver = ["dep:reqwest"] + +[dev-dependencies] +clap = { version = "4", features = ["derive"] } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"] } +idna = "1" +rustls-pemfile = "2" +rustls-pki-types = "1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[[example]] +name = "mdns_discover" +path = "examples/mdns_discover.rs" + +[[example]] +name = "mdns_query" +path = "examples/mdns_query.rs" + +[[example]] +name = "publish" +path = "examples/publish.rs" +required-features = ["h3x-resolver"] + +[[example]] +name = "query" +path = "examples/query.rs" +required-features = ["h3x-resolver"] diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs new file mode 100644 index 0000000..d813f10 --- /dev/null +++ b/ddns/src/lib.rs @@ -0,0 +1,11 @@ +pub mod resolvers; + +pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; +pub use gmdns::{Mdns, MdnsResolver, MdnsResolvers, mdns}; +pub use resolvers::{DnsErrors, Resolvers}; + +#[cfg(feature = "h3x-resolver")] +pub use resolvers::{H3Publisher, H3Resolver}; + +#[cfg(feature = "http-resolver")] +pub use resolvers::HttpResolver; diff --git a/gmdns/Cargo.toml b/gmdns/Cargo.toml new file mode 100644 index 0000000..eb5b101 --- /dev/null +++ b/gmdns/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "gmdns" +version.workspace = true +edition.workspace = true + +[dependencies] +dashmap = "6" +ddns-core = { path = "../ddns-core" } +dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +flume = "0.12" +futures = "0.3" +libc = "0.2" +snafu = "0.8" +socket2 = { version = "0.5.8", features = ["all"] } +tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } +tracing = "0.1" diff --git a/gmdns/src/lib.rs b/gmdns/src/lib.rs new file mode 100644 index 0000000..46facd1 --- /dev/null +++ b/gmdns/src/lib.rs @@ -0,0 +1,7 @@ +mod if_nametoindex; +pub mod mdns; +mod protocol; +pub mod resolvers; + +pub use mdns::Mdns; +pub use resolvers::{MdnsResolver, MdnsResolvers}; From 76fe869903c9972612086e359d6c51a0342fc466 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:29:36 +0800 Subject: [PATCH 13/85] refactor: move DNS protocol code to ddns-core --- ddns-core/Cargo.toml | 4 + {src => ddns-core/src}/parser.rs | 6 +- {src => ddns-core/src}/parser/header.rs | 0 {src => ddns-core/src}/parser/name.rs | 0 {src => ddns-core/src}/parser/packet.rs | 8 ++ {src => ddns-core/src}/parser/question.rs | 20 ++- {src => ddns-core/src}/parser/record.rs | 2 +- .../src}/parser/record/endpoint.rs | 3 +- {src => ddns-core/src}/parser/record/ptr.rs | 0 {src => ddns-core/src}/parser/record/srv.rs | 0 {src => ddns-core/src}/parser/record/txt.rs | 0 {src => ddns-core/src}/parser/sigin.rs | 0 {src => ddns-core/src}/parser/varint.rs | 0 ddns-core/src/wire.rs | 115 ++++++++++++++++++ gmdns-server/Cargo.toml | 2 +- src/wire.rs | 57 --------- 16 files changed, 152 insertions(+), 65 deletions(-) rename {src => ddns-core/src}/parser.rs (60%) rename {src => ddns-core/src}/parser/header.rs (100%) rename {src => ddns-core/src}/parser/name.rs (100%) rename {src => ddns-core/src}/parser/packet.rs (99%) rename {src => ddns-core/src}/parser/question.rs (93%) rename {src => ddns-core/src}/parser/record.rs (99%) rename {src => ddns-core/src}/parser/record/endpoint.rs (99%) rename {src => ddns-core/src}/parser/record/ptr.rs (100%) rename {src => ddns-core/src}/parser/record/srv.rs (100%) rename {src => ddns-core/src}/parser/record/txt.rs (100%) rename {src => ddns-core/src}/parser/sigin.rs (100%) rename {src => ddns-core/src}/parser/varint.rs (100%) create mode 100644 ddns-core/src/wire.rs delete mode 100644 src/wire.rs diff --git a/ddns-core/Cargo.toml b/ddns-core/Cargo.toml index 3af09b8..4e50444 100644 --- a/ddns-core/Cargo.toml +++ b/ddns-core/Cargo.toml @@ -14,4 +14,8 @@ ring = "0.17" rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } rustls-pemfile = "2" snafu = "0.8" +tracing = "0.1" x509-parser = "0.18" + +[dev-dependencies] +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/parser.rs b/ddns-core/src/parser.rs similarity index 60% rename from src/parser.rs rename to ddns-core/src/parser.rs index 69b002e..9fa4f6a 100644 --- a/src/parser.rs +++ b/ddns-core/src/parser.rs @@ -1,7 +1,7 @@ -pub(crate) mod header; -pub(crate) mod name; +pub mod header; +pub mod name; pub mod packet; -pub(crate) mod question; +pub mod question; pub mod record; pub mod sigin; pub mod varint; diff --git a/src/parser/header.rs b/ddns-core/src/parser/header.rs similarity index 100% rename from src/parser/header.rs rename to ddns-core/src/parser/header.rs diff --git a/src/parser/name.rs b/ddns-core/src/parser/name.rs similarity index 100% rename from src/parser/name.rs rename to ddns-core/src/parser/name.rs diff --git a/src/parser/packet.rs b/ddns-core/src/parser/packet.rs similarity index 99% rename from src/parser/packet.rs rename to ddns-core/src/parser/packet.rs index 345ee8f..3ed5063 100644 --- a/src/parser/packet.rs +++ b/ddns-core/src/parser/packet.rs @@ -72,6 +72,14 @@ impl fmt::Display for Packet { } impl Packet { + pub fn id(&self) -> u16 { + self.header.id + } + + pub fn is_query(&self) -> bool { + self.header.flags.query() + } + pub fn query_with_id(service_name: String) -> Self { let mut packet = Packet::default(); let id: u16 = rand::random(); diff --git a/src/parser/question.rs b/ddns-core/src/parser/question.rs similarity index 93% rename from src/parser/question.rs rename to ddns-core/src/parser/question.rs index e498823..5443e99 100644 --- a/src/parser/question.rs +++ b/ddns-core/src/parser/question.rs @@ -1,5 +1,5 @@ use nom::number::streaming::be_u16; -use tokio::io; +use std::io; use super::name::{Name, be_name}; @@ -24,6 +24,24 @@ pub struct Question { pub(crate) qclass: QueryClass, } +impl Question { + pub fn name(&self) -> &Name { + &self.name + } + + pub fn prefer_unicast(&self) -> bool { + self.prefer_unicast + } + + pub fn qtype(&self) -> QueryType { + self.qtype + } + + pub fn qclass(&self) -> QueryClass { + self.qclass + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum QueryType { /// a host addresss diff --git a/src/parser/record.rs b/ddns-core/src/parser/record.rs similarity index 99% rename from src/parser/record.rs rename to ddns-core/src/parser/record.rs index cff5bbf..93915be 100644 --- a/src/parser/record.rs +++ b/ddns-core/src/parser/record.rs @@ -13,7 +13,7 @@ use nom::{ }; use ptr::{Ptr, be_ptr}; use srv::{Srv, be_srv}; -use tokio::io; +use std::io; use txt::Txt; use super::name::{Name, be_name}; diff --git a/src/parser/record/endpoint.rs b/ddns-core/src/parser/record/endpoint.rs similarity index 99% rename from src/parser/record/endpoint.rs rename to ddns-core/src/parser/record/endpoint.rs index 386aefd..f50cf72 100644 --- a/src/parser/record/endpoint.rs +++ b/ddns-core/src/parser/record/endpoint.rs @@ -8,7 +8,7 @@ use std::{ use base64::Engine; use bytes::BufMut; -use h3x::dquic::net::EndpointAddr as DquicEndpointAddr; +use dquic::qbase::net::addr::EndpointAddr as DquicEndpointAddr; use nom::{ IResult, Parser, bytes::streaming::take, @@ -733,7 +733,6 @@ impl TryFrom for DquicEndpointAddr { } } -#[cfg(feature = "h3x-resolver")] pub fn sign_endponit_address( server_id: u8, key: Option<(&(impl SigningKey + ?Sized), SignatureScheme)>, diff --git a/src/parser/record/ptr.rs b/ddns-core/src/parser/record/ptr.rs similarity index 100% rename from src/parser/record/ptr.rs rename to ddns-core/src/parser/record/ptr.rs diff --git a/src/parser/record/srv.rs b/ddns-core/src/parser/record/srv.rs similarity index 100% rename from src/parser/record/srv.rs rename to ddns-core/src/parser/record/srv.rs diff --git a/src/parser/record/txt.rs b/ddns-core/src/parser/record/txt.rs similarity index 100% rename from src/parser/record/txt.rs rename to ddns-core/src/parser/record/txt.rs diff --git a/src/parser/sigin.rs b/ddns-core/src/parser/sigin.rs similarity index 100% rename from src/parser/sigin.rs rename to ddns-core/src/parser/sigin.rs diff --git a/src/parser/varint.rs b/ddns-core/src/parser/varint.rs similarity index 100% rename from src/parser/varint.rs rename to ddns-core/src/parser/varint.rs diff --git a/ddns-core/src/wire.rs b/ddns-core/src/wire.rs new file mode 100644 index 0000000..29a6cd7 --- /dev/null +++ b/ddns-core/src/wire.rs @@ -0,0 +1,115 @@ +/// HTTP multi-record response wire format shared between server and all clients. +/// +/// Wire layout (big-endian, contiguous): +/// ```text +/// +-----------+ (repeated `count` times) +/// | count | +-----------+------+-----------+------+ +/// | u32 BE | | dns_len | dns | cert_len | cert | +/// +-----------+ | u32 BE | ... | u32 BE | ... | +/// +-----------+------+-----------+------+ +/// ``` +use bytes::BufMut; +use nom::{IResult, bytes::streaming::take, number::streaming::be_u32}; + +/// One DNS + certificate pair inside a [`MultiResponse`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResponseRecord { + /// Serialised DNS packet bytes. + pub dns: Vec, + /// DER-encoded leaf certificate of the publisher, or empty when unavailable. + pub cert: Vec, +} + +impl ResponseRecord { + /// SHA-256 fingerprint of the publisher certificate as lowercase hex. + /// Returns `None` when the cert field is empty. + pub fn cert_fingerprint_hex(&self) -> Option { + if self.cert.is_empty() { + return None; + } + use ring::digest::{SHA256, digest}; + let digest = digest(&SHA256, &self.cert); + Some(digest.as_ref().iter().map(|b| format!("{b:02x}")).collect()) + } +} + +/// HTTP response body carrying zero or more DNS records. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MultiResponse { + pub records: Vec, +} + +impl MultiResponse { + pub fn new(iter: impl IntoIterator, Vec)>) -> Self { + Self { + records: iter + .into_iter() + .map(|(dns, cert)| ResponseRecord { dns, cert }) + .collect(), + } + } + + pub fn encoding_size(&self) -> usize { + 4 + self + .records + .iter() + .map(|record| 4 + record.dns.len() + 4 + record.cert.len()) + .sum::() + } + + pub fn encode(&self) -> Vec { + let mut buf = Vec::with_capacity(self.encoding_size()); + buf.put_multi_response(self); + buf + } +} + +pub trait WriteMultiResponse { + fn put_multi_response(&mut self, response: &MultiResponse); +} + +impl WriteMultiResponse for B { + fn put_multi_response(&mut self, response: &MultiResponse) { + self.put_u32(response.records.len() as u32); + for record in &response.records { + self.put_u32(record.dns.len() as u32); + self.put_slice(&record.dns); + self.put_u32(record.cert.len() as u32); + self.put_slice(&record.cert); + } + } +} + +pub fn be_multi_response(input: &[u8]) -> IResult<&[u8], MultiResponse> { + let (mut input, count) = be_u32(input)?; + let mut records = Vec::with_capacity(count as usize); + for _ in 0..count { + let (rest, dns_len) = be_u32(input)?; + let (rest, dns) = take(dns_len as usize)(rest)?; + let (rest, cert_len) = be_u32(rest)?; + let (rest, cert) = take(cert_len as usize)(rest)?; + records.push(ResponseRecord { + dns: dns.to_vec(), + cert: cert.to_vec(), + }); + input = rest; + } + Ok((input, MultiResponse { records })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn multi_response_roundtrips() { + let response = MultiResponse::new([ + (vec![1, 2, 3], vec![4, 5]), + (vec![6, 7, 8, 9], Vec::new()), + ]); + let encoded = response.encode(); + let (remain, decoded) = be_multi_response(&encoded).unwrap(); + assert!(remain.is_empty()); + assert_eq!(decoded, response); + } +} diff --git a/gmdns-server/Cargo.toml b/gmdns-server/Cargo.toml index 60209b1..55a8314 100644 --- a/gmdns-server/Cargo.toml +++ b/gmdns-server/Cargo.toml @@ -8,7 +8,7 @@ name = "gmdns-server" path = "src/main.rs" [dependencies] -gmdns = { path = "..", features = ["h3x-resolver"] } +ddns = { path = "../ddns", features = ["h3x-resolver"] } h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", features = [ "dquic", ] } diff --git a/src/wire.rs b/src/wire.rs deleted file mode 100644 index 25c9719..0000000 --- a/src/wire.rs +++ /dev/null @@ -1,57 +0,0 @@ -/// HTTP multi-record response wire format shared between server and all clients. -/// -/// Wire layout (big-endian, contiguous): -/// ```text -/// +-----------+ (repeated `count` times) -/// | count | +-----------+------+-----------+------+ -/// | u32 BE | | dns_len | dns | cert_len | cert | -/// +-----------+ | u32 BE | ... | u32 BE | ... | -/// +-----------+------+-----------+------+ -/// ``` -use nom::{IResult, bytes::streaming::take, number::streaming::be_u32}; - -/// One DNS + certificate pair inside a [`MultiResponse`]. -#[derive(Debug, Clone)] -pub struct ResponseRecord { - /// Serialised DNS packet bytes. - pub dns: Vec, - /// DER-encoded leaf certificate of the publisher (may be empty). - pub cert: Vec, -} - -impl ResponseRecord { - /// SHA-256 fingerprint of the publisher certificate, as a lowercase hex string. - /// Returns `None` when the cert field is empty. - pub fn cert_fingerprint_hex(&self) -> Option { - if self.cert.is_empty() { - return None; - } - use ring::digest::{SHA256, digest}; - let d = digest(&SHA256, &self.cert); - Some(d.as_ref().iter().map(|b| format!("{b:02x}")).collect()) - } -} - -/// Decoded HTTP response body carrying one or more DNS records. -#[derive(Debug, Clone)] -pub struct MultiResponse { - pub records: Vec, -} - -/// nom parser for [`MultiResponse`]. -pub fn be_multi_response(input: &[u8]) -> IResult<&[u8], MultiResponse> { - let (mut input, count) = be_u32(input)?; - let mut records = Vec::with_capacity(count as usize); - for _ in 0..count { - let (rest, dns_len) = be_u32(input)?; - let (rest, dns) = take(dns_len as usize)(rest)?; - let (rest, cert_len) = be_u32(rest)?; - let (rest, cert) = take(cert_len as usize)(rest)?; - records.push(ResponseRecord { - dns: dns.to_vec(), - cert: cert.to_vec(), - }); - input = rest; - } - Ok((input, MultiResponse { records })) -} From 58d6a6a638ab20f26f4b514015ea868e90636932 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:30:19 +0800 Subject: [PATCH 14/85] refactor: move multicast DNS code to gmdns crate --- {src => gmdns/src}/if_nametoindex.rs | 0 {src => gmdns/src}/mdns.rs | 12 +++++------- {src => gmdns/src}/protocol.rs | 25 ++++++++++++------------- gmdns/src/resolvers.rs | 3 +++ {src => gmdns/src}/resolvers/mdns.rs | 18 +++++++++--------- 5 files changed, 29 insertions(+), 29 deletions(-) rename {src => gmdns/src}/if_nametoindex.rs (100%) rename {src => gmdns/src}/mdns.rs (96%) rename {src => gmdns/src}/protocol.rs (95%) create mode 100644 gmdns/src/resolvers.rs rename {src => gmdns/src}/resolvers/mdns.rs (92%) diff --git a/src/if_nametoindex.rs b/gmdns/src/if_nametoindex.rs similarity index 100% rename from src/if_nametoindex.rs rename to gmdns/src/if_nametoindex.rs diff --git a/src/mdns.rs b/gmdns/src/mdns.rs similarity index 96% rename from src/mdns.rs rename to gmdns/src/mdns.rs index 5d0170a..73b8bda 100644 --- a/src/mdns.rs +++ b/gmdns/src/mdns.rs @@ -8,14 +8,12 @@ use std::{ }; use futures::{Stream, stream}; -use h3x::dquic::qinterface::{Interface, component::Component, io::IO}; +use ddns_core::parser::{packet::Packet, record::endpoint::EndpointAddr}; +use dquic::qinterface::{Interface, component::Component, io::IO}; use tokio::{task::JoinSet, time}; use tracing::Instrument; -use crate::{ - parser::{packet::Packet, record::endpoint::EndpointAddr}, - protocol::MdnsProtocol, -}; +use crate::protocol::MdnsProtocol; #[derive(Clone)] pub struct Mdns { @@ -170,8 +168,8 @@ impl Mdns { query .questions .iter() - .any(|q| host_name.iter().any(|h| h.contains(q.name.as_str()))) - .then(|| Packet::answer(query.header.id, &guard)) + .any(|q| host_name.iter().any(|h| h.contains(q.name().as_str()))) + .then(|| Packet::answer(query.id(), &guard)) }; if let Some(packet) = packet diff --git a/src/protocol.rs b/gmdns/src/protocol.rs similarity index 95% rename from src/protocol.rs rename to gmdns/src/protocol.rs index 31580ab..25a84a4 100644 --- a/src/protocol.rs +++ b/gmdns/src/protocol.rs @@ -13,14 +13,13 @@ use snafu::Snafu; use socket2::{Domain, Socket, Type}; use tokio::{io, net::UdpSocket, task::JoinSet, time}; -use crate::{ - if_nametoindex::if_nametoindex, - parser::{ - packet::{Packet, be_packet}, - record::endpoint::EndpointAddr, - }, +use ddns_core::parser::{ + packet::{Packet, be_packet}, + record::endpoint::EndpointAddr, }; +use crate::if_nametoindex::if_nametoindex; + #[derive(Debug)] pub struct MdnsSocket { udp: UdpSocket, @@ -192,7 +191,7 @@ impl PacketRouter { } pub fn deliver(&self, source: SocketAddr, packet: Packet) { - match (packet.header.flags.query(), packet.header.id) { + match (packet.is_query(), packet.id()) { (true, 0) => { if self.responses.0.try_send((source, packet.clone())).is_err() { // Queue is full, remove oldest message (FIFO) @@ -288,7 +287,7 @@ impl MdnsProtocol { let router = self.router.upgrade().ok_or(Disconnected)?; let packet = Packet::query_with_id(local_name.clone()); - let query_id = NonZero::new(packet.header.id).ok_or_else(|| { + let query_id = NonZero::new(packet.id()).ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "Query id should not be 0") })?; @@ -305,7 +304,7 @@ impl MdnsProtocol { if let Ok(Some((source, packet))) = time::timeout(Duration::from_millis(300), packets.next()).await { - use crate::parser::record::RData::*; + use ddns_core::parser::record::RData::*; let endpoints = packet .answers .iter() @@ -313,17 +312,17 @@ impl MdnsProtocol { tracing::debug!(target: "mdns", ?answer, "recv response"); }) .filter(|answer| { - if answer.name != local_name { + if answer.name() != local_name { tracing::debug!( target: "mdns", - answer_name = answer.name, + answer_name = answer.name(), local_name, "ignored answer for different service name", ); } - answer.name == local_name + answer.name() == local_name }) - .filter_map(|answer| match &answer.data { + .filter_map(|answer| match answer.data() { E(e) => Some(e.clone()), _ => { tracing::debug!(target: "mdns", ?answer, "ignored record"); diff --git a/gmdns/src/resolvers.rs b/gmdns/src/resolvers.rs new file mode 100644 index 0000000..752f034 --- /dev/null +++ b/gmdns/src/resolvers.rs @@ -0,0 +1,3 @@ +mod mdns; + +pub use mdns::{MdnsResolver, MdnsResolvers}; diff --git a/src/resolvers/mdns.rs b/gmdns/src/resolvers/mdns.rs similarity index 92% rename from src/resolvers/mdns.rs rename to gmdns/src/resolvers/mdns.rs index d1714f6..f915cb1 100644 --- a/src/resolvers/mdns.rs +++ b/gmdns/src/resolvers/mdns.rs @@ -9,15 +9,15 @@ use futures::{ FutureExt, Stream, StreamExt, TryFutureExt, future, stream::{self, FuturesUnordered}, }; -use h3x::dquic::{ - net::Family, +use ddns_core::parser::{packet::Packet, record::RData}; +use dquic::{ + qbase::net::{Family, addr::EndpointAddr as DquicEndpointAddr}, qinterface::{BindInterface, WeakInterface, bind_uri::BindUri, io::IO}, - resolver::{RecordStream, ResolveFuture, Source}, + qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }; -use super::{Publish, Resolve}; pub use crate::mdns::Mdns as MdnsResolver; -use crate::{parser::packet::Packet, protocol::MdnsProtocol}; +use crate::protocol::MdnsProtocol; impl MdnsResolver { pub fn source(&self) -> Source { @@ -42,8 +42,8 @@ impl Publish for MdnsResolver { &'a self, name: &'a str, packet: &'a [u8], - ) -> h3x::dquic::resolver::PublishFuture<'a> { - use crate::parser::{packet::be_packet, record::RData}; + ) -> PublishFuture<'a> { + use ddns_core::parser::packet::be_packet; let endpoints = be_packet(packet) .map(|(_, pkt)| { pkt.answers @@ -66,7 +66,7 @@ impl Resolve for MdnsResolver { self.query(name.to_owned()) .map_ok(move |list| { stream::iter(list.into_iter().filter_map(move |ep| { - let ep = h3x::dquic::net::EndpointAddr::try_from(ep).ok()?; + let ep = DquicEndpointAddr::try_from(ep).ok()?; Some((source.clone(), ep)) })) .boxed() @@ -120,7 +120,7 @@ impl MdnsResolvers { let source = resolver.source(); lookup_futures.push(resolver.query(name.to_owned()).map_ok(move |eps| { stream::iter(eps.into_iter().filter_map(move |ep| { - let ep = h3x::dquic::net::EndpointAddr::try_from(ep).ok()?; + let ep = DquicEndpointAddr::try_from(ep).ok()?; Some((source.clone(), ep)) })) })); From a4160062dfd00ee7b7af0e3bd706fb69ddc1db90 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:39:05 +0800 Subject: [PATCH 15/85] refactor: move resolver facade to ddns crate --- ddns-core/src/parser/question.rs | 3 +- ddns-core/src/parser/record.rs | 2 +- ddns-core/src/wire.rs | 6 +-- ddns/Cargo.toml | 4 ++ {examples => ddns/examples}/README.md | 12 ++--- {examples => ddns/examples}/mdns_discover.rs | 12 ++--- {examples => ddns/examples}/mdns_query.rs | 2 +- {examples => ddns/examples}/publish.rs | 4 +- {examples => ddns/examples}/query.rs | 4 +- ddns/src/lib.rs | 6 +-- {src => ddns/src}/resolvers.rs | 36 +++++++++++--- {src => ddns/src}/resolvers/h3.rs | 49 +++++++++----------- {src => ddns/src}/resolvers/http.rs | 14 +++--- gmdns-server/src/lookup.rs | 2 +- gmdns-server/src/publish.rs | 7 ++- gmdns/src/mdns.rs | 2 +- gmdns/src/protocol.rs | 9 ++-- gmdns/src/resolvers/mdns.rs | 14 ++---- src/lib.rs | 12 ----- 19 files changed, 103 insertions(+), 97 deletions(-) rename {examples => ddns/examples}/README.md (86%) rename {examples => ddns/examples}/mdns_discover.rs (81%) rename {examples => ddns/examples}/mdns_query.rs (88%) rename {examples => ddns/examples}/publish.rs (97%) rename {examples => ddns/examples}/query.rs (97%) rename {src => ddns/src}/resolvers.rs (81%) rename {src => ddns/src}/resolvers/h3.rs (90%) rename {src => ddns/src}/resolvers/http.rs (94%) delete mode 100644 src/lib.rs diff --git a/ddns-core/src/parser/question.rs b/ddns-core/src/parser/question.rs index 5443e99..a9126f8 100644 --- a/ddns-core/src/parser/question.rs +++ b/ddns-core/src/parser/question.rs @@ -1,6 +1,7 @@ -use nom::number::streaming::be_u16; use std::io; +use nom::number::streaming::be_u16; + use super::name::{Name, be_name}; /// diff --git a/ddns-core/src/parser/record.rs b/ddns-core/src/parser/record.rs index 93915be..951f055 100644 --- a/ddns-core/src/parser/record.rs +++ b/ddns-core/src/parser/record.rs @@ -1,5 +1,6 @@ use std::{ fmt::Display, + io, net::{Ipv4Addr, Ipv6Addr}, }; @@ -13,7 +14,6 @@ use nom::{ }; use ptr::{Ptr, be_ptr}; use srv::{Srv, be_srv}; -use std::io; use txt::Txt; use super::name::{Name, be_name}; diff --git a/ddns-core/src/wire.rs b/ddns-core/src/wire.rs index 29a6cd7..9d3f539 100644 --- a/ddns-core/src/wire.rs +++ b/ddns-core/src/wire.rs @@ -103,10 +103,8 @@ mod tests { #[test] fn multi_response_roundtrips() { - let response = MultiResponse::new([ - (vec![1, 2, 3], vec![4, 5]), - (vec![6, 7, 8, 9], Vec::new()), - ]); + let response = + MultiResponse::new([(vec![1, 2, 3], vec![4, 5]), (vec![6, 7, 8, 9], Vec::new())]); let encoded = response.encode(); let (remain, decoded) = be_multi_response(&encoded).unwrap(); assert!(remain.is_empty()); diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml index 328fedd..9500296 100644 --- a/ddns/Cargo.toml +++ b/ddns/Cargo.toml @@ -6,6 +6,9 @@ autoexamples = false [dependencies] ddns-core = { path = "../ddns-core" } +nom = "8" +dashmap = "6" +bytes = "1" dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } futures = "0.3" gmdns = { path = "../gmdns" } @@ -30,6 +33,7 @@ h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default idna = "1" rustls-pemfile = "2" rustls-pki-types = "1" +shellexpand = "3" tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/README.md b/ddns/examples/README.md similarity index 86% rename from examples/README.md rename to ddns/examples/README.md index da10ec1..e5a20a2 100644 --- a/examples/README.md +++ b/ddns/examples/README.md @@ -2,21 +2,21 @@ ## Introduction -`gmdns` is a Rust-implemented DNS library that supports the mDNS (Multicast DNS) protocol and interacts with DNS servers via the HTTP/3 (H3) protocol for service discovery and publishing in local and remote networks. This document introduces how to use the example programs of `gmdns` to publish and query DNS services, including detailed program parameters and HTTP packet structures. +`ddns` is a Rust-implemented DNS library that supports the mDNS (Multicast DNS) protocol and interacts with DNS servers via the HTTP/3 (H3) protocol for service discovery and publishing in local and remote networks. This document introduces how to use the example programs of `ddns` to publish and query DNS services, including detailed program parameters and HTTP packet structures. ## Building the Project First, ensure you have a Rust environment. Clone or enter the project directory, then build: ```bash -cargo build --features="h3x-resolver" +cargo build -p ddns --features="h3x-resolver" ``` Note: The example programs require the `h3x-resolver` feature to enable HTTP/3 support. ## HTTP Packet Structure Overview -`gmdns` uses the HTTP/3 protocol to transmit DNS queries and responses, similar to DNS over HTTPS (DoH) but based on the QUIC protocol. The structure of HTTP requests is as follows: +`ddns` uses the HTTP/3 protocol to transmit DNS queries and responses, similar to DNS over HTTPS (DoH) but based on the QUIC protocol. The structure of HTTP requests is as follows: ### URL Structure - **Base URL**: Default `https://localhost:4433/`, used to specify the DNS server's address. @@ -55,7 +55,7 @@ Use the `publish` example to publish a DNS service record to the HTTP/3 DNS serv #### Example Run Command ```bash -cargo run --example publish --features="h3x-resolver" \ +cargo run -p ddns --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --client-name demo.example.genmeta.net \ --client-cert /path/to/demo.example.genmeta.net.pem \ @@ -77,7 +77,7 @@ Use the `query` example to query DNS service records from the HTTP/3 DNS server. #### Example Run Command ```bash -cargo run --example query --features="h3x-resolver" \ +cargo run -p ddns --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --host stun.genmeta.net ``` @@ -100,7 +100,7 @@ Use the `server` example to start an HTTP/3 DNS server. #### Example Run Command ```bash -cargo run --example server --features="h3x-resolver" \ +cargo run -p ddns-server -- --features="h3x-resolver" \ --listen 127.0.0.1:4433 \ --cert examples/keychain/localhost/server.cert \ --key examples/keychain/localhost/server.key diff --git a/examples/mdns_discover.rs b/ddns/examples/mdns_discover.rs similarity index 81% rename from examples/mdns_discover.rs rename to ddns/examples/mdns_discover.rs index 8593c73..659124b 100644 --- a/examples/mdns_discover.rs +++ b/ddns/examples/mdns_discover.rs @@ -21,14 +21,14 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = gmdns::mdns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; + let mdns = ddns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; mdns.insert_host( "test.genmeta.net".to_string(), vec![ { let addr: SocketAddr = "192.168.1.7:7000".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - gmdns::parser::record::endpoint::EndpointAddr::direct_v4(v4) + ddns::MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -36,7 +36,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.13:7000".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - gmdns::parser::record::endpoint::EndpointAddr::direct_v4(v4) + ddns::MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -50,7 +50,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - gmdns::parser::record::endpoint::EndpointAddr::direct_v4(v4) + ddns::MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -58,7 +58,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - gmdns::parser::record::endpoint::EndpointAddr::direct_v4(v4) + ddns::MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -66,7 +66,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - gmdns::parser::record::endpoint::EndpointAddr::direct_v4(v4) + ddns::MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } diff --git a/examples/mdns_query.rs b/ddns/examples/mdns_query.rs similarity index 88% rename from examples/mdns_query.rs rename to ddns/examples/mdns_query.rs index def0f93..5da374b 100644 --- a/examples/mdns_query.rs +++ b/ddns/examples/mdns_query.rs @@ -17,7 +17,7 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = gmdns::mdns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; + let mdns = ddns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; let ret = mdns.query("publish.test.genmeta.net".to_string()).await?; println!("{ret:?}\n"); diff --git a/examples/publish.rs b/ddns/examples/publish.rs similarity index 97% rename from examples/publish.rs rename to ddns/examples/publish.rs index d81ef00..eef2a56 100644 --- a/examples/publish.rs +++ b/ddns/examples/publish.rs @@ -6,7 +6,7 @@ use std::{ }; use clap::Parser; -use gmdns::{parser::record::endpoint::EndpointAddr, resolvers::H3Publisher}; +use ddns::{parser::record::endpoint::EndpointAddr, resolvers::H3Publisher}; use h3x::dquic::{ Identity, Network, QuicEndpoint, cert::handy::{ToCertificate, ToPrivateKey}, @@ -205,7 +205,7 @@ async fn main() -> io::Result<()> { info!("Publishing endpoint: {:?}", endpoint); let mut hosts = std::collections::HashMap::new(); hosts.insert(opt.host.clone(), vec![endpoint]); - let packet = gmdns::MdnsPacket::answer(0, &hosts).to_bytes(); + let packet = ddns::MdnsPacket::answer(0, &hosts).to_bytes(); resolver .publish(&opt.host, &packet) .await diff --git a/examples/query.rs b/ddns/examples/query.rs similarity index 97% rename from examples/query.rs rename to ddns/examples/query.rs index e17eed8..2c15556 100644 --- a/examples/query.rs +++ b/ddns/examples/query.rs @@ -5,7 +5,7 @@ use std::{ }; use clap::Parser; -use gmdns::{MdnsPacket, parser::record::RData, wire::be_multi_response}; +use ddns::{MdnsPacket, parser::record::RData, wire::be_multi_response}; use h3x::{ dquic::{ Network, QuicEndpoint, @@ -146,7 +146,7 @@ async fn main() -> Result<(), Box> { None => println!("Source fingerprint: (no certificate)"), } - match gmdns::parser::packet::be_packet(&record.dns) { + match ddns::parser::packet::be_packet(&record.dns) { Ok((_, packet)) => { print!("{}", format_packet(&packet)); diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs index d813f10..7276bbb 100644 --- a/ddns/src/lib.rs +++ b/ddns/src/lib.rs @@ -2,10 +2,8 @@ pub mod resolvers; pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; pub use gmdns::{Mdns, MdnsResolver, MdnsResolvers, mdns}; +#[cfg(feature = "http-resolver")] +pub use resolvers::HttpResolver; pub use resolvers::{DnsErrors, Resolvers}; - #[cfg(feature = "h3x-resolver")] pub use resolvers::{H3Publisher, H3Resolver}; - -#[cfg(feature = "http-resolver")] -pub use resolvers::HttpResolver; diff --git a/src/resolvers.rs b/ddns/src/resolvers.rs similarity index 81% rename from src/resolvers.rs rename to ddns/src/resolvers.rs index ed1aaa9..e31bd52 100644 --- a/src/resolvers.rs +++ b/ddns/src/resolvers.rs @@ -4,22 +4,26 @@ use std::{ sync::Arc, }; -use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream}; -use h3x::dquic::{ - net::EndpointAddr, - resolver::{Publish, Resolve, ResolveFuture, Source}, +use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Resolve, ResolveFuture, Source}, }; +use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream}; use snafu::Report; use tokio::io; #[cfg(feature = "h3x-resolver")] mod h3; +#[cfg(feature = "http-resolver")] mod http; -mod mdns; /// Extract and validate the DNS host from `name`, which may include a `:port` /// suffix. Returns `Some(host)` if the host part is a valid RFC-compliant DNS /// name, or `None` for raw IP addresses, bracketed IPv6, or malformed input. +#[cfg_attr( + not(any(feature = "h3x-resolver", feature = "http-resolver")), + allow(dead_code) +)] pub(crate) fn resolvable_name(name: &str) -> Option<&str> { let host = match name.rsplit_once(':') { Some((h, port)) if !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) => h, @@ -29,10 +33,11 @@ pub(crate) fn resolvable_name(name: &str) -> Option<&str> { Some(host) } +pub use gmdns::resolvers::{MdnsResolver, MdnsResolvers}; #[cfg(feature = "h3x-resolver")] pub use h3::{H3Publisher, H3Resolver}; +#[cfg(feature = "http-resolver")] pub use http::HttpResolver; -pub use mdns::{MdnsResolver, MdnsResolvers}; type ArcResolver = Arc; @@ -122,3 +127,22 @@ impl Resolve for Resolvers { .boxed() } } + +#[cfg(test)] +mod tests { + use super::resolvable_name; + + #[test] + fn resolvable_name_accepts_dns_name_with_numeric_port() { + assert_eq!( + resolvable_name("example.genmeta.net:443"), + Some("example.genmeta.net") + ); + } + + #[test] + fn resolvable_name_rejects_ip_literals() { + assert_eq!(resolvable_name("127.0.0.1:443"), None); + assert_eq!(resolvable_name("[::1]:443"), None); + } +} diff --git a/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs similarity index 90% rename from src/resolvers/h3.rs rename to ddns/src/resolvers/h3.rs index a95f8ba..96e0dad 100644 --- a/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -1,23 +1,17 @@ use std::{fmt, io, sync::Arc, time::Duration}; use dashmap::DashMap; -use futures::{StreamExt, stream}; -use h3x::{ - dquic::{ - ConnectError, - net::EndpointAddr, - resolver::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, - }, - endpoint::H3Endpoint, - quic, +use ddns_core::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; +use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }; -use reqwest::IntoUrl; +use futures::{StreamExt, stream}; +use h3x::{dquic::ConnectError, endpoint::H3Endpoint, quic}; use tokio::time::Instant; use tracing::trace; use url::Url; -use crate::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; - // Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { endpoint: Arc>, @@ -28,7 +22,7 @@ pub struct H3Resolver { #[derive(Debug)] struct Record { - addrs: Vec, + addrs: Vec, expire: Instant, } @@ -81,14 +75,16 @@ where C::Error: Send + Sync + 'static, C::Connection: Send + 'static, { - pub fn new(base_url: impl IntoUrl, client: H3Endpoint) -> io::Result { - let base_url = base_url - .into_url() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + pub fn new( + base_url: impl AsRef, + client: H3Endpoint, + ) -> io::Result { + let base_url = Url::parse(base_url.as_ref()) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; base_url.host_str().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, - "Base URL must have a valid host", + "base URL must have a valid host", ) })?; @@ -109,7 +105,9 @@ where let bytes = { let endpoints = endpoints .iter() - .filter_map(|ep| crate::parser::record::endpoint::EndpointAddr::try_from(*ep).ok()) + .filter_map(|ep| { + ddns_core::parser::record::endpoint::EndpointAddr::try_from(*ep).ok() + }) .collect(); let mut hosts = std::collections::HashMap::new(); hosts.insert(name.to_string(), endpoints); @@ -144,7 +142,7 @@ where pub const EXCLUDED_DOMAINS: [&str; 2] = ["dns.genmeta.net", "download.genmeta.net"]; pub async fn lookup(&self, name: &str) -> Result> { - use crate::parser::record; + use ddns_core::parser::record; let server = Arc::from(self.base_url.host_str().unwrap_or("")); let source = Source::Http { server }; @@ -218,9 +216,7 @@ where .iter() .filter_map(|answer| match answer.data() { record::RData::E(ep) => { - let endpoint = - TryInto::::try_into(ep.clone()) - .ok()?; + let endpoint = TryInto::::try_into(ep.clone()).ok()?; trace!(?endpoint, "parsed endpoint from record"); Some(endpoint) } @@ -337,11 +333,8 @@ where })?; addrs.extend(mdns_pkt.answers.iter().filter_map(|answer| { match answer.data() { - crate::parser::record::RData::E(ep) => { - TryInto::::try_into( - ep.clone(), - ) - .ok() + ddns_core::parser::record::RData::E(ep) => { + TryInto::::try_into(ep.clone()).ok() } _ => None, } diff --git a/src/resolvers/http.rs b/ddns/src/resolvers/http.rs similarity index 94% rename from src/resolvers/http.rs rename to ddns/src/resolvers/http.rs index 3692ee5..5d98bef 100644 --- a/src/resolvers/http.rs +++ b/ddns/src/resolvers/http.rs @@ -5,16 +5,18 @@ use std::{ }; use dashmap::DashMap; +use ddns_core::parser::packet::be_packet; +use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}, +}; use futures::{StreamExt, TryFutureExt, stream}; -use h3x::dquic::resolver::{Publish, PublishFuture, Resolve, ResolveFuture, Source}; use reqwest::{Client, IntoUrl, StatusCode, Url}; use tokio::time::Instant; -use crate::parser::packet::be_packet; - #[derive(Debug)] struct Record { - addrs: Vec, + addrs: Vec, expire: Instant, } @@ -124,14 +126,14 @@ impl Resolve for HttpResolver { let server = Arc::from(self.base_url.host_str().unwrap_or("")); let soource = Source::Http { server }; - use crate::parser::record; + use ddns_core::parser::record; self.cached_records .retain(|_host, Record { expire, .. }| *expire < now); if let Some(record) = self.cached_records.get(domain) { let endpoint_addrs: Vec<_> = record .addrs .iter() - .map(|e: &h3x::dquic::net::EndpointAddr| (soource.clone(), *e)) + .map(|endpoint: &EndpointAddr| (soource.clone(), *endpoint)) .collect(); return Ok(stream::iter(endpoint_addrs).boxed()); } diff --git a/gmdns-server/src/lookup.rs b/gmdns-server/src/lookup.rs index 45e40c5..74e45e6 100644 --- a/gmdns-server/src/lookup.rs +++ b/gmdns-server/src/lookup.rs @@ -3,13 +3,13 @@ use std::{ net::SocketAddr, }; +use deadpool_redis::redis::{self, AsyncCommands}; use futures::future::BoxFuture; use gmdns::{ MdnsPacket, parser::{packet::be_packet, record::RData}, }; use h3x::endpoint::server::{Request, Response, Service}; -use deadpool_redis::redis::{self, AsyncCommands}; use tracing::debug; use crate::{ diff --git a/gmdns-server/src/publish.rs b/gmdns-server/src/publish.rs index 2580976..3ff0494 100644 --- a/gmdns-server/src/publish.rs +++ b/gmdns-server/src/publish.rs @@ -1,9 +1,9 @@ +use deadpool_redis::redis::{self, AsyncCommands}; use futures::future::BoxFuture; use h3x::{ endpoint::server::{Request, Response, Service}, quic::agent::RemoteAgent, }; -use deadpool_redis::redis::{self, AsyncCommands}; use tokio::time::{Duration, Instant}; use tracing::{debug, info, warn}; @@ -217,7 +217,10 @@ pub async fn publish_record( } // Expire the ZSET key at max(ttl_secs) from now as a safety net. - let _: bool = conn.expire(&set_key, expire_ttl_secs).await.unwrap_or(false); + let _: bool = conn + .expire(&set_key, expire_ttl_secs) + .await + .unwrap_or(false); // Evict stale (score < now - ttl) entries. let cutoff = now_secs.saturating_sub(state.ttl_secs) as f64; diff --git a/gmdns/src/mdns.rs b/gmdns/src/mdns.rs index 73b8bda..2839316 100644 --- a/gmdns/src/mdns.rs +++ b/gmdns/src/mdns.rs @@ -7,9 +7,9 @@ use std::{ time::Duration, }; -use futures::{Stream, stream}; use ddns_core::parser::{packet::Packet, record::endpoint::EndpointAddr}; use dquic::qinterface::{Interface, component::Component, io::IO}; +use futures::{Stream, stream}; use tokio::{task::JoinSet, time}; use tracing::Instrument; diff --git a/gmdns/src/protocol.rs b/gmdns/src/protocol.rs index 25a84a4..b4e353d 100644 --- a/gmdns/src/protocol.rs +++ b/gmdns/src/protocol.rs @@ -8,15 +8,14 @@ use std::{ }; use dashmap::DashMap; -use futures::{Stream, StreamExt}; -use snafu::Snafu; -use socket2::{Domain, Socket, Type}; -use tokio::{io, net::UdpSocket, task::JoinSet, time}; - use ddns_core::parser::{ packet::{Packet, be_packet}, record::endpoint::EndpointAddr, }; +use futures::{Stream, StreamExt}; +use snafu::Snafu; +use socket2::{Domain, Socket, Type}; +use tokio::{io, net::UdpSocket, task::JoinSet, time}; use crate::if_nametoindex::if_nametoindex; diff --git a/gmdns/src/resolvers/mdns.rs b/gmdns/src/resolvers/mdns.rs index f915cb1..bc77708 100644 --- a/gmdns/src/resolvers/mdns.rs +++ b/gmdns/src/resolvers/mdns.rs @@ -5,16 +5,16 @@ use std::{ }; use dashmap::DashMap; -use futures::{ - FutureExt, Stream, StreamExt, TryFutureExt, future, - stream::{self, FuturesUnordered}, -}; use ddns_core::parser::{packet::Packet, record::RData}; use dquic::{ qbase::net::{Family, addr::EndpointAddr as DquicEndpointAddr}, qinterface::{BindInterface, WeakInterface, bind_uri::BindUri, io::IO}, qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }; +use futures::{ + FutureExt, Stream, StreamExt, TryFutureExt, future, + stream::{self, FuturesUnordered}, +}; pub use crate::mdns::Mdns as MdnsResolver; use crate::protocol::MdnsProtocol; @@ -38,11 +38,7 @@ impl fmt::Display for MdnsResolver { } impl Publish for MdnsResolver { - fn publish<'a>( - &'a self, - name: &'a str, - packet: &'a [u8], - ) -> PublishFuture<'a> { + fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { use ddns_core::parser::packet::be_packet; let endpoints = be_packet(packet) .map(|(_, pkt)| { diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 1f0af97..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod if_nametoindex; -pub mod mdns; -pub mod parser; -mod protocol; -pub mod resolvers; -pub mod wire; - -pub type MdnsEndpoint = crate::parser::record::endpoint::EndpointAddr; -pub type MdnsPacket = crate::parser::packet::Packet; - -#[cfg(feature = "h3x-resolver")] -pub use parser::record::endpoint::sign_endponit_address; From 22e13223ea93960eb594a98bda73ef833303773c Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:39:57 +0800 Subject: [PATCH 16/85] refactor: rename gmdns server to ddns-server --- Cargo.toml | 2 +- {gmdns-server => ddns-server}/Cargo.toml | 4 +- {gmdns-server => ddns-server}/server.toml | 2 +- {gmdns-server => ddns-server}/src/config.rs | 0 {gmdns-server => ddns-server}/src/error.rs | 0 {gmdns-server => ddns-server}/src/lookup.rs | 9 +- {gmdns-server => ddns-server}/src/main.rs | 2 +- {gmdns-server => ddns-server}/src/policy.rs | 2 +- {gmdns-server => ddns-server}/src/publish.rs | 0 {gmdns-server => ddns-server}/src/storage.rs | 91 -------------------- 10 files changed, 11 insertions(+), 101 deletions(-) rename {gmdns-server => ddns-server}/Cargo.toml (95%) rename {gmdns-server => ddns-server}/server.toml (97%) rename {gmdns-server => ddns-server}/src/config.rs (100%) rename {gmdns-server => ddns-server}/src/error.rs (100%) rename {gmdns-server => ddns-server}/src/lookup.rs (98%) rename {gmdns-server => ddns-server}/src/main.rs (99%) rename {gmdns-server => ddns-server}/src/policy.rs (98%) rename {gmdns-server => ddns-server}/src/publish.rs (100%) rename {gmdns-server => ddns-server}/src/storage.rs (67%) diff --git a/Cargo.toml b/Cargo.toml index 7dc97e6..03cf6d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["ddns-core", "gmdns", "ddns", "gmdns-server"] +members = ["ddns-core", "gmdns", "ddns", "ddns-server"] resolver = "2" [workspace.package] diff --git a/gmdns-server/Cargo.toml b/ddns-server/Cargo.toml similarity index 95% rename from gmdns-server/Cargo.toml rename to ddns-server/Cargo.toml index 55a8314..b93a207 100644 --- a/gmdns-server/Cargo.toml +++ b/ddns-server/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "gmdns-server" +name = "ddns-server" version = "0.2.0" edition = "2024" [[bin]] -name = "gmdns-server" +name = "ddns-server" path = "src/main.rs" [dependencies] diff --git a/gmdns-server/server.toml b/ddns-server/server.toml similarity index 97% rename from gmdns-server/server.toml rename to ddns-server/server.toml index 79d6b8c..ad72b76 100644 --- a/gmdns-server/server.toml +++ b/ddns-server/server.toml @@ -1,4 +1,4 @@ -# gmdns DNS-over-HTTP/3 Server configuration +# ddns DNS-over-HTTP/3 server configuration # All fields are optional; the values shown below are the built-in defaults. # Socket address to listen on. diff --git a/gmdns-server/src/config.rs b/ddns-server/src/config.rs similarity index 100% rename from gmdns-server/src/config.rs rename to ddns-server/src/config.rs diff --git a/gmdns-server/src/error.rs b/ddns-server/src/error.rs similarity index 100% rename from gmdns-server/src/error.rs rename to ddns-server/src/error.rs diff --git a/gmdns-server/src/lookup.rs b/ddns-server/src/lookup.rs similarity index 98% rename from gmdns-server/src/lookup.rs rename to ddns-server/src/lookup.rs index 74e45e6..8063fa4 100644 --- a/gmdns-server/src/lookup.rs +++ b/ddns-server/src/lookup.rs @@ -3,18 +3,19 @@ use std::{ net::SocketAddr, }; -use deadpool_redis::redis::{self, AsyncCommands}; -use futures::future::BoxFuture; -use gmdns::{ +use ddns::{ MdnsPacket, parser::{packet::be_packet, record::RData}, + wire::MultiResponse, }; +use deadpool_redis::redis::{self, AsyncCommands}; +use futures::future::BoxFuture; use h3x::endpoint::server::{Request, Response, Service}; use tracing::debug; use crate::{ error::{AppError, normalize_host, parse_query_params}, - storage::{AppState, LookupRecord, MultiResponse, Storage, StoredRecord, unix_now_secs}, + storage::{AppState, LookupRecord, Storage, StoredRecord, unix_now_secs}, }; // --------------------------------------------------------------------------- diff --git a/gmdns-server/src/main.rs b/ddns-server/src/main.rs similarity index 99% rename from gmdns-server/src/main.rs rename to ddns-server/src/main.rs index c5d5804..3da655e 100644 --- a/gmdns-server/src/main.rs +++ b/ddns-server/src/main.rs @@ -8,7 +8,7 @@ mod storage; use std::{collections::HashMap, io, net::SocketAddr, str::FromStr, sync::Arc}; use clap::Parser; -use gmdns::{MdnsEndpoint, MdnsPacket}; +use ddns::{MdnsEndpoint, MdnsPacket}; use h3x::{ dquic::{ Identity, Network, QuicEndpoint, diff --git a/gmdns-server/src/policy.rs b/ddns-server/src/policy.rs similarity index 98% rename from gmdns-server/src/policy.rs rename to ddns-server/src/policy.rs index 96094f8..14e5e35 100644 --- a/gmdns-server/src/policy.rs +++ b/ddns-server/src/policy.rs @@ -1,4 +1,4 @@ -use gmdns::parser::{packet::be_packet, record::RData}; +use ddns::parser::{packet::be_packet, record::RData}; use h3x::quic::agent::RemoteAgent; use tracing::warn; diff --git a/gmdns-server/src/publish.rs b/ddns-server/src/publish.rs similarity index 100% rename from gmdns-server/src/publish.rs rename to ddns-server/src/publish.rs diff --git a/gmdns-server/src/storage.rs b/ddns-server/src/storage.rs similarity index 67% rename from gmdns-server/src/storage.rs rename to ddns-server/src/storage.rs index b489387..e194faf 100644 --- a/gmdns-server/src/storage.rs +++ b/ddns-server/src/storage.rs @@ -122,97 +122,6 @@ pub fn be_stored_record(input: &[u8]) -> IResult<&[u8], StoredRecord> { )) } -// --------------------------------------------------------------------------- -// HTTP multi-record response wire type -// --------------------------------------------------------------------------- - -/// One DNS + certificate pair inside a [`MultiResponse`]. -#[derive(Debug, Clone)] -pub struct ResponseRecord { - /// Serialised DNS packet bytes. - pub dns: Vec, - /// DER-encoded leaf certificate of the publisher (may be empty). - pub cert: Vec, -} - -/// HTTP response body carrying zero or more DNS records. -/// -/// Wire layout (big-endian, contiguous): -/// ```text -/// +-----------+ (repeated `count` times) -/// | count | +-----------+------+-----------+------+ -/// | u32 BE | | dns_len | dns | cert_len | cert | -/// +-----------+ | u32 BE | ... | u32 BE | ... | -/// +-----------+------+-----------+------+ -/// ``` -#[derive(Debug, Clone)] -pub struct MultiResponse { - pub records: Vec, -} - -impl MultiResponse { - pub fn new(iter: impl IntoIterator, Vec)>) -> Self { - Self { - records: iter - .into_iter() - .map(|(dns, cert)| ResponseRecord { dns, cert }) - .collect(), - } - } - - pub fn encoding_size(&self) -> usize { - 4 + self - .records - .iter() - .map(|r| 4 + r.dns.len() + 4 + r.cert.len()) - .sum::() - } - - /// Encode to a byte buffer sent as the HTTP response body. - pub fn encode(&self) -> Vec { - let mut buf = Vec::with_capacity(self.encoding_size()); - buf.put_multi_response(self); - buf - } -} - -/// `BufMut` write extension for [`MultiResponse`]. -pub trait WriteMultiResponse { - fn put_multi_response(&mut self, resp: &MultiResponse); -} - -impl WriteMultiResponse for B { - fn put_multi_response(&mut self, resp: &MultiResponse) { - self.put_u32(resp.records.len() as u32); - for r in &resp.records { - self.put_u32(r.dns.len() as u32); - self.put_slice(&r.dns); - self.put_u32(r.cert.len() as u32); - self.put_slice(&r.cert); - } - } -} - -/// nom parser for [`MultiResponse`]. -/// Used by the client-side decoder; provided here to keep the wire format symmetric and testable. -#[allow(dead_code)] -pub fn be_multi_response(input: &[u8]) -> IResult<&[u8], MultiResponse> { - let (mut input, count) = be_u32(input)?; - let mut records = Vec::with_capacity(count as usize); - for _ in 0..count { - let (rest, dns_len) = be_u32(input)?; - let (rest, dns) = take(dns_len as usize)(rest)?; - let (rest, cert_len) = be_u32(rest)?; - let (rest, cert) = take(cert_len as usize)(rest)?; - records.push(ResponseRecord { - dns: dns.to_vec(), - cert: cert.to_vec(), - }); - input = rest; - } - Ok((input, MultiResponse { records })) -} - // --------------------------------------------------------------------------- // Storage // --------------------------------------------------------------------------- From ed6bf8370da5655936e46215bec035ed05d8b842 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 18 May 2026 17:41:09 +0800 Subject: [PATCH 17/85] docs: update crate split documentation --- README.md | 73 ++++++++++++++++++++--------------------- ddns/examples/README.md | 16 ++------- 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 1f50207..c253a2f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# GMDNS +# DDNS / GMDNS -GMDNS is a high-performance mDNS (Multicast DNS) protocol library built with Rust, specifically designed for P2P network discovery and NAT traversal scenarios. It supports the standard RFC 6762 protocol while extending endpoint discovery capabilities through custom resource records, enabling publication and verification of both direct and relay addresses. Additionally, it integrates HTTP/3 support for secure DNS over HTTP/3 (DoH3) interactions with remote DNS servers. +This workspace provides DNS discovery crates for the DHTTP ecosystem: + +| Crate | Role | +| --- | --- | +| `ddns-core` | DNS packet parser, endpoint `E` record, and shared wire types. | +| `gmdns` | RFC 6762 multicast DNS transport and LAN resolver/publisher. | +| `ddns` | Facade crate combining `ddns-core`, `gmdns`, and optional HTTP/3/HTTP resolvers. | +| `ddns-server` | DNS-over-HTTP/3 publish/lookup server binary. | + +`gmdns` is the local multicast DNS layer. `ddns` is the high-level crate to use when an application needs both LAN mDNS and remote DNS-over-HTTP/3 resolver support. ## 🌟 Key Features @@ -17,21 +26,28 @@ Add to your `Cargo.toml`: ```toml [dependencies] -gmdns = { path = "../gmdns" } +ddns = { path = "./ddns" } ``` -For HTTP/3 features, enable the `h3x-resolver` feature: +For mDNS-only use, depend directly on `gmdns`: ```toml [dependencies] -gmdns = { path = "../gmdns", features = ["h3x-resolver"] } +gmdns = { path = "./gmdns" } +``` + +For HTTP/3 resolver/publisher support, enable the `h3x-resolver` feature on `ddns`: + +```toml +[dependencies] +ddns = { path = "./ddns", features = ["h3x-resolver"] } ``` ### Simple mDNS Discovery Example ```rust -use gmdns::mdns::Mdns; use futures::StreamExt; +use gmdns::Mdns; #[tokio::main] async fn main() -> Result<(), std::io::Error> { @@ -50,38 +66,21 @@ async fn main() -> Result<(), std::io::Error> { ### HTTP/3 DNS Publishing Example ```rust -use gmdns::{resolver::h3_resolver::H3Resolver, parser::record::endpoint::EndpointAddr}; -use std::path::Path; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let resolver = H3Resolver::new( - "https://localhost:4433/", - Path::new("examples/keychain/localhost/ca.cert"), - "client", - Path::new("examples/keychain/localhost/client.cert"), - Path::new("examples/keychain/localhost/client.key"), - )?; - - // Publish a DNS record - let endpoint = EndpointAddr::direct_v4("127.0.0.1:5555".parse()?); - resolver.publish("client.genmeta.net", &[endpoint]).await?; - Ok(()) -} +// See ddns/examples/publish.rs for a complete mTLS HTTP/3 publisher. ``` --- ## 🌐 HTTP/3 DNS Server -GMDNS includes support for DNS over HTTP/3 (DoH3), allowing secure publication and querying of DNS records via HTTP/3 protocol. This is useful for remote networks where multicast mDNS is not feasible. +`ddns` includes support for DNS over HTTP/3 (DoH3), allowing secure publication and querying of DNS records via HTTP/3 protocol. This is useful for remote networks where multicast mDNS is not feasible. ### Publishing Services Publish DNS service records to an HTTP/3 DNS server: ```bash -cargo run --example publish --features="h3x-resolver" \ +cargo run -p ddns --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --client-name demo.example.genmeta.net \ --client-cert /path/to/demo.example.genmeta.net.pem \ @@ -95,7 +94,7 @@ cargo run --example publish --features="h3x-resolver" \ Query DNS service records from an HTTP/3 DNS server: ```bash -cargo run --example query --features="h3x-resolver" \ +cargo run -p ddns --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --host stun.genmeta.net ``` @@ -105,13 +104,10 @@ cargo run --example query --features="h3x-resolver" \ Start an HTTP/3 DNS server: ```bash -cargo run --example server --features="h3x-resolver" \ - --listen 127.0.0.1:4433 \ - --cert examples/keychain/localhost/server.cert \ - --key examples/keychain/localhost/server.key +cargo run -p ddns-server -- --config ddns-server/server.toml ``` -For detailed parameters and HTTP packet structures, see [examples/README.md](examples/README.md). +For detailed parameters and HTTP packet structures, see [ddns/examples/README.md](ddns/examples/README.md). --- @@ -201,8 +197,11 @@ When signature is present: `Scheme (u16)` + `Length (VarInt)` + `Data (N bytes)` ## 🛠 Project Structure -- `src/parser/`: Core protocol parsing implementation (Nom parsers). -- `src/protocol.rs`: UDP multicast and packet routing logic. -- `src/mdns.rs`: High-level mDNS discovery and response API. -- `src/resolver/`: HTTP/3 resolver implementation for DoH3 support. -- `examples/`: Sample code including mDNS discovery/query, and HTTP/3 publishing/querying/server examples. +- `ddns-core/src/parser/`: Core protocol parsing implementation (Nom parsers). +- `ddns-core/src/wire.rs`: Shared HTTP multi-record response wire format. +- `gmdns/src/protocol.rs`: UDP multicast and packet routing logic. +- `gmdns/src/mdns.rs`: High-level mDNS discovery and response API. +- `gmdns/src/resolvers/`: LAN mDNS resolver implementation. +- `ddns/src/resolvers/`: Facade resolver chain plus optional HTTP/3 and HTTP resolvers. +- `ddns/examples/`: mDNS discovery/query and HTTP/3 publish/query examples. +- `ddns-server/`: DNS-over-HTTP/3 server binary and configuration. diff --git a/ddns/examples/README.md b/ddns/examples/README.md index e5a20a2..1303a00 100644 --- a/ddns/examples/README.md +++ b/ddns/examples/README.md @@ -86,24 +86,14 @@ This command sends a GET or POST request to the server, the request body contain ### Running the DNS Server (server) -Use the `server` example to start an HTTP/3 DNS server. +Use the `ddns-server` binary to start an HTTP/3 DNS server. #### Program Parameters -- `--redis `: Optional Redis connection URL for persistent storage (default: none, uses in-memory storage). -- `--listen `: Server listen address (default: `127.0.0.1:4433`). -- `--server-name `: Server name (default: `localhost`). -- `--cert `: Server certificate PEM file (default: `examples/keychain/localhost/server.cert`). -- `--key `: Server private key PEM file (default: `examples/keychain/localhost/server.key`). -- `--root-cert `: Root CA certificate PEM file (default: `examples/keychain/localhost/ca.cert`). -- `--require-signature`: Whether to require client-signed records (default: true). -- `--ttl-secs `: TTL time for records in seconds (default: 30). +- `--config `: TOML configuration file path (default: `server.toml`). #### Example Run Command ```bash -cargo run -p ddns-server -- --features="h3x-resolver" \ - --listen 127.0.0.1:4433 \ - --cert examples/keychain/localhost/server.cert \ - --key examples/keychain/localhost/server.key +cargo run -p ddns-server -- --config ddns-server/server.toml ``` After the server starts, it listens for HTTP/3 requests and handles publish and query operations. From 174f63ed36565d58a222a43400f40405a06e6a4e Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 16:40:00 +0800 Subject: [PATCH 18/85] feat(resolvers): move mdns network integration to gmdns --- Cargo.toml | 4 + ddns/Cargo.toml | 1 + ddns/src/lib.rs | 9 +- ddns/src/resolvers.rs | 175 +++++++++++++++++++++++++- ddns/src/resolvers/h3.rs | 9 +- gmdns/Cargo.toml | 5 + gmdns/src/lib.rs | 4 +- gmdns/src/mdns.rs | 10 +- gmdns/src/resolvers.rs | 4 +- gmdns/src/resolvers/mdns.rs | 242 ++++++++++++++++++++++++++---------- 10 files changed, 385 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03cf6d5..ed19484 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,7 @@ edition = "2024" [patch."https://github.com/genmeta/h3x.git"] h3x = { path = "../h3x" } + +[patch."ssh://git@github.com/genmeta/ddns.git"] +ddns-core = { path = "ddns-core" } +gmdns = { path = "gmdns" } diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml index 9500296..4476fb5 100644 --- a/ddns/Cargo.toml +++ b/ddns/Cargo.toml @@ -25,6 +25,7 @@ url = { version = "2", optional = true } [features] default = [] h3x-resolver = ["dep:h3x", "dep:http", "dep:url"] +mdns-resolver = ["dep:h3x", "gmdns/h3x-network"] http-resolver = ["dep:reqwest"] [dev-dependencies] diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs index 7276bbb..853c95f 100644 --- a/ddns/src/lib.rs +++ b/ddns/src/lib.rs @@ -1,9 +1,14 @@ pub mod resolvers; pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; -pub use gmdns::{Mdns, MdnsResolver, MdnsResolvers, mdns}; +pub use gmdns::{Mdns, MdnsResolver, mdns}; #[cfg(feature = "http-resolver")] pub use resolvers::HttpResolver; -pub use resolvers::{DnsErrors, Resolvers}; +#[cfg(feature = "mdns-resolver")] +pub use resolvers::MdnsResolvers; +pub use resolvers::{ + DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsErrors, DnsScheme, + ParseDnsSchemeError, Resolvers, ResolversBuilder, +}; #[cfg(feature = "h3x-resolver")] pub use resolvers::{H3Publisher, H3Resolver}; diff --git a/ddns/src/resolvers.rs b/ddns/src/resolvers.rs index e31bd52..055e48b 100644 --- a/ddns/src/resolvers.rs +++ b/ddns/src/resolvers.rs @@ -33,7 +33,59 @@ pub(crate) fn resolvable_name(name: &str) -> Option<&str> { Some(host) } -pub use gmdns::resolvers::{MdnsResolver, MdnsResolvers}; +/// Default DNS-over-H3 server for DHTTP endpoints. +pub const DHTTP_H3_DNS_SERVER: &str = "https://dns.genmeta.net:4433"; + +/// Default DNS-over-HTTP server for DHTTP endpoints. +pub const DHTTP_HTTP_DNS_SERVER: &str = "https://dns.genmeta.net"; + +/// mDNS service type used by DHTTP endpoints. +pub const DHTTP_MDNS_SERVICE: &str = "_genmeta.local"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum DnsScheme { + Mdns, + Http, + H3, + System, +} + +impl Display for DnsScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Mdns => "mdns", + Self::Http => "http", + Self::H3 => "h3", + Self::System => "system", + }) + } +} + +#[derive(Debug, snafu::Snafu)] +#[snafu(display("unsupported dns scheme {scheme}"))] +pub struct ParseDnsSchemeError { + scheme: String, +} + +impl std::str::FromStr for DnsScheme { + type Err = ParseDnsSchemeError; + + fn from_str(s: &str) -> Result { + match s { + "mdns" => Ok(Self::Mdns), + "http" => Ok(Self::Http), + "h3" => Ok(Self::H3), + "system" => Ok(Self::System), + scheme => Err(ParseDnsSchemeError { + scheme: scheme.to_owned(), + }), + } + } +} + +pub use gmdns::resolvers::MdnsResolver; +#[cfg(feature = "mdns-resolver")] +pub use gmdns::resolvers::MdnsResolvers; #[cfg(feature = "h3x-resolver")] pub use h3::{H3Publisher, H3Resolver}; #[cfg(feature = "http-resolver")] @@ -83,7 +135,62 @@ impl fmt::Display for DnsErrors { impl Error for DnsErrors {} +#[derive(Default)] +pub struct ResolversBuilder { + resolvers: Resolvers, +} + +impl ResolversBuilder { + #[cfg(feature = "mdns-resolver")] + pub async fn mdns( + mut self, + network: Arc, + patterns: Arc>, + ) -> Self { + let mdns = Arc::new(MdnsResolvers::bind(network, patterns, DHTTP_MDNS_SERVICE).await); + self.resolvers = self.resolvers.with(mdns); + self + } + + #[cfg(feature = "h3x-resolver")] + pub fn h3( + mut self, + endpoint: Arc>, + ) -> io::Result + where + C: h3x::quic::Connect + Send + Sync + 'static, + C::Error: Send + Sync + 'static, + C::Connection: Send + 'static, + { + let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint)?; + self.resolvers = self.resolvers.with(Arc::new(resolver)); + Ok(self) + } + + #[cfg(feature = "http-resolver")] + pub fn http(mut self) -> io::Result { + let resolver = HttpResolver::new(DHTTP_HTTP_DNS_SERVER)?; + self.resolvers = self.resolvers.with(Arc::new(resolver)); + Ok(self) + } + + pub fn system(mut self) -> Self { + self.resolvers = self + .resolvers + .with(Arc::new(dquic::qresolve::SystemResolver)); + self + } + + pub fn build(self) -> Resolvers { + self.resolvers + } +} + impl Resolvers { + pub fn builder() -> ResolversBuilder { + ResolversBuilder::default() + } + pub fn new() -> Self { Self::default() } @@ -130,7 +237,11 @@ impl Resolve for Resolvers { #[cfg(test)] mod tests { - use super::resolvable_name; + use std::str::FromStr; + + #[cfg(feature = "mdns-resolver")] + use super::{DHTTP_MDNS_SERVICE, MdnsResolvers, Resolvers}; + use super::{DnsScheme, resolvable_name}; #[test] fn resolvable_name_accepts_dns_name_with_numeric_port() { @@ -145,4 +256,64 @@ mod tests { assert_eq!(resolvable_name("127.0.0.1:443"), None); assert_eq!(resolvable_name("[::1]:443"), None); } + + #[test] + fn dns_scheme_round_trips_supported_schemes_and_rejects_dht() { + let cases = [ + ("mdns", DnsScheme::Mdns), + ("http", DnsScheme::Http), + ("h3", DnsScheme::H3), + ("system", DnsScheme::System), + ]; + + for (text, scheme) in cases { + assert_eq!(DnsScheme::from_str(text).expect("supported scheme"), scheme); + assert_eq!(scheme.to_string(), text); + } + + assert!(DnsScheme::from_str("dht").is_err()); + } + + #[cfg(feature = "mdns-resolver")] + #[tokio::test] + async fn resolvers_builder_can_enable_mdns() { + use std::sync::Arc; + + use h3x::dquic::{Network, binds::BindPattern}; + + let network = Network::builder().build(); + let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern"); + + let resolvers = Resolvers::builder() + .mdns(network, Arc::new(vec![pattern])) + .await + .build(); + + assert!(resolvers.to_string().contains("mDNS resolvers")); + } + + #[cfg(feature = "mdns-resolver")] + #[tokio::test] + async fn mdns_resolvers_bind_installs_mdns_on_null_io_binding() { + use std::sync::Arc; + + use dquic::qinterface::io::IO; + use h3x::dquic::{Network, binds::BindPattern}; + + let network = Network::builder().build(); + let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern"); + let resolvers = MdnsResolvers::bind( + network.clone(), + Arc::new(vec![pattern.clone()]), + DHTTP_MDNS_SERVICE, + ) + .await; + + let ifaces = resolvers + .bound_interfaces(&pattern) + .expect("bound interfaces"); + assert!(!ifaces.is_empty()); + assert!(ifaces[0].borrow().bound_addr().is_err()); + assert!(ifaces[0].with_components(|components, _| components.exist::())); + } } diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 96e0dad..aa38eda 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -78,6 +78,13 @@ where pub fn new( base_url: impl AsRef, client: H3Endpoint, + ) -> io::Result { + Self::from_endpoint(base_url, Arc::new(client)) + } + + pub fn from_endpoint( + base_url: impl AsRef, + endpoint: Arc>, ) -> io::Result { let base_url = Url::parse(base_url.as_ref()) .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; @@ -89,7 +96,7 @@ where })?; Ok(Self { - endpoint: Arc::new(client), + endpoint, base_url, cached_records: DashMap::new(), negative_cache: DashMap::new(), diff --git a/gmdns/Cargo.toml b/gmdns/Cargo.toml index eb5b101..3a116ce 100644 --- a/gmdns/Cargo.toml +++ b/gmdns/Cargo.toml @@ -9,8 +9,13 @@ ddns-core = { path = "../ddns-core" } dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } flume = "0.12" futures = "0.3" +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"], optional = true } libc = "0.2" snafu = "0.8" socket2 = { version = "0.5.8", features = ["all"] } tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } tracing = "0.1" + +[features] +default = [] +h3x-network = ["dep:h3x"] diff --git a/gmdns/src/lib.rs b/gmdns/src/lib.rs index 46facd1..91bc5f3 100644 --- a/gmdns/src/lib.rs +++ b/gmdns/src/lib.rs @@ -4,4 +4,6 @@ mod protocol; pub mod resolvers; pub use mdns::Mdns; -pub use resolvers::{MdnsResolver, MdnsResolvers}; +pub use resolvers::MdnsResolver; +#[cfg(feature = "h3x-network")] +pub use resolvers::{MdnsBindDriver, MdnsResolvers}; diff --git a/gmdns/src/mdns.rs b/gmdns/src/mdns.rs index 2839316..2a73589 100644 --- a/gmdns/src/mdns.rs +++ b/gmdns/src/mdns.rs @@ -77,8 +77,6 @@ impl Mdns { } pub fn reinit(&self, iface: &(impl IO + ?Sized)) { - // Extract interface info - let binding = iface.bind_uri(); let Some((_family, device, _port)) = binding.as_iface_bind_uri() else { return; @@ -86,11 +84,12 @@ impl Mdns { let Ok(bound_addr) = iface.bound_addr() else { return; }; - let ip = bound_addr.ip(); - let mut inner = self.inner.lock().expect("Mdns inner lock poisoned"); + self.reinit_on(device, bound_addr.ip()); + } - // Skip if already using same device/IP with active protocol + pub fn reinit_on(&self, device: &str, ip: IpAddr) { + let mut inner = self.inner.lock().expect("Mdns inner lock poisoned"); if inner.proto.bound_nic() == device && inner.proto.bound_ip() == ip { return; @@ -113,7 +112,6 @@ impl Mdns { self.hosts.clone(), self.service_name.clone(), ); - // Update state with new protocol and tasks } fn spawn_tasks( diff --git a/gmdns/src/resolvers.rs b/gmdns/src/resolvers.rs index 752f034..574de2b 100644 --- a/gmdns/src/resolvers.rs +++ b/gmdns/src/resolvers.rs @@ -1,3 +1,5 @@ mod mdns; -pub use mdns::{MdnsResolver, MdnsResolvers}; +pub use mdns::MdnsResolver; +#[cfg(feature = "h3x-network")] +pub use mdns::{MdnsBindDriver, MdnsResolvers}; diff --git a/gmdns/src/resolvers/mdns.rs b/gmdns/src/resolvers/mdns.rs index bc77708..97b07a1 100644 --- a/gmdns/src/resolvers/mdns.rs +++ b/gmdns/src/resolvers/mdns.rs @@ -1,22 +1,22 @@ -use std::{ - fmt, io, - net::{IpAddr, SocketAddr}, - sync::Arc, -}; +use std::{fmt, io, net::IpAddr}; +#[cfg(feature = "h3x-network")] +use std::{net::SocketAddr, sync::Arc}; -use dashmap::DashMap; -use ddns_core::parser::{packet::Packet, record::RData}; +#[cfg(feature = "h3x-network")] +use ddns_core::parser::packet::Packet; +use ddns_core::parser::record::RData; +#[cfg(feature = "h3x-network")] +use dquic::qresolve::RecordStream; use dquic::{ qbase::net::{Family, addr::EndpointAddr as DquicEndpointAddr}, - qinterface::{BindInterface, WeakInterface, bind_uri::BindUri, io::IO}, - qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, -}; -use futures::{ - FutureExt, Stream, StreamExt, TryFutureExt, future, - stream::{self, FuturesUnordered}, + qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}, }; +use futures::{FutureExt, StreamExt, TryFutureExt, future, stream}; +#[cfg(feature = "h3x-network")] +use futures::{Stream, stream::FuturesUnordered}; pub use crate::mdns::Mdns as MdnsResolver; +#[cfg(feature = "h3x-network")] use crate::protocol::MdnsProtocol; impl MdnsResolver { @@ -39,20 +39,12 @@ impl fmt::Display for MdnsResolver { impl Publish for MdnsResolver { fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { - use ddns_core::parser::packet::be_packet; - let endpoints = be_packet(packet) - .map(|(_, pkt)| { - pkt.answers - .iter() - .filter_map(|rr| match rr.data() { - RData::E(ep) => Some(ep.clone()), - _ => None, - }) - .collect::>() - }) - .unwrap_or_default(); + let endpoints = match endpoints_from_packet(packet) { + Ok(endpoints) => endpoints, + Err(error) => return future::ready(Err(error)).boxed(), + }; self.insert_host(name.to_string(), endpoints); - Box::pin(future::ready(Ok(()))) + future::ready(Ok(())).boxed() } } @@ -71,43 +63,161 @@ impl Resolve for MdnsResolver { } } -#[derive(Default, Clone, Debug)] +fn endpoints_from_packet(packet: &[u8]) -> io::Result> { + use ddns_core::parser::packet::be_packet; + + be_packet(packet) + .map(|(_, pkt)| { + pkt.answers + .iter() + .filter_map(|rr| match rr.data() { + RData::E(ep) => Some(ep.clone()), + _ => None, + }) + .collect::>() + }) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string())) +} + +#[cfg(feature = "h3x-network")] +pub struct MdnsBindDriver { + iface_manager: Arc, + null_io_factory: Arc, + service_name: Arc, +} + +#[cfg(feature = "h3x-network")] +impl MdnsBindDriver { + pub fn new(service_name: impl Into>) -> Self { + Self { + iface_manager: Arc::new(h3x::dquic::net::InterfaceManager::new()), + null_io_factory: Arc::new(h3x::dquic::NullIoFactory), + service_name: service_name.into(), + } + } + + fn install_or_rebind_mdns( + &self, + network: &h3x::dquic::Network, + bind_iface: &h3x::dquic::net::BindInterface, + ) { + let bind_uri = bind_iface.bind_uri(); + let Some((family, device, _port)) = bind_uri.as_iface_bind_uri() else { + tracing::debug!(%bind_uri, "skipping mdns binding for non-interface bind uri"); + return; + }; + let Some(ip) = network.resolve_device_addr(device, family) else { + tracing::debug!(%bind_uri, "skipping mdns binding without local interface address"); + return; + }; + + bind_iface.with_components_mut(|components, _iface| { + match components.try_init_with(|| crate::Mdns::new(&self.service_name, ip, device)) { + Ok(mdns) => mdns.reinit_on(device, ip), + Err(error) => { + let report = snafu::Report::from_error(&error); + tracing::debug!(error = %report, %bind_uri, "failed to initialize mdns binding"); + } + } + }); + } +} + +#[cfg(feature = "h3x-network")] +impl h3x::dquic::BindDriver for MdnsBindDriver { + fn bind<'a>( + &'a self, + network: &'a h3x::dquic::Network, + uri: h3x::dquic::net::BindUri, + ) -> futures::future::BoxFuture<'a, h3x::dquic::net::BindInterface> { + async move { + let iface = self + .iface_manager + .bind(uri, self.null_io_factory.clone()) + .await; + self.install_or_rebind_mdns(network, &iface); + iface + } + .boxed() + } + + fn rebind<'a>( + &'a self, + network: &'a h3x::dquic::Network, + iface: &'a h3x::dquic::net::BindInterface, + ) -> futures::future::BoxFuture<'a, ()> { + async move { + self.install_or_rebind_mdns(network, iface); + } + .boxed() + } +} + +#[cfg(feature = "h3x-network")] pub struct MdnsResolvers { - ifaces: DashMap, + network: Arc, + driver: Arc, + patterns: Arc>, + _handles: Vec, +} + +#[cfg(feature = "h3x-network")] +impl fmt::Debug for MdnsResolvers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MdnsResolvers") + .field("patterns", &self.patterns) + .finish_non_exhaustive() + } } +#[cfg(feature = "h3x-network")] impl fmt::Display for MdnsResolvers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "MDNS Resolvers") + f.write_str("mDNS resolvers") } } +#[cfg(feature = "h3x-network")] impl MdnsResolvers { - pub fn new() -> Self { - Self::default() + pub async fn bind( + network: Arc, + patterns: Arc>, + service_name: impl Into>, + ) -> Self { + let driver = Arc::new(MdnsBindDriver::new(service_name)); + let mut handles = Vec::with_capacity(patterns.len()); + for pattern in patterns.iter() { + handles.push(network.bind_with(driver.clone(), pattern.clone()).await); + } + + Self { + network, + driver, + patterns, + _handles: handles, + } } - pub fn insert_iface(&self, iface: BindInterface) { - let Some(iface) = iface.with_components(|component, iface| { - component.exist::().then(|| iface.downgrade()) - }) else { - return; - }; - self.ifaces.insert(iface.bind_uri(), iface); + pub fn bound_interfaces( + &self, + pattern: &h3x::dquic::binds::BindPattern, + ) -> Option> { + self.network.get_interfaces_with(&self.driver, pattern) } fn for_each_resolver(&self, mut f: impl FnMut(&MdnsResolver)) { - self.ifaces.retain(|_, iface| { - iface - .upgrade() - .ok() - .and_then(|iface| { - iface.bind_interface().with_components(|components, _| { - components.get::().map(&mut f) - }) - }) - .is_some() - }); + for pattern in self.patterns.iter() { + let Some(ifaces) = self.bound_interfaces(pattern) else { + continue; + }; + for iface in ifaces { + iface.with_components(|components, _| { + if let Some(mdns) = components.get::() { + f(mdns); + } + }); + } + } } pub async fn query(&self, name: &str) -> io::Result { @@ -137,19 +247,7 @@ impl MdnsResolvers { .boxed()) } - pub fn merge(&self, other: &Self) { - other.ifaces.iter().for_each(|entry| { - self.ifaces - .entry(entry.key().clone()) - .or_insert_with(|| entry.value().clone()); - }); - } - /// Discover mDNS broadcasts from all active resolvers. - /// - /// Returns a stream of `(SocketAddr, Packet)` pairs by polling all - /// underlying protocols concurrently. Unlike per-resolver `discover()`, - /// this uses a single `Box::pin` allocation for the combined stream. pub fn discover(&self) -> impl Stream + use<> { let mut protos = Vec::new(); self.for_each_resolver(|resolver| { @@ -176,10 +274,7 @@ impl MdnsResolvers { pending.push(receive_one(proto)); return Poll::Ready(Some(item)); } - Poll::Ready(Some(None)) => { - // This resolver's protocol disconnected, skip it - continue; - } + Poll::Ready(Some(None)) => continue, Poll::Ready(None) => return Poll::Ready(None), Poll::Pending => return Poll::Pending, } @@ -188,6 +283,23 @@ impl MdnsResolvers { } } +#[cfg(feature = "h3x-network")] +impl Publish for MdnsResolvers { + fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { + let endpoints = match endpoints_from_packet(packet) { + Ok(endpoints) => endpoints, + Err(error) => return future::ready(Err(error)).boxed(), + }; + + self.for_each_resolver(|resolver| { + resolver.insert_host(name.to_string(), endpoints.clone()); + }); + + future::ready(Ok(())).boxed() + } +} + +#[cfg(feature = "h3x-network")] impl Resolve for MdnsResolvers { fn lookup<'l>(&'l self, name: &'l str) -> ResolveFuture<'l> { self.query(name).boxed() From bfcd36ec4d0560549c0ef8803dedbb4b19f4c4f9 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 17:01:58 +0800 Subject: [PATCH 19/85] docs: design ddns publisher --- .../plans/2026-05-19-ddns-publisher.md | 72 +++++++++++++++++++ .../specs/2026-05-19-ddns-publisher-design.md | 34 +++++++++ 2 files changed, 106 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-19-ddns-publisher.md create mode 100644 docs/superpowers/specs/2026-05-19-ddns-publisher-design.md diff --git a/docs/superpowers/plans/2026-05-19-ddns-publisher.md b/docs/superpowers/plans/2026-05-19-ddns-publisher.md new file mode 100644 index 0000000..2f7717b --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-ddns-publisher.md @@ -0,0 +1,72 @@ +# DDNS Publisher Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add signed DNS publishing for DHTTP endpoints using async identity agents and concrete ddns publishers. + +**Architecture:** `dhttp-identity` owns async agent traits and signature helpers. `ddns-core` signs endpoint records through `LocalAgent`. `ddns` owns `Publisher`, discovers concrete publishers by downcasting, and publishes signed packets. `dhttp::Endpoint` provides the convenience constructor. + +**Tech Stack:** Rust 2024, snafu, futures BoxFuture, dhttp-identity, ddns-core, ddns, h3x/dquic resolver traits. + +--- + +### Task 1: Move async agent traits into dhttp-identity + +**Files:** +- Modify: `dhttp/identity/src/identity.rs` +- Modify: `dhttp/identity/src/lib.rs` +- Modify: `h3x/src/quic/agent.rs` +- Modify: h3x call sites importing `h3x::quic::agent::{LocalAgent, RemoteAgent, SignError, VerifyError}` + +- [ ] Add tests in `dhttp/identity/src/identity.rs` for `Identity` implementing async `LocalAgent` and sync-default `RemoteAgent` verification behavior. +- [ ] Move `LocalAgent`, `RemoteAgent`, `extract_public_key`, `verify_signature`, and `sign_with_key` equivalents into `dhttp-identity` while preserving async signatures. +- [ ] Re-export the identity agent API from `h3x::quic::agent` to minimize h3x call-site churn. +- [ ] Run `cargo test -p dhttp-identity` and `cargo test --features dquic` in h3x. + +### Task 2: Replace ddns-core SigningKey signing with LocalAgent signing + +**Files:** +- Modify: `ddns/ddns-core/Cargo.toml` +- Modify: `ddns/ddns-core/src/parser/record/endpoint.rs` +- Modify: `ddns/ddns-core/src/parser/sigin.rs` + +- [ ] Add a failing async test for signing an `EndpointAddr` through a fake `LocalAgent` that rejects the first preferred compatible scheme and accepts the next one. +- [ ] Add `EndpointAddr::sign_with_agent(&mut self, agent: &(impl LocalAgent + ?Sized)) -> impl Future>`. +- [ ] Delete old `EndpointAddr::sign_with(SigningKey, SignatureScheme)` and update tests/examples to use the async agent method. +- [ ] Keep verification logic unchanged except for imports. +- [ ] Run `cargo test -p ddns-core`. + +### Task 3: Implement ddns Publisher + +**Files:** +- Create: `ddns/ddns/src/publisher.rs` +- Modify: `ddns/ddns/src/lib.rs` +- Modify: `ddns/ddns/src/resolvers.rs` +- Modify: `ddns/gmdns/src/resolvers/mdns.rs` if mDNS needs a public binding iterator + +- [ ] Add tests for `NoPublisherResolver`, downcast discovery, and mDNS same-device/same-family address filtering. +- [ ] Implement `Publisher` with non-optional identity, network, resolver, bind patterns, and 20s default interval. +- [ ] Implement `publish_once` to build signed packets and publish via concrete H3, HTTP, and mDNS publishers discovered through `Any` downcasts. +- [ ] Implement `run` as an infinite async loop that logs warning reports and sleeps 20 seconds between attempts. +- [ ] Run `cargo test --workspace --all-features` in ddns. + +### Task 4: Add dhttp Endpoint publisher entry point + +**Files:** +- Modify: `dhttp/dhttp/src/endpoint.rs` +- Modify: `dhttp/dhttp/src/lib.rs` if re-export plumbing is needed + +- [ ] Add a test or compile-time assertion for anonymous endpoint returning `CreatePublisherError::AnonymousEndpoint`. +- [ ] Implement `Endpoint::publisher(&self) -> Result`. +- [ ] Run `cargo test --workspace` in dhttp. + +### Task 5: Verification and commits + +**Files:** +- All modified files above + +- [ ] Run `cargo fmt` in dhttp, h3x, and ddns. +- [ ] Run `cargo clippy --all-targets --all-features -- -D warnings` in dhttp and ddns. +- [ ] Run `cargo clippy --all-targets --features "dquic,hyper,serde,webtransport,testing" -- -D warnings` in h3x. +- [ ] Run relevant cargo tests in all touched repos. +- [ ] Commit each independent repo with a semantic message. diff --git a/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md b/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md new file mode 100644 index 0000000..474eef1 --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md @@ -0,0 +1,34 @@ +# DDNS Publisher Design + +## Goal + +Add a reusable DNS publisher for DHTTP endpoints. The publisher signs endpoint records with the endpoint identity and publishes them through concrete DNS publishers discovered from the endpoint resolver set. + +## Decisions + +- `LocalAgent` and `RemoteAgent` are identity-layer concepts. Move their async trait definitions to `dhttp-identity`; `Identity` implements them without adding generics to `Identity`. +- `LocalAgent` remains an async signing API. It is an async/remote-capable counterpart of `rustls::sign::SigningKey`, not a replacement with synchronous signing. +- DNS publisher code lives in the `ddns` crate. `dhttp::Endpoint` only exposes a convenience method that constructs a `ddns::Publisher` from endpoint state. +- `Endpoint::publisher()` returns `Result` because anonymous endpoints cannot publish signed DNS records. `Publisher` stores a non-optional identity. +- `EndpointAddr::sign_with(SigningKey, scheme)` is removed. Endpoint record signing uses `dhttp_identity::LocalAgent`. +- Signature scheme selection follows the existing `pick_signature_scheme` preference order: Ed25519, ECDSA P-256, ECDSA P-384, RSA-PSS SHA-256/384/512, RSA-PKCS1 SHA-256/384/512. The async agent API is not expanded with `choose_scheme`; signing tries compatible schemes and treats `UnsupportedScheme` as a cue to try the next candidate. +- `publish_once` returns an error for the first failed publish attempt. `run` publishes every 20 seconds, logs warnings on failures with `snafu::Report`, and does not retain failure state. +- `NoPublisherResolver` is built at publish time when no concrete publisher can be found by downcasting. +- There is no `Resolvers::publish`; publishing is resolver-specific and uses `Any` downcasting. + +## Data Flow + +`dhttp::Endpoint::publisher()` clones the endpoint identity, network, resolver, and bind patterns into `ddns::Publisher`. `Publisher::publish_once()` collects endpoint addresses, builds signed DNS packets, and dispatches them to concrete publishers: + +- H3 and HTTP publishers receive public/STUN-derived endpoint addresses. +- mDNS publishers receive only local QUIC addresses on the same network device and IP family as the mDNS binding. + +Each DNS packet is independently signed with the endpoint identity before publication. + +## Error Handling + +Errors are typed with `snafu`. Display messages are lower-case fragments and do not repeat source errors. `publish_once` returns structured errors; `run` logs `snafu::Report` and continues. + +## Testing + +Unit tests cover agent-based endpoint signing, signature scheme fallback, anonymous endpoint publisher construction, missing publisher reporting, and mDNS address scoping helpers. Workspace tests and clippy run before commits. From b2f7b5ad38ed88a973dd554dd260c7aa2d1fc1a3 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 17:21:05 +0800 Subject: [PATCH 20/85] feat(publisher): add signed dns publishing --- Cargo.toml | 6 + ddns-core/Cargo.toml | 2 + ddns-core/src/parser/record/endpoint.rs | 161 ++++++++- ddns-core/src/parser/sigin.rs | 5 +- ddns-server/Cargo.toml | 1 + ddns-server/src/policy.rs | 2 +- ddns-server/src/publish.rs | 6 +- ddns/Cargo.toml | 1 + ddns/examples/publish.rs | 62 +--- ddns/src/lib.rs | 4 + ddns/src/publisher.rs | 337 ++++++++++++++++++ ddns/src/resolvers.rs | 4 + ddns/src/resolvers/h3.rs | 104 +----- .../plans/2026-05-19-ddns-publisher.md | 8 +- gmdns/src/resolvers/mdns.rs | 33 ++ 15 files changed, 557 insertions(+), 179 deletions(-) create mode 100644 ddns/src/publisher.rs diff --git a/Cargo.toml b/Cargo.toml index ed19484..d6dcfd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,9 @@ h3x = { path = "../h3x" } [patch."ssh://git@github.com/genmeta/ddns.git"] ddns-core = { path = "ddns-core" } gmdns = { path = "gmdns" } + +[patch."ssh://git@github.com/genmeta/dhttp.git"] +dhttp-identity = { path = "../dhttp/identity" } + +[patch."ssh://git@github.com/genmeta/dquic.git"] +dquic = { path = "../dquic/dquic" } diff --git a/ddns-core/Cargo.toml b/ddns-core/Cargo.toml index 4e50444..45f7b14 100644 --- a/ddns-core/Cargo.toml +++ b/ddns-core/Cargo.toml @@ -8,6 +8,7 @@ base64 = "0.22" bitfield-struct = "0.10" bytes = "1" dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } nom = "8" rand = "0.9" ring = "0.17" @@ -18,4 +19,5 @@ tracing = "0.1" x509-parser = "0.18" [dev-dependencies] +futures = "0.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/ddns-core/src/parser/record/endpoint.rs b/ddns-core/src/parser/record/endpoint.rs index f50cf72..e9d59ab 100644 --- a/ddns-core/src/parser/record/endpoint.rs +++ b/ddns-core/src/parser/record/endpoint.rs @@ -16,13 +16,62 @@ use nom::{ error::{ErrorKind, make_error}, number::streaming::{be_u8, be_u16, be_u32, be_u128}, }; -use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer, sign::SigningKey}; +use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer}; +use snafu::{ResultExt, Snafu}; use crate::parser::{ sigin, varint::{VarInt, WriteVarInt, be_varint}, }; +const SIGNATURE_SCHEME_PREFERENCE: &[SignatureScheme] = &[ + SignatureScheme::ED25519, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, +]; + +fn signature_schemes_for_algorithm( + algorithm: rustls::SignatureAlgorithm, +) -> impl Iterator { + SIGNATURE_SCHEME_PREFERENCE + .iter() + .copied() + .filter(move |scheme| match algorithm { + rustls::SignatureAlgorithm::ED25519 => *scheme == SignatureScheme::ED25519, + rustls::SignatureAlgorithm::ECDSA => matches!( + scheme, + SignatureScheme::ECDSA_NISTP256_SHA256 | SignatureScheme::ECDSA_NISTP384_SHA384 + ), + rustls::SignatureAlgorithm::RSA => matches!( + scheme, + SignatureScheme::RSA_PSS_SHA256 + | SignatureScheme::RSA_PSS_SHA384 + | SignatureScheme::RSA_PSS_SHA512 + | SignatureScheme::RSA_PKCS1_SHA256 + | SignatureScheme::RSA_PKCS1_SHA384 + | SignatureScheme::RSA_PKCS1_SHA512 + ), + _ => true, + }) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum SignEndpointError { + #[snafu(display("failed to sign endpoint address"))] + Sign { + source: dhttp_identity::identity::SignError, + }, + #[snafu(display("no supported signature scheme for endpoint address"))] + NoSupportedScheme, +} + /// EndpointAddress record (Type E = 266) /// /// Unified endpoint format that encodes address family, routing, clustering and NAT information @@ -204,19 +253,27 @@ impl EndpointAddr { } } - pub fn sign_with( + pub async fn sign_with_agent( &mut self, - key: &(impl SigningKey + ?Sized), - scheme: SignatureScheme, - ) -> Result<(), sigin::SignError> { + agent: &(impl dhttp_identity::identity::LocalAgent + ?Sized), + ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); - let signature = sigin::sign(key, scheme, &data)?; - self.signature = Some(EndpointSignature { - scheme: u16::from(scheme), - signature, - }); - Ok(()) + for scheme in signature_schemes_for_algorithm(agent.sign_algorithm()) { + match agent.sign(scheme, &data).await { + Ok(signature) => { + self.signature = Some(EndpointSignature { + scheme: u16::from(scheme), + signature, + }); + return Ok(()); + } + Err(dhttp_identity::identity::SignError::UnsupportedScheme { .. }) => {} + Err(source) => return Err(source).context(sign_endpoint_error::SignSnafu), + } + } + + sign_endpoint_error::NoSupportedSchemeSnafu.fail() } pub fn verify_signature( @@ -733,16 +790,16 @@ impl TryFrom for DquicEndpointAddr { } } -pub fn sign_endponit_address( +pub async fn sign_endponit_address( server_id: u8, - key: Option<(&(impl SigningKey + ?Sized), SignatureScheme)>, + agent: Option<&(impl dhttp_identity::identity::LocalAgent + ?Sized)>, endpoint: DquicEndpointAddr, ) -> Option { let mut ep: EndpointAddr = endpoint.try_into().ok()?; ep.set_main(server_id == 0); ep.set_sequence(server_id as u64); - if let Some((key, scheme)) = key { - let _ = ep.sign_with(key, scheme); + if let Some(agent) = agent { + let _ = ep.sign_with_agent(agent).await; } Some(ep) } @@ -755,8 +812,9 @@ mod tests { }; use bytes::BytesMut; + use futures::future::BoxFuture; use ring::signature::KeyPair; - use rustls::sign::Signer; + use rustls::sign::{Signer, SigningKey}; use super::*; @@ -956,6 +1014,29 @@ mod tests { } } + impl dhttp_identity::identity::LocalAgent for Ed25519Key { + fn name(&self) -> &str { + "agent.example" + } + + fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { + &[] + } + + fn sign_algorithm(&self) -> rustls::SignatureAlgorithm { + rustls::SignatureAlgorithm::ED25519 + } + + fn sign( + &self, + scheme: SignatureScheme, + data: &[u8], + ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { + let result = dhttp_identity::identity::sign_with_key(self, scheme, data); + Box::pin(std::future::ready(result)) + } + } + let rng = ring::rand::SystemRandom::new(); let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); let keypair = @@ -971,7 +1052,7 @@ mod tests { let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); let mut ep = EndpointAddr::direct_v4(addr); ep.set_main(true); - ep.sign_with(&key, SignatureScheme::ED25519).unwrap(); + futures::executor::block_on(ep.sign_with_agent(&key)).unwrap(); let mut buf = BytesMut::new(); buf.put_endpoint_addr(&ep); @@ -996,6 +1077,52 @@ mod tests { ); } + #[test] + fn sign_with_agent_tries_next_supported_scheme() { + #[derive(Debug)] + struct FallbackAgent; + + impl dhttp_identity::identity::LocalAgent for FallbackAgent { + fn name(&self) -> &str { + "agent.example" + } + + fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { + &[] + } + + fn sign_algorithm(&self) -> rustls::SignatureAlgorithm { + rustls::SignatureAlgorithm::ECDSA + } + + fn sign( + &self, + scheme: SignatureScheme, + _data: &[u8], + ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { + Box::pin(async move { + match scheme { + SignatureScheme::ECDSA_NISTP256_SHA256 => { + Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }) + } + SignatureScheme::ECDSA_NISTP384_SHA384 => Ok(vec![1, 2, 3]), + _ => Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }), + } + }) + } + } + + let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); + futures::executor::block_on(ep.sign_with_agent(&FallbackAgent)).unwrap(); + + let signature = ep.signature().unwrap(); + assert_eq!( + SignatureScheme::from(signature.scheme), + SignatureScheme::ECDSA_NISTP384_SHA384 + ); + assert_eq!(signature.signature, vec![1, 2, 3]); + } + #[test] fn optional_fields_flags_follow_values() { let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5353); diff --git a/ddns-core/src/parser/sigin.rs b/ddns-core/src/parser/sigin.rs index fa366c1..fa123c5 100644 --- a/ddns-core/src/parser/sigin.rs +++ b/ddns-core/src/parser/sigin.rs @@ -7,7 +7,7 @@ use x509_parser::prelude::FromDer; pub enum SignError { #[snafu(display("unsupported signature scheme {scheme:?}"))] UnsupportedScheme { scheme: SignatureScheme }, - #[snafu(display("crypto error"))] + #[snafu(display("cryptographic operation failed"))] Crypto { #[snafu(source(false))] source: rustls::Error, @@ -35,12 +35,11 @@ pub enum VerifyError { Io { source: std::io::Error }, } -pub(crate) fn sign( +pub fn sign_with_key( key: &(impl SigningKey + ?Sized), scheme: SignatureScheme, data: &[u8], ) -> Result, SignError> { - // FIXME: same as load spki then sign with ring? let signer = key .choose_scheme(&[scheme]) .ok_or(SignError::UnsupportedScheme { scheme })?; diff --git a/ddns-server/Cargo.toml b/ddns-server/Cargo.toml index b93a207..52af607 100644 --- a/ddns-server/Cargo.toml +++ b/ddns-server/Cargo.toml @@ -9,6 +9,7 @@ path = "src/main.rs" [dependencies] ddns = { path = "../ddns", features = ["h3x-resolver"] } +dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", features = [ "dquic", ] } diff --git a/ddns-server/src/policy.rs b/ddns-server/src/policy.rs index 14e5e35..fb44ab8 100644 --- a/ddns-server/src/policy.rs +++ b/ddns-server/src/policy.rs @@ -1,5 +1,5 @@ use ddns::parser::{packet::be_packet, record::RData}; -use h3x::quic::agent::RemoteAgent; +use dhttp_identity::identity::RemoteAgent; use tracing::warn; use crate::error::{AppError, normalize_host}; diff --git a/ddns-server/src/publish.rs b/ddns-server/src/publish.rs index 3ff0494..db1b15d 100644 --- a/ddns-server/src/publish.rs +++ b/ddns-server/src/publish.rs @@ -1,9 +1,7 @@ use deadpool_redis::redis::{self, AsyncCommands}; +use dhttp_identity::identity::RemoteAgent; use futures::future::BoxFuture; -use h3x::{ - endpoint::server::{Request, Response, Service}, - quic::agent::RemoteAgent, -}; +use h3x::endpoint::server::{Request, Response, Service}; use tokio::time::{Duration, Instant}; use tracing::{debug, info, warn}; diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml index 4476fb5..9bb27f9 100644 --- a/ddns/Cargo.toml +++ b/ddns/Cargo.toml @@ -6,6 +6,7 @@ autoexamples = false [dependencies] ddns-core = { path = "../ddns-core" } +dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } nom = "8" dashmap = "6" bytes = "1" diff --git a/ddns/examples/publish.rs b/ddns/examples/publish.rs index eef2a56..5f206cb 100644 --- a/ddns/examples/publish.rs +++ b/ddns/examples/publish.rs @@ -13,10 +13,7 @@ use h3x::dquic::{ client::{ClientQuicConfig, ServerCertVerifierChoice}, resolver::{Publish, handy::SystemResolver}, }; -use rustls::{ - RootCertStore, SignatureScheme, client::WebPkiServerVerifier, pki_types::PrivateKeyDer, - sign::SigningKey, -}; +use rustls::{RootCertStore, client::WebPkiServerVerifier}; use tracing::{Level, info}; #[derive(Parser, Debug)] @@ -91,40 +88,6 @@ fn expand_tilde(path: &Path) -> io::Result { Ok(PathBuf::from(shellexpand::tilde(path).into_owned())) } -fn load_private_key_from_pem(pem: &[u8]) -> io::Result> { - let mut reader = std::io::Cursor::new(pem); - let key = rustls_pemfile::private_key(&mut reader) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No private key found in PEM"))?; - Ok(key) -} - -fn build_signing_key_from_pem(pem: &[u8]) -> io::Result> { - let key = load_private_key_from_pem(pem)?; - rustls::crypto::ring::sign::any_supported_type(&key) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) -} - -fn pick_signature_scheme(key: &dyn SigningKey) -> io::Result { - // Order is preference; choose_scheme picks the first it supports. - let offered = [ - SignatureScheme::ED25519, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - ]; - - let signer = key - .choose_scheme(&offered) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Unsupported key type/scheme"))?; - Ok(signer.scheme()) -} - #[tokio::main] async fn main() -> io::Result<()> { // Install ring crypto provider @@ -145,30 +108,24 @@ async fn main() -> io::Result<()> { let cert_chain_pem = std::fs::read(&client_cert)?; let private_key_pem = std::fs::read(&client_key)?; - let signer = opt - .sign - .then(|| build_signing_key_from_pem(&private_key_pem)) - .transpose()?; - let signer_scheme = signer.as_deref().map(pick_signature_scheme).transpose()?; - // Build WebPki server cert verifier from CA root store let verifier = WebPkiServerVerifier::builder(Arc::new(root_store)) .build() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; // Build TLS identity from cert chain and private key PEM - let identity = Identity { + let identity = Arc::new(Identity { name: opt.client_name.parse().unwrap(), certs: Arc::new(cert_chain_pem.to_certificate()), key: Arc::new(private_key_pem.to_private_key()), ocsp: Arc::new(None), - }; + }); // Build network and QuicEndpoint with client mTLS config let network = Network::builder().build(); let quic = QuicEndpoint::builder() .network(network) - .identity(Arc::new(identity)) + .identity(identity.clone()) .resolver(Arc::new(SystemResolver)) .client(ClientQuicConfig { verifier: ServerCertVerifierChoice::WebPki(verifier), @@ -182,8 +139,8 @@ async fn main() -> io::Result<()> { let resolver = H3Publisher::new(opt.base_url.clone(), h3_endpoint)?; info!(host = %opt.host, addrs = ?opt.addr, base_url = %opt.base_url, "publish.start"); - if let Some(scheme) = signer_scheme { - info!(?scheme, "publish.endpoint_signing.enabled"); + if opt.sign { + info!("publish.endpoint_signing.enabled"); } else { info!("publish.endpoint_signing.disabled"); } @@ -196,10 +153,11 @@ async fn main() -> io::Result<()> { }; endpoint.set_main(opt.is_main); endpoint.set_sequence(opt.sequence); - if let Some((key, scheme)) = signer.as_deref().zip(signer_scheme) { - info!("Signing endpoint with scheme: {:?}", scheme); + if opt.sign { + info!("signing endpoint"); endpoint - .sign_with(key, scheme) + .sign_with_agent(identity.as_ref()) + .await .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } info!("Publishing endpoint: {:?}", endpoint); diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs index 853c95f..32cf680 100644 --- a/ddns/src/lib.rs +++ b/ddns/src/lib.rs @@ -1,7 +1,11 @@ +#[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] +mod publisher; pub mod resolvers; pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; pub use gmdns::{Mdns, MdnsResolver, mdns}; +#[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] +pub use publisher::{CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, PublishOnceError, Publisher}; #[cfg(feature = "http-resolver")] pub use resolvers::HttpResolver; #[cfg(feature = "mdns-resolver")] diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs new file mode 100644 index 0000000..ee01180 --- /dev/null +++ b/ddns/src/publisher.rs @@ -0,0 +1,337 @@ +use std::{ + any::Any, + collections::{HashMap, HashSet}, + io, + sync::Arc, + time::Duration, +}; + +use ddns_core::{ + MdnsPacket, + parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, +}; +use dhttp_identity::identity::LocalAgent; +use dquic::{ + qbase::net::{Family, addr::EndpointAddr}, + qresolve::{Publish, Resolve}, +}; +use snafu::{ResultExt, Snafu}; + +use crate::resolvers::Resolvers; + +pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); + +#[derive(Debug, Snafu)] +#[snafu(module(create_publisher_error))] +pub enum CreatePublisherError { + #[snafu(display("anonymous endpoint cannot publish dns records"))] + AnonymousEndpoint, +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum PublishOnceError { + #[snafu(display("no publisher resolver available"))] + NoPublisherResolver, + #[snafu(display("failed to encode endpoint address"))] + EncodeEndpoint, + #[snafu(display("failed to sign endpoint address"))] + SignEndpoint { source: SignEndpointError }, + #[snafu(display("failed to publish dns packet with {publisher}"))] + Publish { + publisher: String, + source: io::Error, + }, +} + +pub struct Publisher { + identity: Arc, + network: Arc, + resolver: Arc, + bind_patterns: Arc>, + interval: Duration, +} + +impl std::fmt::Debug for Publisher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Publisher") + .field("identity", &self.identity.name()) + .field("bind_patterns", &self.bind_patterns) + .field("interval", &self.interval) + .finish_non_exhaustive() + } +} + +impl Publisher { + pub fn new( + identity: Arc, + network: Arc, + resolver: Arc, + bind_patterns: Arc>, + ) -> Self { + Self { + identity, + network, + resolver, + bind_patterns, + interval: DEFAULT_PUBLISH_INTERVAL, + } + } + + pub fn interval(&self) -> Duration { + self.interval + } + + pub async fn publish_once(&self) -> Result<(), PublishOnceError> { + let mut published = false; + let public_endpoints = self.public_endpoints(); + published |= self + .publish_to_resolver(self.resolver.as_ref(), &public_endpoints) + .await?; + + if !published { + return publish_once_error::NoPublisherResolverSnafu.fail(); + } + + Ok(()) + } + + pub async fn run(&self) -> ! { + loop { + if let Err(error) = self.publish_once().await { + let report = snafu::Report::from_error(&error); + tracing::warn!(error = %report, "dns publish failed"); + } + tokio::time::sleep(self.interval).await; + } + } + + async fn publish_to_resolver( + &self, + resolver: &(dyn Resolve + Send + Sync), + public_endpoints: &[EndpointAddr], + ) -> Result { + let any: &dyn Any = resolver; + + if let Some(resolvers) = any.downcast_ref::() { + let mut published = false; + for resolver in resolvers.iter() { + published |= self + .publish_single_resolver(resolver.as_ref(), public_endpoints) + .await?; + } + return Ok(published); + } + + self.publish_single_resolver(resolver, public_endpoints) + .await + } + + async fn publish_single_resolver( + &self, + resolver: &(dyn Resolve + Send + Sync), + public_endpoints: &[EndpointAddr], + ) -> Result { + let any: &dyn Any = resolver; + + #[cfg(feature = "http-resolver")] + if let Some(http) = any.downcast_ref::() { + self.publish_endpoints(http, public_endpoints).await?; + return Ok(true); + } + + #[cfg(feature = "h3x-resolver")] + if let Some(h3) = + any.downcast_ref::>() + { + self.publish_endpoints(h3, public_endpoints).await?; + return Ok(true); + } + + #[cfg(feature = "mdns-resolver")] + if let Some(mdns) = any.downcast_ref::() { + let mut published = false; + for bound in mdns.bound_resolvers() { + let endpoints = self.local_endpoints_for(&bound.device, bound.family); + self.publish_endpoints(&bound.resolver, &endpoints).await?; + published = true; + } + return Ok(published); + } + + Ok(false) + } + + async fn publish_endpoints( + &self, + publisher: &(dyn Publish + Send + Sync), + endpoints: &[EndpointAddr], + ) -> Result<(), PublishOnceError> { + let packet = self.signed_packet(endpoints).await?; + let name = self.identity.name(); + publisher + .publish(name, &packet) + .await + .context(publish_once_error::PublishSnafu { + publisher: publisher.to_string(), + }) + } + + async fn signed_packet(&self, endpoints: &[EndpointAddr]) -> Result, PublishOnceError> { + let mut signed = Vec::with_capacity(endpoints.len()); + for endpoint in endpoints { + let mut endpoint = DnsEndpointAddr::try_from(*endpoint) + .map_err(|_| publish_once_error::EncodeEndpointSnafu.build())?; + endpoint + .sign_with_agent(self.identity.as_ref()) + .await + .context(publish_once_error::SignEndpointSnafu)?; + signed.push(endpoint); + } + + let mut hosts = HashMap::new(); + hosts.insert(self.identity.name().to_owned(), signed); + Ok(MdnsPacket::answer(0, &hosts).to_bytes()) + } + + fn public_endpoints(&self) -> Vec { + let mut endpoints = HashSet::new(); + for pattern in self.bind_patterns.iter() { + let Some(ifaces) = self.network.get_interfaces(pattern) else { + continue; + }; + for iface in ifaces { + if let Some(endpoint) = endpoint_from_iface(&iface) { + endpoints.insert(endpoint); + } + } + } + endpoints.into_iter().collect() + } + + fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { + let mut endpoints = HashSet::new(); + for pattern in self.bind_patterns.iter() { + let Some(ifaces) = self.network.get_interfaces(pattern) else { + continue; + }; + for iface in ifaces { + let bind_uri = iface.bind_uri(); + let Some((iface_family, iface_device, _port)) = bind_uri.as_iface_bind_uri() else { + continue; + }; + if iface_family != family || iface_device != device { + continue; + } + if let Some(endpoint) = local_endpoint_from_iface(&iface, family) { + endpoints.insert(endpoint); + } + } + } + endpoints.into_iter().collect() + } +} + +fn endpoint_from_iface(iface: &h3x::dquic::net::BindInterface) -> Option { + use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; + + iface.with_components(|components, current| { + if let Some(stun) = components.get::() + && let Some((agent, outer)) = stun.with_clients(|clients| { + clients.values().find_map(|client| { + let outer = client.get_outer_addr()?.ok()?; + Some((client.agent_addr(), outer)) + }) + }) + { + return Some(EndpointAddr::with_agent(agent, outer)); + } + + current.bound_addr().ok().map(EndpointAddr::direct) + }) +} + +fn local_endpoint_from_iface( + iface: &h3x::dquic::net::BindInterface, + family: Family, +) -> Option { + use h3x::dquic::net::IO; + + iface.with_components(|_components, current| { + let addr = current.bound_addr().ok()?; + match (family, addr) { + (Family::V4, std::net::SocketAddr::V4(_)) + | (Family::V6, std::net::SocketAddr::V6(_)) => Some(EndpointAddr::direct(addr)), + _ => None, + } + }) +} + +#[cfg(test)] +mod tests { + use std::{fmt, sync::Arc}; + + use dquic::qresolve::{ResolveFuture, Source}; + use futures::{FutureExt, StreamExt, future::BoxFuture, stream}; + use rustls::{SignatureAlgorithm, SignatureScheme, pki_types::CertificateDer}; + + use super::*; + + #[derive(Debug)] + struct TestAgent; + + impl LocalAgent for TestAgent { + fn name(&self) -> &str { + "agent.example" + } + + fn cert_chain(&self) -> &[CertificateDer<'static>] { + &[] + } + + fn sign_algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::ED25519 + } + + fn sign( + &self, + scheme: SignatureScheme, + _data: &[u8], + ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { + Box::pin(async move { + match scheme { + SignatureScheme::ED25519 => Ok(vec![1, 2, 3]), + _ => Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }), + } + }) + } + } + + #[derive(Debug)] + struct DisplayOnlyResolver; + + impl fmt::Display for DisplayOnlyResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("display only resolver") + } + } + + impl Resolve for DisplayOnlyResolver { + fn lookup<'l>(&'l self, _name: &'l str) -> ResolveFuture<'l> { + async { Ok(stream::empty::<(Source, EndpointAddr)>().boxed()) }.boxed() + } + } + + #[tokio::test] + async fn publish_once_reports_no_publisher_resolver() { + let publisher = Publisher::new( + Arc::new(TestAgent), + h3x::dquic::Network::builder().build(), + Arc::new(DisplayOnlyResolver), + Arc::new(Vec::new()), + ); + + let error = publisher.publish_once().await.unwrap_err(); + assert!(matches!(error, PublishOnceError::NoPublisherResolver)); + } +} diff --git a/ddns/src/resolvers.rs b/ddns/src/resolvers.rs index 055e48b..fe94d21 100644 --- a/ddns/src/resolvers.rs +++ b/ddns/src/resolvers.rs @@ -200,6 +200,10 @@ impl Resolvers { self } + pub fn iter(&self) -> impl Iterator { + self.resolvers.iter() + } + pub async fn lookup( &self, name: &str, diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index aa38eda..1926d91 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -12,7 +12,6 @@ use tokio::time::Instant; use tracing::trace; use url::Url; -// Inner struct that holds the actual H3 client and runs on a dedicated thread pub struct H3Resolver { endpoint: Arc>, base_url: Url, @@ -263,39 +262,10 @@ where C::Connection: Send + 'static, { fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let name = name.to_owned(); - let packed = bytes::Bytes::copy_from_slice(packet); - let base_url = self.base_url.clone(); - let client = self.endpoint.clone(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("failed to build runtime"); - let result = rt.block_on(async { - let mut url = base_url.join("publish").expect("Invalid base URL"); - url.set_query(Some(&format!("host={name}"))); - let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); - let resp = client - .post(uri) - .body(packed) - .await - .map_err(|source| Error::H3Request { source })?; - if resp.status() != http::StatusCode::OK { - return Err(Error::Status { - status: resp.status(), - }); - } - Ok(()) - }); - let _ = tx.send(result); - }); Box::pin(async move { - match rx.await { - Ok(Ok(())) => Ok(()), - Ok(Err(e)) => Err(io::Error::other(e)), - Err(_) => Err(io::Error::other("task cancelled")), + match self.publish_packet(name, packet).await { + Ok(()) => Ok(()), + Err(error) => Err(io::Error::other(error)), } }) } @@ -307,72 +277,10 @@ where C::Connection: Send + 'static, { fn lookup<'l>(&'l self, name: &'l str) -> ResolveFuture<'l> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let name = name.to_owned(); - let base_url = self.base_url.clone(); - let client = self.endpoint.clone(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("failed to build runtime"); - let result = rt.block_on(async { - let mut url = base_url.join("lookup").expect("Invalid URL"); - url.set_query(Some(&format!("host={name}"))); - let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); - let mut resp = client - .get(uri) - .await - .map_err(|source| Error::H3Request { source })?; - match resp.status() { - http::StatusCode::OK => { - let response = resp - .read_to_bytes() - .await - .map_err(|source| Error::H3Stream { source })?; - let (_remain, multi) = be_multi_response(response.as_ref()) - .map_err(|_| Error::ParseMultiResponse)?; - let mut addrs = Vec::new(); - for r in multi.records { - let (_remain, mdns_pkt) = - be_packet(&r.dns).map_err(|source| Error::ParseRecords { - source: source.to_owned(), - })?; - addrs.extend(mdns_pkt.answers.iter().filter_map(|answer| { - match answer.data() { - ddns_core::parser::record::RData::E(ep) => { - TryInto::::try_into(ep.clone()).ok() - } - _ => None, - } - })); - } - if addrs.is_empty() { - return Err(Error::NoRecordFound); - } - let server: Arc = - Arc::from(base_url.host_str().unwrap_or("")); - Ok(stream::iter(addrs.into_iter().map(move |ep| { - ( - Source::Http { - server: server.clone(), - }, - ep, - ) - })) - .boxed()) - } - http::StatusCode::NOT_FOUND => Err(Error::NoRecordFound), - status => Err(Error::Status { status }), - } - }); - let _ = tx.send(result); - }); Box::pin(async move { - match rx.await { - Ok(Ok(stream)) => Ok(stream), - Ok(Err(e)) => Err(io::Error::other(e)), - Err(_) => Err(io::Error::other("task cancelled")), + match H3Resolver::lookup(self, name).await { + Ok(stream) => Ok(stream), + Err(error) => Err(io::Error::other(error)), } }) } diff --git a/docs/superpowers/plans/2026-05-19-ddns-publisher.md b/docs/superpowers/plans/2026-05-19-ddns-publisher.md index 2f7717b..b585eb0 100644 --- a/docs/superpowers/plans/2026-05-19-ddns-publisher.md +++ b/docs/superpowers/plans/2026-05-19-ddns-publisher.md @@ -15,12 +15,12 @@ **Files:** - Modify: `dhttp/identity/src/identity.rs` - Modify: `dhttp/identity/src/lib.rs` -- Modify: `h3x/src/quic/agent.rs` -- Modify: h3x call sites importing `h3x::quic::agent::{LocalAgent, RemoteAgent, SignError, VerifyError}` +- Delete: `h3x/src/quic/agent.rs` +- Modify: h3x call sites to import `dhttp_identity::identity::{LocalAgent, RemoteAgent, SignError, VerifyError}` directly - [ ] Add tests in `dhttp/identity/src/identity.rs` for `Identity` implementing async `LocalAgent` and sync-default `RemoteAgent` verification behavior. - [ ] Move `LocalAgent`, `RemoteAgent`, `extract_public_key`, `verify_signature`, and `sign_with_key` equivalents into `dhttp-identity` while preserving async signatures. -- [ ] Re-export the identity agent API from `h3x::quic::agent` to minimize h3x call-site churn. +- [ ] Delete `h3x::quic::agent`; downstream crates import the identity agent API from `dhttp_identity::identity` directly. - [ ] Run `cargo test -p dhttp-identity` and `cargo test --features dquic` in h3x. ### Task 2: Replace ddns-core SigningKey signing with LocalAgent signing @@ -32,7 +32,7 @@ - [ ] Add a failing async test for signing an `EndpointAddr` through a fake `LocalAgent` that rejects the first preferred compatible scheme and accepts the next one. - [ ] Add `EndpointAddr::sign_with_agent(&mut self, agent: &(impl LocalAgent + ?Sized)) -> impl Future>`. -- [ ] Delete old `EndpointAddr::sign_with(SigningKey, SignatureScheme)` and update tests/examples to use the async agent method. +- [ ] Keep old low-level `ddns_core::parser::sigin::sign_with_key(SigningKey, SignatureScheme, data)` helper, delete only the `EndpointAddr::sign_with(SigningKey, SignatureScheme)` convenience method, and update tests/examples to use the async agent method where endpoint records are signed. - [ ] Keep verification logic unchanged except for imports. - [ ] Run `cargo test -p ddns-core`. diff --git a/gmdns/src/resolvers/mdns.rs b/gmdns/src/resolvers/mdns.rs index 97b07a1..7a6f7a2 100644 --- a/gmdns/src/resolvers/mdns.rs +++ b/gmdns/src/resolvers/mdns.rs @@ -161,6 +161,14 @@ pub struct MdnsResolvers { _handles: Vec, } +#[cfg(feature = "h3x-network")] +#[derive(Debug, Clone)] +pub struct BoundMdnsResolver { + pub device: String, + pub family: Family, + pub resolver: MdnsResolver, +} + #[cfg(feature = "h3x-network")] impl fmt::Debug for MdnsResolvers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -220,6 +228,31 @@ impl MdnsResolvers { } } + pub fn bound_resolvers(&self) -> Vec { + let mut resolvers = Vec::new(); + for pattern in self.patterns.iter() { + let Some(ifaces) = self.bound_interfaces(pattern) else { + continue; + }; + for iface in ifaces { + let bind_uri = iface.bind_uri(); + let Some((family, device, _port)) = bind_uri.as_iface_bind_uri() else { + continue; + }; + iface.with_components(|components, _| { + if let Some(resolver) = components.get::() { + resolvers.push(BoundMdnsResolver { + device: device.to_owned(), + family, + resolver: resolver.clone(), + }); + } + }); + } + } + resolvers + } + pub async fn query(&self, name: &str) -> io::Result { let mut lookup_futures = FuturesUnordered::new(); self.for_each_resolver(|resolver| { From 67593a0101b8351514bc5a59bfe9ae6ac9a2318d Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 21:15:36 +0800 Subject: [PATCH 21/85] feat: add configurable dns publisher options --- ddns/src/lib.rs | 4 +++- ddns/src/publisher.rs | 49 +++++++++++++++++++++++++++++++++++++++ ddns/src/resolvers.rs | 53 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs index 32cf680..b06dc0a 100644 --- a/ddns/src/lib.rs +++ b/ddns/src/lib.rs @@ -5,7 +5,9 @@ pub mod resolvers; pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; pub use gmdns::{Mdns, MdnsResolver, mdns}; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] -pub use publisher::{CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, PublishOnceError, Publisher}; +pub use publisher::{ + CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, PublishOnceError, PublishOptions, Publisher, +}; #[cfg(feature = "http-resolver")] pub use resolvers::HttpResolver; #[cfg(feature = "mdns-resolver")] diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index ee01180..45654de 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -44,12 +44,23 @@ pub enum PublishOnceError { }, } +/// Optional metadata applied to endpoint records before signing. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PublishOptions { + /// Stable server identifier for names served by multiple publishers. + /// + /// `0` marks the endpoint as the main record. Non-zero values mark the + /// record as clustered and encode the identifier as its sequence number. + pub server_id: Option, +} + pub struct Publisher { identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, interval: Duration, + options: PublishOptions, } impl std::fmt::Debug for Publisher { @@ -58,6 +69,7 @@ impl std::fmt::Debug for Publisher { .field("identity", &self.identity.name()) .field("bind_patterns", &self.bind_patterns) .field("interval", &self.interval) + .field("options", &self.options) .finish_non_exhaustive() } } @@ -75,9 +87,19 @@ impl Publisher { resolver, bind_patterns, interval: DEFAULT_PUBLISH_INTERVAL, + options: PublishOptions::default(), } } + pub fn with_options(mut self, options: PublishOptions) -> Self { + self.options = options; + self + } + + pub fn options(&self) -> PublishOptions { + self.options + } + pub fn interval(&self) -> Duration { self.interval } @@ -182,6 +204,10 @@ impl Publisher { for endpoint in endpoints { let mut endpoint = DnsEndpointAddr::try_from(*endpoint) .map_err(|_| publish_once_error::EncodeEndpointSnafu.build())?; + if let Some(server_id) = self.options.server_id { + endpoint.set_main(server_id == 0); + endpoint.set_sequence(server_id.into()); + } endpoint .sign_with_agent(self.identity.as_ref()) .await @@ -334,4 +360,27 @@ mod tests { let error = publisher.publish_once().await.unwrap_err(); assert!(matches!(error, PublishOnceError::NoPublisherResolver)); } + + #[tokio::test] + async fn signed_packet_applies_publish_options_server_id() { + let publisher = Publisher::new( + Arc::new(TestAgent), + h3x::dquic::Network::builder().build(), + Arc::new(DisplayOnlyResolver), + Arc::new(Vec::new()), + ) + .with_options(PublishOptions { server_id: Some(2) }); + + let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + let packet = publisher.signed_packet(&[endpoint]).await.unwrap(); + let (_remain, packet) = ddns_core::parser::packet::be_packet(&packet).unwrap(); + let record = packet.answers.first().expect("endpoint answer"); + let ddns_core::parser::record::RData::E(endpoint) = record.data() else { + panic!("expected endpoint record"); + }; + + assert!(!endpoint.is_main()); + assert!(endpoint.is_clustered()); + assert!(endpoint.is_signed()); + } } diff --git a/ddns/src/resolvers.rs b/ddns/src/resolvers.rs index fe94d21..561dfde 100644 --- a/ddns/src/resolvers.rs +++ b/ddns/src/resolvers.rs @@ -154,7 +154,21 @@ impl ResolversBuilder { #[cfg(feature = "h3x-resolver")] pub fn h3( + self, + endpoint: Arc>, + ) -> io::Result + where + C: h3x::quic::Connect + Send + Sync + 'static, + C::Error: Send + Sync + 'static, + C::Connection: Send + 'static, + { + self.h3_with_base_url(DHTTP_H3_DNS_SERVER, endpoint) + } + + #[cfg(feature = "h3x-resolver")] + pub fn h3_with_base_url( mut self, + base_url: impl AsRef, endpoint: Arc>, ) -> io::Result where @@ -162,14 +176,19 @@ impl ResolversBuilder { C::Error: Send + Sync + 'static, C::Connection: Send + 'static, { - let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint)?; + let resolver = H3Resolver::from_endpoint(base_url, endpoint)?; self.resolvers = self.resolvers.with(Arc::new(resolver)); Ok(self) } #[cfg(feature = "http-resolver")] - pub fn http(mut self) -> io::Result { - let resolver = HttpResolver::new(DHTTP_HTTP_DNS_SERVER)?; + pub fn http(self) -> io::Result { + self.http_with_base_url(DHTTP_HTTP_DNS_SERVER) + } + + #[cfg(feature = "http-resolver")] + pub fn http_with_base_url(mut self, base_url: impl AsRef) -> io::Result { + let resolver = HttpResolver::new(base_url.as_ref())?; self.resolvers = self.resolvers.with(Arc::new(resolver)); Ok(self) } @@ -296,6 +315,34 @@ mod tests { assert!(resolvers.to_string().contains("mDNS resolvers")); } + #[cfg(feature = "h3x-resolver")] + #[tokio::test] + async fn resolvers_builder_accepts_custom_h3_base_url() { + use std::sync::Arc; + + let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( + h3x::dquic::QuicEndpoint::builder().build().await, + )); + + let resolvers = Resolvers::builder() + .h3_with_base_url("https://custom-dns.example:4433", endpoint) + .expect("valid h3 dns url") + .build(); + + assert!(resolvers.to_string().contains("custom-dns.example")); + } + + #[cfg(feature = "http-resolver")] + #[test] + fn resolvers_builder_accepts_custom_http_base_url() { + let resolvers = Resolvers::builder() + .http_with_base_url("https://custom-dns.example") + .expect("valid http dns url") + .build(); + + assert!(resolvers.to_string().contains("custom-dns.example")); + } + #[cfg(feature = "mdns-resolver")] #[tokio::test] async fn mdns_resolvers_bind_installs_mdns_on_null_io_binding() { From e7fb6f00521b85f42bd3c4fdaf262b6b0826f3ca Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 23:05:55 +0800 Subject: [PATCH 22/85] chore(deps): remove local workspace patches --- Cargo.toml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6dcfd8..5b312b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,3 @@ resolver = "2" [workspace.package] version = "0.2.0" edition = "2024" - -[patch."https://github.com/genmeta/h3x.git"] -h3x = { path = "../h3x" } - -[patch."ssh://git@github.com/genmeta/ddns.git"] -ddns-core = { path = "ddns-core" } -gmdns = { path = "gmdns" } - -[patch."ssh://git@github.com/genmeta/dhttp.git"] -dhttp-identity = { path = "../dhttp/identity" } - -[patch."ssh://git@github.com/genmeta/dquic.git"] -dquic = { path = "../dquic/dquic" } From 3d251a555b0b29c91e356c903b0cd64b2a8e545b Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 19 May 2026 23:41:21 +0800 Subject: [PATCH 23/85] fix(ddns-server): keep h3 server running --- ddns-server/Cargo.toml | 1 - ddns-server/src/main.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ddns-server/Cargo.toml b/ddns-server/Cargo.toml index 52af607..77145e9 100644 --- a/ddns-server/Cargo.toml +++ b/ddns-server/Cargo.toml @@ -33,7 +33,6 @@ idna = "1" x509-parser = "0.18" snafu = "0.8" tokio = { version = "1", features = ["full"] } -tokio-util = "0.7" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2" diff --git a/ddns-server/src/main.rs b/ddns-server/src/main.rs index 3da655e..ef4bc37 100644 --- a/ddns-server/src/main.rs +++ b/ddns-server/src/main.rs @@ -19,8 +19,7 @@ use h3x::{ endpoint::{H3Endpoint, server::Router}, }; use rustls::{RootCertStore, server::WebPkiClientVerifier}; -use tokio_util::task::AbortOnDropHandle; -use tracing::{Instrument, info, level_filters::LevelFilter}; +use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt}; use crate::{ @@ -198,7 +197,7 @@ async fn main() -> Result<(), Box> { .await; let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - let _serve = AbortOnDropHandle::new(tokio::spawn(server.serve_owned(router).in_current_span())); + server.serve_owned(router).await?; Ok(()) } From 6f15627b89cad15d48d106c48eba6c2dcb48f54b Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 02:15:49 +0800 Subject: [PATCH 24/85] fix(ddns-server): advertise h3 alpn --- ddns-server/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ddns-server/src/main.rs b/ddns-server/src/main.rs index ef4bc37..00bb9d1 100644 --- a/ddns-server/src/main.rs +++ b/ddns-server/src/main.rs @@ -182,6 +182,7 @@ async fn main() -> Result<(), Box> { ocsp: Arc::new(None), }); let server_config = ServerQuicConfig { + alpns: vec![b"h3".to_vec()], client_cert_verifier: verifier, ..Default::default() }; From 1a1bb2d2f7bbb096f9364170181646c586ab488c Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 03:14:58 +0800 Subject: [PATCH 25/85] fix(publisher): bound dns publish attempts --- ddns/src/lib.rs | 3 ++- ddns/src/publisher.rs | 48 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/ddns/src/lib.rs b/ddns/src/lib.rs index b06dc0a..5d26d87 100644 --- a/ddns/src/lib.rs +++ b/ddns/src/lib.rs @@ -6,7 +6,8 @@ pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wir pub use gmdns::{Mdns, MdnsResolver, mdns}; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] pub use publisher::{ - CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, PublishOnceError, PublishOptions, Publisher, + CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, DEFAULT_PUBLISH_TIMEOUT, PublishOnceError, + PublishOptions, Publisher, }; #[cfg(feature = "http-resolver")] pub use resolvers::HttpResolver; diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 45654de..022e8d0 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -20,6 +20,12 @@ use snafu::{ResultExt, Snafu}; use crate::resolvers::Resolvers; pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); +/// Upper bound for a single publish attempt in the background loop. +/// +/// Network changes can leave an in-flight H3 publish waiting on paths that no +/// longer exist. Timing out the attempt keeps consecutive publishes +/// independent: the next interval observes the current bindings again. +pub const DEFAULT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Debug, Snafu)] #[snafu(module(create_publisher_error))] @@ -60,6 +66,7 @@ pub struct Publisher { resolver: Arc, bind_patterns: Arc>, interval: Duration, + publish_timeout: Duration, options: PublishOptions, } @@ -69,6 +76,7 @@ impl std::fmt::Debug for Publisher { .field("identity", &self.identity.name()) .field("bind_patterns", &self.bind_patterns) .field("interval", &self.interval) + .field("publish_timeout", &self.publish_timeout) .field("options", &self.options) .finish_non_exhaustive() } @@ -87,6 +95,7 @@ impl Publisher { resolver, bind_patterns, interval: DEFAULT_PUBLISH_INTERVAL, + publish_timeout: DEFAULT_PUBLISH_TIMEOUT, options: PublishOptions::default(), } } @@ -104,6 +113,15 @@ impl Publisher { self.interval } + pub fn publish_timeout(&self) -> Duration { + self.publish_timeout + } + + pub fn with_publish_timeout(mut self, timeout: Duration) -> Self { + self.publish_timeout = timeout; + self + } + pub async fn publish_once(&self) -> Result<(), PublishOnceError> { let mut published = false; let public_endpoints = self.public_endpoints(); @@ -120,9 +138,18 @@ impl Publisher { pub async fn run(&self) -> ! { loop { - if let Err(error) = self.publish_once().await { - let report = snafu::Report::from_error(&error); - tracing::warn!(error = %report, "dns publish failed"); + match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { + Ok(Ok(())) => {} + Ok(Err(error)) => { + let report = snafu::Report::from_error(&error); + tracing::warn!(error = %report, "dns publish failed"); + } + Err(_elapsed) => { + tracing::warn!( + timeout_ms = self.publish_timeout.as_millis(), + "dns publish timed out" + ); + } } tokio::time::sleep(self.interval).await; } @@ -361,6 +388,21 @@ mod tests { assert!(matches!(error, PublishOnceError::NoPublisherResolver)); } + #[tokio::test] + async fn publisher_timeout_is_configurable() { + let publisher = Publisher::new( + Arc::new(TestAgent), + h3x::dquic::Network::builder().build(), + Arc::new(DisplayOnlyResolver), + Arc::new(Vec::new()), + ); + assert_eq!(publisher.publish_timeout(), DEFAULT_PUBLISH_TIMEOUT); + + let timeout = Duration::from_secs(3); + let publisher = publisher.with_publish_timeout(timeout); + assert_eq!(publisher.publish_timeout(), timeout); + } + #[tokio::test] async fn signed_packet_applies_publish_options_server_id() { let publisher = Publisher::new( From 8c16a695b4dc874d19bf2b67e6d5d56f58ebec0d Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 04:47:52 +0800 Subject: [PATCH 26/85] fix(server): bind wildcard listen as dual stack --- ddns-server/src/main.rs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/ddns-server/src/main.rs b/ddns-server/src/main.rs index 00bb9d1..404e7bb 100644 --- a/ddns-server/src/main.rs +++ b/ddns-server/src/main.rs @@ -30,6 +30,17 @@ use crate::{ storage::{AppState, MemoryStorage, SeedRecords, Storage}, }; +fn bind_patterns_for_listen(listen: SocketAddr) -> Vec { + let bind_addr = match listen { + SocketAddr::V4(addr) if addr.ip().is_unspecified() => { + SocketAddr::new(std::net::Ipv6Addr::UNSPECIFIED.into(), addr.port()) + } + addr => addr, + }; + + vec![BindPattern::from_str(&format!("inet://{bind_addr}")).expect("valid bind pattern")] +} + // --------------------------------------------------------------------------- // TLS helpers // --------------------------------------------------------------------------- @@ -190,10 +201,7 @@ async fn main() -> Result<(), Box> { .network(Network::builder().build()) .identity(identity) .server(server_config) - .bind(Arc::new(vec![ - BindPattern::from_str(&format!("inet://{}", config.listen)) - .expect("valid bind pattern"), - ])) + .bind(Arc::new(bind_patterns_for_listen(config.listen))) .build() .await; let server = Arc::new(H3Endpoint::new(quic)); @@ -202,3 +210,19 @@ async fn main() -> Result<(), Box> { Ok(()) } + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use super::*; + + #[test] + fn unspecified_ipv4_listen_uses_dual_stack_wildcard() { + let listen: SocketAddr = "0.0.0.0:4433".parse().unwrap(); + let patterns = bind_patterns_for_listen(listen); + + assert_eq!(patterns.len(), 1); + assert_eq!(patterns[0].to_string(), "inet://[::]:4433"); + } +} From a5cdf3a64134055bc75509f5991c72373bdc6b7a Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 05:19:51 +0800 Subject: [PATCH 27/85] fix(resolver): reset h3 pool after request failures --- ddns/src/resolvers.rs | 8 +++++++- ddns/src/resolvers/h3.rs | 30 +++++++++++++++++++----------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ddns/src/resolvers.rs b/ddns/src/resolvers.rs index 561dfde..5f84ffa 100644 --- a/ddns/src/resolvers.rs +++ b/ddns/src/resolvers.rs @@ -262,8 +262,14 @@ impl Resolve for Resolvers { mod tests { use std::str::FromStr; + #[cfg(any( + feature = "h3x-resolver", + feature = "http-resolver", + feature = "mdns-resolver" + ))] + use super::Resolvers; #[cfg(feature = "mdns-resolver")] - use super::{DHTTP_MDNS_SERVICE, MdnsResolvers, Resolvers}; + use super::{DHTTP_MDNS_SERVICE, MdnsResolvers}; use super::{DnsScheme, resolvable_name}; #[test] diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 1926d91..c6174d7 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -102,6 +102,17 @@ where }) } + fn request_error( + &self, + source: h3x::endpoint::client::RequestError, + ) -> Error { + // H3 DNS resolvers keep a long-lived endpoint. A network transition may + // leave the cached H3 connection with stale QUIC paths, so the next + // attempt must establish a fresh connection instead of reusing it. + self.endpoint.clear_pool(); + Error::H3Request { source } + } + pub async fn publish_endpoints( &self, name: &str, @@ -129,12 +140,10 @@ where url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("h3x publishing packet for {} to {}", name, self.base_url); - let resp = self - .endpoint - .post(uri) - .body(packet) - .await - .map_err(|source| Error::H3Request { source })?; + let resp = match self.endpoint.post(uri).body(packet).await { + Ok(resp) => resp, + Err(source) => return Err(self.request_error(source)), + }; if resp.status() != http::StatusCode::OK { return Err(Error::Status { @@ -184,11 +193,10 @@ where let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("sending lookup request to {}", self.base_url); - let mut resp = self - .endpoint - .get(uri) - .await - .map_err(|source| Error::H3Request { source })?; + let mut resp = match self.endpoint.get(uri).await { + Ok(resp) => resp, + Err(source) => return Err(self.request_error(source)), + }; tracing::trace!("received response with status {}", resp.status()); match resp.status() { From e0583791e31e38b3139044ec0224612e43fd4737 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 05:34:28 +0800 Subject: [PATCH 28/85] fix(publisher): reset h3 pools after publish timeout --- ddns/src/publisher.rs | 26 ++++++++++++++++++++++++++ ddns/src/resolvers/h3.rs | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 022e8d0..1300d9d 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -145,6 +145,11 @@ impl Publisher { tracing::warn!(error = %report, "dns publish failed"); } Err(_elapsed) => { + // Dropping a timed-out publish future does not let the H3 + // resolver observe a request error. Reset resolver-owned + // connection state so the next interval reconnects from + // the current network bindings. + self.clear_publish_state(); tracing::warn!( timeout_ms = self.publish_timeout.as_millis(), "dns publish timed out" @@ -176,6 +181,27 @@ impl Publisher { .await } + fn clear_publish_state(&self) { + Self::clear_resolver_publish_state(self.resolver.as_ref()); + } + + fn clear_resolver_publish_state(resolver: &(dyn Resolve + Send + Sync)) { + let any: &dyn Any = resolver; + + if let Some(resolvers) = any.downcast_ref::() { + for resolver in resolvers.iter() { + Self::clear_resolver_publish_state(resolver.as_ref()); + } + } + + #[cfg(feature = "h3x-resolver")] + if let Some(h3) = + any.downcast_ref::>() + { + h3.clear_pool(); + } + } + async fn publish_single_resolver( &self, resolver: &(dyn Resolve + Send + Sync), diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index c6174d7..7143405 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -113,6 +113,10 @@ where Error::H3Request { source } } + pub fn clear_pool(&self) { + self.endpoint.clear_pool(); + } + pub async fn publish_endpoints( &self, name: &str, From d41aef5518a87b4f2c5a8a6196d8d1e1aeafb9a4 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 05:51:15 +0800 Subject: [PATCH 29/85] fix(publisher): republish on binding changes --- ddns/src/publisher.rs | 89 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 1300d9d..e21bf61 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -26,6 +26,7 @@ pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// longer exist. Timing out the attempt keeps consecutive publishes /// independent: the next interval observes the current bindings again. pub const DEFAULT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); +const PUBLISH_CHANGE_DEBOUNCE: Duration = Duration::from_millis(500); #[derive(Debug, Snafu)] #[snafu(module(create_publisher_error))] @@ -137,26 +138,82 @@ impl Publisher { } pub async fn run(&self) -> ! { + let mut locations = self.network.locations().subscribe(); + self.publish_attempt().await; + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; + self.drain_location_events(&mut locations); + loop { - match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { - Ok(Ok(())) => {} - Ok(Err(error)) => { - let report = snafu::Report::from_error(&error); - tracing::warn!(error = %report, "dns publish failed"); - } - Err(_elapsed) => { - // Dropping a timed-out publish future does not let the H3 - // resolver observe a request error. Reset resolver-owned - // connection state so the next interval reconnects from - // the current network bindings. + self.wait_next_publish_trigger(&mut locations).await; + self.publish_attempt().await; + } + } + + async fn publish_attempt(&self) { + match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { + Ok(Ok(())) => {} + Ok(Err(error)) => { + let report = snafu::Report::from_error(&error); + tracing::warn!(error = %report, "dns publish failed"); + } + Err(_elapsed) => { + // Dropping a timed-out publish future does not let the H3 + // resolver observe a request error. Reset resolver-owned + // connection state so the next interval reconnects from + // the current network bindings. + self.clear_publish_state(); + tracing::warn!( + timeout_ms = self.publish_timeout.as_millis(), + "dns publish timed out" + ); + } + } + } + + async fn wait_next_publish_trigger( + &self, + locations: &mut h3x::dquic::qinterface::component::location::Observer, + ) { + let interval = tokio::time::sleep(self.interval); + tokio::pin!(interval); + + loop { + tokio::select! { + _ = &mut interval => return, + event = locations.recv() => { + let Some((bind_uri, _event)) = event else { + interval.await; + return; + }; + if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { + continue; + } + + // A local-address change invalidates cached H3 DNS + // connections even if no request has failed yet. Clear + // resolver-owned connection state before publishing from + // the new binding set. self.clear_publish_state(); - tracing::warn!( - timeout_ms = self.publish_timeout.as_millis(), - "dns publish timed out" - ); + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; + self.drain_location_events(locations); + return; } } - tokio::time::sleep(self.interval).await; + } + } + + fn drain_location_events( + &self, + locations: &mut h3x::dquic::qinterface::component::location::Observer, + ) { + while let Ok((bind_uri, _event)) = locations.try_recv() { + if self + .bind_patterns + .iter() + .any(|pattern| pattern.matches(&bind_uri)) + { + self.clear_publish_state(); + } } } From b9bcb94c8206f24f30f56748a8ccdfd69619e65c Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 06:09:18 +0800 Subject: [PATCH 30/85] fix(publisher): ignore self-generated publish events --- ddns/src/publisher.rs | 113 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index e21bf61..e068770 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -11,8 +11,10 @@ use ddns_core::{ parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, }; use dhttp_identity::identity::LocalAgent; +#[cfg(feature = "mdns-resolver")] +use dquic::qbase::net::Family; use dquic::{ - qbase::net::{Family, addr::EndpointAddr}, + qbase::net::addr::EndpointAddr, qresolve::{Publish, Resolve}, }; use snafu::{ResultExt, Snafu}; @@ -140,12 +142,12 @@ impl Publisher { pub async fn run(&self) -> ! { let mut locations = self.network.locations().subscribe(); self.publish_attempt().await; - tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; - self.drain_location_events(&mut locations); + self.settle_publish_events(&mut locations).await; loop { self.wait_next_publish_trigger(&mut locations).await; self.publish_attempt().await; + self.settle_publish_events(&mut locations).await; } } @@ -217,6 +219,14 @@ impl Publisher { } } + async fn settle_publish_events( + &self, + locations: &mut h3x::dquic::qinterface::component::location::Observer, + ) { + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; + self.drain_location_events(locations); + } + async fn publish_to_resolver( &self, resolver: &(dyn Resolve + Send + Sync), @@ -345,6 +355,7 @@ impl Publisher { endpoints.into_iter().collect() } + #[cfg(feature = "mdns-resolver")] fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { let mut endpoints = HashSet::new(); for pattern in self.bind_patterns.iter() { @@ -387,6 +398,7 @@ fn endpoint_from_iface(iface: &h3x::dquic::net::BindInterface) -> Option= target { + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + } + + let network = h3x::dquic::Network::builder().build(); + let bind_uri: h3x::dquic::net::BindUri = + "inet://127.0.0.1:0".parse().expect("valid bind uri"); + let publish_count = Arc::new(AtomicUsize::new(0)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test http server"); + let port = listener.local_addr().expect("local addr").port(); + let server_network = network.clone(); + let server_bind_uri = bind_uri.clone(); + let server_count = publish_count.clone(); + let server = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + let mut buf = [0_u8; 1024]; + let _ = stream.read(&mut buf).await; + server_count.fetch_add(1, Ordering::SeqCst); + server_network.locations().upsert( + server_bind_uri.clone(), + Arc::new(Ok::( + "127.0.0.1:0".parse().expect("valid socket addr"), + )), + ); + let _ = stream + .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + .await; + } + }); + + let resolver = Arc::new( + crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + .expect("valid http resolver"), + ); + let publisher = Publisher::new( + Arc::new(TestAgent), + network.clone(), + resolver, + Arc::new(vec![ + "inet://127.0.0.1:0".parse().expect("valid bind pattern"), + ]), + ); + let publisher = tokio::spawn(async move { + publisher.run().await; + }); + + wait_for_count(&publish_count, 1).await; + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; + + network.locations().upsert( + bind_uri, + Arc::new(Ok::( + "127.0.0.1:0".parse().expect("valid socket addr"), + )), + ); + wait_for_count(&publish_count, 2).await; + + let third_publish = tokio::time::timeout( + PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(500), + wait_for_count(&publish_count, 3), + ) + .await; + + publisher.abort(); + server.abort(); + + assert!( + third_publish.is_err(), + "publish-generated location events must not trigger another immediate publish" + ); + } } From 224351d7fe33ea756055d10585a71b39f5ebd430 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 06:20:58 +0800 Subject: [PATCH 31/85] fix(publisher): ignore failed location updates --- ddns/src/publisher.rs | 47 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index e068770..abb7fdd 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -1,7 +1,8 @@ use std::{ - any::Any, + any::{Any, TypeId}, collections::{HashMap, HashSet}, io, + net::SocketAddr, sync::Arc, time::Duration, }; @@ -15,7 +16,9 @@ use dhttp_identity::identity::LocalAgent; use dquic::qbase::net::Family; use dquic::{ qbase::net::addr::EndpointAddr, + qinterface::component::location::AddressEvent, qresolve::{Publish, Resolve}, + qtraversal::nat::client::ClientLocationData, }; use snafu::{ResultExt, Snafu}; @@ -183,13 +186,16 @@ impl Publisher { tokio::select! { _ = &mut interval => return, event = locations.recv() => { - let Some((bind_uri, _event)) = event else { + let Some((bind_uri, event)) = event else { interval.await; return; }; if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { continue; } + if !Self::location_event_requires_publish(&event) { + continue; + } // A local-address change invalidates cached H3 DNS // connections even if no request has failed yet. Clear @@ -208,12 +214,15 @@ impl Publisher { &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, ) { - while let Ok((bind_uri, _event)) = locations.try_recv() { - if self + while let Ok((bind_uri, event)) = locations.try_recv() { + if !self .bind_patterns .iter() .any(|pattern| pattern.matches(&bind_uri)) { + continue; + } + if Self::location_event_requires_publish(&event) { self.clear_publish_state(); } } @@ -227,6 +236,28 @@ impl Publisher { self.drain_location_events(locations); } + fn location_event_requires_publish(event: &AddressEvent) -> bool { + match event { + AddressEvent::Upsert(data) => { + // `Locations` also carries transient STUN failures. Those do + // not add a publishable endpoint; treating them as publish + // triggers creates a retry loop while the node is offline. + if let Some(bound_addr) = data.downcast_ref::>() { + return bound_addr.is_ok(); + } + if let Some(stun_addr) = data.downcast_ref::() { + return stun_addr.is_ok(); + } + false + } + AddressEvent::Remove(type_id) => { + *type_id == TypeId::of::>() + || *type_id == TypeId::of::() + } + AddressEvent::Closed => true, + } + } + async fn publish_to_resolver( &self, resolver: &(dyn Resolve + Send + Sync), @@ -560,11 +591,11 @@ mod tests { let mut buf = [0_u8; 1024]; let _ = stream.read(&mut buf).await; server_count.fetch_add(1, Ordering::SeqCst); - server_network.locations().upsert( + server_network.locations().upsert::( server_bind_uri.clone(), - Arc::new(Ok::( - "127.0.0.1:0".parse().expect("valid socket addr"), - )), + Arc::new(Err(dquic::qtraversal::nat::client::ArcIoError::from( + io::Error::from(io::ErrorKind::NetworkUnreachable), + ))), ); let _ = stream .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") From 5e344e58e3a6a7ed9296f50ac7a30ca3aae2a022 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 06:48:00 +0800 Subject: [PATCH 32/85] fix(publisher): retry publishes after binding changes --- ddns/src/publisher.rs | 203 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 180 insertions(+), 23 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index abb7fdd..8fa567d 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -4,7 +4,7 @@ use std::{ io, net::SocketAddr, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use ddns_core::{ @@ -18,7 +18,7 @@ use dquic::{ qbase::net::addr::EndpointAddr, qinterface::component::location::AddressEvent, qresolve::{Publish, Resolve}, - qtraversal::nat::client::ClientLocationData, + qtraversal::nat::client::{ClientLocationData, NatType}, }; use snafu::{ResultExt, Snafu}; @@ -32,6 +32,11 @@ pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// independent: the next interval observes the current bindings again. pub const DEFAULT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); const PUBLISH_CHANGE_DEBOUNCE: Duration = Duration::from_millis(500); +#[cfg(not(test))] +const PUBLISH_CHANGE_RETRY_DELAY: Duration = Duration::from_secs(2); +#[cfg(test)] +const PUBLISH_CHANGE_RETRY_DELAY: Duration = Duration::from_millis(20); +const PUBLISH_CHANGE_RETRY_WINDOW: Duration = Duration::from_secs(90); #[derive(Debug, Snafu)] #[snafu(module(create_publisher_error))] @@ -148,18 +153,25 @@ impl Publisher { self.settle_publish_events(&mut locations).await; loop { - self.wait_next_publish_trigger(&mut locations).await; - self.publish_attempt().await; + let trigger = self.wait_next_publish_trigger(&mut locations).await; + let published = self.publish_attempt().await; self.settle_publish_events(&mut locations).await; + if matches!(trigger, PublishTrigger::Location) && !published { + self.retry_changed_publish(&mut locations).await; + } } } - async fn publish_attempt(&self) { + async fn publish_attempt(&self) -> bool { match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { - Ok(Ok(())) => {} + Ok(Ok(())) => { + tracing::info!("published resolver endpoints"); + true + } Ok(Err(error)) => { let report = snafu::Report::from_error(&error); tracing::warn!(error = %report, "dns publish failed"); + false } Err(_elapsed) => { // Dropping a timed-out publish future does not let the H3 @@ -171,6 +183,7 @@ impl Publisher { timeout_ms = self.publish_timeout.as_millis(), "dns publish timed out" ); + false } } } @@ -178,17 +191,17 @@ impl Publisher { async fn wait_next_publish_trigger( &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) { + ) -> PublishTrigger { let interval = tokio::time::sleep(self.interval); tokio::pin!(interval); loop { tokio::select! { - _ = &mut interval => return, + _ = &mut interval => return PublishTrigger::Interval, event = locations.recv() => { let Some((bind_uri, event)) = event else { interval.await; - return; + return PublishTrigger::Interval; }; if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { continue; @@ -204,12 +217,50 @@ impl Publisher { self.clear_publish_state(); tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; self.drain_location_events(locations); - return; + return PublishTrigger::Location; } } } } + async fn retry_changed_publish( + &self, + locations: &mut h3x::dquic::qinterface::component::location::Observer, + ) { + let deadline = Instant::now() + PUBLISH_CHANGE_RETRY_WINDOW; + + while Instant::now() < deadline { + let retry_delay = tokio::time::sleep(PUBLISH_CHANGE_RETRY_DELAY); + tokio::pin!(retry_delay); + + loop { + tokio::select! { + _ = &mut retry_delay => break, + event = locations.recv() => { + let Some((bind_uri, event)) = event else { + break; + }; + if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { + continue; + } + if !Self::location_event_requires_publish(&event) { + continue; + } + self.clear_publish_state(); + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; + self.drain_location_events(locations); + } + } + } + + if self.publish_attempt().await { + self.settle_publish_events(locations).await; + return; + } + self.settle_publish_events(locations).await; + } + } + fn drain_location_events( &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, @@ -378,7 +429,7 @@ impl Publisher { continue; }; for iface in ifaces { - if let Some(endpoint) = endpoint_from_iface(&iface) { + for endpoint in public_endpoints_from_iface(&iface) { endpoints.insert(endpoint); } } @@ -410,22 +461,35 @@ impl Publisher { } } -fn endpoint_from_iface(iface: &h3x::dquic::net::BindInterface) -> Option { - use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; +enum PublishTrigger { + Interval, + Location, +} + +fn public_endpoints_from_iface(iface: &h3x::dquic::net::BindInterface) -> Vec { + use h3x::dquic::qtraversal::nat::client::StunClientsComponent; iface.with_components(|components, current| { - if let Some(stun) = components.get::() - && let Some((agent, outer)) = stun.with_clients(|clients| { - clients.values().find_map(|client| { + let _ = current; + let Some(stun) = components.get::() else { + return Vec::new(); + }; + + stun.with_clients(|clients| { + clients + .values() + .filter_map(|client| { let outer = client.get_outer_addr()?.ok()?; - Some((client.agent_addr(), outer)) + match client.get_nat_type() { + Some(Ok(NatType::FullCone)) => Some(EndpointAddr::direct(outer)), + Some(Ok(_)) | None => { + Some(EndpointAddr::with_agent(client.agent_addr(), outer)) + } + Some(Err(_)) => None, + } }) - }) - { - return Some(EndpointAddr::with_agent(agent, outer)); - } - - current.bound_addr().ok().map(EndpointAddr::direct) + .collect() + }) }) } @@ -560,6 +624,25 @@ mod tests { assert!(endpoint.is_signed()); } + #[tokio::test] + async fn public_endpoints_do_not_fall_back_to_local_bound_addresses() { + let network = h3x::dquic::Network::builder().build(); + let bind_pattern: h3x::dquic::binds::BindPattern = + "inet://127.0.0.1:0".parse().expect("valid bind pattern"); + let _bind = network.bind(bind_pattern.clone()).await; + let publisher = Publisher::new( + Arc::new(TestAgent), + network, + Arc::new(DisplayOnlyResolver), + Arc::new(vec![bind_pattern]), + ); + + assert!( + publisher.public_endpoints().is_empty(), + "public DNS publishing must wait for STUN-derived external endpoints; local addresses are published through mDNS" + ); + } + #[cfg(feature = "http-resolver")] #[tokio::test] async fn run_drains_events_generated_during_publish_attempt() { @@ -644,4 +727,78 @@ mod tests { "publish-generated location events must not trigger another immediate publish" ); } + + #[cfg(feature = "http-resolver")] + #[tokio::test] + async fn run_retries_location_publish_after_timeout() { + async fn wait_for_count(count: &AtomicUsize, target: usize) { + loop { + if count.load(Ordering::SeqCst) >= target { + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + } + + let network = h3x::dquic::Network::builder().build(); + let bind_uri: h3x::dquic::net::BindUri = + "inet://127.0.0.1:0".parse().expect("valid bind uri"); + let publish_count = Arc::new(AtomicUsize::new(0)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test http server"); + let port = listener.local_addr().expect("local addr").port(); + let server_count = publish_count.clone(); + let server = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + let current = server_count.fetch_add(1, Ordering::SeqCst) + 1; + let mut buf = [0_u8; 1024]; + let _ = stream.read(&mut buf).await; + if current == 2 { + tokio::time::sleep(Duration::from_millis(200)).await; + } + let _ = stream + .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + .await; + } + }); + + let resolver = Arc::new( + crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + .expect("valid http resolver"), + ); + let mut publisher = Publisher::new( + Arc::new(TestAgent), + network.clone(), + resolver, + Arc::new(vec![ + "inet://127.0.0.1:0".parse().expect("valid bind pattern"), + ]), + ) + .with_publish_timeout(Duration::from_millis(50)); + publisher.interval = Duration::from_secs(60); + + let publisher = tokio::spawn(async move { + publisher.run().await; + }); + + wait_for_count(&publish_count, 1).await; + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; + network.locations().upsert( + bind_uri, + Arc::new(Ok::( + "127.0.0.1:0".parse().expect("valid socket addr"), + )), + ); + + tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 3)) + .await + .expect("location-triggered publish should be retried after timeout"); + + publisher.abort(); + server.abort(); + } } From 20aa273f4a0df36f5eead9cc4c1a9e7e581dcb35 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 07:14:09 +0800 Subject: [PATCH 33/85] fix(publisher): fall back to default route endpoint --- ddns/src/publisher.rs | 65 ++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 8fa567d..436e4db 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -429,7 +429,7 @@ impl Publisher { continue; }; for iface in ifaces { - for endpoint in public_endpoints_from_iface(&iface) { + for endpoint in public_endpoints_from_iface(&self.network, &iface) { endpoints.insert(endpoint); } } @@ -466,30 +466,51 @@ enum PublishTrigger { Location, } -fn public_endpoints_from_iface(iface: &h3x::dquic::net::BindInterface) -> Vec { - use h3x::dquic::qtraversal::nat::client::StunClientsComponent; +fn public_endpoints_from_iface( + network: &h3x::dquic::Network, + iface: &h3x::dquic::net::BindInterface, +) -> Vec { + use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; iface.with_components(|components, current| { - let _ = current; - let Some(stun) = components.get::() else { - return Vec::new(); - }; - - stun.with_clients(|clients| { - clients - .values() - .filter_map(|client| { - let outer = client.get_outer_addr()?.ok()?; - match client.get_nat_type() { - Some(Ok(NatType::FullCone)) => Some(EndpointAddr::direct(outer)), - Some(Ok(_)) | None => { - Some(EndpointAddr::with_agent(client.agent_addr(), outer)) - } - Some(Err(_)) => None, - } + let stun_endpoints: Vec = components + .get::() + .map(|stun| { + stun.with_clients(|clients| { + clients + .values() + .filter_map(|client| { + let outer = client.get_outer_addr()?.ok()?; + match client.get_nat_type() { + Some(Ok(NatType::FullCone)) => Some(EndpointAddr::direct(outer)), + Some(Ok(_)) | None => { + Some(EndpointAddr::with_agent(client.agent_addr(), outer)) + } + Some(Err(_)) => None, + } + }) + .collect() }) - .collect() - }) + }) + .unwrap_or_default(); + if !stun_endpoints.is_empty() { + return stun_endpoints; + } + + // If STUN has not produced an external endpoint yet, publish the + // current default-route address as a temporary direct endpoint. This + // preserves reachability during local topology changes while avoiding + // staging-only/private management links that do not have a default + // route. Once STUN converges, its endpoint replaces this fallback. + let addr = match current.bound_addr() { + Ok(addr) => addr, + Err(_) => return Vec::new(), + }; + if network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) { + vec![EndpointAddr::direct(addr)] + } else { + Vec::new() + } }) } From f8f94938fb94ac06b703f038fe6410733f0e8239 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 07:47:38 +0800 Subject: [PATCH 34/85] fix(publisher): retry failed publishes with endpoints --- ddns/src/publisher.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 436e4db..17f2373 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -156,7 +156,9 @@ impl Publisher { let trigger = self.wait_next_publish_trigger(&mut locations).await; let published = self.publish_attempt().await; self.settle_publish_events(&mut locations).await; - if matches!(trigger, PublishTrigger::Location) && !published { + if !published + && (matches!(trigger, PublishTrigger::Location) || self.has_public_endpoints()) + { self.retry_changed_publish(&mut locations).await; } } @@ -437,6 +439,10 @@ impl Publisher { endpoints.into_iter().collect() } + fn has_public_endpoints(&self) -> bool { + !self.public_endpoints().is_empty() + } + #[cfg(feature = "mdns-resolver")] fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { let mut endpoints = HashSet::new(); From 234e904e0f5b82f6e1c7809e0e8c432bab9a22c6 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 08:09:28 +0800 Subject: [PATCH 35/85] fix(resolver): bound h3 lookup retries --- ddns/src/resolvers/h3.rs | 90 +++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 7143405..42970e1 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -12,6 +12,9 @@ use tokio::time::Instant; use tracing::trace; use url::Url; +const LOOKUP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const LOOKUP_REQUEST_ATTEMPTS: usize = 3; + pub struct H3Resolver { endpoint: Arc>, base_url: Url, @@ -53,6 +56,8 @@ pub enum Error { H3Request { source: h3x::endpoint::client::RequestError, }, + #[snafu(display("h3 request timed out after {timeout:?}"))] + RequestTimeout { timeout: Duration }, #[snafu(display("{status}"))] Status { status: http::StatusCode }, @@ -158,6 +163,70 @@ where Ok(()) } + fn retryable_lookup_error(error: &Error) -> bool { + matches!(error, Error::H3Request { .. } | Error::H3Stream { .. }) + } + + async fn lookup_response(&self, uri: http::Uri) -> Result> { + let mut resp = match self.endpoint.get(uri).await { + Ok(resp) => resp, + Err(source) => return Err(self.request_error(source)), + }; + + tracing::trace!("received response with status {}", resp.status()); + match resp.status() { + http::StatusCode::OK => {} + http::StatusCode::NOT_FOUND => return Err(Error::NoRecordFound), + status => return Err(Error::Status { status }), + } + + match resp.read_to_bytes().await { + Ok(response) => Ok(response), + Err(source) => Err(Error::H3Stream { source }), + } + } + + async fn lookup_response_with_retry( + &self, + uri: http::Uri, + ) -> Result> { + for attempt in 1..=LOOKUP_REQUEST_ATTEMPTS { + match tokio::time::timeout(LOOKUP_REQUEST_TIMEOUT, self.lookup_response(uri.clone())) + .await + { + Ok(Ok(response)) => return Ok(response), + Ok(Err(error)) + if Self::retryable_lookup_error(&error) + && attempt < LOOKUP_REQUEST_ATTEMPTS => + { + self.endpoint.clear_pool(); + tracing::debug!( + attempt, + timeout_ms = LOOKUP_REQUEST_TIMEOUT.as_millis(), + "h3 dns lookup failed, retrying" + ); + } + Ok(Err(error)) => return Err(error), + Err(_elapsed) if attempt < LOOKUP_REQUEST_ATTEMPTS => { + self.endpoint.clear_pool(); + tracing::debug!( + attempt, + timeout_ms = LOOKUP_REQUEST_TIMEOUT.as_millis(), + "h3 dns lookup timed out, retrying" + ); + } + Err(_elapsed) => { + self.endpoint.clear_pool(); + return Err(Error::RequestTimeout { + timeout: LOOKUP_REQUEST_TIMEOUT, + }); + } + } + } + + unreachable!("lookup retry loop returns on the final attempt") + } + pub const EXCLUDED_DOMAINS: [&str; 2] = ["dns.genmeta.net", "download.genmeta.net"]; pub async fn lookup(&self, name: &str) -> Result> { @@ -197,26 +266,15 @@ where let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("sending lookup request to {}", self.base_url); - let mut resp = match self.endpoint.get(uri).await { - Ok(resp) => resp, - Err(source) => return Err(self.request_error(source)), - }; - - tracing::trace!("received response with status {}", resp.status()); - match resp.status() { - http::StatusCode::OK => {} - http::StatusCode::NOT_FOUND => { + let response = match self.lookup_response_with_retry(uri).await { + Ok(response) => response, + Err(Error::NoRecordFound) => { self.negative_cache .insert(domain.to_string(), now + negative_ttl); return Err(Error::NoRecordFound); } - status => return Err(Error::Status { status }), - } - - let response = resp - .read_to_bytes() - .await - .map_err(|source| Error::H3Stream { source })?; + Err(error) => return Err(error), + }; // Server always returns multi-record format. let (_remain, multi) = From 259c2f1e6ab4cef3f474a649fb864ee6cbbfdb6e Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 08:23:42 +0800 Subject: [PATCH 36/85] fix(publisher): preserve publishable location changes --- ddns/src/publisher.rs | 106 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 17f2373..d77d8af 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -150,14 +150,17 @@ impl Publisher { pub async fn run(&self) -> ! { let mut locations = self.network.locations().subscribe(); self.publish_attempt().await; - self.settle_publish_events(&mut locations).await; + if self.settle_publish_events(&mut locations).await { + self.retry_changed_publish(&mut locations).await; + } loop { let trigger = self.wait_next_publish_trigger(&mut locations).await; let published = self.publish_attempt().await; - self.settle_publish_events(&mut locations).await; - if !published - && (matches!(trigger, PublishTrigger::Location) || self.has_public_endpoints()) + let changed_during_publish = self.settle_publish_events(&mut locations).await; + if changed_during_publish + || (!published + && (matches!(trigger, PublishTrigger::Location) || self.has_public_endpoints())) { self.retry_changed_publish(&mut locations).await; } @@ -256,8 +259,10 @@ impl Publisher { } if self.publish_attempt().await { - self.settle_publish_events(locations).await; - return; + if !self.settle_publish_events(locations).await { + return; + } + continue; } self.settle_publish_events(locations).await; } @@ -266,7 +271,8 @@ impl Publisher { fn drain_location_events( &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) { + ) -> bool { + let mut requires_publish = false; while let Ok((bind_uri, event)) = locations.try_recv() { if !self .bind_patterns @@ -277,16 +283,18 @@ impl Publisher { } if Self::location_event_requires_publish(&event) { self.clear_publish_state(); + requires_publish = true; } } + requires_publish } async fn settle_publish_events( &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) { + ) -> bool { tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; - self.drain_location_events(locations); + self.drain_location_events(locations) } fn location_event_requires_publish(event: &AddressEvent) -> bool { @@ -670,6 +678,86 @@ mod tests { ); } + #[cfg(feature = "http-resolver")] + #[tokio::test] + async fn run_republishes_when_location_changes_during_publish_attempt() { + async fn wait_for_count(count: &AtomicUsize, target: usize) { + loop { + if count.load(Ordering::SeqCst) >= target { + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + } + + let network = h3x::dquic::Network::builder().build(); + let bind_uri: h3x::dquic::net::BindUri = + "inet://127.0.0.1:0".parse().expect("valid bind uri"); + let publish_count = Arc::new(AtomicUsize::new(0)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test http server"); + let port = listener.local_addr().expect("local addr").port(); + let server_network = network.clone(); + let server_bind_uri = bind_uri.clone(); + let server_count = publish_count.clone(); + let server = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + let current = server_count.fetch_add(1, Ordering::SeqCst) + 1; + let mut buf = [0_u8; 1024]; + let _ = stream.read(&mut buf).await; + if current == 2 { + server_network.locations().upsert( + server_bind_uri.clone(), + Arc::new(Ok::( + "127.0.0.1:10001".parse().expect("valid socket addr"), + )), + ); + } + let _ = stream + .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + .await; + } + }); + + let resolver = Arc::new( + crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + .expect("valid http resolver"), + ); + let mut publisher = Publisher::new( + Arc::new(TestAgent), + network.clone(), + resolver, + Arc::new(vec![ + "inet://127.0.0.1:0".parse().expect("valid bind pattern"), + ]), + ); + publisher.interval = Duration::from_secs(60); + + let publisher = tokio::spawn(async move { + publisher.run().await; + }); + + wait_for_count(&publish_count, 1).await; + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; + network.locations().upsert( + bind_uri, + Arc::new(Ok::( + "127.0.0.1:10000".parse().expect("valid socket addr"), + )), + ); + + tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 3)) + .await + .expect("publishable location changes during publish must trigger another publish"); + + publisher.abort(); + server.abort(); + } + #[cfg(feature = "http-resolver")] #[tokio::test] async fn run_drains_events_generated_during_publish_attempt() { From aae38657062099418c5998cb3660e25d4567eda5 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 08:42:40 +0800 Subject: [PATCH 37/85] fix(resolver): leave retry timeout margin --- ddns/src/resolvers/h3.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 42970e1..98d9050 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -12,7 +12,7 @@ use tokio::time::Instant; use tracing::trace; use url::Url; -const LOOKUP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const LOOKUP_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); const LOOKUP_REQUEST_ATTEMPTS: usize = 3; pub struct H3Resolver { @@ -355,3 +355,18 @@ where }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lookup_retry_budget_leaves_external_timeout_margin() { + let total_budget = LOOKUP_REQUEST_TIMEOUT * LOOKUP_REQUEST_ATTEMPTS as u32; + + assert!( + total_budget <= Duration::from_secs(10), + "h3 lookup must return before common 15s command timeouts so callers can retry" + ); + } +} From dcc3ccf6cce26da87e19469c26f96f6b87e7942b Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 09:25:02 +0800 Subject: [PATCH 38/85] fix(publisher): preserve nat agent for translated full cone --- ddns/src/publisher.rs | 46 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index d77d8af..d58b752 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -495,11 +495,15 @@ fn public_endpoints_from_iface( .values() .filter_map(|client| { let outer = client.get_outer_addr()?.ok()?; + let bound = current.bound_addr().ok()?; match client.get_nat_type() { - Some(Ok(NatType::FullCone)) => Some(EndpointAddr::direct(outer)), - Some(Ok(_)) | None => { - Some(EndpointAddr::with_agent(client.agent_addr(), outer)) - } + Some(Ok(nat_type)) => Some(publish_endpoint_from_stun( + bound, + client.agent_addr(), + outer, + nat_type, + )), + None => Some(EndpointAddr::with_agent(client.agent_addr(), outer)), Some(Err(_)) => None, } }) @@ -528,6 +532,19 @@ fn public_endpoints_from_iface( }) } +fn publish_endpoint_from_stun( + bound: SocketAddr, + agent: SocketAddr, + outer: SocketAddr, + nat_type: NatType, +) -> EndpointAddr { + if nat_type == NatType::FullCone && bound == outer { + EndpointAddr::direct(outer) + } else { + EndpointAddr::with_agent(agent, outer) + } +} + #[cfg(feature = "mdns-resolver")] fn local_endpoint_from_iface( iface: &h3x::dquic::net::BindInterface, @@ -678,6 +695,27 @@ mod tests { ); } + #[test] + fn full_cone_nat_endpoint_preserves_agent_when_outer_differs_from_bound_addr() { + let bound = "10.110.0.10:45635".parse().expect("valid bound addr"); + let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); + let outer = "10.10.0.10:45635".parse().expect("valid outer addr"); + + let endpoint = publish_endpoint_from_stun(bound, agent, outer, NatType::FullCone); + + assert_eq!(endpoint, EndpointAddr::with_agent(agent, outer)); + } + + #[test] + fn full_cone_endpoint_is_direct_without_address_translation() { + let bound = "10.10.0.100:45635".parse().expect("valid bound addr"); + let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); + + let endpoint = publish_endpoint_from_stun(bound, agent, bound, NatType::FullCone); + + assert_eq!(endpoint, EndpointAddr::direct(bound)); + } + #[cfg(feature = "http-resolver")] #[tokio::test] async fn run_republishes_when_location_changes_during_publish_attempt() { From 6bf1f66cd75c858a7eccf52310525d9241d70c13 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 10:23:37 +0800 Subject: [PATCH 39/85] fix(publisher): include local default-route endpoints --- ddns/src/publisher.rs | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index d58b752..ea058ee 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -487,7 +487,8 @@ fn public_endpoints_from_iface( use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; iface.with_components(|components, current| { - let stun_endpoints: Vec = components + let addr = current.bound_addr().ok(); + let mut endpoints: Vec = components .get::() .map(|stun| { stun.with_clients(|clients| { @@ -511,24 +512,20 @@ fn public_endpoints_from_iface( }) }) .unwrap_or_default(); - if !stun_endpoints.is_empty() { - return stun_endpoints; - } - // If STUN has not produced an external endpoint yet, publish the - // current default-route address as a temporary direct endpoint. This - // preserves reachability during local topology changes while avoiding - // staging-only/private management links that do not have a default - // route. Once STUN converges, its endpoint replaces this fallback. - let addr = match current.bound_addr() { - Ok(addr) => addr, - Err(_) => return Vec::new(), - }; - if network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) { - vec![EndpointAddr::direct(addr)] - } else { - Vec::new() + // Also publish the current default-route address. STUN-derived + // endpoints make the node reachable from outside the local network, + // while the bound address is still the shortest valid path for peers + // on the same link and for self-connectivity checks. Restrict this to + // default-route bindings so staging-only management links are not + // advertised. + if let Some(addr) = addr + && network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) + { + endpoints.push(EndpointAddr::direct(addr)); } + + endpoints }) } From ddbd56d36873816823c960abcd15c63e39f01c54 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 10:55:08 +0800 Subject: [PATCH 40/85] fix(publisher): preserve endpoint publish order --- ddns/src/publisher.rs | 44 ++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index ea058ee..49ef69f 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -433,18 +433,19 @@ impl Publisher { } fn public_endpoints(&self) -> Vec { - let mut endpoints = HashSet::new(); + let mut endpoints = Vec::new(); + let mut seen = HashSet::new(); for pattern in self.bind_patterns.iter() { let Some(ifaces) = self.network.get_interfaces(pattern) else { continue; }; for iface in ifaces { for endpoint in public_endpoints_from_iface(&self.network, &iface) { - endpoints.insert(endpoint); + push_unique_endpoint(&mut endpoints, &mut seen, endpoint); } } } - endpoints.into_iter().collect() + endpoints } fn has_public_endpoints(&self) -> bool { @@ -480,6 +481,16 @@ enum PublishTrigger { Location, } +fn push_unique_endpoint( + endpoints: &mut Vec, + seen: &mut HashSet, + endpoint: EndpointAddr, +) { + if seen.insert(endpoint) { + endpoints.push(endpoint); + } +} + fn public_endpoints_from_iface( network: &h3x::dquic::Network, iface: &h3x::dquic::net::BindInterface, @@ -487,7 +498,6 @@ fn public_endpoints_from_iface( use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; iface.with_components(|components, current| { - let addr = current.bound_addr().ok(); let mut endpoints: Vec = components .get::() .map(|stun| { @@ -513,13 +523,8 @@ fn public_endpoints_from_iface( }) .unwrap_or_default(); - // Also publish the current default-route address. STUN-derived - // endpoints make the node reachable from outside the local network, - // while the bound address is still the shortest valid path for peers - // on the same link and for self-connectivity checks. Restrict this to - // default-route bindings so staging-only management links are not - // advertised. - if let Some(addr) = addr + if endpoints.is_empty() + && let Ok(addr) = current.bound_addr() && network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) { endpoints.push(EndpointAddr::direct(addr)); @@ -692,6 +697,23 @@ mod tests { ); } + #[test] + fn push_unique_endpoint_preserves_first_seen_order() { + let agent = EndpointAddr::with_agent( + "10.10.0.2:20004".parse().expect("valid agent addr"), + "10.10.0.10:45635".parse().expect("valid outer addr"), + ); + let direct = EndpointAddr::direct("10.110.0.10:45635".parse().expect("valid direct addr")); + let mut endpoints = Vec::new(); + let mut seen = HashSet::new(); + + push_unique_endpoint(&mut endpoints, &mut seen, agent); + push_unique_endpoint(&mut endpoints, &mut seen, direct); + push_unique_endpoint(&mut endpoints, &mut seen, agent); + + assert_eq!(endpoints, vec![agent, direct]); + } + #[test] fn full_cone_nat_endpoint_preserves_agent_when_outer_differs_from_bound_addr() { let bound = "10.110.0.10:45635".parse().expect("valid bound addr"); From 3a48e05c6e39c0967a7fc40560c815939be0c868 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 11:13:12 +0800 Subject: [PATCH 41/85] fix(publisher): append direct endpoint after stun --- ddns/src/publisher.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 49ef69f..47cd8b8 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -498,6 +498,7 @@ fn public_endpoints_from_iface( use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; iface.with_components(|components, current| { + let addr = current.bound_addr().ok(); let mut endpoints: Vec = components .get::() .map(|stun| { @@ -523,8 +524,13 @@ fn public_endpoints_from_iface( }) .unwrap_or_default(); - if endpoints.is_empty() - && let Ok(addr) = current.bound_addr() + // Also publish the current default-route address. STUN-derived + // endpoints make the node reachable from outside the local network, + // while the bound address is still the shortest valid path for peers + // on the same link and for separate local client processes on the + // same host. Keep it after STUN endpoints so translated-NAT peers get + // the externally reachable candidate first. + if let Some(addr) = addr && network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) { endpoints.push(EndpointAddr::direct(addr)); From dbfc9e57e72d23224a1570e067d97d66e1d23c51 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 13:36:48 +0800 Subject: [PATCH 42/85] refactor(publisher): simplify publish loop and update stun name --- README.md | 2 +- ddns-server/server.toml | 4 +- ddns/examples/README.md | 4 +- ddns/examples/query.rs | 2 +- ddns/src/publisher.rs | 113 +++++++++++++--------------------------- 5 files changed, 41 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index c253a2f..e3eb23e 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Query DNS service records from an HTTP/3 DNS server: ```bash cargo run -p ddns --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ - --host stun.genmeta.net + --host nat.genmeta.net ``` ### Running the DNS Server diff --git a/ddns-server/server.toml b/ddns-server/server.toml index ad72b76..d819957 100644 --- a/ddns-server/server.toml +++ b/ddns-server/server.toml @@ -42,14 +42,14 @@ ttl_secs = 30 # --------------------------------------------------------------------------- [[domain_policies]] -host = "stun.genmeta.net" +host = "nat.genmeta.net" policy = "open_multi" # Static bootstrap STUN endpoints returned even before any node publishes. # Ordering keeps the main :20002 endpoints ahead of the auxiliary :20003 endpoints. [[seed_records]] -host = "stun.genmeta.net" +host = "nat.genmeta.net" endpoints = [] # Add more rules as needed, e.g.: diff --git a/ddns/examples/README.md b/ddns/examples/README.md index 1303a00..f4370b8 100644 --- a/ddns/examples/README.md +++ b/ddns/examples/README.md @@ -73,13 +73,13 @@ Use the `query` example to query DNS service records from the HTTP/3 DNS server. #### Program Parameters - `--base-url `: Base URL of the DNS server (default: `https://dns.genmeta.net:4433/`). - `--server-ca `: CA certificate PEM file path for verifying the online server certificate. -- `--host `: DNS name to query (default: `stun.genmeta.net`). +- `--host `: DNS name to query (default: `nat.genmeta.net`). #### Example Run Command ```bash cargo run -p ddns --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ - --host stun.genmeta.net + --host nat.genmeta.net ``` This command sends a GET or POST request to the server, the request body contains the DNS query message, the server returns matching records. diff --git a/ddns/examples/query.rs b/ddns/examples/query.rs index 2c15556..8299dbb 100644 --- a/ddns/examples/query.rs +++ b/ddns/examples/query.rs @@ -29,7 +29,7 @@ struct Options { server_ca: PathBuf, /// 要查询的线上域名。 - #[arg(long, default_value = "stun.genmeta.net")] + #[arg(long, default_value = "nat.genmeta.net")] host: String, } diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 47cd8b8..b25a16f 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -4,7 +4,7 @@ use std::{ io, net::SocketAddr, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; use ddns_core::{ @@ -32,11 +32,6 @@ pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// independent: the next interval observes the current bindings again. pub const DEFAULT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); const PUBLISH_CHANGE_DEBOUNCE: Duration = Duration::from_millis(500); -#[cfg(not(test))] -const PUBLISH_CHANGE_RETRY_DELAY: Duration = Duration::from_secs(2); -#[cfg(test)] -const PUBLISH_CHANGE_RETRY_DELAY: Duration = Duration::from_millis(20); -const PUBLISH_CHANGE_RETRY_WINDOW: Duration = Duration::from_secs(90); #[derive(Debug, Snafu)] #[snafu(module(create_publisher_error))] @@ -149,21 +144,13 @@ impl Publisher { pub async fn run(&self) -> ! { let mut locations = self.network.locations().subscribe(); - self.publish_attempt().await; - if self.settle_publish_events(&mut locations).await { - self.retry_changed_publish(&mut locations).await; - } + let _ = self.publish_attempt().await; + let _ = self.settle_publish_events(&mut locations).await; loop { - let trigger = self.wait_next_publish_trigger(&mut locations).await; - let published = self.publish_attempt().await; - let changed_during_publish = self.settle_publish_events(&mut locations).await; - if changed_during_publish - || (!published - && (matches!(trigger, PublishTrigger::Location) || self.has_public_endpoints())) - { - self.retry_changed_publish(&mut locations).await; - } + self.wait_next_publish_trigger(&mut locations).await; + let _ = self.publish_attempt().await; + let _ = self.settle_publish_events(&mut locations).await; } } @@ -196,17 +183,17 @@ impl Publisher { async fn wait_next_publish_trigger( &self, locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) -> PublishTrigger { + ) { let interval = tokio::time::sleep(self.interval); tokio::pin!(interval); loop { tokio::select! { - _ = &mut interval => return PublishTrigger::Interval, + _ = &mut interval => return, event = locations.recv() => { let Some((bind_uri, event)) = event else { interval.await; - return PublishTrigger::Interval; + return; }; if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { continue; @@ -222,49 +209,9 @@ impl Publisher { self.clear_publish_state(); tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; self.drain_location_events(locations); - return PublishTrigger::Location; - } - } - } - } - - async fn retry_changed_publish( - &self, - locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) { - let deadline = Instant::now() + PUBLISH_CHANGE_RETRY_WINDOW; - - while Instant::now() < deadline { - let retry_delay = tokio::time::sleep(PUBLISH_CHANGE_RETRY_DELAY); - tokio::pin!(retry_delay); - - loop { - tokio::select! { - _ = &mut retry_delay => break, - event = locations.recv() => { - let Some((bind_uri, event)) = event else { - break; - }; - if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { - continue; - } - if !Self::location_event_requires_publish(&event) { - continue; - } - self.clear_publish_state(); - tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; - self.drain_location_events(locations); - } - } - } - - if self.publish_attempt().await { - if !self.settle_publish_events(locations).await { return; } - continue; } - self.settle_publish_events(locations).await; } } @@ -448,10 +395,6 @@ impl Publisher { endpoints } - fn has_public_endpoints(&self) -> bool { - !self.public_endpoints().is_empty() - } - #[cfg(feature = "mdns-resolver")] fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { let mut endpoints = HashSet::new(); @@ -476,11 +419,6 @@ impl Publisher { } } -enum PublishTrigger { - Interval, - Location, -} - fn push_unique_endpoint( endpoints: &mut Vec, seen: &mut HashSet, @@ -743,7 +681,7 @@ mod tests { #[cfg(feature = "http-resolver")] #[tokio::test] - async fn run_republishes_when_location_changes_during_publish_attempt() { + async fn run_treats_location_publish_attempts_as_independent() { async fn wait_for_count(count: &AtomicUsize, target: usize) { loop { if count.load(Ordering::SeqCst) >= target { @@ -813,12 +751,23 @@ mod tests { )), ); - tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 3)) + tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 2)) .await - .expect("publishable location changes during publish must trigger another publish"); + .expect("publishable location changes should trigger the next independent publish"); + + let third_publish = tokio::time::timeout( + PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(500), + wait_for_count(&publish_count, 3), + ) + .await; publisher.abort(); server.abort(); + + assert!( + third_publish.is_err(), + "location events generated by a publish attempt must not trigger an immediate retry" + ); } #[cfg(feature = "http-resolver")] @@ -908,7 +857,7 @@ mod tests { #[cfg(feature = "http-resolver")] #[tokio::test] - async fn run_retries_location_publish_after_timeout() { + async fn run_does_not_retry_location_publish_after_timeout() { async fn wait_for_count(count: &AtomicUsize, target: usize) { loop { if count.load(Ordering::SeqCst) >= target { @@ -972,11 +921,19 @@ mod tests { )), ); - tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 3)) - .await - .expect("location-triggered publish should be retried after timeout"); + wait_for_count(&publish_count, 2).await; + let third_publish = tokio::time::timeout( + PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(500), + wait_for_count(&publish_count, 3), + ) + .await; publisher.abort(); server.abort(); + + assert!( + third_publish.is_err(), + "timed out location-triggered publish must not be retried before the next interval" + ); } } From 867950f61298f438939e1594533c6c158b6b917a Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 14:52:18 +0800 Subject: [PATCH 43/85] refactor(h3): use raw h3x hyper endpoint api --- ddns-server/Cargo.toml | 3 + ddns-server/src/lookup.rs | 59 +++++----- ddns-server/src/main.rs | 77 ++++++++++--- ddns-server/src/publish.rs | 222 +++++++++++++++++-------------------- ddns/Cargo.toml | 6 +- ddns/examples/query.rs | 19 +++- ddns/src/resolvers/h3.rs | 74 ++++++++++--- 7 files changed, 276 insertions(+), 184 deletions(-) diff --git a/ddns-server/Cargo.toml b/ddns-server/Cargo.toml index 77145e9..e5f6087 100644 --- a/ddns-server/Cargo.toml +++ b/ddns-server/Cargo.toml @@ -12,6 +12,7 @@ ddns = { path = "../ddns", features = ["h3x-resolver"] } dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", features = [ "dquic", + "hyper", ] } # server-specific deps(不再污染核心库) @@ -23,6 +24,8 @@ bytes = "1" base64 = "0.22" clap = { version = "4", features = ["derive"] } http = "1" +http-body-util = "0.1" +tower-service = "0.3" nom = "8" rustls = { version = "0.23", default-features = false, features = [ "logging", diff --git a/ddns-server/src/lookup.rs b/ddns-server/src/lookup.rs index 8063fa4..01935df 100644 --- a/ddns-server/src/lookup.rs +++ b/ddns-server/src/lookup.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + convert::Infallible, net::SocketAddr, }; @@ -9,8 +10,8 @@ use ddns::{ wire::MultiResponse, }; use deadpool_redis::redis::{self, AsyncCommands}; -use futures::future::BoxFuture; -use h3x::endpoint::server::{Request, Response, Service}; +use h3x::message::stream::MessageStreamError; +use http_body_util::{Full, combinators::UnsyncBoxBody}; use tracing::debug; use crate::{ @@ -18,6 +19,9 @@ use crate::{ storage::{AppState, LookupRecord, Storage, StoredRecord, unix_now_secs}, }; +pub type Request = http::Request>; +pub type Response = http::Response>; + // --------------------------------------------------------------------------- // Lookup result type // --------------------------------------------------------------------------- @@ -164,10 +168,15 @@ async fn perform_lookup_multi( // HTTP response helpers // --------------------------------------------------------------------------- -pub async fn write_error(resp: &mut Response, err: AppError) { - resp.set_status(err.status()) - .set_body(bytes::Bytes::from(format!("{}", err))); - let _ = resp.flush().await; +pub fn body_response(status: http::StatusCode, body: impl Into) -> Response { + http::Response::builder() + .status(status) + .body(Full::new(body.into())) + .expect("response parts must be valid") +} + +pub fn write_error(err: AppError) -> Response { + body_response(err.status(), bytes::Bytes::from(err.to_string())) } // --------------------------------------------------------------------------- @@ -187,11 +196,10 @@ pub struct LookupSvc { /// /// Optional query param `limit=N` caps the number of records returned. /// Dynamic records are newest-first; configured seed records are appended after them. -pub async fn lookup_with_cert(state: AppState, request: &mut Request, response: &mut Response) { - let params = parse_query_params(&request.uri()); +pub async fn lookup_with_cert(state: AppState, request: Request) -> Response { + let params = parse_query_params(request.uri()); let Some(host) = params.get("host") else { - write_error(response, AppError::MissingHostParam).await; - return; + return write_error(AppError::MissingHostParam); }; let limit: Option = params @@ -204,38 +212,33 @@ pub async fn lookup_with_cert(state: AppState, request: &mut Request, response: match perform_lookup(&state, host, limit).await { Ok(LookupResult::NotFound) => { debug!(host = %host, "lookup.not_found"); - response - .set_status(http::StatusCode::NOT_FOUND) - .set_body(bytes::Bytes::from_static(b"Not Found")); - let _ = response.flush().await; + body_response( + http::StatusCode::NOT_FOUND, + bytes::Bytes::from_static(b"Not Found"), + ) } Ok(LookupResult::Multi(resp)) => { let body = resp.encode(); debug!(host = %host, records = resp.records.len(), "lookup.found"); - response - .set_status(http::StatusCode::OK) - .set_body(bytes::Bytes::from(body)); + let mut response = body_response(http::StatusCode::OK, bytes::Bytes::from(body)); response.headers_mut().insert( http::HeaderName::from_static("x-record-format"), http::HeaderValue::from_static("multi"), ); - let _ = response.flush().await; + response } - Err(e) => { - write_error(response, e).await; - } + Err(e) => write_error(e), } } -impl Service for LookupSvc { - type Future<'s> = BoxFuture<'s, ()>; - - fn serve<'s>(&self, request: &'s mut Request, response: &'s mut Response) -> Self::Future<'s> { +impl LookupSvc { + pub fn call( + &self, + request: Request, + ) -> impl Future> + Send + 'static { let state = self.state.clone(); - Box::pin(async move { - lookup_with_cert(state, request, response).await; - }) + async move { Ok(lookup_with_cert(state, request).await) } } } diff --git a/ddns-server/src/main.rs b/ddns-server/src/main.rs index 404e7bb..d12b489 100644 --- a/ddns-server/src/main.rs +++ b/ddns-server/src/main.rs @@ -5,10 +5,18 @@ mod policy; mod publish; mod storage; -use std::{collections::HashMap, io, net::SocketAddr, str::FromStr, sync::Arc}; +use std::{ + collections::HashMap, + io, + net::SocketAddr, + str::FromStr, + sync::Arc, + task::{Context, Poll}, +}; use clap::Parser; use ddns::{MdnsEndpoint, MdnsPacket}; +use futures::future::BoxFuture; use h3x::{ dquic::{ Identity, Network, QuicEndpoint, @@ -16,7 +24,8 @@ use h3x::{ cert::handy::{ToCertificate, ToPrivateKey}, server::ServerQuicConfig, }, - endpoint::{H3Endpoint, server::Router}, + endpoint::H3Endpoint, + hyper::server::TowerService, }; use rustls::{RootCertStore, server::WebPkiClientVerifier}; use tracing::{info, level_filters::LevelFilter}; @@ -30,6 +39,49 @@ use crate::{ storage::{AppState, MemoryStorage, SeedRecords, Storage}, }; +#[derive(Clone)] +struct DnsService { + publish: PublishSvc, + lookup: LookupSvc, +} + +impl tower_service::Service for DnsService { + type Response = lookup::Response; + type Error = io::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, request: lookup::Request) -> Self::Future { + let method = request.method().clone(); + let path = request.uri().path().to_owned(); + let publish = self.publish.clone(); + let lookup = self.lookup.clone(); + Box::pin(async move { + match (method, path.as_str()) { + (http::Method::POST, "/publish") => match publish.call(request).await { + Ok(response) => Ok(response), + Err(never) => match never {}, + }, + (http::Method::GET, "/lookup") => match lookup.call(request).await { + Ok(response) => Ok(response), + Err(never) => match never {}, + }, + (_, "/publish" | "/lookup") => Ok(lookup::body_response( + http::StatusCode::METHOD_NOT_ALLOWED, + bytes::Bytes::from_static(b"Method Not Allowed"), + )), + _ => Ok(lookup::body_response( + http::StatusCode::NOT_FOUND, + bytes::Bytes::from_static(b"Not Found"), + )), + } + }) + } +} + fn bind_patterns_for_listen(listen: SocketAddr) -> Vec { let bind_addr = match listen { SocketAddr::V4(addr) if addr.ip().is_unspecified() => { @@ -172,19 +224,14 @@ async fn main() -> Result<(), Box> { let cert_pem = std::fs::read(&config.cert)?; let key_pem = std::fs::read(&config.key)?; - let router = Router::new() - .post( - "/publish", - PublishSvc { - state: state.clone(), - }, - ) - .get( - "/lookup", - LookupSvc { - state: state.clone(), - }, - ); + let router = TowerService(DnsService { + publish: PublishSvc { + state: state.clone(), + }, + lookup: LookupSvc { + state: state.clone(), + }, + }); let identity = Arc::new(Identity { name: config.server_name.parse().unwrap(), diff --git a/ddns-server/src/publish.rs b/ddns-server/src/publish.rs index db1b15d..7309437 100644 --- a/ddns-server/src/publish.rs +++ b/ddns-server/src/publish.rs @@ -1,13 +1,15 @@ +use std::{convert::Infallible, sync::Arc}; + use deadpool_redis::redis::{self, AsyncCommands}; use dhttp_identity::identity::RemoteAgent; -use futures::future::BoxFuture; -use h3x::endpoint::server::{Request, Response, Service}; +use h3x::{connection::ConnectionState, quic}; +use http_body_util::BodyExt; use tokio::time::{Duration, Instant}; use tracing::{debug, info, warn}; use crate::{ error::{AppError, normalize_host, parse_query_params}, - lookup::write_error, + lookup::{Request, Response, body_response, write_error}, policy::{DomainPolicy, client_allowed_host, validate_dns_packet}, storage::{ AppState, Record, Storage, StoredRecord, cert_fingerprint, cert_fingerprint_hex, @@ -24,101 +26,104 @@ pub struct PublishSvc { pub state: AppState, } -impl Service for PublishSvc { - type Future<'s> = BoxFuture<'s, ()>; - - fn serve<'s>(&self, request: &'s mut Request, response: &'s mut Response) -> Self::Future<'s> { +impl PublishSvc { + pub fn call( + &self, + request: Request, + ) -> impl Future> + Send + 'static { let state = self.state.clone(); - Box::pin(async move { - debug!("received publish request"); + async move { Ok(publish_with_cert(state, request).await) } + } +} - let params = parse_query_params(&request.uri()); - debug!("query params: {:?}", params); +async fn publish_with_cert(state: AppState, request: Request) -> Response { + debug!("received publish request"); - let Some(host) = params.get("host") else { - warn!("missing host parameter"); - write_error(response, AppError::MissingHostParam).await; - return; - }; + let params = parse_query_params(request.uri()); + debug!("query params: {:?}", params); - let host = match normalize_host(host) { - Ok(h) => h, - Err(e) => { - write_error(response, e).await; - return; - } - }; - debug!(host = %host, "publish.host"); + let Some(host) = params.get("host") else { + warn!("missing host parameter"); + return write_error(AppError::MissingHostParam); + }; - // Require a valid client certificate for all publish requests. - let Some(agent) = request.agent().cloned() else { - warn!("missing client certificate"); - write_error(response, AppError::MissingClientCertificate).await; - return; - }; + let host = match normalize_host(host) { + Ok(h) => h, + Err(e) => return write_error(e), + }; + debug!(host = %host, "publish.host"); - let policy = state.policies.policy_for(&host).clone(); - - // Standard policy: cert SAN must match the target host. - // OpenMulti policy: any authenticated node may publish — skip SAN check. - if policy == DomainPolicy::Standard { - let allowed = match client_allowed_host(agent.as_ref()) { - Ok(h) => h, - Err(e) => { - warn!(error = %snafu::Report::from_error(&e), "client certificate domain not allowed"); - write_error(response, e).await; - return; - } - }; - if allowed != host { - warn!(allowed = %allowed, requested = %host, "publish.host_mismatch"); - write_error(response, AppError::HostMismatch).await; - return; - } + // Require a valid client certificate for all publish requests. + let agent = match request_connection(&request) { + Some(connection) => match connection.remote_agent().await { + Ok(Some(agent)) => agent, + Ok(None) => { + warn!("missing client certificate"); + return write_error(AppError::MissingClientCertificate); } - - let body = match request.read_to_bytes().await { - Ok(b) => b, - Err(e) => { - warn!(error = %snafu::Report::from_error(&e), "failed to read request body"); - write_error( - response, - AppError::InvalidDnsPacket { - message: e.to_string(), - }, - ) - .await; - return; - } - }; - - // Validate DNS packet; signature check only for Standard hosts. - let require_sig = policy == DomainPolicy::Standard && state.require_signature; - let packet_name = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) - { - Ok(n) => n, - Err(e) => { - write_error(response, e).await; - return; - } - }; - - let packet_host = match normalize_host(&packet_name) { - Ok(h) => h, - Err(e) => { - write_error(response, e).await; - return; - } - }; - - if packet_host != host { - write_error(response, AppError::HostMismatch).await; - return; + Err(error) => { + warn!(error = %snafu::Report::from_error(&error), "failed to read client certificate"); + return write_error(AppError::MissingClientCertificate); + } + }, + None => { + warn!("missing client certificate"); + return write_error(AppError::MissingClientCertificate); + } + }; + + let policy = state.policies.policy_for(&host).clone(); + + // Standard policy: cert SAN must match the target host. + // OpenMulti policy: any authenticated node may publish — skip SAN check. + if policy == DomainPolicy::Standard { + let allowed = match client_allowed_host(agent.as_ref()) { + Ok(h) => h, + Err(e) => { + warn!(error = %snafu::Report::from_error(&e), "client certificate domain not allowed"); + return write_error(e); } + }; + if allowed != host { + warn!(allowed = %allowed, requested = %host, "publish.host_mismatch"); + return write_error(AppError::HostMismatch); + } + } - publish_record(&state, &host, &body, agent.as_ref(), response).await - }) + let body = match request.into_body().collect().await { + Ok(body) => body.to_bytes(), + Err(e) => { + warn!(error = %snafu::Report::from_error(&e), "failed to read request body"); + return write_error(AppError::InvalidDnsPacket { + message: e.to_string(), + }); + } + }; + + // Validate DNS packet; signature check only for Standard hosts. + let require_sig = policy == DomainPolicy::Standard && state.require_signature; + let packet_name = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) { + Ok(n) => n, + Err(e) => return write_error(e), + }; + + let packet_host = match normalize_host(&packet_name) { + Ok(h) => h, + Err(e) => return write_error(e), + }; + + if packet_host != host { + return write_error(AppError::HostMismatch); } + + publish_record(&state, &host, &body, agent.as_ref()).await +} + +fn request_connection(request: &Request) -> Option>> { + request + .extensions() + .get::>>() + .cloned() } /// Unified publish handler: stores the record keyed by (host, cert-fingerprint). @@ -137,8 +142,7 @@ pub async fn publish_record( host: &str, body: &bytes::Bytes, agent: &(impl RemoteAgent + ?Sized), - response: &mut Response, -) { +) -> Response { let cert_bytes = agent .cert_chain() .first() @@ -153,14 +157,9 @@ pub async fn publish_record( let mut conn = match pool.get().await { Ok(c) => c, Err(e) => { - write_error( - response, - AppError::Redis { - message: e.to_string(), - }, - ) - .await; - return; + return write_error(AppError::Redis { + message: e.to_string(), + }); } }; let ttl_secs = state.ttl_secs; @@ -190,28 +189,18 @@ pub async fn publish_record( .set_ex::<_, _, ()>(&fp_key, &new_member, ttl_secs) .await { - write_error( - response, - AppError::Redis { - message: e.to_string(), - }, - ) - .await; - return; + return write_error(AppError::Redis { + message: e.to_string(), + }); } if let Err(e) = conn .zadd::<_, _, _, ()>(&set_key, &new_member, now_secs as f64) .await { - write_error( - response, - AppError::Redis { - message: e.to_string(), - }, - ) - .await; - return; + return write_error(AppError::Redis { + message: e.to_string(), + }); } // Expire the ZSET key at max(ttl_secs) from now as a safety net. @@ -249,8 +238,5 @@ pub async fn publish_record( } info!(host = %host, ttl = state.ttl_secs, bytes = body.len(), fp = %fp_hex, "publish.ok"); - response - .set_status(http::StatusCode::OK) - .set_body(bytes::Bytes::from_static(b"OK")); - let _ = response.flush().await; + body_response(http::StatusCode::OK, bytes::Bytes::from_static(b"OK")) } diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml index 9bb27f9..c7d7f9c 100644 --- a/ddns/Cargo.toml +++ b/ddns/Cargo.toml @@ -18,14 +18,16 @@ snafu = "0.8" tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } tracing = "0.1" -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"], optional = true } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic", "hyper"], optional = true } http = { version = "1", optional = true } +http-body = { version = "1", optional = true } +http-body-util = { version = "0.1", optional = true } reqwest = { version = "0.12", default-features = false, features = ["charset", "rustls-tls", "http2", "macos-system-configuration", "json"], optional = true } url = { version = "2", optional = true } [features] default = [] -h3x-resolver = ["dep:h3x", "dep:http", "dep:url"] +h3x-resolver = ["dep:h3x", "dep:http", "dep:http-body", "dep:http-body-util", "dep:url"] mdns-resolver = ["dep:h3x", "gmdns/h3x-network"] http-resolver = ["dep:reqwest"] diff --git a/ddns/examples/query.rs b/ddns/examples/query.rs index 8299dbb..ddf9b78 100644 --- a/ddns/examples/query.rs +++ b/ddns/examples/query.rs @@ -14,6 +14,7 @@ use h3x::{ }, endpoint::H3Endpoint, }; +use http_body_util::{BodyExt, Empty}; use rustls::{RootCertStore, client::WebPkiServerVerifier}; use tracing::{Level, info}; @@ -122,11 +123,23 @@ async fn main() -> Result<(), Box> { info!(url = %url, "lookup.start"); let uri: http::Uri = url.parse()?; - let client = Arc::new(client); - let mut resp = client.get(uri).await?; + let authority = uri + .authority() + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "query URL must include authority", + ) + })? + .clone(); + let connection = Arc::new(client).connect(authority).await?; + let request = http::Request::get(uri) + .body(Empty::::new()) + .expect("query request must be valid"); + let resp = connection.execute_hyper_request(request).await?; if resp.status().is_success() { - let bytes = resp.read_to_bytes().await?; + let bytes = resp.into_body().collect().await?.to_bytes(); let (_remain, multi) = be_multi_response(bytes.as_ref()).map_err(|e| { io::Error::new( diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 98d9050..940a6cb 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -1,4 +1,4 @@ -use std::{fmt, io, sync::Arc, time::Duration}; +use std::{convert::Infallible, fmt, io, sync::Arc, time::Duration}; use dashmap::DashMap; use ddns_core::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; @@ -7,7 +7,11 @@ use dquic::{ qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, }; use futures::{StreamExt, stream}; -use h3x::{dquic::ConnectError, endpoint::H3Endpoint, quic}; +use h3x::{ + dquic::ConnectError, endpoint::H3Endpoint, hyper::client::RequestError as HyperRequestError, + quic, +}; +use http_body_util::{BodyExt, Empty, Full}; use tokio::time::Instant; use tracing::trace; use url::Url; @@ -52,9 +56,11 @@ pub enum Error { H3Stream { source: h3x::endpoint::server::MessageStreamError, }, + #[snafu(display("failed to connect h3 endpoint"))] + Connect { source: h3x::pool::ConnectError }, #[snafu(display("h3 request error"))] H3Request { - source: h3x::endpoint::client::RequestError, + source: HyperRequestError, }, #[snafu(display("h3 request timed out after {timeout:?}"))] RequestTimeout { timeout: Duration }, @@ -107,17 +113,46 @@ where }) } - fn request_error( - &self, - source: h3x::endpoint::client::RequestError, - ) -> Error { + fn connect_error(&self, source: h3x::pool::ConnectError) -> Error { // H3 DNS resolvers keep a long-lived endpoint. A network transition may // leave the cached H3 connection with stale QUIC paths, so the next // attempt must establish a fresh connection instead of reusing it. + self.endpoint.clear_pool(); + Error::Connect { source } + } + + fn request_error(&self, source: HyperRequestError) -> Error { self.endpoint.clear_pool(); Error::H3Request { source } } + async fn execute_request( + &self, + request: http::Request< + impl http_body::Body + Send + 'static, + >, + ) -> Result< + http::Response< + impl http_body::Body, + >, + Error, + > { + let authority = request + .uri() + .authority() + .expect("h3 dns request URL must include an authority") + .clone(); + let connection = self + .endpoint + .connect(authority) + .await + .map_err(|source| self.connect_error(source))?; + connection + .execute_hyper_request(request) + .await + .map_err(|source| self.request_error(source)) + } + pub fn clear_pool(&self) { self.endpoint.clear_pool(); } @@ -149,10 +184,10 @@ where url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); tracing::trace!("h3x publishing packet for {} to {}", name, self.base_url); - let resp = match self.endpoint.post(uri).body(packet).await { - Ok(resp) => resp, - Err(source) => return Err(self.request_error(source)), - }; + let request = http::Request::post(uri) + .body(Full::new(bytes::Bytes::copy_from_slice(packet))) + .expect("h3 dns publish request must be valid"); + let resp = self.execute_request(request).await?; if resp.status() != http::StatusCode::OK { return Err(Error::Status { @@ -164,14 +199,17 @@ where } fn retryable_lookup_error(error: &Error) -> bool { - matches!(error, Error::H3Request { .. } | Error::H3Stream { .. }) + matches!( + error, + Error::Connect { .. } | Error::H3Request { .. } | Error::H3Stream { .. } + ) } async fn lookup_response(&self, uri: http::Uri) -> Result> { - let mut resp = match self.endpoint.get(uri).await { - Ok(resp) => resp, - Err(source) => return Err(self.request_error(source)), - }; + let request = http::Request::get(uri) + .body(Empty::::new()) + .expect("h3 dns lookup request must be valid"); + let resp = self.execute_request(request).await?; tracing::trace!("received response with status {}", resp.status()); match resp.status() { @@ -180,8 +218,8 @@ where status => return Err(Error::Status { status }), } - match resp.read_to_bytes().await { - Ok(response) => Ok(response), + match resp.into_body().collect().await { + Ok(response) => Ok(response.to_bytes()), Err(source) => Err(Error::H3Stream { source }), } } From effe065235743f0ad6fd051ea6a9e200da0fd467 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 20:18:39 +0800 Subject: [PATCH 44/85] chore: add dns publish diagnostics --- ddns-server/src/lookup.rs | 5 +++++ ddns-server/src/policy.rs | 17 +++++++++++------ ddns-server/src/publish.rs | 11 ++++++++++- ddns/src/publisher.rs | 30 +++++++++++++++++++++++++++++- ddns/src/resolvers/h3.rs | 38 ++++++++++++++++++++++++++++---------- 5 files changed, 83 insertions(+), 18 deletions(-) diff --git a/ddns-server/src/lookup.rs b/ddns-server/src/lookup.rs index 01935df..bde14b9 100644 --- a/ddns-server/src/lookup.rs +++ b/ddns-server/src/lookup.rs @@ -176,6 +176,11 @@ pub fn body_response(status: http::StatusCode, body: impl Into) -> } pub fn write_error(err: AppError) -> Response { + debug!( + status = %err.status(), + error = %err, + "writing error response" + ); body_response(err.status(), bytes::Bytes::from(err.to_string())) } diff --git a/ddns-server/src/policy.rs b/ddns-server/src/policy.rs index fb44ab8..64184d0 100644 --- a/ddns-server/src/policy.rs +++ b/ddns-server/src/policy.rs @@ -1,6 +1,6 @@ use ddns::parser::{packet::be_packet, record::RData}; use dhttp_identity::identity::RemoteAgent; -use tracing::warn; +use tracing::{debug, warn}; use crate::error::{AppError, normalize_host}; @@ -107,6 +107,10 @@ pub fn validate_dns_packet( if !remaining.is_empty() { warn!(remain = remaining.len(), "dns.parse.extra_bytes"); } + debug!( + answers = dns_packet.answers.len(), + require_signature, "validating dns packet" + ); if require_signature { let has_signature = dns_packet @@ -136,9 +140,10 @@ pub fn validate_dns_packet( } } - dns_packet - .answers - .first() - .map(|record| record.name().to_string()) - .ok_or(AppError::NoAnswersInPacket) + let Some(first_answer) = dns_packet.answers.first() else { + debug!("dns packet has no answers"); + return Err(AppError::NoAnswersInPacket); + }; + + Ok(first_answer.name().to_string()) } diff --git a/ddns-server/src/publish.rs b/ddns-server/src/publish.rs index 7309437..4446ecd 100644 --- a/ddns-server/src/publish.rs +++ b/ddns-server/src/publish.rs @@ -102,9 +102,18 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { // Validate DNS packet; signature check only for Standard hosts. let require_sig = policy == DomainPolicy::Standard && state.require_signature; + debug!( + host = %host, + bytes = body.len(), + require_signature = require_sig, + "validating publish packet" + ); let packet_name = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) { Ok(n) => n, - Err(e) => return write_error(e), + Err(e) => { + debug!(host = %host, error = %e, "publish packet rejected"); + return write_error(e); + } }; let packet_host = match normalize_host(&packet_name) { diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index b25a16f..3a2f4e4 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -131,6 +131,11 @@ impl Publisher { pub async fn publish_once(&self) -> Result<(), PublishOnceError> { let mut published = false; let public_endpoints = self.public_endpoints(); + tracing::debug!( + endpoint_count = public_endpoints.len(), + endpoints = ?public_endpoints, + "publishing public endpoints" + ); published |= self .publish_to_resolver(self.resolver.as_ref(), &public_endpoints) .await?; @@ -155,6 +160,10 @@ impl Publisher { } async fn publish_attempt(&self) -> bool { + tracing::trace!( + timeout_ms = self.publish_timeout.as_millis(), + "starting dns publish attempt" + ); match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { Ok(Ok(())) => { tracing::info!("published resolver endpoints"); @@ -350,6 +359,13 @@ impl Publisher { ) -> Result<(), PublishOnceError> { let packet = self.signed_packet(endpoints).await?; let name = self.identity.name(); + tracing::debug!( + publisher = %publisher, + name, + endpoint_count = endpoints.len(), + packet_len = packet.len(), + "publishing dns packet" + ); publisher .publish(name, &packet) .await @@ -384,6 +400,7 @@ impl Publisher { let mut seen = HashSet::new(); for pattern in self.bind_patterns.iter() { let Some(ifaces) = self.network.get_interfaces(pattern) else { + tracing::trace!(?pattern, "no interfaces for bind pattern"); continue; }; for iface in ifaces { @@ -436,6 +453,7 @@ fn public_endpoints_from_iface( use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; iface.with_components(|components, current| { + let bind_uri = current.bind_uri(); let addr = current.bound_addr().ok(); let mut endpoints: Vec = components .get::() @@ -461,6 +479,7 @@ fn public_endpoints_from_iface( }) }) .unwrap_or_default(); + let stun_endpoint_count = endpoints.len(); // Also publish the current default-route address. STUN-derived // endpoints make the node reachable from outside the local network, @@ -469,11 +488,20 @@ fn public_endpoints_from_iface( // same host. Keep it after STUN endpoints so translated-NAT peers get // the externally reachable candidate first. if let Some(addr) = addr - && network.bound_addr_is_on_default_route(¤t.bind_uri(), addr) + && network.bound_addr_is_on_default_route(&bind_uri, addr) { endpoints.push(EndpointAddr::direct(addr)); } + tracing::trace!( + bind_uri = %bind_uri, + bound_addr = ?addr, + stun_endpoint_count, + endpoint_count = endpoints.len(), + endpoints = ?endpoints, + "collected public endpoints from interface" + ); + endpoints }) } diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 940a6cb..728e4f3 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -142,15 +142,28 @@ where .authority() .expect("h3 dns request URL must include an authority") .clone(); - let connection = self - .endpoint - .connect(authority) - .await - .map_err(|source| self.connect_error(source))?; - connection - .execute_hyper_request(request) - .await - .map_err(|source| self.request_error(source)) + tracing::trace!(%authority, "connecting h3 dns endpoint"); + let connection = match self.endpoint.connect(authority.clone()).await { + Ok(connection) => { + tracing::trace!(%authority, "connected h3 dns endpoint"); + connection + } + Err(source) => return Err(self.connect_error(source)), + }; + + let method = request.method().clone(); + let uri = request.uri().clone(); + tracing::trace!(%method, %uri, "executing h3 dns request"); + match connection.execute_hyper_request(request).await { + Ok(response) => { + tracing::trace!( + status = %response.status(), + "h3 dns request response received" + ); + Ok(response) + } + Err(source) => Err(self.request_error(source)), + } } pub fn clear_pool(&self) { @@ -183,7 +196,12 @@ where let mut url = self.base_url.join("publish").expect("Invalid base URL"); url.set_query(Some(&format!("host={name}"))); let uri: http::Uri = url.as_str().parse().expect("URL should be valid URI"); - tracing::trace!("h3x publishing packet for {} to {}", name, self.base_url); + tracing::trace!( + name, + packet_len = packet.len(), + url = %self.base_url, + "h3x publishing packet" + ); let request = http::Request::post(uri) .body(Full::new(bytes::Bytes::copy_from_slice(packet))) .expect("h3 dns publish request must be valid"); From 01ea585351c6b9d1e40575f9f74c425ea3d352c7 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 20 May 2026 23:31:31 +0800 Subject: [PATCH 45/85] fix: bind mdns sockets to apple interfaces --- gmdns/src/protocol.rs | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/gmdns/src/protocol.rs b/gmdns/src/protocol.rs index b4e353d..252352c 100644 --- a/gmdns/src/protocol.rs +++ b/gmdns/src/protocol.rs @@ -1,6 +1,6 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - num::NonZero, + num::{NonZero, NonZeroU32}, pin::Pin, sync::{Arc, Weak}, task::{Context, Poll}, @@ -50,6 +50,22 @@ impl MdnsSocket { socket.bind(&bind.into())?; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] socket.bind_device(Some(device.as_bytes()))?; + #[cfg(any( + target_os = "ios", + target_os = "visionos", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + ))] + { + let ifindex = NonZeroU32::new(if_nametoindex(device)?).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "interface index must be non-zero", + ) + })?; + socket.bind_device_by_index_v4(Some(ifindex))?; + } // Always enable multicast loopback so that mDNS services on the // same host (but in different processes) can communicate. socket.set_multicast_loop_v4(true)?; @@ -75,9 +91,22 @@ impl MdnsSocket { // same host (but in different processes) can communicate. socket.set_multicast_loop_v6(true)?; // TODO: 外面传进来 - let ifindex = if_nametoindex(device)?; - socket.join_multicast_v6(&MULTICAST_ADDR_V6, ifindex)?; - socket.set_multicast_if_v6(ifindex)?; + let ifindex = NonZeroU32::new(if_nametoindex(device)?).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "interface index must be non-zero", + ) + })?; + #[cfg(any( + target_os = "ios", + target_os = "visionos", + target_os = "macos", + target_os = "tvos", + target_os = "watchos", + ))] + socket.bind_device_by_index_v6(Some(ifindex))?; + socket.join_multicast_v6(&MULTICAST_ADDR_V6, ifindex.get())?; + socket.set_multicast_if_v6(ifindex.get())?; socket } From 8a7188e0acf4dd2ade97248dc7a2539aec6901be Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 21 May 2026 12:53:58 +0800 Subject: [PATCH 46/85] fix(server): accept empty dns publish as clear --- ddns-server/src/policy.rs | 55 +++++++++-- ddns-server/src/publish.rs | 196 +++++++++++++++++++++++++++++++++++-- 2 files changed, 235 insertions(+), 16 deletions(-) diff --git a/ddns-server/src/policy.rs b/ddns-server/src/policy.rs index 64184d0..40b5d5d 100644 --- a/ddns-server/src/policy.rs +++ b/ddns-server/src/policy.rs @@ -55,6 +55,12 @@ impl DomainPolicies { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ValidatedDnsPacket { + Records { host: String }, + Empty, +} + // --------------------------------------------------------------------------- // Certificate helpers // --------------------------------------------------------------------------- @@ -100,7 +106,7 @@ pub fn validate_dns_packet( packet: &[u8], require_signature: bool, agent: &(impl RemoteAgent + ?Sized), -) -> Result { +) -> Result { let (remaining, dns_packet) = be_packet(packet).map_err(|e| AppError::InvalidDnsPacket { message: e.to_string(), })?; @@ -112,6 +118,11 @@ pub fn validate_dns_packet( require_signature, "validating dns packet" ); + let Some(first_answer) = dns_packet.answers.first() else { + debug!("dns packet has no answers"); + return Ok(ValidatedDnsPacket::Empty); + }; + if require_signature { let has_signature = dns_packet .answers @@ -140,10 +151,42 @@ pub fn validate_dns_packet( } } - let Some(first_answer) = dns_packet.answers.first() else { - debug!("dns packet has no answers"); - return Err(AppError::NoAnswersInPacket); - }; + Ok(ValidatedDnsPacket::Records { + host: first_answer.name().to_string(), + }) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; - Ok(first_answer.name().to_string()) + use ddns::MdnsPacket; + use dhttp_identity::identity::RemoteAgent; + use rustls::pki_types::CertificateDer; + + use super::*; + + #[derive(Debug)] + struct TestAgent; + + impl RemoteAgent for TestAgent { + fn name(&self) -> &str { + "agent.example" + } + + fn cert_chain(&self) -> &[CertificateDer<'static>] { + &[] + } + } + + #[test] + fn validate_dns_packet_accepts_empty_packet_as_clear_operation() { + let hosts: HashMap> = + HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); + let packet = MdnsPacket::answer(0, &hosts).to_bytes(); + + let validated = validate_dns_packet(&packet, true, &TestAgent).unwrap(); + + assert!(matches!(validated, ValidatedDnsPacket::Empty)); + } } diff --git a/ddns-server/src/publish.rs b/ddns-server/src/publish.rs index 4446ecd..4a1bb35 100644 --- a/ddns-server/src/publish.rs +++ b/ddns-server/src/publish.rs @@ -10,7 +10,7 @@ use tracing::{debug, info, warn}; use crate::{ error::{AppError, normalize_host, parse_query_params}, lookup::{Request, Response, body_response, write_error}, - policy::{DomainPolicy, client_allowed_host, validate_dns_packet}, + policy::{DomainPolicy, ValidatedDnsPacket, client_allowed_host, validate_dns_packet}, storage::{ AppState, Record, Storage, StoredRecord, cert_fingerprint, cert_fingerprint_hex, unix_now_secs, @@ -108,7 +108,7 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { require_signature = require_sig, "validating publish packet" ); - let packet_name = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) { + let packet = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) { Ok(n) => n, Err(e) => { debug!(host = %host, error = %e, "publish packet rejected"); @@ -116,16 +116,21 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { } }; - let packet_host = match normalize_host(&packet_name) { - Ok(h) => h, - Err(e) => return write_error(e), - }; + match packet { + ValidatedDnsPacket::Records { host: packet_name } => { + let packet_host = match normalize_host(&packet_name) { + Ok(h) => h, + Err(e) => return write_error(e), + }; - if packet_host != host { - return write_error(AppError::HostMismatch); - } + if packet_host != host { + return write_error(AppError::HostMismatch); + } - publish_record(&state, &host, &body, agent.as_ref()).await + publish_record(&state, &host, &body, agent.as_ref()).await + } + ValidatedDnsPacket::Empty => clear_record(&state, &host, agent.as_ref()).await, + } } fn request_connection(request: &Request) -> Option>> { @@ -249,3 +254,174 @@ pub async fn publish_record( info!(host = %host, ttl = state.ttl_secs, bytes = body.len(), fp = %fp_hex, "publish.ok"); body_response(http::StatusCode::OK, bytes::Bytes::from_static(b"OK")) } + +pub async fn clear_record( + state: &AppState, + host: &str, + agent: &(impl RemoteAgent + ?Sized), +) -> Response { + let cert_bytes = agent + .cert_chain() + .first() + .map(|c| c.as_ref().to_vec()) + .unwrap_or_default(); + + let fp = cert_fingerprint(&cert_bytes); + let fp_hex = cert_fingerprint_hex(&cert_bytes); + + match &state.storage { + Storage::Redis(pool) => { + let mut conn = match pool.get().await { + Ok(c) => c, + Err(e) => { + return write_error(AppError::Redis { + message: e.to_string(), + }); + } + }; + + let fp_key = format!("{host}:fp:{fp_hex}"); + let set_key = format!("{host}:multi"); + + let old_member: Option> = conn.get(&fp_key).await.unwrap_or(None); + if let Some(old) = old_member { + let _: () = conn.zrem(&set_key, &old).await.unwrap_or(()); + } + if let Err(e) = conn.del::<_, ()>(&fp_key).await { + return write_error(AppError::Redis { + message: e.to_string(), + }); + } + } + Storage::Memory(mem) => { + let remove_host = if let Some(mut host_map) = mem.records.get_mut(host) { + host_map.remove(&fp); + host_map.is_empty() + } else { + false + }; + if remove_host { + mem.records.remove(host); + } + } + } + + info!(host = %host, fp = %fp_hex, "publish.clear"); + body_response(http::StatusCode::OK, bytes::Bytes::from_static(b"OK")) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddrV4}, + sync::Arc, + }; + + use ddns::{MdnsPacket, parser::record::endpoint::EndpointAddr}; + use dhttp_identity::identity::RemoteAgent; + use rustls::pki_types::CertificateDer; + + use super::*; + use crate::{ + lookup::{LookupResult, perform_lookup}, + policy::DomainPolicies, + storage::{MemoryStorage, SeedRecords}, + }; + + #[derive(Debug)] + struct TestAgent { + name: &'static str, + certs: Vec>, + } + + impl TestAgent { + fn new(name: &'static str, cert_bytes: Vec) -> Self { + Self { + name, + certs: vec![CertificateDer::from(cert_bytes)], + } + } + } + + impl RemoteAgent for TestAgent { + fn name(&self) -> &str { + self.name + } + + fn cert_chain(&self) -> &[CertificateDer<'static>] { + &self.certs + } + } + + fn memory_state() -> AppState { + AppState { + storage: Storage::Memory(MemoryStorage::new()), + require_signature: true, + ttl_secs: 30, + policies: Arc::new(DomainPolicies::default()), + seed_records: SeedRecords::default(), + } + } + + fn packet_for(host: &str, last_octet: u8) -> bytes::Bytes { + let endpoint = EndpointAddr::direct_v4(SocketAddrV4::new( + Ipv4Addr::new(203, 0, 113, last_octet), + 4433, + )); + let mut hosts = HashMap::new(); + hosts.insert(host.to_owned(), vec![endpoint]); + bytes::Bytes::from(MdnsPacket::answer(0, &hosts).to_bytes()) + } + + #[tokio::test] + async fn clear_record_removes_only_current_certificate_fingerprint() { + let state = memory_state(); + let host = "reimu.pilot.genmeta.net"; + let agent_a = TestAgent::new("agent-a", vec![1]); + let agent_b = TestAgent::new("agent-b", vec![2]); + let packet_a = packet_for(host, 1); + let packet_b = packet_for(host, 2); + + assert_eq!( + publish_record(&state, host, &packet_a, &agent_a) + .await + .status(), + http::StatusCode::OK + ); + assert_eq!( + publish_record(&state, host, &packet_b, &agent_b) + .await + .status(), + http::StatusCode::OK + ); + + assert_eq!( + clear_record(&state, host, &agent_a).await.status(), + http::StatusCode::OK + ); + + let LookupResult::Multi(response) = perform_lookup(&state, host, None).await.unwrap() + else { + panic!("agent b record should remain"); + }; + assert_eq!(response.records.len(), 1); + assert_eq!(response.records[0].cert, agent_b.certs[0].as_ref()); + } + + #[tokio::test] + async fn clear_record_is_idempotent_for_missing_fingerprint() { + let state = memory_state(); + let host = "reimu.pilot.genmeta.net"; + let agent = TestAgent::new("agent", vec![1]); + + assert_eq!( + clear_record(&state, host, &agent).await.status(), + http::StatusCode::OK + ); + assert!(matches!( + perform_lookup(&state, host, None).await.unwrap(), + LookupResult::NotFound + )); + } +} From eadd8514110b3cfc4b9c8910fc1b5a272aad40a3 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 21 May 2026 13:07:06 +0800 Subject: [PATCH 47/85] fix(publisher): replace stale publish attempts on changes --- ddns/src/publisher.rs | 212 ++++++++++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 79 deletions(-) diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index 3a2f4e4..e295668 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -1,8 +1,10 @@ use std::{ any::{Any, TypeId}, collections::{HashMap, HashSet}, + future::Future, io, net::SocketAddr, + pin::Pin, sync::Arc, time::Duration, }; @@ -31,7 +33,9 @@ pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// longer exist. Timing out the attempt keeps consecutive publishes /// independent: the next interval observes the current bindings again. pub const DEFAULT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); -const PUBLISH_CHANGE_DEBOUNCE: Duration = Duration::from_millis(500); +const PUBLISH_CHANGE_DEBOUNCE: Duration = Duration::from_millis(50); + +type PublishLoopFuture<'a> = Pin + Send + 'a>>; #[derive(Debug, Snafu)] #[snafu(module(create_publisher_error))] @@ -149,16 +153,53 @@ impl Publisher { pub async fn run(&self) -> ! { let mut locations = self.network.locations().subscribe(); - let _ = self.publish_attempt().await; - let _ = self.settle_publish_events(&mut locations).await; + let interval = tokio::time::sleep(self.interval); + tokio::pin!(interval); + // Keep at most one publish attempt in flight. A timer tick or + // publishable location change drops the current future and starts a new + // debounced attempt so a stale H3 publish cannot block publication from + // the latest bindings. + let mut current_publish = self.new_publish_loop_future(); loop { - self.wait_next_publish_trigger(&mut locations).await; - let _ = self.publish_attempt().await; - let _ = self.settle_publish_events(&mut locations).await; + tokio::select! { + _ = &mut current_publish => { + current_publish = Self::pending_publish_loop_future(); + } + _ = &mut interval => { + interval.as_mut().reset(tokio::time::Instant::now() + self.interval); + self.clear_publish_state(); + current_publish = self.new_publish_loop_future(); + } + event = locations.recv() => { + let Some((bind_uri, event)) = event else { + continue; + }; + if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { + continue; + } + if !Self::location_event_requires_publish(&event) { + continue; + } + + self.clear_publish_state(); + current_publish = self.new_publish_loop_future(); + } + } } } + fn new_publish_loop_future(&self) -> PublishLoopFuture<'_> { + Box::pin(async move { + tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; + let _ = self.publish_attempt().await; + }) + } + + fn pending_publish_loop_future<'a>() -> PublishLoopFuture<'a> { + Box::pin(std::future::pending()) + } + async fn publish_attempt(&self) -> bool { tracing::trace!( timeout_ms = self.publish_timeout.as_millis(), @@ -189,70 +230,6 @@ impl Publisher { } } - async fn wait_next_publish_trigger( - &self, - locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) { - let interval = tokio::time::sleep(self.interval); - tokio::pin!(interval); - - loop { - tokio::select! { - _ = &mut interval => return, - event = locations.recv() => { - let Some((bind_uri, event)) = event else { - interval.await; - return; - }; - if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { - continue; - } - if !Self::location_event_requires_publish(&event) { - continue; - } - - // A local-address change invalidates cached H3 DNS - // connections even if no request has failed yet. Clear - // resolver-owned connection state before publishing from - // the new binding set. - self.clear_publish_state(); - tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; - self.drain_location_events(locations); - return; - } - } - } - } - - fn drain_location_events( - &self, - locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) -> bool { - let mut requires_publish = false; - while let Ok((bind_uri, event)) = locations.try_recv() { - if !self - .bind_patterns - .iter() - .any(|pattern| pattern.matches(&bind_uri)) - { - continue; - } - if Self::location_event_requires_publish(&event) { - self.clear_publish_state(); - requires_publish = true; - } - } - requires_publish - } - - async fn settle_publish_events( - &self, - locations: &mut h3x::dquic::qinterface::component::location::Observer, - ) -> bool { - tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE).await; - self.drain_location_events(locations) - } - fn location_event_requires_publish(event: &AddressEvent) -> bool { match event { AddressEvent::Upsert(data) => { @@ -709,7 +686,7 @@ mod tests { #[cfg(feature = "http-resolver")] #[tokio::test] - async fn run_treats_location_publish_attempts_as_independent() { + async fn run_restarts_when_publish_attempt_observes_location_change() { async fn wait_for_count(count: &AtomicUsize, target: usize) { loop { if count.load(Ordering::SeqCst) >= target { @@ -783,24 +760,20 @@ mod tests { .await .expect("publishable location changes should trigger the next independent publish"); - let third_publish = tokio::time::timeout( + tokio::time::timeout( PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(500), wait_for_count(&publish_count, 3), ) - .await; + .await + .expect("publishable location events should replace the current publish attempt"); publisher.abort(); server.abort(); - - assert!( - third_publish.is_err(), - "location events generated by a publish attempt must not trigger an immediate retry" - ); } #[cfg(feature = "http-resolver")] #[tokio::test] - async fn run_drains_events_generated_during_publish_attempt() { + async fn run_ignores_transient_location_failures_generated_during_publish_attempt() { async fn wait_for_count(count: &AtomicUsize, target: usize) { loop { if count.load(Ordering::SeqCst) >= target { @@ -964,4 +937,85 @@ mod tests { "timed out location-triggered publish must not be retried before the next interval" ); } + + #[cfg(feature = "http-resolver")] + #[tokio::test] + async fn run_replaces_in_flight_publish_on_publishable_location_change() { + async fn wait_for_count(count: &AtomicUsize, target: usize) { + loop { + if count.load(Ordering::SeqCst) >= target { + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + } + + let network = h3x::dquic::Network::builder().build(); + let bind_uri: h3x::dquic::net::BindUri = + "inet://127.0.0.1:0".parse().expect("valid bind uri"); + let publish_count = Arc::new(AtomicUsize::new(0)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test http server"); + let port = listener.local_addr().expect("local addr").port(); + let server_count = publish_count.clone(); + let server = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + let current = server_count.fetch_add(1, Ordering::SeqCst) + 1; + tokio::spawn(async move { + let mut buf = [0_u8; 1024]; + let _ = stream.read(&mut buf).await; + if current == 1 { + std::future::pending::<()>().await; + } + let _ = stream + .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + .await; + }); + } + }); + + let resolver = Arc::new( + crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + .expect("valid http resolver"), + ); + let mut publisher = Publisher::new( + Arc::new(TestAgent), + network.clone(), + resolver, + Arc::new(vec![ + "inet://127.0.0.1:0".parse().expect("valid bind pattern"), + ]), + ) + .with_publish_timeout(Duration::from_secs(30)); + publisher.interval = Duration::from_secs(60); + + let publisher = tokio::spawn(async move { + publisher.run().await; + }); + + tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 1)) + .await + .expect("initial publish should start"); + + network.locations().upsert( + bind_uri, + Arc::new(Ok::( + "127.0.0.1:10000".parse().expect("valid socket addr"), + )), + ); + + tokio::time::timeout( + PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(800), + wait_for_count(&publish_count, 2), + ) + .await + .expect("publishable location change should replace the in-flight publish"); + + publisher.abort(); + server.abort(); + } } From 51ceaa0b914455ddee3c6b0f2ccd8d9ac7d81861 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 21 May 2026 18:06:30 +0800 Subject: [PATCH 48/85] fix(resolver): report h3 dns source --- ddns/src/resolvers/h3.rs | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/ddns/src/resolvers/h3.rs b/ddns/src/resolvers/h3.rs index 728e4f3..41b6443 100644 --- a/ddns/src/resolvers/h3.rs +++ b/ddns/src/resolvers/h3.rs @@ -42,11 +42,7 @@ impl fmt::Debug for H3Resolver { impl fmt::Display for H3Resolver { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "H3 DNS Resolver({})", - self.base_url.host_str().unwrap_or("") - ) + write!(f, "H3 DNS Resolver({})", self.base_url) } } @@ -287,8 +283,8 @@ where pub async fn lookup(&self, name: &str) -> Result> { use ddns_core::parser::record; - let server = Arc::from(self.base_url.host_str().unwrap_or("")); - let source = Source::Http { server }; + let server = Arc::from(self.base_url.origin().ascii_serialization()); + let source = Source::H3 { server }; let Some(domain) = super::resolvable_name(name) else { return Err(Error::NoRecordFound); @@ -425,4 +421,33 @@ mod tests { "h3 lookup must return before common 15s command timeouts so callers can retry" ); } + + #[tokio::test] + async fn cached_lookup_reports_h3_dns_source() { + let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( + h3x::dquic::QuicEndpoint::builder().build().await, + )); + let resolver = H3Resolver::from_endpoint("https://dns.genmeta.net:4433", endpoint).unwrap(); + resolver.cached_records.insert( + "car.lab.genmeta.net".to_owned(), + Record { + addrs: vec![EndpointAddr::direct("192.168.5.78:41748".parse().unwrap())], + expire: Instant::now() + Duration::from_secs(60), + }, + ); + + let mut records = resolver.lookup("car.lab.genmeta.net").await.unwrap(); + let (source, endpoint) = records.next().await.unwrap(); + + assert_eq!( + source, + Source::H3 { + server: Arc::from("https://dns.genmeta.net:4433") + } + ); + assert_eq!( + endpoint, + EndpointAddr::direct("192.168.5.78:41748".parse().unwrap()) + ); + } } From 4058ed9ea57086dc970f8dbf3227c00d1a7ddc42 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 22 May 2026 16:26:48 +0800 Subject: [PATCH 49/85] fix ddns tests and suppress proc-macro future-incompat warnings --- Cargo.toml | 3 + ddns/src/publisher.rs | 35 +- patches/proc-macro-error2/.gitignore | 4 + patches/proc-macro-error2/CHANGELOG.md | 180 ++++++ patches/proc-macro-error2/Cargo.toml | 41 ++ patches/proc-macro-error2/LICENSE-APACHE | 201 +++++++ patches/proc-macro-error2/LICENSE-MIT | 21 + patches/proc-macro-error2/README.md | 250 ++++++++ .../proc-macro-error-attr/.gitignore | 4 + .../proc-macro-error-attr/Cargo.toml | 22 + .../proc-macro-error-attr/LICENSE-APACHE | 201 +++++++ .../proc-macro-error-attr/LICENSE-MIT | 21 + .../proc-macro-error-attr/src/lib.rs | 111 ++++ .../proc-macro-error-attr/src/parse.rs | 89 +++ .../proc-macro-error-attr/src/settings.rs | 72 +++ patches/proc-macro-error2/src/diagnostic.rs | 360 +++++++++++ patches/proc-macro-error2/src/dummy.rs | 151 +++++ patches/proc-macro-error2/src/imp/delegate.rs | 68 +++ patches/proc-macro-error2/src/imp/fallback.rs | 30 + patches/proc-macro-error2/src/lib.rs | 565 ++++++++++++++++++ patches/proc-macro-error2/src/macros.rs | 288 +++++++++ patches/proc-macro-error2/src/sealed.rs | 3 + .../proc-macro-error2/test-crate/.gitignore | 4 + .../proc-macro-error2/test-crate/Cargo.toml | 26 + patches/proc-macro-error2/test-crate/lib.rs | 272 +++++++++ .../proc-macro-error2/tests/macro-errors.rs | 6 + patches/proc-macro-error2/tests/ok.rs | 8 + .../proc-macro-error2/tests/runtime-errors.rs | 13 + patches/proc-macro-error2/tests/ui/abort.rs | 10 + .../proc-macro-error2/tests/ui/abort.stderr | 48 ++ .../tests/ui/append_dummy.rs | 12 + .../tests/ui/append_dummy.stderr | 5 + .../tests/ui/children_messages.rs | 5 + .../tests/ui/children_messages.stderr | 23 + patches/proc-macro-error2/tests/ui/dummy.rs | 12 + .../proc-macro-error2/tests/ui/dummy.stderr | 5 + patches/proc-macro-error2/tests/ui/emit.rs | 6 + .../proc-macro-error2/tests/ui/emit.stderr | 48 ++ .../tests/ui/explicit_span_range.rs | 5 + .../tests/ui/explicit_span_range.stderr | 5 + patches/proc-macro-error2/tests/ui/misuse.rs | 10 + .../proc-macro-error2/tests/ui/misuse.stderr | 24 + .../tests/ui/multiple_tokens.rs | 4 + .../tests/ui/multiple_tokens.stderr | 5 + .../tests/ui/not_proc_macro.rs | 4 + .../tests/ui/not_proc_macro.stderr | 9 + .../proc-macro-error2/tests/ui/option_ext.rs | 5 + .../tests/ui/option_ext.stderr | 7 + .../proc-macro-error2/tests/ui/result_ext.rs | 6 + .../tests/ui/result_ext.stderr | 11 + .../tests/ui/to_tokens_span.rs | 5 + .../tests/ui/to_tokens_span.stderr | 11 + .../tests/ui/unknown_setting.rs | 4 + .../tests/ui/unknown_setting.stderr | 5 + .../tests/ui/unrelated_panic.rs | 5 + .../tests/ui/unrelated_panic.stderr | 7 + 56 files changed, 3340 insertions(+), 15 deletions(-) create mode 100644 patches/proc-macro-error2/.gitignore create mode 100644 patches/proc-macro-error2/CHANGELOG.md create mode 100644 patches/proc-macro-error2/Cargo.toml create mode 100644 patches/proc-macro-error2/LICENSE-APACHE create mode 100644 patches/proc-macro-error2/LICENSE-MIT create mode 100644 patches/proc-macro-error2/README.md create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/.gitignore create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs create mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs create mode 100644 patches/proc-macro-error2/src/diagnostic.rs create mode 100644 patches/proc-macro-error2/src/dummy.rs create mode 100644 patches/proc-macro-error2/src/imp/delegate.rs create mode 100644 patches/proc-macro-error2/src/imp/fallback.rs create mode 100644 patches/proc-macro-error2/src/lib.rs create mode 100644 patches/proc-macro-error2/src/macros.rs create mode 100644 patches/proc-macro-error2/src/sealed.rs create mode 100644 patches/proc-macro-error2/test-crate/.gitignore create mode 100644 patches/proc-macro-error2/test-crate/Cargo.toml create mode 100644 patches/proc-macro-error2/test-crate/lib.rs create mode 100644 patches/proc-macro-error2/tests/macro-errors.rs create mode 100644 patches/proc-macro-error2/tests/ok.rs create mode 100644 patches/proc-macro-error2/tests/runtime-errors.rs create mode 100644 patches/proc-macro-error2/tests/ui/abort.rs create mode 100644 patches/proc-macro-error2/tests/ui/abort.stderr create mode 100644 patches/proc-macro-error2/tests/ui/append_dummy.rs create mode 100644 patches/proc-macro-error2/tests/ui/append_dummy.stderr create mode 100644 patches/proc-macro-error2/tests/ui/children_messages.rs create mode 100644 patches/proc-macro-error2/tests/ui/children_messages.stderr create mode 100644 patches/proc-macro-error2/tests/ui/dummy.rs create mode 100644 patches/proc-macro-error2/tests/ui/dummy.stderr create mode 100644 patches/proc-macro-error2/tests/ui/emit.rs create mode 100644 patches/proc-macro-error2/tests/ui/emit.stderr create mode 100644 patches/proc-macro-error2/tests/ui/explicit_span_range.rs create mode 100644 patches/proc-macro-error2/tests/ui/explicit_span_range.stderr create mode 100644 patches/proc-macro-error2/tests/ui/misuse.rs create mode 100644 patches/proc-macro-error2/tests/ui/misuse.stderr create mode 100644 patches/proc-macro-error2/tests/ui/multiple_tokens.rs create mode 100644 patches/proc-macro-error2/tests/ui/multiple_tokens.stderr create mode 100644 patches/proc-macro-error2/tests/ui/not_proc_macro.rs create mode 100644 patches/proc-macro-error2/tests/ui/not_proc_macro.stderr create mode 100644 patches/proc-macro-error2/tests/ui/option_ext.rs create mode 100644 patches/proc-macro-error2/tests/ui/option_ext.stderr create mode 100644 patches/proc-macro-error2/tests/ui/result_ext.rs create mode 100644 patches/proc-macro-error2/tests/ui/result_ext.stderr create mode 100644 patches/proc-macro-error2/tests/ui/to_tokens_span.rs create mode 100644 patches/proc-macro-error2/tests/ui/to_tokens_span.stderr create mode 100644 patches/proc-macro-error2/tests/ui/unknown_setting.rs create mode 100644 patches/proc-macro-error2/tests/ui/unknown_setting.stderr create mode 100644 patches/proc-macro-error2/tests/ui/unrelated_panic.rs create mode 100644 patches/proc-macro-error2/tests/ui/unrelated_panic.stderr diff --git a/Cargo.toml b/Cargo.toml index 5b312b5..48645b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,6 @@ resolver = "2" [workspace.package] version = "0.2.0" edition = "2024" + +[patch.crates-io] +proc-macro-error2 = { path = "patches/proc-macro-error2" } diff --git a/ddns/src/publisher.rs b/ddns/src/publisher.rs index e295668..b5faa0d 100644 --- a/ddns/src/publisher.rs +++ b/ddns/src/publisher.rs @@ -152,7 +152,7 @@ impl Publisher { } pub async fn run(&self) -> ! { - let mut locations = self.network.locations().subscribe(); + let mut locations = self.network.quic().locations().subscribe(); let interval = tokio::time::sleep(self.interval); tokio::pin!(interval); // Keep at most one publish attempt in flight. A timer tick or @@ -376,7 +376,7 @@ impl Publisher { let mut endpoints = Vec::new(); let mut seen = HashSet::new(); for pattern in self.bind_patterns.iter() { - let Some(ifaces) = self.network.get_interfaces(pattern) else { + let Some(ifaces) = self.network.quic().get_interfaces(pattern) else { tracing::trace!(?pattern, "no interfaces for bind pattern"); continue; }; @@ -393,7 +393,7 @@ impl Publisher { fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { let mut endpoints = HashSet::new(); for pattern in self.bind_patterns.iter() { - let Some(ifaces) = self.network.get_interfaces(pattern) else { + let Some(ifaces) = self.network.quic().get_interfaces(pattern) else { continue; }; for iface in ifaces { @@ -632,7 +632,7 @@ mod tests { let network = h3x::dquic::Network::builder().build(); let bind_pattern: h3x::dquic::binds::BindPattern = "inet://127.0.0.1:0".parse().expect("valid bind pattern"); - let _bind = network.bind(bind_pattern.clone()).await; + let _bind = network.quic().bind(bind_pattern.clone()).await; let publisher = Publisher::new( Arc::new(TestAgent), network, @@ -716,7 +716,7 @@ mod tests { let mut buf = [0_u8; 1024]; let _ = stream.read(&mut buf).await; if current == 2 { - server_network.locations().upsert( + server_network.quic().locations().upsert( server_bind_uri.clone(), Arc::new(Ok::( "127.0.0.1:10001".parse().expect("valid socket addr"), @@ -749,7 +749,7 @@ mod tests { wait_for_count(&publish_count, 1).await; tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; - network.locations().upsert( + network.quic().locations().upsert( bind_uri, Arc::new(Ok::( "127.0.0.1:10000".parse().expect("valid socket addr"), @@ -802,12 +802,17 @@ mod tests { let mut buf = [0_u8; 1024]; let _ = stream.read(&mut buf).await; server_count.fetch_add(1, Ordering::SeqCst); - server_network.locations().upsert::( - server_bind_uri.clone(), - Arc::new(Err(dquic::qtraversal::nat::client::ArcIoError::from( - io::Error::from(io::ErrorKind::NetworkUnreachable), - ))), - ); + server_network + .quic() + .locations() + .upsert::( + server_bind_uri.clone(), + Arc::new(Err( + dquic::qtraversal::nat::client::DetectOuterAddrError::Rebinded { + bind_uri: server_bind_uri.clone(), + }, + )), + ); let _ = stream .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") .await; @@ -833,7 +838,7 @@ mod tests { wait_for_count(&publish_count, 1).await; tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; - network.locations().upsert( + network.quic().locations().upsert( bind_uri, Arc::new(Ok::( "127.0.0.1:0".parse().expect("valid socket addr"), @@ -915,7 +920,7 @@ mod tests { wait_for_count(&publish_count, 1).await; tokio::time::sleep(PUBLISH_CHANGE_DEBOUNCE + Duration::from_millis(100)).await; - network.locations().upsert( + network.quic().locations().upsert( bind_uri, Arc::new(Ok::( "127.0.0.1:0".parse().expect("valid socket addr"), @@ -1001,7 +1006,7 @@ mod tests { .await .expect("initial publish should start"); - network.locations().upsert( + network.quic().locations().upsert( bind_uri, Arc::new(Ok::( "127.0.0.1:10000".parse().expect("valid socket addr"), diff --git a/patches/proc-macro-error2/.gitignore b/patches/proc-macro-error2/.gitignore new file mode 100644 index 0000000..5e81b66 --- /dev/null +++ b/patches/proc-macro-error2/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.fuse_hidden* diff --git a/patches/proc-macro-error2/CHANGELOG.md b/patches/proc-macro-error2/CHANGELOG.md new file mode 100644 index 0000000..cad92f6 --- /dev/null +++ b/patches/proc-macro-error2/CHANGELOG.md @@ -0,0 +1,180 @@ +# v2.0.1 (2024-09-06) + +* Fixed a span location issue due to mistake in refactoring (#2) + +# v2.0.0 (2024-09-05) + +No changes, simply releasing pre-release as full release. + +# v2.0.0-pre.1 (2024-09-01) + +* __Crate has been renamed to `proc-macro-error2`, due to the old maintainer's inactivity.__ + +* `syn` has been upgraded to `2` +* MSRV has been bumped to `1.61` +* Warnings have been fixed, including `clippy::pedantic` lints +* CI has been converted to GitHub actions, and testing infrastructure significantly simplified. +* Automatic nightly detection has been removed, use the `nightly` feature for improved diagnostics at the cost of stability. + +# v1.0.4 (2020-7-31) + +* `SpanRange` facility is now public. +* Docs have been improved. +* Introduced the `syn-error` feature so you can opt-out from the `syn` dependency. + +# v1.0.3 (2020-6-26) + +* Corrected a few typos. +* Fixed the `emit_call_site_warning` macro. + +# v1.0.2 (2020-4-9) + +* An obsolete note was removed from documentation. + +# v1.0.1 (2020-4-9) + +* `proc-macro-hack` is now well tested and supported. Not sure about `proc-macro-nested`, + please fill a request if you need it. +* Fixed `emit_call_site_error`. +* Documentation improvements. + +# v1.0.0 (2020-3-25) + +I believe the API can be considered stable because it's been a few months without +breaking changes, and I also don't think this crate will receive much further evolution. +It's perfect, admit it. + +Hence, meet the new, stable release! + +### Improvements + +* Supported nested `#[proc_macro_error]` attributes. Well, you aren't supposed to do that, + but I caught myself doing it by accident on one occasion and the behavior was... surprising. + Better to handle this smooth. + +# v0.4.12 (2020-3-23) + +* Error message on macros' misuse is now a bit more understandable. + +# v0.4.11 (2020-3-02) + +* `build.rs` no longer fails when `rustc` date could not be determined, + (thanks to [`Fabian Möller`](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/8) + for noticing and to [`Igor Gnatenko`](https://gitlab.com/CreepySkeleton/proc-macro-error/-/merge_requests/25) + for fixing). + +# v0.4.10 (2020-2-29) + +* `proc-macro-error` doesn't depend on syn\[full\] anymore, the compilation + is \~30secs faster. + +# v0.4.9 (2020-2-13) + +* New function: `append_dummy`. + +# v0.4.8 (2020-2-01) + +* Support for children messages + +# v0.4.7 (2020-1-31) + +* Now any type that implements `quote::ToTokens` can be used instead of spans. + This allows for high quality error messages. + +# v0.4.6 (2020-1-31) + +* `From` implementation doesn't lose span info anymore, see + [#6](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/6). + +# v0.4.5 (2020-1-20) +Just a small intermediate release. + +* Fix some bugs. +* Populate license files into subfolders. + +# v0.4.4 (2019-11-13) +* Fix `abort_if_dirty` + warnings bug +* Allow trailing commas in macros + +# v0.4.2 (2019-11-7) +* FINALLY fixed `__pme__suggestions not found` bug + +# v0.4.1 (2019-11-7) YANKED +* Fixed `__pme__suggestions not found` bug +* Documentation improvements, links checked + +# v0.4.0 (2019-11-6) YANKED + +## New features +* "help" messages that can have their own span on nightly, they + inherit parent span on stable. + ```rust + let cond_help = if condition { Some("some help message") else { None } }; + abort!( + span, // parent span + "something's wrong, {} wrongs in total", 10; // main message + help = "here's a help for you, {}", "take it"; // unconditional help message + help =? cond_help; // conditional help message, must be Option + note = note_span => "don't forget the note, {}", "would you?" // notes can have their own span but it's effective only on nightly + ) + ``` +* Warnings via `emit_warning` and `emit_warning_call_site`. Nightly only, they're ignored on stable. +* Now `proc-macro-error` delegates to `proc_macro::Diagnostic` on nightly. + +## Breaking changes +* `MacroError` is now replaced by `Diagnostic`. Its API resembles `proc_macro::Diagnostic`. +* `Diagnostic` does not implement `From<&str/String>` so `Result::abort_or_exit()` + won't work anymore (nobody used it anyway). +* `macro_error!` macro is replaced with `diagnostic!`. + +## Improvements +* Now `proc-macro-error` renders notes exactly just like rustc does. +* We don't parse a body of a function annotated with `#[proc_macro_error]` anymore, + only looking at the signature. This should somewhat decrease expansion time for large functions. + +# v0.3.3 (2019-10-16) +* Now you can use any word instead of "help", undocumented. + +# v0.3.2 (2019-10-16) +* Introduced support for "help" messages, undocumented. + +# v0.3.0 (2019-10-8) + +## The crate has been completely rewritten from scratch! + +## Changes (most are breaking): +* Renamed macros: + * `span_error` => `abort` + * `call_site_error` => `abort_call_site` +* `filter_macro_errors` was replaced by `#[proc_macro_error]` attribute. +* `set_dummy` now takes `TokenStream` instead of `Option` +* Support for multiple errors via `emit_error` and `emit_call_site_error` +* New `macro_error` macro for building errors in format=like style. +* `MacroError` API had been reconsidered. It also now implements `quote::ToTokens`. + +# v0.2.6 (2019-09-02) +* Introduce support for dummy implementations via `dummy::set_dummy` +* `multi::*` is now deprecated, will be completely rewritten in v0.3 + +# v0.2.0 (2019-08-15) + +## Breaking changes +* `trigger_error` replaced with `MacroError::trigger` and `filter_macro_error_panics` + is hidden from docs. + This is not quite a breaking change since users weren't supposed to use these functions directly anyway. +* All dependencies are updated to `v1.*`. + +## New features +* Ability to stack multiple errors via `multi::MultiMacroErrors` and emit them at once. + +## Improvements +* Now `MacroError` implements `std::fmt::Display` instead of `std::string::ToString`. +* `MacroError::span` inherent method. +* `From for proc_macro/proc_macro2::TokenStream` implementations. +* `AsRef/AsMut for MacroError` implementations. + +# v0.1.x (2019-07-XX) + +## New features +* An easy way to report errors inside within a proc-macro via `span_error`, + `call_site_error` and `filter_macro_errors`. diff --git a/patches/proc-macro-error2/Cargo.toml b/patches/proc-macro-error2/Cargo.toml new file mode 100644 index 0000000..fcc6161 --- /dev/null +++ b/patches/proc-macro-error2/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "proc-macro-error2" +authors = [ + "CreepySkeleton ", + "GnomedDev ", +] +version = "2.0.1" +description = "Almost drop-in replacement to panics in proc-macros" +repository = "https://github.com/GnomedDev/proc-macro-error-2" +rust-version = "1.61" +keywords = ["proc-macro", "error", "errors"] +categories = ["development-tools::procedural-macro-helpers"] +license = "MIT OR Apache-2.0" +edition = "2021" + +[dependencies] +quote = "1" +proc-macro2 = "1" +proc-macro-error-attr2 = { path = "./proc-macro-error-attr", version = "=2.0.0" } + +[dependencies.syn] +version = "2" +optional = true +default-features = false + +[dev-dependencies] +test-crate = { path = "./test-crate" } +syn = { version = "2", features = ["full"] } +trybuild = { version = "1.0.99", features = ["diff"] } + +[features] +default = ["syn-error"] +syn-error = ["dep:syn"] +nightly = [] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(run_ui_tests)'] } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = { level = "allow" } diff --git a/patches/proc-macro-error2/LICENSE-APACHE b/patches/proc-macro-error2/LICENSE-APACHE new file mode 100644 index 0000000..cc17374 --- /dev/null +++ b/patches/proc-macro-error2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019-2020 CreepySkeleton + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/patches/proc-macro-error2/LICENSE-MIT b/patches/proc-macro-error2/LICENSE-MIT new file mode 100644 index 0000000..fc73e59 --- /dev/null +++ b/patches/proc-macro-error2/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 CreepySkeleton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/patches/proc-macro-error2/README.md b/patches/proc-macro-error2/README.md new file mode 100644 index 0000000..0910eac --- /dev/null +++ b/patches/proc-macro-error2/README.md @@ -0,0 +1,250 @@ +# Makes error reporting in procedural macros nice and easy + +[![docs.rs](https://docs.rs/proc-macro-error2/badge.svg)](https://docs.rs/proc-macro-error2) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) + +This crate aims to make error reporting in proc-macros simple and easy to use. +Migrate from `panic!`-based errors for as little effort as possible! + +Also, you can explicitly [append a dummy token stream][crate::dummy] to your errors. + +To achieve this, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +`compile_error!`. It detects the most preferable way to emit errors based on compiler's version. +When the underlying diagnostic type is finally stabilized, this crate will be simply +delegating to it, requiring no changes in your code! + +So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +available on stable ahead of time and your error-reporting code future-proof. + +```toml +[dependencies] +proc-macro-error2 = "2.0" +``` + +*Supports rustc 1.61 and up* + +[Documentation and guide][guide] + +## Quick example + +Code: + +```rust +#[proc_macro] +#[proc_macro_error] +pub fn make_fn(input: TokenStream) -> TokenStream { + let mut input = TokenStream2::from(input).into_iter(); + let name = input.next().unwrap(); + if let Some(second) = input.next() { + abort! { second, + "I don't like this part!"; + note = "I see what you did there..."; + help = "I need only one part, you know?"; + } + } + + quote!( fn #name() {} ).into() +} +``` + +This is how the error is rendered in a terminal: + +

+ +

+ +And this is what your users will see in their IDE: + +

+ +

+ +## Examples + +### Panic-like usage + +```rust +use proc_macro_error2::{ + proc_macro_error, + abort, + abort_call_site, + ResultExt, + OptionExt, +}; +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; +use quote::quote; + +// This is your main entry point +#[proc_macro] +// This attribute *MUST* be placed on top of the #[proc_macro] function +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + if let Err(err) = some_logic(&input) { + // we've got a span to blame, let's use it + // This immediately aborts the proc-macro and shows the error + // + // You can use `proc_macro::Span`, `proc_macro2::Span`, and + // anything that implements `quote::ToTokens` (almost every type from + // `syn` and `proc_macro2`) + abort!(err, "You made an error, go fix it: {}", err.msg); + } + + // `Result` has some handy shortcuts if your error type implements + // `Into`. `Option` has one unconditionally. + more_logic(&input).expect_or_abort("What a careless user, behave!"); + + if !more_logic_for_logic_god(&input) { + // We don't have an exact location this time, + // so just highlight the proc-macro invocation itself + abort_call_site!( + "Bad, bad user! Now go stand in the corner and think about what you did!"); + } + + // Now all the processing is done, return `proc_macro::TokenStream` + quote!(/* stuff */).into() +} +``` + +### `proc_macro::Diagnostic`-like usage + +```rust +use proc_macro_error2::*; +use proc_macro::TokenStream; +use syn::{spanned::Spanned, DeriveInput, ItemStruct, Fields, Attribute , parse_macro_input}; +use quote::quote; + +fn process_attrs(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| match process_attr(attr) { + Ok(res) => Some(res), + Err(msg) => { + emit_error!(attr, "Invalid attribute: {}", msg); + None + } + }) + .collect() +} + +fn process_fields(_attrs: &Fields) -> Vec { + // processing fields in pretty much the same way as attributes + unimplemented!() +} + +#[proc_macro] +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + let attrs = process_attrs(&input.attrs); + + // abort right now if some errors were encountered + // at the attributes processing stage + abort_if_dirty(); + + let fields = process_fields(&input.fields); + + // no need to think about emitted errors + // #[proc_macro_error] will handle them for you + // + // just return a TokenStream as you normally would + quote!(/* stuff */).into() +} +``` + +## Real world examples + +* [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) + (abort-like usage) +* [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) + +## Limitations + +- Warnings are emitted only on nightly, they are ignored on stable. +- "help" suggestions can't have their own span info on stable, + (essentially inheriting the parent span). +- If your macro happens to trigger a panic, no errors will be displayed. This is not a + technical limitation but rather intentional design. `panic` is not for error reporting. + +## MSRV policy + +The MSRV is currently `1.61`, and this is considered a breaking change to increase. + +However, if an existing dependency requires a higher MSRV without a semver breaking update, this may be raised. + +## Motivation + +Error handling in proc-macros sucks. There's not much of a choice today: +you either "bubble up" the error up to the top-level of the macro and convert it to +a [`compile_error!`][compl_err] invocation or just use a good old panic. Both these ways suck: + +- Former sucks because it's quite redundant to unroll a proper error handling + just for critical errors that will crash the macro anyway; so people mostly + choose not to bother with it at all and use panic. Simple `.expect` is too tempting. + + Also, if you do decide to implement this `Result`-based architecture in your macro + you're going to have to rewrite it entirely once [`proc_macro::Diagnostic`][] is finally + stable. Not cool. + +- Later sucks because there's no way to carry out the span info via `panic!`. + `rustc` will highlight the invocation itself but not some specific token inside it. + + Furthermore, panics aren't for error-reporting at all; panics are for bug-detecting + (like unwrapping on `None` or out-of-range indexing) or for early development stages + when you need a prototype ASAP so error handling can wait. Mixing these usages only + messes things up. + +- There is [`proc_macro::Diagnostic`][] which is awesome but it has been experimental + for more than a year and is unlikely to be stabilized any time soon. + + This crate's API is intentionally designed to be compatible with `proc_macro::Diagnostic` + and delegates to it whenever possible. Once `Diagnostics` is stable this crate + will **always** delegate to it, no code changes will be required on user side. + +That said, we need a solution, but this solution must meet these conditions: + +- It must be better than `panic!`. The main point: it must offer a way to carry the span information + over to user. +- It must take as little effort as possible to migrate from `panic!`. Ideally, a new + macro with similar semantics plus ability to carry out span info. +- It must maintain compatibility with [`proc_macro::Diagnostic`][] . +- **It must be usable on stable**. + +This crate aims to provide such a mechanism. All you have to do is annotate your top-level +`#[proc_macro]` function with `#[proc_macro_error]` attribute and change panics to +[`abort!`]/[`abort_call_site!`] where appropriate, see [the Guide][guide]. + +## Disclaimer +Please note that **this crate is not intended to be used in any way other +than error reporting in procedural macros**, use `Result` and `?` (possibly along with one of the +many helpers out there) for anything else. + +
+ +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + + + +[compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html +[`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html + +[crate::dummy]: https://docs.rs/proc-macro-error2/1/proc_macro_error/dummy/index.html +[crate::multi]: https://docs.rs/proc-macro-error2/1/proc_macro_error/multi/index.html + +[`abort_call_site!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort_call_site.html +[`abort!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort.html +[guide]: https://docs.rs/proc-macro-error2 diff --git a/patches/proc-macro-error2/proc-macro-error-attr/.gitignore b/patches/proc-macro-error2/proc-macro-error-attr/.gitignore new file mode 100644 index 0000000..5e81b66 --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.fuse_hidden* diff --git a/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml b/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml new file mode 100644 index 0000000..29a4f0c --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "proc-macro-error-attr2" +version = "2.0.0" +authors = [ + "CreepySkeleton ", + "GnomedDev ", +] +edition = "2021" +rust-version = "1.61" +description = "Attribute macro for the proc-macro-error2 crate" +license = "MIT OR Apache-2.0" +repository = "https://github.com/GnomedDev/proc-macro-error-2" + +[lib] +proc-macro = true + +[dependencies] +quote = "1" +proc-macro2 = "1" + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } diff --git a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE new file mode 100644 index 0000000..658240a --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019-2020 CreepySkeleton + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT new file mode 100644 index 0000000..fc73e59 --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 CreepySkeleton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs new file mode 100644 index 0000000..0f25931 --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs @@ -0,0 +1,111 @@ +//! This is `#[proc_macro_error]` attribute to be used with +//! [`proc-macro-error`](https://docs.rs/proc-macro-error2/). There you go. + +use crate::parse::parse_input; +use crate::parse::Attribute; +use proc_macro::TokenStream; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2, TokenTree}; +use quote::{quote, quote_spanned}; + +use crate::settings::{ + parse_settings, + Setting::{AllowNotMacro, AssertUnwindSafe, ProcMacroHack}, + Settings, +}; + +mod parse; +mod settings; + +type Result = std::result::Result; + +struct Error { + span: Span, + message: String, +} + +impl Error { + fn new(span: Span, message: String) -> Self { + Error { span, message } + } + + fn into_compile_error(self) -> TokenStream2 { + let mut message = Literal::string(&self.message); + message.set_span(self.span); + quote_spanned!(self.span=> compile_error!{#message}) + } +} + +#[proc_macro_attribute] +pub fn proc_macro_error(attr: TokenStream, input: TokenStream) -> TokenStream { + match impl_proc_macro_error(attr.into(), input.clone().into()) { + Ok(ts) => ts, + Err(e) => { + let error = e.into_compile_error(); + let input = TokenStream2::from(input); + + quote!(#input #error).into() + } + } +} + +fn impl_proc_macro_error(attr: TokenStream2, input: TokenStream2) -> Result { + let (attrs, signature, body) = parse_input(input)?; + let mut settings = parse_settings(attr)?; + + let is_proc_macro = is_proc_macro(&attrs); + if is_proc_macro { + settings.set(AssertUnwindSafe); + } + + if detect_proc_macro_hack(&attrs) { + settings.set(ProcMacroHack); + } + + if settings.is_set(ProcMacroHack) { + settings.set(AllowNotMacro); + } + + if !(settings.is_set(AllowNotMacro) || is_proc_macro) { + return Err(Error::new( + Span::call_site(), + "#[proc_macro_error] attribute can be used only with procedural macros\n\n \ + = hint: if you are really sure that #[proc_macro_error] should be applied \ + to this exact function, use #[proc_macro_error(allow_not_macro)]\n" + .into(), + )); + } + + let body = gen_body(&body, &settings); + + let res = quote! { + #(#attrs)* + #(#signature)* + { #body } + }; + Ok(res.into()) +} + +fn gen_body(block: &TokenTree, settings: &Settings) -> proc_macro2::TokenStream { + let is_proc_macro_hack = settings.is_set(ProcMacroHack); + let closure = if settings.is_set(AssertUnwindSafe) { + quote!(::std::panic::AssertUnwindSafe(|| #block )) + } else { + quote!(|| #block) + }; + + quote!( ::proc_macro_error2::entry_point(#closure, #is_proc_macro_hack) ) +} + +fn detect_proc_macro_hack(attrs: &[Attribute]) -> bool { + attrs + .iter() + .any(|attr| attr.path_is_ident("proc_macro_hack")) +} + +fn is_proc_macro(attrs: &[Attribute]) -> bool { + attrs.iter().any(|attr| { + attr.path_is_ident("proc_macro") + || attr.path_is_ident("proc_macro_derive") + || attr.path_is_ident("proc_macro_attribute") + }) +} diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs new file mode 100644 index 0000000..eedb495 --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs @@ -0,0 +1,89 @@ +use crate::{Error, Result}; +use proc_macro2::{Delimiter, Ident, Span, TokenStream, TokenTree}; +use quote::ToTokens; +use std::iter::Peekable; + +pub(crate) fn parse_input( + input: TokenStream, +) -> Result<(Vec, Vec, TokenTree)> { + let mut input = input.into_iter().peekable(); + let mut attrs = Vec::new(); + + while let Some(attr) = parse_next_attr(&mut input)? { + attrs.push(attr); + } + + let sig = parse_signature(&mut input); + let body = input.next().ok_or_else(|| { + Error::new( + Span::call_site(), + "`#[proc_macro_error]` can be applied only to functions".to_string(), + ) + })?; + + Ok((attrs, sig, body)) +} + +fn parse_next_attr( + input: &mut Peekable>, +) -> Result> { + let shebang = match input.peek() { + Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => input.next().unwrap(), + _ => return Ok(None), + }; + + let group = match input.peek() { + Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => { + let res = group.clone(); + input.next(); + res + } + other => { + let span = other.map_or(Span::call_site(), TokenTree::span); + return Err(Error::new(span, "expected `[`".to_string())); + } + }; + + let path = match group.stream().into_iter().next() { + Some(TokenTree::Ident(ident)) => Some(ident), + _ => None, + }; + + Ok(Some(Attribute { + shebang, + group: TokenTree::Group(group), + path, + })) +} + +fn parse_signature(input: &mut Peekable>) -> Vec { + let mut sig = Vec::new(); + loop { + match input.peek() { + Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => { + return sig; + } + None => return sig, + _ => sig.push(input.next().unwrap()), + } + } +} + +pub(crate) struct Attribute { + pub(crate) shebang: TokenTree, + pub(crate) group: TokenTree, + pub(crate) path: Option, +} + +impl Attribute { + pub(crate) fn path_is_ident(&self, ident: &str) -> bool { + self.path.as_ref().map_or(false, |p| *p == ident) + } +} + +impl ToTokens for Attribute { + fn to_tokens(&self, ts: &mut TokenStream) { + self.shebang.to_tokens(ts); + self.group.to_tokens(ts); + } +} diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs new file mode 100644 index 0000000..f87bd0b --- /dev/null +++ b/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs @@ -0,0 +1,72 @@ +use crate::{Error, Result}; +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; + +macro_rules! decl_settings { + ($($val:expr => $variant:ident),+ $(,)*) => { + #[derive(PartialEq, Clone, Copy)] + pub(crate) enum Setting { + $($variant),* + } + + fn ident_to_setting(ident: Ident) -> Result { + match &*ident.to_string() { + $($val => Ok(Setting::$variant),)* + _ => { + let possible_vals = [$($val),*] + .iter() + .map(|v| format!("`{}`", v)) + .collect::>() + .join(", "); + + Err(Error::new( + ident.span(), + format!("unknown setting `{}`, expected one of {}", ident, possible_vals))) + } + } + } + }; +} + +decl_settings! { + "assert_unwind_safe" => AssertUnwindSafe, + "allow_not_macro" => AllowNotMacro, + "proc_macro_hack" => ProcMacroHack, +} + +pub(crate) fn parse_settings(input: TokenStream) -> Result { + let mut input = input.into_iter(); + let mut res = Settings(Vec::new()); + loop { + match input.next() { + Some(TokenTree::Ident(ident)) => { + res.0.push(ident_to_setting(ident)?); + } + None => return Ok(res), + other => { + let span = other.map_or(Span::call_site(), |tt| tt.span()); + return Err(Error::new(span, "expected identifier".to_string())); + } + } + + match input.next() { + Some(TokenTree::Punct(ref punct)) if punct.as_char() == ',' => {} + None => return Ok(res), + other => { + let span = other.map_or(Span::call_site(), |tt| tt.span()); + return Err(Error::new(span, "expected `,`".to_string())); + } + } + } +} + +pub(crate) struct Settings(Vec); + +impl Settings { + pub(crate) fn is_set(&self, setting: Setting) -> bool { + self.0.iter().any(|s| *s == setting) + } + + pub(crate) fn set(&mut self, setting: Setting) { + self.0.push(setting); + } +} diff --git a/patches/proc-macro-error2/src/diagnostic.rs b/patches/proc-macro-error2/src/diagnostic.rs new file mode 100644 index 0000000..0455511 --- /dev/null +++ b/patches/proc-macro-error2/src/diagnostic.rs @@ -0,0 +1,360 @@ +use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange}; +use proc_macro2::Span; +use proc_macro2::TokenStream; + +use quote::{quote_spanned, ToTokens}; + +/// Represents a diagnostic level +/// +/// # Warnings +/// +/// Warnings are ignored on stable/beta +#[derive(Debug, PartialEq)] +#[non_exhaustive] +pub enum Level { + Error, + Warning, +} + +/// Represents a single diagnostic message +#[derive(Debug)] +#[must_use = "A diagnostic does nothing unless emitted"] +pub struct Diagnostic { + pub(crate) level: Level, + pub(crate) span_range: SpanRange, + pub(crate) msg: String, + pub(crate) suggestions: Vec<(SuggestionKind, String, Option)>, + pub(crate) children: Vec<(SpanRange, String)>, +} + +/// A collection of methods that do not exist in `proc_macro::Diagnostic` +/// but still useful to have around. +/// +/// This trait is sealed and cannot be implemented outside of `proc_macro_error`. +pub trait DiagnosticExt: Sealed { + /// Create a new diagnostic message that points to the `span_range`. + /// + /// This function is the same as `Diagnostic::spanned` but produces considerably + /// better error messages for multi-token spans on stable. + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self; + + /// Add another error message to self such that it will be emitted right after + /// the main message. + /// + /// This function is the same as `Diagnostic::span_error` but produces considerably + /// better error messages for multi-token spans on stable. + #[must_use] + fn span_range_error(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_help` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + #[must_use] + fn span_range_help(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_note` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + #[must_use] + fn span_range_note(self, span_range: SpanRange, msg: String) -> Self; +} + +impl DiagnosticExt for Diagnostic { + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self { + Diagnostic { + level, + span_range, + msg: message, + suggestions: vec![], + children: vec![], + } + } + + fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self { + self.children.push((span_range, msg)); + self + } + + fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Help, msg, Some(span_range))); + self + } + + fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Note, msg, Some(span_range))); + self + } +} + +impl Diagnostic { + /// Create a new diagnostic message that points to `Span::call_site()` + pub fn new(level: Level, message: String) -> Self { + Diagnostic::spanned(Span::call_site(), level, message) + } + + /// Create a new diagnostic message that points to the `span` + pub fn spanned(span: Span, level: Level, message: String) -> Self { + Diagnostic::spanned_range( + SpanRange { + first: span, + last: span, + }, + level, + message, + ) + } + + /// Add another error message to self such that it will be emitted right after + /// the main message. + pub fn span_error(self, span: Span, msg: String) -> Self { + self.span_range_error( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_help(self, span: Span, msg: String) -> Self { + self.span_range_help( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message. + pub fn help(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Help, msg, None)); + self + } + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_note(self, span: Span, msg: String) -> Self { + self.span_range_note( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a note to your main message + pub fn note(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Note, msg, None)); + self + } + + /// The message of main warning/error (no notes attached) + #[must_use] + pub fn message(&self) -> &str { + &self.msg + } + + /// Abort the proc-macro's execution and display the diagnostic. + /// + /// # Warnings + /// + /// Warnings are not emitted on stable and beta, but this function will abort anyway. + pub fn abort(self) -> ! { + self.emit(); + abort_now() + } + + /// Display the diagnostic while not aborting macro execution. + /// + /// # Warnings + /// + /// Warnings are ignored on stable/beta + pub fn emit(self) { + check_correctness(); + crate::imp::emit_diagnostic(self); + } +} + +/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!** +#[doc(hidden)] +impl Diagnostic { + pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.span_help(span, msg), + _ => self.span_note(span, msg), + } + } + + pub fn suggestion(self, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.help(msg), + _ => self.note(msg), + } + } +} + +impl ToTokens for Diagnostic { + fn to_tokens(&self, ts: &mut TokenStream) { + use std::borrow::Cow; + + fn ensure_lf(buf: &mut String, s: &str) { + if s.ends_with('\n') { + buf.push_str(s); + } else { + buf.push_str(s); + buf.push('\n'); + } + } + + fn diag_to_tokens( + span_range: SpanRange, + level: &Level, + msg: &str, + suggestions: &[(SuggestionKind, String, Option)], + ) -> TokenStream { + if *level == Level::Warning { + return TokenStream::new(); + } + + let message = if suggestions.is_empty() { + Cow::Borrowed(msg) + } else { + let mut message = String::new(); + ensure_lf(&mut message, msg); + message.push('\n'); + + for (kind, note, _span) in suggestions { + message.push_str(" = "); + message.push_str(kind.name()); + message.push_str(": "); + ensure_lf(&mut message, note); + } + message.push('\n'); + + Cow::Owned(message) + }; + + let mut msg = proc_macro2::Literal::string(&message); + msg.set_span(span_range.last); + let group = quote_spanned!(span_range.last=> { #msg } ); + quote_spanned!(span_range.first=> compile_error!#group) + } + + ts.extend(diag_to_tokens( + self.span_range, + &self.level, + &self.msg, + &self.suggestions, + )); + ts.extend( + self.children + .iter() + .map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, msg, &[])), + ); + } +} + +#[derive(Debug)] +pub(crate) enum SuggestionKind { + Help, + Note, +} + +impl SuggestionKind { + fn name(&self) -> &'static str { + match self { + SuggestionKind::Note => "note", + SuggestionKind::Help => "help", + } + } +} + +#[cfg(feature = "syn-error")] +impl From for Diagnostic { + fn from(err: syn::Error) -> Self { + use proc_macro2::{Delimiter, TokenTree}; + + fn gut_error(ts: &mut impl Iterator) -> Option<(SpanRange, String)> { + let start_span = ts.next()?.span(); + ts.next().expect(":1"); + ts.next().expect("core"); + ts.next().expect(":2"); + ts.next().expect(":3"); + ts.next().expect("compile_error"); + ts.next().expect("!"); + + let lit = match ts.next().unwrap() { + TokenTree::Group(group) => { + // Currently `syn` builds `compile_error!` invocations + // exclusively in `ident{"..."}` (braced) form which is not + // followed by `;` (semicolon). + // + // But if it changes to `ident("...");` (parenthesized) + // or `ident["..."];` (bracketed) form, + // we will need to skip the `;` as well. + // Highly unlikely, but better safe than sorry. + + if group.delimiter() == Delimiter::Parenthesis + || group.delimiter() == Delimiter::Bracket + { + ts.next().unwrap(); // ; + } + + match group.stream().into_iter().next().unwrap() { + TokenTree::Literal(lit) => lit, + _ => unreachable!(""), + } + } + _ => unreachable!(""), + }; + + let last = lit.span(); + let mut msg = lit.to_string(); + + // "abc" => abc + msg.pop(); + msg.remove(0); + + Some(( + SpanRange { + first: start_span, + last, + }, + msg, + )) + } + + let mut ts = err.to_compile_error().into_iter(); + + let (span_range, msg) = gut_error(&mut ts).unwrap(); + let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg); + + while let Some((span_range, msg)) = gut_error(&mut ts) { + res = res.span_range_error(span_range, msg); + } + + res + } +} diff --git a/patches/proc-macro-error2/src/dummy.rs b/patches/proc-macro-error2/src/dummy.rs new file mode 100644 index 0000000..5bc98bd --- /dev/null +++ b/patches/proc-macro-error2/src/dummy.rs @@ -0,0 +1,151 @@ +//! Facility to emit dummy implementations (or whatever) in case +//! an error happen. +//! +//! `compile_error!` does not abort a compilation right away. This means +//! `rustc` doesn't just show you the error and abort, it carries on the +//! compilation process looking for other errors to report. +//! +//! Let's consider an example: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error2::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // second BOOM! +//! } +//! ``` +//! +//! The problem is: the generated token stream contains only `compile_error!` +//! invocation, the impl was not generated. That means user will see two compilation +//! errors: +//! +//! ```text +//! error: something's wrong +//! --> $DIR/probe.rs:9:10 +//! | +//! 9 |#[proc_macro_derive(MyTrait)] +//! | ^^^^^^^ +//! +//! error[E0599]: no function or associated item named `do_thing` found for type `Foo` in the current scope +//! --> src\main.rs:3:10 +//! | +//! 1 | struct Foo; +//! | ----------- function or associated item `do_thing` not found for this +//! 2 | fn main() { +//! 3 | Foo::do_thing(); // second BOOM! +//! | ^^^^^^^^ function or associated item not found in `Foo` +//! ``` +//! +//! But the second error is meaningless! We definitely need to fix this. +//! +//! Most used approach in cases like this is "dummy implementation" - +//! omit `impl MyTrait for #name` and fill functions bodies with `unimplemented!()`. +//! +//! This is how you do it: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error2::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // first of all - we set a dummy impl which will be appended to +//! // `compile_error!` invocations in case a trigger does happen +//! set_dummy(quote! { +//! impl MyTrait for #name { +//! fn do_thing() { unimplemented!() } +//! } +//! }); +//! +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // no more errors! +//! } +//! ``` + +use proc_macro2::TokenStream; +use std::cell::RefCell; + +use crate::check_correctness; + +thread_local! { + static DUMMY_IMPL: RefCell> = const { RefCell::new(None) }; +} + +/// Sets dummy token stream which will be appended to `compile_error!(msg);...` +/// invocations in case you'll emit any errors. +/// +/// See [guide](../index.html#guide). +#[allow(clippy::must_use_candidate)] // Mutates thread local state +pub fn set_dummy(dummy: TokenStream) -> Option { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(Some(dummy))) +} + +/// Same as [`set_dummy`] but, instead of resetting, appends tokens to the +/// existing dummy (if any). Behaves as `set_dummy` if no dummy is present. +pub fn append_dummy(dummy: TokenStream) { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| { + let mut cell = old_dummy.borrow_mut(); + if let Some(ts) = cell.as_mut() { + ts.extend(dummy); + } else { + *cell = Some(dummy); + } + }); +} + +pub(crate) fn cleanup() -> Option { + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(None)) +} diff --git a/patches/proc-macro-error2/src/imp/delegate.rs b/patches/proc-macro-error2/src/imp/delegate.rs new file mode 100644 index 0000000..a3e6a80 --- /dev/null +++ b/patches/proc-macro-error2/src/imp/delegate.rs @@ -0,0 +1,68 @@ +//! This implementation uses [`proc_macro::Diagnostic`], nightly only. + +use std::cell::Cell; + +use proc_macro::{Diagnostic as PDiag, Level as PLevel}; + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level, SuggestionKind}, +}; + +pub fn abort_if_dirty() { + check_correctness(); + if IS_DIRTY.with(|c| c.get()) { + abort_now() + } +} + +pub(crate) fn cleanup() -> Vec { + IS_DIRTY.with(|c| c.set(false)); + vec![] +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + let Diagnostic { + level, + span_range, + msg, + suggestions, + children, + } = diag; + + let span = span_range.collapse().unwrap(); + + let level = match level { + Level::Warning => PLevel::Warning, + Level::Error => { + IS_DIRTY.with(|c| c.set(true)); + PLevel::Error + } + }; + + let mut res = PDiag::spanned(span, level, msg); + + for (kind, msg, span) in suggestions { + res = match (kind, span) { + (SuggestionKind::Note, Some(span_range)) => { + res.span_note(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Help, Some(span_range)) => { + res.span_help(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Note, None) => res.note(msg), + (SuggestionKind::Help, None) => res.help(msg), + } + } + + for (span_range, msg) in children { + let span = span_range.collapse().unwrap(); + res = res.span_error(span, msg); + } + + res.emit() +} + +thread_local! { + static IS_DIRTY: Cell = Cell::new(false); +} diff --git a/patches/proc-macro-error2/src/imp/fallback.rs b/patches/proc-macro-error2/src/imp/fallback.rs new file mode 100644 index 0000000..c10eb9a --- /dev/null +++ b/patches/proc-macro-error2/src/imp/fallback.rs @@ -0,0 +1,30 @@ +//! This implementation uses self-written stable facilities. + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level}, +}; +use std::cell::RefCell; + +pub fn abort_if_dirty() { + check_correctness(); + ERR_STORAGE.with(|storage| { + if !storage.borrow().is_empty() { + abort_now() + } + }); +} + +pub(crate) fn cleanup() -> Vec { + ERR_STORAGE.with(|storage| storage.replace(Vec::new())) +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + if diag.level == Level::Error { + ERR_STORAGE.with(|storage| storage.borrow_mut().push(diag)); + } +} + +thread_local! { + static ERR_STORAGE: RefCell> = const { RefCell::new(Vec::new()) }; +} diff --git a/patches/proc-macro-error2/src/lib.rs b/patches/proc-macro-error2/src/lib.rs new file mode 100644 index 0000000..d496790 --- /dev/null +++ b/patches/proc-macro-error2/src/lib.rs @@ -0,0 +1,565 @@ +//! # proc-macro-error2 +//! +//! This crate aims to make error reporting in proc-macros simple and easy to use. +//! Migrate from `panic!`-based errors for as little effort as possible! +//! +//! (Also, you can explicitly [append a dummy token stream](dummy/index.html) to your errors). +//! +//! To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +//! `compile_error!`. It detects the best way of emitting available based on compiler's version. +//! When the underlying diagnostic type is finally stabilized, this crate will simply be +//! delegating to it requiring no changes in your code! +//! +//! So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +//! available on stable ahead of time *and* your error-reporting code future-proof. +//! +//! ## Cargo features +//! +//! This crate provides *enabled by default* `syn-error` feature that gates +//! `impl From for Diagnostic` conversion. If you don't use `syn` and want +//! to cut off some of compilation time, you can disable it via +//! +//! ```toml +//! [dependencies] +//! proc-macro-error2 = { version = "2.0.0", default-features = false } +//! ``` +//! +//! ***Please note that disabling this feature makes sense only if you don't depend on `syn` +//! directly or indirectly, and you very likely do.** +//! +//! ## Real world examples +//! +//! * [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) +//! (abort-like usage) +//! * [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) +//! +//! ## Limitations +//! +//! - Warnings are emitted only on nightly, they are ignored on stable. +//! - "help" suggestions can't have their own span info on stable, +//! (essentially inheriting the parent span). +//! - If a panic occurs somewhere in your macro no errors will be displayed. This is not a +//! technical limitation but rather intentional design. `panic` is not for error reporting. +//! +//! ### `#[proc_macro_error]` attribute +//! +//! **This attribute MUST be present on the top level of your macro** (the function +//! annotated with any of `#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`). +//! +//! This attribute performs the setup and cleanup necessary to make things work. +//! +//! In most cases you'll need the simple `#[proc_macro_error]` form without any +//! additional settings. Feel free to [skip the "Syntax" section](#macros). +//! +//! #### Syntax +//! +//! `#[proc_macro_error]` or `#[proc_macro_error(settings...)]`, where `settings...` +//! is a comma-separated list of: +//! +//! - `proc_macro_hack`: +//! +//! In order to correctly cooperate with `#[proc_macro_hack]`, `#[proc_macro_error]` +//! attribute must be placed *before* (above) it, like this: +//! +//! ```no_run +//! # use proc_macro2::TokenStream; +//! # const IGNORE: &str = " +//! #[proc_macro_error] +//! #[proc_macro_hack] +//! #[proc_macro] +//! # "; +//! fn my_macro(input: TokenStream) -> TokenStream { +//! unimplemented!() +//! } +//! ``` +//! +//! If, for some reason, you can't place it like that you can use +//! `#[proc_macro_error(proc_macro_hack)]` instead. +//! +//! # Note +//! +//! If `proc-macro-hack` was detected (by any means) `allow_not_macro` +//! and `assert_unwind_safe` will be applied automatically. +//! +//! - `allow_not_macro`: +//! +//! By default, the attribute checks that it's applied to a proc-macro. +//! If none of `#[proc_macro]`, `#[proc_macro_derive]` nor `#[proc_macro_attribute]` are +//! present it will panic. It's the intention - this crate is supposed to be used only with +//! proc-macros. +//! +//! This setting is made to bypass the check, useful in certain circumstances. +//! +//! Pay attention: the function this attribute is applied to must return +//! `proc_macro::TokenStream`. +//! +//! This setting is implied if `proc-macro-hack` was detected. +//! +//! - `assert_unwind_safe`: +//! +//! By default, your code must be [unwind safe]. If your code is not unwind safe, +//! but you believe it's correct, you can use this setting to bypass the check. +//! You would need this for code that uses `lazy_static` or `thread_local` with +//! `Cell/RefCell` inside (and the like). +//! +//! This setting is implied if `#[proc_macro_error]` is applied to a function +//! marked as `#[proc_macro]`, `#[proc_macro_derive]` or `#[proc_macro_attribute]`. +//! +//! This setting is also implied if `proc-macro-hack` was detected. +//! +//! ## Macros +//! +//! Most of the time you want to use the macros. Syntax is described in the next section below. +//! +//! You'll need to decide how you want to emit errors: +//! +//! * Emit the error and abort. Very much panic-like usage. Served by [`abort!`] and +//! [`abort_call_site!`]. +//! * Emit the error but do not abort right away, looking for other errors to report. +//! Served by [`emit_error!`] and [`emit_call_site_error!`]. +//! +//! You **can** mix these usages. +//! +//! `abort` and `emit_error` take a "source span" as the first argument. This source +//! will be used to highlight the place the error originates from. It must be one of: +//! +//! * *Something* that implements [`ToTokens`] (most types in `syn` and `proc-macro2` do). +//! This source is the preferable one since it doesn't lose span information on multi-token +//! spans, see [this issue](https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/6) +//! for details. +//! * [`proc_macro::Span`] +//! * [`proc-macro2::Span`] +//! +//! The rest is your message in format-like style. +//! +//! See [the next section](#syntax-1) for detailed syntax. +//! +//! - [`abort!`]: +//! +//! Very much panic-like usage - abort right away and show the error. +//! Expands to [`!`] (never type). +//! +//! - [`abort_call_site!`]: +//! +//! Shortcut for `abort!(Span::call_site(), ...)`. Expands to [`!`] (never type). +//! +//! - [`emit_error!`]: +//! +//! [`proc_macro::Diagnostic`]-like usage - emit the error but keep going, +//! looking for other errors to report. +//! The compilation will fail nonetheless. Expands to [`()`] (unit type). +//! +//! - [`emit_call_site_error!`]: +//! +//! Shortcut for `emit_error!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`emit_warning!`]: +//! +//! Like `emit_error!` but emit a warning instead of error. The compilation won't fail +//! because of warnings. +//! Expands to [`()`] (unit type). +//! +//! **Beware**: warnings are nightly only, they are completely ignored on stable. +//! +//! - [`emit_call_site_warning!`]: +//! +//! Shortcut for `emit_warning!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`diagnostic`]: +//! +//! Build an instance of `Diagnostic` in format-like style. +//! +//! #### Syntax +//! +//! All the macros have pretty much the same syntax: +//! +//! 1. ```ignore +//! abort!(single_expr) +//! ``` +//! Shortcut for `Diagnostic::from(expr).abort()`. +//! +//! 2. ```ignore +//! abort!(span, message) +//! ``` +//! The first argument is an expression the span info should be taken from. +//! +//! The second argument is the error message, it must implement [`ToString`]. +//! +//! 3. ```ignore +//! abort!(span, format_literal, format_args...) +//! ``` +//! +//! This form is pretty much the same as 2, except `format!(format_literal, format_args...)` +//! will be used to for the message instead of [`ToString`]. +//! +//! That's it. `abort!`, `emit_warning`, `emit_error` share this exact syntax. +//! +//! `abort_call_site!`, `emit_call_site_warning`, `emit_call_site_error` lack 1 form +//! and do not take span in 2'th and 3'th forms. Those are essentially shortcuts for +//! `macro!(Span::call_site(), args...)`. +//! +//! `diagnostic!` requires a [`Level`] instance between `span` and second argument +//! (1'th form is the same). +//! +//! > **Important!** +//! > +//! > If you have some type from `proc_macro` or `syn` to point to, do not call `.span()` +//! > on it but rather use it directly: +//! > ```no_run +//! > # use proc_macro_error2::abort; +//! > # let input = proc_macro2::TokenStream::new(); +//! > let ty: syn::Type = syn::parse2(input).unwrap(); +//! > abort!(ty, "BOOM"); +//! > // ^^ <-- avoid .span() +//! > ``` +//! > +//! > `.span()` calls work too, but you may experience regressions in message quality. +//! +//! #### Note attachments +//! +//! 3. Every macro can have "note" attachments (only 2 and 3 form). +//! ```ignore +//! let opt_help = if have_some_info { Some("did you mean `this`?") } else { None }; +//! +//! abort!( +//! span, message; // <--- attachments start with `;` (semicolon) +//! +//! help = "format {} {}", "arg1", "arg2"; // <--- every attachment ends with `;`, +//! // maybe except the last one +//! +//! note = "to_string"; // <--- one arg uses `.to_string()` instead of `format!()` +//! +//! yay = "I see what {} did here", "you"; // <--- "help =" and "hint =" are mapped +//! // to Diagnostic::help, +//! // anything else is Diagnostic::note +//! +//! wow = note_span => "custom span"; // <--- attachments can have their own span +//! // it takes effect only on nightly though +//! +//! hint =? opt_help; // <-- "optional" attachment, get displayed only if `Some` +//! // must be single `Option` expression +//! +//! note =? note_span => opt_help // <-- optional attachments can have custom spans too +//! ); +//! ``` +//! + +//! ### Diagnostic type +//! +//! [`Diagnostic`] type is intentionally designed to be API compatible with [`proc_macro::Diagnostic`]. +//! Not all API is implemented, only the part that can be reasonably implemented on stable. +//! +//! +//! [`abort!`]: macro.abort.html +//! [`abort_call_site!`]: macro.abort_call_site.html +//! [`emit_warning!`]: macro.emit_warning.html +//! [`emit_error!`]: macro.emit_error.html +//! [`emit_call_site_warning!`]: macro.emit_call_site_error.html +//! [`emit_call_site_error!`]: macro.emit_call_site_warning.html +//! [`diagnostic!`]: macro.diagnostic.html +//! [`Diagnostic`]: struct.Diagnostic.html +//! +//! [`proc_macro::Span`]: https://doc.rust-lang.org/proc_macro/struct.Span.html +//! [`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html +//! +//! [unwind safe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html#what-is-unwind-safety +//! [`!`]: https://doc.rust-lang.org/std/primitive.never.html +//! [`()`]: https://doc.rust-lang.org/std/primitive.unit.html +//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html +//! +//! [`proc-macro2::Span`]: https://docs.rs/proc-macro2/1.0.10/proc_macro2/struct.Span.html +//! [`ToTokens`]: https://docs.rs/quote/1.0.3/quote/trait.ToTokens.html +//! + +#![cfg_attr(feature = "nightly", feature(proc_macro_diagnostic))] +#![forbid(unsafe_code)] + +pub extern crate proc_macro; + +pub use crate::{ + diagnostic::{Diagnostic, DiagnosticExt, Level}, + dummy::{append_dummy, set_dummy}, +}; +pub use proc_macro_error_attr2::proc_macro_error; + +use proc_macro2::Span; +use quote::{quote, ToTokens}; + +use std::cell::Cell; +use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; + +pub mod dummy; + +mod diagnostic; +mod macros; +mod sealed; + +#[cfg(not(feature = "nightly"))] +#[path = "imp/fallback.rs"] +mod imp; + +#[cfg(feature = "nightly")] +#[path = "imp/delegate.rs"] +mod imp; + +#[derive(Debug, Clone, Copy)] +#[must_use = "A SpanRange does nothing unless used"] +pub struct SpanRange { + pub first: Span, + pub last: Span, +} + +impl SpanRange { + /// Create a range with the `first` and `last` spans being the same. + pub fn single_span(span: Span) -> Self { + SpanRange { + first: span, + last: span, + } + } + + /// Create a `SpanRange` resolving at call site. + pub fn call_site() -> Self { + SpanRange::single_span(Span::call_site()) + } + + /// Construct span range from a `TokenStream`. This method always preserves all the + /// range. + /// + /// ### Note + /// + /// If the stream is empty, the result is `SpanRange::call_site()`. If the stream + /// consists of only one `TokenTree`, the result is `SpanRange::single_span(tt.span())` + /// that doesn't lose anything. + pub fn from_tokens(ts: &dyn ToTokens) -> Self { + let mut spans = ts.to_token_stream().into_iter().map(|tt| tt.span()); + let first = spans.next().unwrap_or_else(Span::call_site); + let last = spans.last().unwrap_or(first); + + SpanRange { first, last } + } + + /// Join two span ranges. The resulting range will start at `self.first` and end at + /// `other.last`. + pub fn join_range(self, other: SpanRange) -> Self { + SpanRange { + first: self.first, + last: other.last, + } + } + + /// Collapse the range into single span, preserving as much information as possible. + #[must_use] + pub fn collapse(self) -> Span { + self.first.join(self.last).unwrap_or(self.first) + } +} + +/// This traits expands `Result>` with some handy shortcuts. +pub trait ResultExt { + type Ok; + + /// Behaves like `Result::unwrap`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + fn unwrap_or_abort(self) -> Self::Ok; + + /// Behaves like `Result::expect`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + /// If it aborts then resulting error message will be preceded with `message`. + fn expect_or_abort(self, msg: &str) -> Self::Ok; +} + +/// This traits expands `Option` with some handy shortcuts. +pub trait OptionExt { + type Some; + + /// Behaves like `Option::expect`: if self is `Some` yield the contained value, + /// otherwise abort macro execution via `abort_call_site!`. + /// If it aborts the `message` will be used for [`compile_error!`][compl_err] invocation. + /// + /// [compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html + fn expect_or_abort(self, msg: &str) -> Self::Some; +} + +/// Abort macro execution and display all the emitted errors, if any. +/// +/// Does nothing if no errors were emitted (warnings do not count). +pub fn abort_if_dirty() { + imp::abort_if_dirty(); +} + +impl> ResultExt for Result { + type Ok = T; + + fn unwrap_or_abort(self) -> T { + match self { + Ok(res) => res, + Err(e) => e.into().abort(), + } + } + + fn expect_or_abort(self, message: &str) -> T { + match self { + Ok(res) => res, + Err(e) => { + let mut e = e.into(); + e.msg = format!("{}: {}", message, e.msg); + e.abort() + } + } + } +} + +impl OptionExt for Option { + type Some = T; + + fn expect_or_abort(self, message: &str) -> T { + match self { + Some(res) => res, + None => abort_call_site!(message), + } + } +} + +/// This is the entry point for a proc-macro. +/// +/// **NOT PUBLIC API, SUBJECT TO CHANGE WITHOUT ANY NOTICE** +#[doc(hidden)] +pub fn entry_point(f: F, proc_macro_hack: bool) -> proc_macro::TokenStream +where + F: FnOnce() -> proc_macro::TokenStream + UnwindSafe, +{ + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() + 1)); + let caught = catch_unwind(f); + let dummy = dummy::cleanup(); + let err_storage = imp::cleanup(); + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() - 1)); + + let gen_error = || { + if proc_macro_hack { + quote! {{ + macro_rules! proc_macro_call { + () => ( unimplemented!() ) + } + + #(#err_storage)* + #dummy + + unimplemented!() + }} + } else { + quote!( #(#err_storage)* #dummy ) + } + }; + + match caught { + Ok(ts) => { + if err_storage.is_empty() { + ts + } else { + gen_error().into() + } + } + + Err(boxed) => match boxed.downcast::() { + Ok(_) => gen_error().into(), + Err(boxed) => resume_unwind(boxed), + }, + } +} + +fn abort_now() -> ! { + check_correctness(); + std::panic::panic_any(AbortNow) +} + +thread_local! { + static ENTERED_ENTRY_POINT: Cell = const { Cell::new(0) }; +} + +struct AbortNow; + +fn check_correctness() { + assert!( + ENTERED_ENTRY_POINT.with(Cell::get) != 0, + "proc-macro-error2 API cannot be used outside of `entry_point` invocation, \ + perhaps you forgot to annotate your #[proc_macro] function with `#[proc_macro_error]" + ); +} + +/// **ALL THE STUFF INSIDE IS NOT PUBLIC API!!!** +#[doc(hidden)] +pub mod __export { + // reexports for use in macros + pub use proc_macro; + pub use proc_macro2; + + use proc_macro2::Span; + use quote::ToTokens; + + use crate::SpanRange; + + // inspired by + // https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md#simple-application + + pub trait SpanAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait Span2AsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait ToTokensAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait SpanRangeAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + impl ToTokensAsSpanRange for &T { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + let mut ts = self.to_token_stream().into_iter(); + let first = match ts.next() { + Some(t) => t.span(), + None => Span::call_site(), + }; + + let last = match ts.last() { + Some(t) => t.span(), + None => first, + }; + + SpanRange { first, last } + } + } + + impl Span2AsSpanRange for Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: *self, + last: *self, + } + } + } + + impl SpanAsSpanRange for proc_macro::Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: (*self).into(), + last: (*self).into(), + } + } + } + + impl SpanRangeAsSpanRange for SpanRange { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + *self + } + } +} diff --git a/patches/proc-macro-error2/src/macros.rs b/patches/proc-macro-error2/src/macros.rs new file mode 100644 index 0000000..747b684 --- /dev/null +++ b/patches/proc-macro-error2/src/macros.rs @@ -0,0 +1,288 @@ +// FIXME: this can be greatly simplified via $()? +// as soon as MRSV hits 1.32 + +/// Build [`Diagnostic`](struct.Diagnostic.html) instance from provided arguments. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! diagnostic { + // from alias + ($err:expr) => { $crate::Diagnostic::from($err) }; + + // span, message, help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+ ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + ($span:expr, $level:expr, $msg:expr ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + // span, message, no help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ) + }}; + + ($span:expr, $level:expr, $msg:expr) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()) + }}; + + + // trailing commas + + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $fmt, $($args),* ; $($rest)*) + }; + ($span:expr, $level:expr, $msg:expr, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $msg ; $($rest)*) + }; + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+,) => { + $crate::diagnostic!($span, $level, $fmt, $($args),*) + }; + ($span:expr, $level:expr, $msg:expr,) => { + $crate::diagnostic!($span, $level, $msg) + }; + // ($err:expr,) => { $crate::diagnostic!($err) }; +} + +/// Abort proc-macro execution right now and display the error. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +#[macro_export] +macro_rules! abort { + ($err:expr) => { + $crate::diagnostic!($err).abort() + }; + + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Error, $($tts)*).abort() + }; +} + +/// Shortcut for `abort!(Span::call_site(), msg...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! abort_call_site { + ($($tts:tt)*) => { + $crate::abort!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit an error while not aborting the proc-macro right away. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_error { + ($err:expr) => { + $crate::diagnostic!($err).emit() + }; + + ($span:expr, $($tts:tt)*) => {{ + let level = $crate::Level::Error; + $crate::diagnostic!($span, level, $($tts)*).emit() + }}; +} + +/// Shortcut for `emit_error!(Span::call_site(), ...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting.. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_error { + ($($tts:tt)*) => { + $crate::emit_error!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit a warning. Warnings are not errors and compilation won't fail because of them. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_warning { + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Warning, $($tts)*).emit() + }; +} + +/// Shortcut for `emit_warning!(Span::call_site(), ...)`. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_warning { + ($($tts:tt)*) => {{ + $crate::emit_warning!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __pme__suggestions { + ($var:ident) => (); + + ($var:ident $help:ident =? $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.suggestion(stringify!($help), msg.to_string()) + } else { + $var + }; + }; + ($var:ident $help:ident =? $span:expr => $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.span_suggestion($span.into(), stringify!($help), msg.to_string()) + } else { + $var + }; + }; + + ($var:ident $help:ident =? $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident =? $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + + + ($var:ident $help:ident = $msg:expr) => { + let $var = $var.suggestion(stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+) => { + let $var = $var.suggestion( + stringify!($help), + format!($fmt, $($args),*) + ); + }; + ($var:ident $help:ident = $span:expr => $msg:expr) => { + let $var = $var.span_suggestion($span.into(), stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+) => { + let $var = $var.span_suggestion( + $span.into(), + stringify!($help), + format!($fmt, $($args),*) + ); + }; + + ($var:ident $help:ident = $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + + // trailing commas + + ($var:ident $help:ident = $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $msg) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+,) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $span => $msg) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),*,) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args)*) + }; + ($var:ident $help:ident = $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg; $($rest)*) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*; $($rest)*) + }; +} diff --git a/patches/proc-macro-error2/src/sealed.rs b/patches/proc-macro-error2/src/sealed.rs new file mode 100644 index 0000000..a2d5081 --- /dev/null +++ b/patches/proc-macro-error2/src/sealed.rs @@ -0,0 +1,3 @@ +pub trait Sealed {} + +impl Sealed for crate::Diagnostic {} diff --git a/patches/proc-macro-error2/test-crate/.gitignore b/patches/proc-macro-error2/test-crate/.gitignore new file mode 100644 index 0000000..5e81b66 --- /dev/null +++ b/patches/proc-macro-error2/test-crate/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.fuse_hidden* diff --git a/patches/proc-macro-error2/test-crate/Cargo.toml b/patches/proc-macro-error2/test-crate/Cargo.toml new file mode 100644 index 0000000..254cb09 --- /dev/null +++ b/patches/proc-macro-error2/test-crate/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "test-crate" +version = "0.0.0" +authors = [ + "CreepySkeleton ", + "GnomedDev ", +] +edition = "2021" +publish = false + +[lib] +path = "lib.rs" +proc-macro = true + +[dependencies] +proc-macro-error2 = { path = "../" } +quote = "1" +proc-macro2 = "1" + +[dependencies.syn] +version = "2" +features = ["full"] + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = { level = "allow" } diff --git a/patches/proc-macro-error2/test-crate/lib.rs b/patches/proc-macro-error2/test-crate/lib.rs new file mode 100644 index 0000000..1ecdfbc --- /dev/null +++ b/patches/proc-macro-error2/test-crate/lib.rs @@ -0,0 +1,272 @@ +use proc_macro2::{Span, TokenStream}; +use proc_macro_error2::{ + abort, abort_call_site, diagnostic, emit_call_site_error, emit_call_site_warning, emit_error, + emit_warning, proc_macro_error, set_dummy, Diagnostic, Level, OptionExt, ResultExt, SpanRange, +}; + +use quote::quote; +use syn::{parse_macro_input, spanned::Spanned}; + +// Macros and Diagnostic + +#[proc_macro] +#[proc_macro_error] +pub fn abort_from(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + abort!( + span, + syn::Error::new(Span::call_site(), "abort!(span, from) test") + ) +} + +#[proc_macro] +#[proc_macro_error] +pub fn abort_to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + abort!(span, "abort!(span, single_expr) test") +} + +#[proc_macro] +#[proc_macro_error] +pub fn abort_format(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + abort!(span, "abort!(span, expr1, {}) test", "expr2") +} + +#[proc_macro] +#[proc_macro_error] +pub fn abort_call_site_test(_: proc_macro::TokenStream) -> proc_macro::TokenStream { + abort_call_site!("abort_call_site! test") +} + +#[proc_macro] +#[proc_macro_error] +pub fn direct_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + Diagnostic::spanned(span.into(), Level::Error, "Diagnostic::abort() test".into()).abort() +} + +#[proc_macro] +#[proc_macro_error] +pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut spans = input.into_iter().step_by(2).map(|t| t.span()); + emit_error!( + spans.next().unwrap(), + syn::Error::new(Span::call_site(), "emit!(span, from) test") + ); + emit_error!( + spans.next().unwrap(), + "emit!(span, expr1, {}) test", + "expr2" + ); + emit_error!(spans.next().unwrap(), "emit!(span, single_expr) test"); + Diagnostic::spanned( + spans.next().unwrap().into(), + Level::Error, + "Diagnostic::emit() test".into(), + ) + .emit(); + + emit_call_site_error!("emit_call_site_error!(expr) test"); + + // NOOP on stable, just checking that the macros themselves compile. + emit_warning!(spans.next().unwrap(), "emit_warning! test"); + emit_call_site_warning!("emit_call_site_warning! test"); + + quote!().into() +} + +// Notes + +#[proc_macro] +#[proc_macro_error] +pub fn abort_notes(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut spans = input.into_iter().map(|s| s.span()); + let span = spans.next().unwrap(); + let span2 = spans.next().unwrap(); + + let some_note = Some("Some note"); + let none_note: Option<&'static str> = None; + + abort! { + span, "This is {} error", "an"; + + note = "simple note"; + help = "simple help"; + hint = "simple hint"; + yay = "simple yay"; + + note = "format {}", "note"; + + note =? some_note; + note =? none_note; + + note = span2 => "spanned simple note"; + note = span2 => "spanned format {}", "note"; + note =? span2 => some_note; + note =? span2 => none_note; + } +} + +#[proc_macro] +#[proc_macro_error] +pub fn emit_notes(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut spans = input.into_iter().step_by(2).map(|s| s.span()); + let span = spans.next().unwrap(); + let span2 = spans.next().unwrap(); + + let some_note = Some("Some note"); + let none_note: Option<&'static str> = None; + + abort! { + span, "This is {} error", "an"; + + note = "simple note"; + help = "simple help"; + hint = "simple hint"; + yay = "simple yay"; + + note = "format {}", "note"; + + note =? some_note; + note =? none_note; + + note = span2 => "spanned simple note"; + note = span2 => "spanned format {}", "note"; + note =? span2 => some_note; + note =? span2 => none_note; + } +} + +// Extension traits + +#[proc_macro] +#[proc_macro_error] +pub fn option_ext(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let none: Option<()> = None; + none.expect_or_abort("Option::expect_or_abort() test"); + quote!().into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn result_unwrap_or_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + let err = Diagnostic::spanned( + span.into(), + Level::Error, + "Result::unwrap_or_abort() test".to_string(), + ); + let res: Result<(), _> = Err(err); + res.unwrap_or_abort(); + quote!().into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn result_expect_or_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + let err = Diagnostic::spanned( + span.into(), + Level::Error, + "Result::expect_or_abort() test".to_string(), + ); + let res: Result<(), _> = Err(err); + res.expect_or_abort("BOOM"); + quote!().into() +} + +// Dummy + +#[proc_macro] +#[proc_macro_error] +pub fn dummy(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + set_dummy(quote! { + impl Default for NeedDefault { + fn default() -> Self { NeedDefault::A } + } + }); + + abort!(span, "set_dummy test"); +} + +#[proc_macro] +#[proc_macro_error] +pub fn append_dummy(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let span = input.into_iter().next().unwrap().span(); + set_dummy(quote! { + impl Default for NeedDefault + }); + + proc_macro_error2::append_dummy(quote!({ + fn default() -> Self { + NeedDefault::A + } + })); + + abort!(span, "append_dummy test"); +} + +// Panic + +#[proc_macro] +#[proc_macro_error] +pub fn unrelated_panic(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { + panic!("unrelated panic test") +} + +// Success + +#[proc_macro] +#[proc_macro_error] +pub fn ok(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = TokenStream::from(input); + quote!(fn #input() {}).into() +} + +// Multiple tokens + +#[proc_macro_attribute] +#[proc_macro_error] +pub fn multiple_tokens( + _: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let input = proc_macro2::TokenStream::from(input); + abort!(input, "..."); +} + +#[proc_macro] +#[proc_macro_error] +pub fn to_tokens_span(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ty = parse_macro_input!(input as syn::Type); + emit_error!(ty, "whole type"); + emit_error!(ty.span(), "explicit .span()"); + quote!().into() +} + +#[proc_macro] +#[proc_macro_error] +pub fn explicit_span_range(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut spans = input.into_iter().step_by(2).map(|s| s.span()); + let first = Span::from(spans.next().unwrap()); + let last = Span::from(spans.nth(1).unwrap()); + abort!(SpanRange { first, last }, "explicit SpanRange") +} + +// Children messages + +#[proc_macro] +#[proc_macro_error] +pub fn children_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut spans = input.into_iter().step_by(2).map(|s| s.span()); + diagnostic!(spans.next().unwrap(), Level::Error, "main macro message") + .span_error(spans.next().unwrap().into(), "child message".into()) + .emit(); + + let mut main = syn::Error::new(spans.next().unwrap().into(), "main syn::Error"); + let child = syn::Error::new(spans.next().unwrap().into(), "child syn::Error"); + main.combine(child); + Diagnostic::from(main).abort() +} diff --git a/patches/proc-macro-error2/tests/macro-errors.rs b/patches/proc-macro-error2/tests/macro-errors.rs new file mode 100644 index 0000000..bfe1ea3 --- /dev/null +++ b/patches/proc-macro-error2/tests/macro-errors.rs @@ -0,0 +1,6 @@ +#[test] +#[cfg(run_ui_tests)] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/patches/proc-macro-error2/tests/ok.rs b/patches/proc-macro-error2/tests/ok.rs new file mode 100644 index 0000000..402edbe --- /dev/null +++ b/patches/proc-macro-error2/tests/ok.rs @@ -0,0 +1,8 @@ +use test_crate::*; + +ok!(it_works); + +#[test] +fn check_it_works() { + it_works(); +} diff --git a/patches/proc-macro-error2/tests/runtime-errors.rs b/patches/proc-macro-error2/tests/runtime-errors.rs new file mode 100644 index 0000000..bb7e726 --- /dev/null +++ b/patches/proc-macro-error2/tests/runtime-errors.rs @@ -0,0 +1,13 @@ +use proc_macro_error2::*; + +#[test] +#[should_panic = "proc-macro-error2 API cannot be used outside of"] +fn missing_attr_emit() { + emit_call_site_error!("You won't see me"); +} + +#[test] +#[should_panic = "proc-macro-error2 API cannot be used outside of"] +fn missing_attr_abort() { + abort_call_site!("You won't see me"); +} diff --git a/patches/proc-macro-error2/tests/ui/abort.rs b/patches/proc-macro-error2/tests/ui/abort.rs new file mode 100644 index 0000000..e998e90 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/abort.rs @@ -0,0 +1,10 @@ +use test_crate::*; + +abort_from!(one, two); +abort_to_string!(one, two); +abort_format!(one, two); +direct_abort!(one, two); +abort_notes!(one, two); +abort_call_site_test!(one, two); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/abort.stderr b/patches/proc-macro-error2/tests/ui/abort.stderr new file mode 100644 index 0000000..2a210c5 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/abort.stderr @@ -0,0 +1,48 @@ +error: abort!(span, from) test + --> tests/ui/abort.rs:3:13 + | +3 | abort_from!(one, two); + | ^^^ + +error: abort!(span, single_expr) test + --> tests/ui/abort.rs:4:18 + | +4 | abort_to_string!(one, two); + | ^^^ + +error: abort!(span, expr1, expr2) test + --> tests/ui/abort.rs:5:15 + | +5 | abort_format!(one, two); + | ^^^ + +error: Diagnostic::abort() test + --> tests/ui/abort.rs:6:15 + | +6 | direct_abort!(one, two); + | ^^^ + +error: This is an error + + = note: simple note + = help: simple help + = help: simple hint + = note: simple yay + = note: format note + = note: Some note + = note: spanned simple note + = note: spanned format note + = note: Some note + + --> tests/ui/abort.rs:7:14 + | +7 | abort_notes!(one, two); + | ^^^ + +error: abort_call_site! test + --> tests/ui/abort.rs:8:1 + | +8 | abort_call_site_test!(one, two); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `abort_call_site_test` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/append_dummy.rs b/patches/proc-macro-error2/tests/ui/append_dummy.rs new file mode 100644 index 0000000..8838404 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/append_dummy.rs @@ -0,0 +1,12 @@ +use test_crate::*; + +enum NeedDefault { + A, + B, +} + +append_dummy!(need_default); + +fn main() { + let _ = NeedDefault::default(); +} diff --git a/patches/proc-macro-error2/tests/ui/append_dummy.stderr b/patches/proc-macro-error2/tests/ui/append_dummy.stderr new file mode 100644 index 0000000..c53708b --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/append_dummy.stderr @@ -0,0 +1,5 @@ +error: append_dummy test + --> tests/ui/append_dummy.rs:8:15 + | +8 | append_dummy!(need_default); + | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/children_messages.rs b/patches/proc-macro-error2/tests/ui/children_messages.rs new file mode 100644 index 0000000..a10ca7c --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/children_messages.rs @@ -0,0 +1,5 @@ +use test_crate::*; + +children_messages!(one, two, three, four); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/children_messages.stderr b/patches/proc-macro-error2/tests/ui/children_messages.stderr new file mode 100644 index 0000000..092eb05 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/children_messages.stderr @@ -0,0 +1,23 @@ +error: main macro message + --> tests/ui/children_messages.rs:3:20 + | +3 | children_messages!(one, two, three, four); + | ^^^ + +error: child message + --> tests/ui/children_messages.rs:3:25 + | +3 | children_messages!(one, two, three, four); + | ^^^ + +error: main syn::Error + --> tests/ui/children_messages.rs:3:30 + | +3 | children_messages!(one, two, three, four); + | ^^^^^ + +error: child syn::Error + --> tests/ui/children_messages.rs:3:37 + | +3 | children_messages!(one, two, three, four); + | ^^^^ diff --git a/patches/proc-macro-error2/tests/ui/dummy.rs b/patches/proc-macro-error2/tests/ui/dummy.rs new file mode 100644 index 0000000..ba146a5 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/dummy.rs @@ -0,0 +1,12 @@ +use test_crate::*; + +enum NeedDefault { + A, + B, +} + +dummy!(need_default); + +fn main() { + let _ = NeedDefault::default(); +} diff --git a/patches/proc-macro-error2/tests/ui/dummy.stderr b/patches/proc-macro-error2/tests/ui/dummy.stderr new file mode 100644 index 0000000..197d5ca --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/dummy.stderr @@ -0,0 +1,5 @@ +error: set_dummy test + --> tests/ui/dummy.rs:8:8 + | +8 | dummy!(need_default); + | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/emit.rs b/patches/proc-macro-error2/tests/ui/emit.rs new file mode 100644 index 0000000..6f1e389 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/emit.rs @@ -0,0 +1,6 @@ +use test_crate::*; + +emit!(one, two, three, four, five); +emit_notes!(one, two); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/emit.stderr b/patches/proc-macro-error2/tests/ui/emit.stderr new file mode 100644 index 0000000..02fc95e --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/emit.stderr @@ -0,0 +1,48 @@ +error: emit!(span, from) test + --> tests/ui/emit.rs:3:7 + | +3 | emit!(one, two, three, four, five); + | ^^^ + +error: emit!(span, expr1, expr2) test + --> tests/ui/emit.rs:3:12 + | +3 | emit!(one, two, three, four, five); + | ^^^ + +error: emit!(span, single_expr) test + --> tests/ui/emit.rs:3:17 + | +3 | emit!(one, two, three, four, five); + | ^^^^^ + +error: Diagnostic::emit() test + --> tests/ui/emit.rs:3:24 + | +3 | emit!(one, two, three, four, five); + | ^^^^ + +error: emit_call_site_error!(expr) test + --> tests/ui/emit.rs:3:1 + | +3 | emit!(one, two, three, four, five); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `emit` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: This is an error + + = note: simple note + = help: simple help + = help: simple hint + = note: simple yay + = note: format note + = note: Some note + = note: spanned simple note + = note: spanned format note + = note: Some note + + --> tests/ui/emit.rs:4:13 + | +4 | emit_notes!(one, two); + | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/explicit_span_range.rs b/patches/proc-macro-error2/tests/ui/explicit_span_range.rs new file mode 100644 index 0000000..7f0ff24 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/explicit_span_range.rs @@ -0,0 +1,5 @@ +use test_crate::*; + +explicit_span_range!(one, two, three, four); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr b/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr new file mode 100644 index 0000000..dd480c6 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr @@ -0,0 +1,5 @@ +error: explicit SpanRange + --> tests/ui/explicit_span_range.rs:3:22 + | +3 | explicit_span_range!(one, two, three, four); + | ^^^^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/misuse.rs b/patches/proc-macro-error2/tests/ui/misuse.rs new file mode 100644 index 0000000..dd86c0f --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/misuse.rs @@ -0,0 +1,10 @@ +use proc_macro_error2::abort; + +struct Foo; + +#[allow(unused)] +fn foo() { + abort!(Foo, "BOOM"); +} + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/misuse.stderr b/patches/proc-macro-error2/tests/ui/misuse.stderr new file mode 100644 index 0000000..ac17d0f --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/misuse.stderr @@ -0,0 +1,24 @@ +error[E0599]: the method `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange` exists for reference `&Foo`, but its trait bounds were not satisfied + --> tests/ui/misuse.rs:7:5 + | +3 | struct Foo; + | ---------- doesn't satisfy `Foo: quote::to_tokens::ToTokens` +... +7 | abort!(Foo, "BOOM"); + | ^^^^^^^^^^^^^^^^^^^ method cannot be called on `&Foo` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `Foo: quote::to_tokens::ToTokens` + which is required by `&Foo: ToTokensAsSpanRange` +note: the trait `quote::to_tokens::ToTokens` must be implemented + --> $CARGO/quote-1.0.37/src/to_tokens.rs + | + | pub trait ToTokens { + | ^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following traits define an item `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange`, perhaps you need to implement one of them: + candidate #1: `Span2AsSpanRange` + candidate #2: `SpanAsSpanRange` + candidate #3: `SpanRangeAsSpanRange` + candidate #4: `ToTokensAsSpanRange` + = note: this error originates in the macro `$crate::diagnostic` which comes from the expansion of the macro `abort` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/multiple_tokens.rs b/patches/proc-macro-error2/tests/ui/multiple_tokens.rs new file mode 100644 index 0000000..50fc2dd --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/multiple_tokens.rs @@ -0,0 +1,4 @@ +#[test_crate::multiple_tokens] +type T = (); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr b/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr new file mode 100644 index 0000000..af0cab0 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr @@ -0,0 +1,5 @@ +error: ... + --> tests/ui/multiple_tokens.rs:2:1 + | +2 | type T = (); + | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/not_proc_macro.rs b/patches/proc-macro-error2/tests/ui/not_proc_macro.rs new file mode 100644 index 0000000..03c596b --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/not_proc_macro.rs @@ -0,0 +1,4 @@ +use proc_macro_error2::proc_macro_error; + +#[proc_macro_error] +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr b/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr new file mode 100644 index 0000000..67012aa --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr @@ -0,0 +1,9 @@ +error: #[proc_macro_error] attribute can be used only with procedural macros + + = hint: if you are really sure that #[proc_macro_error] should be applied to this exact function, use #[proc_macro_error(allow_not_macro)] + --> tests/ui/not_proc_macro.rs:3:1 + | +3 | #[proc_macro_error] + | ^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `proc_macro_error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/option_ext.rs b/patches/proc-macro-error2/tests/ui/option_ext.rs new file mode 100644 index 0000000..19a650d --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/option_ext.rs @@ -0,0 +1,5 @@ +use test_crate::*; + +option_ext!(one, two); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/option_ext.stderr b/patches/proc-macro-error2/tests/ui/option_ext.stderr new file mode 100644 index 0000000..49bb22d --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/option_ext.stderr @@ -0,0 +1,7 @@ +error: Option::expect_or_abort() test + --> tests/ui/option_ext.rs:3:1 + | +3 | option_ext!(one, two); + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `option_ext` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/result_ext.rs b/patches/proc-macro-error2/tests/ui/result_ext.rs new file mode 100644 index 0000000..b5a7951 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/result_ext.rs @@ -0,0 +1,6 @@ +use test_crate::*; + +result_unwrap_or_abort!(one, two); +result_expect_or_abort!(one, two); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/result_ext.stderr b/patches/proc-macro-error2/tests/ui/result_ext.stderr new file mode 100644 index 0000000..f9321f9 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/result_ext.stderr @@ -0,0 +1,11 @@ +error: Result::unwrap_or_abort() test + --> tests/ui/result_ext.rs:3:25 + | +3 | result_unwrap_or_abort!(one, two); + | ^^^ + +error: BOOM: Result::expect_or_abort() test + --> tests/ui/result_ext.rs:4:25 + | +4 | result_expect_or_abort!(one, two); + | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/to_tokens_span.rs b/patches/proc-macro-error2/tests/ui/to_tokens_span.rs new file mode 100644 index 0000000..942e94a --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/to_tokens_span.rs @@ -0,0 +1,5 @@ +use test_crate::*; + +to_tokens_span!(std::option::Option); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr b/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr new file mode 100644 index 0000000..bb3b543 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr @@ -0,0 +1,11 @@ +error: whole type + --> tests/ui/to_tokens_span.rs:3:17 + | +3 | to_tokens_span!(std::option::Option); + | ^^^^^^^^^^^^^^^^^^^ + +error: explicit .span() + --> tests/ui/to_tokens_span.rs:3:17 + | +3 | to_tokens_span!(std::option::Option); + | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/unknown_setting.rs b/patches/proc-macro-error2/tests/ui/unknown_setting.rs new file mode 100644 index 0000000..5e54a89 --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/unknown_setting.rs @@ -0,0 +1,4 @@ +use proc_macro_error2::proc_macro_error; + +#[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/unknown_setting.stderr b/patches/proc-macro-error2/tests/ui/unknown_setting.stderr new file mode 100644 index 0000000..eb349cc --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/unknown_setting.stderr @@ -0,0 +1,5 @@ +error: unknown setting `trololo`, expected one of `assert_unwind_safe`, `allow_not_macro`, `proc_macro_hack` + --> tests/ui/unknown_setting.rs:3:57 + | +3 | #[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] + | ^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/unrelated_panic.rs b/patches/proc-macro-error2/tests/ui/unrelated_panic.rs new file mode 100644 index 0000000..9d450ff --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/unrelated_panic.rs @@ -0,0 +1,5 @@ +use test_crate::*; + +unrelated_panic!(); + +fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr b/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr new file mode 100644 index 0000000..a1dec9c --- /dev/null +++ b/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr @@ -0,0 +1,7 @@ +error: proc macro panicked + --> tests/ui/unrelated_panic.rs:3:1 + | +3 | unrelated_panic!(); + | ^^^^^^^^^^^^^^^^^^ + | + = help: message: unrelated panic test From d39950a9325844c21d1b402143f9ff529771a3b2 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 22 May 2026 18:23:22 +0800 Subject: [PATCH 50/85] refactor: consolidate ddns crates --- Cargo.toml | 100 +++++++++++++++++- README.md | 52 ++++----- ddns-core/Cargo.toml | 23 ---- ddns-server/Cargo.toml | 43 -------- ddns/Cargo.toml | 60 ----------- {ddns/examples => examples}/README.md | 8 +- {ddns/examples => examples}/mdns_discover.rs | 0 {ddns/examples => examples}/mdns_query.rs | 0 {ddns/examples => examples}/publish.rs | 0 {ddns/examples => examples}/query.rs | 0 gmdns/Cargo.toml | 21 ---- ddns-server/server.toml => server.toml | 0 .../src => src/bin/ddns-server}/config.rs | 0 .../src => src/bin/ddns-server}/error.rs | 0 .../src => src/bin/ddns-server}/lookup.rs | 0 .../src => src/bin/ddns-server}/main.rs | 0 .../src => src/bin/ddns-server}/policy.rs | 0 .../src => src/bin/ddns-server}/publish.rs | 0 .../src => src/bin/ddns-server}/storage.rs | 0 ddns-core/src/lib.rs => src/core.rs | 0 {ddns-core/src => src/core}/parser.rs | 0 {ddns-core/src => src/core}/parser/header.rs | 0 {ddns-core/src => src/core}/parser/name.rs | 0 {ddns-core/src => src/core}/parser/packet.rs | 0 .../src => src/core}/parser/question.rs | 0 {ddns-core/src => src/core}/parser/record.rs | 0 .../core}/parser/record/endpoint.rs | 0 .../src => src/core}/parser/record/ptr.rs | 0 .../src => src/core}/parser/record/srv.rs | 0 .../src => src/core}/parser/record/txt.rs | 0 {ddns-core/src => src/core}/parser/sigin.rs | 0 {ddns-core/src => src/core}/parser/varint.rs | 0 {ddns-core/src => src/core}/wire.rs | 0 {ddns/src => src}/lib.rs | 7 +- gmdns/src/lib.rs => src/mdns.rs | 6 +- {gmdns/src => src/mdns}/if_nametoindex.rs | 0 {gmdns/src => src/mdns}/protocol.rs | 12 +-- {gmdns/src => src/mdns}/resolvers.rs | 2 +- {gmdns/src => src/mdns}/resolvers/mdns.rs | 42 ++++---- gmdns/src/mdns.rs => src/mdns/service.rs | 4 +- {ddns/src => src}/publisher.rs | 19 ++-- {ddns/src => src}/resolvers.rs | 9 +- {ddns/src => src}/resolvers/h3.rs | 7 +- {ddns/src => src}/resolvers/http.rs | 5 +- 44 files changed, 184 insertions(+), 236 deletions(-) delete mode 100644 ddns-core/Cargo.toml delete mode 100644 ddns-server/Cargo.toml delete mode 100644 ddns/Cargo.toml rename {ddns/examples => examples}/README.md (95%) rename {ddns/examples => examples}/mdns_discover.rs (100%) rename {ddns/examples => examples}/mdns_query.rs (100%) rename {ddns/examples => examples}/publish.rs (100%) rename {ddns/examples => examples}/query.rs (100%) delete mode 100644 gmdns/Cargo.toml rename ddns-server/server.toml => server.toml (100%) rename {ddns-server/src => src/bin/ddns-server}/config.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/error.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/lookup.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/main.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/policy.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/publish.rs (100%) rename {ddns-server/src => src/bin/ddns-server}/storage.rs (100%) rename ddns-core/src/lib.rs => src/core.rs (100%) rename {ddns-core/src => src/core}/parser.rs (100%) rename {ddns-core/src => src/core}/parser/header.rs (100%) rename {ddns-core/src => src/core}/parser/name.rs (100%) rename {ddns-core/src => src/core}/parser/packet.rs (100%) rename {ddns-core/src => src/core}/parser/question.rs (100%) rename {ddns-core/src => src/core}/parser/record.rs (100%) rename {ddns-core/src => src/core}/parser/record/endpoint.rs (100%) rename {ddns-core/src => src/core}/parser/record/ptr.rs (100%) rename {ddns-core/src => src/core}/parser/record/srv.rs (100%) rename {ddns-core/src => src/core}/parser/record/txt.rs (100%) rename {ddns-core/src => src/core}/parser/sigin.rs (100%) rename {ddns-core/src => src/core}/parser/varint.rs (100%) rename {ddns-core/src => src/core}/wire.rs (100%) rename {ddns/src => src}/lib.rs (82%) rename gmdns/src/lib.rs => src/mdns.rs (66%) rename {gmdns/src => src/mdns}/if_nametoindex.rs (100%) rename {gmdns/src => src/mdns}/protocol.rs (99%) rename {gmdns/src => src/mdns}/resolvers.rs (71%) rename {gmdns/src => src/mdns}/resolvers/mdns.rs (92%) rename gmdns/src/mdns.rs => src/mdns/service.rs (98%) rename {ddns/src => src}/publisher.rs (98%) rename {ddns/src => src}/resolvers.rs (98%) rename {ddns/src => src}/resolvers/h3.rs (98%) rename {ddns/src => src}/resolvers/http.rs (98%) diff --git a/Cargo.toml b/Cargo.toml index 48645b1..4755bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,100 @@ -[workspace] -members = ["ddns-core", "gmdns", "ddns", "ddns-server"] -resolver = "2" - -[workspace.package] +[package] +name = "ddns" version = "0.2.0" edition = "2024" +autoexamples = false + +[dependencies] +base64 = "0.22" +bitfield-struct = "0.10" +bytes = "1" +dashmap = "6" +dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } +dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +flume = "0.12" +futures = "0.3" +libc = "0.2" +nom = "8" +rand = "0.9" +ring = "0.17" +rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } +rustls-pemfile = "2" +snafu = "0.8" +socket2 = { version = "0.5.8", features = ["all"] } +tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread", "io-util"] } +tracing = "0.1" +x509-parser = "0.18" + +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, optional = true } +http = { version = "1", optional = true } +http-body = { version = "1", optional = true } +http-body-util = { version = "0.1", optional = true } +reqwest = { version = "0.12", default-features = false, features = ["charset", "rustls-tls", "http2", "macos-system-configuration", "json"], optional = true } +url = { version = "2", optional = true } + +clap = { version = "4", features = ["derive"], optional = true } +deadpool-redis = { version = "0.23", optional = true } +idna = { version = "1", optional = true } +serde = { version = "1", features = ["derive"], optional = true } +toml = { version = "0.8", optional = true } +tower-service = { version = "0.3", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } + +[features] +default = [] +h3x-resolver = [ + "dep:h3x", + "h3x/dquic", + "h3x/hyper", + "dep:http", + "dep:http-body", + "dep:http-body-util", + "dep:url", +] +mdns-resolver = ["dep:h3x", "h3x/dquic"] +http-resolver = ["dep:reqwest"] +server = [ + "h3x-resolver", + "dep:clap", + "dep:deadpool-redis", + "dep:idna", + "dep:serde", + "dep:toml", + "dep:tower-service", + "dep:tracing-subscriber", +] + +[dev-dependencies] +clap = { version = "4", features = ["derive"] } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"] } +idna = "1" +rustls-pki-types = "1" +shellexpand = "3" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[[bin]] +name = "ddns-server" +path = "src/bin/ddns-server/main.rs" +required-features = ["server"] + +[[example]] +name = "mdns_discover" +path = "examples/mdns_discover.rs" + +[[example]] +name = "mdns_query" +path = "examples/mdns_query.rs" + +[[example]] +name = "publish" +path = "examples/publish.rs" +required-features = ["h3x-resolver"] + +[[example]] +name = "query" +path = "examples/query.rs" +required-features = ["h3x-resolver"] [patch.crates-io] proc-macro-error2 = { path = "patches/proc-macro-error2" } diff --git a/README.md b/README.md index e3eb23e..2525330 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# DDNS / GMDNS +# DDNS -This workspace provides DNS discovery crates for the DHTTP ecosystem: +This package provides DNS discovery for the DHTTP ecosystem. The old `ddns-core`, `gmdns`, `ddns`, and `ddns-server` crate boundaries are now modules and binaries inside one package named `ddns`. -| Crate | Role | +| Module / target | Role | | --- | --- | -| `ddns-core` | DNS packet parser, endpoint `E` record, and shared wire types. | -| `gmdns` | RFC 6762 multicast DNS transport and LAN resolver/publisher. | -| `ddns` | Facade crate combining `ddns-core`, `gmdns`, and optional HTTP/3/HTTP resolvers. | -| `ddns-server` | DNS-over-HTTP/3 publish/lookup server binary. | +| `ddns::core` | DNS packet parser, endpoint `E` record, and shared wire types. | +| `ddns::mdns` | RFC 6762 multicast DNS transport and LAN resolver/publisher. | +| `ddns::resolvers` | Facade resolver chain plus optional HTTP/3/HTTP resolvers. | +| `ddns-server` | DNS-over-HTTP/3 publish/lookup server binary (`server` feature). | -`gmdns` is the local multicast DNS layer. `ddns` is the high-level crate to use when an application needs both LAN mDNS and remote DNS-over-HTTP/3 resolver support. +`ddns::mdns` is the local multicast DNS layer. `ddns` is the high-level crate to use when an application needs both LAN mDNS and remote DNS-over-HTTP/3 resolver support. ## 🌟 Key Features @@ -29,13 +29,6 @@ Add to your `Cargo.toml`: ddns = { path = "./ddns" } ``` -For mDNS-only use, depend directly on `gmdns`: - -```toml -[dependencies] -gmdns = { path = "./gmdns" } -``` - For HTTP/3 resolver/publisher support, enable the `h3x-resolver` feature on `ddns`: ```toml @@ -47,7 +40,7 @@ ddns = { path = "./ddns", features = ["h3x-resolver"] } ```rust use futures::StreamExt; -use gmdns::Mdns; +use ddns::Mdns; #[tokio::main] async fn main() -> Result<(), std::io::Error> { @@ -66,7 +59,7 @@ async fn main() -> Result<(), std::io::Error> { ### HTTP/3 DNS Publishing Example ```rust -// See ddns/examples/publish.rs for a complete mTLS HTTP/3 publisher. +// See examples/publish.rs for a complete mTLS HTTP/3 publisher. ``` --- @@ -80,7 +73,7 @@ async fn main() -> Result<(), std::io::Error> { Publish DNS service records to an HTTP/3 DNS server: ```bash -cargo run -p ddns --example publish --features="h3x-resolver" \ +cargo run --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --client-name demo.example.genmeta.net \ --client-cert /path/to/demo.example.genmeta.net.pem \ @@ -94,7 +87,7 @@ cargo run -p ddns --example publish --features="h3x-resolver" \ Query DNS service records from an HTTP/3 DNS server: ```bash -cargo run -p ddns --example query --features="h3x-resolver" \ +cargo run --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --host nat.genmeta.net ``` @@ -104,10 +97,10 @@ cargo run -p ddns --example query --features="h3x-resolver" \ Start an HTTP/3 DNS server: ```bash -cargo run -p ddns-server -- --config ddns-server/server.toml +cargo run --bin ddns-server --features="server" -- --config server.toml ``` -For detailed parameters and HTTP packet structures, see [ddns/examples/README.md](ddns/examples/README.md). +For detailed parameters and HTTP packet structures, see [examples/README.md](examples/README.md). --- @@ -197,11 +190,12 @@ When signature is present: `Scheme (u16)` + `Length (VarInt)` + `Data (N bytes)` ## 🛠 Project Structure -- `ddns-core/src/parser/`: Core protocol parsing implementation (Nom parsers). -- `ddns-core/src/wire.rs`: Shared HTTP multi-record response wire format. -- `gmdns/src/protocol.rs`: UDP multicast and packet routing logic. -- `gmdns/src/mdns.rs`: High-level mDNS discovery and response API. -- `gmdns/src/resolvers/`: LAN mDNS resolver implementation. -- `ddns/src/resolvers/`: Facade resolver chain plus optional HTTP/3 and HTTP resolvers. -- `ddns/examples/`: mDNS discovery/query and HTTP/3 publish/query examples. -- `ddns-server/`: DNS-over-HTTP/3 server binary and configuration. +- `src/core/parser/`: Core protocol parsing implementation (Nom parsers). +- `src/core/wire.rs`: Shared HTTP multi-record response wire format. +- `src/mdns/protocol.rs`: UDP multicast and packet routing logic. +- `src/mdns/service.rs`: High-level mDNS discovery and response API. +- `src/mdns/resolvers/`: LAN mDNS resolver implementation. +- `src/resolvers/`: Facade resolver chain plus optional HTTP/3 and HTTP resolvers. +- `examples/`: mDNS discovery/query and HTTP/3 publish/query examples. +- `src/bin/ddns-server/`: DNS-over-HTTP/3 server binary. +- `server.toml`: Example server configuration. diff --git a/ddns-core/Cargo.toml b/ddns-core/Cargo.toml deleted file mode 100644 index 45f7b14..0000000 --- a/ddns-core/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "ddns-core" -version.workspace = true -edition.workspace = true - -[dependencies] -base64 = "0.22" -bitfield-struct = "0.10" -bytes = "1" -dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } -dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } -nom = "8" -rand = "0.9" -ring = "0.17" -rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } -rustls-pemfile = "2" -snafu = "0.8" -tracing = "0.1" -x509-parser = "0.18" - -[dev-dependencies] -futures = "0.3" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/ddns-server/Cargo.toml b/ddns-server/Cargo.toml deleted file mode 100644 index e5f6087..0000000 --- a/ddns-server/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "ddns-server" -version = "0.2.0" -edition = "2024" - -[[bin]] -name = "ddns-server" -path = "src/main.rs" - -[dependencies] -ddns = { path = "../ddns", features = ["h3x-resolver"] } -dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", features = [ - "dquic", - "hyper", -] } - -# server-specific deps(不再污染核心库) -deadpool-redis = "0.23" -serde = { version = "1", features = ["derive"] } -toml = "0.8" -dashmap = "6" -bytes = "1" -base64 = "0.22" -clap = { version = "4", features = ["derive"] } -http = "1" -http-body-util = "0.1" -tower-service = "0.3" -nom = "8" -rustls = { version = "0.23", default-features = false, features = [ - "logging", - "ring", -] } -rustls-pemfile = "2" -idna = "1" -x509-parser = "0.18" -snafu = "0.8" -tokio = { version = "1", features = ["full"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -url = "2" -futures = "0.3" -ring = "0.17" diff --git a/ddns/Cargo.toml b/ddns/Cargo.toml deleted file mode 100644 index c7d7f9c..0000000 --- a/ddns/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "ddns" -version.workspace = true -edition.workspace = true -autoexamples = false - -[dependencies] -ddns-core = { path = "../ddns-core" } -dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } -nom = "8" -dashmap = "6" -bytes = "1" -dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } -futures = "0.3" -gmdns = { path = "../gmdns" } -rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } -snafu = "0.8" -tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } -tracing = "0.1" - -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic", "hyper"], optional = true } -http = { version = "1", optional = true } -http-body = { version = "1", optional = true } -http-body-util = { version = "0.1", optional = true } -reqwest = { version = "0.12", default-features = false, features = ["charset", "rustls-tls", "http2", "macos-system-configuration", "json"], optional = true } -url = { version = "2", optional = true } - -[features] -default = [] -h3x-resolver = ["dep:h3x", "dep:http", "dep:http-body", "dep:http-body-util", "dep:url"] -mdns-resolver = ["dep:h3x", "gmdns/h3x-network"] -http-resolver = ["dep:reqwest"] - -[dev-dependencies] -clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"] } -idna = "1" -rustls-pemfile = "2" -rustls-pki-types = "1" -shellexpand = "3" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -[[example]] -name = "mdns_discover" -path = "examples/mdns_discover.rs" - -[[example]] -name = "mdns_query" -path = "examples/mdns_query.rs" - -[[example]] -name = "publish" -path = "examples/publish.rs" -required-features = ["h3x-resolver"] - -[[example]] -name = "query" -path = "examples/query.rs" -required-features = ["h3x-resolver"] diff --git a/ddns/examples/README.md b/examples/README.md similarity index 95% rename from ddns/examples/README.md rename to examples/README.md index f4370b8..9df3937 100644 --- a/ddns/examples/README.md +++ b/examples/README.md @@ -9,7 +9,7 @@ First, ensure you have a Rust environment. Clone or enter the project directory, then build: ```bash -cargo build -p ddns --features="h3x-resolver" +cargo build --features="h3x-resolver" ``` Note: The example programs require the `h3x-resolver` feature to enable HTTP/3 support. @@ -55,7 +55,7 @@ Use the `publish` example to publish a DNS service record to the HTTP/3 DNS serv #### Example Run Command ```bash -cargo run -p ddns --example publish --features="h3x-resolver" \ +cargo run --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --client-name demo.example.genmeta.net \ --client-cert /path/to/demo.example.genmeta.net.pem \ @@ -77,7 +77,7 @@ Use the `query` example to query DNS service records from the HTTP/3 DNS server. #### Example Run Command ```bash -cargo run -p ddns --example query --features="h3x-resolver" \ +cargo run --example query --features="h3x-resolver" \ --server-ca /path/to/root.crt \ --host nat.genmeta.net ``` @@ -93,7 +93,7 @@ Use the `ddns-server` binary to start an HTTP/3 DNS server. #### Example Run Command ```bash -cargo run -p ddns-server -- --config ddns-server/server.toml +cargo run --bin ddns-server --features="server" -- --config server.toml ``` After the server starts, it listens for HTTP/3 requests and handles publish and query operations. diff --git a/ddns/examples/mdns_discover.rs b/examples/mdns_discover.rs similarity index 100% rename from ddns/examples/mdns_discover.rs rename to examples/mdns_discover.rs diff --git a/ddns/examples/mdns_query.rs b/examples/mdns_query.rs similarity index 100% rename from ddns/examples/mdns_query.rs rename to examples/mdns_query.rs diff --git a/ddns/examples/publish.rs b/examples/publish.rs similarity index 100% rename from ddns/examples/publish.rs rename to examples/publish.rs diff --git a/ddns/examples/query.rs b/examples/query.rs similarity index 100% rename from ddns/examples/query.rs rename to examples/query.rs diff --git a/gmdns/Cargo.toml b/gmdns/Cargo.toml deleted file mode 100644 index 3a116ce..0000000 --- a/gmdns/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "gmdns" -version.workspace = true -edition.workspace = true - -[dependencies] -dashmap = "6" -ddns-core = { path = "../ddns-core" } -dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } -flume = "0.12" -futures = "0.3" -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"], optional = true } -libc = "0.2" -snafu = "0.8" -socket2 = { version = "0.5.8", features = ["all"] } -tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread"] } -tracing = "0.1" - -[features] -default = [] -h3x-network = ["dep:h3x"] diff --git a/ddns-server/server.toml b/server.toml similarity index 100% rename from ddns-server/server.toml rename to server.toml diff --git a/ddns-server/src/config.rs b/src/bin/ddns-server/config.rs similarity index 100% rename from ddns-server/src/config.rs rename to src/bin/ddns-server/config.rs diff --git a/ddns-server/src/error.rs b/src/bin/ddns-server/error.rs similarity index 100% rename from ddns-server/src/error.rs rename to src/bin/ddns-server/error.rs diff --git a/ddns-server/src/lookup.rs b/src/bin/ddns-server/lookup.rs similarity index 100% rename from ddns-server/src/lookup.rs rename to src/bin/ddns-server/lookup.rs diff --git a/ddns-server/src/main.rs b/src/bin/ddns-server/main.rs similarity index 100% rename from ddns-server/src/main.rs rename to src/bin/ddns-server/main.rs diff --git a/ddns-server/src/policy.rs b/src/bin/ddns-server/policy.rs similarity index 100% rename from ddns-server/src/policy.rs rename to src/bin/ddns-server/policy.rs diff --git a/ddns-server/src/publish.rs b/src/bin/ddns-server/publish.rs similarity index 100% rename from ddns-server/src/publish.rs rename to src/bin/ddns-server/publish.rs diff --git a/ddns-server/src/storage.rs b/src/bin/ddns-server/storage.rs similarity index 100% rename from ddns-server/src/storage.rs rename to src/bin/ddns-server/storage.rs diff --git a/ddns-core/src/lib.rs b/src/core.rs similarity index 100% rename from ddns-core/src/lib.rs rename to src/core.rs diff --git a/ddns-core/src/parser.rs b/src/core/parser.rs similarity index 100% rename from ddns-core/src/parser.rs rename to src/core/parser.rs diff --git a/ddns-core/src/parser/header.rs b/src/core/parser/header.rs similarity index 100% rename from ddns-core/src/parser/header.rs rename to src/core/parser/header.rs diff --git a/ddns-core/src/parser/name.rs b/src/core/parser/name.rs similarity index 100% rename from ddns-core/src/parser/name.rs rename to src/core/parser/name.rs diff --git a/ddns-core/src/parser/packet.rs b/src/core/parser/packet.rs similarity index 100% rename from ddns-core/src/parser/packet.rs rename to src/core/parser/packet.rs diff --git a/ddns-core/src/parser/question.rs b/src/core/parser/question.rs similarity index 100% rename from ddns-core/src/parser/question.rs rename to src/core/parser/question.rs diff --git a/ddns-core/src/parser/record.rs b/src/core/parser/record.rs similarity index 100% rename from ddns-core/src/parser/record.rs rename to src/core/parser/record.rs diff --git a/ddns-core/src/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs similarity index 100% rename from ddns-core/src/parser/record/endpoint.rs rename to src/core/parser/record/endpoint.rs diff --git a/ddns-core/src/parser/record/ptr.rs b/src/core/parser/record/ptr.rs similarity index 100% rename from ddns-core/src/parser/record/ptr.rs rename to src/core/parser/record/ptr.rs diff --git a/ddns-core/src/parser/record/srv.rs b/src/core/parser/record/srv.rs similarity index 100% rename from ddns-core/src/parser/record/srv.rs rename to src/core/parser/record/srv.rs diff --git a/ddns-core/src/parser/record/txt.rs b/src/core/parser/record/txt.rs similarity index 100% rename from ddns-core/src/parser/record/txt.rs rename to src/core/parser/record/txt.rs diff --git a/ddns-core/src/parser/sigin.rs b/src/core/parser/sigin.rs similarity index 100% rename from ddns-core/src/parser/sigin.rs rename to src/core/parser/sigin.rs diff --git a/ddns-core/src/parser/varint.rs b/src/core/parser/varint.rs similarity index 100% rename from ddns-core/src/parser/varint.rs rename to src/core/parser/varint.rs diff --git a/ddns-core/src/wire.rs b/src/core/wire.rs similarity index 100% rename from ddns-core/src/wire.rs rename to src/core/wire.rs diff --git a/ddns/src/lib.rs b/src/lib.rs similarity index 82% rename from ddns/src/lib.rs rename to src/lib.rs index 5d26d87..e2e4854 100644 --- a/ddns/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ +pub mod core; +pub mod mdns; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] mod publisher; pub mod resolvers; -pub use ddns_core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; -pub use gmdns::{Mdns, MdnsResolver, mdns}; +pub use core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; + +pub use mdns::{Mdns, MdnsResolver}; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] pub use publisher::{ CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, DEFAULT_PUBLISH_TIMEOUT, PublishOnceError, diff --git a/gmdns/src/lib.rs b/src/mdns.rs similarity index 66% rename from gmdns/src/lib.rs rename to src/mdns.rs index 91bc5f3..a5e4fe7 100644 --- a/gmdns/src/lib.rs +++ b/src/mdns.rs @@ -1,9 +1,9 @@ mod if_nametoindex; -pub mod mdns; mod protocol; pub mod resolvers; +mod service; -pub use mdns::Mdns; pub use resolvers::MdnsResolver; -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] pub use resolvers::{MdnsBindDriver, MdnsResolvers}; +pub use service::Mdns; diff --git a/gmdns/src/if_nametoindex.rs b/src/mdns/if_nametoindex.rs similarity index 100% rename from gmdns/src/if_nametoindex.rs rename to src/mdns/if_nametoindex.rs diff --git a/gmdns/src/protocol.rs b/src/mdns/protocol.rs similarity index 99% rename from gmdns/src/protocol.rs rename to src/mdns/protocol.rs index 252352c..3b15fc2 100644 --- a/gmdns/src/protocol.rs +++ b/src/mdns/protocol.rs @@ -8,16 +8,16 @@ use std::{ }; use dashmap::DashMap; -use ddns_core::parser::{ - packet::{Packet, be_packet}, - record::endpoint::EndpointAddr, -}; use futures::{Stream, StreamExt}; use snafu::Snafu; use socket2::{Domain, Socket, Type}; use tokio::{io, net::UdpSocket, task::JoinSet, time}; -use crate::if_nametoindex::if_nametoindex; +use super::if_nametoindex::if_nametoindex; +use crate::core::parser::{ + packet::{Packet, be_packet}, + record::endpoint::EndpointAddr, +}; #[derive(Debug)] pub struct MdnsSocket { @@ -332,7 +332,7 @@ impl MdnsProtocol { if let Ok(Some((source, packet))) = time::timeout(Duration::from_millis(300), packets.next()).await { - use ddns_core::parser::record::RData::*; + use crate::core::parser::record::RData::*; let endpoints = packet .answers .iter() diff --git a/gmdns/src/resolvers.rs b/src/mdns/resolvers.rs similarity index 71% rename from gmdns/src/resolvers.rs rename to src/mdns/resolvers.rs index 574de2b..bd1d379 100644 --- a/gmdns/src/resolvers.rs +++ b/src/mdns/resolvers.rs @@ -1,5 +1,5 @@ mod mdns; pub use mdns::MdnsResolver; -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] pub use mdns::{MdnsBindDriver, MdnsResolvers}; diff --git a/gmdns/src/resolvers/mdns.rs b/src/mdns/resolvers/mdns.rs similarity index 92% rename from gmdns/src/resolvers/mdns.rs rename to src/mdns/resolvers/mdns.rs index 7a6f7a2..42750bf 100644 --- a/gmdns/src/resolvers/mdns.rs +++ b/src/mdns/resolvers/mdns.rs @@ -1,23 +1,23 @@ use std::{fmt, io, net::IpAddr}; -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] use std::{net::SocketAddr, sync::Arc}; -#[cfg(feature = "h3x-network")] -use ddns_core::parser::packet::Packet; -use ddns_core::parser::record::RData; -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] use dquic::qresolve::RecordStream; use dquic::{ qbase::net::{Family, addr::EndpointAddr as DquicEndpointAddr}, qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}, }; use futures::{FutureExt, StreamExt, TryFutureExt, future, stream}; -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] use futures::{Stream, stream::FuturesUnordered}; +#[cfg(feature = "mdns-resolver")] +use super::super::protocol::MdnsProtocol; +#[cfg(feature = "mdns-resolver")] +use crate::core::parser::packet::Packet; +use crate::core::parser::record::RData; pub use crate::mdns::Mdns as MdnsResolver; -#[cfg(feature = "h3x-network")] -use crate::protocol::MdnsProtocol; impl MdnsResolver { pub fn source(&self) -> Source { @@ -63,8 +63,8 @@ impl Resolve for MdnsResolver { } } -fn endpoints_from_packet(packet: &[u8]) -> io::Result> { - use ddns_core::parser::packet::be_packet; +fn endpoints_from_packet(packet: &[u8]) -> io::Result> { + use crate::core::parser::packet::be_packet; be_packet(packet) .map(|(_, pkt)| { @@ -79,14 +79,14 @@ fn endpoints_from_packet(packet: &[u8]) -> io::Result, null_io_factory: Arc, service_name: Arc, } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl MdnsBindDriver { pub fn new(service_name: impl Into>) -> Self { Self { @@ -112,7 +112,7 @@ impl MdnsBindDriver { }; bind_iface.with_components_mut(|components, _iface| { - match components.try_init_with(|| crate::Mdns::new(&self.service_name, ip, device)) { + match components.try_init_with(|| crate::mdns::Mdns::new(&self.service_name, ip, device)) { Ok(mdns) => mdns.reinit_on(device, ip), Err(error) => { let report = snafu::Report::from_error(&error); @@ -123,7 +123,7 @@ impl MdnsBindDriver { } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl h3x::dquic::BindDriver for MdnsBindDriver { fn bind<'a>( &'a self, @@ -153,7 +153,7 @@ impl h3x::dquic::BindDriver for MdnsBindDriver { } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] pub struct MdnsResolvers { network: Arc, driver: Arc, @@ -161,7 +161,7 @@ pub struct MdnsResolvers { _handles: Vec, } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] #[derive(Debug, Clone)] pub struct BoundMdnsResolver { pub device: String, @@ -169,7 +169,7 @@ pub struct BoundMdnsResolver { pub resolver: MdnsResolver, } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl fmt::Debug for MdnsResolvers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MdnsResolvers") @@ -178,14 +178,14 @@ impl fmt::Debug for MdnsResolvers { } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl fmt::Display for MdnsResolvers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("mDNS resolvers") } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl MdnsResolvers { pub async fn bind( network: Arc, @@ -316,7 +316,7 @@ impl MdnsResolvers { } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl Publish for MdnsResolvers { fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { let endpoints = match endpoints_from_packet(packet) { @@ -332,7 +332,7 @@ impl Publish for MdnsResolvers { } } -#[cfg(feature = "h3x-network")] +#[cfg(feature = "mdns-resolver")] impl Resolve for MdnsResolvers { fn lookup<'l>(&'l self, name: &'l str) -> ResolveFuture<'l> { self.query(name).boxed() diff --git a/gmdns/src/mdns.rs b/src/mdns/service.rs similarity index 98% rename from gmdns/src/mdns.rs rename to src/mdns/service.rs index 2a73589..29f1748 100644 --- a/gmdns/src/mdns.rs +++ b/src/mdns/service.rs @@ -7,13 +7,13 @@ use std::{ time::Duration, }; -use ddns_core::parser::{packet::Packet, record::endpoint::EndpointAddr}; use dquic::qinterface::{Interface, component::Component, io::IO}; use futures::{Stream, stream}; use tokio::{task::JoinSet, time}; use tracing::Instrument; -use crate::protocol::MdnsProtocol; +use super::protocol::MdnsProtocol; +use crate::core::parser::{packet::Packet, record::endpoint::EndpointAddr}; #[derive(Clone)] pub struct Mdns { diff --git a/ddns/src/publisher.rs b/src/publisher.rs similarity index 98% rename from ddns/src/publisher.rs rename to src/publisher.rs index b5faa0d..5c60406 100644 --- a/ddns/src/publisher.rs +++ b/src/publisher.rs @@ -9,10 +9,6 @@ use std::{ time::Duration, }; -use ddns_core::{ - MdnsPacket, - parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, -}; use dhttp_identity::identity::LocalAgent; #[cfg(feature = "mdns-resolver")] use dquic::qbase::net::Family; @@ -24,7 +20,13 @@ use dquic::{ }; use snafu::{ResultExt, Snafu}; -use crate::resolvers::Resolvers; +use crate::{ + core::{ + MdnsPacket, + parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, + }, + resolvers::Resolvers, +}; pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// Upper bound for a single publish attempt in the background loop. @@ -299,6 +301,9 @@ impl Publisher { resolver: &(dyn Resolve + Send + Sync), public_endpoints: &[EndpointAddr], ) -> Result { + #[cfg(not(any(feature = "http-resolver", feature = "h3x-resolver")))] + let _ = public_endpoints; + let any: &dyn Any = resolver; #[cfg(feature = "http-resolver")] @@ -616,9 +621,9 @@ mod tests { let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); let packet = publisher.signed_packet(&[endpoint]).await.unwrap(); - let (_remain, packet) = ddns_core::parser::packet::be_packet(&packet).unwrap(); + let (_remain, packet) = crate::core::parser::packet::be_packet(&packet).unwrap(); let record = packet.answers.first().expect("endpoint answer"); - let ddns_core::parser::record::RData::E(endpoint) = record.data() else { + let crate::core::parser::record::RData::E(endpoint) = record.data() else { panic!("expected endpoint record"); }; diff --git a/ddns/src/resolvers.rs b/src/resolvers.rs similarity index 98% rename from ddns/src/resolvers.rs rename to src/resolvers.rs index 5f84ffa..4349a8c 100644 --- a/ddns/src/resolvers.rs +++ b/src/resolvers.rs @@ -83,14 +83,15 @@ impl std::str::FromStr for DnsScheme { } } -pub use gmdns::resolvers::MdnsResolver; -#[cfg(feature = "mdns-resolver")] -pub use gmdns::resolvers::MdnsResolvers; #[cfg(feature = "h3x-resolver")] pub use h3::{H3Publisher, H3Resolver}; #[cfg(feature = "http-resolver")] pub use http::HttpResolver; +pub use crate::mdns::resolvers::MdnsResolver; +#[cfg(feature = "mdns-resolver")] +pub use crate::mdns::resolvers::MdnsResolvers; + type ArcResolver = Arc; #[derive(Default, Clone, Debug)] @@ -371,6 +372,6 @@ mod tests { .expect("bound interfaces"); assert!(!ifaces.is_empty()); assert!(ifaces[0].borrow().bound_addr().is_err()); - assert!(ifaces[0].with_components(|components, _| components.exist::())); + assert!(ifaces[0].with_components(|components, _| components.exist::())); } } diff --git a/ddns/src/resolvers/h3.rs b/src/resolvers/h3.rs similarity index 98% rename from ddns/src/resolvers/h3.rs rename to src/resolvers/h3.rs index 41b6443..5dd565e 100644 --- a/ddns/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -1,7 +1,6 @@ use std::{convert::Infallible, fmt, io, sync::Arc, time::Duration}; use dashmap::DashMap; -use ddns_core::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; use dquic::{ qbase::net::addr::EndpointAddr, qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture, Source}, @@ -16,6 +15,8 @@ use tokio::time::Instant; use tracing::trace; use url::Url; +use crate::core::{MdnsPacket, parser::packet::be_packet, wire::be_multi_response}; + const LOOKUP_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); const LOOKUP_REQUEST_ATTEMPTS: usize = 3; @@ -176,7 +177,7 @@ where let endpoints = endpoints .iter() .filter_map(|ep| { - ddns_core::parser::record::endpoint::EndpointAddr::try_from(*ep).ok() + crate::core::parser::record::endpoint::EndpointAddr::try_from(*ep).ok() }) .collect(); let mut hosts = std::collections::HashMap::new(); @@ -282,7 +283,7 @@ where pub const EXCLUDED_DOMAINS: [&str; 2] = ["dns.genmeta.net", "download.genmeta.net"]; pub async fn lookup(&self, name: &str) -> Result> { - use ddns_core::parser::record; + use crate::core::parser::record; let server = Arc::from(self.base_url.origin().ascii_serialization()); let source = Source::H3 { server }; diff --git a/ddns/src/resolvers/http.rs b/src/resolvers/http.rs similarity index 98% rename from ddns/src/resolvers/http.rs rename to src/resolvers/http.rs index 5d98bef..6e15aeb 100644 --- a/ddns/src/resolvers/http.rs +++ b/src/resolvers/http.rs @@ -5,7 +5,6 @@ use std::{ }; use dashmap::DashMap; -use ddns_core::parser::packet::be_packet; use dquic::{ qbase::net::addr::EndpointAddr, qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}, @@ -14,6 +13,8 @@ use futures::{StreamExt, TryFutureExt, stream}; use reqwest::{Client, IntoUrl, StatusCode, Url}; use tokio::time::Instant; +use crate::core::parser::packet::be_packet; + #[derive(Debug)] struct Record { addrs: Vec, @@ -126,7 +127,7 @@ impl Resolve for HttpResolver { let server = Arc::from(self.base_url.host_str().unwrap_or("")); let soource = Source::Http { server }; - use ddns_core::parser::record; + use crate::core::parser::record; self.cached_records .retain(|_host, Record { expire, .. }| *expire < now); if let Some(record) = self.cached_records.get(domain) { From 0ff552e40f0e106f6cb3e2c7bc041f9ec4a22ceb Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 25 May 2026 16:21:13 +0800 Subject: [PATCH 51/85] Inject ddns bootstrap constants at build time --- README.md | 2 +- build.rs | 30 ++++++++++++++++++++++++++++++ examples/README.md | 4 ++-- examples/mdns_discover.rs | 4 +--- examples/mdns_query.rs | 4 +--- examples/publish.rs | 6 +++++- examples/query.rs | 6 +++++- src/bootstrap.rs | 1 + src/lib.rs | 2 ++ src/resolvers.rs | 27 +++++++++++++++++++++------ src/resolvers/h3.rs | 4 ++-- 11 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 build.rs create mode 100644 src/bootstrap.rs diff --git a/README.md b/README.md index 2525330..a9ef92e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ use ddns::Mdns; #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Create mDNS instance - let mdns = Mdns::new("_genmeta.local", "127.0.0.1".parse().unwrap(), "lo0")?; + let mdns = Mdns::new(ddns::DHTTP_MDNS_SERVICE, "127.0.0.1".parse().unwrap(), "lo0")?; // Listen to discovery stream let mut stream = mdns.discover(); diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..80d5895 --- /dev/null +++ b/build.rs @@ -0,0 +1,30 @@ +use std::{env, fs, path::PathBuf}; + +const H3_DNS_SERVER_ENV: &str = "DHTTP_H3_DNS_SERVER"; +const HTTP_DNS_SERVER_ENV: &str = "DHTTP_HTTP_DNS_SERVER"; +const MDNS_SERVICE_ENV: &str = "DHTTP_MDNS_SERVICE"; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); + + let h3_dns_server = required_env(H3_DNS_SERVER_ENV); + let http_dns_server = required_env(HTTP_DNS_SERVER_ENV); + let mdns_service = required_env(MDNS_SERVICE_ENV); + + let bootstrap = format!( + "// @generated by build.rs; do not edit.\n\ + pub const DHTTP_H3_DNS_SERVER: &str = {h3_dns_server:?};\n\ + pub const DHTTP_HTTP_DNS_SERVER: &str = {http_dns_server:?};\n\ + pub const DHTTP_MDNS_SERVICE: &str = {mdns_service:?};\n" + ); + fs::write(out_dir.join("bootstrap.rs"), bootstrap) + .expect("failed to write generated DHTTP DNS bootstrap constants"); + + println!("cargo::rerun-if-env-changed={H3_DNS_SERVER_ENV}"); + println!("cargo::rerun-if-env-changed={HTTP_DNS_SERVER_ENV}"); + println!("cargo::rerun-if-env-changed={MDNS_SERVICE_ENV}"); +} + +fn required_env(name: &str) -> String { + env::var(name).unwrap_or_else(|_| panic!("missing {name}; set it before building ddns")) +} diff --git a/examples/README.md b/examples/README.md index 9df3937..36e0b0c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -43,7 +43,7 @@ Note: The example programs require the `h3x-resolver` feature to enable HTTP/3 s Use the `publish` example to publish a DNS service record to the HTTP/3 DNS server. #### Program Parameters -- `--base-url `: Base URL of the DNS server (default: `https://dns.genmeta.net:4433/`). +- `--base-url `: Base URL of the DNS server (default: build-time `DHTTP_H3_DNS_SERVER` with a trailing slash). - `--server-ca `: CA certificate PEM file path for verifying the online server certificate. - `--client-name `: Client identity name used for mTLS. - `--client-cert `: Client certificate chain PEM file. @@ -71,7 +71,7 @@ This command establishes an HTTP/3 connection to the server, sends a POST reques Use the `query` example to query DNS service records from the HTTP/3 DNS server. #### Program Parameters -- `--base-url `: Base URL of the DNS server (default: `https://dns.genmeta.net:4433/`). +- `--base-url `: Base URL of the DNS server (default: build-time `DHTTP_H3_DNS_SERVER` with a trailing slash). - `--server-ca `: CA certificate PEM file path for verifying the online server certificate. - `--host `: DNS name to query (default: `nat.genmeta.net`). diff --git a/examples/mdns_discover.rs b/examples/mdns_discover.rs index 659124b..c1f3fe9 100644 --- a/examples/mdns_discover.rs +++ b/examples/mdns_discover.rs @@ -6,8 +6,6 @@ use std::{ use clap::Parser; use futures::StreamExt; -const SERVICE_NAME: &str = "_genmeta.local"; - #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { @@ -21,7 +19,7 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = ddns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; + let mdns = ddns::Mdns::new(ddns::DHTTP_MDNS_SERVICE, args.ip, &args.device)?; mdns.insert_host( "test.genmeta.net".to_string(), vec![ diff --git a/examples/mdns_query.rs b/examples/mdns_query.rs index 5da374b..ecc5d9a 100644 --- a/examples/mdns_query.rs +++ b/examples/mdns_query.rs @@ -2,8 +2,6 @@ use std::{io::Error, net::IpAddr}; use clap::Parser; -const SERVICE_NAME: &str = "_genmeta.local"; - #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { @@ -17,7 +15,7 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = ddns::Mdns::new(SERVICE_NAME, args.ip, &args.device)?; + let mdns = ddns::Mdns::new(ddns::DHTTP_MDNS_SERVICE, args.ip, &args.device)?; let ret = mdns.query("publish.test.genmeta.net".to_string()).await?; println!("{ret:?}\n"); diff --git a/examples/publish.rs b/examples/publish.rs index 5f206cb..ef2d8ce 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -20,7 +20,7 @@ use tracing::{Level, info}; #[command(version, about, long_about = None)] struct Options { /// Base URL of the线上 H3 DNS server. - #[arg(long, default_value = "https://dns.genmeta.net:4433/")] + #[arg(long, default_value_t = default_h3_base_url())] base_url: String, /// 用于校验线上服务端证书的 CA PEM 文件。 @@ -61,6 +61,10 @@ struct Options { sequence: u64, } +fn default_h3_base_url() -> String { + format!("{}/", ddns::DHTTP_H3_DNS_SERVER.trim_end_matches('/')) +} + fn load_root_store_from_pem(path: &Path) -> io::Result { let pem = std::fs::read(path)?; diff --git a/examples/query.rs b/examples/query.rs index ddf9b78..6ba80e7 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -22,7 +22,7 @@ use tracing::{Level, info}; #[command(version, about, long_about = None)] struct Options { /// Base URL of the线上 HTTP/3 DNS server. - #[arg(long, default_value = "https://dns.genmeta.net:4433/")] + #[arg(long, default_value_t = default_h3_base_url())] base_url: String, /// 用于校验线上服务端证书的 CA PEM 文件。 @@ -34,6 +34,10 @@ struct Options { host: String, } +fn default_h3_base_url() -> String { + format!("{}/", ddns::DHTTP_H3_DNS_SERVER.trim_end_matches('/')) +} + fn load_root_store_from_pem(path: &Path) -> io::Result { let pem = std::fs::read(path)?; let mut store = RootCertStore::empty(); diff --git a/src/bootstrap.rs b/src/bootstrap.rs new file mode 100644 index 0000000..aa8c097 --- /dev/null +++ b/src/bootstrap.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/bootstrap.rs")); diff --git a/src/lib.rs b/src/lib.rs index e2e4854..0c1caf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +mod bootstrap; + pub mod core; pub mod mdns; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] diff --git a/src/resolvers.rs b/src/resolvers.rs index 4349a8c..a309a3b 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -34,13 +34,13 @@ pub(crate) fn resolvable_name(name: &str) -> Option<&str> { } /// Default DNS-over-H3 server for DHTTP endpoints. -pub const DHTTP_H3_DNS_SERVER: &str = "https://dns.genmeta.net:4433"; +pub const DHTTP_H3_DNS_SERVER: &str = crate::bootstrap::DHTTP_H3_DNS_SERVER; /// Default DNS-over-HTTP server for DHTTP endpoints. -pub const DHTTP_HTTP_DNS_SERVER: &str = "https://dns.genmeta.net"; +pub const DHTTP_HTTP_DNS_SERVER: &str = crate::bootstrap::DHTTP_HTTP_DNS_SERVER; /// mDNS service type used by DHTTP endpoints. -pub const DHTTP_MDNS_SERVICE: &str = "_genmeta.local"; +pub const DHTTP_MDNS_SERVICE: &str = crate::bootstrap::DHTTP_MDNS_SERVICE; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum DnsScheme { @@ -263,15 +263,30 @@ impl Resolve for Resolvers { mod tests { use std::str::FromStr; + #[cfg(feature = "mdns-resolver")] + use super::MdnsResolvers; #[cfg(any( feature = "h3x-resolver", feature = "http-resolver", feature = "mdns-resolver" ))] use super::Resolvers; - #[cfg(feature = "mdns-resolver")] - use super::{DHTTP_MDNS_SERVICE, MdnsResolvers}; - use super::{DnsScheme, resolvable_name}; + use super::{ + DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsScheme, resolvable_name, + }; + + #[test] + fn resolver_defaults_come_from_compile_time_environment() { + if let Some(expected) = option_env!("DHTTP_H3_DNS_SERVER") { + assert_eq!(DHTTP_H3_DNS_SERVER, expected); + } + if let Some(expected) = option_env!("DHTTP_HTTP_DNS_SERVER") { + assert_eq!(DHTTP_HTTP_DNS_SERVER, expected); + } + if let Some(expected) = option_env!("DHTTP_MDNS_SERVICE") { + assert_eq!(DHTTP_MDNS_SERVICE, expected); + } + } #[test] fn resolvable_name_accepts_dns_name_with_numeric_port() { diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 5dd565e..c26c59e 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -428,7 +428,7 @@ mod tests { let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( h3x::dquic::QuicEndpoint::builder().build().await, )); - let resolver = H3Resolver::from_endpoint("https://dns.genmeta.net:4433", endpoint).unwrap(); + let resolver = H3Resolver::from_endpoint(crate::DHTTP_H3_DNS_SERVER, endpoint).unwrap(); resolver.cached_records.insert( "car.lab.genmeta.net".to_owned(), Record { @@ -443,7 +443,7 @@ mod tests { assert_eq!( source, Source::H3 { - server: Arc::from("https://dns.genmeta.net:4433") + server: Arc::from(crate::DHTTP_H3_DNS_SERVER) } ); assert_eq!( From 1068c477612a8b8d5418d408b4197344ef8dad07 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 28 May 2026 12:42:09 +0800 Subject: [PATCH 52/85] chore: use https git dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4755bc4..1653c42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ base64 = "0.22" bitfield-struct = "0.10" bytes = "1" dashmap = "6" -dhttp-identity = { git = "ssh://git@github.com/genmeta/dhttp.git", branch = "main" } -dquic = { git = "ssh://git@github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "main" } +dquic = { git = "https://github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } flume = "0.12" futures = "0.3" libc = "0.2" From 52684ebd0bc3b56a67d5eaa7fb5eec1dc0b5e875 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 29 May 2026 17:59:20 +0800 Subject: [PATCH 53/85] refactor: nest ddns public API exports --- examples/mdns_discover.rs | 13 +++++++------ examples/mdns_query.rs | 3 ++- examples/publish.rs | 9 ++++++--- examples/query.rs | 9 ++++++--- src/bin/ddns-server/lookup.rs | 2 +- src/bin/ddns-server/main.rs | 2 +- src/bin/ddns-server/policy.rs | 6 +++--- src/bin/ddns-server/publish.rs | 2 +- src/core.rs | 6 ++---- src/core/parser.rs | 2 -- src/core/parser/packet.rs | 15 ++++++++------- src/core/parser/record/endpoint.rs | 2 +- src/core/parser/record/ptr.rs | 2 +- src/core/parser/record/srv.rs | 4 ++-- src/lib.rs | 21 +-------------------- src/mdns.rs | 7 +------ src/mdns/resolvers.rs | 6 +----- src/mdns/resolvers/mdns.rs | 4 ++-- src/publisher.rs | 16 ++++++++-------- src/resolvers.rs | 25 +++++++++++++------------ src/resolvers/h3.rs | 6 ++++-- 21 files changed, 71 insertions(+), 91 deletions(-) diff --git a/examples/mdns_discover.rs b/examples/mdns_discover.rs index c1f3fe9..beb3771 100644 --- a/examples/mdns_discover.rs +++ b/examples/mdns_discover.rs @@ -4,6 +4,7 @@ use std::{ }; use clap::Parser; +use ddns::{core::MdnsEndpoint, mdns::service::Mdns, resolvers::DHTTP_MDNS_SERVICE}; use futures::StreamExt; #[derive(Parser, Debug)] @@ -19,14 +20,14 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = ddns::Mdns::new(ddns::DHTTP_MDNS_SERVICE, args.ip, &args.device)?; + let mdns = Mdns::new(DHTTP_MDNS_SERVICE, args.ip, &args.device)?; mdns.insert_host( "test.genmeta.net".to_string(), vec![ { let addr: SocketAddr = "192.168.1.7:7000".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - ddns::MdnsEndpoint::direct_v4(v4) + MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -34,7 +35,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.13:7000".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - ddns::MdnsEndpoint::direct_v4(v4) + MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -48,7 +49,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - ddns::MdnsEndpoint::direct_v4(v4) + MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -56,7 +57,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - ddns::MdnsEndpoint::direct_v4(v4) + MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } @@ -64,7 +65,7 @@ async fn main() -> Result<(), Error> { { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); if let SocketAddr::V4(v4) = addr { - ddns::MdnsEndpoint::direct_v4(v4) + MdnsEndpoint::direct_v4(v4) } else { panic!("Expected IPv4 address"); } diff --git a/examples/mdns_query.rs b/examples/mdns_query.rs index ecc5d9a..ac8c839 100644 --- a/examples/mdns_query.rs +++ b/examples/mdns_query.rs @@ -1,6 +1,7 @@ use std::{io::Error, net::IpAddr}; use clap::Parser; +use ddns::{mdns::service::Mdns, resolvers::DHTTP_MDNS_SERVICE}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -15,7 +16,7 @@ struct Args { async fn main() -> Result<(), Error> { tracing_subscriber::fmt::init(); let args = Args::parse(); - let mdns = ddns::Mdns::new(ddns::DHTTP_MDNS_SERVICE, args.ip, &args.device)?; + let mdns = Mdns::new(DHTTP_MDNS_SERVICE, args.ip, &args.device)?; let ret = mdns.query("publish.test.genmeta.net".to_string()).await?; println!("{ret:?}\n"); diff --git a/examples/publish.rs b/examples/publish.rs index ef2d8ce..a4ee662 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -6,7 +6,10 @@ use std::{ }; use clap::Parser; -use ddns::{parser::record::endpoint::EndpointAddr, resolvers::H3Publisher}; +use ddns::{ + core::parser::record::endpoint::EndpointAddr, + resolvers::{DHTTP_H3_DNS_SERVER, h3::H3Publisher}, +}; use h3x::dquic::{ Identity, Network, QuicEndpoint, cert::handy::{ToCertificate, ToPrivateKey}, @@ -62,7 +65,7 @@ struct Options { } fn default_h3_base_url() -> String { - format!("{}/", ddns::DHTTP_H3_DNS_SERVER.trim_end_matches('/')) + format!("{}/", DHTTP_H3_DNS_SERVER.trim_end_matches('/')) } fn load_root_store_from_pem(path: &Path) -> io::Result { @@ -167,7 +170,7 @@ async fn main() -> io::Result<()> { info!("Publishing endpoint: {:?}", endpoint); let mut hosts = std::collections::HashMap::new(); hosts.insert(opt.host.clone(), vec![endpoint]); - let packet = ddns::MdnsPacket::answer(0, &hosts).to_bytes(); + let packet = ddns::core::MdnsPacket::answer(0, &hosts).to_bytes(); resolver .publish(&opt.host, &packet) .await diff --git a/examples/query.rs b/examples/query.rs index 6ba80e7..945bdec 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -5,7 +5,10 @@ use std::{ }; use clap::Parser; -use ddns::{MdnsPacket, parser::record::RData, wire::be_multi_response}; +use ddns::{ + core::{MdnsPacket, parser::record::RData, wire::be_multi_response}, + resolvers::DHTTP_H3_DNS_SERVER, +}; use h3x::{ dquic::{ Network, QuicEndpoint, @@ -35,7 +38,7 @@ struct Options { } fn default_h3_base_url() -> String { - format!("{}/", ddns::DHTTP_H3_DNS_SERVER.trim_end_matches('/')) + format!("{}/", DHTTP_H3_DNS_SERVER.trim_end_matches('/')) } fn load_root_store_from_pem(path: &Path) -> io::Result { @@ -163,7 +166,7 @@ async fn main() -> Result<(), Box> { None => println!("Source fingerprint: (no certificate)"), } - match ddns::parser::packet::be_packet(&record.dns) { + match ddns::core::parser::packet::be_packet(&record.dns) { Ok((_, packet)) => { print!("{}", format_packet(&packet)); diff --git a/src/bin/ddns-server/lookup.rs b/src/bin/ddns-server/lookup.rs index bde14b9..afce2b0 100644 --- a/src/bin/ddns-server/lookup.rs +++ b/src/bin/ddns-server/lookup.rs @@ -4,7 +4,7 @@ use std::{ net::SocketAddr, }; -use ddns::{ +use ddns::core::{ MdnsPacket, parser::{packet::be_packet, record::RData}, wire::MultiResponse, diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index d12b489..1b67c27 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -15,7 +15,7 @@ use std::{ }; use clap::Parser; -use ddns::{MdnsEndpoint, MdnsPacket}; +use ddns::core::{MdnsEndpoint, MdnsPacket}; use futures::future::BoxFuture; use h3x::{ dquic::{ diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index 40b5d5d..edbd428 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -1,4 +1,4 @@ -use ddns::parser::{packet::be_packet, record::RData}; +use ddns::core::parser::{packet::be_packet, record::RData}; use dhttp_identity::identity::RemoteAgent; use tracing::{debug, warn}; @@ -160,7 +160,7 @@ pub fn validate_dns_packet( mod tests { use std::collections::HashMap; - use ddns::MdnsPacket; + use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; use dhttp_identity::identity::RemoteAgent; use rustls::pki_types::CertificateDer; @@ -181,7 +181,7 @@ mod tests { #[test] fn validate_dns_packet_accepts_empty_packet_as_clear_operation() { - let hosts: HashMap> = + let hosts: HashMap> = HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); diff --git a/src/bin/ddns-server/publish.rs b/src/bin/ddns-server/publish.rs index 4a1bb35..7e02370 100644 --- a/src/bin/ddns-server/publish.rs +++ b/src/bin/ddns-server/publish.rs @@ -318,7 +318,7 @@ mod tests { sync::Arc, }; - use ddns::{MdnsPacket, parser::record::endpoint::EndpointAddr}; + use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; use dhttp_identity::identity::RemoteAgent; use rustls::pki_types::CertificateDer; diff --git a/src/core.rs b/src/core.rs index a43ac4b..308bbb2 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,7 +1,5 @@ pub mod parser; pub mod wire; -pub type MdnsEndpoint = crate::parser::record::endpoint::EndpointAddr; -pub type MdnsPacket = crate::parser::packet::Packet; - -pub use parser::record::endpoint::sign_endponit_address; +pub type MdnsEndpoint = parser::record::endpoint::EndpointAddr; +pub type MdnsPacket = parser::packet::Packet; diff --git a/src/core/parser.rs b/src/core/parser.rs index 9fa4f6a..847dca7 100644 --- a/src/core/parser.rs +++ b/src/core/parser.rs @@ -5,5 +5,3 @@ pub mod question; pub mod record; pub mod sigin; pub mod varint; - -pub use name::{NameCompression, put_name}; diff --git a/src/core/parser/packet.rs b/src/core/parser/packet.rs index 3ed5063..fb7bc9b 100644 --- a/src/core/parser/packet.rs +++ b/src/core/parser/packet.rs @@ -4,14 +4,15 @@ use bytes::BufMut; use super::{ header::{Header, be_header}, - question::{QueryClass, QueryType, Question, be_question}, - record::{Class, RData, ResourceRecord, endpoint::EndpointAddr}, -}; -use crate::parser::{ - header::WriteHeader, name::{NameCompression, put_name}, - record::{Type, be_record, endpoint::WriteEndpointAddr, srv::Srv}, + question::{QueryClass, QueryType, Question, be_question}, + record::{ + Class, RData, ResourceRecord, Type, be_record, + endpoint::{EndpointAddr, WriteEndpointAddr}, + srv::Srv, + }, }; +use crate::core::parser::header::WriteHeader; /// Parsed DNS packet #[derive(Default, Clone)] @@ -283,7 +284,7 @@ mod test { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use super::*; - use crate::parser::{ + use crate::core::parser::{ self, question::{QueryClass, QueryType}, record::{Class, RData, Type, srv::Srv}, diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index e9d59ab..65481bb 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -19,7 +19,7 @@ use nom::{ use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer}; use snafu::{ResultExt, Snafu}; -use crate::parser::{ +use crate::core::parser::{ sigin, varint::{VarInt, WriteVarInt, be_varint}, }; diff --git a/src/core/parser/record/ptr.rs b/src/core/parser/record/ptr.rs index ac30188..7a24bbb 100644 --- a/src/core/parser/record/ptr.rs +++ b/src/core/parser/record/ptr.rs @@ -1,4 +1,4 @@ -use crate::parser::name::{Name, be_name}; +use crate::core::parser::name::{Name, be_name}; #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Ptr(Name); diff --git a/src/core/parser/record/srv.rs b/src/core/parser/record/srv.rs index 98e2d78..60d0608 100644 --- a/src/core/parser/record/srv.rs +++ b/src/core/parser/record/srv.rs @@ -1,6 +1,6 @@ use nom::number::streaming::be_u16; -use crate::parser::name::{Name, be_name}; +use crate::core::parser::name::{Name, be_name}; #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Srv { @@ -56,7 +56,7 @@ pub fn be_srv<'a>(input: &'a [u8], origin: &'a [u8]) -> nom::IResult<&'a [u8], S #[cfg(test)] mod test { use super::*; - use crate::parser::{ + use crate::core::parser::{ packet::be_packet, question::{QueryClass, QueryType}, record::{Class, RData}, diff --git a/src/lib.rs b/src/lib.rs index 0c1caf2..193112c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,24 +3,5 @@ mod bootstrap; pub mod core; pub mod mdns; #[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] -mod publisher; +pub mod publisher; pub mod resolvers; - -pub use core::{MdnsEndpoint, MdnsPacket, parser, sign_endponit_address, wire}; - -pub use mdns::{Mdns, MdnsResolver}; -#[cfg(any(feature = "h3x-resolver", feature = "mdns-resolver"))] -pub use publisher::{ - CreatePublisherError, DEFAULT_PUBLISH_INTERVAL, DEFAULT_PUBLISH_TIMEOUT, PublishOnceError, - PublishOptions, Publisher, -}; -#[cfg(feature = "http-resolver")] -pub use resolvers::HttpResolver; -#[cfg(feature = "mdns-resolver")] -pub use resolvers::MdnsResolvers; -pub use resolvers::{ - DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsErrors, DnsScheme, - ParseDnsSchemeError, Resolvers, ResolversBuilder, -}; -#[cfg(feature = "h3x-resolver")] -pub use resolvers::{H3Publisher, H3Resolver}; diff --git a/src/mdns.rs b/src/mdns.rs index a5e4fe7..dd51460 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -1,9 +1,4 @@ mod if_nametoindex; mod protocol; pub mod resolvers; -mod service; - -pub use resolvers::MdnsResolver; -#[cfg(feature = "mdns-resolver")] -pub use resolvers::{MdnsBindDriver, MdnsResolvers}; -pub use service::Mdns; +pub mod service; diff --git a/src/mdns/resolvers.rs b/src/mdns/resolvers.rs index bd1d379..1bd416e 100644 --- a/src/mdns/resolvers.rs +++ b/src/mdns/resolvers.rs @@ -1,5 +1 @@ -mod mdns; - -pub use mdns::MdnsResolver; -#[cfg(feature = "mdns-resolver")] -pub use mdns::{MdnsBindDriver, MdnsResolvers}; +pub mod mdns; diff --git a/src/mdns/resolvers/mdns.rs b/src/mdns/resolvers/mdns.rs index 42750bf..72f6010 100644 --- a/src/mdns/resolvers/mdns.rs +++ b/src/mdns/resolvers/mdns.rs @@ -17,7 +17,7 @@ use super::super::protocol::MdnsProtocol; #[cfg(feature = "mdns-resolver")] use crate::core::parser::packet::Packet; use crate::core::parser::record::RData; -pub use crate::mdns::Mdns as MdnsResolver; +pub type MdnsResolver = crate::mdns::service::Mdns; impl MdnsResolver { pub fn source(&self) -> Source { @@ -112,7 +112,7 @@ impl MdnsBindDriver { }; bind_iface.with_components_mut(|components, _iface| { - match components.try_init_with(|| crate::mdns::Mdns::new(&self.service_name, ip, device)) { + match components.try_init_with(|| crate::mdns::service::Mdns::new(&self.service_name, ip, device)) { Ok(mdns) => mdns.reinit_on(device, ip), Err(error) => { let report = snafu::Report::from_error(&error); diff --git a/src/publisher.rs b/src/publisher.rs index 5c60406..75841b6 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -290,7 +290,7 @@ impl Publisher { #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { h3.clear_pool(); } @@ -307,21 +307,21 @@ impl Publisher { let any: &dyn Any = resolver; #[cfg(feature = "http-resolver")] - if let Some(http) = any.downcast_ref::() { + if let Some(http) = any.downcast_ref::() { self.publish_endpoints(http, public_endpoints).await?; return Ok(true); } #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { self.publish_endpoints(h3, public_endpoints).await?; return Ok(true); } #[cfg(feature = "mdns-resolver")] - if let Some(mdns) = any.downcast_ref::() { + if let Some(mdns) = any.downcast_ref::() { let mut published = false; for bound in mdns.bound_resolvers() { let endpoints = self.local_endpoints_for(&bound.device, bound.family); @@ -735,7 +735,7 @@ mod tests { }); let resolver = Arc::new( - crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); let mut publisher = Publisher::new( @@ -825,7 +825,7 @@ mod tests { }); let resolver = Arc::new( - crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); let publisher = Publisher::new( @@ -905,7 +905,7 @@ mod tests { }); let resolver = Arc::new( - crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); let mut publisher = Publisher::new( @@ -989,7 +989,7 @@ mod tests { }); let resolver = Arc::new( - crate::resolvers::HttpResolver::new(format!("http://127.0.0.1:{port}/")) + crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); let mut publisher = Publisher::new( diff --git a/src/resolvers.rs b/src/resolvers.rs index a309a3b..b565216 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -13,9 +13,16 @@ use snafu::Report; use tokio::io; #[cfg(feature = "h3x-resolver")] -mod h3; +pub mod h3; #[cfg(feature = "http-resolver")] -mod http; +pub mod http; + +#[cfg(feature = "mdns-resolver")] +use crate::mdns::resolvers::mdns::MdnsResolvers; +#[cfg(feature = "h3x-resolver")] +use h3::H3Resolver; +#[cfg(feature = "http-resolver")] +use http::HttpResolver; /// Extract and validate the DNS host from `name`, which may include a `:port` /// suffix. Returns `Some(host)` if the host part is a valid RFC-compliant DNS @@ -83,15 +90,6 @@ impl std::str::FromStr for DnsScheme { } } -#[cfg(feature = "h3x-resolver")] -pub use h3::{H3Publisher, H3Resolver}; -#[cfg(feature = "http-resolver")] -pub use http::HttpResolver; - -pub use crate::mdns::resolvers::MdnsResolver; -#[cfg(feature = "mdns-resolver")] -pub use crate::mdns::resolvers::MdnsResolvers; - type ArcResolver = Arc; #[derive(Default, Clone, Debug)] @@ -387,6 +385,9 @@ mod tests { .expect("bound interfaces"); assert!(!ifaces.is_empty()); assert!(ifaces[0].borrow().bound_addr().is_err()); - assert!(ifaces[0].with_components(|components, _| components.exist::())); + assert!( + ifaces[0] + .with_components(|components, _| components.exist::()) + ); } } diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index c26c59e..156415f 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -411,6 +411,8 @@ where #[cfg(test)] mod tests { + use crate::resolvers::DHTTP_H3_DNS_SERVER; + use super::*; #[test] @@ -428,7 +430,7 @@ mod tests { let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( h3x::dquic::QuicEndpoint::builder().build().await, )); - let resolver = H3Resolver::from_endpoint(crate::DHTTP_H3_DNS_SERVER, endpoint).unwrap(); + let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint).unwrap(); resolver.cached_records.insert( "car.lab.genmeta.net".to_owned(), Record { @@ -443,7 +445,7 @@ mod tests { assert_eq!( source, Source::H3 { - server: Arc::from(crate::DHTTP_H3_DNS_SERVER) + server: Arc::from(DHTTP_H3_DNS_SERVER) } ); assert_eq!( From 3ed6b92365922efb0a35ab5c8b1c240f8a46b326 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 31 May 2026 16:51:48 +0800 Subject: [PATCH 54/85] ci: prepare ddns crates.io release --- .github/workflows/release-crates.yml | 53 ++++++++++++++++++++++++++++ Cargo.toml | 6 ++++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/release-crates.yml diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml new file mode 100644 index 0000000..7f6ed67 --- /dev/null +++ b/.github/workflows/release-crates.yml @@ -0,0 +1,53 @@ +name: Release crates.io + +on: + pull_request: + workflow_dispatch: + push: + branches: + - main + tags: + - 'v*' + +env: + CARGO_TERM_COLOR: always + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust stable toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Test crate + run: cargo test --all-features --all-targets + + - name: Authenticate to crates.io + if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + uses: rust-lang/crates-io-auth-action@v1 + id: auth + + - name: Release ddns crate + shell: bash + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: | + set -euo pipefail + + if [[ "${GITHUB_REF_TYPE}" == "tag" && "${GITHUB_REF_NAME}" == v* ]]; then + mode=publish + else + mode=dry-run + fi + + if [[ "$mode" == "dry-run" ]]; then + cargo publish --dry-run + else + cargo publish + fi \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1653c42..01688e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "ddns" +description = "DNS discovery and resolver support for DHTTP applications" version = "0.2.0" edition = "2024" +license = "Apache-2.0" +repository = "https://github.com/genmeta/ddns" +readme = "README.md" +keywords = ["dhttp", "dns", "mdns", "http3", "quic"] +categories = ["network-programming", "asynchronous"] autoexamples = false [dependencies] From fb7e1db5213b5fb783d333ab9f45865148fb2183 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 31 May 2026 17:05:59 +0800 Subject: [PATCH 55/85] refactor: migrate endpoint signing to authorities --- README.md | 2 +- .../plans/2026-05-19-ddns-publisher.md | 22 ++++---- .../specs/2026-05-19-ddns-publisher-design.md | 10 ++-- examples/publish.rs | 2 +- src/bin/ddns-server/main.rs | 2 +- src/bin/ddns-server/policy.rs | 26 +++++----- src/bin/ddns-server/publish.rs | 50 +++++++++---------- src/core/parser/record/endpoint.rs | 30 +++++------ src/publisher.rs | 30 +++++------ 9 files changed, 88 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index a9ef92e..6085e8e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ use ddns::Mdns; #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Create mDNS instance - let mdns = Mdns::new(ddns::DHTTP_MDNS_SERVICE, "127.0.0.1".parse().unwrap(), "lo0")?; + let mdns = Mdns::new(DHTTP_MDNS_SERVICE, "127.0.0.1".parse().unwrap(), "lo0")?; // Listen to discovery stream let mut stream = mdns.discover(); diff --git a/docs/superpowers/plans/2026-05-19-ddns-publisher.md b/docs/superpowers/plans/2026-05-19-ddns-publisher.md index b585eb0..61408e4 100644 --- a/docs/superpowers/plans/2026-05-19-ddns-publisher.md +++ b/docs/superpowers/plans/2026-05-19-ddns-publisher.md @@ -4,35 +4,35 @@ **Goal:** Add signed DNS publishing for DHTTP endpoints using async identity agents and concrete ddns publishers. -**Architecture:** `dhttp-identity` owns async agent traits and signature helpers. `ddns-core` signs endpoint records through `LocalAgent`. `ddns` owns `Publisher`, discovers concrete publishers by downcasting, and publishes signed packets. `dhttp::Endpoint` provides the convenience constructor. +**Architecture:** `dhttp-identity` owns async authority traits and signature helpers. `ddns-core` signs endpoint records through `LocalAuthority`. `ddns` owns `Publisher`, discovers concrete publishers by downcasting, and publishes signed packets. `dhttp::Endpoint` provides the convenience constructor. **Tech Stack:** Rust 2024, snafu, futures BoxFuture, dhttp-identity, ddns-core, ddns, h3x/dquic resolver traits. --- -### Task 1: Move async agent traits into dhttp-identity +### Task 1: Move async authority traits into dhttp-identity **Files:** - Modify: `dhttp/identity/src/identity.rs` - Modify: `dhttp/identity/src/lib.rs` -- Delete: `h3x/src/quic/agent.rs` -- Modify: h3x call sites to import `dhttp_identity::identity::{LocalAgent, RemoteAgent, SignError, VerifyError}` directly +- Delete: `h3x/src/quic/authority.rs` +- Modify: h3x call sites to import `dhttp_identity::identity::{LocalAuthority, RemoteAuthority, SignError, VerifyError}` directly -- [ ] Add tests in `dhttp/identity/src/identity.rs` for `Identity` implementing async `LocalAgent` and sync-default `RemoteAgent` verification behavior. -- [ ] Move `LocalAgent`, `RemoteAgent`, `extract_public_key`, `verify_signature`, and `sign_with_key` equivalents into `dhttp-identity` while preserving async signatures. -- [ ] Delete `h3x::quic::agent`; downstream crates import the identity agent API from `dhttp_identity::identity` directly. +- [ ] Add tests in `dhttp/identity/src/identity.rs` for `Identity` implementing async `LocalAuthority` and sync-default `RemoteAuthority` verification behavior. +- [ ] Move `LocalAuthority`, `RemoteAuthority`, `extract_public_key`, `verify_signature`, and `sign_with_key` equivalents into `dhttp-identity` while preserving async signatures. +- [ ] Delete `h3x::quic::authority`; downstream crates import the identity authority API from `dhttp_identity::identity` directly. - [ ] Run `cargo test -p dhttp-identity` and `cargo test --features dquic` in h3x. -### Task 2: Replace ddns-core SigningKey signing with LocalAgent signing +### Task 2: Replace ddns-core SigningKey signing with LocalAuthority signing **Files:** - Modify: `ddns/ddns-core/Cargo.toml` - Modify: `ddns/ddns-core/src/parser/record/endpoint.rs` - Modify: `ddns/ddns-core/src/parser/sigin.rs` -- [ ] Add a failing async test for signing an `EndpointAddr` through a fake `LocalAgent` that rejects the first preferred compatible scheme and accepts the next one. -- [ ] Add `EndpointAddr::sign_with_agent(&mut self, agent: &(impl LocalAgent + ?Sized)) -> impl Future>`. -- [ ] Keep old low-level `ddns_core::parser::sigin::sign_with_key(SigningKey, SignatureScheme, data)` helper, delete only the `EndpointAddr::sign_with(SigningKey, SignatureScheme)` convenience method, and update tests/examples to use the async agent method where endpoint records are signed. +- [ ] Add a failing async test for signing an `EndpointAddr` through a fake `LocalAuthority` that rejects the first preferred compatible scheme and accepts the next one. +- [ ] Add `EndpointAddr::sign_with_authority(&mut self, authority: &(impl LocalAuthority + ?Sized)) -> impl Future>`. +- [ ] Keep old low-level `ddns_core::parser::sigin::sign_with_key(SigningKey, SignatureScheme, data)` helper, delete only the `EndpointAddr::sign_with(SigningKey, SignatureScheme)` convenience method, and update tests/examples to use the async authority method where endpoint records are signed. - [ ] Keep verification logic unchanged except for imports. - [ ] Run `cargo test -p ddns-core`. diff --git a/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md b/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md index 474eef1..9c3724a 100644 --- a/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md +++ b/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md @@ -6,12 +6,12 @@ Add a reusable DNS publisher for DHTTP endpoints. The publisher signs endpoint r ## Decisions -- `LocalAgent` and `RemoteAgent` are identity-layer concepts. Move their async trait definitions to `dhttp-identity`; `Identity` implements them without adding generics to `Identity`. -- `LocalAgent` remains an async signing API. It is an async/remote-capable counterpart of `rustls::sign::SigningKey`, not a replacement with synchronous signing. +- `LocalAuthority` and `RemoteAuthority` are identity-layer concepts. Move their async trait definitions to `dhttp-identity`; `Identity` implements them without adding generics to `Identity`. +- `LocalAuthority` remains an async signing API. It is an async/remote-capable counterpart of `rustls::sign::SigningKey`, not a replacement with synchronous signing. - DNS publisher code lives in the `ddns` crate. `dhttp::Endpoint` only exposes a convenience method that constructs a `ddns::Publisher` from endpoint state. - `Endpoint::publisher()` returns `Result` because anonymous endpoints cannot publish signed DNS records. `Publisher` stores a non-optional identity. -- `EndpointAddr::sign_with(SigningKey, scheme)` is removed. Endpoint record signing uses `dhttp_identity::LocalAgent`. -- Signature scheme selection follows the existing `pick_signature_scheme` preference order: Ed25519, ECDSA P-256, ECDSA P-384, RSA-PSS SHA-256/384/512, RSA-PKCS1 SHA-256/384/512. The async agent API is not expanded with `choose_scheme`; signing tries compatible schemes and treats `UnsupportedScheme` as a cue to try the next candidate. +- `EndpointAddr::sign_with(SigningKey, scheme)` is removed. Endpoint record signing uses `dhttp_identity::LocalAuthority`. +- Signature scheme selection follows the existing `pick_signature_scheme` preference order: Ed25519, ECDSA P-256, ECDSA P-384, RSA-PSS SHA-256/384/512, RSA-PKCS1 SHA-256/384/512. The async authority API is not expanded with `choose_scheme`; signing tries compatible schemes and treats `UnsupportedScheme` as a cue to try the next candidate. - `publish_once` returns an error for the first failed publish attempt. `run` publishes every 20 seconds, logs warnings on failures with `snafu::Report`, and does not retain failure state. - `NoPublisherResolver` is built at publish time when no concrete publisher can be found by downcasting. - There is no `Resolvers::publish`; publishing is resolver-specific and uses `Any` downcasting. @@ -31,4 +31,4 @@ Errors are typed with `snafu`. Display messages are lower-case fragments and do ## Testing -Unit tests cover agent-based endpoint signing, signature scheme fallback, anonymous endpoint publisher construction, missing publisher reporting, and mDNS address scoping helpers. Workspace tests and clippy run before commits. +Unit tests cover authority-based endpoint signing, signature scheme fallback, anonymous endpoint publisher construction, missing publisher reporting, and mDNS address scoping helpers. Workspace tests and clippy run before commits. diff --git a/examples/publish.rs b/examples/publish.rs index a4ee662..2f572ef 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -163,7 +163,7 @@ async fn main() -> io::Result<()> { if opt.sign { info!("signing endpoint"); endpoint - .sign_with_agent(identity.as_ref()) + .sign_with_authority(identity.as_ref()) .await .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index 1b67c27..a9fbda8 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -253,7 +253,7 @@ async fn main() -> Result<(), Box> { .await; let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - server.serve_owned(router).await?; + server.listen_owned(router).await?; Ok(()) } diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index edbd428..6fb5451 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -1,5 +1,5 @@ use ddns::core::parser::{packet::be_packet, record::RData}; -use dhttp_identity::identity::RemoteAgent; +use dhttp_identity::identity::RemoteAuthority; use tracing::{debug, warn}; use crate::error::{AppError, normalize_host}; @@ -65,10 +65,10 @@ pub enum ValidatedDnsPacket { // Certificate helpers // --------------------------------------------------------------------------- -pub fn extract_client_dns_sans(agent: &(impl RemoteAgent + ?Sized)) -> Vec { +pub fn extract_client_dns_sans(authority: &(impl RemoteAuthority + ?Sized)) -> Vec { use x509_parser::prelude::*; - let Some(leaf) = agent.cert_chain().first() else { + let Some(leaf) = authority.cert_chain().first() else { return vec![]; }; @@ -87,8 +87,10 @@ pub fn extract_client_dns_sans(agent: &(impl RemoteAgent + ?Sized)) -> Vec Result { - let mut sans = extract_client_dns_sans(agent) +pub fn client_allowed_host( + authority: &(impl RemoteAuthority + ?Sized), +) -> Result { + let mut sans = extract_client_dns_sans(authority) .into_iter() .filter_map(|h| normalize_host(&h).ok()) .collect::>(); @@ -105,7 +107,7 @@ pub fn client_allowed_host(agent: &(impl RemoteAgent + ?Sized)) -> Result Result { let (remaining, dns_packet) = be_packet(packet).map_err(|e| AppError::InvalidDnsPacket { message: e.to_string(), @@ -137,7 +139,7 @@ pub fn validate_dns_packet( if let RData::E(endpoint) = record.data() && endpoint.is_signed() { - let cert = agent + let cert = authority .cert_chain() .first() .ok_or(AppError::MissingClientCertificate)?; @@ -161,17 +163,17 @@ mod tests { use std::collections::HashMap; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAgent; + use dhttp_identity::identity::RemoteAuthority; use rustls::pki_types::CertificateDer; use super::*; #[derive(Debug)] - struct TestAgent; + struct TestAuthority; - impl RemoteAgent for TestAgent { + impl RemoteAuthority for TestAuthority { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[CertificateDer<'static>] { @@ -185,7 +187,7 @@ mod tests { HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); - let validated = validate_dns_packet(&packet, true, &TestAgent).unwrap(); + let validated = validate_dns_packet(&packet, true, &TestAuthority).unwrap(); assert!(matches!(validated, ValidatedDnsPacket::Empty)); } diff --git a/src/bin/ddns-server/publish.rs b/src/bin/ddns-server/publish.rs index 7e02370..69b42e7 100644 --- a/src/bin/ddns-server/publish.rs +++ b/src/bin/ddns-server/publish.rs @@ -1,7 +1,7 @@ use std::{convert::Infallible, sync::Arc}; use deadpool_redis::redis::{self, AsyncCommands}; -use dhttp_identity::identity::RemoteAgent; +use dhttp_identity::identity::RemoteAuthority; use h3x::{connection::ConnectionState, quic}; use http_body_util::BodyExt; use tokio::time::{Duration, Instant}; @@ -54,9 +54,9 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { debug!(host = %host, "publish.host"); // Require a valid client certificate for all publish requests. - let agent = match request_connection(&request) { - Some(connection) => match connection.remote_agent().await { - Ok(Some(agent)) => agent, + let authority = match request_connection(&request) { + Some(connection) => match connection.remote_authority().await { + Ok(Some(authority)) => authority, Ok(None) => { warn!("missing client certificate"); return write_error(AppError::MissingClientCertificate); @@ -77,7 +77,7 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { // Standard policy: cert SAN must match the target host. // OpenMulti policy: any authenticated node may publish — skip SAN check. if policy == DomainPolicy::Standard { - let allowed = match client_allowed_host(agent.as_ref()) { + let allowed = match client_allowed_host(authority.as_ref()) { Ok(h) => h, Err(e) => { warn!(error = %snafu::Report::from_error(&e), "client certificate domain not allowed"); @@ -108,7 +108,7 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { require_signature = require_sig, "validating publish packet" ); - let packet = match validate_dns_packet(body.as_ref(), require_sig, agent.as_ref()) { + let packet = match validate_dns_packet(body.as_ref(), require_sig, authority.as_ref()) { Ok(n) => n, Err(e) => { debug!(host = %host, error = %e, "publish packet rejected"); @@ -127,9 +127,9 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { return write_error(AppError::HostMismatch); } - publish_record(&state, &host, &body, agent.as_ref()).await + publish_record(&state, &host, &body, authority.as_ref()).await } - ValidatedDnsPacket::Empty => clear_record(&state, &host, agent.as_ref()).await, + ValidatedDnsPacket::Empty => clear_record(&state, &host, authority.as_ref()).await, } } @@ -155,9 +155,9 @@ pub async fn publish_record( state: &AppState, host: &str, body: &bytes::Bytes, - agent: &(impl RemoteAgent + ?Sized), + authority: &(impl RemoteAuthority + ?Sized), ) -> Response { - let cert_bytes = agent + let cert_bytes = authority .cert_chain() .first() .map(|c| c.as_ref().to_vec()) @@ -258,9 +258,9 @@ pub async fn publish_record( pub async fn clear_record( state: &AppState, host: &str, - agent: &(impl RemoteAgent + ?Sized), + authority: &(impl RemoteAuthority + ?Sized), ) -> Response { - let cert_bytes = agent + let cert_bytes = authority .cert_chain() .first() .map(|c| c.as_ref().to_vec()) @@ -319,7 +319,7 @@ mod tests { }; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAgent; + use dhttp_identity::identity::RemoteAuthority; use rustls::pki_types::CertificateDer; use super::*; @@ -330,12 +330,12 @@ mod tests { }; #[derive(Debug)] - struct TestAgent { + struct TestAuthority { name: &'static str, certs: Vec>, } - impl TestAgent { + impl TestAuthority { fn new(name: &'static str, cert_bytes: Vec) -> Self { Self { name, @@ -344,7 +344,7 @@ mod tests { } } - impl RemoteAgent for TestAgent { + impl RemoteAuthority for TestAuthority { fn name(&self) -> &str { self.name } @@ -378,45 +378,45 @@ mod tests { async fn clear_record_removes_only_current_certificate_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let agent_a = TestAgent::new("agent-a", vec![1]); - let agent_b = TestAgent::new("agent-b", vec![2]); + let authority_a = TestAuthority::new("authority-a", vec![1]); + let authority_b = TestAuthority::new("authority-b", vec![2]); let packet_a = packet_for(host, 1); let packet_b = packet_for(host, 2); assert_eq!( - publish_record(&state, host, &packet_a, &agent_a) + publish_record(&state, host, &packet_a, &authority_a) .await .status(), http::StatusCode::OK ); assert_eq!( - publish_record(&state, host, &packet_b, &agent_b) + publish_record(&state, host, &packet_b, &authority_b) .await .status(), http::StatusCode::OK ); assert_eq!( - clear_record(&state, host, &agent_a).await.status(), + clear_record(&state, host, &authority_a).await.status(), http::StatusCode::OK ); let LookupResult::Multi(response) = perform_lookup(&state, host, None).await.unwrap() else { - panic!("agent b record should remain"); + panic!("authority b record should remain"); }; assert_eq!(response.records.len(), 1); - assert_eq!(response.records[0].cert, agent_b.certs[0].as_ref()); + assert_eq!(response.records[0].cert, authority_b.certs[0].as_ref()); } #[tokio::test] async fn clear_record_is_idempotent_for_missing_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let agent = TestAgent::new("agent", vec![1]); + let authority = TestAuthority::new("authority", vec![1]); assert_eq!( - clear_record(&state, host, &agent).await.status(), + clear_record(&state, host, &authority).await.status(), http::StatusCode::OK ); assert!(matches!( diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index 65481bb..4dbc9d5 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -253,14 +253,14 @@ impl EndpointAddr { } } - pub async fn sign_with_agent( + pub async fn sign_with_authority( &mut self, - agent: &(impl dhttp_identity::identity::LocalAgent + ?Sized), + authority: &(impl dhttp_identity::identity::LocalAuthority + ?Sized), ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); - for scheme in signature_schemes_for_algorithm(agent.sign_algorithm()) { - match agent.sign(scheme, &data).await { + for scheme in signature_schemes_for_algorithm(authority.sign_algorithm()) { + match authority.sign(scheme, &data).await { Ok(signature) => { self.signature = Some(EndpointSignature { scheme: u16::from(scheme), @@ -792,14 +792,14 @@ impl TryFrom for DquicEndpointAddr { pub async fn sign_endponit_address( server_id: u8, - agent: Option<&(impl dhttp_identity::identity::LocalAgent + ?Sized)>, + authority: Option<&(impl dhttp_identity::identity::LocalAuthority + ?Sized)>, endpoint: DquicEndpointAddr, ) -> Option { let mut ep: EndpointAddr = endpoint.try_into().ok()?; ep.set_main(server_id == 0); ep.set_sequence(server_id as u64); - if let Some(agent) = agent { - let _ = ep.sign_with_agent(agent).await; + if let Some(authority) = authority { + let _ = ep.sign_with_authority(authority).await; } Some(ep) } @@ -1014,9 +1014,9 @@ mod tests { } } - impl dhttp_identity::identity::LocalAgent for Ed25519Key { + impl dhttp_identity::identity::LocalAuthority for Ed25519Key { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { @@ -1052,7 +1052,7 @@ mod tests { let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); let mut ep = EndpointAddr::direct_v4(addr); ep.set_main(true); - futures::executor::block_on(ep.sign_with_agent(&key)).unwrap(); + futures::executor::block_on(ep.sign_with_authority(&key)).unwrap(); let mut buf = BytesMut::new(); buf.put_endpoint_addr(&ep); @@ -1078,13 +1078,13 @@ mod tests { } #[test] - fn sign_with_agent_tries_next_supported_scheme() { + fn sign_with_authority_tries_next_supported_scheme() { #[derive(Debug)] - struct FallbackAgent; + struct FallbackAuthority; - impl dhttp_identity::identity::LocalAgent for FallbackAgent { + impl dhttp_identity::identity::LocalAuthority for FallbackAuthority { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { @@ -1113,7 +1113,7 @@ mod tests { } let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); - futures::executor::block_on(ep.sign_with_agent(&FallbackAgent)).unwrap(); + futures::executor::block_on(ep.sign_with_authority(&FallbackAuthority)).unwrap(); let signature = ep.signature().unwrap(); assert_eq!( diff --git a/src/publisher.rs b/src/publisher.rs index 75841b6..9f3128e 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -9,7 +9,7 @@ use std::{ time::Duration, }; -use dhttp_identity::identity::LocalAgent; +use dhttp_identity::identity::LocalAuthority; #[cfg(feature = "mdns-resolver")] use dquic::qbase::net::Family; use dquic::{ @@ -73,7 +73,7 @@ pub struct PublishOptions { } pub struct Publisher { - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -96,7 +96,7 @@ impl std::fmt::Debug for Publisher { impl Publisher { pub fn new( - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -366,7 +366,7 @@ impl Publisher { endpoint.set_sequence(server_id.into()); } endpoint - .sign_with_agent(self.identity.as_ref()) + .sign_with_authority(self.identity.as_ref()) .await .context(publish_once_error::SignEndpointSnafu)?; signed.push(endpoint); @@ -537,11 +537,11 @@ mod tests { use super::*; #[derive(Debug)] - struct TestAgent; + struct TestAuthority; - impl LocalAgent for TestAgent { + impl LocalAuthority for TestAuthority { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[CertificateDer<'static>] { @@ -584,7 +584,7 @@ mod tests { #[tokio::test] async fn publish_once_reports_no_publisher_resolver() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -597,7 +597,7 @@ mod tests { #[tokio::test] async fn publisher_timeout_is_configurable() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -612,7 +612,7 @@ mod tests { #[tokio::test] async fn signed_packet_applies_publish_options_server_id() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -639,7 +639,7 @@ mod tests { "inet://127.0.0.1:0".parse().expect("valid bind pattern"); let _bind = network.quic().bind(bind_pattern.clone()).await; let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network, Arc::new(DisplayOnlyResolver), Arc::new(vec![bind_pattern]), @@ -739,7 +739,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -829,7 +829,7 @@ mod tests { .expect("valid http resolver"), ); let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -909,7 +909,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -993,7 +993,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ From 6becba5bed1a43b43c1d08f360a4ac4c7f924db8 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 31 May 2026 17:16:10 +0800 Subject: [PATCH 56/85] chore: normalize release metadata formatting --- .github/workflows/release-crates.yml | 4 ++-- Cargo.toml | 31 +++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml index 7f6ed67..ab2a4b5 100644 --- a/.github/workflows/release-crates.yml +++ b/.github/workflows/release-crates.yml @@ -7,7 +7,7 @@ on: branches: - main tags: - - 'v*' + - "v*" env: CARGO_TERM_COLOR: always @@ -50,4 +50,4 @@ jobs: cargo publish --dry-run else cargo publish - fi \ No newline at end of file + fi diff --git a/Cargo.toml b/Cargo.toml index 01688e5..5d4e8de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,22 @@ libc = "0.2" nom = "8" rand = "0.9" ring = "0.17" -rustls = { version = "0.23", default-features = false, features = ["logging", "ring"] } +rustls = { version = "0.23", default-features = false, features = [ + "logging", + "ring", +] } rustls-pemfile = "2" snafu = "0.8" socket2 = { version = "0.5.8", features = ["all"] } -tokio = { version = "1", features = ["time", "macros", "net", "sync", "rt", "rt-multi-thread", "io-util"] } +tokio = { version = "1", features = [ + "time", + "macros", + "net", + "sync", + "rt", + "rt-multi-thread", + "io-util", +] } tracing = "0.1" x509-parser = "0.18" @@ -35,7 +46,13 @@ h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } -reqwest = { version = "0.12", default-features = false, features = ["charset", "rustls-tls", "http2", "macos-system-configuration", "json"], optional = true } +reqwest = { version = "0.12", default-features = false, features = [ + "charset", + "rustls-tls", + "http2", + "macos-system-configuration", + "json", +], optional = true } url = { version = "2", optional = true } clap = { version = "4", features = ["derive"], optional = true } @@ -44,7 +61,9 @@ idna = { version = "1", optional = true } serde = { version = "1", features = ["derive"], optional = true } toml = { version = "0.8", optional = true } tower-service = { version = "0.3", optional = true } -tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", +], optional = true } [features] default = [] @@ -72,7 +91,9 @@ server = [ [dev-dependencies] clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = ["dquic"] } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = [ + "dquic", +] } idna = "1" rustls-pki-types = "1" shellexpand = "3" From 42f0ccc2b60ddb5f20661802aad72e27a3f1ae28 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 31 May 2026 20:48:40 +0800 Subject: [PATCH 57/85] fix: sync ddns with agent APIs --- src/bin/ddns-server/main.rs | 2 +- src/bin/ddns-server/policy.rs | 16 +++++++------- src/bin/ddns-server/publish.rs | 22 +++++++++---------- src/core/parser/record/endpoint.rs | 28 ++++++++++++------------ src/publisher.rs | 34 +++++++++++++++--------------- src/resolvers.rs | 4 ++-- 6 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index a9fbda8..1b67c27 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -253,7 +253,7 @@ async fn main() -> Result<(), Box> { .await; let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - server.listen_owned(router).await?; + server.serve_owned(router).await?; Ok(()) } diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index 6fb5451..4882062 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -1,5 +1,5 @@ use ddns::core::parser::{packet::be_packet, record::RData}; -use dhttp_identity::identity::RemoteAuthority; +use dhttp_identity::identity::RemoteAgent; use tracing::{debug, warn}; use crate::error::{AppError, normalize_host}; @@ -65,7 +65,7 @@ pub enum ValidatedDnsPacket { // Certificate helpers // --------------------------------------------------------------------------- -pub fn extract_client_dns_sans(authority: &(impl RemoteAuthority + ?Sized)) -> Vec { +pub fn extract_client_dns_sans(authority: &(impl RemoteAgent + ?Sized)) -> Vec { use x509_parser::prelude::*; let Some(leaf) = authority.cert_chain().first() else { @@ -88,7 +88,7 @@ pub fn extract_client_dns_sans(authority: &(impl RemoteAuthority + ?Sized)) -> V } pub fn client_allowed_host( - authority: &(impl RemoteAuthority + ?Sized), + authority: &(impl RemoteAgent + ?Sized), ) -> Result { let mut sans = extract_client_dns_sans(authority) .into_iter() @@ -107,7 +107,7 @@ pub fn client_allowed_host( pub fn validate_dns_packet( packet: &[u8], require_signature: bool, - authority: &(impl RemoteAuthority + ?Sized), + authority: &(impl RemoteAgent + ?Sized), ) -> Result { let (remaining, dns_packet) = be_packet(packet).map_err(|e| AppError::InvalidDnsPacket { message: e.to_string(), @@ -163,15 +163,15 @@ mod tests { use std::collections::HashMap; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAuthority; + use dhttp_identity::identity::RemoteAgent; use rustls::pki_types::CertificateDer; use super::*; #[derive(Debug)] - struct TestAuthority; + struct TestAgent; - impl RemoteAuthority for TestAuthority { + impl RemoteAgent for TestAgent { fn name(&self) -> &str { "authority.example" } @@ -187,7 +187,7 @@ mod tests { HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); - let validated = validate_dns_packet(&packet, true, &TestAuthority).unwrap(); + let validated = validate_dns_packet(&packet, true, &TestAgent).unwrap(); assert!(matches!(validated, ValidatedDnsPacket::Empty)); } diff --git a/src/bin/ddns-server/publish.rs b/src/bin/ddns-server/publish.rs index 69b42e7..decafef 100644 --- a/src/bin/ddns-server/publish.rs +++ b/src/bin/ddns-server/publish.rs @@ -1,7 +1,7 @@ use std::{convert::Infallible, sync::Arc}; use deadpool_redis::redis::{self, AsyncCommands}; -use dhttp_identity::identity::RemoteAuthority; +use dhttp_identity::identity::RemoteAgent; use h3x::{connection::ConnectionState, quic}; use http_body_util::BodyExt; use tokio::time::{Duration, Instant}; @@ -55,7 +55,7 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { // Require a valid client certificate for all publish requests. let authority = match request_connection(&request) { - Some(connection) => match connection.remote_authority().await { + Some(connection) => match connection.remote_agent().await { Ok(Some(authority)) => authority, Ok(None) => { warn!("missing client certificate"); @@ -155,7 +155,7 @@ pub async fn publish_record( state: &AppState, host: &str, body: &bytes::Bytes, - authority: &(impl RemoteAuthority + ?Sized), + authority: &(impl RemoteAgent + ?Sized), ) -> Response { let cert_bytes = authority .cert_chain() @@ -258,7 +258,7 @@ pub async fn publish_record( pub async fn clear_record( state: &AppState, host: &str, - authority: &(impl RemoteAuthority + ?Sized), + authority: &(impl RemoteAgent + ?Sized), ) -> Response { let cert_bytes = authority .cert_chain() @@ -319,7 +319,7 @@ mod tests { }; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAuthority; + use dhttp_identity::identity::RemoteAgent; use rustls::pki_types::CertificateDer; use super::*; @@ -330,12 +330,12 @@ mod tests { }; #[derive(Debug)] - struct TestAuthority { + struct TestAgent { name: &'static str, certs: Vec>, } - impl TestAuthority { + impl TestAgent { fn new(name: &'static str, cert_bytes: Vec) -> Self { Self { name, @@ -344,7 +344,7 @@ mod tests { } } - impl RemoteAuthority for TestAuthority { + impl RemoteAgent for TestAgent { fn name(&self) -> &str { self.name } @@ -378,8 +378,8 @@ mod tests { async fn clear_record_removes_only_current_certificate_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let authority_a = TestAuthority::new("authority-a", vec![1]); - let authority_b = TestAuthority::new("authority-b", vec![2]); + let authority_a = TestAgent::new("authority-a", vec![1]); + let authority_b = TestAgent::new("authority-b", vec![2]); let packet_a = packet_for(host, 1); let packet_b = packet_for(host, 2); @@ -413,7 +413,7 @@ mod tests { async fn clear_record_is_idempotent_for_missing_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let authority = TestAuthority::new("authority", vec![1]); + let authority = TestAgent::new("authority", vec![1]); assert_eq!( clear_record(&state, host, &authority).await.status(), diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index 4dbc9d5..cda7622 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -253,14 +253,14 @@ impl EndpointAddr { } } - pub async fn sign_with_authority( + pub async fn sign_with_agent( &mut self, - authority: &(impl dhttp_identity::identity::LocalAuthority + ?Sized), + agent: &(impl dhttp_identity::identity::LocalAgent + ?Sized), ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); - for scheme in signature_schemes_for_algorithm(authority.sign_algorithm()) { - match authority.sign(scheme, &data).await { + for scheme in signature_schemes_for_algorithm(agent.sign_algorithm()) { + match agent.sign(scheme, &data).await { Ok(signature) => { self.signature = Some(EndpointSignature { scheme: u16::from(scheme), @@ -792,14 +792,14 @@ impl TryFrom for DquicEndpointAddr { pub async fn sign_endponit_address( server_id: u8, - authority: Option<&(impl dhttp_identity::identity::LocalAuthority + ?Sized)>, + agent: Option<&(impl dhttp_identity::identity::LocalAgent + ?Sized)>, endpoint: DquicEndpointAddr, ) -> Option { let mut ep: EndpointAddr = endpoint.try_into().ok()?; ep.set_main(server_id == 0); ep.set_sequence(server_id as u64); - if let Some(authority) = authority { - let _ = ep.sign_with_authority(authority).await; + if let Some(agent) = agent { + let _ = ep.sign_with_agent(agent).await; } Some(ep) } @@ -1014,7 +1014,7 @@ mod tests { } } - impl dhttp_identity::identity::LocalAuthority for Ed25519Key { + impl dhttp_identity::identity::LocalAgent for Ed25519Key { fn name(&self) -> &str { "authority.example" } @@ -1052,7 +1052,7 @@ mod tests { let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); let mut ep = EndpointAddr::direct_v4(addr); ep.set_main(true); - futures::executor::block_on(ep.sign_with_authority(&key)).unwrap(); + futures::executor::block_on(ep.sign_with_agent(&key)).unwrap(); let mut buf = BytesMut::new(); buf.put_endpoint_addr(&ep); @@ -1078,13 +1078,13 @@ mod tests { } #[test] - fn sign_with_authority_tries_next_supported_scheme() { + fn sign_with_agent_tries_next_supported_scheme() { #[derive(Debug)] - struct FallbackAuthority; + struct FallbackAgent; - impl dhttp_identity::identity::LocalAuthority for FallbackAuthority { + impl dhttp_identity::identity::LocalAgent for FallbackAgent { fn name(&self) -> &str { - "authority.example" + "agent.example" } fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { @@ -1113,7 +1113,7 @@ mod tests { } let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); - futures::executor::block_on(ep.sign_with_authority(&FallbackAuthority)).unwrap(); + futures::executor::block_on(ep.sign_with_agent(&FallbackAgent)).unwrap(); let signature = ep.signature().unwrap(); assert_eq!( diff --git a/src/publisher.rs b/src/publisher.rs index 9f3128e..2a83de5 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -9,7 +9,7 @@ use std::{ time::Duration, }; -use dhttp_identity::identity::LocalAuthority; +use dhttp_identity::identity::LocalAgent; #[cfg(feature = "mdns-resolver")] use dquic::qbase::net::Family; use dquic::{ @@ -73,7 +73,7 @@ pub struct PublishOptions { } pub struct Publisher { - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -96,7 +96,7 @@ impl std::fmt::Debug for Publisher { impl Publisher { pub fn new( - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -290,7 +290,7 @@ impl Publisher { #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { h3.clear_pool(); } @@ -314,7 +314,7 @@ impl Publisher { #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { self.publish_endpoints(h3, public_endpoints).await?; return Ok(true); @@ -366,7 +366,7 @@ impl Publisher { endpoint.set_sequence(server_id.into()); } endpoint - .sign_with_authority(self.identity.as_ref()) + .sign_with_agent(self.identity.as_ref()) .await .context(publish_once_error::SignEndpointSnafu)?; signed.push(endpoint); @@ -537,11 +537,11 @@ mod tests { use super::*; #[derive(Debug)] - struct TestAuthority; + struct TestAgent; - impl LocalAuthority for TestAuthority { + impl LocalAgent for TestAgent { fn name(&self) -> &str { - "authority.example" + "agent.example" } fn cert_chain(&self) -> &[CertificateDer<'static>] { @@ -584,7 +584,7 @@ mod tests { #[tokio::test] async fn publish_once_reports_no_publisher_resolver() { let publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -597,7 +597,7 @@ mod tests { #[tokio::test] async fn publisher_timeout_is_configurable() { let publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -612,7 +612,7 @@ mod tests { #[tokio::test] async fn signed_packet_applies_publish_options_server_id() { let publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -639,7 +639,7 @@ mod tests { "inet://127.0.0.1:0".parse().expect("valid bind pattern"); let _bind = network.quic().bind(bind_pattern.clone()).await; let publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), network, Arc::new(DisplayOnlyResolver), Arc::new(vec![bind_pattern]), @@ -739,7 +739,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), network.clone(), resolver, Arc::new(vec![ @@ -829,7 +829,7 @@ mod tests { .expect("valid http resolver"), ); let publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), network.clone(), resolver, Arc::new(vec![ @@ -909,7 +909,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), network.clone(), resolver, Arc::new(vec![ @@ -993,7 +993,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAuthority), + Arc::new(TestAgent), network.clone(), resolver, Arc::new(vec![ diff --git a/src/resolvers.rs b/src/resolvers.rs index b565216..0aa21d4 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -13,14 +13,14 @@ use snafu::Report; use tokio::io; #[cfg(feature = "h3x-resolver")] -pub mod h3; +mod h3; #[cfg(feature = "http-resolver")] pub mod http; #[cfg(feature = "mdns-resolver")] use crate::mdns::resolvers::mdns::MdnsResolvers; #[cfg(feature = "h3x-resolver")] -use h3::H3Resolver; +pub use h3::{H3Publisher, H3Resolver}; #[cfg(feature = "http-resolver")] use http::HttpResolver; From 31f06dd41a276163ecedb8de90f96e22b48e8cc9 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 1 Jun 2026 18:41:11 +0800 Subject: [PATCH 58/85] feat(resolvers): add deferred weak resolver wrappers --- examples/publish.rs | 2 +- src/bin/ddns-server/policy.rs | 4 +- src/publisher.rs | 4 +- src/resolvers.rs | 36 ++++-- src/resolvers/deferred.rs | 218 ++++++++++++++++++++++++++++++++++ src/resolvers/h3.rs | 33 +++-- src/resolvers/weak.rs | 201 +++++++++++++++++++++++++++++++ 7 files changed, 470 insertions(+), 28 deletions(-) create mode 100644 src/resolvers/deferred.rs create mode 100644 src/resolvers/weak.rs diff --git a/examples/publish.rs b/examples/publish.rs index 2f572ef..a4ee662 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -163,7 +163,7 @@ async fn main() -> io::Result<()> { if opt.sign { info!("signing endpoint"); endpoint - .sign_with_authority(identity.as_ref()) + .sign_with_agent(identity.as_ref()) .await .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index 4882062..f533454 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -87,9 +87,7 @@ pub fn extract_client_dns_sans(authority: &(impl RemoteAgent + ?Sized)) -> Vec Result { +pub fn client_allowed_host(authority: &(impl RemoteAgent + ?Sized)) -> Result { let mut sans = extract_client_dns_sans(authority) .into_iter() .filter_map(|h| normalize_host(&h).ok()) diff --git a/src/publisher.rs b/src/publisher.rs index 2a83de5..75841b6 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -290,7 +290,7 @@ impl Publisher { #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { h3.clear_pool(); } @@ -314,7 +314,7 @@ impl Publisher { #[cfg(feature = "h3x-resolver")] if let Some(h3) = - any.downcast_ref::>() + any.downcast_ref::>() { self.publish_endpoints(h3, public_endpoints).await?; return Ok(true); diff --git a/src/resolvers.rs b/src/resolvers.rs index 0aa21d4..63bbfd4 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -13,17 +13,16 @@ use snafu::Report; use tokio::io; #[cfg(feature = "h3x-resolver")] -mod h3; +pub mod h3; #[cfg(feature = "http-resolver")] pub mod http; -#[cfg(feature = "mdns-resolver")] -use crate::mdns::resolvers::mdns::MdnsResolvers; -#[cfg(feature = "h3x-resolver")] -pub use h3::{H3Publisher, H3Resolver}; #[cfg(feature = "http-resolver")] use http::HttpResolver; +#[cfg(feature = "mdns-resolver")] +use crate::mdns::resolvers::mdns::MdnsResolvers; + /// Extract and validate the DNS host from `name`, which may include a `:port` /// suffix. Returns `Some(host)` if the host part is a valid RFC-compliant DNS /// name, or `None` for raw IP addresses, bracketed IPv6, or malformed input. @@ -90,6 +89,9 @@ impl std::str::FromStr for DnsScheme { } } +pub mod deferred; +pub mod weak; + type ArcResolver = Arc; #[derive(Default, Clone, Debug)] @@ -140,6 +142,11 @@ pub struct ResolversBuilder { } impl ResolversBuilder { + pub fn resolver(mut self, resolver: ArcResolver) -> Self { + self.resolvers.push(resolver); + self + } + #[cfg(feature = "mdns-resolver")] pub async fn mdns( mut self, @@ -147,7 +154,7 @@ impl ResolversBuilder { patterns: Arc>, ) -> Self { let mdns = Arc::new(MdnsResolvers::bind(network, patterns, DHTTP_MDNS_SERVICE).await); - self.resolvers = self.resolvers.with(mdns); + self.resolvers.push(mdns); self } @@ -175,8 +182,8 @@ impl ResolversBuilder { C::Error: Send + Sync + 'static, C::Connection: Send + 'static, { - let resolver = H3Resolver::from_endpoint(base_url, endpoint)?; - self.resolvers = self.resolvers.with(Arc::new(resolver)); + let resolver = h3::H3Resolver::from_endpoint(base_url, endpoint)?; + self.resolvers.push(Arc::new(resolver)); Ok(self) } @@ -188,14 +195,13 @@ impl ResolversBuilder { #[cfg(feature = "http-resolver")] pub fn http_with_base_url(mut self, base_url: impl AsRef) -> io::Result { let resolver = HttpResolver::new(base_url.as_ref())?; - self.resolvers = self.resolvers.with(Arc::new(resolver)); + self.resolvers.push(Arc::new(resolver)); Ok(self) } pub fn system(mut self) -> Self { - self.resolvers = self - .resolvers - .with(Arc::new(dquic::qresolve::SystemResolver)); + self.resolvers + .push(Arc::new(dquic::qresolve::SystemResolver)); self } @@ -214,10 +220,14 @@ impl Resolvers { } pub fn with(mut self, resolver: ArcResolver) -> Self { - self.resolvers.push(resolver); + self.push(resolver); self } + pub fn push(&mut self, resolver: ArcResolver) { + self.resolvers.push(resolver); + } + pub fn iter(&self) -> impl Iterator { self.resolvers.iter() } diff --git a/src/resolvers/deferred.rs b/src/resolvers/deferred.rs new file mode 100644 index 0000000..a961ac8 --- /dev/null +++ b/src/resolvers/deferred.rs @@ -0,0 +1,218 @@ +use std::{fmt, io}; + +use dquic::qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture}; +use futures::FutureExt; +use snafu::{ResultExt, Snafu}; +use tokio::sync::OnceCell; + +#[derive(Debug, Snafu)] +#[snafu(module, visibility(pub))] +pub enum DeferredLookupError { + #[snafu(display("deferred resolver has not been initialized"))] + Uninitialized, + #[snafu(display("deferred resolver lookup failed"))] + Lookup { source: io::Error }, +} + +#[derive(Debug, Snafu)] +#[snafu(module, visibility(pub))] +pub enum DeferredPublishError { + #[snafu(display("deferred resolver has not been initialized"))] + Uninitialized, + #[snafu(display("deferred resolver publish failed"))] + Publish { source: io::Error }, +} + +#[derive(Debug, Snafu)] +#[snafu(module, visibility(pub))] +pub enum SetDeferredResolverError { + #[snafu(display("deferred resolver has already been initialized"))] + AlreadyInitialized, +} + +pub struct DeferredResolver { + inner: OnceCell, +} + +impl fmt::Debug for DeferredResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeferredResolver") + .field("initialized", &self.inner.get().is_some()) + .finish() + } +} + +impl Default for DeferredResolver { + fn default() -> Self { + Self::new() + } +} + +impl DeferredResolver { + #[must_use] + pub fn new() -> Self { + Self { + inner: OnceCell::new(), + } + } + + pub fn set(&self, resolver: R) -> Result<(), SetDeferredResolverError> { + if self.inner.set(resolver).is_err() { + return set_deferred_resolver_error::AlreadyInitializedSnafu.fail(); + } + Ok(()) + } + + #[must_use] + pub fn get(&self) -> Option<&R> { + self.inner.get() + } +} + +impl fmt::Display for DeferredResolver +where + R: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.inner.get() { + Some(resolver) => write!(f, "DeferredResolver({resolver})"), + None => f.write_str("DeferredResolver(uninitialized)"), + } + } +} + +impl DeferredResolver +where + R: Resolve + 'static, +{ + pub async fn lookup_typed(&self, name: &str) -> Result { + let Some(resolver) = self.get() else { + return deferred_lookup_error::UninitializedSnafu.fail(); + }; + resolver + .lookup(name) + .await + .context(deferred_lookup_error::LookupSnafu) + } +} + +impl Resolve for DeferredResolver +where + R: Resolve + 'static, +{ + fn lookup<'a>(&'a self, name: &'a str) -> ResolveFuture<'a> { + async move { self.lookup_typed(name).await.map_err(io::Error::other) }.boxed() + } +} + +impl DeferredResolver +where + R: Publish + 'static, +{ + pub async fn publish_typed( + &self, + name: &str, + packet: &[u8], + ) -> Result<(), DeferredPublishError> { + let Some(resolver) = self.get() else { + return deferred_publish_error::UninitializedSnafu.fail(); + }; + resolver + .publish(name, packet) + .await + .context(deferred_publish_error::PublishSnafu) + } +} + +impl Publish for DeferredResolver +where + R: Publish + 'static, +{ + fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { + async move { + self.publish_typed(name, packet) + .await + .map_err(io::Error::other) + } + .boxed() + } +} + +#[cfg(test)] +mod tests { + use std::fmt; + + use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Publish, Resolve, Source}, + }; + use futures::{FutureExt, StreamExt}; + + use super::*; + + #[derive(Debug)] + struct TestResolver; + + impl fmt::Display for TestResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("test resolver") + } + } + + impl Resolve for TestResolver { + fn lookup<'a>(&'a self, _name: &'a str) -> dquic::qresolve::ResolveFuture<'a> { + async move { + let endpoint = EndpointAddr::direct("127.0.0.1:4433".parse().unwrap()); + Ok(futures::stream::iter([(Source::System, endpoint)]).boxed()) + } + .boxed() + } + } + + impl Publish for TestResolver { + fn publish<'a>( + &'a self, + _name: &'a str, + _packet: &'a [u8], + ) -> dquic::qresolve::PublishFuture<'a> { + async move { Ok(()) }.boxed() + } + } + + #[tokio::test] + async fn lookup_before_set_returns_typed_uninitialized_error() { + let resolver: DeferredResolver = DeferredResolver::new(); + + let error = match resolver.lookup_typed("example.test").await { + Ok(_) => panic!("uninitialized resolver must not resolve"), + Err(error) => error, + }; + + assert!(matches!(error, DeferredLookupError::Uninitialized)); + } + + #[tokio::test] + async fn lookup_after_set_forwards_to_inner_resolver() { + let resolver = DeferredResolver::new(); + resolver.set(TestResolver).expect("first set succeeds"); + + let mut stream = resolver.lookup_typed("example.test").await.unwrap(); + let (_source, endpoint) = stream.next().await.expect("forwarded endpoint"); + + assert_eq!( + endpoint, + EndpointAddr::direct("127.0.0.1:4433".parse().unwrap()) + ); + } + + #[tokio::test] + async fn publish_after_set_forwards_to_inner_resolver() { + let resolver = DeferredResolver::new(); + resolver.set(TestResolver).expect("first set succeeds"); + + resolver + .publish_typed("example.test", b"packet") + .await + .unwrap(); + } +} diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 156415f..ce419f0 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -280,8 +280,6 @@ where unreachable!("lookup retry loop returns on the final attempt") } - pub const EXCLUDED_DOMAINS: [&str; 2] = ["dns.genmeta.net", "download.genmeta.net"]; - pub async fn lookup(&self, name: &str) -> Result> { use crate::core::parser::record; let server = Arc::from(self.base_url.origin().ascii_serialization()); @@ -291,11 +289,6 @@ where return Err(Error::NoRecordFound); }; - // 1. Exclude certain domains from lookup - if Self::EXCLUDED_DOMAINS.contains(&domain) { - return Err(Error::NoRecordFound); - } - let now = Instant::now(); let positive_ttl = Duration::from_secs(10); let negative_ttl = Duration::from_secs(2); @@ -411,9 +404,8 @@ where #[cfg(test)] mod tests { - use crate::resolvers::DHTTP_H3_DNS_SERVER; - use super::*; + use crate::resolvers::DHTTP_H3_DNS_SERVER; #[test] fn lookup_retry_budget_leaves_external_timeout_margin() { @@ -453,4 +445,27 @@ mod tests { EndpointAddr::direct("192.168.5.78:41748".parse().unwrap()) ); } + + #[tokio::test] + async fn cached_dns_genmeta_net_record_is_returned() { + let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( + h3x::dquic::QuicEndpoint::builder().build().await, + )); + let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint).unwrap(); + resolver.cached_records.insert( + "dns.genmeta.net".to_owned(), + Record { + addrs: vec![EndpointAddr::direct("192.0.2.53:4433".parse().unwrap())], + expire: Instant::now() + Duration::from_secs(60), + }, + ); + + let mut records = resolver.lookup("dns.genmeta.net").await.unwrap(); + let (_source, endpoint) = records.next().await.unwrap(); + + assert_eq!( + endpoint, + EndpointAddr::direct("192.0.2.53:4433".parse().unwrap()) + ); + } } diff --git a/src/resolvers/weak.rs b/src/resolvers/weak.rs new file mode 100644 index 0000000..965df02 --- /dev/null +++ b/src/resolvers/weak.rs @@ -0,0 +1,201 @@ +use std::{ + fmt, io, + sync::{Arc, Weak}, +}; + +use dquic::qresolve::{Publish, PublishFuture, RecordStream, Resolve, ResolveFuture}; +use futures::FutureExt; +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +#[snafu(module, visibility(pub))] +pub enum WeakLookupError { + #[snafu(display("weak resolver target has been dropped"))] + Dropped, + #[snafu(display("weak resolver lookup failed"))] + Lookup { source: io::Error }, +} + +#[derive(Debug, Snafu)] +#[snafu(module, visibility(pub))] +pub enum WeakPublishError { + #[snafu(display("weak resolver target has been dropped"))] + Dropped, + #[snafu(display("weak resolver publish failed"))] + Publish { source: io::Error }, +} + +pub struct WeakResolver { + inner: Weak, +} + +impl fmt::Debug for WeakResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WeakResolver") + .field("alive", &self.inner.strong_count().gt(&0)) + .finish() + } +} + +impl Clone for WeakResolver { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl WeakResolver { + #[must_use] + pub fn new(inner: Weak) -> Self { + Self { inner } + } + + pub fn upgrade(&self) -> Result, WeakLookupError> { + self.inner.upgrade().ok_or(WeakLookupError::Dropped) + } +} + +impl fmt::Display for WeakResolver +where + R: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.inner.upgrade() { + Some(resolver) => write!(f, "WeakResolver({resolver})"), + None => f.write_str("WeakResolver(dropped)"), + } + } +} + +impl WeakResolver +where + R: Resolve + 'static, +{ + pub async fn lookup_typed(&self, name: &str) -> Result { + let resolver = self.upgrade()?; + resolver + .lookup(name) + .await + .context(weak_lookup_error::LookupSnafu) + } +} + +impl Resolve for WeakResolver +where + R: Resolve + 'static, +{ + fn lookup<'a>(&'a self, name: &'a str) -> ResolveFuture<'a> { + async move { self.lookup_typed(name).await.map_err(io::Error::other) }.boxed() + } +} + +impl WeakResolver +where + R: Publish + 'static, +{ + pub async fn publish_typed(&self, name: &str, packet: &[u8]) -> Result<(), WeakPublishError> { + let Some(resolver) = self.inner.upgrade() else { + return weak_publish_error::DroppedSnafu.fail(); + }; + resolver + .publish(name, packet) + .await + .context(weak_publish_error::PublishSnafu) + } +} + +impl Publish for WeakResolver +where + R: Publish + 'static, +{ + fn publish<'a>(&'a self, name: &'a str, packet: &'a [u8]) -> PublishFuture<'a> { + async move { + self.publish_typed(name, packet) + .await + .map_err(io::Error::other) + } + .boxed() + } +} + +#[cfg(test)] +mod tests { + use std::{fmt, sync::Arc}; + + use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Publish, Resolve, Source}, + }; + use futures::{FutureExt, StreamExt}; + + use super::*; + + #[derive(Debug)] + struct TestResolver; + + impl fmt::Display for TestResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("test resolver") + } + } + + impl Resolve for TestResolver { + fn lookup<'a>(&'a self, _name: &'a str) -> dquic::qresolve::ResolveFuture<'a> { + async move { + let endpoint = EndpointAddr::direct("127.0.0.1:4433".parse().unwrap()); + Ok(futures::stream::iter([(Source::System, endpoint)]).boxed()) + } + .boxed() + } + } + + impl Publish for TestResolver { + fn publish<'a>( + &'a self, + _name: &'a str, + _packet: &'a [u8], + ) -> dquic::qresolve::PublishFuture<'a> { + async move { Ok(()) }.boxed() + } + } + + #[tokio::test] + async fn lookup_after_target_drop_returns_typed_error() { + let strong = Arc::new(TestResolver); + let resolver = WeakResolver::new(Arc::downgrade(&strong)); + drop(strong); + + let error = match resolver.lookup_typed("example.test").await { + Ok(_) => panic!("dropped weak resolver must not resolve"), + Err(error) => error, + }; + + assert!(matches!(error, WeakLookupError::Dropped)); + } + + #[tokio::test] + async fn lookup_forwards_while_target_is_alive() { + let strong = Arc::new(TestResolver); + let resolver = WeakResolver::new(Arc::downgrade(&strong)); + + let mut stream = resolver.lookup_typed("example.test").await.unwrap(); + let (_source, endpoint) = stream.next().await.expect("forwarded endpoint"); + + assert_eq!( + endpoint, + EndpointAddr::direct("127.0.0.1:4433".parse().unwrap()) + ); + } + + #[tokio::test] + async fn publish_forwards_while_target_is_alive() { + let strong = Arc::new(TestResolver); + let resolver = WeakResolver::new(Arc::downgrade(&strong)); + + resolver + .publish_typed("example.test", b"packet") + .await + .unwrap(); + } +} From 5f7ec9dc112626cbd17d77b45257c82931bef496 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 2 Jun 2026 13:49:41 +0800 Subject: [PATCH 59/85] refactor: use authority identity naming --- .../plans/2026-05-19-ddns-publisher.md | 2 +- examples/publish.rs | 2 +- src/bin/ddns-server/main.rs | 2 +- src/bin/ddns-server/policy.rs | 18 ++++++----- src/bin/ddns-server/publish.rs | 22 +++++++------- src/core/parser/record/endpoint.rs | 28 ++++++++--------- src/publisher.rs | 30 +++++++++---------- 7 files changed, 53 insertions(+), 51 deletions(-) diff --git a/docs/superpowers/plans/2026-05-19-ddns-publisher.md b/docs/superpowers/plans/2026-05-19-ddns-publisher.md index 61408e4..6edad3c 100644 --- a/docs/superpowers/plans/2026-05-19-ddns-publisher.md +++ b/docs/superpowers/plans/2026-05-19-ddns-publisher.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Add signed DNS publishing for DHTTP endpoints using async identity agents and concrete ddns publishers. +**Goal:** Add signed DNS publishing for DHTTP endpoints using async identity authorities and concrete ddns publishers. **Architecture:** `dhttp-identity` owns async authority traits and signature helpers. `ddns-core` signs endpoint records through `LocalAuthority`. `ddns` owns `Publisher`, discovers concrete publishers by downcasting, and publishes signed packets. `dhttp::Endpoint` provides the convenience constructor. diff --git a/examples/publish.rs b/examples/publish.rs index a4ee662..2f572ef 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -163,7 +163,7 @@ async fn main() -> io::Result<()> { if opt.sign { info!("signing endpoint"); endpoint - .sign_with_agent(identity.as_ref()) + .sign_with_authority(identity.as_ref()) .await .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index 1b67c27..a9fbda8 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -253,7 +253,7 @@ async fn main() -> Result<(), Box> { .await; let server = Arc::new(H3Endpoint::new(quic)); info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); - server.serve_owned(router).await?; + server.listen_owned(router).await?; Ok(()) } diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index f533454..6fb5451 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -1,5 +1,5 @@ use ddns::core::parser::{packet::be_packet, record::RData}; -use dhttp_identity::identity::RemoteAgent; +use dhttp_identity::identity::RemoteAuthority; use tracing::{debug, warn}; use crate::error::{AppError, normalize_host}; @@ -65,7 +65,7 @@ pub enum ValidatedDnsPacket { // Certificate helpers // --------------------------------------------------------------------------- -pub fn extract_client_dns_sans(authority: &(impl RemoteAgent + ?Sized)) -> Vec { +pub fn extract_client_dns_sans(authority: &(impl RemoteAuthority + ?Sized)) -> Vec { use x509_parser::prelude::*; let Some(leaf) = authority.cert_chain().first() else { @@ -87,7 +87,9 @@ pub fn extract_client_dns_sans(authority: &(impl RemoteAgent + ?Sized)) -> Vec Result { +pub fn client_allowed_host( + authority: &(impl RemoteAuthority + ?Sized), +) -> Result { let mut sans = extract_client_dns_sans(authority) .into_iter() .filter_map(|h| normalize_host(&h).ok()) @@ -105,7 +107,7 @@ pub fn client_allowed_host(authority: &(impl RemoteAgent + ?Sized)) -> Result Result { let (remaining, dns_packet) = be_packet(packet).map_err(|e| AppError::InvalidDnsPacket { message: e.to_string(), @@ -161,15 +163,15 @@ mod tests { use std::collections::HashMap; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAgent; + use dhttp_identity::identity::RemoteAuthority; use rustls::pki_types::CertificateDer; use super::*; #[derive(Debug)] - struct TestAgent; + struct TestAuthority; - impl RemoteAgent for TestAgent { + impl RemoteAuthority for TestAuthority { fn name(&self) -> &str { "authority.example" } @@ -185,7 +187,7 @@ mod tests { HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); - let validated = validate_dns_packet(&packet, true, &TestAgent).unwrap(); + let validated = validate_dns_packet(&packet, true, &TestAuthority).unwrap(); assert!(matches!(validated, ValidatedDnsPacket::Empty)); } diff --git a/src/bin/ddns-server/publish.rs b/src/bin/ddns-server/publish.rs index decafef..69b42e7 100644 --- a/src/bin/ddns-server/publish.rs +++ b/src/bin/ddns-server/publish.rs @@ -1,7 +1,7 @@ use std::{convert::Infallible, sync::Arc}; use deadpool_redis::redis::{self, AsyncCommands}; -use dhttp_identity::identity::RemoteAgent; +use dhttp_identity::identity::RemoteAuthority; use h3x::{connection::ConnectionState, quic}; use http_body_util::BodyExt; use tokio::time::{Duration, Instant}; @@ -55,7 +55,7 @@ async fn publish_with_cert(state: AppState, request: Request) -> Response { // Require a valid client certificate for all publish requests. let authority = match request_connection(&request) { - Some(connection) => match connection.remote_agent().await { + Some(connection) => match connection.remote_authority().await { Ok(Some(authority)) => authority, Ok(None) => { warn!("missing client certificate"); @@ -155,7 +155,7 @@ pub async fn publish_record( state: &AppState, host: &str, body: &bytes::Bytes, - authority: &(impl RemoteAgent + ?Sized), + authority: &(impl RemoteAuthority + ?Sized), ) -> Response { let cert_bytes = authority .cert_chain() @@ -258,7 +258,7 @@ pub async fn publish_record( pub async fn clear_record( state: &AppState, host: &str, - authority: &(impl RemoteAgent + ?Sized), + authority: &(impl RemoteAuthority + ?Sized), ) -> Response { let cert_bytes = authority .cert_chain() @@ -319,7 +319,7 @@ mod tests { }; use ddns::core::{MdnsPacket, parser::record::endpoint::EndpointAddr}; - use dhttp_identity::identity::RemoteAgent; + use dhttp_identity::identity::RemoteAuthority; use rustls::pki_types::CertificateDer; use super::*; @@ -330,12 +330,12 @@ mod tests { }; #[derive(Debug)] - struct TestAgent { + struct TestAuthority { name: &'static str, certs: Vec>, } - impl TestAgent { + impl TestAuthority { fn new(name: &'static str, cert_bytes: Vec) -> Self { Self { name, @@ -344,7 +344,7 @@ mod tests { } } - impl RemoteAgent for TestAgent { + impl RemoteAuthority for TestAuthority { fn name(&self) -> &str { self.name } @@ -378,8 +378,8 @@ mod tests { async fn clear_record_removes_only_current_certificate_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let authority_a = TestAgent::new("authority-a", vec![1]); - let authority_b = TestAgent::new("authority-b", vec![2]); + let authority_a = TestAuthority::new("authority-a", vec![1]); + let authority_b = TestAuthority::new("authority-b", vec![2]); let packet_a = packet_for(host, 1); let packet_b = packet_for(host, 2); @@ -413,7 +413,7 @@ mod tests { async fn clear_record_is_idempotent_for_missing_fingerprint() { let state = memory_state(); let host = "reimu.pilot.genmeta.net"; - let authority = TestAgent::new("authority", vec![1]); + let authority = TestAuthority::new("authority", vec![1]); assert_eq!( clear_record(&state, host, &authority).await.status(), diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index cda7622..4dbc9d5 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -253,14 +253,14 @@ impl EndpointAddr { } } - pub async fn sign_with_agent( + pub async fn sign_with_authority( &mut self, - agent: &(impl dhttp_identity::identity::LocalAgent + ?Sized), + authority: &(impl dhttp_identity::identity::LocalAuthority + ?Sized), ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); - for scheme in signature_schemes_for_algorithm(agent.sign_algorithm()) { - match agent.sign(scheme, &data).await { + for scheme in signature_schemes_for_algorithm(authority.sign_algorithm()) { + match authority.sign(scheme, &data).await { Ok(signature) => { self.signature = Some(EndpointSignature { scheme: u16::from(scheme), @@ -792,14 +792,14 @@ impl TryFrom for DquicEndpointAddr { pub async fn sign_endponit_address( server_id: u8, - agent: Option<&(impl dhttp_identity::identity::LocalAgent + ?Sized)>, + authority: Option<&(impl dhttp_identity::identity::LocalAuthority + ?Sized)>, endpoint: DquicEndpointAddr, ) -> Option { let mut ep: EndpointAddr = endpoint.try_into().ok()?; ep.set_main(server_id == 0); ep.set_sequence(server_id as u64); - if let Some(agent) = agent { - let _ = ep.sign_with_agent(agent).await; + if let Some(authority) = authority { + let _ = ep.sign_with_authority(authority).await; } Some(ep) } @@ -1014,7 +1014,7 @@ mod tests { } } - impl dhttp_identity::identity::LocalAgent for Ed25519Key { + impl dhttp_identity::identity::LocalAuthority for Ed25519Key { fn name(&self) -> &str { "authority.example" } @@ -1052,7 +1052,7 @@ mod tests { let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); let mut ep = EndpointAddr::direct_v4(addr); ep.set_main(true); - futures::executor::block_on(ep.sign_with_agent(&key)).unwrap(); + futures::executor::block_on(ep.sign_with_authority(&key)).unwrap(); let mut buf = BytesMut::new(); buf.put_endpoint_addr(&ep); @@ -1078,13 +1078,13 @@ mod tests { } #[test] - fn sign_with_agent_tries_next_supported_scheme() { + fn sign_with_authority_tries_next_supported_scheme() { #[derive(Debug)] - struct FallbackAgent; + struct FallbackAuthority; - impl dhttp_identity::identity::LocalAgent for FallbackAgent { + impl dhttp_identity::identity::LocalAuthority for FallbackAuthority { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { @@ -1113,7 +1113,7 @@ mod tests { } let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); - futures::executor::block_on(ep.sign_with_agent(&FallbackAgent)).unwrap(); + futures::executor::block_on(ep.sign_with_authority(&FallbackAuthority)).unwrap(); let signature = ep.signature().unwrap(); assert_eq!( diff --git a/src/publisher.rs b/src/publisher.rs index 75841b6..9f3128e 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -9,7 +9,7 @@ use std::{ time::Duration, }; -use dhttp_identity::identity::LocalAgent; +use dhttp_identity::identity::LocalAuthority; #[cfg(feature = "mdns-resolver")] use dquic::qbase::net::Family; use dquic::{ @@ -73,7 +73,7 @@ pub struct PublishOptions { } pub struct Publisher { - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -96,7 +96,7 @@ impl std::fmt::Debug for Publisher { impl Publisher { pub fn new( - identity: Arc, + identity: Arc, network: Arc, resolver: Arc, bind_patterns: Arc>, @@ -366,7 +366,7 @@ impl Publisher { endpoint.set_sequence(server_id.into()); } endpoint - .sign_with_agent(self.identity.as_ref()) + .sign_with_authority(self.identity.as_ref()) .await .context(publish_once_error::SignEndpointSnafu)?; signed.push(endpoint); @@ -537,11 +537,11 @@ mod tests { use super::*; #[derive(Debug)] - struct TestAgent; + struct TestAuthority; - impl LocalAgent for TestAgent { + impl LocalAuthority for TestAuthority { fn name(&self) -> &str { - "agent.example" + "authority.example" } fn cert_chain(&self) -> &[CertificateDer<'static>] { @@ -584,7 +584,7 @@ mod tests { #[tokio::test] async fn publish_once_reports_no_publisher_resolver() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -597,7 +597,7 @@ mod tests { #[tokio::test] async fn publisher_timeout_is_configurable() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -612,7 +612,7 @@ mod tests { #[tokio::test] async fn signed_packet_applies_publish_options_server_id() { let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), h3x::dquic::Network::builder().build(), Arc::new(DisplayOnlyResolver), Arc::new(Vec::new()), @@ -639,7 +639,7 @@ mod tests { "inet://127.0.0.1:0".parse().expect("valid bind pattern"); let _bind = network.quic().bind(bind_pattern.clone()).await; let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network, Arc::new(DisplayOnlyResolver), Arc::new(vec![bind_pattern]), @@ -739,7 +739,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -829,7 +829,7 @@ mod tests { .expect("valid http resolver"), ); let publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -909,7 +909,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ @@ -993,7 +993,7 @@ mod tests { .expect("valid http resolver"), ); let mut publisher = Publisher::new( - Arc::new(TestAgent), + Arc::new(TestAuthority), network.clone(), resolver, Arc::new(vec![ From 8e08aedb420c57e400111b114c1fd4359231feb9 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 2 Jun 2026 14:15:00 +0800 Subject: [PATCH 60/85] chore: align dependency versions --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d4e8de..27b6448 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ flume = "0.12" futures = "0.3" libc = "0.2" nom = "8" -rand = "0.9" +rand = "0.10" ring = "0.17" rustls = { version = "0.23", default-features = false, features = [ "logging", @@ -29,7 +29,7 @@ rustls = { version = "0.23", default-features = false, features = [ ] } rustls-pemfile = "2" snafu = "0.8" -socket2 = { version = "0.5.8", features = ["all"] } +socket2 = { version = "0.6", features = ["all"] } tokio = { version = "1", features = [ "time", "macros", @@ -59,7 +59,7 @@ clap = { version = "4", features = ["derive"], optional = true } deadpool-redis = { version = "0.23", optional = true } idna = { version = "1", optional = true } serde = { version = "1", features = ["derive"], optional = true } -toml = { version = "0.8", optional = true } +toml = { version = "1", optional = true } tower-service = { version = "0.3", optional = true } tracing-subscriber = { version = "0.3", features = [ "env-filter", From d1a42a706b4b605cd21359ec422ec15274c289ba Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 2 Jun 2026 16:06:38 +0800 Subject: [PATCH 61/85] chore: remove unused dependencies --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27b6448..89fa291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,10 +94,7 @@ clap = { version = "4", features = ["derive"] } h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = [ "dquic", ] } -idna = "1" -rustls-pki-types = "1" shellexpand = "3" -tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter"] } [[bin]] From 5a9774d6d0de221693d5d587efa40f0085076178 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 2 Jun 2026 17:47:53 +0800 Subject: [PATCH 62/85] chore: update resolver dependencies --- Cargo.toml | 14 +++++---- examples/publish.rs | 5 ---- examples/query.rs | 3 -- src/bin/ddns-server/main.rs | 5 ---- src/core/parser/sigin.rs | 2 +- src/resolvers/http.rs | 58 +++++++++++++++++++++++-------------- 6 files changed, 45 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89fa291..7baa1c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ autoexamples = false [dependencies] base64 = "0.22" -bitfield-struct = "0.10" +bitfield-struct = "0.13" bytes = "1" dashmap = "6" dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "main" } @@ -27,8 +27,9 @@ rustls = { version = "0.23", default-features = false, features = [ "logging", "ring", ] } +rustls-native-certs = { version = "0.8", optional = true } rustls-pemfile = "2" -snafu = "0.8" +snafu = "0.9" socket2 = { version = "0.6", features = ["all"] } tokio = { version = "1", features = [ "time", @@ -46,12 +47,13 @@ h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } -reqwest = { version = "0.12", default-features = false, features = [ +reqwest = { version = "0.13", default-features = false, features = [ "charset", - "rustls-tls", "http2", - "macos-system-configuration", "json", + "query", + "rustls-no-provider", + "system-proxy", ], optional = true } url = { version = "2", optional = true } @@ -77,7 +79,7 @@ h3x-resolver = [ "dep:url", ] mdns-resolver = ["dep:h3x", "h3x/dquic"] -http-resolver = ["dep:reqwest"] +http-resolver = ["dep:reqwest", "dep:rustls-native-certs"] server = [ "h3x-resolver", "dep:clap", diff --git a/examples/publish.rs b/examples/publish.rs index 2f572ef..57c9085 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -97,11 +97,6 @@ fn expand_tilde(path: &Path) -> io::Result { #[tokio::main] async fn main() -> io::Result<()> { - // Install ring crypto provider - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install ring crypto provider"); - tracing_subscriber::fmt() .with_max_level(Level::DEBUG) .init(); diff --git a/examples/query.rs b/examples/query.rs index 945bdec..80235cd 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -100,9 +100,6 @@ fn expand_tilde(path: &Path) -> io::Result { #[tokio::main] async fn main() -> Result<(), Box> { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install ring crypto provider"); tracing_subscriber::fmt() .with_max_level(Level::DEBUG) .init(); diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index a9fbda8..ee4071b 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -148,11 +148,6 @@ fn build_seed_records(seed_records: &[SeedRecordConfig]) -> io::Result Result<(), Box> { - // Install ring crypto provider - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install ring crypto provider"); - tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::filter::filter_fn(|metadata| { diff --git a/src/core/parser/sigin.rs b/src/core/parser/sigin.rs index fa123c5..395b727 100644 --- a/src/core/parser/sigin.rs +++ b/src/core/parser/sigin.rs @@ -31,7 +31,7 @@ pub enum VerifyError { InvalidPem { source: std::io::Error }, #[snafu(display("invalid base64"))] InvalidBase64 { source: base64::DecodeError }, - #[snafu(display("IO error"))] + #[snafu(display("io error"))] Io { source: std::io::Error }, } diff --git a/src/resolvers/http.rs b/src/resolvers/http.rs index 6e15aeb..80a46c8 100644 --- a/src/resolvers/http.rs +++ b/src/resolvers/http.rs @@ -1,8 +1,4 @@ -use std::{ - fmt::Display, - io, - sync::{Arc, LazyLock}, -}; +use std::{fmt::Display, io, sync::Arc}; use dashmap::DashMap; use dquic::{ @@ -33,7 +29,7 @@ impl Display for HttpResolver { write!( f, "Http DNS({})", - self.base_url.host_str().expect("Cheked in constructor") + self.base_url.host_str().expect("checked in constructor") ) } } @@ -46,28 +42,48 @@ impl HttpResolver { base_url.host_str().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, - "Base URL must have a valid host", + "base URL must have a valid host", ) })?; - static HTTP_CLIENT: LazyLock = LazyLock::new(|| { - Client::builder() - .build() - // with certs? - .expect("Failed to build HTTP client for HttpResolver") - }); - Ok(Self { - http_client: HTTP_CLIENT.clone(), + http_client: build_http_client()?, base_url, cached_records: DashMap::new(), }) } } +fn build_http_client() -> io::Result { + let native_certs = rustls_native_certs::load_native_certs(); + for error in &native_certs.errors { + let report = snafu::Report::from_error(error); + tracing::warn!(error = %report, "failed to load native root certificate"); + } + + let mut root_store = rustls::RootCertStore::empty(); + let (valid_roots, invalid_roots) = root_store.add_parsable_certificates(native_certs.certs); + if invalid_roots > 0 { + tracing::debug!(invalid_roots, "ignored invalid native root certificates"); + } + if valid_roots == 0 { + tracing::warn!("no native root certificates loaded for http resolver"); + } + + let mut tls = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + tls.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + + Client::builder() + .use_preconfigured_tls(tls) + .build() + .map_err(io::Error::other) +} + #[derive(Debug, snafu::Snafu)] enum Error { - #[snafu(display("HTTP request failed"))] + #[snafu(display("http request failed"))] Reqwest { source: reqwest::Error }, #[snafu(display("{status}"))] @@ -99,18 +115,16 @@ impl Publish for HttpResolver { Box::pin(async move { let mut url = self.base_url.join("publish").expect("Invalid base URL"); url.set_query(Some(&format!("host={name}"))); - let client = reqwest::Client::new(); - let response = client + let response = self + .http_client .post(url) .header("Content-Type", "application/octet-stream") .body(packet.to_vec()) .send() .await - .map_err(|e| io::Error::other(e.to_string()))?; + .map_err(io::Error::other)?; - let _response = response - .error_for_status() - .map_err(|e| io::Error::other(e.to_string()))?; + let _response = response.error_for_status().map_err(io::Error::other)?; Ok(()) }) } From 21c0881c2e02f3d47058a3e397ade5cca091ff10 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 4 Jun 2026 19:59:24 +0800 Subject: [PATCH 63/85] fix: add bootstrap placeholder defaults --- build.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/build.rs b/build.rs index 80d5895..8794f25 100644 --- a/build.rs +++ b/build.rs @@ -4,12 +4,16 @@ const H3_DNS_SERVER_ENV: &str = "DHTTP_H3_DNS_SERVER"; const HTTP_DNS_SERVER_ENV: &str = "DHTTP_HTTP_DNS_SERVER"; const MDNS_SERVICE_ENV: &str = "DHTTP_MDNS_SERVICE"; +const DEFAULT_H3_DNS_SERVER: &str = "https://dhttp.example.net"; +const DEFAULT_HTTP_DNS_SERVER: &str = "https://dhttp.example.net"; +const DEFAULT_MDNS_SERVICE: &str = "dhttp.example.net"; + fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); - let h3_dns_server = required_env(H3_DNS_SERVER_ENV); - let http_dns_server = required_env(HTTP_DNS_SERVER_ENV); - let mdns_service = required_env(MDNS_SERVICE_ENV); + let h3_dns_server = env_or_default(H3_DNS_SERVER_ENV, DEFAULT_H3_DNS_SERVER); + let http_dns_server = env_or_default(HTTP_DNS_SERVER_ENV, DEFAULT_HTTP_DNS_SERVER); + let mdns_service = env_or_default(MDNS_SERVICE_ENV, DEFAULT_MDNS_SERVICE); let bootstrap = format!( "// @generated by build.rs; do not edit.\n\ @@ -25,6 +29,43 @@ fn main() { println!("cargo::rerun-if-env-changed={MDNS_SERVICE_ENV}"); } -fn required_env(name: &str) -> String { - env::var(name).unwrap_or_else(|_| panic!("missing {name}; set it before building ddns")) +fn env_or_default(name: &str, default: &str) -> String { + env::var(name).unwrap_or_else(|_| default.to_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_env_uses_dhttp_example_net_placeholder() { + let name = format!("__DDNS_MISSING_BOOTSTRAP_{}", std::process::id()); + + assert_eq!( + env_or_default(&name, DEFAULT_H3_DNS_SERVER), + "https://dhttp.example.net" + ); + assert_eq!( + env_or_default(&name, DEFAULT_HTTP_DNS_SERVER), + "https://dhttp.example.net" + ); + assert_eq!( + env_or_default(&name, DEFAULT_MDNS_SERVICE), + "dhttp.example.net" + ); + } + + #[test] + fn placeholder_urls_do_not_include_explicit_ports() { + for url in [DEFAULT_H3_DNS_SERVER, DEFAULT_HTTP_DNS_SERVER] { + let authority = url + .strip_prefix("https://") + .expect("placeholder URL uses https") + .split('/') + .next() + .expect("placeholder URL has an authority"); + + assert_eq!(authority, "dhttp.example.net"); + } + } } From 4fd0a57677f9f4ee03c13072c8f857ad26170380 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 4 Jun 2026 22:55:18 +0800 Subject: [PATCH 64/85] ci: rename release workflows to publish --- .github/workflows/{release-crates.yml => publish-crates.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{release-crates.yml => publish-crates.yml} (100%) diff --git a/.github/workflows/release-crates.yml b/.github/workflows/publish-crates.yml similarity index 100% rename from .github/workflows/release-crates.yml rename to .github/workflows/publish-crates.yml From 81053dc2e4b64c30e94a2fa04d08c462ea948cd5 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 5 Jun 2026 17:53:49 +0800 Subject: [PATCH 65/85] refactor: omit endpoint signature schemes --- src/core/parser/record/endpoint.rs | 118 +++++------------------------ src/core/parser/sigin.rs | 63 ++++----------- src/publisher.rs | 18 ++--- 3 files changed, 40 insertions(+), 159 deletions(-) diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index 4dbc9d5..9d6956d 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -16,7 +16,7 @@ use nom::{ error::{ErrorKind, make_error}, number::streaming::{be_u8, be_u16, be_u32, be_u128}, }; -use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer}; +use rustls::pki_types::SubjectPublicKeyInfoDer; use snafu::{ResultExt, Snafu}; use crate::core::parser::{ @@ -24,43 +24,6 @@ use crate::core::parser::{ varint::{VarInt, WriteVarInt, be_varint}, }; -const SIGNATURE_SCHEME_PREFERENCE: &[SignatureScheme] = &[ - SignatureScheme::ED25519, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, -]; - -fn signature_schemes_for_algorithm( - algorithm: rustls::SignatureAlgorithm, -) -> impl Iterator { - SIGNATURE_SCHEME_PREFERENCE - .iter() - .copied() - .filter(move |scheme| match algorithm { - rustls::SignatureAlgorithm::ED25519 => *scheme == SignatureScheme::ED25519, - rustls::SignatureAlgorithm::ECDSA => matches!( - scheme, - SignatureScheme::ECDSA_NISTP256_SHA256 | SignatureScheme::ECDSA_NISTP384_SHA384 - ), - rustls::SignatureAlgorithm::RSA => matches!( - scheme, - SignatureScheme::RSA_PSS_SHA256 - | SignatureScheme::RSA_PSS_SHA384 - | SignatureScheme::RSA_PSS_SHA512 - | SignatureScheme::RSA_PKCS1_SHA256 - | SignatureScheme::RSA_PKCS1_SHA384 - | SignatureScheme::RSA_PKCS1_SHA512 - ), - _ => true, - }) -} - #[derive(Debug, Snafu)] #[snafu(module)] pub enum SignEndpointError { @@ -68,8 +31,6 @@ pub enum SignEndpointError { Sign { source: dhttp_identity::identity::SignError, }, - #[snafu(display("no supported signature scheme for endpoint address"))] - NoSupportedScheme, } /// EndpointAddress record (Type E = 266) @@ -83,7 +44,7 @@ pub enum SignEndpointError { /// +-------+-----------------+--------------------+----------------+----------------------------+ /// | flags | sequence(varint)| addr | load(optional) | signature (optional) | /// +-------+-----------------+--------------------+----------------+----------------------------+ -/// | u8 | QUIC varint | see addr layout | f32 | scheme(u16)+len(varint)+N | +/// | u8 | QUIC varint | see addr layout | f32 | len(varint)+N | /// +-------+-----------------+--------------------+----------------+----------------------------+ /// /// addr layout: @@ -114,7 +75,6 @@ pub enum SignEndpointError { /// #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct EndpointSignature { - scheme: u16, signature: Vec, } @@ -259,21 +219,12 @@ impl EndpointAddr { ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); - for scheme in signature_schemes_for_algorithm(authority.sign_algorithm()) { - match authority.sign(scheme, &data).await { - Ok(signature) => { - self.signature = Some(EndpointSignature { - scheme: u16::from(scheme), - signature, - }); - return Ok(()); - } - Err(dhttp_identity::identity::SignError::UnsupportedScheme { .. }) => {} - Err(source) => return Err(source).context(sign_endpoint_error::SignSnafu), - } - } - - sign_endpoint_error::NoSupportedSchemeSnafu.fail() + let signature = authority + .sign(&data) + .await + .context(sign_endpoint_error::SignSnafu)?; + self.signature = Some(EndpointSignature { signature }); + Ok(()) } pub fn verify_signature( @@ -284,12 +235,7 @@ impl EndpointAddr { return Ok(false); }; let data = self.signed_data(); - sigin::verify( - spki, - SignatureScheme::from(sig.scheme), - &data, - &sig.signature, - ) + sigin::verify(spki, &data, &sig.signature) } pub fn verify_signature_from_der(&self, cert_der: &[u8]) -> Result { @@ -381,7 +327,7 @@ impl EndpointAddr { { let sig_len = VarInt::try_from(sig.signature.len() as u64).unwrap_or(VarInt::from_u32(0)); - meta_len += 2 + sig_len.encoding_size() + sig.signature.len(); + meta_len += sig_len.encoding_size() + sig.signature.len(); } let addr_len = match (self.is_ipv6(), self.is_nat()) { @@ -481,7 +427,6 @@ impl WriteEndpointAddr for B { if endpoint.is_signed() && let Some(sig) = endpoint.signature() { - self.put_u16(sig.scheme); let len = VarInt::try_from(sig.signature.len() as u64).unwrap_or(VarInt::from_u32(0)); self.put_varint(len); self.put_slice(&sig.signature); @@ -659,15 +604,13 @@ fn be_endpoint_signature(input: &[u8], flags: u8) -> IResult<&[u8], Option rustls::SignatureAlgorithm { - rustls::SignatureAlgorithm::ED25519 - } - fn sign( &self, - scheme: SignatureScheme, data: &[u8], ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { - let result = dhttp_identity::identity::sign_with_key(self, scheme, data); + let result = dhttp_identity::identity::sign_with_key(self, data); Box::pin(std::future::ready(result)) } } @@ -1078,11 +1019,11 @@ mod tests { } #[test] - fn sign_with_authority_tries_next_supported_scheme() { + fn sign_with_authority_stores_canonical_signature() { #[derive(Debug)] - struct FallbackAuthority; + struct StaticAuthority; - impl dhttp_identity::identity::LocalAuthority for FallbackAuthority { + impl dhttp_identity::identity::LocalAuthority for StaticAuthority { fn name(&self) -> &str { "authority.example" } @@ -1091,35 +1032,18 @@ mod tests { &[] } - fn sign_algorithm(&self) -> rustls::SignatureAlgorithm { - rustls::SignatureAlgorithm::ECDSA - } - fn sign( &self, - scheme: SignatureScheme, _data: &[u8], ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { - Box::pin(async move { - match scheme { - SignatureScheme::ECDSA_NISTP256_SHA256 => { - Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }) - } - SignatureScheme::ECDSA_NISTP384_SHA384 => Ok(vec![1, 2, 3]), - _ => Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }), - } - }) + Box::pin(std::future::ready(Ok(vec![1, 2, 3]))) } } let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); - futures::executor::block_on(ep.sign_with_authority(&FallbackAuthority)).unwrap(); + futures::executor::block_on(ep.sign_with_authority(&StaticAuthority)).unwrap(); let signature = ep.signature().unwrap(); - assert_eq!( - SignatureScheme::from(signature.scheme), - SignatureScheme::ECDSA_NISTP384_SHA384 - ); assert_eq!(signature.signature, vec![1, 2, 3]); } diff --git a/src/core/parser/sigin.rs b/src/core/parser/sigin.rs index 395b727..b6d8ed7 100644 --- a/src/core/parser/sigin.rs +++ b/src/core/parser/sigin.rs @@ -1,30 +1,22 @@ -use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer, sign::SigningKey}; -use snafu::Snafu; -use x509_parser::prelude::FromDer; +use rustls::{pki_types::SubjectPublicKeyInfoDer, sign::SigningKey}; +use snafu::{ResultExt, Snafu}; #[derive(Debug, Snafu)] #[snafu(module)] pub enum SignError { - #[snafu(display("unsupported signature scheme {scheme:?}"))] - UnsupportedScheme { scheme: SignatureScheme }, - #[snafu(display("cryptographic operation failed"))] - Crypto { - #[snafu(source(false))] - source: rustls::Error, + #[snafu(display("failed to sign DHTTP identity data"))] + Identity { + source: dhttp_identity::identity::SignError, }, } -impl From for SignError { - fn from(source: rustls::Error) -> Self { - Self::Crypto { source } - } -} - #[derive(Debug, Snafu)] #[snafu(module)] pub enum VerifyError { - #[snafu(display("unsupported signature scheme {scheme:?}"))] - UnsupportedScheme { scheme: SignatureScheme }, + #[snafu(display("failed to verify DHTTP identity signature"))] + Identity { + source: dhttp_identity::identity::VerifyError, + }, #[snafu(display("invalid certificate: {details}"))] InvalidCertificate { details: String }, #[snafu(display("invalid PEM"))] @@ -35,44 +27,15 @@ pub enum VerifyError { Io { source: std::io::Error }, } -pub fn sign_with_key( - key: &(impl SigningKey + ?Sized), - scheme: SignatureScheme, - data: &[u8], -) -> Result, SignError> { - let signer = key - .choose_scheme(&[scheme]) - .ok_or(SignError::UnsupportedScheme { scheme })?; - Ok(signer.sign(data)?) +pub fn sign_with_key(key: &(impl SigningKey + ?Sized), data: &[u8]) -> Result, SignError> { + dhttp_identity::identity::sign_with_key(key, data).context(sign_error::IdentitySnafu) } pub(crate) fn verify( spki: SubjectPublicKeyInfoDer, - scheme: SignatureScheme, data: &[u8], signature: &[u8], ) -> Result { - let algorithm: &'static dyn ring::signature::VerificationAlgorithm = match scheme { - SignatureScheme::ECDSA_NISTP384_SHA384 => &ring::signature::ECDSA_P384_SHA384_ASN1, - SignatureScheme::ECDSA_NISTP256_SHA256 => &ring::signature::ECDSA_P256_SHA256_ASN1, - SignatureScheme::ED25519 => &ring::signature::ED25519, - SignatureScheme::RSA_PKCS1_SHA256 => &ring::signature::RSA_PKCS1_2048_8192_SHA256, - SignatureScheme::RSA_PKCS1_SHA384 => &ring::signature::RSA_PKCS1_2048_8192_SHA384, - SignatureScheme::RSA_PKCS1_SHA512 => &ring::signature::RSA_PKCS1_2048_8192_SHA512, - SignatureScheme::RSA_PSS_SHA256 => &ring::signature::RSA_PSS_2048_8192_SHA512, - SignatureScheme::RSA_PSS_SHA384 => &ring::signature::RSA_PSS_2048_8192_SHA384, - SignatureScheme::RSA_PSS_SHA512 => &ring::signature::RSA_PSS_2048_8192_SHA512, - _ => return Err(VerifyError::UnsupportedScheme { scheme }), - }; - - let public_key = match x509_parser::x509::SubjectPublicKeyInfo::from_der(&spki) { - Ok((_remain, spki)) => spki.subject_public_key, - Err(_error) => unreachable!("rustls returned an invalid peer_certificates."), - }; - - Ok( - ring::signature::UnparsedPublicKey::new(algorithm, public_key) - .verify(data, signature) - .is_ok(), - ) + dhttp_identity::identity::verify_signature(spki, data, signature) + .context(verify_error::IdentitySnafu) } diff --git a/src/publisher.rs b/src/publisher.rs index 9f3128e..7160f11 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -531,7 +531,7 @@ mod tests { use dquic::qresolve::{ResolveFuture, Source}; use futures::{FutureExt, StreamExt, future::BoxFuture, stream}; - use rustls::{SignatureAlgorithm, SignatureScheme, pki_types::CertificateDer}; + use rustls::pki_types::CertificateDer; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; @@ -548,21 +548,15 @@ mod tests { &[] } - fn sign_algorithm(&self) -> SignatureAlgorithm { - SignatureAlgorithm::ED25519 - } - fn sign( &self, - scheme: SignatureScheme, _data: &[u8], ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { - Box::pin(async move { - match scheme { - SignatureScheme::ED25519 => Ok(vec![1, 2, 3]), - _ => Err(dhttp_identity::identity::SignError::UnsupportedScheme { scheme }), - } - }) + // Match the Ed25519 signature length used by DHTTP's canonical + // key-to-scheme policy. Short fake signatures can collide with + // legacy E-record fixed RDLENGTH values during parser + // compatibility dispatch. + Box::pin(async move { Ok(vec![0x2a; 64]) }) } } From fb05dbd4a699be3d9245ad62285220fa8131f9c5 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 7 Jun 2026 00:06:01 +0800 Subject: [PATCH 66/85] fix: update h3x hyper facade imports --- src/bin/ddns-server/lookup.rs | 2 +- src/bin/ddns-server/main.rs | 2 +- src/resolvers/h3.rs | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bin/ddns-server/lookup.rs b/src/bin/ddns-server/lookup.rs index afce2b0..e8c2616 100644 --- a/src/bin/ddns-server/lookup.rs +++ b/src/bin/ddns-server/lookup.rs @@ -10,7 +10,7 @@ use ddns::core::{ wire::MultiResponse, }; use deadpool_redis::redis::{self, AsyncCommands}; -use h3x::message::stream::MessageStreamError; +use h3x::dhttp::message::MessageStreamError; use http_body_util::{Full, combinators::UnsyncBoxBody}; use tracing::debug; diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index ee4071b..fdcfc0e 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -25,7 +25,7 @@ use h3x::{ server::ServerQuicConfig, }, endpoint::H3Endpoint, - hyper::server::TowerService, + hyper::TowerService, }; use rustls::{RootCertStore, server::WebPkiClientVerifier}; use tracing::{info, level_filters::LevelFilter}; diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index ce419f0..b85bc98 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -7,8 +7,7 @@ use dquic::{ }; use futures::{StreamExt, stream}; use h3x::{ - dquic::ConnectError, endpoint::H3Endpoint, hyper::client::RequestError as HyperRequestError, - quic, + dquic::ConnectError, endpoint::H3Endpoint, hyper::RequestError as HyperRequestError, quic, }; use http_body_util::{BodyExt, Empty, Full}; use tokio::time::Instant; @@ -51,7 +50,7 @@ impl fmt::Display for H3Resolver { pub enum Error { #[snafu(display("h3 stream error"))] H3Stream { - source: h3x::endpoint::server::MessageStreamError, + source: h3x::dhttp::message::MessageStreamError, }, #[snafu(display("failed to connect h3 endpoint"))] Connect { source: h3x::pool::ConnectError }, @@ -130,7 +129,7 @@ where >, ) -> Result< http::Response< - impl http_body::Body, + impl http_body::Body, >, Error, > { From 82809575f93069cfba3a793d33450d2f32ae99ab Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 8 Jun 2026 10:42:13 +0800 Subject: [PATCH 67/85] ci: rename publish workflow titles --- .github/workflows/publish-crates.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index ab2a4b5..59312cc 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -1,4 +1,4 @@ -name: Release crates.io +name: Publish crates.io on: pull_request: From 57083dde3d8c6c3838a9a3348ef9ebd73e526315 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 8 Jun 2026 13:34:52 +0800 Subject: [PATCH 68/85] fix: encode endpoint signature scheme --- src/core/parser/record/endpoint.rs | 184 ++++++++++++++++++++++++++--- src/core/parser/sigin.rs | 77 +++++++++++- src/publisher.rs | 11 +- 3 files changed, 251 insertions(+), 21 deletions(-) diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index 9d6956d..8d91cd9 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -16,7 +16,7 @@ use nom::{ error::{ErrorKind, make_error}, number::streaming::{be_u8, be_u16, be_u32, be_u128}, }; -use rustls::pki_types::SubjectPublicKeyInfoDer; +use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer}; use snafu::{ResultExt, Snafu}; use crate::core::parser::{ @@ -27,6 +27,8 @@ use crate::core::parser::{ #[derive(Debug, Snafu)] #[snafu(module)] pub enum SignEndpointError { + #[snafu(display("failed to determine endpoint signature scheme"))] + SignatureScheme { source: sigin::SignatureSchemeError }, #[snafu(display("failed to sign endpoint address"))] Sign { source: dhttp_identity::identity::SignError, @@ -44,7 +46,7 @@ pub enum SignEndpointError { /// +-------+-----------------+--------------------+----------------+----------------------------+ /// | flags | sequence(varint)| addr | load(optional) | signature (optional) | /// +-------+-----------------+--------------------+----------------+----------------------------+ -/// | u8 | QUIC varint | see addr layout | f32 | len(varint)+N | +/// | u8 | QUIC varint | see addr layout | f32 | scheme(u16)+len(varint)+N | /// +-------+-----------------+--------------------+----------------+----------------------------+ /// /// addr layout: @@ -75,6 +77,7 @@ pub enum SignEndpointError { /// #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct EndpointSignature { + scheme: u16, signature: Vec, } @@ -219,11 +222,16 @@ impl EndpointAddr { ) -> Result<(), SignEndpointError> { self.set_signed(true); let data = self.signed_data(); + let scheme = sigin::signature_scheme(authority.public_key()) + .context(sign_endpoint_error::SignatureSchemeSnafu)?; let signature = authority .sign(&data) .await .context(sign_endpoint_error::SignSnafu)?; - self.signature = Some(EndpointSignature { signature }); + self.signature = Some(EndpointSignature { + scheme: u16::from(scheme), + signature, + }); Ok(()) } @@ -235,7 +243,12 @@ impl EndpointAddr { return Ok(false); }; let data = self.signed_data(); - sigin::verify(spki, &data, &sig.signature) + sigin::verify( + spki, + SignatureScheme::from(sig.scheme), + &data, + &sig.signature, + ) } pub fn verify_signature_from_der(&self, cert_der: &[u8]) -> Result { @@ -327,7 +340,7 @@ impl EndpointAddr { { let sig_len = VarInt::try_from(sig.signature.len() as u64).unwrap_or(VarInt::from_u32(0)); - meta_len += sig_len.encoding_size() + sig.signature.len(); + meta_len += 2 + sig_len.encoding_size() + sig.signature.len(); } let addr_len = match (self.is_ipv6(), self.is_nat()) { @@ -427,6 +440,7 @@ impl WriteEndpointAddr for B { if endpoint.is_signed() && let Some(sig) = endpoint.signature() { + self.put_u16(sig.scheme); let len = VarInt::try_from(sig.signature.len() as u64).unwrap_or(VarInt::from_u32(0)); self.put_varint(len); self.put_slice(&sig.signature); @@ -604,13 +618,15 @@ fn be_endpoint_signature(input: &[u8], flags: u8) -> IResult<&[u8], Option Vec { + let mut spki = Vec::with_capacity(44); + spki.extend_from_slice(&[ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, + ]); + spki.extend_from_slice(public_key); + spki + } + #[test] fn legacy_endpoint_v4_direct_without_meta() { let port = 5353u16; @@ -931,9 +956,49 @@ mod tests { } #[test] - fn endpoint_signature_roundtrip_and_verify() { + fn signed_endpoint_accepts_scheme_inclusive_signature() { + let addr = SocketAddrV4::new(Ipv4Addr::new(10, 10, 0, 7), 20004); + let scheme = u16::from(SignatureScheme::ED25519); + let signature = vec![0xaa; 64]; + let sig_len = VarInt::try_from(signature.len() as u64).unwrap(); + + let mut buf = BytesMut::new(); + buf.put_u8(EndpointAddr::FLAG_SIGNED); + buf.put_socket_addr_v4(&addr); + buf.put_u16(scheme); + buf.put_varint(sig_len); + buf.extend_from_slice(&signature); + + let (remain, decoded) = be_endpoint_addr(&buf).unwrap(); + + assert!(remain.is_empty()); + assert!(decoded.is_signed()); + assert_eq!(decoded.addr(), SocketAddr::V4(addr)); + assert_eq!(decoded.signature().unwrap().signature, signature); + } + + #[test] + fn signed_endpoint_rejects_signature_without_scheme() { + let addr = SocketAddrV4::new(Ipv4Addr::new(10, 10, 0, 7), 20004); + let signature = vec![0xaa; 64]; + let sig_len = VarInt::try_from(signature.len() as u64).unwrap(); + + let mut buf = BytesMut::new(); + buf.put_u8(EndpointAddr::FLAG_SIGNED); + buf.put_socket_addr_v4(&addr); + buf.put_varint(sig_len); + buf.extend_from_slice(&signature); + + assert!(be_endpoint_addr(&buf).is_err()); + } + + #[test] + fn signed_endpoint_writes_actual_scheme_before_signature_length() { #[derive(Debug)] - struct Ed25519Key(Arc); + struct Ed25519Key { + keypair: Arc, + spki: Vec, + } #[derive(Debug)] struct Ed25519Signer(Arc); @@ -952,7 +1017,7 @@ mod tests { fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { offered .contains(&SignatureScheme::ED25519) - .then(|| Box::new(Ed25519Signer(self.0.clone())) as Box) + .then(|| Box::new(Ed25519Signer(self.keypair.clone())) as Box) } fn algorithm(&self) -> rustls::SignatureAlgorithm { @@ -969,6 +1034,10 @@ mod tests { &[] } + fn public_key(&self) -> SubjectPublicKeyInfoDer<'_> { + SubjectPublicKeyInfoDer::from(self.spki.as_slice()) + } + fn sign( &self, data: &[u8], @@ -982,13 +1051,84 @@ mod tests { let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); let keypair = Arc::new(ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()); - let key = Ed25519Key(keypair.clone()); + let spki = ed25519_spki(keypair.public_key().as_ref()); + let key = Ed25519Key { keypair, spki }; - let mut spki = Vec::with_capacity(44); - spki.extend_from_slice(&[ - 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, - ]); - spki.extend_from_slice(keypair.public_key().as_ref()); + let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); + futures::executor::block_on(ep.sign_with_authority(&key)).unwrap(); + + let mut buf = BytesMut::new(); + buf.put_endpoint_addr(&ep); + + let scheme_offset = 1 + 2 + 4; + let encoded_scheme = u16::from_be_bytes([buf[scheme_offset], buf[scheme_offset + 1]]); + assert_eq!(encoded_scheme, u16::from(SignatureScheme::ED25519)); + } + + #[test] + fn endpoint_signature_roundtrip_and_verify() { + #[derive(Debug)] + struct Ed25519Key { + keypair: Arc, + spki: Vec, + } + + #[derive(Debug)] + struct Ed25519Signer(Arc); + + impl Signer for Ed25519Signer { + fn sign(&self, message: &[u8]) -> Result, rustls::Error> { + Ok(self.0.sign(message).as_ref().to_vec()) + } + + fn scheme(&self) -> SignatureScheme { + SignatureScheme::ED25519 + } + } + + impl SigningKey for Ed25519Key { + fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { + offered + .contains(&SignatureScheme::ED25519) + .then(|| Box::new(Ed25519Signer(self.keypair.clone())) as Box) + } + + fn algorithm(&self) -> rustls::SignatureAlgorithm { + rustls::SignatureAlgorithm::ED25519 + } + } + + impl dhttp_identity::identity::LocalAuthority for Ed25519Key { + fn name(&self) -> &str { + "authority.example" + } + + fn cert_chain(&self) -> &[rustls::pki_types::CertificateDer<'static>] { + &[] + } + + fn public_key(&self) -> SubjectPublicKeyInfoDer<'_> { + SubjectPublicKeyInfoDer::from(self.spki.as_slice()) + } + + fn sign( + &self, + data: &[u8], + ) -> BoxFuture<'_, Result, dhttp_identity::identity::SignError>> { + let result = dhttp_identity::identity::sign_with_key(self, data); + Box::pin(std::future::ready(result)) + } + } + + let rng = ring::rand::SystemRandom::new(); + let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); + let keypair = + Arc::new(ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()); + let spki = ed25519_spki(keypair.public_key().as_ref()); + let key = Ed25519Key { + keypair: keypair.clone(), + spki: spki.clone(), + }; let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); let mut ep = EndpointAddr::direct_v4(addr); @@ -1021,7 +1161,9 @@ mod tests { #[test] fn sign_with_authority_stores_canonical_signature() { #[derive(Debug)] - struct StaticAuthority; + struct StaticAuthority { + spki: Vec, + } impl dhttp_identity::identity::LocalAuthority for StaticAuthority { fn name(&self) -> &str { @@ -1032,6 +1174,10 @@ mod tests { &[] } + fn public_key(&self) -> SubjectPublicKeyInfoDer<'_> { + SubjectPublicKeyInfoDer::from(self.spki.as_slice()) + } + fn sign( &self, _data: &[u8], @@ -1041,9 +1187,13 @@ mod tests { } let mut ep = EndpointAddr::direct_v4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5353)); - futures::executor::block_on(ep.sign_with_authority(&StaticAuthority)).unwrap(); + let authority = StaticAuthority { + spki: ed25519_spki(&[0; 32]), + }; + futures::executor::block_on(ep.sign_with_authority(&authority)).unwrap(); let signature = ep.signature().unwrap(); + assert_eq!(signature.scheme, u16::from(SignatureScheme::ED25519)); assert_eq!(signature.signature, vec![1, 2, 3]); } diff --git a/src/core/parser/sigin.rs b/src/core/parser/sigin.rs index b6d8ed7..597d90e 100644 --- a/src/core/parser/sigin.rs +++ b/src/core/parser/sigin.rs @@ -1,5 +1,13 @@ -use rustls::{pki_types::SubjectPublicKeyInfoDer, sign::SigningKey}; +use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer, sign::SigningKey}; use snafu::{ResultExt, Snafu}; +use x509_parser::{ + oid_registry::{ + OID_EC_P256, OID_KEY_TYPE_EC_PUBLIC_KEY, OID_NIST_EC_P384, OID_PKCS1_RSAENCRYPTION, + OID_SIG_ED25519, + }, + prelude::FromDer, + x509::SubjectPublicKeyInfo, +}; #[derive(Debug, Snafu)] #[snafu(module)] @@ -17,6 +25,8 @@ pub enum VerifyError { Identity { source: dhttp_identity::identity::VerifyError, }, + #[snafu(display("unsupported signature scheme {scheme:?}"))] + UnsupportedScheme { scheme: SignatureScheme }, #[snafu(display("invalid certificate: {details}"))] InvalidCertificate { details: String }, #[snafu(display("invalid PEM"))] @@ -27,15 +37,76 @@ pub enum VerifyError { Io { source: std::io::Error }, } +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum SignatureSchemeError { + #[snafu(display("unsupported public key type"))] + UnsupportedKey, +} + pub fn sign_with_key(key: &(impl SigningKey + ?Sized), data: &[u8]) -> Result, SignError> { dhttp_identity::identity::sign_with_key(key, data).context(sign_error::IdentitySnafu) } +pub(crate) fn signature_scheme( + spki: SubjectPublicKeyInfoDer<'_>, +) -> Result { + let Ok((_remain, spki)) = SubjectPublicKeyInfo::from_der(spki.as_ref()) else { + return signature_scheme_error::UnsupportedKeySnafu.fail(); + }; + + if spki.algorithm.algorithm == OID_SIG_ED25519 { + return Ok(SignatureScheme::ED25519); + } + + if spki.algorithm.algorithm == OID_PKCS1_RSAENCRYPTION { + return Ok(SignatureScheme::RSA_PSS_SHA512); + } + + if spki.algorithm.algorithm != OID_KEY_TYPE_EC_PUBLIC_KEY { + return signature_scheme_error::UnsupportedKeySnafu.fail(); + } + + let Some(curve) = spki + .algorithm + .parameters + .as_ref() + .and_then(|parameters| parameters.as_oid().ok()) + else { + return signature_scheme_error::UnsupportedKeySnafu.fail(); + }; + + if curve == OID_EC_P256 { + Ok(SignatureScheme::ECDSA_NISTP256_SHA256) + } else if curve == OID_NIST_EC_P384 { + Ok(SignatureScheme::ECDSA_NISTP384_SHA384) + } else { + signature_scheme_error::UnsupportedKeySnafu.fail() + } +} + pub(crate) fn verify( spki: SubjectPublicKeyInfoDer, + scheme: SignatureScheme, data: &[u8], signature: &[u8], ) -> Result { - dhttp_identity::identity::verify_signature(spki, data, signature) - .context(verify_error::IdentitySnafu) + let algorithm: &'static dyn ring::signature::VerificationAlgorithm = match scheme { + SignatureScheme::ECDSA_NISTP384_SHA384 => &ring::signature::ECDSA_P384_SHA384_ASN1, + SignatureScheme::ECDSA_NISTP256_SHA256 => &ring::signature::ECDSA_P256_SHA256_ASN1, + SignatureScheme::ED25519 => &ring::signature::ED25519, + SignatureScheme::RSA_PSS_SHA512 => &ring::signature::RSA_PSS_2048_8192_SHA512, + _ => return verify_error::UnsupportedSchemeSnafu { scheme }.fail(), + }; + + let public_key = match SubjectPublicKeyInfo::from_der(spki.as_ref()) { + Ok((_remain, spki)) => spki.subject_public_key, + Err(_) => return Ok(false), + }; + + Ok( + ring::signature::UnparsedPublicKey::new(algorithm, public_key) + .verify(data, signature) + .is_ok(), + ) } diff --git a/src/publisher.rs b/src/publisher.rs index 7160f11..bdb741a 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -531,7 +531,7 @@ mod tests { use dquic::qresolve::{ResolveFuture, Source}; use futures::{FutureExt, StreamExt, future::BoxFuture, stream}; - use rustls::pki_types::CertificateDer; + use rustls::pki_types::{CertificateDer, SubjectPublicKeyInfoDer}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; @@ -539,6 +539,11 @@ mod tests { #[derive(Debug)] struct TestAuthority; + const ED25519_TEST_SPKI: [u8; 44] = [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + impl LocalAuthority for TestAuthority { fn name(&self) -> &str { "authority.example" @@ -548,6 +553,10 @@ mod tests { &[] } + fn public_key(&self) -> SubjectPublicKeyInfoDer<'_> { + SubjectPublicKeyInfoDer::from(ED25519_TEST_SPKI.as_slice()) + } + fn sign( &self, _data: &[u8], From b807fc5ff1850c704ce045d56a1fc9a86c11ef19 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 8 Jun 2026 14:06:53 +0800 Subject: [PATCH 69/85] test(resolvers): document h3 endpoint port semantics --- src/resolvers.rs | 8 ++++++++ src/resolvers/h3.rs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/resolvers.rs b/src/resolvers.rs index 63bbfd4..99843fa 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -304,6 +304,14 @@ mod tests { ); } + #[test] + fn resolvable_name_accepts_stun_authority_with_numeric_port() { + assert_eq!( + resolvable_name("nat.genmeta.net:20004"), + Some("nat.genmeta.net") + ); + } + #[test] fn resolvable_name_rejects_ip_literals() { assert_eq!(resolvable_name("127.0.0.1:443"), None); diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index b85bc98..5914d9b 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -467,4 +467,27 @@ mod tests { EndpointAddr::direct("192.0.2.53:4433".parse().unwrap()) ); } + + #[tokio::test] + async fn cached_lookup_uses_e_record_port_not_input_port() { + let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new( + h3x::dquic::QuicEndpoint::builder().build().await, + )); + let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint).unwrap(); + resolver.cached_records.insert( + "nat.genmeta.net".to_owned(), + Record { + addrs: vec![EndpointAddr::direct("192.0.2.10:21000".parse().unwrap())], + expire: Instant::now() + Duration::from_secs(60), + }, + ); + + let mut records = resolver.lookup("nat.genmeta.net:20004").await.unwrap(); + let (_source, endpoint) = records.next().await.unwrap(); + + assert_eq!( + endpoint, + EndpointAddr::direct("192.0.2.10:21000".parse().unwrap()) + ); + } } From c600308b54aa7aa2150343c9a9834cb89a31cad4 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 8 Jun 2026 15:40:11 +0800 Subject: [PATCH 70/85] feat: publish explicit dns host endpoints --- src/publisher.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 5 deletions(-) diff --git a/src/publisher.rs b/src/publisher.rs index bdb741a..5e736e7 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -153,6 +153,29 @@ impl Publisher { Ok(()) } + pub async fn publish_once_for( + &self, + host: &str, + endpoints: &[EndpointAddr], + ) -> Result<(), PublishOnceError> { + let mut published = false; + tracing::debug!( + host, + endpoint_count = endpoints.len(), + endpoints = ?endpoints, + "publishing explicit endpoints" + ); + published |= self + .publish_supplied_endpoints_to_resolver(self.resolver.as_ref(), host, endpoints) + .await?; + + if !published { + return publish_once_error::NoPublisherResolverSnafu.fail(); + } + + Ok(()) + } + pub async fn run(&self) -> ! { let mut locations = self.network.quic().locations().subscribe(); let interval = tokio::time::sleep(self.interval); @@ -275,6 +298,28 @@ impl Publisher { .await } + async fn publish_supplied_endpoints_to_resolver( + &self, + resolver: &(dyn Resolve + Send + Sync), + host: &str, + endpoints: &[EndpointAddr], + ) -> Result { + let any: &dyn Any = resolver; + + if let Some(resolvers) = any.downcast_ref::() { + let mut published = false; + for resolver in resolvers.iter() { + published |= self + .publish_single_resolver_for(resolver.as_ref(), host, endpoints) + .await?; + } + return Ok(published); + } + + self.publish_single_resolver_for(resolver, host, endpoints) + .await + } + fn clear_publish_state(&self) { Self::clear_resolver_publish_state(self.resolver.as_ref()); } @@ -334,29 +379,94 @@ impl Publisher { Ok(false) } + async fn publish_single_resolver_for( + &self, + resolver: &(dyn Resolve + Send + Sync), + host: &str, + endpoints: &[EndpointAddr], + ) -> Result { + #[cfg(not(any( + feature = "http-resolver", + feature = "h3x-resolver", + feature = "mdns-resolver" + )))] + { + let _ = host; + let _ = endpoints; + } + + let any: &dyn Any = resolver; + + #[cfg(feature = "http-resolver")] + if let Some(http) = any.downcast_ref::() { + self.publish_endpoints_for(http, host, endpoints).await?; + return Ok(true); + } + + #[cfg(feature = "h3x-resolver")] + if let Some(h3) = + any.downcast_ref::>() + { + self.publish_endpoints_for(h3, host, endpoints).await?; + return Ok(true); + } + + #[cfg(feature = "mdns-resolver")] + if let Some(mdns) = any.downcast_ref::() { + let mut published = false; + for bound in mdns.bound_resolvers() { + self.publish_endpoints_for(&bound.resolver, host, endpoints) + .await?; + published = true; + } + return Ok(published); + } + + Ok(false) + } + async fn publish_endpoints( &self, publisher: &(dyn Publish + Send + Sync), endpoints: &[EndpointAddr], ) -> Result<(), PublishOnceError> { - let packet = self.signed_packet(endpoints).await?; - let name = self.identity.name(); + self.publish_endpoints_for(publisher, self.identity.name(), endpoints) + .await + } + + async fn publish_endpoints_for( + &self, + publisher: &(dyn Publish + Send + Sync), + host: &str, + endpoints: &[EndpointAddr], + ) -> Result<(), PublishOnceError> { + let packet = self.signed_packet_for(host, endpoints).await?; tracing::debug!( publisher = %publisher, - name, + host, endpoint_count = endpoints.len(), packet_len = packet.len(), "publishing dns packet" ); publisher - .publish(name, &packet) + .publish(host, &packet) .await .context(publish_once_error::PublishSnafu { publisher: publisher.to_string(), }) } + #[cfg(test)] async fn signed_packet(&self, endpoints: &[EndpointAddr]) -> Result, PublishOnceError> { + self.signed_packet_for(self.identity.name(), endpoints) + .await + } + + async fn signed_packet_for( + &self, + host: &str, + endpoints: &[EndpointAddr], + ) -> Result, PublishOnceError> { let mut signed = Vec::with_capacity(endpoints.len()); for endpoint in endpoints { let mut endpoint = DnsEndpointAddr::try_from(*endpoint) @@ -373,7 +483,7 @@ impl Publisher { } let mut hosts = HashMap::new(); - hosts.insert(self.identity.name().to_owned(), signed); + hosts.insert(host.to_owned(), signed); Ok(MdnsPacket::answer(0, &hosts).to_bytes()) } @@ -597,6 +707,24 @@ mod tests { assert!(matches!(error, PublishOnceError::NoPublisherResolver)); } + #[tokio::test] + async fn publish_once_for_reports_no_publisher_resolver() { + let publisher = Publisher::new( + Arc::new(TestAuthority), + h3x::dquic::Network::builder().build(), + Arc::new(DisplayOnlyResolver), + Arc::new(Vec::new()), + ); + let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + + let error = publisher + .publish_once_for("nat.genmeta.net", &[endpoint]) + .await + .unwrap_err(); + + assert!(matches!(error, PublishOnceError::NoPublisherResolver)); + } + #[tokio::test] async fn publisher_timeout_is_configurable() { let publisher = Publisher::new( @@ -635,6 +763,33 @@ mod tests { assert!(endpoint.is_signed()); } + #[tokio::test] + async fn signed_packet_for_uses_supplied_host_and_keeps_signature_and_options() { + let publisher = Publisher::new( + Arc::new(TestAuthority), + h3x::dquic::Network::builder().build(), + Arc::new(DisplayOnlyResolver), + Arc::new(Vec::new()), + ) + .with_options(PublishOptions { server_id: Some(2) }); + + let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + let packet = publisher + .signed_packet_for("nat.genmeta.net", &[endpoint]) + .await + .unwrap(); + let (_remain, packet) = crate::core::parser::packet::be_packet(&packet).unwrap(); + let record = packet.answers.first().expect("endpoint answer"); + let crate::core::parser::record::RData::E(endpoint) = record.data() else { + panic!("expected endpoint record"); + }; + + assert_eq!(record.name().to_string(), "nat.genmeta.net"); + assert!(!endpoint.is_main()); + assert!(endpoint.is_clustered()); + assert!(endpoint.is_signed()); + } + #[tokio::test] async fn public_endpoints_do_not_fall_back_to_local_bound_addresses() { let network = h3x::dquic::Network::builder().build(); From 550005d04bd47286fbf477591b8c7cd688b61a2c Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 8 Jun 2026 16:43:30 +0800 Subject: [PATCH 71/85] feat(identity): use dhttp suffix in dns examples --- README.md | 8 ++++---- examples/README.md | 8 ++++---- examples/mdns_discover.rs | 4 ++-- examples/mdns_query.rs | 2 +- src/bin/ddns-server/error.rs | 23 +++++++++++++++++++++-- src/bin/ddns-server/policy.rs | 2 +- src/bin/ddns-server/publish.rs | 4 ++-- src/mdns/service.rs | 21 +++++++++++++++++++-- src/resolvers.rs | 4 ++-- src/resolvers/h3.rs | 4 ++-- 10 files changed, 58 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6085e8e..2f16ceb 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ Publish DNS service records to an HTTP/3 DNS server: ```bash cargo run --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ - --client-name demo.example.genmeta.net \ - --client-cert /path/to/demo.example.genmeta.net.pem \ - --client-key /path/to/demo.example.genmeta.net.key \ - --host demo.example.genmeta.net \ + --client-name demo.example.dhttp.net \ + --client-cert /path/to/demo.example.dhttp.net.pem \ + --client-key /path/to/demo.example.dhttp.net.key \ + --host demo.example.dhttp.net \ --addr 192.168.1.100:8080 ``` diff --git a/examples/README.md b/examples/README.md index 36e0b0c..5df706f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -57,10 +57,10 @@ Use the `publish` example to publish a DNS service record to the HTTP/3 DNS serv ```bash cargo run --example publish --features="h3x-resolver" \ --server-ca /path/to/root.crt \ - --client-name demo.example.genmeta.net \ - --client-cert /path/to/demo.example.genmeta.net.pem \ - --client-key /path/to/demo.example.genmeta.net.key \ - --host demo.example.genmeta.net \ + --client-name demo.example.dhttp.net \ + --client-cert /path/to/demo.example.dhttp.net.pem \ + --client-key /path/to/demo.example.dhttp.net.key \ + --host demo.example.dhttp.net \ --addr 192.168.1.100:8080,192.168.1.101:8080 ``` diff --git a/examples/mdns_discover.rs b/examples/mdns_discover.rs index beb3771..ce6eb12 100644 --- a/examples/mdns_discover.rs +++ b/examples/mdns_discover.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Error> { let args = Args::parse(); let mdns = Mdns::new(DHTTP_MDNS_SERVICE, args.ip, &args.device)?; mdns.insert_host( - "test.genmeta.net".to_string(), + "test.dhttp.net".to_string(), vec![ { let addr: SocketAddr = "192.168.1.7:7000".parse().unwrap(); @@ -44,7 +44,7 @@ async fn main() -> Result<(), Error> { ); mdns.insert_host( - "mdns.test.genmeta.net".to_string(), + "mdns.test.dhttp.net".to_string(), vec![ { let addr: SocketAddr = "192.168.1.7:7001".parse().unwrap(); diff --git a/examples/mdns_query.rs b/examples/mdns_query.rs index ac8c839..16fd3a6 100644 --- a/examples/mdns_query.rs +++ b/examples/mdns_query.rs @@ -18,7 +18,7 @@ async fn main() -> Result<(), Error> { let args = Args::parse(); let mdns = Mdns::new(DHTTP_MDNS_SERVICE, args.ip, &args.device)?; - let ret = mdns.query("publish.test.genmeta.net".to_string()).await?; + let ret = mdns.query("publish.test.dhttp.net".to_string()).await?; println!("{ret:?}\n"); Ok(()) } diff --git a/src/bin/ddns-server/error.rs b/src/bin/ddns-server/error.rs index e242522..6bb6e70 100644 --- a/src/bin/ddns-server/error.rs +++ b/src/bin/ddns-server/error.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use dhttp_identity::name::DhttpName; + #[derive(Debug, snafu::Snafu)] pub enum AppError { #[snafu(display("missing host parameter"))] @@ -68,8 +70,8 @@ pub fn normalize_host(host: &str) -> Result { let host = idna::domain_to_ascii(host).map_err(|_| AppError::InvalidHost)?; let host = host.to_ascii_lowercase(); - // 校验是否为 genmeta.net 域名 - if !host.ends_with("genmeta.net") { + // 校验是否为 DHTTP identity 域名 + if !host.ends_with(DhttpName::SUFFIX) { return Err(AppError::DomainNotAllowed); } @@ -82,3 +84,20 @@ pub fn parse_query_params(uri: &http::Uri) -> HashMap { .into_owned() .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_host_uses_dhttp_identity_suffix() { + assert_eq!( + normalize_host("Reimu.Pilot.Dhttp.Net:443").unwrap(), + "reimu.pilot.dhttp.net" + ); + assert!(matches!( + normalize_host("reimu.pilot.genmeta.net"), + Err(AppError::DomainNotAllowed) + )); + } +} diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index 6fb5451..1034d31 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -184,7 +184,7 @@ mod tests { #[test] fn validate_dns_packet_accepts_empty_packet_as_clear_operation() { let hosts: HashMap> = - HashMap::from([("reimu.pilot.genmeta.net".to_owned(), Vec::new())]); + HashMap::from([("reimu.pilot.dhttp.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); let validated = validate_dns_packet(&packet, true, &TestAuthority).unwrap(); diff --git a/src/bin/ddns-server/publish.rs b/src/bin/ddns-server/publish.rs index 69b42e7..5b07347 100644 --- a/src/bin/ddns-server/publish.rs +++ b/src/bin/ddns-server/publish.rs @@ -377,7 +377,7 @@ mod tests { #[tokio::test] async fn clear_record_removes_only_current_certificate_fingerprint() { let state = memory_state(); - let host = "reimu.pilot.genmeta.net"; + let host = "reimu.pilot.dhttp.net"; let authority_a = TestAuthority::new("authority-a", vec![1]); let authority_b = TestAuthority::new("authority-b", vec![2]); let packet_a = packet_for(host, 1); @@ -412,7 +412,7 @@ mod tests { #[tokio::test] async fn clear_record_is_idempotent_for_missing_fingerprint() { let state = memory_state(); - let host = "reimu.pilot.genmeta.net"; + let host = "reimu.pilot.dhttp.net"; let authority = TestAuthority::new("authority", vec![1]); assert_eq!( diff --git a/src/mdns/service.rs b/src/mdns/service.rs index 29f1748..d6a5d69 100644 --- a/src/mdns/service.rs +++ b/src/mdns/service.rs @@ -7,6 +7,7 @@ use std::{ time::Duration, }; +use dhttp_identity::name::DhttpName; use dquic::qinterface::{Interface, component::Component, io::IO}; use futures::{Stream, stream}; use tokio::{task::JoinSet, time}; @@ -257,8 +258,8 @@ impl Mdns { #[inline] fn local_name(service_name: String, name: String) -> String { - name.split_once("genmeta.net") - .map(|(prefix, _)| format!("{prefix}{service_name}")) + name.strip_suffix(DhttpName::SUFFIX) + .map(|prefix| format!("{prefix}.{service_name}")) .unwrap_or_else(|| name) } } @@ -272,3 +273,19 @@ impl Component for Mdns { self.reinit(iface); } } + +#[cfg(test)] +mod tests { + use super::Mdns; + + #[test] + fn local_name_uses_dhttp_identity_suffix() { + assert_eq!( + Mdns::local_name( + "_gensokyo.local".to_string(), + "reimu.pilot.dhttp.net".to_string() + ), + "reimu.pilot._gensokyo.local" + ); + } +} diff --git a/src/resolvers.rs b/src/resolvers.rs index 99843fa..014e520 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -299,8 +299,8 @@ mod tests { #[test] fn resolvable_name_accepts_dns_name_with_numeric_port() { assert_eq!( - resolvable_name("example.genmeta.net:443"), - Some("example.genmeta.net") + resolvable_name("example.dhttp.net:443"), + Some("example.dhttp.net") ); } diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 5914d9b..22f45aa 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -423,14 +423,14 @@ mod tests { )); let resolver = H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, endpoint).unwrap(); resolver.cached_records.insert( - "car.lab.genmeta.net".to_owned(), + "car.lab.dhttp.net".to_owned(), Record { addrs: vec![EndpointAddr::direct("192.168.5.78:41748".parse().unwrap())], expire: Instant::now() + Duration::from_secs(60), }, ); - let mut records = resolver.lookup("car.lab.genmeta.net").await.unwrap(); + let mut records = resolver.lookup("car.lab.dhttp.net").await.unwrap(); let (source, endpoint) = records.next().await.unwrap(); assert_eq!( From ddb22b4dec8e696b37de5a3a7ab82cb8d4c02a09 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 9 Jun 2026 13:22:34 +0800 Subject: [PATCH 72/85] refactor: split dns endpoint publisher roles --- src/publisher.rs | 823 +++++++++++--------------------------- src/publisher/address.rs | 444 ++++++++++++++++++++ src/publisher/dispatch.rs | 152 +++++++ src/publisher/packet.rs | 88 ++++ 4 files changed, 909 insertions(+), 598 deletions(-) create mode 100644 src/publisher/address.rs create mode 100644 src/publisher/dispatch.rs create mode 100644 src/publisher/packet.rs diff --git a/src/publisher.rs b/src/publisher.rs index 5e736e7..653c836 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -1,32 +1,20 @@ -use std::{ - any::{Any, TypeId}, - collections::{HashMap, HashSet}, - future::Future, - io, - net::SocketAddr, - pin::Pin, - sync::Arc, - time::Duration, -}; +mod address; +mod dispatch; +mod packet; -use dhttp_identity::identity::LocalAuthority; -#[cfg(feature = "mdns-resolver")] -use dquic::qbase::net::Family; -use dquic::{ - qbase::net::addr::EndpointAddr, - qinterface::component::location::AddressEvent, - qresolve::{Publish, Resolve}, - qtraversal::nat::client::{ClientLocationData, NatType}, -}; -use snafu::{ResultExt, Snafu}; +use std::{any::TypeId, future::Future, io, net::SocketAddr, pin::Pin, sync::Arc, time::Duration}; -use crate::{ - core::{ - MdnsPacket, - parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, - }, - resolvers::Resolvers, +pub use address::{ + AddressSelector, AddressView, AddressViewSource, EndpointBindingAddresses, FnAddressView, + PublishAddressGroup, PublishAddressScope, PublishAddresses, }; +use dhttp_identity::{identity::LocalAuthority, name::Name}; +use dquic::{ + qinterface::component::location::AddressEvent, qresolve::Resolve, + qtraversal::nat::client::ClientLocationData, +}; +pub use packet::{EndpointRecordSigner, SignEndpointRecordsError}; +use snafu::Snafu; pub const DEFAULT_PUBLISH_INTERVAL: Duration = Duration::from_secs(20); /// Upper bound for a single publish attempt in the background loop. @@ -51,10 +39,8 @@ pub enum CreatePublisherError { pub enum PublishOnceError { #[snafu(display("no publisher resolver available"))] NoPublisherResolver, - #[snafu(display("failed to encode endpoint address"))] - EncodeEndpoint, - #[snafu(display("failed to sign endpoint address"))] - SignEndpoint { source: SignEndpointError }, + #[snafu(display("failed to sign endpoint records"))] + SignEndpointRecords { source: SignEndpointRecordsError }, #[snafu(display("failed to publish dns packet with {publisher}"))] Publish { publisher: String, @@ -72,53 +58,138 @@ pub struct PublishOptions { pub server_id: Option, } -pub struct Publisher { - identity: Arc, - network: Arc, - resolver: Arc, - bind_patterns: Arc>, +pub trait PublisherResolver: Send + Sync + 'static { + fn as_resolver(&self) -> &(dyn Resolve + Send + Sync); +} + +impl PublisherResolver for T +where + T: Resolve + Send + Sync + Sized + 'static, +{ + fn as_resolver(&self) -> &(dyn Resolve + Send + Sync) { + self + } +} + +impl PublisherResolver for dyn Resolve + Send + Sync { + fn as_resolver(&self) -> &(dyn Resolve + Send + Sync) { + self + } +} + +pub struct Publisher { + signer: EndpointRecordSigner, + resolver: Arc, +} + +impl std::fmt::Debug for Publisher +where + A: LocalAuthority + Send + Sync + ?Sized, + R: PublisherResolver + ?Sized, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Publisher") + .field("signer", &self.signer) + .field("resolver", &self.resolver.as_resolver().to_string()) + .finish() + } +} + +pub type EndpointPublisher = Publisher; + +impl Publisher +where + A: LocalAuthority + Send + Sync + ?Sized, + R: PublisherResolver + ?Sized, +{ + pub fn new(signer: EndpointRecordSigner, resolver: Arc) -> Self { + Self { signer, resolver } + } + + pub fn signer(&self) -> &EndpointRecordSigner { + &self.signer + } + + pub fn options(&self) -> PublishOptions { + self.signer.options() + } + + pub fn resolver(&self) -> &Arc { + &self.resolver + } + + pub async fn publish_once( + &self, + name: &Name<'_>, + addresses: &V, + ) -> Result<(), PublishOnceError> + where + V: AddressView + Sync, + { + let mut published = false; + published |= self + .publish_to_resolver(self.resolver.as_resolver(), name, addresses) + .await?; + + if !published { + return publish_once_error::NoPublisherResolverSnafu.fail(); + } + + Ok(()) + } +} + +pub struct EndpointPublicationLoop { + name: Name<'static>, + publisher: Publisher, + source: S, interval: Duration, publish_timeout: Duration, - options: PublishOptions, } -impl std::fmt::Debug for Publisher { +impl std::fmt::Debug for EndpointPublicationLoop +where + A: LocalAuthority + Send + Sync + ?Sized, + R: PublisherResolver + ?Sized, + S: std::fmt::Debug, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Publisher") - .field("identity", &self.identity.name()) - .field("bind_patterns", &self.bind_patterns) + f.debug_struct("EndpointPublicationLoop") + .field("name", &self.name) + .field("publisher", &self.publisher) + .field("source", &self.source) .field("interval", &self.interval) .field("publish_timeout", &self.publish_timeout) - .field("options", &self.options) - .finish_non_exhaustive() + .finish() } } -impl Publisher { - pub fn new( - identity: Arc, - network: Arc, - resolver: Arc, - bind_patterns: Arc>, - ) -> Self { +impl EndpointPublicationLoop +where + A: LocalAuthority + Send + Sync + ?Sized, + R: PublisherResolver + ?Sized, + S: AddressViewSource + Send + Sync, +{ + pub fn new(name: Name<'static>, publisher: Publisher, source: S) -> Self { Self { - identity, - network, - resolver, - bind_patterns, + name, + publisher, + source, interval: DEFAULT_PUBLISH_INTERVAL, publish_timeout: DEFAULT_PUBLISH_TIMEOUT, - options: PublishOptions::default(), } } - pub fn with_options(mut self, options: PublishOptions) -> Self { - self.options = options; - self + pub fn name(&self) -> &Name<'static> { + &self.name + } + + pub fn publisher(&self) -> &Publisher { + &self.publisher } pub fn options(&self) -> PublishOptions { - self.options + self.publisher.options() } pub fn interval(&self) -> Duration { @@ -134,50 +205,8 @@ impl Publisher { self } - pub async fn publish_once(&self) -> Result<(), PublishOnceError> { - let mut published = false; - let public_endpoints = self.public_endpoints(); - tracing::debug!( - endpoint_count = public_endpoints.len(), - endpoints = ?public_endpoints, - "publishing public endpoints" - ); - published |= self - .publish_to_resolver(self.resolver.as_ref(), &public_endpoints) - .await?; - - if !published { - return publish_once_error::NoPublisherResolverSnafu.fail(); - } - - Ok(()) - } - - pub async fn publish_once_for( - &self, - host: &str, - endpoints: &[EndpointAddr], - ) -> Result<(), PublishOnceError> { - let mut published = false; - tracing::debug!( - host, - endpoint_count = endpoints.len(), - endpoints = ?endpoints, - "publishing explicit endpoints" - ); - published |= self - .publish_supplied_endpoints_to_resolver(self.resolver.as_ref(), host, endpoints) - .await?; - - if !published { - return publish_once_error::NoPublisherResolverSnafu.fail(); - } - - Ok(()) - } - pub async fn run(&self) -> ! { - let mut locations = self.network.quic().locations().subscribe(); + let mut locations = self.source.subscribe(); let interval = tokio::time::sleep(self.interval); tokio::pin!(interval); // Keep at most one publish attempt in flight. A timer tick or @@ -200,7 +229,7 @@ impl Publisher { let Some((bind_uri, event)) = event else { continue; }; - if !self.bind_patterns.iter().any(|pattern| pattern.matches(&bind_uri)) { + if !self.source.observes(&bind_uri) { continue; } if !Self::location_event_requires_publish(&event) { @@ -230,14 +259,20 @@ impl Publisher { timeout_ms = self.publish_timeout.as_millis(), "starting dns publish attempt" ); - match tokio::time::timeout(self.publish_timeout, self.publish_once()).await { + let addresses = self.source.address_view(); + match tokio::time::timeout( + self.publish_timeout, + self.publisher.publish_once(&self.name, &addresses), + ) + .await + { Ok(Ok(())) => { - tracing::info!("published resolver endpoints"); + tracing::info!(name = %self.name, "published resolver endpoints"); true } Ok(Err(error)) => { let report = snafu::Report::from_error(&error); - tracing::warn!(error = %report, "dns publish failed"); + tracing::warn!(error = %report, name = %self.name, "dns publish failed"); false } Err(_elapsed) => { @@ -248,6 +283,7 @@ impl Publisher { self.clear_publish_state(); tracing::warn!( timeout_ms = self.publish_timeout.as_millis(), + name = %self.name, "dns publish timed out" ); false @@ -255,6 +291,10 @@ impl Publisher { } } + fn clear_publish_state(&self) { + dispatch::clear_resolver_publish_state(self.publisher.resolver.as_resolver()); + } + fn location_event_requires_publish(event: &AddressEvent) -> bool { match event { AddressEvent::Upsert(data) => { @@ -276,357 +316,13 @@ impl Publisher { AddressEvent::Closed => true, } } - - async fn publish_to_resolver( - &self, - resolver: &(dyn Resolve + Send + Sync), - public_endpoints: &[EndpointAddr], - ) -> Result { - let any: &dyn Any = resolver; - - if let Some(resolvers) = any.downcast_ref::() { - let mut published = false; - for resolver in resolvers.iter() { - published |= self - .publish_single_resolver(resolver.as_ref(), public_endpoints) - .await?; - } - return Ok(published); - } - - self.publish_single_resolver(resolver, public_endpoints) - .await - } - - async fn publish_supplied_endpoints_to_resolver( - &self, - resolver: &(dyn Resolve + Send + Sync), - host: &str, - endpoints: &[EndpointAddr], - ) -> Result { - let any: &dyn Any = resolver; - - if let Some(resolvers) = any.downcast_ref::() { - let mut published = false; - for resolver in resolvers.iter() { - published |= self - .publish_single_resolver_for(resolver.as_ref(), host, endpoints) - .await?; - } - return Ok(published); - } - - self.publish_single_resolver_for(resolver, host, endpoints) - .await - } - - fn clear_publish_state(&self) { - Self::clear_resolver_publish_state(self.resolver.as_ref()); - } - - fn clear_resolver_publish_state(resolver: &(dyn Resolve + Send + Sync)) { - let any: &dyn Any = resolver; - - if let Some(resolvers) = any.downcast_ref::() { - for resolver in resolvers.iter() { - Self::clear_resolver_publish_state(resolver.as_ref()); - } - } - - #[cfg(feature = "h3x-resolver")] - if let Some(h3) = - any.downcast_ref::>() - { - h3.clear_pool(); - } - } - - async fn publish_single_resolver( - &self, - resolver: &(dyn Resolve + Send + Sync), - public_endpoints: &[EndpointAddr], - ) -> Result { - #[cfg(not(any(feature = "http-resolver", feature = "h3x-resolver")))] - let _ = public_endpoints; - - let any: &dyn Any = resolver; - - #[cfg(feature = "http-resolver")] - if let Some(http) = any.downcast_ref::() { - self.publish_endpoints(http, public_endpoints).await?; - return Ok(true); - } - - #[cfg(feature = "h3x-resolver")] - if let Some(h3) = - any.downcast_ref::>() - { - self.publish_endpoints(h3, public_endpoints).await?; - return Ok(true); - } - - #[cfg(feature = "mdns-resolver")] - if let Some(mdns) = any.downcast_ref::() { - let mut published = false; - for bound in mdns.bound_resolvers() { - let endpoints = self.local_endpoints_for(&bound.device, bound.family); - self.publish_endpoints(&bound.resolver, &endpoints).await?; - published = true; - } - return Ok(published); - } - - Ok(false) - } - - async fn publish_single_resolver_for( - &self, - resolver: &(dyn Resolve + Send + Sync), - host: &str, - endpoints: &[EndpointAddr], - ) -> Result { - #[cfg(not(any( - feature = "http-resolver", - feature = "h3x-resolver", - feature = "mdns-resolver" - )))] - { - let _ = host; - let _ = endpoints; - } - - let any: &dyn Any = resolver; - - #[cfg(feature = "http-resolver")] - if let Some(http) = any.downcast_ref::() { - self.publish_endpoints_for(http, host, endpoints).await?; - return Ok(true); - } - - #[cfg(feature = "h3x-resolver")] - if let Some(h3) = - any.downcast_ref::>() - { - self.publish_endpoints_for(h3, host, endpoints).await?; - return Ok(true); - } - - #[cfg(feature = "mdns-resolver")] - if let Some(mdns) = any.downcast_ref::() { - let mut published = false; - for bound in mdns.bound_resolvers() { - self.publish_endpoints_for(&bound.resolver, host, endpoints) - .await?; - published = true; - } - return Ok(published); - } - - Ok(false) - } - - async fn publish_endpoints( - &self, - publisher: &(dyn Publish + Send + Sync), - endpoints: &[EndpointAddr], - ) -> Result<(), PublishOnceError> { - self.publish_endpoints_for(publisher, self.identity.name(), endpoints) - .await - } - - async fn publish_endpoints_for( - &self, - publisher: &(dyn Publish + Send + Sync), - host: &str, - endpoints: &[EndpointAddr], - ) -> Result<(), PublishOnceError> { - let packet = self.signed_packet_for(host, endpoints).await?; - tracing::debug!( - publisher = %publisher, - host, - endpoint_count = endpoints.len(), - packet_len = packet.len(), - "publishing dns packet" - ); - publisher - .publish(host, &packet) - .await - .context(publish_once_error::PublishSnafu { - publisher: publisher.to_string(), - }) - } - - #[cfg(test)] - async fn signed_packet(&self, endpoints: &[EndpointAddr]) -> Result, PublishOnceError> { - self.signed_packet_for(self.identity.name(), endpoints) - .await - } - - async fn signed_packet_for( - &self, - host: &str, - endpoints: &[EndpointAddr], - ) -> Result, PublishOnceError> { - let mut signed = Vec::with_capacity(endpoints.len()); - for endpoint in endpoints { - let mut endpoint = DnsEndpointAddr::try_from(*endpoint) - .map_err(|_| publish_once_error::EncodeEndpointSnafu.build())?; - if let Some(server_id) = self.options.server_id { - endpoint.set_main(server_id == 0); - endpoint.set_sequence(server_id.into()); - } - endpoint - .sign_with_authority(self.identity.as_ref()) - .await - .context(publish_once_error::SignEndpointSnafu)?; - signed.push(endpoint); - } - - let mut hosts = HashMap::new(); - hosts.insert(host.to_owned(), signed); - Ok(MdnsPacket::answer(0, &hosts).to_bytes()) - } - - fn public_endpoints(&self) -> Vec { - let mut endpoints = Vec::new(); - let mut seen = HashSet::new(); - for pattern in self.bind_patterns.iter() { - let Some(ifaces) = self.network.quic().get_interfaces(pattern) else { - tracing::trace!(?pattern, "no interfaces for bind pattern"); - continue; - }; - for iface in ifaces { - for endpoint in public_endpoints_from_iface(&self.network, &iface) { - push_unique_endpoint(&mut endpoints, &mut seen, endpoint); - } - } - } - endpoints - } - - #[cfg(feature = "mdns-resolver")] - fn local_endpoints_for(&self, device: &str, family: Family) -> Vec { - let mut endpoints = HashSet::new(); - for pattern in self.bind_patterns.iter() { - let Some(ifaces) = self.network.quic().get_interfaces(pattern) else { - continue; - }; - for iface in ifaces { - let bind_uri = iface.bind_uri(); - let Some((iface_family, iface_device, _port)) = bind_uri.as_iface_bind_uri() else { - continue; - }; - if iface_family != family || iface_device != device { - continue; - } - if let Some(endpoint) = local_endpoint_from_iface(&iface, family) { - endpoints.insert(endpoint); - } - } - } - endpoints.into_iter().collect() - } -} - -fn push_unique_endpoint( - endpoints: &mut Vec, - seen: &mut HashSet, - endpoint: EndpointAddr, -) { - if seen.insert(endpoint) { - endpoints.push(endpoint); - } -} - -fn public_endpoints_from_iface( - network: &h3x::dquic::Network, - iface: &h3x::dquic::net::BindInterface, -) -> Vec { - use h3x::dquic::{net::IO, qtraversal::nat::client::StunClientsComponent}; - - iface.with_components(|components, current| { - let bind_uri = current.bind_uri(); - let addr = current.bound_addr().ok(); - let mut endpoints: Vec = components - .get::() - .map(|stun| { - stun.with_clients(|clients| { - clients - .values() - .filter_map(|client| { - let outer = client.get_outer_addr()?.ok()?; - let bound = current.bound_addr().ok()?; - match client.get_nat_type() { - Some(Ok(nat_type)) => Some(publish_endpoint_from_stun( - bound, - client.agent_addr(), - outer, - nat_type, - )), - None => Some(EndpointAddr::with_agent(client.agent_addr(), outer)), - Some(Err(_)) => None, - } - }) - .collect() - }) - }) - .unwrap_or_default(); - let stun_endpoint_count = endpoints.len(); - - // Also publish the current default-route address. STUN-derived - // endpoints make the node reachable from outside the local network, - // while the bound address is still the shortest valid path for peers - // on the same link and for separate local client processes on the - // same host. Keep it after STUN endpoints so translated-NAT peers get - // the externally reachable candidate first. - if let Some(addr) = addr - && network.bound_addr_is_on_default_route(&bind_uri, addr) - { - endpoints.push(EndpointAddr::direct(addr)); - } - - tracing::trace!( - bind_uri = %bind_uri, - bound_addr = ?addr, - stun_endpoint_count, - endpoint_count = endpoints.len(), - endpoints = ?endpoints, - "collected public endpoints from interface" - ); - - endpoints - }) -} - -fn publish_endpoint_from_stun( - bound: SocketAddr, - agent: SocketAddr, - outer: SocketAddr, - nat_type: NatType, -) -> EndpointAddr { - if nat_type == NatType::FullCone && bound == outer { - EndpointAddr::direct(outer) - } else { - EndpointAddr::with_agent(agent, outer) - } } -#[cfg(feature = "mdns-resolver")] -fn local_endpoint_from_iface( - iface: &h3x::dquic::net::BindInterface, - family: Family, -) -> Option { - use h3x::dquic::net::IO; - - iface.with_components(|_components, current| { - let addr = current.bound_addr().ok()?; - match (family, addr) { - (Family::V4, std::net::SocketAddr::V4(_)) - | (Family::V6, std::net::SocketAddr::V6(_)) => Some(EndpointAddr::direct(addr)), - _ => None, - } - }) -} +pub type EndpointPublisherLoop = EndpointPublicationLoop< + dyn LocalAuthority + Send + Sync, + dyn Resolve + Send + Sync, + EndpointBindingAddresses, +>; #[cfg(test)] mod tests { @@ -690,35 +386,42 @@ mod tests { impl Resolve for DisplayOnlyResolver { fn lookup<'l>(&'l self, _name: &'l str) -> ResolveFuture<'l> { - async { Ok(stream::empty::<(Source, EndpointAddr)>().boxed()) }.boxed() + async { Ok(stream::empty::<(Source, dquic::qbase::net::addr::EndpointAddr)>().boxed()) } + .boxed() } } - #[tokio::test] - async fn publish_once_reports_no_publisher_resolver() { - let publisher = Publisher::new( - Arc::new(TestAuthority), - h3x::dquic::Network::builder().build(), - Arc::new(DisplayOnlyResolver), - Arc::new(Vec::new()), - ); + fn test_name() -> Name<'static> { + "authority.example".parse().unwrap() + } - let error = publisher.publish_once().await.unwrap_err(); - assert!(matches!(error, PublishOnceError::NoPublisherResolver)); + fn test_publisher(resolver: Arc) -> Publisher + where + R: Resolve + Send + Sync, + { + let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)); + Publisher::new(signer, resolver) + } + + fn test_source(network: Arc) -> EndpointBindingAddresses { + EndpointBindingAddresses::new( + network, + Arc::new(vec![ + "inet://127.0.0.1:0".parse().expect("valid bind pattern"), + ]), + ) } #[tokio::test] - async fn publish_once_for_reports_no_publisher_resolver() { - let publisher = Publisher::new( - Arc::new(TestAuthority), - h3x::dquic::Network::builder().build(), - Arc::new(DisplayOnlyResolver), - Arc::new(Vec::new()), - ); - let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + async fn publish_once_reports_no_publisher_resolver() { + let publisher = test_publisher(Arc::new(DisplayOnlyResolver)); + let addresses = + PublishAddresses::new().wide_area([dquic::qbase::net::addr::EndpointAddr::direct( + "127.0.0.1:443".parse().unwrap(), + )]); let error = publisher - .publish_once_for("nat.genmeta.net", &[endpoint]) + .publish_once(&test_name(), &addresses) .await .unwrap_err(); @@ -727,31 +430,26 @@ mod tests { #[tokio::test] async fn publisher_timeout_is_configurable() { - let publisher = Publisher::new( - Arc::new(TestAuthority), - h3x::dquic::Network::builder().build(), - Arc::new(DisplayOnlyResolver), - Arc::new(Vec::new()), - ); - assert_eq!(publisher.publish_timeout(), DEFAULT_PUBLISH_TIMEOUT); + let network = h3x::dquic::Network::builder().build(); + let publisher = test_publisher(Arc::new(DisplayOnlyResolver)); + let publisher_loop = + EndpointPublicationLoop::new(test_name(), publisher, test_source(network)); + assert_eq!(publisher_loop.publish_timeout(), DEFAULT_PUBLISH_TIMEOUT); let timeout = Duration::from_secs(3); - let publisher = publisher.with_publish_timeout(timeout); - assert_eq!(publisher.publish_timeout(), timeout); + let publisher_loop = publisher_loop.with_publish_timeout(timeout); + assert_eq!(publisher_loop.publish_timeout(), timeout); } #[tokio::test] - async fn signed_packet_applies_publish_options_server_id() { - let publisher = Publisher::new( - Arc::new(TestAuthority), - h3x::dquic::Network::builder().build(), - Arc::new(DisplayOnlyResolver), - Arc::new(Vec::new()), - ) - .with_options(PublishOptions { server_id: Some(2) }); - - let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); - let packet = publisher.signed_packet(&[endpoint]).await.unwrap(); + async fn signer_applies_publish_options_server_id() { + let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)) + .with_options(PublishOptions { server_id: Some(2) }); + let name: Name<'static> = "authority.example".parse().unwrap(); + + let endpoint = + dquic::qbase::net::addr::EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + let packet = signer.signed_packet(&name, &[endpoint]).await.unwrap(); let (_remain, packet) = crate::core::parser::packet::be_packet(&packet).unwrap(); let record = packet.answers.first().expect("endpoint answer"); let crate::core::parser::record::RData::E(endpoint) = record.data() else { @@ -764,20 +462,14 @@ mod tests { } #[tokio::test] - async fn signed_packet_for_uses_supplied_host_and_keeps_signature_and_options() { - let publisher = Publisher::new( - Arc::new(TestAuthority), - h3x::dquic::Network::builder().build(), - Arc::new(DisplayOnlyResolver), - Arc::new(Vec::new()), - ) - .with_options(PublishOptions { server_id: Some(2) }); - - let endpoint = EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); - let packet = publisher - .signed_packet_for("nat.genmeta.net", &[endpoint]) - .await - .unwrap(); + async fn signer_uses_supplied_record_owner_name() { + let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)) + .with_options(PublishOptions { server_id: Some(2) }); + let name: Name<'static> = "nat.genmeta.net".parse().unwrap(); + + let endpoint = + dquic::qbase::net::addr::EndpointAddr::direct("127.0.0.1:443".parse().unwrap()); + let packet = signer.signed_packet(&name, &[endpoint]).await.unwrap(); let (_remain, packet) = crate::core::parser::packet::be_packet(&packet).unwrap(); let record = packet.answers.first().expect("endpoint answer"); let crate::core::parser::record::RData::E(endpoint) = record.data() else { @@ -791,60 +483,15 @@ mod tests { } #[tokio::test] - async fn public_endpoints_do_not_fall_back_to_local_bound_addresses() { + async fn binding_address_view_does_not_expose_loopback_as_wide_area_without_stun() { let network = h3x::dquic::Network::builder().build(); let bind_pattern: h3x::dquic::binds::BindPattern = "inet://127.0.0.1:0".parse().expect("valid bind pattern"); let _bind = network.quic().bind(bind_pattern.clone()).await; - let publisher = Publisher::new( - Arc::new(TestAuthority), - network, - Arc::new(DisplayOnlyResolver), - Arc::new(vec![bind_pattern]), - ); - - assert!( - publisher.public_endpoints().is_empty(), - "public DNS publishing must wait for STUN-derived external endpoints; local addresses are published through mDNS" - ); - } - - #[test] - fn push_unique_endpoint_preserves_first_seen_order() { - let agent = EndpointAddr::with_agent( - "10.10.0.2:20004".parse().expect("valid agent addr"), - "10.10.0.10:45635".parse().expect("valid outer addr"), - ); - let direct = EndpointAddr::direct("10.110.0.10:45635".parse().expect("valid direct addr")); - let mut endpoints = Vec::new(); - let mut seen = HashSet::new(); + let source = EndpointBindingAddresses::new(network, Arc::new(vec![bind_pattern])); + let view = source.address_view(); - push_unique_endpoint(&mut endpoints, &mut seen, agent); - push_unique_endpoint(&mut endpoints, &mut seen, direct); - push_unique_endpoint(&mut endpoints, &mut seen, agent); - - assert_eq!(endpoints, vec![agent, direct]); - } - - #[test] - fn full_cone_nat_endpoint_preserves_agent_when_outer_differs_from_bound_addr() { - let bound = "10.110.0.10:45635".parse().expect("valid bound addr"); - let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); - let outer = "10.10.0.10:45635".parse().expect("valid outer addr"); - - let endpoint = publish_endpoint_from_stun(bound, agent, outer, NatType::FullCone); - - assert_eq!(endpoint, EndpointAddr::with_agent(agent, outer)); - } - - #[test] - fn full_cone_endpoint_is_direct_without_address_translation() { - let bound = "10.10.0.100:45635".parse().expect("valid bound addr"); - let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); - - let endpoint = publish_endpoint_from_stun(bound, agent, bound, NatType::FullCone); - - assert_eq!(endpoint, EndpointAddr::direct(bound)); + assert!(view.endpoints(AddressSelector::WideArea).next().is_none()); } #[cfg(feature = "http-resolver")] @@ -896,18 +543,13 @@ mod tests { crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); - let mut publisher = Publisher::new( - Arc::new(TestAuthority), - network.clone(), - resolver, - Arc::new(vec![ - "inet://127.0.0.1:0".parse().expect("valid bind pattern"), - ]), - ); - publisher.interval = Duration::from_secs(60); + let publisher = test_publisher(resolver); + let source = test_source(network.clone()); + let mut publisher_loop = EndpointPublicationLoop::new(test_name(), publisher, source); + publisher_loop.interval = Duration::from_secs(60); let publisher = tokio::spawn(async move { - publisher.run().await; + publisher_loop.run().await; }); wait_for_count(&publish_count, 1).await; @@ -986,16 +628,11 @@ mod tests { crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); - let publisher = Publisher::new( - Arc::new(TestAuthority), - network.clone(), - resolver, - Arc::new(vec![ - "inet://127.0.0.1:0".parse().expect("valid bind pattern"), - ]), - ); + let publisher = test_publisher(resolver); + let source = test_source(network.clone()); + let publisher_loop = EndpointPublicationLoop::new(test_name(), publisher, source); let publisher = tokio::spawn(async move { - publisher.run().await; + publisher_loop.run().await; }); wait_for_count(&publish_count, 1).await; @@ -1066,19 +703,14 @@ mod tests { crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); - let mut publisher = Publisher::new( - Arc::new(TestAuthority), - network.clone(), - resolver, - Arc::new(vec![ - "inet://127.0.0.1:0".parse().expect("valid bind pattern"), - ]), - ) - .with_publish_timeout(Duration::from_millis(50)); - publisher.interval = Duration::from_secs(60); + let publisher = test_publisher(resolver); + let source = test_source(network.clone()); + let mut publisher_loop = EndpointPublicationLoop::new(test_name(), publisher, source) + .with_publish_timeout(Duration::from_millis(50)); + publisher_loop.interval = Duration::from_secs(60); let publisher = tokio::spawn(async move { - publisher.run().await; + publisher_loop.run().await; }); wait_for_count(&publish_count, 1).await; @@ -1150,19 +782,14 @@ mod tests { crate::resolvers::http::HttpResolver::new(format!("http://127.0.0.1:{port}/")) .expect("valid http resolver"), ); - let mut publisher = Publisher::new( - Arc::new(TestAuthority), - network.clone(), - resolver, - Arc::new(vec![ - "inet://127.0.0.1:0".parse().expect("valid bind pattern"), - ]), - ) - .with_publish_timeout(Duration::from_secs(30)); - publisher.interval = Duration::from_secs(60); + let publisher = test_publisher(resolver); + let source = test_source(network.clone()); + let mut publisher_loop = EndpointPublicationLoop::new(test_name(), publisher, source) + .with_publish_timeout(Duration::from_secs(30)); + publisher_loop.interval = Duration::from_secs(60); let publisher = tokio::spawn(async move { - publisher.run().await; + publisher_loop.run().await; }); tokio::time::timeout(Duration::from_secs(2), wait_for_count(&publish_count, 1)) diff --git a/src/publisher/address.rs b/src/publisher/address.rs new file mode 100644 index 0000000..207150b --- /dev/null +++ b/src/publisher/address.rs @@ -0,0 +1,444 @@ +use std::{ + collections::HashSet, + net::SocketAddr, + sync::{Arc, OnceLock}, +}; + +use dquic::{ + qbase::net::{Family, addr::EndpointAddr}, + qinterface::component::location::Observer, +}; +use h3x::dquic::{ + Network, + binds::BindPattern, + net::{BindInterface, BindUri, IO, Scheme}, + qtraversal::nat::client::{NatType, StunClientsComponent}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressSelector<'a> { + WideArea, + LocalLink { device: &'a str, family: Family }, +} + +pub trait AddressView { + fn endpoints<'a>( + &'a self, + selector: AddressSelector<'a>, + ) -> impl Iterator + 'a; +} + +pub struct FnAddressView { + f: F, +} + +impl FnAddressView { + pub fn new(f: F) -> Self { + Self { f } + } +} + +impl AddressView for FnAddressView +where + F: for<'a> Fn(AddressSelector<'a>) -> I, + I: IntoIterator, + I::IntoIter: 'static, +{ + fn endpoints<'a>( + &'a self, + selector: AddressSelector<'a>, + ) -> impl Iterator + 'a { + (self.f)(selector).into_iter() + } +} + +pub trait AddressViewSource { + fn address_view(&self) -> impl AddressView + Send + Sync + '_; + fn subscribe(&self) -> Observer; + fn observes(&self, bind_uri: &BindUri) -> bool; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PublishAddressScope { + WideArea, + LocalLink { device: Arc, family: Family }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublishAddressGroup { + scope: PublishAddressScope, + endpoints: Vec, +} + +impl PublishAddressGroup { + pub fn wide_area(endpoints: I) -> Self + where + I: IntoIterator, + { + Self { + scope: PublishAddressScope::WideArea, + endpoints: endpoints.into_iter().collect(), + } + } + + pub fn local_link(device: impl Into>, family: Family, endpoints: I) -> Self + where + I: IntoIterator, + { + Self { + scope: PublishAddressScope::LocalLink { + device: device.into(), + family, + }, + endpoints: endpoints.into_iter().collect(), + } + } + + fn matches(&self, selector: AddressSelector<'_>) -> bool { + match (&self.scope, selector) { + (PublishAddressScope::WideArea, AddressSelector::WideArea) => true, + ( + PublishAddressScope::LocalLink { device, family }, + AddressSelector::LocalLink { + device: selected_device, + family: selected_family, + }, + ) => device.as_ref() == selected_device && *family == selected_family, + _ => false, + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PublishAddresses { + groups: Vec, +} + +impl PublishAddresses { + pub fn new() -> Self { + Self::default() + } + + pub fn group(mut self, group: PublishAddressGroup) -> Self { + self.groups.push(group); + self + } + + pub fn wide_area(self, endpoints: I) -> Self + where + I: IntoIterator, + { + self.group(PublishAddressGroup::wide_area(endpoints)) + } + + pub fn local_link(self, device: impl Into>, family: Family, endpoints: I) -> Self + where + I: IntoIterator, + { + self.group(PublishAddressGroup::local_link(device, family, endpoints)) + } +} + +impl AddressView for PublishAddresses { + fn endpoints<'a>( + &'a self, + selector: AddressSelector<'a>, + ) -> impl Iterator + 'a { + self.groups + .iter() + .filter(move |group| group.matches(selector)) + .flat_map(move |group| group.endpoints.iter().copied()) + } +} + +#[derive(Clone)] +pub struct EndpointBindingAddresses { + network: Arc, + bind_patterns: Arc>, +} + +impl std::fmt::Debug for EndpointBindingAddresses { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EndpointBindingAddresses") + .field("bind_patterns", &self.bind_patterns) + .finish_non_exhaustive() + } +} + +impl EndpointBindingAddresses { + pub fn new(network: Arc, bind_patterns: Arc>) -> Self { + Self { + network, + bind_patterns, + } + } +} + +impl AddressViewSource for EndpointBindingAddresses { + fn address_view(&self) -> impl AddressView + Send + Sync + '_ { + EndpointBindingAddressView::new(self.network.clone(), self.bind_patterns.clone()) + } + + fn subscribe(&self) -> Observer { + self.network.quic().locations().subscribe() + } + + fn observes(&self, bind_uri: &BindUri) -> bool { + self.bind_patterns + .iter() + .any(|pattern| pattern.matches(bind_uri)) + } +} + +struct EndpointBindingAddressView { + bindings: Vec, +} + +impl EndpointBindingAddressView { + fn new(network: Arc, bind_patterns: Arc>) -> Self { + let mut bindings = Vec::new(); + for pattern in bind_patterns.iter() { + let Some(ifaces) = network.quic().get_interfaces(pattern) else { + tracing::trace!(?pattern, "no interfaces for bind pattern"); + continue; + }; + for iface in ifaces { + bindings.push(BindingAddress::new(network.clone(), pattern.clone(), iface)); + } + } + Self { bindings } + } +} + +impl AddressView for EndpointBindingAddressView { + fn endpoints<'a>( + &'a self, + selector: AddressSelector<'a>, + ) -> impl Iterator + 'a { + let mut seen = HashSet::new(); + self.bindings + .iter() + .filter(move |binding| binding.may_match(selector)) + .flat_map(move |binding| binding.endpoints(selector)) + .filter(move |endpoint| seen.insert(*endpoint)) + } +} + +struct BindingAddress { + network: Arc, + pattern: BindPattern, + bind_uri: BindUri, + iface: BindInterface, + wide_area: OnceLock>, + local_link: OnceLock>, +} + +impl BindingAddress { + fn new(network: Arc, pattern: BindPattern, iface: BindInterface) -> Self { + let bind_uri = iface.bind_uri(); + Self { + network, + pattern, + bind_uri, + iface, + wide_area: OnceLock::new(), + local_link: OnceLock::new(), + } + } + + fn may_match(&self, selector: AddressSelector<'_>) -> bool { + match selector { + AddressSelector::WideArea => true, + AddressSelector::LocalLink { device, family } => { + pattern_may_match_local_link(&self.pattern, device, family) + && bind_uri_matches_local_link(&self.bind_uri, device, family) + } + } + } + + fn endpoints<'a>( + &'a self, + selector: AddressSelector<'a>, + ) -> impl Iterator + 'a { + let endpoints = match selector { + AddressSelector::WideArea => self + .wide_area + .get_or_init(|| public_endpoints_from_iface(&self.network, &self.iface)), + AddressSelector::LocalLink { family, .. } => self + .local_link + .get_or_init(|| local_endpoints_from_iface(&self.iface, family)), + }; + endpoints.iter().copied() + } +} + +fn pattern_may_match_local_link(pattern: &BindPattern, device: &str, family: Family) -> bool { + if pattern.scheme != Scheme::Iface { + return false; + } + if pattern + .host + .family() + .is_some_and(|pattern_family| pattern_family != family) + { + return false; + } + pattern.host.matches(device) +} + +fn bind_uri_matches_local_link(bind_uri: &BindUri, device: &str, family: Family) -> bool { + bind_uri + .as_iface_bind_uri() + .is_some_and(|(iface_family, iface_device, _port)| { + iface_family == family && iface_device == device + }) +} + +fn public_endpoints_from_iface(network: &Network, iface: &BindInterface) -> Vec { + iface.with_components(|components, current| { + let bind_uri = current.bind_uri(); + let addr = current.bound_addr().ok(); + let mut endpoints: Vec = components + .get::() + .map(|stun| { + stun.with_clients(|clients| { + clients + .values() + .filter_map(|client| { + let outer = client.get_outer_addr()?.ok()?; + let bound = current.bound_addr().ok()?; + match client.get_nat_type() { + Some(Ok(nat_type)) => Some(publish_endpoint_from_stun( + bound, + client.agent_addr(), + outer, + nat_type, + )), + None => Some(EndpointAddr::with_agent(client.agent_addr(), outer)), + Some(Err(_)) => None, + } + }) + .collect() + }) + }) + .unwrap_or_default(); + let stun_endpoint_count = endpoints.len(); + + if let Some(addr) = addr + && network.bound_addr_is_on_default_route(&bind_uri, addr) + { + endpoints.push(EndpointAddr::direct(addr)); + } + + tracing::trace!( + bind_uri = %bind_uri, + bound_addr = ?addr, + stun_endpoint_count, + endpoint_count = endpoints.len(), + endpoints = ?endpoints, + "collected wide-area endpoints from interface" + ); + + endpoints + }) +} + +fn publish_endpoint_from_stun( + bound: SocketAddr, + agent: SocketAddr, + outer: SocketAddr, + nat_type: NatType, +) -> EndpointAddr { + if nat_type == NatType::FullCone && bound == outer { + EndpointAddr::direct(outer) + } else { + EndpointAddr::with_agent(agent, outer) + } +} + +fn local_endpoints_from_iface(iface: &BindInterface, family: Family) -> Vec { + iface.with_components(|_components, current| { + let Some(addr) = current.bound_addr().ok() else { + return Vec::new(); + }; + match (family, addr) { + (Family::V4, SocketAddr::V4(_)) | (Family::V6, SocketAddr::V6(_)) => { + vec![EndpointAddr::direct(addr)] + } + _ => Vec::new(), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn publish_addresses_select_wide_area_only_for_wide_area_selector() { + let wide = EndpointAddr::direct("203.0.113.10:443".parse().unwrap()); + let local = EndpointAddr::direct("192.168.1.20:443".parse().unwrap()); + let addresses = + PublishAddresses::new() + .wide_area([wide]) + .local_link("en0", Family::V4, [local]); + + let selected: Vec<_> = addresses.endpoints(AddressSelector::WideArea).collect(); + + assert_eq!(selected, vec![wide]); + } + + #[test] + fn publish_addresses_select_matching_local_link_group() { + let en0 = EndpointAddr::direct("192.168.1.20:443".parse().unwrap()); + let en1 = EndpointAddr::direct("192.168.2.20:443".parse().unwrap()); + let addresses = PublishAddresses::new() + .local_link("en0", Family::V4, [en0]) + .local_link("en1", Family::V4, [en1]); + + let selected: Vec<_> = addresses + .endpoints(AddressSelector::LocalLink { + device: "en1", + family: Family::V4, + }) + .collect(); + + assert_eq!(selected, vec![en1]); + } + + #[test] + fn publish_addresses_reject_local_link_family_mismatch() { + let endpoint = EndpointAddr::direct("192.168.1.20:443".parse().unwrap()); + let addresses = PublishAddresses::new().local_link("en0", Family::V4, [endpoint]); + + let selected: Vec<_> = addresses + .endpoints(AddressSelector::LocalLink { + device: "en0", + family: Family::V6, + }) + .collect(); + + assert!(selected.is_empty()); + } + + #[test] + fn full_cone_nat_endpoint_preserves_agent_when_outer_differs_from_bound_addr() { + let bound = "10.110.0.10:45635".parse().expect("valid bound addr"); + let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); + let outer = "10.10.0.10:45635".parse().expect("valid outer addr"); + + let endpoint = publish_endpoint_from_stun(bound, agent, outer, NatType::FullCone); + + assert_eq!(endpoint, EndpointAddr::with_agent(agent, outer)); + } + + #[test] + fn full_cone_endpoint_is_direct_without_address_translation() { + let bound = "10.10.0.100:45635".parse().expect("valid bound addr"); + let agent = "10.10.0.2:20004".parse().expect("valid agent addr"); + + let endpoint = publish_endpoint_from_stun(bound, agent, bound, NatType::FullCone); + + assert_eq!(endpoint, EndpointAddr::direct(bound)); + } +} diff --git a/src/publisher/dispatch.rs b/src/publisher/dispatch.rs new file mode 100644 index 0000000..07d4db6 --- /dev/null +++ b/src/publisher/dispatch.rs @@ -0,0 +1,152 @@ +use std::any::Any; + +use dhttp_identity::{identity::LocalAuthority, name::Name}; +use dquic::{ + qbase::net::addr::EndpointAddr, + qresolve::{Publish, Resolve}, +}; +use snafu::ResultExt; + +use super::{ + AddressSelector, AddressView, PublishOnceError, Publisher, PublisherResolver, + publish_once_error, +}; +use crate::resolvers::Resolvers; + +impl Publisher +where + A: LocalAuthority + Send + Sync + ?Sized, + R: PublisherResolver + ?Sized, +{ + pub(crate) async fn publish_to_resolver( + &self, + resolver: &(dyn Resolve + Send + Sync), + name: &Name<'_>, + addresses: &V, + ) -> Result + where + V: AddressView + Sync, + { + let any: &dyn Any = resolver; + + if let Some(resolvers) = any.downcast_ref::() { + let mut published = false; + for resolver in resolvers.iter() { + published |= self + .publish_single_resolver(resolver.as_ref(), name, addresses) + .await?; + } + return Ok(published); + } + + self.publish_single_resolver(resolver, name, addresses) + .await + } + + async fn publish_single_resolver( + &self, + resolver: &(dyn Resolve + Send + Sync), + name: &Name<'_>, + addresses: &V, + ) -> Result + where + V: AddressView + Sync, + { + #[cfg(not(any( + feature = "http-resolver", + feature = "h3x-resolver", + feature = "mdns-resolver" + )))] + { + let _ = name; + let _ = addresses; + } + + let any: &dyn Any = resolver; + + #[cfg(feature = "http-resolver")] + if let Some(http) = any.downcast_ref::() { + self.publish_selected(http, name, addresses, AddressSelector::WideArea) + .await?; + return Ok(true); + } + + #[cfg(feature = "h3x-resolver")] + if let Some(h3) = + any.downcast_ref::>() + { + self.publish_selected(h3, name, addresses, AddressSelector::WideArea) + .await?; + return Ok(true); + } + + #[cfg(feature = "mdns-resolver")] + if let Some(mdns) = any.downcast_ref::() { + let mut published = false; + for bound in mdns.bound_resolvers() { + self.publish_selected( + &bound.resolver, + name, + addresses, + AddressSelector::LocalLink { + device: &bound.device, + family: bound.family, + }, + ) + .await?; + published = true; + } + return Ok(published); + } + + Ok(false) + } + + async fn publish_selected( + &self, + publisher: &(dyn Publish + Send + Sync), + name: &Name<'_>, + addresses: &V, + selector: AddressSelector<'_>, + ) -> Result<(), PublishOnceError> + where + V: AddressView + Sync, + { + let endpoints: Vec = addresses.endpoints(selector).collect(); + let packet = self + .signer + .signed_packet(name, &endpoints) + .await + .context(publish_once_error::SignEndpointRecordsSnafu)?; + tracing::debug!( + publisher = %publisher, + name = %name, + endpoint_count = endpoints.len(), + packet_len = packet.len(), + "publishing dns packet" + ); + publisher + .publish(name.as_str(), &packet) + .await + .context(publish_once_error::PublishSnafu { + publisher: publisher.to_string(), + }) + } +} + +pub(crate) fn clear_resolver_publish_state(resolver: &(dyn Resolve + Send + Sync)) { + let any: &dyn Any = resolver; + + if let Some(resolvers) = any.downcast_ref::() { + for resolver in resolvers.iter() { + clear_resolver_publish_state(resolver.as_ref()); + } + } + + #[cfg(feature = "h3x-resolver")] + if let Some(h3) = + any.downcast_ref::>() + { + h3.clear_pool(); + } +} diff --git a/src/publisher/packet.rs b/src/publisher/packet.rs new file mode 100644 index 0000000..7528e8a --- /dev/null +++ b/src/publisher/packet.rs @@ -0,0 +1,88 @@ +use std::{collections::HashMap, sync::Arc}; + +use dhttp_identity::{identity::LocalAuthority, name::Name}; +use dquic::qbase::net::addr::EndpointAddr; +use snafu::{ResultExt, Snafu}; + +use super::PublishOptions; +use crate::core::{ + MdnsPacket, + parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, +}; + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum SignEndpointRecordsError { + #[snafu(display("failed to encode endpoint address"))] + EncodeEndpoint, + #[snafu(display("failed to sign endpoint address"))] + SignEndpoint { source: SignEndpointError }, +} + +pub struct EndpointRecordSigner { + authority: Arc, + options: PublishOptions, +} + +impl std::fmt::Debug for EndpointRecordSigner +where + A: LocalAuthority, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EndpointRecordSigner") + .field("authority", &self.authority.name()) + .field("options", &self.options) + .finish() + } +} + +impl EndpointRecordSigner +where + A: LocalAuthority + Send + Sync + ?Sized, +{ + pub fn new(authority: Arc) -> Self { + Self { + authority, + options: PublishOptions::default(), + } + } + + pub fn with_options(mut self, options: PublishOptions) -> Self { + self.options = options; + self + } + + pub fn options(&self) -> PublishOptions { + self.options + } + + pub fn authority(&self) -> &Arc { + &self.authority + } + + pub async fn signed_packet( + &self, + name: &Name<'_>, + endpoints: &[EndpointAddr], + ) -> Result, SignEndpointRecordsError> { + let mut signed = Vec::with_capacity(endpoints.len()); + for endpoint in endpoints { + let Ok(mut endpoint) = DnsEndpointAddr::try_from(*endpoint) else { + return sign_endpoint_records_error::EncodeEndpointSnafu.fail(); + }; + if let Some(server_id) = self.options.server_id { + endpoint.set_main(server_id == 0); + endpoint.set_sequence(server_id.into()); + } + endpoint + .sign_with_authority(self.authority.as_ref()) + .await + .context(sign_endpoint_records_error::SignEndpointSnafu)?; + signed.push(endpoint); + } + + let mut hosts = HashMap::new(); + hosts.insert(name.as_str().to_owned(), signed); + Ok(MdnsPacket::answer(0, &hosts).to_bytes()) + } +} From c267e7f9ff7e4ab24c9e481b92f9a6982bbf504f Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 9 Jun 2026 14:26:09 +0800 Subject: [PATCH 73/85] chore: clean repository artifacts --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 + .vscode/settings.json | 5 - Cargo.toml | 3 - patches/proc-macro-error2/.gitignore | 4 - patches/proc-macro-error2/CHANGELOG.md | 180 ------ patches/proc-macro-error2/Cargo.toml | 41 -- patches/proc-macro-error2/LICENSE-APACHE | 201 ------- patches/proc-macro-error2/LICENSE-MIT | 21 - patches/proc-macro-error2/README.md | 250 -------- .../proc-macro-error-attr/.gitignore | 4 - .../proc-macro-error-attr/Cargo.toml | 22 - .../proc-macro-error-attr/LICENSE-APACHE | 201 ------- .../proc-macro-error-attr/LICENSE-MIT | 21 - .../proc-macro-error-attr/src/lib.rs | 111 ---- .../proc-macro-error-attr/src/parse.rs | 89 --- .../proc-macro-error-attr/src/settings.rs | 72 --- patches/proc-macro-error2/src/diagnostic.rs | 360 ----------- patches/proc-macro-error2/src/dummy.rs | 151 ----- patches/proc-macro-error2/src/imp/delegate.rs | 68 --- patches/proc-macro-error2/src/imp/fallback.rs | 30 - patches/proc-macro-error2/src/lib.rs | 565 ------------------ patches/proc-macro-error2/src/macros.rs | 288 --------- patches/proc-macro-error2/src/sealed.rs | 3 - .../proc-macro-error2/test-crate/.gitignore | 4 - .../proc-macro-error2/test-crate/Cargo.toml | 26 - patches/proc-macro-error2/test-crate/lib.rs | 272 --------- .../proc-macro-error2/tests/macro-errors.rs | 6 - patches/proc-macro-error2/tests/ok.rs | 8 - .../proc-macro-error2/tests/runtime-errors.rs | 13 - patches/proc-macro-error2/tests/ui/abort.rs | 10 - .../proc-macro-error2/tests/ui/abort.stderr | 48 -- .../tests/ui/append_dummy.rs | 12 - .../tests/ui/append_dummy.stderr | 5 - .../tests/ui/children_messages.rs | 5 - .../tests/ui/children_messages.stderr | 23 - patches/proc-macro-error2/tests/ui/dummy.rs | 12 - .../proc-macro-error2/tests/ui/dummy.stderr | 5 - patches/proc-macro-error2/tests/ui/emit.rs | 6 - .../proc-macro-error2/tests/ui/emit.stderr | 48 -- .../tests/ui/explicit_span_range.rs | 5 - .../tests/ui/explicit_span_range.stderr | 5 - patches/proc-macro-error2/tests/ui/misuse.rs | 10 - .../proc-macro-error2/tests/ui/misuse.stderr | 24 - .../tests/ui/multiple_tokens.rs | 4 - .../tests/ui/multiple_tokens.stderr | 5 - .../tests/ui/not_proc_macro.rs | 4 - .../tests/ui/not_proc_macro.stderr | 9 - .../proc-macro-error2/tests/ui/option_ext.rs | 5 - .../tests/ui/option_ext.stderr | 7 - .../proc-macro-error2/tests/ui/result_ext.rs | 6 - .../tests/ui/result_ext.stderr | 11 - .../tests/ui/to_tokens_span.rs | 5 - .../tests/ui/to_tokens_span.stderr | 11 - .../tests/ui/unknown_setting.rs | 4 - .../tests/ui/unknown_setting.stderr | 5 - .../tests/ui/unrelated_panic.rs | 5 - .../tests/ui/unrelated_panic.stderr | 7 - 58 files changed, 3 insertions(+), 3325 deletions(-) delete mode 100644 .DS_Store delete mode 100644 .vscode/settings.json delete mode 100644 patches/proc-macro-error2/.gitignore delete mode 100644 patches/proc-macro-error2/CHANGELOG.md delete mode 100644 patches/proc-macro-error2/Cargo.toml delete mode 100644 patches/proc-macro-error2/LICENSE-APACHE delete mode 100644 patches/proc-macro-error2/LICENSE-MIT delete mode 100644 patches/proc-macro-error2/README.md delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/.gitignore delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs delete mode 100644 patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs delete mode 100644 patches/proc-macro-error2/src/diagnostic.rs delete mode 100644 patches/proc-macro-error2/src/dummy.rs delete mode 100644 patches/proc-macro-error2/src/imp/delegate.rs delete mode 100644 patches/proc-macro-error2/src/imp/fallback.rs delete mode 100644 patches/proc-macro-error2/src/lib.rs delete mode 100644 patches/proc-macro-error2/src/macros.rs delete mode 100644 patches/proc-macro-error2/src/sealed.rs delete mode 100644 patches/proc-macro-error2/test-crate/.gitignore delete mode 100644 patches/proc-macro-error2/test-crate/Cargo.toml delete mode 100644 patches/proc-macro-error2/test-crate/lib.rs delete mode 100644 patches/proc-macro-error2/tests/macro-errors.rs delete mode 100644 patches/proc-macro-error2/tests/ok.rs delete mode 100644 patches/proc-macro-error2/tests/runtime-errors.rs delete mode 100644 patches/proc-macro-error2/tests/ui/abort.rs delete mode 100644 patches/proc-macro-error2/tests/ui/abort.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/append_dummy.rs delete mode 100644 patches/proc-macro-error2/tests/ui/append_dummy.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/children_messages.rs delete mode 100644 patches/proc-macro-error2/tests/ui/children_messages.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/dummy.rs delete mode 100644 patches/proc-macro-error2/tests/ui/dummy.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/emit.rs delete mode 100644 patches/proc-macro-error2/tests/ui/emit.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/explicit_span_range.rs delete mode 100644 patches/proc-macro-error2/tests/ui/explicit_span_range.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/misuse.rs delete mode 100644 patches/proc-macro-error2/tests/ui/misuse.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/multiple_tokens.rs delete mode 100644 patches/proc-macro-error2/tests/ui/multiple_tokens.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/not_proc_macro.rs delete mode 100644 patches/proc-macro-error2/tests/ui/not_proc_macro.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/option_ext.rs delete mode 100644 patches/proc-macro-error2/tests/ui/option_ext.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/result_ext.rs delete mode 100644 patches/proc-macro-error2/tests/ui/result_ext.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/to_tokens_span.rs delete mode 100644 patches/proc-macro-error2/tests/ui/to_tokens_span.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/unknown_setting.rs delete mode 100644 patches/proc-macro-error2/tests/ui/unknown_setting.stderr delete mode 100644 patches/proc-macro-error2/tests/ui/unrelated_panic.rs delete mode 100644 patches/proc-macro-error2/tests/ui/unrelated_panic.stderr diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b4ef695316a7ea9aaab5d8a528cf99e9dd0f88ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-EW?5S}$5KZ+25!5Y%sWm*h5=*eN12u>0-p%-nqUakqO*M5;0j%0vYs%Aql)m(k5J_H#?vlAT!x zD!xa%RjxI{PB#%L6QY19@D~-}Z#PXvYSB3jobNYR3*x>`SgqFEVI6b1(#`wb?bgG- z>kq#5ch6#L!{!rGDT}%n)Tai86j0NtPQKYSTJ7qu>P;-;*=i*tE-C`l=3)T4&W=5svt#5wG? zCLC;DC!AJJdYyXen(Jj~02O+8Cz|d%ky(2k&HCfDM{nEty^?-@=N@qei(=wDGCMx= z5c^pLuxGR7Rt!on3Wx%tz(fIFA3QWh-(qA?FCFOg5dfIRv^IA1prq zyGe2-3Wx&#N&%Iw)~XdelHFSq56640hjxO-#&MBBor2D8$9lk9@dlbU_&go}eT$Jn R%)sPFK+7PVDDbNad;w)Lr@R0F diff --git a/.gitignore b/.gitignore index 1daef7f..57506c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ Cargo.lock *.log build + +.DS_Store +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 2afd034..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "lldb.showDisassembly": "auto", - "lldb.dereferencePointers": true, - "lldb.consoleMode": "commands" -} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7baa1c1..87fb36a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,3 @@ required-features = ["h3x-resolver"] name = "query" path = "examples/query.rs" required-features = ["h3x-resolver"] - -[patch.crates-io] -proc-macro-error2 = { path = "patches/proc-macro-error2" } diff --git a/patches/proc-macro-error2/.gitignore b/patches/proc-macro-error2/.gitignore deleted file mode 100644 index 5e81b66..0000000 --- a/patches/proc-macro-error2/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock -.fuse_hidden* diff --git a/patches/proc-macro-error2/CHANGELOG.md b/patches/proc-macro-error2/CHANGELOG.md deleted file mode 100644 index cad92f6..0000000 --- a/patches/proc-macro-error2/CHANGELOG.md +++ /dev/null @@ -1,180 +0,0 @@ -# v2.0.1 (2024-09-06) - -* Fixed a span location issue due to mistake in refactoring (#2) - -# v2.0.0 (2024-09-05) - -No changes, simply releasing pre-release as full release. - -# v2.0.0-pre.1 (2024-09-01) - -* __Crate has been renamed to `proc-macro-error2`, due to the old maintainer's inactivity.__ - -* `syn` has been upgraded to `2` -* MSRV has been bumped to `1.61` -* Warnings have been fixed, including `clippy::pedantic` lints -* CI has been converted to GitHub actions, and testing infrastructure significantly simplified. -* Automatic nightly detection has been removed, use the `nightly` feature for improved diagnostics at the cost of stability. - -# v1.0.4 (2020-7-31) - -* `SpanRange` facility is now public. -* Docs have been improved. -* Introduced the `syn-error` feature so you can opt-out from the `syn` dependency. - -# v1.0.3 (2020-6-26) - -* Corrected a few typos. -* Fixed the `emit_call_site_warning` macro. - -# v1.0.2 (2020-4-9) - -* An obsolete note was removed from documentation. - -# v1.0.1 (2020-4-9) - -* `proc-macro-hack` is now well tested and supported. Not sure about `proc-macro-nested`, - please fill a request if you need it. -* Fixed `emit_call_site_error`. -* Documentation improvements. - -# v1.0.0 (2020-3-25) - -I believe the API can be considered stable because it's been a few months without -breaking changes, and I also don't think this crate will receive much further evolution. -It's perfect, admit it. - -Hence, meet the new, stable release! - -### Improvements - -* Supported nested `#[proc_macro_error]` attributes. Well, you aren't supposed to do that, - but I caught myself doing it by accident on one occasion and the behavior was... surprising. - Better to handle this smooth. - -# v0.4.12 (2020-3-23) - -* Error message on macros' misuse is now a bit more understandable. - -# v0.4.11 (2020-3-02) - -* `build.rs` no longer fails when `rustc` date could not be determined, - (thanks to [`Fabian Möller`](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/8) - for noticing and to [`Igor Gnatenko`](https://gitlab.com/CreepySkeleton/proc-macro-error/-/merge_requests/25) - for fixing). - -# v0.4.10 (2020-2-29) - -* `proc-macro-error` doesn't depend on syn\[full\] anymore, the compilation - is \~30secs faster. - -# v0.4.9 (2020-2-13) - -* New function: `append_dummy`. - -# v0.4.8 (2020-2-01) - -* Support for children messages - -# v0.4.7 (2020-1-31) - -* Now any type that implements `quote::ToTokens` can be used instead of spans. - This allows for high quality error messages. - -# v0.4.6 (2020-1-31) - -* `From` implementation doesn't lose span info anymore, see - [#6](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/6). - -# v0.4.5 (2020-1-20) -Just a small intermediate release. - -* Fix some bugs. -* Populate license files into subfolders. - -# v0.4.4 (2019-11-13) -* Fix `abort_if_dirty` + warnings bug -* Allow trailing commas in macros - -# v0.4.2 (2019-11-7) -* FINALLY fixed `__pme__suggestions not found` bug - -# v0.4.1 (2019-11-7) YANKED -* Fixed `__pme__suggestions not found` bug -* Documentation improvements, links checked - -# v0.4.0 (2019-11-6) YANKED - -## New features -* "help" messages that can have their own span on nightly, they - inherit parent span on stable. - ```rust - let cond_help = if condition { Some("some help message") else { None } }; - abort!( - span, // parent span - "something's wrong, {} wrongs in total", 10; // main message - help = "here's a help for you, {}", "take it"; // unconditional help message - help =? cond_help; // conditional help message, must be Option - note = note_span => "don't forget the note, {}", "would you?" // notes can have their own span but it's effective only on nightly - ) - ``` -* Warnings via `emit_warning` and `emit_warning_call_site`. Nightly only, they're ignored on stable. -* Now `proc-macro-error` delegates to `proc_macro::Diagnostic` on nightly. - -## Breaking changes -* `MacroError` is now replaced by `Diagnostic`. Its API resembles `proc_macro::Diagnostic`. -* `Diagnostic` does not implement `From<&str/String>` so `Result::abort_or_exit()` - won't work anymore (nobody used it anyway). -* `macro_error!` macro is replaced with `diagnostic!`. - -## Improvements -* Now `proc-macro-error` renders notes exactly just like rustc does. -* We don't parse a body of a function annotated with `#[proc_macro_error]` anymore, - only looking at the signature. This should somewhat decrease expansion time for large functions. - -# v0.3.3 (2019-10-16) -* Now you can use any word instead of "help", undocumented. - -# v0.3.2 (2019-10-16) -* Introduced support for "help" messages, undocumented. - -# v0.3.0 (2019-10-8) - -## The crate has been completely rewritten from scratch! - -## Changes (most are breaking): -* Renamed macros: - * `span_error` => `abort` - * `call_site_error` => `abort_call_site` -* `filter_macro_errors` was replaced by `#[proc_macro_error]` attribute. -* `set_dummy` now takes `TokenStream` instead of `Option` -* Support for multiple errors via `emit_error` and `emit_call_site_error` -* New `macro_error` macro for building errors in format=like style. -* `MacroError` API had been reconsidered. It also now implements `quote::ToTokens`. - -# v0.2.6 (2019-09-02) -* Introduce support for dummy implementations via `dummy::set_dummy` -* `multi::*` is now deprecated, will be completely rewritten in v0.3 - -# v0.2.0 (2019-08-15) - -## Breaking changes -* `trigger_error` replaced with `MacroError::trigger` and `filter_macro_error_panics` - is hidden from docs. - This is not quite a breaking change since users weren't supposed to use these functions directly anyway. -* All dependencies are updated to `v1.*`. - -## New features -* Ability to stack multiple errors via `multi::MultiMacroErrors` and emit them at once. - -## Improvements -* Now `MacroError` implements `std::fmt::Display` instead of `std::string::ToString`. -* `MacroError::span` inherent method. -* `From for proc_macro/proc_macro2::TokenStream` implementations. -* `AsRef/AsMut for MacroError` implementations. - -# v0.1.x (2019-07-XX) - -## New features -* An easy way to report errors inside within a proc-macro via `span_error`, - `call_site_error` and `filter_macro_errors`. diff --git a/patches/proc-macro-error2/Cargo.toml b/patches/proc-macro-error2/Cargo.toml deleted file mode 100644 index fcc6161..0000000 --- a/patches/proc-macro-error2/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "proc-macro-error2" -authors = [ - "CreepySkeleton ", - "GnomedDev ", -] -version = "2.0.1" -description = "Almost drop-in replacement to panics in proc-macros" -repository = "https://github.com/GnomedDev/proc-macro-error-2" -rust-version = "1.61" -keywords = ["proc-macro", "error", "errors"] -categories = ["development-tools::procedural-macro-helpers"] -license = "MIT OR Apache-2.0" -edition = "2021" - -[dependencies] -quote = "1" -proc-macro2 = "1" -proc-macro-error-attr2 = { path = "./proc-macro-error-attr", version = "=2.0.0" } - -[dependencies.syn] -version = "2" -optional = true -default-features = false - -[dev-dependencies] -test-crate = { path = "./test-crate" } -syn = { version = "2", features = ["full"] } -trybuild = { version = "1.0.99", features = ["diff"] } - -[features] -default = ["syn-error"] -syn-error = ["dep:syn"] -nightly = [] - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(run_ui_tests)'] } - -[lints.clippy] -pedantic = { level = "warn", priority = -1 } -module_name_repetitions = { level = "allow" } diff --git a/patches/proc-macro-error2/LICENSE-APACHE b/patches/proc-macro-error2/LICENSE-APACHE deleted file mode 100644 index cc17374..0000000 --- a/patches/proc-macro-error2/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2019-2020 CreepySkeleton - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/patches/proc-macro-error2/LICENSE-MIT b/patches/proc-macro-error2/LICENSE-MIT deleted file mode 100644 index fc73e59..0000000 --- a/patches/proc-macro-error2/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-2020 CreepySkeleton - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/patches/proc-macro-error2/README.md b/patches/proc-macro-error2/README.md deleted file mode 100644 index 0910eac..0000000 --- a/patches/proc-macro-error2/README.md +++ /dev/null @@ -1,250 +0,0 @@ -# Makes error reporting in procedural macros nice and easy - -[![docs.rs](https://docs.rs/proc-macro-error2/badge.svg)](https://docs.rs/proc-macro-error2) -[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) - -This crate aims to make error reporting in proc-macros simple and easy to use. -Migrate from `panic!`-based errors for as little effort as possible! - -Also, you can explicitly [append a dummy token stream][crate::dummy] to your errors. - -To achieve this, this crate serves as a tiny shim around `proc_macro::Diagnostic` and -`compile_error!`. It detects the most preferable way to emit errors based on compiler's version. -When the underlying diagnostic type is finally stabilized, this crate will be simply -delegating to it, requiring no changes in your code! - -So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality -available on stable ahead of time and your error-reporting code future-proof. - -```toml -[dependencies] -proc-macro-error2 = "2.0" -``` - -*Supports rustc 1.61 and up* - -[Documentation and guide][guide] - -## Quick example - -Code: - -```rust -#[proc_macro] -#[proc_macro_error] -pub fn make_fn(input: TokenStream) -> TokenStream { - let mut input = TokenStream2::from(input).into_iter(); - let name = input.next().unwrap(); - if let Some(second) = input.next() { - abort! { second, - "I don't like this part!"; - note = "I see what you did there..."; - help = "I need only one part, you know?"; - } - } - - quote!( fn #name() {} ).into() -} -``` - -This is how the error is rendered in a terminal: - -

- -

- -And this is what your users will see in their IDE: - -

- -

- -## Examples - -### Panic-like usage - -```rust -use proc_macro_error2::{ - proc_macro_error, - abort, - abort_call_site, - ResultExt, - OptionExt, -}; -use proc_macro::TokenStream; -use syn::{DeriveInput, parse_macro_input}; -use quote::quote; - -// This is your main entry point -#[proc_macro] -// This attribute *MUST* be placed on top of the #[proc_macro] function -#[proc_macro_error] -pub fn make_answer(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - if let Err(err) = some_logic(&input) { - // we've got a span to blame, let's use it - // This immediately aborts the proc-macro and shows the error - // - // You can use `proc_macro::Span`, `proc_macro2::Span`, and - // anything that implements `quote::ToTokens` (almost every type from - // `syn` and `proc_macro2`) - abort!(err, "You made an error, go fix it: {}", err.msg); - } - - // `Result` has some handy shortcuts if your error type implements - // `Into`. `Option` has one unconditionally. - more_logic(&input).expect_or_abort("What a careless user, behave!"); - - if !more_logic_for_logic_god(&input) { - // We don't have an exact location this time, - // so just highlight the proc-macro invocation itself - abort_call_site!( - "Bad, bad user! Now go stand in the corner and think about what you did!"); - } - - // Now all the processing is done, return `proc_macro::TokenStream` - quote!(/* stuff */).into() -} -``` - -### `proc_macro::Diagnostic`-like usage - -```rust -use proc_macro_error2::*; -use proc_macro::TokenStream; -use syn::{spanned::Spanned, DeriveInput, ItemStruct, Fields, Attribute , parse_macro_input}; -use quote::quote; - -fn process_attrs(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter_map(|attr| match process_attr(attr) { - Ok(res) => Some(res), - Err(msg) => { - emit_error!(attr, "Invalid attribute: {}", msg); - None - } - }) - .collect() -} - -fn process_fields(_attrs: &Fields) -> Vec { - // processing fields in pretty much the same way as attributes - unimplemented!() -} - -#[proc_macro] -#[proc_macro_error] -pub fn make_answer(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - let attrs = process_attrs(&input.attrs); - - // abort right now if some errors were encountered - // at the attributes processing stage - abort_if_dirty(); - - let fields = process_fields(&input.fields); - - // no need to think about emitted errors - // #[proc_macro_error] will handle them for you - // - // just return a TokenStream as you normally would - quote!(/* stuff */).into() -} -``` - -## Real world examples - -* [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) - (abort-like usage) -* [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) - -## Limitations - -- Warnings are emitted only on nightly, they are ignored on stable. -- "help" suggestions can't have their own span info on stable, - (essentially inheriting the parent span). -- If your macro happens to trigger a panic, no errors will be displayed. This is not a - technical limitation but rather intentional design. `panic` is not for error reporting. - -## MSRV policy - -The MSRV is currently `1.61`, and this is considered a breaking change to increase. - -However, if an existing dependency requires a higher MSRV without a semver breaking update, this may be raised. - -## Motivation - -Error handling in proc-macros sucks. There's not much of a choice today: -you either "bubble up" the error up to the top-level of the macro and convert it to -a [`compile_error!`][compl_err] invocation or just use a good old panic. Both these ways suck: - -- Former sucks because it's quite redundant to unroll a proper error handling - just for critical errors that will crash the macro anyway; so people mostly - choose not to bother with it at all and use panic. Simple `.expect` is too tempting. - - Also, if you do decide to implement this `Result`-based architecture in your macro - you're going to have to rewrite it entirely once [`proc_macro::Diagnostic`][] is finally - stable. Not cool. - -- Later sucks because there's no way to carry out the span info via `panic!`. - `rustc` will highlight the invocation itself but not some specific token inside it. - - Furthermore, panics aren't for error-reporting at all; panics are for bug-detecting - (like unwrapping on `None` or out-of-range indexing) or for early development stages - when you need a prototype ASAP so error handling can wait. Mixing these usages only - messes things up. - -- There is [`proc_macro::Diagnostic`][] which is awesome but it has been experimental - for more than a year and is unlikely to be stabilized any time soon. - - This crate's API is intentionally designed to be compatible with `proc_macro::Diagnostic` - and delegates to it whenever possible. Once `Diagnostics` is stable this crate - will **always** delegate to it, no code changes will be required on user side. - -That said, we need a solution, but this solution must meet these conditions: - -- It must be better than `panic!`. The main point: it must offer a way to carry the span information - over to user. -- It must take as little effort as possible to migrate from `panic!`. Ideally, a new - macro with similar semantics plus ability to carry out span info. -- It must maintain compatibility with [`proc_macro::Diagnostic`][] . -- **It must be usable on stable**. - -This crate aims to provide such a mechanism. All you have to do is annotate your top-level -`#[proc_macro]` function with `#[proc_macro_error]` attribute and change panics to -[`abort!`]/[`abort_call_site!`] where appropriate, see [the Guide][guide]. - -## Disclaimer -Please note that **this crate is not intended to be used in any way other -than error reporting in procedural macros**, use `Result` and `?` (possibly along with one of the -many helpers out there) for anything else. - -
- -#### License - - -Licensed under either of
Apache License, Version -2.0 or MIT license at your option. - - -
- - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in this crate by you, as defined in the Apache-2.0 license, shall -be dual licensed as above, without any additional terms or conditions. - - - -[compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html -[`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html - -[crate::dummy]: https://docs.rs/proc-macro-error2/1/proc_macro_error/dummy/index.html -[crate::multi]: https://docs.rs/proc-macro-error2/1/proc_macro_error/multi/index.html - -[`abort_call_site!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort_call_site.html -[`abort!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort.html -[guide]: https://docs.rs/proc-macro-error2 diff --git a/patches/proc-macro-error2/proc-macro-error-attr/.gitignore b/patches/proc-macro-error2/proc-macro-error-attr/.gitignore deleted file mode 100644 index 5e81b66..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock -.fuse_hidden* diff --git a/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml b/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml deleted file mode 100644 index 29a4f0c..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "proc-macro-error-attr2" -version = "2.0.0" -authors = [ - "CreepySkeleton ", - "GnomedDev ", -] -edition = "2021" -rust-version = "1.61" -description = "Attribute macro for the proc-macro-error2 crate" -license = "MIT OR Apache-2.0" -repository = "https://github.com/GnomedDev/proc-macro-error-2" - -[lib] -proc-macro = true - -[dependencies] -quote = "1" -proc-macro2 = "1" - -[lints.clippy] -pedantic = { level = "warn", priority = -1 } diff --git a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE deleted file mode 100644 index 658240a..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2019-2020 CreepySkeleton - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT b/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT deleted file mode 100644 index fc73e59..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-2020 CreepySkeleton - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs deleted file mode 100644 index 0f25931..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/src/lib.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! This is `#[proc_macro_error]` attribute to be used with -//! [`proc-macro-error`](https://docs.rs/proc-macro-error2/). There you go. - -use crate::parse::parse_input; -use crate::parse::Attribute; -use proc_macro::TokenStream; -use proc_macro2::{Literal, Span, TokenStream as TokenStream2, TokenTree}; -use quote::{quote, quote_spanned}; - -use crate::settings::{ - parse_settings, - Setting::{AllowNotMacro, AssertUnwindSafe, ProcMacroHack}, - Settings, -}; - -mod parse; -mod settings; - -type Result = std::result::Result; - -struct Error { - span: Span, - message: String, -} - -impl Error { - fn new(span: Span, message: String) -> Self { - Error { span, message } - } - - fn into_compile_error(self) -> TokenStream2 { - let mut message = Literal::string(&self.message); - message.set_span(self.span); - quote_spanned!(self.span=> compile_error!{#message}) - } -} - -#[proc_macro_attribute] -pub fn proc_macro_error(attr: TokenStream, input: TokenStream) -> TokenStream { - match impl_proc_macro_error(attr.into(), input.clone().into()) { - Ok(ts) => ts, - Err(e) => { - let error = e.into_compile_error(); - let input = TokenStream2::from(input); - - quote!(#input #error).into() - } - } -} - -fn impl_proc_macro_error(attr: TokenStream2, input: TokenStream2) -> Result { - let (attrs, signature, body) = parse_input(input)?; - let mut settings = parse_settings(attr)?; - - let is_proc_macro = is_proc_macro(&attrs); - if is_proc_macro { - settings.set(AssertUnwindSafe); - } - - if detect_proc_macro_hack(&attrs) { - settings.set(ProcMacroHack); - } - - if settings.is_set(ProcMacroHack) { - settings.set(AllowNotMacro); - } - - if !(settings.is_set(AllowNotMacro) || is_proc_macro) { - return Err(Error::new( - Span::call_site(), - "#[proc_macro_error] attribute can be used only with procedural macros\n\n \ - = hint: if you are really sure that #[proc_macro_error] should be applied \ - to this exact function, use #[proc_macro_error(allow_not_macro)]\n" - .into(), - )); - } - - let body = gen_body(&body, &settings); - - let res = quote! { - #(#attrs)* - #(#signature)* - { #body } - }; - Ok(res.into()) -} - -fn gen_body(block: &TokenTree, settings: &Settings) -> proc_macro2::TokenStream { - let is_proc_macro_hack = settings.is_set(ProcMacroHack); - let closure = if settings.is_set(AssertUnwindSafe) { - quote!(::std::panic::AssertUnwindSafe(|| #block )) - } else { - quote!(|| #block) - }; - - quote!( ::proc_macro_error2::entry_point(#closure, #is_proc_macro_hack) ) -} - -fn detect_proc_macro_hack(attrs: &[Attribute]) -> bool { - attrs - .iter() - .any(|attr| attr.path_is_ident("proc_macro_hack")) -} - -fn is_proc_macro(attrs: &[Attribute]) -> bool { - attrs.iter().any(|attr| { - attr.path_is_ident("proc_macro") - || attr.path_is_ident("proc_macro_derive") - || attr.path_is_ident("proc_macro_attribute") - }) -} diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs deleted file mode 100644 index eedb495..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/src/parse.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{Error, Result}; -use proc_macro2::{Delimiter, Ident, Span, TokenStream, TokenTree}; -use quote::ToTokens; -use std::iter::Peekable; - -pub(crate) fn parse_input( - input: TokenStream, -) -> Result<(Vec, Vec, TokenTree)> { - let mut input = input.into_iter().peekable(); - let mut attrs = Vec::new(); - - while let Some(attr) = parse_next_attr(&mut input)? { - attrs.push(attr); - } - - let sig = parse_signature(&mut input); - let body = input.next().ok_or_else(|| { - Error::new( - Span::call_site(), - "`#[proc_macro_error]` can be applied only to functions".to_string(), - ) - })?; - - Ok((attrs, sig, body)) -} - -fn parse_next_attr( - input: &mut Peekable>, -) -> Result> { - let shebang = match input.peek() { - Some(TokenTree::Punct(ref punct)) if punct.as_char() == '#' => input.next().unwrap(), - _ => return Ok(None), - }; - - let group = match input.peek() { - Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => { - let res = group.clone(); - input.next(); - res - } - other => { - let span = other.map_or(Span::call_site(), TokenTree::span); - return Err(Error::new(span, "expected `[`".to_string())); - } - }; - - let path = match group.stream().into_iter().next() { - Some(TokenTree::Ident(ident)) => Some(ident), - _ => None, - }; - - Ok(Some(Attribute { - shebang, - group: TokenTree::Group(group), - path, - })) -} - -fn parse_signature(input: &mut Peekable>) -> Vec { - let mut sig = Vec::new(); - loop { - match input.peek() { - Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => { - return sig; - } - None => return sig, - _ => sig.push(input.next().unwrap()), - } - } -} - -pub(crate) struct Attribute { - pub(crate) shebang: TokenTree, - pub(crate) group: TokenTree, - pub(crate) path: Option, -} - -impl Attribute { - pub(crate) fn path_is_ident(&self, ident: &str) -> bool { - self.path.as_ref().map_or(false, |p| *p == ident) - } -} - -impl ToTokens for Attribute { - fn to_tokens(&self, ts: &mut TokenStream) { - self.shebang.to_tokens(ts); - self.group.to_tokens(ts); - } -} diff --git a/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs b/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs deleted file mode 100644 index f87bd0b..0000000 --- a/patches/proc-macro-error2/proc-macro-error-attr/src/settings.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::{Error, Result}; -use proc_macro2::{Ident, Span, TokenStream, TokenTree}; - -macro_rules! decl_settings { - ($($val:expr => $variant:ident),+ $(,)*) => { - #[derive(PartialEq, Clone, Copy)] - pub(crate) enum Setting { - $($variant),* - } - - fn ident_to_setting(ident: Ident) -> Result { - match &*ident.to_string() { - $($val => Ok(Setting::$variant),)* - _ => { - let possible_vals = [$($val),*] - .iter() - .map(|v| format!("`{}`", v)) - .collect::>() - .join(", "); - - Err(Error::new( - ident.span(), - format!("unknown setting `{}`, expected one of {}", ident, possible_vals))) - } - } - } - }; -} - -decl_settings! { - "assert_unwind_safe" => AssertUnwindSafe, - "allow_not_macro" => AllowNotMacro, - "proc_macro_hack" => ProcMacroHack, -} - -pub(crate) fn parse_settings(input: TokenStream) -> Result { - let mut input = input.into_iter(); - let mut res = Settings(Vec::new()); - loop { - match input.next() { - Some(TokenTree::Ident(ident)) => { - res.0.push(ident_to_setting(ident)?); - } - None => return Ok(res), - other => { - let span = other.map_or(Span::call_site(), |tt| tt.span()); - return Err(Error::new(span, "expected identifier".to_string())); - } - } - - match input.next() { - Some(TokenTree::Punct(ref punct)) if punct.as_char() == ',' => {} - None => return Ok(res), - other => { - let span = other.map_or(Span::call_site(), |tt| tt.span()); - return Err(Error::new(span, "expected `,`".to_string())); - } - } - } -} - -pub(crate) struct Settings(Vec); - -impl Settings { - pub(crate) fn is_set(&self, setting: Setting) -> bool { - self.0.iter().any(|s| *s == setting) - } - - pub(crate) fn set(&mut self, setting: Setting) { - self.0.push(setting); - } -} diff --git a/patches/proc-macro-error2/src/diagnostic.rs b/patches/proc-macro-error2/src/diagnostic.rs deleted file mode 100644 index 0455511..0000000 --- a/patches/proc-macro-error2/src/diagnostic.rs +++ /dev/null @@ -1,360 +0,0 @@ -use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange}; -use proc_macro2::Span; -use proc_macro2::TokenStream; - -use quote::{quote_spanned, ToTokens}; - -/// Represents a diagnostic level -/// -/// # Warnings -/// -/// Warnings are ignored on stable/beta -#[derive(Debug, PartialEq)] -#[non_exhaustive] -pub enum Level { - Error, - Warning, -} - -/// Represents a single diagnostic message -#[derive(Debug)] -#[must_use = "A diagnostic does nothing unless emitted"] -pub struct Diagnostic { - pub(crate) level: Level, - pub(crate) span_range: SpanRange, - pub(crate) msg: String, - pub(crate) suggestions: Vec<(SuggestionKind, String, Option)>, - pub(crate) children: Vec<(SpanRange, String)>, -} - -/// A collection of methods that do not exist in `proc_macro::Diagnostic` -/// but still useful to have around. -/// -/// This trait is sealed and cannot be implemented outside of `proc_macro_error`. -pub trait DiagnosticExt: Sealed { - /// Create a new diagnostic message that points to the `span_range`. - /// - /// This function is the same as `Diagnostic::spanned` but produces considerably - /// better error messages for multi-token spans on stable. - fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self; - - /// Add another error message to self such that it will be emitted right after - /// the main message. - /// - /// This function is the same as `Diagnostic::span_error` but produces considerably - /// better error messages for multi-token spans on stable. - #[must_use] - fn span_range_error(self, span_range: SpanRange, msg: String) -> Self; - - /// Attach a "help" note to your main message, the note will have it's own span on nightly. - /// - /// This function is the same as `Diagnostic::span_help` but produces considerably - /// better error messages for multi-token spans on stable. - /// - /// # Span - /// - /// The span is ignored on stable, the note effectively inherits its parent's (main message) span - #[must_use] - fn span_range_help(self, span_range: SpanRange, msg: String) -> Self; - - /// Attach a note to your main message, the note will have it's own span on nightly. - /// - /// This function is the same as `Diagnostic::span_note` but produces considerably - /// better error messages for multi-token spans on stable. - /// - /// # Span - /// - /// The span is ignored on stable, the note effectively inherits its parent's (main message) span - #[must_use] - fn span_range_note(self, span_range: SpanRange, msg: String) -> Self; -} - -impl DiagnosticExt for Diagnostic { - fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self { - Diagnostic { - level, - span_range, - msg: message, - suggestions: vec![], - children: vec![], - } - } - - fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self { - self.children.push((span_range, msg)); - self - } - - fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self { - self.suggestions - .push((SuggestionKind::Help, msg, Some(span_range))); - self - } - - fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self { - self.suggestions - .push((SuggestionKind::Note, msg, Some(span_range))); - self - } -} - -impl Diagnostic { - /// Create a new diagnostic message that points to `Span::call_site()` - pub fn new(level: Level, message: String) -> Self { - Diagnostic::spanned(Span::call_site(), level, message) - } - - /// Create a new diagnostic message that points to the `span` - pub fn spanned(span: Span, level: Level, message: String) -> Self { - Diagnostic::spanned_range( - SpanRange { - first: span, - last: span, - }, - level, - message, - ) - } - - /// Add another error message to self such that it will be emitted right after - /// the main message. - pub fn span_error(self, span: Span, msg: String) -> Self { - self.span_range_error( - SpanRange { - first: span, - last: span, - }, - msg, - ) - } - - /// Attach a "help" note to your main message, the note will have it's own span on nightly. - /// - /// # Span - /// - /// The span is ignored on stable, the note effectively inherits its parent's (main message) span - pub fn span_help(self, span: Span, msg: String) -> Self { - self.span_range_help( - SpanRange { - first: span, - last: span, - }, - msg, - ) - } - - /// Attach a "help" note to your main message. - pub fn help(mut self, msg: String) -> Self { - self.suggestions.push((SuggestionKind::Help, msg, None)); - self - } - - /// Attach a note to your main message, the note will have it's own span on nightly. - /// - /// # Span - /// - /// The span is ignored on stable, the note effectively inherits its parent's (main message) span - pub fn span_note(self, span: Span, msg: String) -> Self { - self.span_range_note( - SpanRange { - first: span, - last: span, - }, - msg, - ) - } - - /// Attach a note to your main message - pub fn note(mut self, msg: String) -> Self { - self.suggestions.push((SuggestionKind::Note, msg, None)); - self - } - - /// The message of main warning/error (no notes attached) - #[must_use] - pub fn message(&self) -> &str { - &self.msg - } - - /// Abort the proc-macro's execution and display the diagnostic. - /// - /// # Warnings - /// - /// Warnings are not emitted on stable and beta, but this function will abort anyway. - pub fn abort(self) -> ! { - self.emit(); - abort_now() - } - - /// Display the diagnostic while not aborting macro execution. - /// - /// # Warnings - /// - /// Warnings are ignored on stable/beta - pub fn emit(self) { - check_correctness(); - crate::imp::emit_diagnostic(self); - } -} - -/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!** -#[doc(hidden)] -impl Diagnostic { - pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self { - match suggestion { - "help" | "hint" => self.span_help(span, msg), - _ => self.span_note(span, msg), - } - } - - pub fn suggestion(self, suggestion: &str, msg: String) -> Self { - match suggestion { - "help" | "hint" => self.help(msg), - _ => self.note(msg), - } - } -} - -impl ToTokens for Diagnostic { - fn to_tokens(&self, ts: &mut TokenStream) { - use std::borrow::Cow; - - fn ensure_lf(buf: &mut String, s: &str) { - if s.ends_with('\n') { - buf.push_str(s); - } else { - buf.push_str(s); - buf.push('\n'); - } - } - - fn diag_to_tokens( - span_range: SpanRange, - level: &Level, - msg: &str, - suggestions: &[(SuggestionKind, String, Option)], - ) -> TokenStream { - if *level == Level::Warning { - return TokenStream::new(); - } - - let message = if suggestions.is_empty() { - Cow::Borrowed(msg) - } else { - let mut message = String::new(); - ensure_lf(&mut message, msg); - message.push('\n'); - - for (kind, note, _span) in suggestions { - message.push_str(" = "); - message.push_str(kind.name()); - message.push_str(": "); - ensure_lf(&mut message, note); - } - message.push('\n'); - - Cow::Owned(message) - }; - - let mut msg = proc_macro2::Literal::string(&message); - msg.set_span(span_range.last); - let group = quote_spanned!(span_range.last=> { #msg } ); - quote_spanned!(span_range.first=> compile_error!#group) - } - - ts.extend(diag_to_tokens( - self.span_range, - &self.level, - &self.msg, - &self.suggestions, - )); - ts.extend( - self.children - .iter() - .map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, msg, &[])), - ); - } -} - -#[derive(Debug)] -pub(crate) enum SuggestionKind { - Help, - Note, -} - -impl SuggestionKind { - fn name(&self) -> &'static str { - match self { - SuggestionKind::Note => "note", - SuggestionKind::Help => "help", - } - } -} - -#[cfg(feature = "syn-error")] -impl From for Diagnostic { - fn from(err: syn::Error) -> Self { - use proc_macro2::{Delimiter, TokenTree}; - - fn gut_error(ts: &mut impl Iterator) -> Option<(SpanRange, String)> { - let start_span = ts.next()?.span(); - ts.next().expect(":1"); - ts.next().expect("core"); - ts.next().expect(":2"); - ts.next().expect(":3"); - ts.next().expect("compile_error"); - ts.next().expect("!"); - - let lit = match ts.next().unwrap() { - TokenTree::Group(group) => { - // Currently `syn` builds `compile_error!` invocations - // exclusively in `ident{"..."}` (braced) form which is not - // followed by `;` (semicolon). - // - // But if it changes to `ident("...");` (parenthesized) - // or `ident["..."];` (bracketed) form, - // we will need to skip the `;` as well. - // Highly unlikely, but better safe than sorry. - - if group.delimiter() == Delimiter::Parenthesis - || group.delimiter() == Delimiter::Bracket - { - ts.next().unwrap(); // ; - } - - match group.stream().into_iter().next().unwrap() { - TokenTree::Literal(lit) => lit, - _ => unreachable!(""), - } - } - _ => unreachable!(""), - }; - - let last = lit.span(); - let mut msg = lit.to_string(); - - // "abc" => abc - msg.pop(); - msg.remove(0); - - Some(( - SpanRange { - first: start_span, - last, - }, - msg, - )) - } - - let mut ts = err.to_compile_error().into_iter(); - - let (span_range, msg) = gut_error(&mut ts).unwrap(); - let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg); - - while let Some((span_range, msg)) = gut_error(&mut ts) { - res = res.span_range_error(span_range, msg); - } - - res - } -} diff --git a/patches/proc-macro-error2/src/dummy.rs b/patches/proc-macro-error2/src/dummy.rs deleted file mode 100644 index 5bc98bd..0000000 --- a/patches/proc-macro-error2/src/dummy.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Facility to emit dummy implementations (or whatever) in case -//! an error happen. -//! -//! `compile_error!` does not abort a compilation right away. This means -//! `rustc` doesn't just show you the error and abort, it carries on the -//! compilation process looking for other errors to report. -//! -//! Let's consider an example: -//! -//! ```rust,ignore -//! use proc_macro::TokenStream; -//! use proc_macro_error2::*; -//! -//! trait MyTrait { -//! fn do_thing(); -//! } -//! -//! // this proc macro is supposed to generate MyTrait impl -//! #[proc_macro_derive(MyTrait)] -//! #[proc_macro_error] -//! fn example(input: TokenStream) -> TokenStream { -//! // somewhere deep inside -//! abort!(span, "something's wrong"); -//! -//! // this implementation will be generated if no error happened -//! quote! { -//! impl MyTrait for #name { -//! fn do_thing() {/* whatever */} -//! } -//! } -//! } -//! -//! // ================ -//! // in main.rs -//! -//! // this derive triggers an error -//! #[derive(MyTrait)] // first BOOM! -//! struct Foo; -//! -//! fn main() { -//! Foo::do_thing(); // second BOOM! -//! } -//! ``` -//! -//! The problem is: the generated token stream contains only `compile_error!` -//! invocation, the impl was not generated. That means user will see two compilation -//! errors: -//! -//! ```text -//! error: something's wrong -//! --> $DIR/probe.rs:9:10 -//! | -//! 9 |#[proc_macro_derive(MyTrait)] -//! | ^^^^^^^ -//! -//! error[E0599]: no function or associated item named `do_thing` found for type `Foo` in the current scope -//! --> src\main.rs:3:10 -//! | -//! 1 | struct Foo; -//! | ----------- function or associated item `do_thing` not found for this -//! 2 | fn main() { -//! 3 | Foo::do_thing(); // second BOOM! -//! | ^^^^^^^^ function or associated item not found in `Foo` -//! ``` -//! -//! But the second error is meaningless! We definitely need to fix this. -//! -//! Most used approach in cases like this is "dummy implementation" - -//! omit `impl MyTrait for #name` and fill functions bodies with `unimplemented!()`. -//! -//! This is how you do it: -//! -//! ```rust,ignore -//! use proc_macro::TokenStream; -//! use proc_macro_error2::*; -//! -//! trait MyTrait { -//! fn do_thing(); -//! } -//! -//! // this proc macro is supposed to generate MyTrait impl -//! #[proc_macro_derive(MyTrait)] -//! #[proc_macro_error] -//! fn example(input: TokenStream) -> TokenStream { -//! // first of all - we set a dummy impl which will be appended to -//! // `compile_error!` invocations in case a trigger does happen -//! set_dummy(quote! { -//! impl MyTrait for #name { -//! fn do_thing() { unimplemented!() } -//! } -//! }); -//! -//! // somewhere deep inside -//! abort!(span, "something's wrong"); -//! -//! // this implementation will be generated if no error happened -//! quote! { -//! impl MyTrait for #name { -//! fn do_thing() {/* whatever */} -//! } -//! } -//! } -//! -//! // ================ -//! // in main.rs -//! -//! // this derive triggers an error -//! #[derive(MyTrait)] // first BOOM! -//! struct Foo; -//! -//! fn main() { -//! Foo::do_thing(); // no more errors! -//! } -//! ``` - -use proc_macro2::TokenStream; -use std::cell::RefCell; - -use crate::check_correctness; - -thread_local! { - static DUMMY_IMPL: RefCell> = const { RefCell::new(None) }; -} - -/// Sets dummy token stream which will be appended to `compile_error!(msg);...` -/// invocations in case you'll emit any errors. -/// -/// See [guide](../index.html#guide). -#[allow(clippy::must_use_candidate)] // Mutates thread local state -pub fn set_dummy(dummy: TokenStream) -> Option { - check_correctness(); - DUMMY_IMPL.with(|old_dummy| old_dummy.replace(Some(dummy))) -} - -/// Same as [`set_dummy`] but, instead of resetting, appends tokens to the -/// existing dummy (if any). Behaves as `set_dummy` if no dummy is present. -pub fn append_dummy(dummy: TokenStream) { - check_correctness(); - DUMMY_IMPL.with(|old_dummy| { - let mut cell = old_dummy.borrow_mut(); - if let Some(ts) = cell.as_mut() { - ts.extend(dummy); - } else { - *cell = Some(dummy); - } - }); -} - -pub(crate) fn cleanup() -> Option { - DUMMY_IMPL.with(|old_dummy| old_dummy.replace(None)) -} diff --git a/patches/proc-macro-error2/src/imp/delegate.rs b/patches/proc-macro-error2/src/imp/delegate.rs deleted file mode 100644 index a3e6a80..0000000 --- a/patches/proc-macro-error2/src/imp/delegate.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! This implementation uses [`proc_macro::Diagnostic`], nightly only. - -use std::cell::Cell; - -use proc_macro::{Diagnostic as PDiag, Level as PLevel}; - -use crate::{ - abort_now, check_correctness, - diagnostic::{Diagnostic, Level, SuggestionKind}, -}; - -pub fn abort_if_dirty() { - check_correctness(); - if IS_DIRTY.with(|c| c.get()) { - abort_now() - } -} - -pub(crate) fn cleanup() -> Vec { - IS_DIRTY.with(|c| c.set(false)); - vec![] -} - -pub(crate) fn emit_diagnostic(diag: Diagnostic) { - let Diagnostic { - level, - span_range, - msg, - suggestions, - children, - } = diag; - - let span = span_range.collapse().unwrap(); - - let level = match level { - Level::Warning => PLevel::Warning, - Level::Error => { - IS_DIRTY.with(|c| c.set(true)); - PLevel::Error - } - }; - - let mut res = PDiag::spanned(span, level, msg); - - for (kind, msg, span) in suggestions { - res = match (kind, span) { - (SuggestionKind::Note, Some(span_range)) => { - res.span_note(span_range.collapse().unwrap(), msg) - } - (SuggestionKind::Help, Some(span_range)) => { - res.span_help(span_range.collapse().unwrap(), msg) - } - (SuggestionKind::Note, None) => res.note(msg), - (SuggestionKind::Help, None) => res.help(msg), - } - } - - for (span_range, msg) in children { - let span = span_range.collapse().unwrap(); - res = res.span_error(span, msg); - } - - res.emit() -} - -thread_local! { - static IS_DIRTY: Cell = Cell::new(false); -} diff --git a/patches/proc-macro-error2/src/imp/fallback.rs b/patches/proc-macro-error2/src/imp/fallback.rs deleted file mode 100644 index c10eb9a..0000000 --- a/patches/proc-macro-error2/src/imp/fallback.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! This implementation uses self-written stable facilities. - -use crate::{ - abort_now, check_correctness, - diagnostic::{Diagnostic, Level}, -}; -use std::cell::RefCell; - -pub fn abort_if_dirty() { - check_correctness(); - ERR_STORAGE.with(|storage| { - if !storage.borrow().is_empty() { - abort_now() - } - }); -} - -pub(crate) fn cleanup() -> Vec { - ERR_STORAGE.with(|storage| storage.replace(Vec::new())) -} - -pub(crate) fn emit_diagnostic(diag: Diagnostic) { - if diag.level == Level::Error { - ERR_STORAGE.with(|storage| storage.borrow_mut().push(diag)); - } -} - -thread_local! { - static ERR_STORAGE: RefCell> = const { RefCell::new(Vec::new()) }; -} diff --git a/patches/proc-macro-error2/src/lib.rs b/patches/proc-macro-error2/src/lib.rs deleted file mode 100644 index d496790..0000000 --- a/patches/proc-macro-error2/src/lib.rs +++ /dev/null @@ -1,565 +0,0 @@ -//! # proc-macro-error2 -//! -//! This crate aims to make error reporting in proc-macros simple and easy to use. -//! Migrate from `panic!`-based errors for as little effort as possible! -//! -//! (Also, you can explicitly [append a dummy token stream](dummy/index.html) to your errors). -//! -//! To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and -//! `compile_error!`. It detects the best way of emitting available based on compiler's version. -//! When the underlying diagnostic type is finally stabilized, this crate will simply be -//! delegating to it requiring no changes in your code! -//! -//! So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality -//! available on stable ahead of time *and* your error-reporting code future-proof. -//! -//! ## Cargo features -//! -//! This crate provides *enabled by default* `syn-error` feature that gates -//! `impl From for Diagnostic` conversion. If you don't use `syn` and want -//! to cut off some of compilation time, you can disable it via -//! -//! ```toml -//! [dependencies] -//! proc-macro-error2 = { version = "2.0.0", default-features = false } -//! ``` -//! -//! ***Please note that disabling this feature makes sense only if you don't depend on `syn` -//! directly or indirectly, and you very likely do.** -//! -//! ## Real world examples -//! -//! * [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) -//! (abort-like usage) -//! * [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) -//! -//! ## Limitations -//! -//! - Warnings are emitted only on nightly, they are ignored on stable. -//! - "help" suggestions can't have their own span info on stable, -//! (essentially inheriting the parent span). -//! - If a panic occurs somewhere in your macro no errors will be displayed. This is not a -//! technical limitation but rather intentional design. `panic` is not for error reporting. -//! -//! ### `#[proc_macro_error]` attribute -//! -//! **This attribute MUST be present on the top level of your macro** (the function -//! annotated with any of `#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`). -//! -//! This attribute performs the setup and cleanup necessary to make things work. -//! -//! In most cases you'll need the simple `#[proc_macro_error]` form without any -//! additional settings. Feel free to [skip the "Syntax" section](#macros). -//! -//! #### Syntax -//! -//! `#[proc_macro_error]` or `#[proc_macro_error(settings...)]`, where `settings...` -//! is a comma-separated list of: -//! -//! - `proc_macro_hack`: -//! -//! In order to correctly cooperate with `#[proc_macro_hack]`, `#[proc_macro_error]` -//! attribute must be placed *before* (above) it, like this: -//! -//! ```no_run -//! # use proc_macro2::TokenStream; -//! # const IGNORE: &str = " -//! #[proc_macro_error] -//! #[proc_macro_hack] -//! #[proc_macro] -//! # "; -//! fn my_macro(input: TokenStream) -> TokenStream { -//! unimplemented!() -//! } -//! ``` -//! -//! If, for some reason, you can't place it like that you can use -//! `#[proc_macro_error(proc_macro_hack)]` instead. -//! -//! # Note -//! -//! If `proc-macro-hack` was detected (by any means) `allow_not_macro` -//! and `assert_unwind_safe` will be applied automatically. -//! -//! - `allow_not_macro`: -//! -//! By default, the attribute checks that it's applied to a proc-macro. -//! If none of `#[proc_macro]`, `#[proc_macro_derive]` nor `#[proc_macro_attribute]` are -//! present it will panic. It's the intention - this crate is supposed to be used only with -//! proc-macros. -//! -//! This setting is made to bypass the check, useful in certain circumstances. -//! -//! Pay attention: the function this attribute is applied to must return -//! `proc_macro::TokenStream`. -//! -//! This setting is implied if `proc-macro-hack` was detected. -//! -//! - `assert_unwind_safe`: -//! -//! By default, your code must be [unwind safe]. If your code is not unwind safe, -//! but you believe it's correct, you can use this setting to bypass the check. -//! You would need this for code that uses `lazy_static` or `thread_local` with -//! `Cell/RefCell` inside (and the like). -//! -//! This setting is implied if `#[proc_macro_error]` is applied to a function -//! marked as `#[proc_macro]`, `#[proc_macro_derive]` or `#[proc_macro_attribute]`. -//! -//! This setting is also implied if `proc-macro-hack` was detected. -//! -//! ## Macros -//! -//! Most of the time you want to use the macros. Syntax is described in the next section below. -//! -//! You'll need to decide how you want to emit errors: -//! -//! * Emit the error and abort. Very much panic-like usage. Served by [`abort!`] and -//! [`abort_call_site!`]. -//! * Emit the error but do not abort right away, looking for other errors to report. -//! Served by [`emit_error!`] and [`emit_call_site_error!`]. -//! -//! You **can** mix these usages. -//! -//! `abort` and `emit_error` take a "source span" as the first argument. This source -//! will be used to highlight the place the error originates from. It must be one of: -//! -//! * *Something* that implements [`ToTokens`] (most types in `syn` and `proc-macro2` do). -//! This source is the preferable one since it doesn't lose span information on multi-token -//! spans, see [this issue](https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/6) -//! for details. -//! * [`proc_macro::Span`] -//! * [`proc-macro2::Span`] -//! -//! The rest is your message in format-like style. -//! -//! See [the next section](#syntax-1) for detailed syntax. -//! -//! - [`abort!`]: -//! -//! Very much panic-like usage - abort right away and show the error. -//! Expands to [`!`] (never type). -//! -//! - [`abort_call_site!`]: -//! -//! Shortcut for `abort!(Span::call_site(), ...)`. Expands to [`!`] (never type). -//! -//! - [`emit_error!`]: -//! -//! [`proc_macro::Diagnostic`]-like usage - emit the error but keep going, -//! looking for other errors to report. -//! The compilation will fail nonetheless. Expands to [`()`] (unit type). -//! -//! - [`emit_call_site_error!`]: -//! -//! Shortcut for `emit_error!(Span::call_site(), ...)`. Expands to [`()`] (unit type). -//! -//! - [`emit_warning!`]: -//! -//! Like `emit_error!` but emit a warning instead of error. The compilation won't fail -//! because of warnings. -//! Expands to [`()`] (unit type). -//! -//! **Beware**: warnings are nightly only, they are completely ignored on stable. -//! -//! - [`emit_call_site_warning!`]: -//! -//! Shortcut for `emit_warning!(Span::call_site(), ...)`. Expands to [`()`] (unit type). -//! -//! - [`diagnostic`]: -//! -//! Build an instance of `Diagnostic` in format-like style. -//! -//! #### Syntax -//! -//! All the macros have pretty much the same syntax: -//! -//! 1. ```ignore -//! abort!(single_expr) -//! ``` -//! Shortcut for `Diagnostic::from(expr).abort()`. -//! -//! 2. ```ignore -//! abort!(span, message) -//! ``` -//! The first argument is an expression the span info should be taken from. -//! -//! The second argument is the error message, it must implement [`ToString`]. -//! -//! 3. ```ignore -//! abort!(span, format_literal, format_args...) -//! ``` -//! -//! This form is pretty much the same as 2, except `format!(format_literal, format_args...)` -//! will be used to for the message instead of [`ToString`]. -//! -//! That's it. `abort!`, `emit_warning`, `emit_error` share this exact syntax. -//! -//! `abort_call_site!`, `emit_call_site_warning`, `emit_call_site_error` lack 1 form -//! and do not take span in 2'th and 3'th forms. Those are essentially shortcuts for -//! `macro!(Span::call_site(), args...)`. -//! -//! `diagnostic!` requires a [`Level`] instance between `span` and second argument -//! (1'th form is the same). -//! -//! > **Important!** -//! > -//! > If you have some type from `proc_macro` or `syn` to point to, do not call `.span()` -//! > on it but rather use it directly: -//! > ```no_run -//! > # use proc_macro_error2::abort; -//! > # let input = proc_macro2::TokenStream::new(); -//! > let ty: syn::Type = syn::parse2(input).unwrap(); -//! > abort!(ty, "BOOM"); -//! > // ^^ <-- avoid .span() -//! > ``` -//! > -//! > `.span()` calls work too, but you may experience regressions in message quality. -//! -//! #### Note attachments -//! -//! 3. Every macro can have "note" attachments (only 2 and 3 form). -//! ```ignore -//! let opt_help = if have_some_info { Some("did you mean `this`?") } else { None }; -//! -//! abort!( -//! span, message; // <--- attachments start with `;` (semicolon) -//! -//! help = "format {} {}", "arg1", "arg2"; // <--- every attachment ends with `;`, -//! // maybe except the last one -//! -//! note = "to_string"; // <--- one arg uses `.to_string()` instead of `format!()` -//! -//! yay = "I see what {} did here", "you"; // <--- "help =" and "hint =" are mapped -//! // to Diagnostic::help, -//! // anything else is Diagnostic::note -//! -//! wow = note_span => "custom span"; // <--- attachments can have their own span -//! // it takes effect only on nightly though -//! -//! hint =? opt_help; // <-- "optional" attachment, get displayed only if `Some` -//! // must be single `Option` expression -//! -//! note =? note_span => opt_help // <-- optional attachments can have custom spans too -//! ); -//! ``` -//! - -//! ### Diagnostic type -//! -//! [`Diagnostic`] type is intentionally designed to be API compatible with [`proc_macro::Diagnostic`]. -//! Not all API is implemented, only the part that can be reasonably implemented on stable. -//! -//! -//! [`abort!`]: macro.abort.html -//! [`abort_call_site!`]: macro.abort_call_site.html -//! [`emit_warning!`]: macro.emit_warning.html -//! [`emit_error!`]: macro.emit_error.html -//! [`emit_call_site_warning!`]: macro.emit_call_site_error.html -//! [`emit_call_site_error!`]: macro.emit_call_site_warning.html -//! [`diagnostic!`]: macro.diagnostic.html -//! [`Diagnostic`]: struct.Diagnostic.html -//! -//! [`proc_macro::Span`]: https://doc.rust-lang.org/proc_macro/struct.Span.html -//! [`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html -//! -//! [unwind safe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html#what-is-unwind-safety -//! [`!`]: https://doc.rust-lang.org/std/primitive.never.html -//! [`()`]: https://doc.rust-lang.org/std/primitive.unit.html -//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html -//! -//! [`proc-macro2::Span`]: https://docs.rs/proc-macro2/1.0.10/proc_macro2/struct.Span.html -//! [`ToTokens`]: https://docs.rs/quote/1.0.3/quote/trait.ToTokens.html -//! - -#![cfg_attr(feature = "nightly", feature(proc_macro_diagnostic))] -#![forbid(unsafe_code)] - -pub extern crate proc_macro; - -pub use crate::{ - diagnostic::{Diagnostic, DiagnosticExt, Level}, - dummy::{append_dummy, set_dummy}, -}; -pub use proc_macro_error_attr2::proc_macro_error; - -use proc_macro2::Span; -use quote::{quote, ToTokens}; - -use std::cell::Cell; -use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; - -pub mod dummy; - -mod diagnostic; -mod macros; -mod sealed; - -#[cfg(not(feature = "nightly"))] -#[path = "imp/fallback.rs"] -mod imp; - -#[cfg(feature = "nightly")] -#[path = "imp/delegate.rs"] -mod imp; - -#[derive(Debug, Clone, Copy)] -#[must_use = "A SpanRange does nothing unless used"] -pub struct SpanRange { - pub first: Span, - pub last: Span, -} - -impl SpanRange { - /// Create a range with the `first` and `last` spans being the same. - pub fn single_span(span: Span) -> Self { - SpanRange { - first: span, - last: span, - } - } - - /// Create a `SpanRange` resolving at call site. - pub fn call_site() -> Self { - SpanRange::single_span(Span::call_site()) - } - - /// Construct span range from a `TokenStream`. This method always preserves all the - /// range. - /// - /// ### Note - /// - /// If the stream is empty, the result is `SpanRange::call_site()`. If the stream - /// consists of only one `TokenTree`, the result is `SpanRange::single_span(tt.span())` - /// that doesn't lose anything. - pub fn from_tokens(ts: &dyn ToTokens) -> Self { - let mut spans = ts.to_token_stream().into_iter().map(|tt| tt.span()); - let first = spans.next().unwrap_or_else(Span::call_site); - let last = spans.last().unwrap_or(first); - - SpanRange { first, last } - } - - /// Join two span ranges. The resulting range will start at `self.first` and end at - /// `other.last`. - pub fn join_range(self, other: SpanRange) -> Self { - SpanRange { - first: self.first, - last: other.last, - } - } - - /// Collapse the range into single span, preserving as much information as possible. - #[must_use] - pub fn collapse(self) -> Span { - self.first.join(self.last).unwrap_or(self.first) - } -} - -/// This traits expands `Result>` with some handy shortcuts. -pub trait ResultExt { - type Ok; - - /// Behaves like `Result::unwrap`: if self is `Ok` yield the contained value, - /// otherwise abort macro execution via `abort!`. - fn unwrap_or_abort(self) -> Self::Ok; - - /// Behaves like `Result::expect`: if self is `Ok` yield the contained value, - /// otherwise abort macro execution via `abort!`. - /// If it aborts then resulting error message will be preceded with `message`. - fn expect_or_abort(self, msg: &str) -> Self::Ok; -} - -/// This traits expands `Option` with some handy shortcuts. -pub trait OptionExt { - type Some; - - /// Behaves like `Option::expect`: if self is `Some` yield the contained value, - /// otherwise abort macro execution via `abort_call_site!`. - /// If it aborts the `message` will be used for [`compile_error!`][compl_err] invocation. - /// - /// [compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html - fn expect_or_abort(self, msg: &str) -> Self::Some; -} - -/// Abort macro execution and display all the emitted errors, if any. -/// -/// Does nothing if no errors were emitted (warnings do not count). -pub fn abort_if_dirty() { - imp::abort_if_dirty(); -} - -impl> ResultExt for Result { - type Ok = T; - - fn unwrap_or_abort(self) -> T { - match self { - Ok(res) => res, - Err(e) => e.into().abort(), - } - } - - fn expect_or_abort(self, message: &str) -> T { - match self { - Ok(res) => res, - Err(e) => { - let mut e = e.into(); - e.msg = format!("{}: {}", message, e.msg); - e.abort() - } - } - } -} - -impl OptionExt for Option { - type Some = T; - - fn expect_or_abort(self, message: &str) -> T { - match self { - Some(res) => res, - None => abort_call_site!(message), - } - } -} - -/// This is the entry point for a proc-macro. -/// -/// **NOT PUBLIC API, SUBJECT TO CHANGE WITHOUT ANY NOTICE** -#[doc(hidden)] -pub fn entry_point(f: F, proc_macro_hack: bool) -> proc_macro::TokenStream -where - F: FnOnce() -> proc_macro::TokenStream + UnwindSafe, -{ - ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() + 1)); - let caught = catch_unwind(f); - let dummy = dummy::cleanup(); - let err_storage = imp::cleanup(); - ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() - 1)); - - let gen_error = || { - if proc_macro_hack { - quote! {{ - macro_rules! proc_macro_call { - () => ( unimplemented!() ) - } - - #(#err_storage)* - #dummy - - unimplemented!() - }} - } else { - quote!( #(#err_storage)* #dummy ) - } - }; - - match caught { - Ok(ts) => { - if err_storage.is_empty() { - ts - } else { - gen_error().into() - } - } - - Err(boxed) => match boxed.downcast::() { - Ok(_) => gen_error().into(), - Err(boxed) => resume_unwind(boxed), - }, - } -} - -fn abort_now() -> ! { - check_correctness(); - std::panic::panic_any(AbortNow) -} - -thread_local! { - static ENTERED_ENTRY_POINT: Cell = const { Cell::new(0) }; -} - -struct AbortNow; - -fn check_correctness() { - assert!( - ENTERED_ENTRY_POINT.with(Cell::get) != 0, - "proc-macro-error2 API cannot be used outside of `entry_point` invocation, \ - perhaps you forgot to annotate your #[proc_macro] function with `#[proc_macro_error]" - ); -} - -/// **ALL THE STUFF INSIDE IS NOT PUBLIC API!!!** -#[doc(hidden)] -pub mod __export { - // reexports for use in macros - pub use proc_macro; - pub use proc_macro2; - - use proc_macro2::Span; - use quote::ToTokens; - - use crate::SpanRange; - - // inspired by - // https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md#simple-application - - pub trait SpanAsSpanRange { - #[allow(non_snake_case)] - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; - } - - pub trait Span2AsSpanRange { - #[allow(non_snake_case)] - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; - } - - pub trait ToTokensAsSpanRange { - #[allow(non_snake_case)] - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; - } - - pub trait SpanRangeAsSpanRange { - #[allow(non_snake_case)] - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; - } - - impl ToTokensAsSpanRange for &T { - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { - let mut ts = self.to_token_stream().into_iter(); - let first = match ts.next() { - Some(t) => t.span(), - None => Span::call_site(), - }; - - let last = match ts.last() { - Some(t) => t.span(), - None => first, - }; - - SpanRange { first, last } - } - } - - impl Span2AsSpanRange for Span { - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { - SpanRange { - first: *self, - last: *self, - } - } - } - - impl SpanAsSpanRange for proc_macro::Span { - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { - SpanRange { - first: (*self).into(), - last: (*self).into(), - } - } - } - - impl SpanRangeAsSpanRange for SpanRange { - fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { - *self - } - } -} diff --git a/patches/proc-macro-error2/src/macros.rs b/patches/proc-macro-error2/src/macros.rs deleted file mode 100644 index 747b684..0000000 --- a/patches/proc-macro-error2/src/macros.rs +++ /dev/null @@ -1,288 +0,0 @@ -// FIXME: this can be greatly simplified via $()? -// as soon as MRSV hits 1.32 - -/// Build [`Diagnostic`](struct.Diagnostic.html) instance from provided arguments. -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! diagnostic { - // from alias - ($err:expr) => { $crate::Diagnostic::from($err) }; - - // span, message, help - ($span:expr, $level:expr, $fmt:expr, $($args:expr),+ ; $($rest:tt)+) => {{ - #[allow(unused_imports)] - use $crate::__export::{ - ToTokensAsSpanRange, - Span2AsSpanRange, - SpanAsSpanRange, - SpanRangeAsSpanRange - }; - use $crate::DiagnosticExt; - let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); - - let diag = $crate::Diagnostic::spanned_range( - span_range, - $level, - format!($fmt, $($args),*) - ); - $crate::__pme__suggestions!(diag $($rest)*); - diag - }}; - - ($span:expr, $level:expr, $msg:expr ; $($rest:tt)+) => {{ - #[allow(unused_imports)] - use $crate::__export::{ - ToTokensAsSpanRange, - Span2AsSpanRange, - SpanAsSpanRange, - SpanRangeAsSpanRange - }; - use $crate::DiagnosticExt; - let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); - - let diag = $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()); - $crate::__pme__suggestions!(diag $($rest)*); - diag - }}; - - // span, message, no help - ($span:expr, $level:expr, $fmt:expr, $($args:expr),+) => {{ - #[allow(unused_imports)] - use $crate::__export::{ - ToTokensAsSpanRange, - Span2AsSpanRange, - SpanAsSpanRange, - SpanRangeAsSpanRange - }; - use $crate::DiagnosticExt; - let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); - - $crate::Diagnostic::spanned_range( - span_range, - $level, - format!($fmt, $($args),*) - ) - }}; - - ($span:expr, $level:expr, $msg:expr) => {{ - #[allow(unused_imports)] - use $crate::__export::{ - ToTokensAsSpanRange, - Span2AsSpanRange, - SpanAsSpanRange, - SpanRangeAsSpanRange - }; - use $crate::DiagnosticExt; - let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); - - $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()) - }}; - - - // trailing commas - - ($span:expr, $level:expr, $fmt:expr, $($args:expr),+, ; $($rest:tt)+) => { - $crate::diagnostic!($span, $level, $fmt, $($args),* ; $($rest)*) - }; - ($span:expr, $level:expr, $msg:expr, ; $($rest:tt)+) => { - $crate::diagnostic!($span, $level, $msg ; $($rest)*) - }; - ($span:expr, $level:expr, $fmt:expr, $($args:expr),+,) => { - $crate::diagnostic!($span, $level, $fmt, $($args),*) - }; - ($span:expr, $level:expr, $msg:expr,) => { - $crate::diagnostic!($span, $level, $msg) - }; - // ($err:expr,) => { $crate::diagnostic!($err) }; -} - -/// Abort proc-macro execution right now and display the error. -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -#[macro_export] -macro_rules! abort { - ($err:expr) => { - $crate::diagnostic!($err).abort() - }; - - ($span:expr, $($tts:tt)*) => { - $crate::diagnostic!($span, $crate::Level::Error, $($tts)*).abort() - }; -} - -/// Shortcut for `abort!(Span::call_site(), msg...)`. This macro -/// is still preferable over plain panic, panics are not for error reporting. -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! abort_call_site { - ($($tts:tt)*) => { - $crate::abort!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) - }; -} - -/// Emit an error while not aborting the proc-macro right away. -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! emit_error { - ($err:expr) => { - $crate::diagnostic!($err).emit() - }; - - ($span:expr, $($tts:tt)*) => {{ - let level = $crate::Level::Error; - $crate::diagnostic!($span, level, $($tts)*).emit() - }}; -} - -/// Shortcut for `emit_error!(Span::call_site(), ...)`. This macro -/// is still preferable over plain panic, panics are not for error reporting.. -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! emit_call_site_error { - ($($tts:tt)*) => { - $crate::emit_error!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) - }; -} - -/// Emit a warning. Warnings are not errors and compilation won't fail because of them. -/// -/// **Does nothing on stable** -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! emit_warning { - ($span:expr, $($tts:tt)*) => { - $crate::diagnostic!($span, $crate::Level::Warning, $($tts)*).emit() - }; -} - -/// Shortcut for `emit_warning!(Span::call_site(), ...)`. -/// -/// **Does nothing on stable** -/// -/// # Syntax -/// -/// See [the guide](index.html#guide). -/// -#[macro_export] -macro_rules! emit_call_site_warning { - ($($tts:tt)*) => {{ - $crate::emit_warning!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __pme__suggestions { - ($var:ident) => (); - - ($var:ident $help:ident =? $msg:expr) => { - let $var = if let Some(msg) = $msg { - $var.suggestion(stringify!($help), msg.to_string()) - } else { - $var - }; - }; - ($var:ident $help:ident =? $span:expr => $msg:expr) => { - let $var = if let Some(msg) = $msg { - $var.span_suggestion($span.into(), stringify!($help), msg.to_string()) - } else { - $var - }; - }; - - ($var:ident $help:ident =? $msg:expr ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help =? $msg); - $crate::__pme__suggestions!($var $($rest)*); - }; - ($var:ident $help:ident =? $span:expr => $msg:expr ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help =? $span => $msg); - $crate::__pme__suggestions!($var $($rest)*); - }; - - - ($var:ident $help:ident = $msg:expr) => { - let $var = $var.suggestion(stringify!($help), $msg.to_string()); - }; - ($var:ident $help:ident = $fmt:expr, $($args:expr),+) => { - let $var = $var.suggestion( - stringify!($help), - format!($fmt, $($args),*) - ); - }; - ($var:ident $help:ident = $span:expr => $msg:expr) => { - let $var = $var.span_suggestion($span.into(), stringify!($help), $msg.to_string()); - }; - ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+) => { - let $var = $var.span_suggestion( - $span.into(), - stringify!($help), - format!($fmt, $($args),*) - ); - }; - - ($var:ident $help:ident = $msg:expr ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $msg); - $crate::__pme__suggestions!($var $($rest)*); - }; - ($var:ident $help:ident = $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $fmt, $($args),*); - $crate::__pme__suggestions!($var $($rest)*); - }; - ($var:ident $help:ident = $span:expr => $msg:expr ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $span => $msg); - $crate::__pme__suggestions!($var $($rest)*); - }; - ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*); - $crate::__pme__suggestions!($var $($rest)*); - }; - - // trailing commas - - ($var:ident $help:ident = $msg:expr,) => { - $crate::__pme__suggestions!($var $help = $msg) - }; - ($var:ident $help:ident = $fmt:expr, $($args:expr),+,) => { - $crate::__pme__suggestions!($var $help = $fmt, $($args)*) - }; - ($var:ident $help:ident = $span:expr => $msg:expr,) => { - $crate::__pme__suggestions!($var $help = $span => $msg) - }; - ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),*,) => { - $crate::__pme__suggestions!($var $help = $span => $fmt, $($args)*) - }; - ($var:ident $help:ident = $msg:expr, ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $msg; $($rest)*) - }; - ($var:ident $help:ident = $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $fmt, $($args),*; $($rest)*) - }; - ($var:ident $help:ident = $span:expr => $msg:expr, ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $span => $msg; $($rest)*) - }; - ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { - $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*; $($rest)*) - }; -} diff --git a/patches/proc-macro-error2/src/sealed.rs b/patches/proc-macro-error2/src/sealed.rs deleted file mode 100644 index a2d5081..0000000 --- a/patches/proc-macro-error2/src/sealed.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub trait Sealed {} - -impl Sealed for crate::Diagnostic {} diff --git a/patches/proc-macro-error2/test-crate/.gitignore b/patches/proc-macro-error2/test-crate/.gitignore deleted file mode 100644 index 5e81b66..0000000 --- a/patches/proc-macro-error2/test-crate/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock -.fuse_hidden* diff --git a/patches/proc-macro-error2/test-crate/Cargo.toml b/patches/proc-macro-error2/test-crate/Cargo.toml deleted file mode 100644 index 254cb09..0000000 --- a/patches/proc-macro-error2/test-crate/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "test-crate" -version = "0.0.0" -authors = [ - "CreepySkeleton ", - "GnomedDev ", -] -edition = "2021" -publish = false - -[lib] -path = "lib.rs" -proc-macro = true - -[dependencies] -proc-macro-error2 = { path = "../" } -quote = "1" -proc-macro2 = "1" - -[dependencies.syn] -version = "2" -features = ["full"] - -[lints.clippy] -pedantic = { level = "warn", priority = -1 } -module_name_repetitions = { level = "allow" } diff --git a/patches/proc-macro-error2/test-crate/lib.rs b/patches/proc-macro-error2/test-crate/lib.rs deleted file mode 100644 index 1ecdfbc..0000000 --- a/patches/proc-macro-error2/test-crate/lib.rs +++ /dev/null @@ -1,272 +0,0 @@ -use proc_macro2::{Span, TokenStream}; -use proc_macro_error2::{ - abort, abort_call_site, diagnostic, emit_call_site_error, emit_call_site_warning, emit_error, - emit_warning, proc_macro_error, set_dummy, Diagnostic, Level, OptionExt, ResultExt, SpanRange, -}; - -use quote::quote; -use syn::{parse_macro_input, spanned::Spanned}; - -// Macros and Diagnostic - -#[proc_macro] -#[proc_macro_error] -pub fn abort_from(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - abort!( - span, - syn::Error::new(Span::call_site(), "abort!(span, from) test") - ) -} - -#[proc_macro] -#[proc_macro_error] -pub fn abort_to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - abort!(span, "abort!(span, single_expr) test") -} - -#[proc_macro] -#[proc_macro_error] -pub fn abort_format(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - abort!(span, "abort!(span, expr1, {}) test", "expr2") -} - -#[proc_macro] -#[proc_macro_error] -pub fn abort_call_site_test(_: proc_macro::TokenStream) -> proc_macro::TokenStream { - abort_call_site!("abort_call_site! test") -} - -#[proc_macro] -#[proc_macro_error] -pub fn direct_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - Diagnostic::spanned(span.into(), Level::Error, "Diagnostic::abort() test".into()).abort() -} - -#[proc_macro] -#[proc_macro_error] -pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut spans = input.into_iter().step_by(2).map(|t| t.span()); - emit_error!( - spans.next().unwrap(), - syn::Error::new(Span::call_site(), "emit!(span, from) test") - ); - emit_error!( - spans.next().unwrap(), - "emit!(span, expr1, {}) test", - "expr2" - ); - emit_error!(spans.next().unwrap(), "emit!(span, single_expr) test"); - Diagnostic::spanned( - spans.next().unwrap().into(), - Level::Error, - "Diagnostic::emit() test".into(), - ) - .emit(); - - emit_call_site_error!("emit_call_site_error!(expr) test"); - - // NOOP on stable, just checking that the macros themselves compile. - emit_warning!(spans.next().unwrap(), "emit_warning! test"); - emit_call_site_warning!("emit_call_site_warning! test"); - - quote!().into() -} - -// Notes - -#[proc_macro] -#[proc_macro_error] -pub fn abort_notes(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut spans = input.into_iter().map(|s| s.span()); - let span = spans.next().unwrap(); - let span2 = spans.next().unwrap(); - - let some_note = Some("Some note"); - let none_note: Option<&'static str> = None; - - abort! { - span, "This is {} error", "an"; - - note = "simple note"; - help = "simple help"; - hint = "simple hint"; - yay = "simple yay"; - - note = "format {}", "note"; - - note =? some_note; - note =? none_note; - - note = span2 => "spanned simple note"; - note = span2 => "spanned format {}", "note"; - note =? span2 => some_note; - note =? span2 => none_note; - } -} - -#[proc_macro] -#[proc_macro_error] -pub fn emit_notes(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut spans = input.into_iter().step_by(2).map(|s| s.span()); - let span = spans.next().unwrap(); - let span2 = spans.next().unwrap(); - - let some_note = Some("Some note"); - let none_note: Option<&'static str> = None; - - abort! { - span, "This is {} error", "an"; - - note = "simple note"; - help = "simple help"; - hint = "simple hint"; - yay = "simple yay"; - - note = "format {}", "note"; - - note =? some_note; - note =? none_note; - - note = span2 => "spanned simple note"; - note = span2 => "spanned format {}", "note"; - note =? span2 => some_note; - note =? span2 => none_note; - } -} - -// Extension traits - -#[proc_macro] -#[proc_macro_error] -pub fn option_ext(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let none: Option<()> = None; - none.expect_or_abort("Option::expect_or_abort() test"); - quote!().into() -} - -#[proc_macro] -#[proc_macro_error] -pub fn result_unwrap_or_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - let err = Diagnostic::spanned( - span.into(), - Level::Error, - "Result::unwrap_or_abort() test".to_string(), - ); - let res: Result<(), _> = Err(err); - res.unwrap_or_abort(); - quote!().into() -} - -#[proc_macro] -#[proc_macro_error] -pub fn result_expect_or_abort(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - let err = Diagnostic::spanned( - span.into(), - Level::Error, - "Result::expect_or_abort() test".to_string(), - ); - let res: Result<(), _> = Err(err); - res.expect_or_abort("BOOM"); - quote!().into() -} - -// Dummy - -#[proc_macro] -#[proc_macro_error] -pub fn dummy(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - set_dummy(quote! { - impl Default for NeedDefault { - fn default() -> Self { NeedDefault::A } - } - }); - - abort!(span, "set_dummy test"); -} - -#[proc_macro] -#[proc_macro_error] -pub fn append_dummy(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let span = input.into_iter().next().unwrap().span(); - set_dummy(quote! { - impl Default for NeedDefault - }); - - proc_macro_error2::append_dummy(quote!({ - fn default() -> Self { - NeedDefault::A - } - })); - - abort!(span, "append_dummy test"); -} - -// Panic - -#[proc_macro] -#[proc_macro_error] -pub fn unrelated_panic(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { - panic!("unrelated panic test") -} - -// Success - -#[proc_macro] -#[proc_macro_error] -pub fn ok(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let input = TokenStream::from(input); - quote!(fn #input() {}).into() -} - -// Multiple tokens - -#[proc_macro_attribute] -#[proc_macro_error] -pub fn multiple_tokens( - _: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let input = proc_macro2::TokenStream::from(input); - abort!(input, "..."); -} - -#[proc_macro] -#[proc_macro_error] -pub fn to_tokens_span(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ty = parse_macro_input!(input as syn::Type); - emit_error!(ty, "whole type"); - emit_error!(ty.span(), "explicit .span()"); - quote!().into() -} - -#[proc_macro] -#[proc_macro_error] -pub fn explicit_span_range(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut spans = input.into_iter().step_by(2).map(|s| s.span()); - let first = Span::from(spans.next().unwrap()); - let last = Span::from(spans.nth(1).unwrap()); - abort!(SpanRange { first, last }, "explicit SpanRange") -} - -// Children messages - -#[proc_macro] -#[proc_macro_error] -pub fn children_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut spans = input.into_iter().step_by(2).map(|s| s.span()); - diagnostic!(spans.next().unwrap(), Level::Error, "main macro message") - .span_error(spans.next().unwrap().into(), "child message".into()) - .emit(); - - let mut main = syn::Error::new(spans.next().unwrap().into(), "main syn::Error"); - let child = syn::Error::new(spans.next().unwrap().into(), "child syn::Error"); - main.combine(child); - Diagnostic::from(main).abort() -} diff --git a/patches/proc-macro-error2/tests/macro-errors.rs b/patches/proc-macro-error2/tests/macro-errors.rs deleted file mode 100644 index bfe1ea3..0000000 --- a/patches/proc-macro-error2/tests/macro-errors.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[test] -#[cfg(run_ui_tests)] -fn ui() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/*.rs"); -} diff --git a/patches/proc-macro-error2/tests/ok.rs b/patches/proc-macro-error2/tests/ok.rs deleted file mode 100644 index 402edbe..0000000 --- a/patches/proc-macro-error2/tests/ok.rs +++ /dev/null @@ -1,8 +0,0 @@ -use test_crate::*; - -ok!(it_works); - -#[test] -fn check_it_works() { - it_works(); -} diff --git a/patches/proc-macro-error2/tests/runtime-errors.rs b/patches/proc-macro-error2/tests/runtime-errors.rs deleted file mode 100644 index bb7e726..0000000 --- a/patches/proc-macro-error2/tests/runtime-errors.rs +++ /dev/null @@ -1,13 +0,0 @@ -use proc_macro_error2::*; - -#[test] -#[should_panic = "proc-macro-error2 API cannot be used outside of"] -fn missing_attr_emit() { - emit_call_site_error!("You won't see me"); -} - -#[test] -#[should_panic = "proc-macro-error2 API cannot be used outside of"] -fn missing_attr_abort() { - abort_call_site!("You won't see me"); -} diff --git a/patches/proc-macro-error2/tests/ui/abort.rs b/patches/proc-macro-error2/tests/ui/abort.rs deleted file mode 100644 index e998e90..0000000 --- a/patches/proc-macro-error2/tests/ui/abort.rs +++ /dev/null @@ -1,10 +0,0 @@ -use test_crate::*; - -abort_from!(one, two); -abort_to_string!(one, two); -abort_format!(one, two); -direct_abort!(one, two); -abort_notes!(one, two); -abort_call_site_test!(one, two); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/abort.stderr b/patches/proc-macro-error2/tests/ui/abort.stderr deleted file mode 100644 index 2a210c5..0000000 --- a/patches/proc-macro-error2/tests/ui/abort.stderr +++ /dev/null @@ -1,48 +0,0 @@ -error: abort!(span, from) test - --> tests/ui/abort.rs:3:13 - | -3 | abort_from!(one, two); - | ^^^ - -error: abort!(span, single_expr) test - --> tests/ui/abort.rs:4:18 - | -4 | abort_to_string!(one, two); - | ^^^ - -error: abort!(span, expr1, expr2) test - --> tests/ui/abort.rs:5:15 - | -5 | abort_format!(one, two); - | ^^^ - -error: Diagnostic::abort() test - --> tests/ui/abort.rs:6:15 - | -6 | direct_abort!(one, two); - | ^^^ - -error: This is an error - - = note: simple note - = help: simple help - = help: simple hint - = note: simple yay - = note: format note - = note: Some note - = note: spanned simple note - = note: spanned format note - = note: Some note - - --> tests/ui/abort.rs:7:14 - | -7 | abort_notes!(one, two); - | ^^^ - -error: abort_call_site! test - --> tests/ui/abort.rs:8:1 - | -8 | abort_call_site_test!(one, two); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `abort_call_site_test` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/append_dummy.rs b/patches/proc-macro-error2/tests/ui/append_dummy.rs deleted file mode 100644 index 8838404..0000000 --- a/patches/proc-macro-error2/tests/ui/append_dummy.rs +++ /dev/null @@ -1,12 +0,0 @@ -use test_crate::*; - -enum NeedDefault { - A, - B, -} - -append_dummy!(need_default); - -fn main() { - let _ = NeedDefault::default(); -} diff --git a/patches/proc-macro-error2/tests/ui/append_dummy.stderr b/patches/proc-macro-error2/tests/ui/append_dummy.stderr deleted file mode 100644 index c53708b..0000000 --- a/patches/proc-macro-error2/tests/ui/append_dummy.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: append_dummy test - --> tests/ui/append_dummy.rs:8:15 - | -8 | append_dummy!(need_default); - | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/children_messages.rs b/patches/proc-macro-error2/tests/ui/children_messages.rs deleted file mode 100644 index a10ca7c..0000000 --- a/patches/proc-macro-error2/tests/ui/children_messages.rs +++ /dev/null @@ -1,5 +0,0 @@ -use test_crate::*; - -children_messages!(one, two, three, four); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/children_messages.stderr b/patches/proc-macro-error2/tests/ui/children_messages.stderr deleted file mode 100644 index 092eb05..0000000 --- a/patches/proc-macro-error2/tests/ui/children_messages.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error: main macro message - --> tests/ui/children_messages.rs:3:20 - | -3 | children_messages!(one, two, three, four); - | ^^^ - -error: child message - --> tests/ui/children_messages.rs:3:25 - | -3 | children_messages!(one, two, three, four); - | ^^^ - -error: main syn::Error - --> tests/ui/children_messages.rs:3:30 - | -3 | children_messages!(one, two, three, four); - | ^^^^^ - -error: child syn::Error - --> tests/ui/children_messages.rs:3:37 - | -3 | children_messages!(one, two, three, four); - | ^^^^ diff --git a/patches/proc-macro-error2/tests/ui/dummy.rs b/patches/proc-macro-error2/tests/ui/dummy.rs deleted file mode 100644 index ba146a5..0000000 --- a/patches/proc-macro-error2/tests/ui/dummy.rs +++ /dev/null @@ -1,12 +0,0 @@ -use test_crate::*; - -enum NeedDefault { - A, - B, -} - -dummy!(need_default); - -fn main() { - let _ = NeedDefault::default(); -} diff --git a/patches/proc-macro-error2/tests/ui/dummy.stderr b/patches/proc-macro-error2/tests/ui/dummy.stderr deleted file mode 100644 index 197d5ca..0000000 --- a/patches/proc-macro-error2/tests/ui/dummy.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: set_dummy test - --> tests/ui/dummy.rs:8:8 - | -8 | dummy!(need_default); - | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/emit.rs b/patches/proc-macro-error2/tests/ui/emit.rs deleted file mode 100644 index 6f1e389..0000000 --- a/patches/proc-macro-error2/tests/ui/emit.rs +++ /dev/null @@ -1,6 +0,0 @@ -use test_crate::*; - -emit!(one, two, three, four, five); -emit_notes!(one, two); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/emit.stderr b/patches/proc-macro-error2/tests/ui/emit.stderr deleted file mode 100644 index 02fc95e..0000000 --- a/patches/proc-macro-error2/tests/ui/emit.stderr +++ /dev/null @@ -1,48 +0,0 @@ -error: emit!(span, from) test - --> tests/ui/emit.rs:3:7 - | -3 | emit!(one, two, three, four, five); - | ^^^ - -error: emit!(span, expr1, expr2) test - --> tests/ui/emit.rs:3:12 - | -3 | emit!(one, two, three, four, five); - | ^^^ - -error: emit!(span, single_expr) test - --> tests/ui/emit.rs:3:17 - | -3 | emit!(one, two, three, four, five); - | ^^^^^ - -error: Diagnostic::emit() test - --> tests/ui/emit.rs:3:24 - | -3 | emit!(one, two, three, four, five); - | ^^^^ - -error: emit_call_site_error!(expr) test - --> tests/ui/emit.rs:3:1 - | -3 | emit!(one, two, three, four, five); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `emit` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: This is an error - - = note: simple note - = help: simple help - = help: simple hint - = note: simple yay - = note: format note - = note: Some note - = note: spanned simple note - = note: spanned format note - = note: Some note - - --> tests/ui/emit.rs:4:13 - | -4 | emit_notes!(one, two); - | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/explicit_span_range.rs b/patches/proc-macro-error2/tests/ui/explicit_span_range.rs deleted file mode 100644 index 7f0ff24..0000000 --- a/patches/proc-macro-error2/tests/ui/explicit_span_range.rs +++ /dev/null @@ -1,5 +0,0 @@ -use test_crate::*; - -explicit_span_range!(one, two, three, four); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr b/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr deleted file mode 100644 index dd480c6..0000000 --- a/patches/proc-macro-error2/tests/ui/explicit_span_range.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: explicit SpanRange - --> tests/ui/explicit_span_range.rs:3:22 - | -3 | explicit_span_range!(one, two, three, four); - | ^^^^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/misuse.rs b/patches/proc-macro-error2/tests/ui/misuse.rs deleted file mode 100644 index dd86c0f..0000000 --- a/patches/proc-macro-error2/tests/ui/misuse.rs +++ /dev/null @@ -1,10 +0,0 @@ -use proc_macro_error2::abort; - -struct Foo; - -#[allow(unused)] -fn foo() { - abort!(Foo, "BOOM"); -} - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/misuse.stderr b/patches/proc-macro-error2/tests/ui/misuse.stderr deleted file mode 100644 index ac17d0f..0000000 --- a/patches/proc-macro-error2/tests/ui/misuse.stderr +++ /dev/null @@ -1,24 +0,0 @@ -error[E0599]: the method `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange` exists for reference `&Foo`, but its trait bounds were not satisfied - --> tests/ui/misuse.rs:7:5 - | -3 | struct Foo; - | ---------- doesn't satisfy `Foo: quote::to_tokens::ToTokens` -... -7 | abort!(Foo, "BOOM"); - | ^^^^^^^^^^^^^^^^^^^ method cannot be called on `&Foo` due to unsatisfied trait bounds - | - = note: the following trait bounds were not satisfied: - `Foo: quote::to_tokens::ToTokens` - which is required by `&Foo: ToTokensAsSpanRange` -note: the trait `quote::to_tokens::ToTokens` must be implemented - --> $CARGO/quote-1.0.37/src/to_tokens.rs - | - | pub trait ToTokens { - | ^^^^^^^^^^^^^^^^^^ - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following traits define an item `FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange`, perhaps you need to implement one of them: - candidate #1: `Span2AsSpanRange` - candidate #2: `SpanAsSpanRange` - candidate #3: `SpanRangeAsSpanRange` - candidate #4: `ToTokensAsSpanRange` - = note: this error originates in the macro `$crate::diagnostic` which comes from the expansion of the macro `abort` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/multiple_tokens.rs b/patches/proc-macro-error2/tests/ui/multiple_tokens.rs deleted file mode 100644 index 50fc2dd..0000000 --- a/patches/proc-macro-error2/tests/ui/multiple_tokens.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[test_crate::multiple_tokens] -type T = (); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr b/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr deleted file mode 100644 index af0cab0..0000000 --- a/patches/proc-macro-error2/tests/ui/multiple_tokens.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: ... - --> tests/ui/multiple_tokens.rs:2:1 - | -2 | type T = (); - | ^^^^^^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/not_proc_macro.rs b/patches/proc-macro-error2/tests/ui/not_proc_macro.rs deleted file mode 100644 index 03c596b..0000000 --- a/patches/proc-macro-error2/tests/ui/not_proc_macro.rs +++ /dev/null @@ -1,4 +0,0 @@ -use proc_macro_error2::proc_macro_error; - -#[proc_macro_error] -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr b/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr deleted file mode 100644 index 67012aa..0000000 --- a/patches/proc-macro-error2/tests/ui/not_proc_macro.stderr +++ /dev/null @@ -1,9 +0,0 @@ -error: #[proc_macro_error] attribute can be used only with procedural macros - - = hint: if you are really sure that #[proc_macro_error] should be applied to this exact function, use #[proc_macro_error(allow_not_macro)] - --> tests/ui/not_proc_macro.rs:3:1 - | -3 | #[proc_macro_error] - | ^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `proc_macro_error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/option_ext.rs b/patches/proc-macro-error2/tests/ui/option_ext.rs deleted file mode 100644 index 19a650d..0000000 --- a/patches/proc-macro-error2/tests/ui/option_ext.rs +++ /dev/null @@ -1,5 +0,0 @@ -use test_crate::*; - -option_ext!(one, two); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/option_ext.stderr b/patches/proc-macro-error2/tests/ui/option_ext.stderr deleted file mode 100644 index 49bb22d..0000000 --- a/patches/proc-macro-error2/tests/ui/option_ext.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: Option::expect_or_abort() test - --> tests/ui/option_ext.rs:3:1 - | -3 | option_ext!(one, two); - | ^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `option_ext` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/patches/proc-macro-error2/tests/ui/result_ext.rs b/patches/proc-macro-error2/tests/ui/result_ext.rs deleted file mode 100644 index b5a7951..0000000 --- a/patches/proc-macro-error2/tests/ui/result_ext.rs +++ /dev/null @@ -1,6 +0,0 @@ -use test_crate::*; - -result_unwrap_or_abort!(one, two); -result_expect_or_abort!(one, two); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/result_ext.stderr b/patches/proc-macro-error2/tests/ui/result_ext.stderr deleted file mode 100644 index f9321f9..0000000 --- a/patches/proc-macro-error2/tests/ui/result_ext.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Result::unwrap_or_abort() test - --> tests/ui/result_ext.rs:3:25 - | -3 | result_unwrap_or_abort!(one, two); - | ^^^ - -error: BOOM: Result::expect_or_abort() test - --> tests/ui/result_ext.rs:4:25 - | -4 | result_expect_or_abort!(one, two); - | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/to_tokens_span.rs b/patches/proc-macro-error2/tests/ui/to_tokens_span.rs deleted file mode 100644 index 942e94a..0000000 --- a/patches/proc-macro-error2/tests/ui/to_tokens_span.rs +++ /dev/null @@ -1,5 +0,0 @@ -use test_crate::*; - -to_tokens_span!(std::option::Option); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr b/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr deleted file mode 100644 index bb3b543..0000000 --- a/patches/proc-macro-error2/tests/ui/to_tokens_span.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: whole type - --> tests/ui/to_tokens_span.rs:3:17 - | -3 | to_tokens_span!(std::option::Option); - | ^^^^^^^^^^^^^^^^^^^ - -error: explicit .span() - --> tests/ui/to_tokens_span.rs:3:17 - | -3 | to_tokens_span!(std::option::Option); - | ^^^ diff --git a/patches/proc-macro-error2/tests/ui/unknown_setting.rs b/patches/proc-macro-error2/tests/ui/unknown_setting.rs deleted file mode 100644 index 5e54a89..0000000 --- a/patches/proc-macro-error2/tests/ui/unknown_setting.rs +++ /dev/null @@ -1,4 +0,0 @@ -use proc_macro_error2::proc_macro_error; - -#[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/unknown_setting.stderr b/patches/proc-macro-error2/tests/ui/unknown_setting.stderr deleted file mode 100644 index eb349cc..0000000 --- a/patches/proc-macro-error2/tests/ui/unknown_setting.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: unknown setting `trololo`, expected one of `assert_unwind_safe`, `allow_not_macro`, `proc_macro_hack` - --> tests/ui/unknown_setting.rs:3:57 - | -3 | #[proc_macro_error(allow_not_macro, assert_unwind_safe, trololo)] - | ^^^^^^^ diff --git a/patches/proc-macro-error2/tests/ui/unrelated_panic.rs b/patches/proc-macro-error2/tests/ui/unrelated_panic.rs deleted file mode 100644 index 9d450ff..0000000 --- a/patches/proc-macro-error2/tests/ui/unrelated_panic.rs +++ /dev/null @@ -1,5 +0,0 @@ -use test_crate::*; - -unrelated_panic!(); - -fn main() {} diff --git a/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr b/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr deleted file mode 100644 index a1dec9c..0000000 --- a/patches/proc-macro-error2/tests/ui/unrelated_panic.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: proc macro panicked - --> tests/ui/unrelated_panic.rs:3:1 - | -3 | unrelated_panic!(); - | ^^^^^^^^^^^^^^^^^^ - | - = help: message: unrelated panic test From 89554ce2f85a739b189589254ecd8db8438c2f69 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 9 Jun 2026 14:52:57 +0800 Subject: [PATCH 74/85] docs: refresh public documentation --- README.md | 285 +++++++++--------- .../plans/2026-05-19-ddns-publisher.md | 72 ----- .../specs/2026-05-19-ddns-publisher-design.md | 34 --- examples/README.md | 152 +++++----- 4 files changed, 224 insertions(+), 319 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-19-ddns-publisher.md delete mode 100644 docs/superpowers/specs/2026-05-19-ddns-publisher-design.md diff --git a/README.md b/README.md index 2f16ceb..aaf79dc 100644 --- a/README.md +++ b/README.md @@ -1,201 +1,216 @@ # DDNS -This package provides DNS discovery for the DHTTP ecosystem. The old `ddns-core`, `gmdns`, `ddns`, and `ddns-server` crate boundaries are now modules and binaries inside one package named `ddns`. +`ddns` provides DNS discovery and resolver support for DHTTP applications. It is a +single Rust package: the historical `ddns-core`, `gmdns`, `ddns`, and +`ddns-server` crate boundaries now live as modules and feature-gated targets in +one crate named `ddns`. + +## Crate layout | Module / target | Role | | --- | --- | -| `ddns::core` | DNS packet parser, endpoint `E` record, and shared wire types. | -| `ddns::mdns` | RFC 6762 multicast DNS transport and LAN resolver/publisher. | -| `ddns::resolvers` | Facade resolver chain plus optional HTTP/3/HTTP resolvers. | -| `ddns-server` | DNS-over-HTTP/3 publish/lookup server binary (`server` feature). | +| `ddns::core` | DNS packet parser, resource-record types, endpoint `E` record encoding, and HTTP multi-record response wire format. | +| `ddns::mdns` | RFC 6762 multicast DNS transport, LAN publisher, and LAN resolver support. | +| `ddns::resolvers` | Resolver chain plus optional System, mDNS, DNS-over-H3, and DNS-over-HTTP resolvers. | +| `ddns::publisher` | Feature-gated endpoint record signing and publishing loop helpers for DHTTP endpoints. | +| `ddns-server` | DNS-over-H3 publish/lookup server binary, enabled by the `server` feature. | -`ddns::mdns` is the local multicast DNS layer. `ddns` is the high-level crate to use when an application needs both LAN mDNS and remote DNS-over-HTTP/3 resolver support. +`ddns` is endpoint-facing support code for the DHTTP ecosystem. Applications +normally reach it through the `dhttp` endpoint facade; lower-level consumers can +depend on package `ddns` directly when they need DNS wire types, resolver +composition, mDNS, or the DNS-over-H3 server. -## 🌟 Key Features +## Features -- **Standards Compliant**: Supports standard DNS packet format and mDNS multicast discovery. -- **P2P Enhanced**: Custom `E` record type supporting IPv4/IPv6 direct and relay addresses. -- **Security Verification**: Built-in signature schemes (Ed25519, etc.) ensuring endpoint data authenticity and integrity. -- **High Performance Parsing**: Zero-copy parsing framework based on `nom` for blazing-fast packet processing. -- **Async-Driven**: Fully compatible with `tokio` async runtime for high-concurrency network environments. -- **HTTP/3 Integration**: Supports DNS over HTTP/3 (DoH3) for secure remote DNS queries and publishing. +All optional integrations are feature-gated; the default feature set is empty. -## 🚀 Quick Start +| Feature | Enables | +| --- | --- | +| `h3x-resolver` | DNS-over-H3 resolver and publisher using `h3x`/`dquic`. | +| `mdns-resolver` | mDNS resolver integration backed by an existing `h3x::dquic::Network`. | +| `http-resolver` | DNS-over-HTTP resolver/publisher using `reqwest` and native roots. | +| `server` | `ddns-server`, Redis storage support, TOML config parsing, and tracing setup. | -Add to your `Cargo.toml`: +## Bootstrap constants -```toml -[dependencies] -ddns = { path = "./ddns" } -``` +`build.rs` generates the resolver defaults exposed from `ddns::resolvers`: -For HTTP/3 resolver/publisher support, enable the `h3x-resolver` feature on `ddns`: +| Environment variable | Public constant | Fallback when unset | +| --- | --- | --- | +| `DHTTP_H3_DNS_SERVER` | `DHTTP_H3_DNS_SERVER` | `https://dhttp.example.net` | +| `DHTTP_HTTP_DNS_SERVER` | `DHTTP_HTTP_DNS_SERVER` | `https://dhttp.example.net` | +| `DHTTP_MDNS_SERVICE` | `DHTTP_MDNS_SERVICE` | `dhttp.example.net` | -```toml -[dependencies] -ddns = { path = "./ddns", features = ["h3x-resolver"] } -``` +The fallbacks are docs/build placeholders, not operational defaults. Real +endpoint, server, and E2E runs should set the DHTTP bootstrap environment before +building. + +## Quick start -### Simple mDNS Discovery Example +### Resolver chain + +`Resolvers` queries all configured resolvers and streams endpoint addresses from +successful backends. System DNS is always available; mDNS, H3, and HTTP builders +appear behind their features. ```rust +use ddns::resolvers::Resolvers; use futures::StreamExt; -use ddns::Mdns; #[tokio::main] -async fn main() -> Result<(), std::io::Error> { - // Create mDNS instance - let mdns = Mdns::new(DHTTP_MDNS_SERVICE, "127.0.0.1".parse().unwrap(), "lo0")?; - - // Listen to discovery stream - let mut stream = mdns.discover(); - while let Some((addr, packet)) = stream.next().await { - println!("Discovered packet from {}: {:?}", addr, packet); +async fn main() -> Result<(), ddns::resolvers::DnsErrors> { + let resolvers = Resolvers::builder().system().build(); + let mut endpoints = resolvers.lookup("demo.example.dhttp.net").await?; + + while let Some((source, endpoint)) = endpoints.next().await { + println!("{source:?}: {endpoint}"); } + Ok(()) } ``` -### HTTP/3 DNS Publishing Example +### mDNS discovery ```rust -// See examples/publish.rs for a complete mTLS HTTP/3 publisher. -``` +use ddns::{mdns::service::Mdns, resolvers::DHTTP_MDNS_SERVICE}; +use futures::StreamExt; ---- +#[tokio::main(flavor = "current_thread")] +async fn main() -> std::io::Result<()> { + let mdns = Mdns::new( + DHTTP_MDNS_SERVICE, + std::net::Ipv4Addr::LOCALHOST.into(), + "lo0", + )?; + let mut discoveries = mdns.discover(); + + while let Some((source, packet)) = discoveries.next().await { + println!("received packet from {source}: {packet}"); + } -## 🌐 HTTP/3 DNS Server + Ok(()) +} +``` -`ddns` includes support for DNS over HTTP/3 (DoH3), allowing secure publication and querying of DNS records via HTTP/3 protocol. This is useful for remote networks where multicast mDNS is not feasible. +Runnable examples live in `examples/`: -### Publishing Services +```bash +cargo run --example mdns_discover -- --ip 127.0.0.1 --device lo0 +cargo run --example mdns_query -- --ip 192.168.5.156 --device en0 +``` -Publish DNS service records to an HTTP/3 DNS server: +### DNS-over-H3 examples ```bash -cargo run --example publish --features="h3x-resolver" \ +cargo run --example query --features h3x-resolver -- \ + --server-ca /path/to/root.crt \ + --host nat.genmeta.net + +cargo run --example publish --features h3x-resolver -- \ --server-ca /path/to/root.crt \ --client-name demo.example.dhttp.net \ --client-cert /path/to/demo.example.dhttp.net.pem \ --client-key /path/to/demo.example.dhttp.net.key \ --host demo.example.dhttp.net \ - --addr 192.168.1.100:8080 + --addr 192.168.1.100:8080,192.168.1.101:8080 ``` -### Querying Services +See [`examples/README.md`](examples/README.md) for the example CLI parameters +and response decoding notes. -Query DNS service records from an HTTP/3 DNS server: +## DNS-over-H3 server -```bash -cargo run --example query --features="h3x-resolver" \ - --server-ca /path/to/root.crt \ - --host nat.genmeta.net -``` - -### Running the DNS Server - -Start an HTTP/3 DNS server: +Start the server with the `server` feature: ```bash -cargo run --bin ddns-server --features="server" -- --config server.toml +cargo run --bin ddns-server --features server -- --config server.toml ``` -For detailed parameters and HTTP packet structures, see [examples/README.md](examples/README.md). - ---- - -## 📖 Protocol Specification +The server exposes two HTTP/3 routes: -### 1. Packet Layout +| Route | Meaning | +| --- | --- | +| `POST /publish?host=` | Publish a DNS packet for `host`. Client mTLS is required. | +| `GET /lookup?host=[&limit=N]` | Look up active records for `host`; `limit` caps newest-first dynamic records. | -DNS packets consist of a fixed header and four variable-length sections: +Lookup responses use header `x-record-format: multi` and the binary body from +`ddns::core::wire::MultiResponse`: ```text -+---------------------+-----------------------+-----------------------+-----------------------+-----------------------+ -| Header (12 bytes) | Question Section | Answer Section | Nameserver Section | Additional Section | -+---------------------+-----------------------+-----------------------+-----------------------+-----------------------+ -| Transaction ID | Query list | Answer RR list | Authority RR list | Additional RR list | -| and Flags | | | | | -+---------------------+-----------------------+-----------------------+-----------------------+-----------------------+ +u32 count +repeated count times: + u32 dns_len | dns packet bytes | u32 cert_len | DER publisher certificate bytes ``` -#### 1.1 Header -Fixed length of 12 bytes. Contains ID, Flags, and counters for subsequent sections (QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT). - -#### 1.2 Resource Record -Answer, Nameserver, and Additional sections all use this format: +Server configuration lives in `server.toml`: -- **NAME**: Variable-length domain name, supports RFC 1035 compression. -- **TYPE (u16)**: Record type (e.g., A=1, SRV=33, E=266). -- **CLASS (u16)**: Protocol class. In mDNS, the highest bit (bit 15) is used for cache-flush flag. -- **TTL (u32)**: Cache time-to-live (seconds). -- **RDLEN (u16)**: Length of resource data (RDATA). -- **RDATA**: Specific resource content, format determined by TYPE. +- storage is in-memory by default, or Redis when `redis = "redis://..."` is set; +- `ttl_secs` controls dynamic record expiry; +- `require_signature` controls signed endpoint-record enforcement for Standard + domains; +- `domain_policies` are matched in order, with unlisted domains using the + Standard policy; +- `seed_records` add static bootstrap endpoints to lookup results. -### 2. Custom Type Definitions (QType) +Domain policies: -| Type | Value | Description | RDATA Format | -| :------- | :---- | :--------------- | :-------------------------------- | -| **A** | 1 | IPv4 address | 4-byte IP | -| **AAAA** | 28 | IPv6 address | 16-byte IP | -| **SRV** | 33 | Service location | Priority + Weight + Port + Target | -| **E** | 266 | Endpoint address | Flags + Seq + Addr(s) + [Sig] | +| Policy | Behavior | +| --- | --- | +| `standard` | Client certificate DNS SAN must match the published host; signed `E` records are required when `require_signature = true`; each certificate fingerprint owns one active record for the host. | +| `open_multi` | Any authenticated client certificate may publish; signature checks are skipped; multiple certificate fingerprints can coexist and lookup returns newest-first records. | -### 3. Endpoint Extensions (Type E) +Public DHTTP identity hostnames should use the canonical `DhttpName::SUFFIX` +(`.dhttp.net`). Infrastructure names such as `nat.genmeta.net` can remain under +Genmeta infrastructure domains. -#### 3.1 RDATA Wire Format +## Endpoint `E` records -##### Packet Format +Custom DNS record type `E` (`QTYPE = 266`) carries DHTTP endpoint addresses. The +current wire format is: ```text -+--------+-----------------+--------------------+----------------------------+ -| flags | sequence(varint)| addr(s) | signature (optional) | -+--------+-----------------+--------------------+----------------------------+ -| u8 | QUIC varint | v4: 2+4 / v6: 2+16 | scheme(u16)+len(varint)+N | -+--------+-----------------+--------------------+----------------------------+ +flags(u8) +[sequence(varint) if CLUSTERED] +primary address: port(u16) + IPv4/IPv6 bytes +[agent address if NAT] +[load(f32) if LOAD] +[signature: scheme(u16) + len(varint) + bytes if SIGNED] ``` -##### flags (u8) Field Definition: -- bit 7 (0x80): **FAMILY** - Address family (0=IPv4, 1=IPv6) -- bit 6 (0x40): **MAIN** - Primary address flag -- bit 5 (0x20): **SEQUENCED** - Sequence number present -- bit 4 (0x10): **FORWARD** - Connection type (0=direct, 1=relay) -- bit 3 (0x08): **SIGNED** - Signature present -- bits 2-0: Reserved - -##### Address Format: -- **Direct**: `port(u16)` + `IP(u32/u128)` -- **Relay**: `outer_port(u16)` + `outer_IP(u32/u128)` + `agent_port(u16)` + `agent_IP(u32/u128)` -- **sequence**: DNS record sequence number. Records with the same sequence are considered from the same machine and can use multipath connections. -- **signature**: When `SIGNED` flag is set, signature field is appended. - -#### 3.2 Flag Bit Masks - -- `0b1000_0000`: **FAMILY** (Address family: 0=IPv4, 1=IPv6) -- `0b0100_0000`: **MAIN** (Primary address flag) -- `0b0010_0000`: **SEQUENCED** (Sequence number present) -- `0b0001_0000`: **FORWARD** (Connection type: 0=direct, 1=relay) -- `0b0000_1000`: **SIGNED** (Signature present) - -#### 3.3 Address Format Details +Flag bits: -- **Direct**: `Port(u16)` + `IP(u32/u128)` -- **Relay**: `OuterPort(u16)` + `OuterIP(u32/u128)` + `AgentPort(u16)` + `AgentIP(u32/u128)` +| Bit mask | Name | Meaning | +| --- | --- | --- | +| `0x80` | `FAMILY` | `0` = IPv4, `1` = IPv6. | +| `0x40` | `MAIN` | Primary endpoint for the name. | +| `0x20` | `CLUSTERED` | Sequence number is present; multiple publishers share the name. | +| `0x10` | `NAT` | Agent address is present for NAT traversal. | +| `0x08` | `LOAD` | One-minute load value is present. | +| `0x01` | `SIGNED` | Signature with explicit TLS signature scheme is present. | -#### 3.4 Signature Format +Signed records encode the signature scheme in the record; the no-scheme signed +format is not accepted. Legacy unsigned fixed-length endpoint address records are +still parsed by length for address-only compatibility. -When signature is present: `Scheme (u16)` + `Length (VarInt)` + `Data (N bytes)`. +## Project structure ---- - -## 🛠 Project Structure - -- `src/core/parser/`: Core protocol parsing implementation (Nom parsers). -- `src/core/wire.rs`: Shared HTTP multi-record response wire format. -- `src/mdns/protocol.rs`: UDP multicast and packet routing logic. -- `src/mdns/service.rs`: High-level mDNS discovery and response API. -- `src/mdns/resolvers/`: LAN mDNS resolver implementation. -- `src/resolvers/`: Facade resolver chain plus optional HTTP/3 and HTTP resolvers. -- `examples/`: mDNS discovery/query and HTTP/3 publish/query examples. -- `src/bin/ddns-server/`: DNS-over-HTTP/3 server binary. -- `server.toml`: Example server configuration. +```text +src/core.rs DNS core module root +src/core/parser/ DNS packet, name, question, record, varint, and signature parsers +src/core/parser/record/ A/AAAA/SRV/TXT/PTR/CNAME/E record parsing and encoding +src/core/wire.rs HTTP multi-record response wire format +src/mdns.rs mDNS module root +src/mdns/protocol.rs UDP multicast socket and packet routing +src/mdns/service.rs High-level mDNS service API +src/mdns/resolvers/ mDNS resolver integration +src/resolvers.rs Resolver chain and resolver defaults +src/resolvers/h3.rs DNS-over-H3 resolver/publisher +src/resolvers/http.rs DNS-over-HTTP resolver/publisher +src/resolvers/deferred.rs Deferred resolver initialization helper +src/publisher.rs Endpoint record signer and publication loop +src/publisher/ Address selection, publish dispatch, packet signing +src/bin/ddns-server/ DNS-over-H3 server implementation +examples/ mDNS and DNS-over-H3 example programs +server.toml Example server configuration +``` diff --git a/docs/superpowers/plans/2026-05-19-ddns-publisher.md b/docs/superpowers/plans/2026-05-19-ddns-publisher.md deleted file mode 100644 index 6edad3c..0000000 --- a/docs/superpowers/plans/2026-05-19-ddns-publisher.md +++ /dev/null @@ -1,72 +0,0 @@ -# DDNS Publisher Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add signed DNS publishing for DHTTP endpoints using async identity authorities and concrete ddns publishers. - -**Architecture:** `dhttp-identity` owns async authority traits and signature helpers. `ddns-core` signs endpoint records through `LocalAuthority`. `ddns` owns `Publisher`, discovers concrete publishers by downcasting, and publishes signed packets. `dhttp::Endpoint` provides the convenience constructor. - -**Tech Stack:** Rust 2024, snafu, futures BoxFuture, dhttp-identity, ddns-core, ddns, h3x/dquic resolver traits. - ---- - -### Task 1: Move async authority traits into dhttp-identity - -**Files:** -- Modify: `dhttp/identity/src/identity.rs` -- Modify: `dhttp/identity/src/lib.rs` -- Delete: `h3x/src/quic/authority.rs` -- Modify: h3x call sites to import `dhttp_identity::identity::{LocalAuthority, RemoteAuthority, SignError, VerifyError}` directly - -- [ ] Add tests in `dhttp/identity/src/identity.rs` for `Identity` implementing async `LocalAuthority` and sync-default `RemoteAuthority` verification behavior. -- [ ] Move `LocalAuthority`, `RemoteAuthority`, `extract_public_key`, `verify_signature`, and `sign_with_key` equivalents into `dhttp-identity` while preserving async signatures. -- [ ] Delete `h3x::quic::authority`; downstream crates import the identity authority API from `dhttp_identity::identity` directly. -- [ ] Run `cargo test -p dhttp-identity` and `cargo test --features dquic` in h3x. - -### Task 2: Replace ddns-core SigningKey signing with LocalAuthority signing - -**Files:** -- Modify: `ddns/ddns-core/Cargo.toml` -- Modify: `ddns/ddns-core/src/parser/record/endpoint.rs` -- Modify: `ddns/ddns-core/src/parser/sigin.rs` - -- [ ] Add a failing async test for signing an `EndpointAddr` through a fake `LocalAuthority` that rejects the first preferred compatible scheme and accepts the next one. -- [ ] Add `EndpointAddr::sign_with_authority(&mut self, authority: &(impl LocalAuthority + ?Sized)) -> impl Future>`. -- [ ] Keep old low-level `ddns_core::parser::sigin::sign_with_key(SigningKey, SignatureScheme, data)` helper, delete only the `EndpointAddr::sign_with(SigningKey, SignatureScheme)` convenience method, and update tests/examples to use the async authority method where endpoint records are signed. -- [ ] Keep verification logic unchanged except for imports. -- [ ] Run `cargo test -p ddns-core`. - -### Task 3: Implement ddns Publisher - -**Files:** -- Create: `ddns/ddns/src/publisher.rs` -- Modify: `ddns/ddns/src/lib.rs` -- Modify: `ddns/ddns/src/resolvers.rs` -- Modify: `ddns/gmdns/src/resolvers/mdns.rs` if mDNS needs a public binding iterator - -- [ ] Add tests for `NoPublisherResolver`, downcast discovery, and mDNS same-device/same-family address filtering. -- [ ] Implement `Publisher` with non-optional identity, network, resolver, bind patterns, and 20s default interval. -- [ ] Implement `publish_once` to build signed packets and publish via concrete H3, HTTP, and mDNS publishers discovered through `Any` downcasts. -- [ ] Implement `run` as an infinite async loop that logs warning reports and sleeps 20 seconds between attempts. -- [ ] Run `cargo test --workspace --all-features` in ddns. - -### Task 4: Add dhttp Endpoint publisher entry point - -**Files:** -- Modify: `dhttp/dhttp/src/endpoint.rs` -- Modify: `dhttp/dhttp/src/lib.rs` if re-export plumbing is needed - -- [ ] Add a test or compile-time assertion for anonymous endpoint returning `CreatePublisherError::AnonymousEndpoint`. -- [ ] Implement `Endpoint::publisher(&self) -> Result`. -- [ ] Run `cargo test --workspace` in dhttp. - -### Task 5: Verification and commits - -**Files:** -- All modified files above - -- [ ] Run `cargo fmt` in dhttp, h3x, and ddns. -- [ ] Run `cargo clippy --all-targets --all-features -- -D warnings` in dhttp and ddns. -- [ ] Run `cargo clippy --all-targets --features "dquic,hyper,serde,webtransport,testing" -- -D warnings` in h3x. -- [ ] Run relevant cargo tests in all touched repos. -- [ ] Commit each independent repo with a semantic message. diff --git a/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md b/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md deleted file mode 100644 index 9c3724a..0000000 --- a/docs/superpowers/specs/2026-05-19-ddns-publisher-design.md +++ /dev/null @@ -1,34 +0,0 @@ -# DDNS Publisher Design - -## Goal - -Add a reusable DNS publisher for DHTTP endpoints. The publisher signs endpoint records with the endpoint identity and publishes them through concrete DNS publishers discovered from the endpoint resolver set. - -## Decisions - -- `LocalAuthority` and `RemoteAuthority` are identity-layer concepts. Move their async trait definitions to `dhttp-identity`; `Identity` implements them without adding generics to `Identity`. -- `LocalAuthority` remains an async signing API. It is an async/remote-capable counterpart of `rustls::sign::SigningKey`, not a replacement with synchronous signing. -- DNS publisher code lives in the `ddns` crate. `dhttp::Endpoint` only exposes a convenience method that constructs a `ddns::Publisher` from endpoint state. -- `Endpoint::publisher()` returns `Result` because anonymous endpoints cannot publish signed DNS records. `Publisher` stores a non-optional identity. -- `EndpointAddr::sign_with(SigningKey, scheme)` is removed. Endpoint record signing uses `dhttp_identity::LocalAuthority`. -- Signature scheme selection follows the existing `pick_signature_scheme` preference order: Ed25519, ECDSA P-256, ECDSA P-384, RSA-PSS SHA-256/384/512, RSA-PKCS1 SHA-256/384/512. The async authority API is not expanded with `choose_scheme`; signing tries compatible schemes and treats `UnsupportedScheme` as a cue to try the next candidate. -- `publish_once` returns an error for the first failed publish attempt. `run` publishes every 20 seconds, logs warnings on failures with `snafu::Report`, and does not retain failure state. -- `NoPublisherResolver` is built at publish time when no concrete publisher can be found by downcasting. -- There is no `Resolvers::publish`; publishing is resolver-specific and uses `Any` downcasting. - -## Data Flow - -`dhttp::Endpoint::publisher()` clones the endpoint identity, network, resolver, and bind patterns into `ddns::Publisher`. `Publisher::publish_once()` collects endpoint addresses, builds signed DNS packets, and dispatches them to concrete publishers: - -- H3 and HTTP publishers receive public/STUN-derived endpoint addresses. -- mDNS publishers receive only local QUIC addresses on the same network device and IP family as the mDNS binding. - -Each DNS packet is independently signed with the endpoint identity before publication. - -## Error Handling - -Errors are typed with `snafu`. Display messages are lower-case fragments and do not repeat source errors. `publish_once` returns structured errors; `run` logs `snafu::Report` and continues. - -## Testing - -Unit tests cover authority-based endpoint signing, signature scheme fallback, anonymous endpoint publisher construction, missing publisher reporting, and mDNS address scoping helpers. Workspace tests and clippy run before commits. diff --git a/examples/README.md b/examples/README.md index 5df706f..61204c1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,61 +1,71 @@ -# DNS Server Documentation +# DDNS examples -## Introduction +This directory contains runnable examples for the single `ddns` package. -`ddns` is a Rust-implemented DNS library that supports the mDNS (Multicast DNS) protocol and interacts with DNS servers via the HTTP/3 (H3) protocol for service discovery and publishing in local and remote networks. This document introduces how to use the example programs of `ddns` to publish and query DNS services, including detailed program parameters and HTTP packet structures. +| Example | Feature requirement | Purpose | +| --- | --- | --- | +| `mdns_discover` | none | Bind an mDNS service, publish sample local hosts, and print multicast packets. | +| `mdns_query` | none | Query a DHTTP name over local mDNS. | +| `query` | `h3x-resolver` | Query a DNS-over-H3 server and decode the multi-record response. | +| `publish` | `h3x-resolver` | Publish signed endpoint `E` records to a DNS-over-H3 server using client mTLS. | -## Building the Project +Run all commands from the `ddns/` repository. -First, ensure you have a Rust environment. Clone or enter the project directory, then build: +## mDNS examples + +Bind to a local interface and print multicast traffic: ```bash -cargo build --features="h3x-resolver" +cargo run --example mdns_discover -- \ + --ip 127.0.0.1 \ + --device lo0 ``` -Note: The example programs require the `h3x-resolver` feature to enable HTTP/3 support. +Query a name over mDNS: -## HTTP Packet Structure Overview +```bash +cargo run --example mdns_query -- \ + --ip 192.168.5.156 \ + --device en0 +``` -`ddns` uses the HTTP/3 protocol to transmit DNS queries and responses, similar to DNS over HTTPS (DoH) but based on the QUIC protocol. The structure of HTTP requests is as follows: +Replace `--ip` and `--device` with an address and interface that exist on the +local machine. The mDNS service name defaults to the build-time +`DHTTP_MDNS_SERVICE` constant. -### URL Structure -- **Base URL**: Default `https://localhost:4433/`, used to specify the DNS server's address. -- **Path**: For queries, usually the root path `/`, the server parses the DNS query based on the request body. -- **Query Parameters**: Optional, used to specify query type or options. +## DNS-over-H3 query -### HTTP Headers -- **Content-Type**: `application/dns-message` (for DNS message body) or `application/json` (if using JSON format). -- **Accept**: `application/dns-message` or `application/json`. -- **User-Agent**: Client identifier. -- **Authorization**: If authentication is needed, use Bearer token or other mechanisms. +```bash +cargo run --example query --features h3x-resolver -- \ + --server-ca /path/to/root.crt \ + --host nat.genmeta.net +``` -### Request Body (Body) -- DNS queries are sent in binary DNS message format (RFC 1035), containing query name, type (such as A, AAAA, SRV), and class. -- For publishing, the request body contains the DNS record data to be published. +Options: -### Response Body -- The server returns a DNS response message containing query results or confirmation of publishing. +| Option | Meaning | +| --- | --- | +| `--base-url ` | DNS-over-H3 server base URL. Defaults to build-time `DHTTP_H3_DNS_SERVER` with a trailing slash. | +| `--server-ca ` | PEM root CA used to verify the DNS server certificate. | +| `--host ` | DNS host to query. Defaults to `nat.genmeta.net`. | -## Usage Examples +The example sends `GET /lookup?host=`. A successful server response is a +`ddns::core::wire::MultiResponse` body with header `x-record-format: multi`: -### Publishing Services (publish) +```text +u32 count +repeated count times: + u32 dns_len | dns packet bytes | u32 cert_len | DER publisher certificate bytes +``` -Use the `publish` example to publish a DNS service record to the HTTP/3 DNS server. +The example prints each DNS packet, the publisher certificate fingerprint when a +certificate is present, and endpoint signature verification status for signed +`E` records. -#### Program Parameters -- `--base-url `: Base URL of the DNS server (default: build-time `DHTTP_H3_DNS_SERVER` with a trailing slash). -- `--server-ca `: CA certificate PEM file path for verifying the online server certificate. -- `--client-name `: Client identity name used for mTLS. -- `--client-cert `: Client certificate chain PEM file. -- `--client-key `: Client private key PEM file. -- `--sign`: Whether to sign the Endpoint record with the client private key (default: true). -- `--host `: DNS name to publish, must match the SAN in the client certificate. -- `--addr `: List of socket addresses to publish, separated by commas. -- `--is-main`: Whether it is the main record (default: true). +## DNS-over-H3 publish -#### Example Run Command ```bash -cargo run --example publish --features="h3x-resolver" \ +cargo run --example publish --features h3x-resolver -- \ --server-ca /path/to/root.crt \ --client-name demo.example.dhttp.net \ --client-cert /path/to/demo.example.dhttp.net.pem \ @@ -64,47 +74,33 @@ cargo run --example publish --features="h3x-resolver" \ --addr 192.168.1.100:8080,192.168.1.101:8080 ``` -This command establishes an HTTP/3 connection to the server, sends a POST request containing DNS records, the server verifies the signature and stores the records. - -### Querying Services (query) - -Use the `query` example to query DNS service records from the HTTP/3 DNS server. - -#### Program Parameters -- `--base-url `: Base URL of the DNS server (default: build-time `DHTTP_H3_DNS_SERVER` with a trailing slash). -- `--server-ca `: CA certificate PEM file path for verifying the online server certificate. -- `--host `: DNS name to query (default: `nat.genmeta.net`). - -#### Example Run Command -```bash -cargo run --example query --features="h3x-resolver" \ - --server-ca /path/to/root.crt \ - --host nat.genmeta.net -``` - -This command sends a GET or POST request to the server, the request body contains the DNS query message, the server returns matching records. - -### Running the DNS Server (server) +Options: + +| Option | Meaning | +| --- | --- | +| `--base-url ` | DNS-over-H3 server base URL. Defaults to build-time `DHTTP_H3_DNS_SERVER` with a trailing slash. | +| `--server-ca ` | PEM root CA used to verify the DNS server certificate. | +| `--client-name ` | DHTTP identity name presented by the client endpoint. | +| `--client-cert ` | Client certificate chain PEM for mTLS and endpoint signature verification. | +| `--client-key ` | Client private key PEM. | +| `--sign ` | Whether to sign each endpoint `E` record. Defaults to `true`. | +| `--host ` | DNS host to publish. Standard-policy servers require this to match the client certificate DNS SAN. | +| `--addr ` | One or more socket addresses to publish. | +| `--is-main ` | Whether each endpoint is marked as the main address. Defaults to `true`. | +| `--sequence ` | Cluster sequence number encoded in the endpoint record; `0` disables the clustered flag. Defaults to `1`. | + +The example sends `POST /publish?host=` with a binary DNS packet body. For +Standard policy domains, the server requires a client certificate whose single +DNS SAN matches `host`; when `require_signature = true`, at least one signed +endpoint record must verify against the publisher certificate. Open-multi policy +domains still require client mTLS but skip the host SAN and endpoint signature +checks. + +## Running the server -Use the `ddns-server` binary to start an HTTP/3 DNS server. - -#### Program Parameters -- `--config `: TOML configuration file path (default: `server.toml`). - -#### Example Run Command ```bash -cargo run --bin ddns-server --features="server" -- --config server.toml +cargo run --bin ddns-server --features server -- --config server.toml ``` -After the server starts, it listens for HTTP/3 requests and handles publish and query operations. - -## Other Examples - -The project also includes other example programs such as `mdns_discover.rs` and `mdns_query.rs` for pure mDNS discovery and query operations, not involving HTTP/3. Please refer to the source code for more details. - -## Notes - -- Ensure that the local network supports QUIC and HTTP/3. -- Certificate and key files must be configured correctly, otherwise TLS handshake will fail. -- For production environments, use valid certificates and secure key management. -- For more configuration options, please refer to the project's main README.md file. +`server.toml` documents the available fields: listener, TLS identity, client root +CA, optional Redis storage, TTL, domain policies, and static seed records. From 2723d7fb0f57e8b9ae1389ddc5b0e921dcb79283 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 9 Jun 2026 15:26:50 +0800 Subject: [PATCH 75/85] fix: add publishable genmeta dependency versions --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87fb36a..4584e62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ base64 = "0.22" bitfield-struct = "0.13" bytes = "1" dashmap = "6" -dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "main" } -dquic = { git = "https://github.com/genmeta/dquic.git", branch = "feat/v0.5.1" } +dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "main", version = "0.1.0" } +dquic = { git = "https://github.com/genmeta/dquic.git", branch = "feat/v0.5.1", version = "0.5.1" } flume = "0.12" futures = "0.3" libc = "0.2" @@ -43,7 +43,7 @@ tokio = { version = "1", features = [ tracing = "0.1" x509-parser = "0.18" -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, optional = true } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.2.0", default-features = false, optional = true } http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } @@ -93,7 +93,7 @@ server = [ [dev-dependencies] clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", default-features = false, features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.2.0", default-features = false, features = [ "dquic", ] } shellexpand = "3" From a67cfed865682290f723a4baf4c1fd7372da34e2 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 12 Jun 2026 20:00:44 +0800 Subject: [PATCH 76/85] feat: derive dns selectors from certificate ski --- README.md | 7 ++ examples/README.md | 6 +- examples/publish.rs | 19 ++-- src/bin/ddns-server/error.rs | 14 +++ src/bin/ddns-server/policy.rs | 153 ++++++++++++++++++++++++++++- src/core/parser/record/endpoint.rs | 98 +++++++++++++++--- src/mdns/resolvers/mdns.rs | 58 ++++++----- src/publisher.rs | 57 +++++------ src/publisher/packet.rs | 37 +++---- src/resolvers.rs | 1 + src/resolvers/h3.rs | 31 +++--- src/resolvers/http.rs | 11 +-- src/resolvers/selector.rs | 134 +++++++++++++++++++++++++ tests/fixtures/malformed.der | Bin 0 -> 474 bytes tests/fixtures/missing.der | Bin 0 -> 447 bytes tests/fixtures/valid.der | Bin 0 -> 524 bytes 16 files changed, 496 insertions(+), 130 deletions(-) create mode 100644 src/resolvers/selector.rs create mode 100644 tests/fixtures/malformed.der create mode 100644 tests/fixtures/missing.der create mode 100644 tests/fixtures/valid.der diff --git a/README.md b/README.md index aaf79dc..106f8a3 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,13 @@ Flag bits: | `0x08` | `LOAD` | One-minute load value is present. | | `0x01` | `SIGNED` | Signature with explicit TLS signature scheme is present. | +For DHTTP endpoint publishing, `MAIN` and `sequence` are derived from the +publisher certificate's DHTTP subject key identifier. Operators do not choose +these fields manually: `primary` certificates publish `MAIN = true`, +`secondary` certificates publish `MAIN = false`, and the certificate-chain +sequence becomes the normalized endpoint-record sequence. An omitted sequence +field means sequence `0`. + Signed records encode the signature scheme in the record; the no-scheme signed format is not accepted. Legacy unsigned fixed-length endpoint address records are still parsed by length for address-only compatibility. diff --git a/examples/README.md b/examples/README.md index 61204c1..f299a72 100644 --- a/examples/README.md +++ b/examples/README.md @@ -86,8 +86,10 @@ Options: | `--sign ` | Whether to sign each endpoint `E` record. Defaults to `true`. | | `--host ` | DNS host to publish. Standard-policy servers require this to match the client certificate DNS SAN. | | `--addr ` | One or more socket addresses to publish. | -| `--is-main ` | Whether each endpoint is marked as the main address. Defaults to `true`. | -| `--sequence ` | Cluster sequence number encoded in the endpoint record; `0` disables the clustered flag. Defaults to `1`. | + +The example derives the endpoint selector from the client certificate SKI before +signing records. Use the correct certificate chain instead of manual selector +flags. The example sends `POST /publish?host=` with a binary DNS packet body. For Standard policy domains, the server requires a client certificate whose single diff --git a/examples/publish.rs b/examples/publish.rs index 57c9085..629fe9f 100644 --- a/examples/publish.rs +++ b/examples/publish.rs @@ -56,12 +56,6 @@ struct Options { /// 要发布的地址列表。 #[arg(long, value_delimiter = ',', num_args = 1..)] addr: Vec, - - #[arg(long, default_value_t = true)] - is_main: bool, - - #[arg(long, default_value_t = 1)] - sequence: u64, } fn default_h3_base_url() -> String { @@ -146,15 +140,18 @@ async fn main() -> io::Result<()> { } else { info!("publish.endpoint_signing.disabled"); } + let selector = identity + .dhttp_subject_key_identifier() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let chain = selector.chain(); for &addr in &opt.addr { - info!("Creating endpoint for address: {}", addr); + info!("creating endpoint for address: {}", addr); let mut endpoint = match addr { SocketAddr::V4(v4) => EndpointAddr::direct_v4(v4), SocketAddr::V6(v6) => EndpointAddr::direct_v6(v6), }; - endpoint.set_main(opt.is_main); - endpoint.set_sequence(opt.sequence); + endpoint.set_certificate_chain_key(chain); if opt.sign { info!("signing endpoint"); endpoint @@ -162,7 +159,7 @@ async fn main() -> io::Result<()> { .await .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } - info!("Publishing endpoint: {:?}", endpoint); + info!("publishing endpoint: {:?}", endpoint); let mut hosts = std::collections::HashMap::new(); hosts.insert(opt.host.clone(), vec![endpoint]); let packet = ddns::core::MdnsPacket::answer(0, &hosts).to_bytes(); @@ -170,7 +167,7 @@ async fn main() -> io::Result<()> { .publish(&opt.host, &packet) .await .map_err(io::Error::other)?; - info!("Successfully published endpoint for {}", addr); + info!("successfully published endpoint for {}", addr); } info!("publish.ok"); diff --git a/src/bin/ddns-server/error.rs b/src/bin/ddns-server/error.rs index 6bb6e70..c8930ba 100644 --- a/src/bin/ddns-server/error.rs +++ b/src/bin/ddns-server/error.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use dhttp_identity::name::DhttpName; #[derive(Debug, snafu::Snafu)] +#[snafu(module, visibility(pub(crate)))] pub enum AppError { #[snafu(display("missing host parameter"))] MissingHostParam, @@ -20,6 +21,16 @@ pub enum AppError { ClientCertDomainNotAllowed, #[snafu(display("invalid DNS packet: {message}"))] InvalidDnsPacket { message: String }, + #[snafu(display("publisher certificate selector is invalid"))] + PublisherCertificateSelector { + source: dhttp_identity::identity::ExtractDhttpSubjectKeyIdentifierError, + }, + #[snafu(display("endpoint record selector is invalid"))] + EndpointRecordSelector { + source: ddns::core::parser::record::endpoint::EndpointSelectorError, + }, + #[snafu(display("endpoint record selector does not match publisher certificate selector"))] + EndpointSelectorMismatch, #[snafu(display("no answers in packet"))] NoAnswersInPacket, #[snafu(display("signature required"))] @@ -41,6 +52,9 @@ impl AppError { AppError::MissingClientCertificate => http::StatusCode::UNAUTHORIZED, AppError::ClientCertDomainNotAllowed => http::StatusCode::FORBIDDEN, AppError::InvalidDnsPacket { .. } => http::StatusCode::BAD_REQUEST, + AppError::PublisherCertificateSelector { .. } => http::StatusCode::BAD_REQUEST, + AppError::EndpointRecordSelector { .. } => http::StatusCode::BAD_REQUEST, + AppError::EndpointSelectorMismatch => http::StatusCode::BAD_REQUEST, AppError::NoAnswersInPacket => http::StatusCode::UNPROCESSABLE_ENTITY, AppError::SignatureRequired => http::StatusCode::BAD_REQUEST, AppError::InvalidSignature => http::StatusCode::BAD_REQUEST, diff --git a/src/bin/ddns-server/policy.rs b/src/bin/ddns-server/policy.rs index 1034d31..413ad4b 100644 --- a/src/bin/ddns-server/policy.rs +++ b/src/bin/ddns-server/policy.rs @@ -1,8 +1,9 @@ use ddns::core::parser::{packet::be_packet, record::RData}; -use dhttp_identity::identity::RemoteAuthority; +use dhttp_identity::identity::{RemoteAuthority, RemoteAuthorityCertificateExt}; +use snafu::ResultExt; use tracing::{debug, warn}; -use crate::error::{AppError, normalize_host}; +use crate::error::{AppError, app_error, normalize_host}; // --------------------------------------------------------------------------- // Domain policy @@ -125,6 +126,8 @@ pub fn validate_dns_packet( return Ok(ValidatedDnsPacket::Empty); }; + validate_endpoint_selectors(&dns_packet, authority)?; + if require_signature { let has_signature = dns_packet .answers @@ -158,6 +161,47 @@ pub fn validate_dns_packet( }) } +fn validate_endpoint_selectors( + dns_packet: &ddns::core::parser::packet::Packet, + authority: &(impl RemoteAuthority + ?Sized), +) -> Result<(), AppError> { + let mut endpoints = dns_packet + .answers + .iter() + .filter_map(|record| match record.data() { + RData::E(endpoint) => Some(endpoint), + _ => None, + }); + + let Some(first_endpoint) = endpoints.next() else { + return Ok(()); + }; + + let expected = authority + .dhttp_subject_key_identifier() + .context(app_error::PublisherCertificateSelectorSnafu)? + .chain() + .clone(); + + let first = first_endpoint + .certificate_chain_key() + .context(app_error::EndpointRecordSelectorSnafu)?; + if first != expected { + return Err(AppError::EndpointSelectorMismatch); + } + + for endpoint in endpoints { + let actual = endpoint + .certificate_chain_key() + .context(app_error::EndpointRecordSelectorSnafu)?; + if actual != expected { + return Err(AppError::EndpointSelectorMismatch); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -169,7 +213,35 @@ mod tests { use super::*; #[derive(Debug)] - struct TestAuthority; + struct TestAuthority { + certs: Vec>, + } + + impl TestAuthority { + fn valid() -> Self { + Self { + certs: vec![CertificateDer::from( + include_bytes!("../../../tests/fixtures/valid.der").to_vec(), + )], + } + } + + fn missing_ski() -> Self { + Self { + certs: vec![CertificateDer::from( + include_bytes!("../../../tests/fixtures/missing.der").to_vec(), + )], + } + } + + fn malformed_ski() -> Self { + Self { + certs: vec![CertificateDer::from( + include_bytes!("../../../tests/fixtures/malformed.der").to_vec(), + )], + } + } + } impl RemoteAuthority for TestAuthority { fn name(&self) -> &str { @@ -177,17 +249,88 @@ mod tests { } fn cert_chain(&self) -> &[CertificateDer<'static>] { - &[] + &self.certs } } + fn packet_with_endpoint(endpoint: EndpointAddr) -> Vec { + let hosts: HashMap> = + HashMap::from([("reimu.pilot.dhttp.net".to_owned(), vec![endpoint])]); + MdnsPacket::answer(0, &hosts).to_bytes() + } + + #[test] + fn validate_dns_packet_accepts_matching_certificate_selector() { + let mut endpoint = EndpointAddr::direct_v4("192.0.2.10:4433".parse().unwrap()); + endpoint.set_main(true); + endpoint.set_sequence(0); + let packet = packet_with_endpoint(endpoint); + + let validated = validate_dns_packet(&packet, false, &TestAuthority::valid()).unwrap(); + + assert!(matches!(validated, ValidatedDnsPacket::Records { .. })); + } + + #[test] + fn validate_dns_packet_rejects_mismatched_endpoint_kind() { + let mut endpoint = EndpointAddr::direct_v4("192.0.2.10:4433".parse().unwrap()); + endpoint.set_main(false); + endpoint.set_sequence(0); + let packet = packet_with_endpoint(endpoint); + + let error = validate_dns_packet(&packet, false, &TestAuthority::valid()).unwrap_err(); + + assert!(matches!(error, AppError::EndpointSelectorMismatch)); + } + + #[test] + fn validate_dns_packet_rejects_mismatched_endpoint_sequence() { + let mut endpoint = EndpointAddr::direct_v4("192.0.2.10:4433".parse().unwrap()); + endpoint.set_main(true); + endpoint.set_sequence(7); + let packet = packet_with_endpoint(endpoint); + + let error = validate_dns_packet(&packet, false, &TestAuthority::valid()).unwrap_err(); + + assert!(matches!(error, AppError::EndpointSelectorMismatch)); + } + + #[test] + fn validate_dns_packet_rejects_missing_publisher_ski() { + let mut endpoint = EndpointAddr::direct_v4("192.0.2.10:4433".parse().unwrap()); + endpoint.set_main(true); + let packet = packet_with_endpoint(endpoint); + + let error = validate_dns_packet(&packet, false, &TestAuthority::missing_ski()).unwrap_err(); + + assert!(matches!( + error, + AppError::PublisherCertificateSelector { .. } + )); + } + + #[test] + fn validate_dns_packet_rejects_malformed_publisher_ski() { + let mut endpoint = EndpointAddr::direct_v4("192.0.2.10:4433".parse().unwrap()); + endpoint.set_main(true); + let packet = packet_with_endpoint(endpoint); + + let error = + validate_dns_packet(&packet, false, &TestAuthority::malformed_ski()).unwrap_err(); + + assert!(matches!( + error, + AppError::PublisherCertificateSelector { .. } + )); + } + #[test] fn validate_dns_packet_accepts_empty_packet_as_clear_operation() { let hosts: HashMap> = HashMap::from([("reimu.pilot.dhttp.net".to_owned(), Vec::new())]); let packet = MdnsPacket::answer(0, &hosts).to_bytes(); - let validated = validate_dns_packet(&packet, true, &TestAuthority).unwrap(); + let validated = validate_dns_packet(&packet, true, &TestAuthority::valid()).unwrap(); assert!(matches!(validated, ValidatedDnsPacket::Empty)); } diff --git a/src/core/parser/record/endpoint.rs b/src/core/parser/record/endpoint.rs index 8d91cd9..75167b5 100644 --- a/src/core/parser/record/endpoint.rs +++ b/src/core/parser/record/endpoint.rs @@ -8,6 +8,7 @@ use std::{ use base64::Engine; use bytes::BufMut; +use dhttp_identity::certificate::{CertificateChainKey, CertificateChainKind, CertificateSequence}; use dquic::qbase::net::addr::EndpointAddr as DquicEndpointAddr; use nom::{ IResult, Parser, @@ -35,6 +36,13 @@ pub enum SignEndpointError { }, } +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum EndpointSelectorError { + #[snafu(display("endpoint record sequence does not fit certificate sequence"))] + SequenceTooLarge { sequence: u64 }, +} + /// EndpointAddress record (Type E = 266) /// /// Unified endpoint format that encodes address family, routing, clustering and NAT information @@ -371,6 +379,28 @@ impl EndpointAddr { } } + pub fn certificate_chain_key(&self) -> Result { + let kind = if self.is_main() { + CertificateChainKind::Primary + } else { + CertificateChainKind::Secondary + }; + let sequence = self.sequence.map(VarInt::into_inner).unwrap_or(0); + if sequence > u64::from(u32::MAX) { + return endpoint_selector_error::SequenceTooLargeSnafu { sequence }.fail(); + } + let sequence = sequence as u32; + Ok(CertificateChainKey::new( + CertificateSequence::from(sequence), + kind, + )) + } + + pub fn set_certificate_chain_key(&mut self, chain: &CertificateChainKey) { + self.set_main(chain.kind() == CertificateChainKind::Primary); + self.set_sequence(u64::from(chain.sequence().get())); + } + pub fn load(&self) -> Option { self.load } @@ -749,20 +779,6 @@ impl TryFrom for DquicEndpointAddr { } } -pub async fn sign_endponit_address( - server_id: u8, - authority: Option<&(impl dhttp_identity::identity::LocalAuthority + ?Sized)>, - endpoint: DquicEndpointAddr, -) -> Option { - let mut ep: EndpointAddr = endpoint.try_into().ok()?; - ep.set_main(server_id == 0); - ep.set_sequence(server_id as u64); - if let Some(authority) = authority { - let _ = ep.sign_with_authority(authority).await; - } - Some(ep) -} - #[cfg(test)] mod tests { use std::{ @@ -771,6 +787,9 @@ mod tests { }; use bytes::BytesMut; + use dhttp_identity::certificate::{ + CertificateChainKey, CertificateChainKind, CertificateSequence, + }; use futures::future::BoxFuture; use ring::signature::KeyPair; use rustls::{ @@ -780,6 +799,10 @@ mod tests { use super::*; + fn chain(sequence: u32, kind: CertificateChainKind) -> CertificateChainKey { + CertificateChainKey::new(CertificateSequence::from(sequence), kind) + } + fn ed25519_spki(public_key: &[u8]) -> Vec { let mut spki = Vec::with_capacity(44); spki.extend_from_slice(&[ @@ -789,6 +812,53 @@ mod tests { spki } + #[test] + fn endpoint_selector_normalizes_missing_sequence_to_primary_zero() { + let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 5353); + let mut endpoint = EndpointAddr::direct_v4(addr); + endpoint.set_main(true); + + let selector = endpoint + .certificate_chain_key() + .expect("missing sequence normalizes to selector"); + + assert_eq!(selector, chain(0, CertificateChainKind::Primary)); + } + + #[test] + fn endpoint_selector_normalizes_missing_sequence_to_secondary_zero() { + let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 2), 5353); + let endpoint = EndpointAddr::direct_v4(addr); + + let selector = endpoint + .certificate_chain_key() + .expect("missing sequence normalizes to selector"); + + assert_eq!(selector, chain(0, CertificateChainKind::Secondary)); + } + + #[test] + fn endpoint_selector_sets_primary_and_secondary_chains() { + let addr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 3), 5353); + let mut endpoint = EndpointAddr::direct_v4(addr); + + endpoint.set_certificate_chain_key(&chain(7, CertificateChainKind::Primary)); + assert!(endpoint.is_main()); + assert!(endpoint.is_clustered()); + assert_eq!( + endpoint.certificate_chain_key().unwrap(), + chain(7, CertificateChainKind::Primary) + ); + + endpoint.set_certificate_chain_key(&chain(0, CertificateChainKind::Secondary)); + assert!(!endpoint.is_main()); + assert!(!endpoint.is_clustered()); + assert_eq!( + endpoint.certificate_chain_key().unwrap(), + chain(0, CertificateChainKind::Secondary) + ); + } + #[test] fn legacy_endpoint_v4_direct_without_meta() { let port = 5353u16; diff --git a/src/mdns/resolvers/mdns.rs b/src/mdns/resolvers/mdns.rs index 72f6010..16912ab 100644 --- a/src/mdns/resolvers/mdns.rs +++ b/src/mdns/resolvers/mdns.rs @@ -5,7 +5,7 @@ use std::{net::SocketAddr, sync::Arc}; #[cfg(feature = "mdns-resolver")] use dquic::qresolve::RecordStream; use dquic::{ - qbase::net::{Family, addr::EndpointAddr as DquicEndpointAddr}, + qbase::net::Family, qresolve::{Publish, PublishFuture, Resolve, ResolveFuture, Source}, }; use futures::{FutureExt, StreamExt, TryFutureExt, future, stream}; @@ -53,11 +53,8 @@ impl Resolve for MdnsResolver { let source = self.source(); self.query(name.to_owned()) .map_ok(move |list| { - stream::iter(list.into_iter().filter_map(move |ep| { - let ep = DquicEndpointAddr::try_from(ep).ok()?; - Some((source.clone(), ep)) - })) - .boxed() + let endpoints = crate::resolvers::selector::selected_endpoint_addrs(list); + stream::iter(endpoints.into_iter().map(move |ep| (source.clone(), ep))).boxed() }) .boxed() } @@ -255,29 +252,46 @@ impl MdnsResolvers { pub async fn query(&self, name: &str) -> io::Result { let mut lookup_futures = FuturesUnordered::new(); + let mut has_resolver = false; self.for_each_resolver(|resolver| { + has_resolver = true; let source = resolver.source(); - lookup_futures.push(resolver.query(name.to_owned()).map_ok(move |eps| { - stream::iter(eps.into_iter().filter_map(move |ep| { - let ep = DquicEndpointAddr::try_from(ep).ok()?; - Some((source.clone(), ep)) - })) - })); + lookup_futures.push( + resolver + .query(name.to_owned()) + .map_ok(move |eps| (source, eps)), + ); }); + if !has_resolver { + return Err(io::Error::other("no mdns resolvers available")); + } let mut last_error = None; - let no_resolver = || io::Error::other("no mdns resolvers available"); - let stream = loop { - match lookup_futures.next().await { - Some(Ok(stream)) => break stream, - Some(Err(error)) => last_error = Some(error), - None => return Err(last_error.unwrap_or_else(no_resolver)), + let mut has_success = false; + let mut records = Vec::new(); + while let Some(result) = lookup_futures.next().await { + match result { + Ok((source, endpoints)) => { + has_success = true; + records.extend( + endpoints + .into_iter() + .map(|endpoint| (source.clone(), endpoint)), + ); + } + Err(error) => last_error = Some(error), } - }; + } + + if !has_success { + return Err( + last_error.unwrap_or_else(|| io::Error::other("no mdns resolvers available")) + ); + } + + let records = crate::resolvers::selector::selected_endpoint_records(records); - Ok(stream - .chain(lookup_futures.flat_map(stream::iter).flatten()) - .boxed()) + Ok(stream::iter(records).boxed()) } /// Discover mDNS broadcasts from all active resolvers. diff --git a/src/publisher.rs b/src/publisher.rs index 653c836..1375a2e 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -48,13 +48,12 @@ pub enum PublishOnceError { }, } -/// Optional metadata applied to endpoint records before signing. +/// Deprecated compatibility options for the old endpoint publisher API. +/// +/// `server_id` is ignored. Endpoint record selectors are derived from the +/// publisher certificate's DHTTP subject key identifier. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct PublishOptions { - /// Stable server identifier for names served by multiple publishers. - /// - /// `0` marks the endpoint as the main record. Non-zero values mark the - /// record as clustered and encode the identifier as its sequence number. pub server_id: Option, } @@ -110,10 +109,6 @@ where &self.signer } - pub fn options(&self) -> PublishOptions { - self.signer.options() - } - pub fn resolver(&self) -> &Arc { &self.resolver } @@ -188,10 +183,6 @@ where &self.publisher } - pub fn options(&self) -> PublishOptions { - self.publisher.options() - } - pub fn interval(&self) -> Duration { self.interval } @@ -326,18 +317,14 @@ pub type EndpointPublisherLoop = EndpointPublicationLoop< #[cfg(test)] mod tests { - use std::{ - fmt, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, - }; + #[cfg(feature = "http-resolver")] + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::{fmt, sync::Arc, time::Duration}; use dquic::qresolve::{ResolveFuture, Source}; use futures::{FutureExt, StreamExt, future::BoxFuture, stream}; use rustls::pki_types::{CertificateDer, SubjectPublicKeyInfoDer}; + #[cfg(feature = "http-resolver")] use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; @@ -356,7 +343,13 @@ mod tests { } fn cert_chain(&self) -> &[CertificateDer<'static>] { - &[] + static CERTS: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| { + vec![CertificateDer::from( + include_bytes!("../tests/fixtures/valid.der").to_vec(), + )] + }); + CERTS.as_slice() } fn public_key(&self) -> SubjectPublicKeyInfoDer<'_> { @@ -442,9 +435,8 @@ mod tests { } #[tokio::test] - async fn signer_applies_publish_options_server_id() { - let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)) - .with_options(PublishOptions { server_id: Some(2) }); + async fn signer_applies_certificate_selector_from_authority_ski() { + let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)); let name: Name<'static> = "authority.example".parse().unwrap(); let endpoint = @@ -456,15 +448,18 @@ mod tests { panic!("expected endpoint record"); }; - assert!(!endpoint.is_main()); - assert!(endpoint.is_clustered()); + assert!(endpoint.is_main()); + assert!(!endpoint.is_clustered()); assert!(endpoint.is_signed()); + assert_eq!( + endpoint.certificate_chain_key().unwrap().sequence().get(), + 0 + ); } #[tokio::test] async fn signer_uses_supplied_record_owner_name() { - let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)) - .with_options(PublishOptions { server_id: Some(2) }); + let signer = EndpointRecordSigner::new(Arc::new(TestAuthority)); let name: Name<'static> = "nat.genmeta.net".parse().unwrap(); let endpoint = @@ -477,8 +472,8 @@ mod tests { }; assert_eq!(record.name().to_string(), "nat.genmeta.net"); - assert!(!endpoint.is_main()); - assert!(endpoint.is_clustered()); + assert!(endpoint.is_main()); + assert!(!endpoint.is_clustered()); assert!(endpoint.is_signed()); } diff --git a/src/publisher/packet.rs b/src/publisher/packet.rs index 7528e8a..956afe2 100644 --- a/src/publisher/packet.rs +++ b/src/publisher/packet.rs @@ -1,10 +1,12 @@ use std::{collections::HashMap, sync::Arc}; -use dhttp_identity::{identity::LocalAuthority, name::Name}; +use dhttp_identity::{ + identity::{LocalAuthority, LocalAuthorityCertificateExt}, + name::Name, +}; use dquic::qbase::net::addr::EndpointAddr; use snafu::{ResultExt, Snafu}; -use super::PublishOptions; use crate::core::{ MdnsPacket, parser::record::endpoint::{EndpointAddr as DnsEndpointAddr, SignEndpointError}, @@ -15,13 +17,16 @@ use crate::core::{ pub enum SignEndpointRecordsError { #[snafu(display("failed to encode endpoint address"))] EncodeEndpoint, + #[snafu(display("failed to extract dhttp certificate selector"))] + CertificateSelector { + source: dhttp_identity::identity::ExtractDhttpSubjectKeyIdentifierError, + }, #[snafu(display("failed to sign endpoint address"))] SignEndpoint { source: SignEndpointError }, } pub struct EndpointRecordSigner { authority: Arc, - options: PublishOptions, } impl std::fmt::Debug for EndpointRecordSigner @@ -31,7 +36,6 @@ where fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EndpointRecordSigner") .field("authority", &self.authority.name()) - .field("options", &self.options) .finish() } } @@ -41,19 +45,7 @@ where A: LocalAuthority + Send + Sync + ?Sized, { pub fn new(authority: Arc) -> Self { - Self { - authority, - options: PublishOptions::default(), - } - } - - pub fn with_options(mut self, options: PublishOptions) -> Self { - self.options = options; - self - } - - pub fn options(&self) -> PublishOptions { - self.options + Self { authority } } pub fn authority(&self) -> &Arc { @@ -65,15 +57,18 @@ where name: &Name<'_>, endpoints: &[EndpointAddr], ) -> Result, SignEndpointRecordsError> { + let selector = self + .authority + .dhttp_subject_key_identifier() + .context(sign_endpoint_records_error::CertificateSelectorSnafu)?; + let chain = selector.chain(); + let mut signed = Vec::with_capacity(endpoints.len()); for endpoint in endpoints { let Ok(mut endpoint) = DnsEndpointAddr::try_from(*endpoint) else { return sign_endpoint_records_error::EncodeEndpointSnafu.fail(); }; - if let Some(server_id) = self.options.server_id { - endpoint.set_main(server_id == 0); - endpoint.set_sequence(server_id.into()); - } + endpoint.set_certificate_chain_key(chain); endpoint .sign_with_authority(self.authority.as_ref()) .await diff --git a/src/resolvers.rs b/src/resolvers.rs index 014e520..6da796b 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -90,6 +90,7 @@ impl std::str::FromStr for DnsScheme { } pub mod deferred; +pub(crate) mod selector; pub mod weak; type ArcResolver = Arc; diff --git a/src/resolvers/h3.rs b/src/resolvers/h3.rs index 22f45aa..a54cdc6 100644 --- a/src/resolvers/h3.rs +++ b/src/resolvers/h3.rs @@ -325,28 +325,25 @@ where let (_remain, multi) = be_multi_response(response.as_ref()).map_err(|_| Error::ParseMultiResponse)?; - let mut addrs = Vec::new(); + let mut endpoint_records = Vec::new(); for r in multi.records { let (_remain, packet) = be_packet(&r.dns).map_err(|source| Error::ParseRecords { source: source.to_owned(), })?; - addrs.extend( - packet - .answers - .iter() - .filter_map(|answer| match answer.data() { - record::RData::E(ep) => { - let endpoint = TryInto::::try_into(ep.clone()).ok()?; - trace!(?endpoint, "parsed endpoint from record"); - Some(endpoint) - } - _ => { - tracing::debug!(?answer, "ignored record"); - None - } - }), - ); + endpoint_records.extend(packet.answers.iter().filter_map( + |answer| match answer.data() { + record::RData::E(ep) => Some(ep.clone()), + _ => { + tracing::debug!(?answer, "ignored record"); + None + } + }, + )); + } + let addrs = crate::resolvers::selector::selected_endpoint_addrs(endpoint_records); + for endpoint in &addrs { + trace!(?endpoint, "parsed endpoint from selected record group"); } if addrs.is_empty() { diff --git a/src/resolvers/http.rs b/src/resolvers/http.rs index 80a46c8..03984d9 100644 --- a/src/resolvers/http.rs +++ b/src/resolvers/http.rs @@ -165,20 +165,17 @@ impl Resolve for HttpResolver { source: source.to_owned(), })?; - let addrs = packet + let endpoints = packet .answers .iter() .filter_map(|answer| match answer.data() { - record::RData::E(ep) => { - let endpoint = ep.clone().try_into().ok()?; - Some(endpoint) - } + record::RData::E(ep) => Some(ep.clone()), _ => { tracing::debug!(?answer, "ignored record"); None } - }) - .collect::>(); + }); + let addrs = crate::resolvers::selector::selected_endpoint_addrs(endpoints); if addrs.is_empty() { return Err(Error::NoRecordFound); } diff --git a/src/resolvers/selector.rs b/src/resolvers/selector.rs new file mode 100644 index 0000000..87017f4 --- /dev/null +++ b/src/resolvers/selector.rs @@ -0,0 +1,134 @@ +use dhttp_identity::certificate::{CertificateChainKey, CertificateChainKind}; +use dquic::qbase::net::addr::EndpointAddr as DquicEndpointAddr; + +use crate::core::parser::record::endpoint::EndpointAddr as DnsEndpointAddr; + +pub(crate) fn selected_endpoint_addrs( + records: impl IntoIterator, +) -> Vec { + selected_endpoint_records(records.into_iter().map(|record| ((), record))) + .into_iter() + .map(|((), endpoint)| endpoint) + .collect() +} + +pub(crate) fn selected_endpoint_records( + records: impl IntoIterator, +) -> Vec<(T, DquicEndpointAddr)> { + let mut groups: Vec<(CertificateChainKey, Vec<(T, DquicEndpointAddr)>)> = Vec::new(); + + for (tag, record) in records { + let Ok(selector) = record.certificate_chain_key() else { + continue; + }; + let Ok(endpoint) = DquicEndpointAddr::try_from(record) else { + continue; + }; + + if let Some((_key, endpoints)) = groups.iter_mut().find(|(key, _)| *key == selector) { + endpoints.push((tag, endpoint)); + } else { + groups.push((selector, vec![(tag, endpoint)])); + } + } + + let selected = groups + .iter() + .position(|(key, endpoints)| { + key.kind() == CertificateChainKind::Primary && !endpoints.is_empty() + }) + .or_else(|| { + groups + .iter() + .position(|(_key, endpoints)| !endpoints.is_empty()) + }); + + selected + .map(|index| groups.swap_remove(index).1) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use crate::core::parser::record::endpoint::EndpointAddr; + + fn direct(addr: &str, main: bool, sequence: u64) -> EndpointAddr { + let mut endpoint = match addr.parse().unwrap() { + std::net::SocketAddr::V4(addr) => EndpointAddr::direct_v4(addr), + std::net::SocketAddr::V6(addr) => EndpointAddr::direct_v6(addr), + }; + endpoint.set_main(main); + endpoint.set_sequence(sequence); + endpoint + } + + #[test] + fn selected_endpoint_addrs_prefers_primary_group() { + let secondary = direct("192.0.2.20:4433", false, 0); + let primary_a = direct("192.0.2.10:4433", true, 2); + let primary_b = direct("192.0.2.11:4433", true, 2); + + let selected = super::selected_endpoint_addrs([secondary, primary_a, primary_b]); + + assert_eq!(selected.len(), 2); + assert_eq!( + selected[0], + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.10:4433".parse().unwrap()) + ); + assert_eq!( + selected[1], + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.11:4433".parse().unwrap()) + ); + } + + #[test] + fn selected_endpoint_addrs_uses_one_secondary_group_when_no_primary_exists() { + let secondary_a = direct("192.0.2.20:4433", false, 5); + let secondary_b = direct("192.0.2.21:4433", false, 5); + let other_secondary = direct("192.0.2.30:4433", false, 6); + + let selected = super::selected_endpoint_addrs([secondary_a, secondary_b, other_secondary]); + + assert_eq!(selected.len(), 2); + assert_eq!( + selected[0], + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.20:4433".parse().unwrap()) + ); + assert_eq!( + selected[1], + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.21:4433".parse().unwrap()) + ); + } + + #[test] + fn selected_endpoint_addrs_treats_missing_sequence_as_zero() { + let mut first = direct("192.0.2.40:4433", true, 0); + first.set_clustered(false); + let second = direct("192.0.2.41:4433", true, 0); + + let selected = super::selected_endpoint_addrs([first, second]); + + assert_eq!(selected.len(), 2); + } + + #[test] + fn selected_endpoint_records_uses_one_group_across_sources() { + let selected = super::selected_endpoint_records([ + ("wifi", direct("192.0.2.50:4433", true, 3)), + ("ethernet", direct("192.0.2.51:4433", true, 4)), + ("wifi", direct("192.0.2.52:4433", true, 3)), + ]); + + assert_eq!(selected.len(), 2); + assert_eq!(selected[0].0, "wifi"); + assert_eq!( + selected[0].1, + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.50:4433".parse().unwrap()) + ); + assert_eq!(selected[1].0, "wifi"); + assert_eq!( + selected[1].1, + dquic::qbase::net::addr::EndpointAddr::direct("192.0.2.52:4433".parse().unwrap()) + ); + } +} diff --git a/tests/fixtures/malformed.der b/tests/fixtures/malformed.der new file mode 100644 index 0000000000000000000000000000000000000000..d3f98e037e132e1425524928fdffb88be92da9b6 GIT binary patch literal 474 zcmXqLV!US1#2CGRnTe5!NrbJS=>O_!=^3prmGTqrRll1eWj@z{i;Y98&EuRc3p0~} zmZ64$8XI#c3p0;=ZemVaeo<~}ie73(Vs1fBs$Oz_u3kz;NlAfTUTTSfoH(zMnSq&s zrJrHo3zZd%K(yL{6OClE@+p;%LOMQQr?>xV{$!A`*=d5aURD8SB_2>FU zL3{UXUi|wLkD*FTA)hO2?;DlhOLTv(SMHcCdtqhzf-{Sq3>*x!fc}*gWKlLyY$E7q zA&^==7H$^Ky!;Yfa9ALRA9E&yK^l`G!=tZ9JF8`ncRa6IecS7G+q5E=~s1;9sOr zV$NVNNM$l)Sf3gx{`AEW}v+&Ko MS(~F1rXE}l08@6TYXATM literal 0 HcmV?d00001 diff --git a/tests/fixtures/valid.der b/tests/fixtures/valid.der new file mode 100644 index 0000000000000000000000000000000000000000..d5566ddfdea106e96eea4f16a94fb329c1176d10 GIT binary patch literal 524 zcmXqLV&X7pV(eYO%*4pVB;sE%W9Q%LDf&u6Tkdu@q?~lPIMK*}i;Y98&EuRc3p0~} zx}mCpG8=O!3p0;Qa!zJyUWs06MPhD2PO4sVey(0hMoCG5US4X6ft)z6k(q&+fu*6D zfq{ud6ohMN0Ob;IP?>=M8#~y3CPp?^?M4;`CFUdsmIynG(mSsg^s;~Vn^d^B?&pry zWcl83_O<82P0hXH-0D)8`|T%kaM+1%ei9^hH>vx7qtn@H1NF`8cSWw2Zd-bE?e)i& zks+x+4<%R^eUGbgTrH6A{OF!>+2Pfxt*e>^+!t3FR2ZlOeJU%+qF^A`gx|NmAccG^ zZY(YaRzPTIWNcz;W^Q4bn3SB7nnp4oDa4sG84S{x3>lWU@f9y_zQ%vCSjy93Lh6wZ zC;xl8A8fBv;|XoB-oUut_$g;at(Y@&>64bZ91+K+0<|nYB7N@tkJ-OI>79=HC$n>P lmdB6l)yhYVmH+Zdm`O|$QM?#hG$B>t$Ffe&>z1)RmjJD^v-bc1 literal 0 HcmV?d00001 From 81eae5d543941990fd6836ab15da472e0e9070a1 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 12 Jun 2026 21:06:27 +0800 Subject: [PATCH 77/85] refactor: remove publish options compatibility --- src/publisher.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/publisher.rs b/src/publisher.rs index 1375a2e..13ba706 100644 --- a/src/publisher.rs +++ b/src/publisher.rs @@ -48,15 +48,6 @@ pub enum PublishOnceError { }, } -/// Deprecated compatibility options for the old endpoint publisher API. -/// -/// `server_id` is ignored. Endpoint record selectors are derived from the -/// publisher certificate's DHTTP subject key identifier. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct PublishOptions { - pub server_id: Option, -} - pub trait PublisherResolver: Send + Sync + 'static { fn as_resolver(&self) -> &(dyn Resolve + Send + Sync); } From 22b898873e1d73632a46a3ac3d9cdfa278db68e8 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 14 Jun 2026 14:36:06 +0800 Subject: [PATCH 78/85] test: lock dns error block format --- src/resolvers.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/src/resolvers.rs b/src/resolvers.rs index 6da796b..0076708 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -270,7 +270,7 @@ impl Resolve for Resolvers { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{error::Error as StdError, fmt, io, str::FromStr}; #[cfg(feature = "mdns-resolver")] use super::MdnsResolvers; @@ -281,9 +281,54 @@ mod tests { ))] use super::Resolvers; use super::{ - DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsScheme, resolvable_name, + DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsErrors, DnsScheme, + resolvable_name, }; + #[derive(Debug)] + struct TestSourceError { + message: &'static str, + source: Option>, + } + + impl TestSourceError { + fn leaf(message: &'static str) -> Self { + Self { + message, + source: None, + } + } + + fn with_source(message: &'static str, source: TestSourceError) -> Self { + Self { + message, + source: Some(Box::new(source)), + } + } + } + + impl fmt::Display for TestSourceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.message) + } + } + + impl StdError for TestSourceError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn StdError + 'static)) + } + } + + fn other_error(message: &'static str) -> io::Error { + io::Error::other(message) + } + + fn chained_other_error(root: TestSourceError) -> io::Error { + io::Error::other(root) + } + #[test] fn resolver_defaults_come_from_compile_time_environment() { if let Some(expected) = option_env!("DHTTP_H3_DNS_SERVER") { @@ -336,6 +381,85 @@ mod tests { assert!(DnsScheme::from_str("dht").is_err()); } + #[test] + fn dns_errors_render_no_resolvers_available_when_empty() { + let error = DnsErrors { errors: vec![] }; + + assert_eq!(error.to_string(), "no DNS resolvers available"); + } + + #[test] + fn dns_errors_render_resolver_bullets_in_stored_order() { + let error = DnsErrors { + errors: vec![ + ( + "System DNS Resolver".to_string(), + other_error("invalid socket address"), + ), + ("mDNS resolvers".to_string(), other_error("timed out")), + ], + }; + + assert_eq!( + error.to_string(), + concat!( + "all DNS resolvers failed\n", + " - System DNS Resolver: invalid socket address\n", + " - mDNS resolvers: timed out" + ) + ); + } + + #[test] + fn dns_errors_render_numbered_source_chain_for_one_resolver() { + let error = DnsErrors { + errors: vec![( + "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(), + chained_other_error(TestSourceError::with_source( + "deferred resolver lookup failed", + TestSourceError::leaf("no DNS record found"), + )), + )], + }; + + assert_eq!( + error.to_string(), + concat!( + "all DNS resolvers failed\n", + " - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n", + " 1. deferred resolver lookup failed\n", + " 2. no DNS record found" + ) + ); + } + + #[test] + fn dns_errors_render_repeated_source_messages_without_deduplication() { + let error = DnsErrors { + errors: vec![( + "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(), + chained_other_error(TestSourceError::with_source( + "deferred resolver lookup failed", + TestSourceError::with_source( + "deferred resolver lookup failed", + TestSourceError::leaf("no DNS record found"), + ), + )), + )], + }; + + assert_eq!( + error.to_string(), + concat!( + "all DNS resolvers failed\n", + " - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n", + " 1. deferred resolver lookup failed\n", + " 2. deferred resolver lookup failed\n", + " 3. no DNS record found" + ) + ); + } + #[cfg(feature = "mdns-resolver")] #[tokio::test] async fn resolvers_builder_can_enable_mdns() { From eace052cd489ca4646406e484fffb85179e3d959 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 14 Jun 2026 14:37:54 +0800 Subject: [PATCH 79/85] fix: format dns resolver error blocks --- src/resolvers.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/resolvers.rs b/src/resolvers.rs index 0076708..337f980 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -9,7 +9,6 @@ use dquic::{ qresolve::{Resolve, ResolveFuture, Source}, }; use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream}; -use snafu::Report; use tokio::io; #[cfg(feature = "h3x-resolver")] @@ -122,14 +121,40 @@ pub struct DnsErrors { errors: Vec<(String, io::Error)>, } +fn format_dns_error_sources( + f: &mut fmt::Formatter<'_>, + error: &(dyn Error + 'static), +) -> fmt::Result { + let mut index = 1; + let mut current = error.source(); + + while let Some(source) = current { + write!(f, "\n {index}. {source}")?; + index += 1; + current = source.source(); + } + + Ok(()) +} + +fn format_dns_error_entry( + f: &mut fmt::Formatter<'_>, + resolver: &str, + error: &io::Error, +) -> fmt::Result { + write!(f, "\n - {resolver}: {error}")?; + format_dns_error_sources(f, error) +} + impl fmt::Display for DnsErrors { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.errors.is_empty() { return write!(f, "no DNS resolvers available"); } - writeln!(f, "all DNS resolvers failed")?; - for (resolver, error) in self.errors.iter() { - write!(f, "`{resolver}` failed: {}", Report::from_error(error))?; + + write!(f, "all DNS resolvers failed")?; + for (resolver, error) in &self.errors { + format_dns_error_entry(f, resolver, error)?; } Ok(()) } @@ -427,8 +452,7 @@ mod tests { concat!( "all DNS resolvers failed\n", " - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n", - " 1. deferred resolver lookup failed\n", - " 2. no DNS record found" + " 1. no DNS record found" ) ); } @@ -454,8 +478,7 @@ mod tests { "all DNS resolvers failed\n", " - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n", " 1. deferred resolver lookup failed\n", - " 2. deferred resolver lookup failed\n", - " 3. no DNS record found" + " 2. no DNS record found" ) ); } From 5afe65dbe4d935845931896468c0e2dfa8f563a4 Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 14 Jun 2026 23:13:01 +0800 Subject: [PATCH 80/85] release: prepare v0.3.0 --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4584e62..73afeb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ddns" description = "DNS discovery and resolver support for DHTTP applications" -version = "0.2.0" +version = "0.3.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/genmeta/ddns" @@ -43,7 +43,7 @@ tokio = { version = "1", features = [ tracing = "0.1" x509-parser = "0.18" -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.2.0", default-features = false, optional = true } +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.3.0", default-features = false, optional = true } http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } @@ -93,7 +93,7 @@ server = [ [dev-dependencies] clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.2.0", default-features = false, features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.3.0", default-features = false, features = [ "dquic", ] } shellexpand = "3" From ed23d98da6bb72e0047d44e1a0c90d1ca05caabe Mon Sep 17 00:00:00 2001 From: eareimu Date: Sun, 14 Jun 2026 23:14:59 +0800 Subject: [PATCH 81/85] refactor(server): use explicit bind patterns --- server.toml | 4 +- src/bin/ddns-server/config.rs | 77 ++++++++++++++++++++++++++++++++--- src/bin/ddns-server/main.rs | 33 +-------------- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/server.toml b/server.toml index d819957..90eafe6 100644 --- a/server.toml +++ b/server.toml @@ -1,8 +1,8 @@ # ddns DNS-over-HTTP/3 server configuration # All fields are optional; the values shown below are the built-in defaults. -# Socket address to listen on. -listen = "0.0.0.0:4433" +# Bind patterns to listen on. Dual-stack service is expressed explicitly. +binds = ["0.0.0.0:4433", "[::]:4433"] # TLS server name (SNI). server_name = "dns.genmeta.net" diff --git a/src/bin/ddns-server/config.rs b/src/bin/ddns-server/config.rs index 245c112..8b663bd 100644 --- a/src/bin/ddns-server/config.rs +++ b/src/bin/ddns-server/config.rs @@ -1,10 +1,12 @@ use std::{ net::SocketAddr, path::{Path, PathBuf}, + str::FromStr, }; use clap::Parser; -use serde::Deserialize; +use h3x::dquic::binds::BindPattern; +use serde::{Deserialize, Deserializer, de::Error as _}; // --------------------------------------------------------------------------- // CLI @@ -29,9 +31,12 @@ pub struct Config { /// Redis URL (e.g. "redis://127.0.0.1/"). Omit to use in-memory storage. pub redis: Option, - /// Socket to listen on. - #[serde(default = "Config::default_listen")] - pub listen: SocketAddr, + /// Bind patterns to listen on. + #[serde( + default = "Config::default_binds", + deserialize_with = "deserialize_bind_patterns" + )] + pub binds: Vec, /// Server name (used as TLS SNI). #[serde(default = "Config::default_server_name")] @@ -74,8 +79,13 @@ impl Config { self } - pub fn default_listen() -> SocketAddr { - "0.0.0.0:4433".parse().unwrap() + pub fn default_binds() -> Vec { + ["0.0.0.0:4433", "[::]:4433"] + .into_iter() + .map(|value| { + BindPattern::from_str(value).expect("default bind pattern should be valid") + }) + .collect() } pub fn default_server_name() -> String { "localhost".into() @@ -97,6 +107,21 @@ impl Config { } } +fn deserialize_bind_patterns<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let values = Vec::::deserialize(deserializer)?; + values + .into_iter() + .map(|value| { + BindPattern::from_str(&value).map_err(|error| { + D::Error::custom(format!("invalid bind pattern `{value}`: {error}")) + }) + }) + .collect() +} + fn expand_home_dir(path: &Path) -> PathBuf { let Some(path_str) = path.to_str() else { return path.to_path_buf(); @@ -144,3 +169,43 @@ pub enum PolicyKind { Standard, OpenMulti, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_binds_are_explicit_dual_stack() { + let binds = Config::default_binds(); + + assert_eq!(binds.len(), 2); + assert_eq!(binds[0].to_string(), "inet://0.0.0.0:4433"); + assert_eq!(binds[1].to_string(), "inet://[::]:4433"); + } + + #[test] + fn config_parses_bare_socket_bind_patterns() { + let config: Config = toml::from_str( + r#" + binds = ["0.0.0.0:4433", "[::]:4433"] + "#, + ) + .expect("config should parse"); + + assert_eq!(config.binds.len(), 2); + assert_eq!(config.binds[0].to_string(), "inet://0.0.0.0:4433"); + assert_eq!(config.binds[1].to_string(), "inet://[::]:4433"); + } + + #[test] + fn legacy_listen_field_is_rejected() { + let error = toml::from_str::( + r#" + listen = "0.0.0.0:4433" + "#, + ) + .expect_err("legacy listen should be rejected"); + + assert!(error.to_string().contains("unknown field `listen`")); + } +} diff --git a/src/bin/ddns-server/main.rs b/src/bin/ddns-server/main.rs index fdcfc0e..735fa74 100644 --- a/src/bin/ddns-server/main.rs +++ b/src/bin/ddns-server/main.rs @@ -9,7 +9,6 @@ use std::{ collections::HashMap, io, net::SocketAddr, - str::FromStr, sync::Arc, task::{Context, Poll}, }; @@ -20,7 +19,6 @@ use futures::future::BoxFuture; use h3x::{ dquic::{ Identity, Network, QuicEndpoint, - binds::BindPattern, cert::handy::{ToCertificate, ToPrivateKey}, server::ServerQuicConfig, }, @@ -82,17 +80,6 @@ impl tower_service::Service for DnsService { } } -fn bind_patterns_for_listen(listen: SocketAddr) -> Vec { - let bind_addr = match listen { - SocketAddr::V4(addr) if addr.ip().is_unspecified() => { - SocketAddr::new(std::net::Ipv6Addr::UNSPECIFIED.into(), addr.port()) - } - addr => addr, - }; - - vec![BindPattern::from_str(&format!("inet://{bind_addr}")).expect("valid bind pattern")] -} - // --------------------------------------------------------------------------- // TLS helpers // --------------------------------------------------------------------------- @@ -243,28 +230,12 @@ async fn main() -> Result<(), Box> { .network(Network::builder().build()) .identity(identity) .server(server_config) - .bind(Arc::new(bind_patterns_for_listen(config.listen))) + .bind(Arc::new(config.binds.clone())) .build() .await; let server = Arc::new(H3Endpoint::new(quic)); - info!(listen = %config.listen, server_name = %config.server_name, "h3_server.start"); + info!(binds = ?config.binds, server_name = %config.server_name, "h3_server.start"); server.listen_owned(router).await?; Ok(()) } - -#[cfg(test)] -mod tests { - use std::net::SocketAddr; - - use super::*; - - #[test] - fn unspecified_ipv4_listen_uses_dual_stack_wildcard() { - let listen: SocketAddr = "0.0.0.0:4433".parse().unwrap(); - let patterns = bind_patterns_for_listen(listen); - - assert_eq!(patterns.len(), 1); - assert_eq!(patterns[0].to_string(), "inet://[::]:4433"); - } -} From f705233e4af02a22624be268d713b1649e8ffa10 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 15 Jun 2026 12:37:51 +0800 Subject: [PATCH 82/85] release: converge upstream tags --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 73afeb7..01c8eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ base64 = "0.22" bitfield-struct = "0.13" bytes = "1" dashmap = "6" -dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "main", version = "0.1.0" } -dquic = { git = "https://github.com/genmeta/dquic.git", branch = "feat/v0.5.1", version = "0.5.1" } +dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "publish", version = "0.1.0" } +dquic = { git = "https://github.com/genmeta/dquic.git", tag = "v0.5.1", version = "0.5.1" } flume = "0.12" futures = "0.3" libc = "0.2" @@ -43,7 +43,7 @@ tokio = { version = "1", features = [ tracing = "0.1" x509-parser = "0.18" -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.3.0", default-features = false, optional = true } +h3x = { git = "https://github.com/genmeta/h3x.git", tag = "v0.3.0", version = "0.3.0", default-features = false, optional = true } http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } @@ -93,7 +93,7 @@ server = [ [dev-dependencies] clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "endpoint", version = "0.3.0", default-features = false, features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", tag = "v0.3.0", version = "0.3.0", default-features = false, features = [ "dquic", ] } shellexpand = "3" From 5990614c5d910645be7db3bd6648732840d307ee Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 15 Jun 2026 13:57:02 +0800 Subject: [PATCH 83/85] fix: publish dyns package name --- .github/workflows/publish-crates.yml | 2 +- Cargo.toml | 5 ++++- README.md | 11 ++++++++--- examples/README.md | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 59312cc..6eae601 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -33,7 +33,7 @@ jobs: uses: rust-lang/crates-io-auth-action@v1 id: auth - - name: Release ddns crate + - name: Release dyns crate shell: bash env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} diff --git a/Cargo.toml b/Cargo.toml index 01c8eec..2bda2a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ddns" +name = "dyns" description = "DNS discovery and resolver support for DHTTP applications" version = "0.3.0" edition = "2024" @@ -10,6 +10,9 @@ keywords = ["dhttp", "dns", "mdns", "http3", "quic"] categories = ["network-programming", "asynchronous"] autoexamples = false +[lib] +name = "ddns" + [dependencies] base64 = "0.22" bitfield-struct = "0.13" diff --git a/README.md b/README.md index 106f8a3..071aaaa 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ `ddns` provides DNS discovery and resolver support for DHTTP applications. It is a single Rust package: the historical `ddns-core`, `gmdns`, `ddns`, and `ddns-server` crate boundaries now live as modules and feature-gated targets in -one crate named `ddns`. +one published Cargo package named `dyns`, with a library target kept as `ddns` +for source compatibility. ## Crate layout @@ -17,8 +18,12 @@ one crate named `ddns`. `ddns` is endpoint-facing support code for the DHTTP ecosystem. Applications normally reach it through the `dhttp` endpoint facade; lower-level consumers can -depend on package `ddns` directly when they need DNS wire types, resolver -composition, mDNS, or the DNS-over-H3 server. +depend on package `dyns` directly (typically renamed locally to `ddns`) when +they need DNS wire types, resolver composition, mDNS, or the DNS-over-H3 server. + +```toml +ddns = { package = "dyns", version = "0.3.0" } +``` ## Features diff --git a/examples/README.md b/examples/README.md index f299a72..c1e5780 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,7 @@ # DDNS examples -This directory contains runnable examples for the single `ddns` package. +This directory contains runnable examples for the single published `dyns` +package, whose library target remains `ddns`. | Example | Feature requirement | Purpose | | --- | --- | --- | From d9551af34c0eb2caa0c2dd80db74072fa4b96676 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 15 Jun 2026 16:08:14 +0800 Subject: [PATCH 84/85] release: converge registry deps and gate crates publish --- .github/workflows/publish-crates.yml | 57 ++++++++++++++++++++++++++-- Cargo.toml | 8 ++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 6eae601..579324f 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -46,8 +46,59 @@ jobs: mode=dry-run fi + package_name=dyns + package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + if [[ "$mode" == "dry-run" ]]; then - cargo publish --dry-run - else - cargo publish + cargo publish --dry-run --locked + exit 0 fi + + crate_state="$( + python3 - <<'PY' "$package_name" "$package_version" + import sys + import urllib.error + import urllib.request + + name, version = sys.argv[1], sys.argv[2] + headers = {"User-Agent": "genmeta ddns publish workflow"} + version_url = f"https://crates.io/api/v1/crates/{name}/{version}" + version_request = urllib.request.Request(version_url, headers=headers) + try: + with urllib.request.urlopen(version_request, timeout=20) as response: + if response.status == 200: + print("published_version") + else: + raise SystemExit(f"unexpected crates.io status for {name} {version}: {response.status}") + except urllib.error.HTTPError as error: + if error.code == 404: + crate_url = f"https://crates.io/api/v1/crates/{name}" + crate_request = urllib.request.Request(crate_url, headers=headers) + try: + with urllib.request.urlopen(crate_request, timeout=20) as response: + if response.status == 200: + print("missing_version") + else: + raise SystemExit(f"unexpected crates.io crate status for {name}: {response.status}") + except urllib.error.HTTPError as crate_error: + if crate_error.code == 404: + print("missing_crate") + else: + raise + else: + raise + PY + )" + + if [[ "$crate_state" == "published_version" ]]; then + echo "skip $package_name $package_version (already on crates.io)" + exit 0 + fi + + if [[ "$crate_state" == "missing_crate" ]]; then + echo "skip $package_name $package_version (crate not yet initialized on crates.io)" + exit 0 + fi + + echo "publish $package_name $package_version" + cargo publish --locked diff --git a/Cargo.toml b/Cargo.toml index 2bda2a1..b84ddea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ base64 = "0.22" bitfield-struct = "0.13" bytes = "1" dashmap = "6" -dhttp-identity = { git = "https://github.com/genmeta/dhttp.git", branch = "publish", version = "0.1.0" } -dquic = { git = "https://github.com/genmeta/dquic.git", tag = "v0.5.1", version = "0.5.1" } +dhttp-identity = "0.1.0" +dquic = "0.5.1" flume = "0.12" futures = "0.3" libc = "0.2" @@ -46,7 +46,7 @@ tokio = { version = "1", features = [ tracing = "0.1" x509-parser = "0.18" -h3x = { git = "https://github.com/genmeta/h3x.git", tag = "v0.3.0", version = "0.3.0", default-features = false, optional = true } +h3x = { version = "0.3.1", default-features = false, optional = true } http = { version = "1", optional = true } http-body = { version = "1", optional = true } http-body-util = { version = "0.1", optional = true } @@ -96,7 +96,7 @@ server = [ [dev-dependencies] clap = { version = "4", features = ["derive"] } -h3x = { git = "https://github.com/genmeta/h3x.git", tag = "v0.3.0", version = "0.3.0", default-features = false, features = [ +h3x = { version = "0.3.1", default-features = false, features = [ "dquic", ] } shellexpand = "3" From a1fd466f338b3f48c7e964c745ee604c3f276193 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 15 Jun 2026 16:29:12 +0800 Subject: [PATCH 85/85] ci: gate dry-run by registry publish eligibility --- .github/workflows/publish-crates.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 579324f..bfdffde 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -49,11 +49,6 @@ jobs: package_name=dyns package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" - if [[ "$mode" == "dry-run" ]]; then - cargo publish --dry-run --locked - exit 0 - fi - crate_state="$( python3 - <<'PY' "$package_name" "$package_version" import sys @@ -100,5 +95,11 @@ jobs: exit 0 fi + if [[ "$mode" == "dry-run" ]]; then + echo "dry-run $package_name $package_version" + cargo publish --dry-run --locked + exit 0 + fi + echo "publish $package_name $package_version" cargo publish --locked