From 953c0aa164cca976e93f2414f134db5660199585 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 22 Jun 2026 14:49:18 +0800 Subject: [PATCH 1/9] feat(home): add scoped dhttp home loading --- home/build.rs | 24 +++++++ home/src/bootstrap.rs | 1 + home/src/lib.rs | 163 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 home/build.rs create mode 100644 home/src/bootstrap.rs diff --git a/home/build.rs b/home/build.rs new file mode 100644 index 0000000..4ff37a0 --- /dev/null +++ b/home/build.rs @@ -0,0 +1,24 @@ +use std::{env, fs, path::PathBuf}; + +const GLOBAL_HOME_ENV: &str = "DHTTP_GLOBAL_HOME"; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); + let bootstrap = format!( + "// @generated by build.rs; do not edit.\n\ + pub const DHTTP_GLOBAL_HOME: Option<&str> = {};\n", + option_expr(env::var(GLOBAL_HOME_ENV).ok().as_deref()) + ); + + fs::write(out_dir.join("bootstrap.rs"), bootstrap) + .expect("failed to write generated dhttp-home bootstrap constants"); + + println!("cargo::rerun-if-env-changed={GLOBAL_HOME_ENV}"); +} + +fn option_expr(value: Option<&str>) -> String { + match value { + Some(value) => format!("Some({value:?})"), + None => "None".to_owned(), + } +} diff --git a/home/src/bootstrap.rs b/home/src/bootstrap.rs new file mode 100644 index 0000000..aa8c097 --- /dev/null +++ b/home/src/bootstrap.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/bootstrap.rs")); diff --git a/home/src/lib.rs b/home/src/lib.rs index ee91eda..c9cef2c 100644 --- a/home/src/lib.rs +++ b/home/src/lib.rs @@ -1,11 +1,23 @@ pub mod identity; +mod bootstrap; + use std::path::{Path, PathBuf}; #[cfg(any(unix, windows))] use snafu::OptionExt; use snafu::Snafu; +const USER_HOME_ENV: &str = "DHTTP_HOME"; +const GLOBAL_HOME_ENV: &str = "DHTTP_GLOBAL_HOME"; +const DEFAULT_UNIX_GLOBAL_HOME: &str = "/etc/dhttp"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HomeScope { + User, + Global, +} + /// A handle to the user's dhttp home directory (e.g. `~/.dhttp/`). /// /// `DhttpHome` describes a directory that contains per-identity profiles and @@ -28,6 +40,20 @@ pub enum LocateDhttpHomeError { UnsupportedPlatform {}, } +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum LoadDhttpHomeError { + #[cfg(any(unix, windows))] + #[snafu(display("cannot locate user home directory"))] + NoUserHome {}, + #[snafu(display("global dhttp home is not configured"))] + GlobalHomeNotConfigured {}, + #[snafu(display( + "dhttp home cannot be automatically located on this platform, try setting DHTTP_HOME environment variable" + ))] + UnsupportedPlatform {}, +} + impl DhttpHome { pub const DIR_NAME: &str = ".dhttp"; @@ -39,8 +65,23 @@ impl DhttpHome { Self::new(home_dir.into().join(Self::DIR_NAME)) } + pub fn load(scope: HomeScope) -> Result { + match scope { + HomeScope::User => Ok(Self::new(resolve_user_home_path( + std::env::var_os(USER_HOME_ENV).map(PathBuf::from), + user_home_dir(), + )?)), + HomeScope::Global => Ok(Self::new(resolve_global_home_path( + std::env::var_os(GLOBAL_HOME_ENV).map(PathBuf::from), + bootstrap::DHTTP_GLOBAL_HOME, + platform_default_global_home(), + )?)), + } + } + + #[deprecated(note = "use DhttpHome::load(HomeScope::User) instead")] pub fn load_from_environment() -> Result { - if let Some(path) = std::env::var_os("DHTTP_HOME") { + if let Some(path) = std::env::var_os(USER_HOME_ENV) { return Ok(Self::new(PathBuf::from(path))); } @@ -67,3 +108,123 @@ impl AsRef for DhttpHome { self.as_path() } } + +fn resolve_user_home_path( + runtime_home: Option, + user_home_dir: Option, +) -> Result { + if let Some(path) = runtime_home { + return Ok(path); + } + + #[cfg(any(unix, windows))] + let home_dir = user_home_dir.context(load_dhttp_home_error::NoUserHomeSnafu)?; + + #[cfg(not(any(unix, windows)))] + let home_dir = user_home_dir.context(load_dhttp_home_error::UnsupportedPlatformSnafu)?; + + Ok(home_dir.join(DhttpHome::DIR_NAME)) +} + +fn resolve_global_home_path( + runtime_home: Option, + compiled_home: Option<&str>, + default_home: Option<&str>, +) -> Result { + if let Some(path) = runtime_home { + return Ok(path); + } + if let Some(path) = compiled_home { + return Ok(PathBuf::from(path)); + } + if let Some(path) = default_home { + return Ok(PathBuf::from(path)); + } + + load_dhttp_home_error::GlobalHomeNotConfiguredSnafu.fail() +} + +fn user_home_dir() -> Option { + #[cfg(any(unix, windows))] + { + return dirs::home_dir(); + } + + #[allow(unreachable_code)] + None +} + +fn platform_default_global_home() -> Option<&'static str> { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + return Some(DEFAULT_UNIX_GLOBAL_HOME); + } + + #[allow(unreachable_code)] + None +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{LoadDhttpHomeError, resolve_global_home_path, resolve_user_home_path}; + + #[test] + fn user_scope_path_prefers_runtime_home_env() { + let path = resolve_user_home_path( + Some(PathBuf::from("/runtime/dhttp-home")), + Some(PathBuf::from("/home/reimu")), + ) + .expect("user path should resolve"); + + assert_eq!(path, PathBuf::from("/runtime/dhttp-home")); + } + + #[test] + fn user_scope_path_falls_back_to_user_home_dir() { + let path = resolve_user_home_path(None, Some(PathBuf::from("/home/reimu"))) + .expect("user path should resolve"); + + assert_eq!(path, PathBuf::from("/home/reimu/.dhttp")); + } + + #[test] + fn global_scope_path_prefers_runtime_env_over_compile_time_and_default() { + let path = resolve_global_home_path( + Some(PathBuf::from("/runtime/global")), + Some("/compiled/global"), + Some("/etc/dhttp"), + ) + .expect("global path should resolve"); + + assert_eq!(path, PathBuf::from("/runtime/global")); + } + + #[test] + fn global_scope_path_uses_compile_time_home_when_runtime_is_missing() { + let path = resolve_global_home_path(None, Some("/compiled/global"), Some("/etc/dhttp")) + .expect("global path should resolve"); + + assert_eq!(path, PathBuf::from("/compiled/global")); + } + + #[test] + fn global_scope_path_uses_platform_default_when_overrides_are_missing() { + let path = resolve_global_home_path(None, None, Some("/etc/dhttp")) + .expect("global path should resolve"); + + assert_eq!(path, PathBuf::from("/etc/dhttp")); + } + + #[test] + fn global_scope_path_errors_when_no_source_is_available() { + let error = resolve_global_home_path(None, None, None) + .expect_err("missing global path sources must fail"); + + assert!(matches!( + error, + LoadDhttpHomeError::GlobalHomeNotConfigured {} + )); + } +} From b650bc25eca963eeb9b560b644e94375659da25e Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 24 Jun 2026 00:26:28 +0800 Subject: [PATCH 2/9] fix(dhttp): isolate h3 dns client endpoints Build dedicated H3-capable QUIC clients for endpoint DNS resolver and publisher paths instead of reusing arbitrary serving endpoint transport config. --- dhttp/src/ddns.rs | 199 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 183 insertions(+), 16 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index 3f47405..5446ac4 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -108,6 +108,46 @@ impl DhttpDnsPlan { } type DeferredEndpointResolver = resolvers::deferred::DeferredResolver; +type EndpointH3Client = Arc>; + +#[derive(Clone)] +struct EndpointH3Clients { + resolver: EndpointH3Client, + publisher: EndpointH3Client, +} + +#[derive(Clone)] +struct EndpointH3Connector { + quic: QuicEndpoint, +} + +impl EndpointH3Connector { + fn new(quic: QuicEndpoint) -> Self { + Self { quic } + } +} + +impl crate::h3x::quic::Connect for EndpointH3Connector { + type Connection = crate::dquic::connection::Connection; + type Error = crate::dquic::ConnectError; + + async fn connect( + &self, + server: &::http::uri::Authority, + ) -> Result, Self::Error> { + crate::h3x::quic::Connect::connect(&self.quic, server).await + } +} + +impl crate::h3x::quic::WithLocalAuthority for EndpointH3Connector { + type LocalAuthority = crate::dquic::Identity; + + async fn local_authority( + &self, + ) -> Result, crate::h3x::quic::ConnectionError> { + crate::h3x::quic::WithLocalAuthority::local_authority(&self.quic).await + } +} #[derive(Debug, snafu::Snafu)] #[snafu(module(build_dhttp_network_with_dns_error))] @@ -187,12 +227,8 @@ async fn network_stun_resolver_from_plan( let operations = dns_plan.effective_ops(); let h3_resolver = if uses_h3(&operations) { let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()).await?; - let h3_quic = QuicEndpoint::builder() - .network(network.clone()) - .resolver(h3_underlay) - .bind(bind.clone()) - .build() - .await; + let h3_quic = + dedicated_network_h3_client_quic(network.clone(), bind.clone(), h3_underlay).await; Some(Arc::new(h3_resolver_for_network( h3_dns_server.as_ref(), h3_quic, @@ -237,10 +273,7 @@ async fn endpoint_dns_from_quic( ) -> Result<(resolvers::Resolvers, publishers::Publishers), BuildQuicEndpointWithDnsError> { let operations = dns_plan.effective_ops(); let endpoint_h3 = if uses_h3(&operations) { - let h3_underlay = endpoint_h3_underlay(&operations, endpoint).await?; - let mut h3_quic = endpoint.clone(); - h3_quic.set_resolver(h3_underlay); - Some(Arc::new(H3Endpoint::new(h3_quic))) + Some(endpoint_h3_clients_from_quic(&operations, endpoint).await?) } else { None }; @@ -277,14 +310,18 @@ async fn endpoint_dns_from_quic( let h3_endpoint = endpoint_h3 .clone() .expect("BUG: endpoint H3 endpoint exists when H3 DNS is used"); - let h3 = Arc::new(h3_resolver_for_endpoint( + let h3_resolver = Arc::new(h3_resolver_for_endpoint( + h3_dns_server.as_ref(), + h3_endpoint.resolver, + )?); + let h3_publisher = Arc::new(h3_resolver_for_endpoint( h3_dns_server.as_ref(), - h3_endpoint, + h3_endpoint.publisher, )?); - resolver_builder = resolver_builder.resolver(h3.clone()); + resolver_builder = resolver_builder.resolver(h3_resolver); publishers.push(publishers::Publisher::new( publishers::PublishScope::WideArea, - h3, + h3_publisher, )); } DhttpDnsOp::Resolver(resolver) => { @@ -300,6 +337,55 @@ async fn endpoint_dns_from_quic( Ok((resolvers, publishers)) } +async fn endpoint_h3_clients_from_quic( + operations: &[DhttpDnsOp], + endpoint: &QuicEndpoint, +) -> Result { + let h3_underlay = endpoint_h3_underlay(operations, endpoint).await?; + let resolver_quic = dedicated_h3_client_quic(endpoint, h3_underlay.clone()).await; + let publisher_quic = dedicated_h3_client_quic(endpoint, h3_underlay).await; + + Ok(EndpointH3Clients { + // Endpoint-facing DNS resolution and publication can run concurrently + // while the endpoint is also serving traffic. Keep separate H3 pools + // and dedicated QUIC endpoints. The H3 DNS clients must always use + // DHTTP's H3-capable trust/ALPN defaults instead of inheriting an + // arbitrary serving endpoint transport config; callers such as pishoo + // may construct the serving QUIC endpoint directly and omit H3 ALPNs. + // Preserve the serving identity so authenticated H3 DNS publish can + // still sign requests and present client certificates. + resolver: Arc::new(H3Endpoint::new(EndpointH3Connector::new(resolver_quic))), + publisher: Arc::new(H3Endpoint::new(EndpointH3Connector::new(publisher_quic))), + }) +} + +async fn dedicated_h3_client_quic(endpoint: &QuicEndpoint, resolver: ArcResolver) -> QuicEndpoint { + QuicEndpoint::builder() + .network(endpoint.network().clone()) + .maybe_identity(endpoint.identity()) + .resolver(resolver) + .client(crate::trust::default_client_quic_config()) + .server(crate::trust::default_server_quic_config()) + .bind(endpoint.bind_patterns().clone()) + .build() + .await +} + +async fn dedicated_network_h3_client_quic( + network: Arc, + bind: Arc>, + resolver: ArcResolver, +) -> QuicEndpoint { + QuicEndpoint::builder() + .network(network) + .resolver(resolver) + .client(crate::trust::default_client_quic_config()) + .server(crate::trust::default_server_quic_config()) + .bind(bind) + .build() + .await +} + async fn endpoint_h3_underlay( operations: &[DhttpDnsOp], endpoint: &QuicEndpoint, @@ -369,8 +455,8 @@ fn h3_resolver_for_network( fn h3_resolver_for_endpoint( h3_dns_server: &str, - h3: Arc>, -) -> Result, BuildQuicEndpointWithDnsError> { + h3: EndpointH3Client, +) -> Result, BuildQuicEndpointWithDnsError> { resolvers::H3Resolver::from_endpoint(h3_dns_server, h3) .context(build_quic_endpoint_with_dns_error::InvalidH3DnsServerSnafu) } @@ -435,6 +521,7 @@ fn has_system_dns(operations: &[DhttpDnsOp]) -> bool { #[cfg(test)] mod tests { + use std::str::FromStr; use std::{ fmt, sync::{ @@ -580,4 +667,84 @@ mod tests { ); assert!(publishers.iter().next().is_none()); } + + #[tokio::test] + async fn endpoint_h3_dns_clients_split_resolver_and_publisher_connectors() { + let endpoint = crate::dquic::QuicEndpoint::builder().build().await; + let operations = vec![DhttpDnsOp::Dns(resolvers::DnsScheme::H3)]; + let mut source_quic = endpoint.clone(); + let source_client = (*source_quic.client_config_mut()).clone(); + let source_server = (*source_quic.server_config_mut()).clone(); + + let clients = endpoint_h3_clients_from_quic(&operations, &endpoint) + .await + .expect("h3 dns clients should build"); + + assert!( + !Arc::ptr_eq(&clients.resolver, &clients.publisher), + "resolver and publisher must not share the same H3 endpoint pool" + ); + assert!( + Arc::ptr_eq(clients.resolver.quic().quic.network(), endpoint.network()), + "resolver h3 dns client should stay on the serving endpoint network" + ); + assert!( + Arc::ptr_eq(clients.publisher.quic().quic.network(), endpoint.network()), + "publisher h3 dns client should stay on the serving endpoint network" + ); + + let mut resolver_quic = clients.resolver.quic().quic.clone(); + let resolver_client = (*resolver_quic.client_config_mut()).clone(); + let resolver_server = (*resolver_quic.server_config_mut()).clone(); + let mut publisher_quic = clients.publisher.quic().quic.clone(); + let publisher_client = (*publisher_quic.client_config_mut()).clone(); + let publisher_server = (*publisher_quic.server_config_mut()).clone(); + + assert_eq!(resolver_client, crate::trust::default_client_quic_config()); + assert_eq!(publisher_client, crate::trust::default_client_quic_config()); + assert_eq!(resolver_server, crate::trust::default_server_quic_config()); + assert_eq!(publisher_server, crate::trust::default_server_quic_config()); + assert!( + source_client.alpns.is_empty() && source_server.alpns.is_empty(), + "test source endpoint should keep the raw quic defaults so dedicated H3 DNS clients prove they install DHTTP H3 defaults independently" + ); + assert!( + Arc::ptr_eq( + clients.resolver.quic().quic.resolver(), + clients.publisher.quic().quic.resolver() + ), + "resolver and publisher h3 dns clients should share the same dedicated underlay resolver chain" + ); + assert!( + !Arc::ptr_eq( + clients.publisher.quic().quic.resolver(), + endpoint.resolver() + ), + "publisher h3 dns client should override the serving endpoint resolver with the dedicated underlay resolver" + ); + } + + #[tokio::test] + async fn network_h3_stun_resolver_quic_uses_dhttp_h3_defaults() { + let network = crate::dquic::Network::builder().build(); + let bind = Arc::new(vec![ + crate::dquic::binds::BindPattern::from_str("*") + .expect("wildcard bind pattern should parse"), + ]); + let operations = vec![DhttpDnsOp::Dns(resolvers::DnsScheme::H3)]; + let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()) + .await + .expect("network h3 underlay should build"); + + let mut quic = dedicated_network_h3_client_quic(network, bind, h3_underlay).await; + + assert_eq!( + (*quic.client_config_mut()).clone(), + crate::trust::default_client_quic_config() + ); + assert_eq!( + (*quic.server_config_mut()).clone(), + crate::trust::default_server_quic_config() + ); + } } From 8cdcc1f07600c14aa583967a53fd89c77d18aac1 Mon Sep 17 00:00:00 2001 From: eareimu Date: Mon, 22 Jun 2026 15:49:24 +0800 Subject: [PATCH 3/9] feat(dhttp): use ddns stun bootstrap names --- access/src/db/mod.rs | 1 + api/src/home.rs | 1 + dhttp/build.rs | 4 ++-- dhttp/src/endpoint.rs | 17 +++++++++++++---- dhttp/src/message.rs | 9 +++++++++ dhttp/src/network.rs | 15 ++++++--------- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/access/src/db/mod.rs b/access/src/db/mod.rs index a8f2aa2..0eacf11 100644 --- a/access/src/db/mod.rs +++ b/access/src/db/mod.rs @@ -51,6 +51,7 @@ pub enum AccessDbError { InitializeDatabase { source: sea_orm::DbErr }, } +#[allow(deprecated)] pub fn load_dhttp_home() -> Result { DhttpHome::load_from_environment().context(LocateDhttpHomeSnafu) } diff --git a/api/src/home.rs b/api/src/home.rs index d8ae20c..1a44fd0 100644 --- a/api/src/home.rs +++ b/api/src/home.rs @@ -14,6 +14,7 @@ pub struct IdentityProfile(dhttp::home::identity::IdentityProfile); impl DhttpHome { pub fn load() -> Result { + #[allow(deprecated)] dhttp::home::DhttpHome::load_from_environment() .map(Self) .map_err(|error| DhttpError::from_error("home.load", error)) diff --git a/dhttp/build.rs b/dhttp/build.rs index 115a959..88a2131 100644 --- a/dhttp/build.rs +++ b/dhttp/build.rs @@ -3,7 +3,7 @@ use std::{env, fs, path::PathBuf}; const ROOT_CA_ENV: &str = "DHTTP_ROOT_CA"; const STUN_SERVER_ENV: &str = "DHTTP_STUN_SERVER"; -const DEFAULT_STUN_SERVER: &str = "dhttp.example.net:1"; +const DEFAULT_STUN_SERVER: &str = "stun.dhttp.example.net"; const DEFAULT_ROOT_CA_PEM: &str = "\ -----BEGIN CERTIFICATE-----\n\ MIIDKTCCAhGgAwIBAgIUHNScq6R2U5QYUzxkEkNDaOJt4yMwDQYJKoZIhvcNAQEL\n\ @@ -84,7 +84,7 @@ mod tests { assert_eq!( env_or_default(&name, DEFAULT_STUN_SERVER), - "dhttp.example.net:1" + "stun.dhttp.example.net" ); } diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 6fe5997..9945639 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -88,10 +88,11 @@ pub enum CreateEndpointPublicationLoopError { AnonymousEndpoint, } -/// Default STUN server for NAT traversal. +/// Default STUN bootstrap name for NAT traversal. /// -/// STUN server resolution uses this authority so the well-known port remains -/// part of the query. +/// DDNS resolution of this name returns the actual socket addresses and ports +/// from endpoint `E` records; the bootstrap value itself is a logical lookup +/// name, not a raw `host:port` transport authority. pub const STUN_SERVER: &str = crate::bootstrap::DHTTP_STUN_SERVER; fn normalize_bind(bind: Arc>) -> Arc> { @@ -393,6 +394,7 @@ impl Endpoint { let name = name .try_into() .context(load_endpoint_error::InvalidNameSnafu)?; + #[allow(deprecated)] let home = crate::home::DhttpHome::load_from_environment() .context(load_endpoint_error::NoHomeSnafu)?; @@ -621,6 +623,13 @@ mod tests { } } + #[test] + fn stun_server_placeholder_is_plain_name_when_compile_time_env_is_absent() { + if option_env!("DHTTP_STUN_SERVER").is_none() { + assert_eq!(STUN_SERVER, "stun.dhttp.example.net"); + } + } + #[tokio::test] async fn check_builder_api() { let endpoint = Arc::new( @@ -1067,7 +1076,7 @@ mod tests { .network() .quic() .stun_resolver() - .lookup("stun.example.test:3478") + .lookup("stun.example.test") .await .expect("custom STUN resolver should be called"); diff --git a/dhttp/src/message.rs b/dhttp/src/message.rs index d7cc0e0..ad3ec81 100644 --- a/dhttp/src/message.rs +++ b/dhttp/src/message.rs @@ -2060,6 +2060,15 @@ mod tests { assert_eq!(authority.as_str(), "alice@reimu.pilot.dhttp.net:443"); } + #[test] + fn into_authority_expands_dhttp_shorthand_selector_with_base() { + let self_name = "self.host".parse::().unwrap(); + + let authority = "reimu.hakurei~:0".into_authority(Some(&self_name)).unwrap(); + + assert_eq!(authority.as_str(), "reimu.hakurei.dhttp.net:0"); + } + #[test] fn into_authority_rejects_bare_tilde_without_base() { let error = "~".into_authority(None).unwrap_err(); diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 70d3c9f..bedc6ab 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -242,14 +242,14 @@ mod tests { #[tokio::test] async fn builder_allows_custom_stun_server() { let dhttp_network = DhttpNetwork::builder() - .stun_server(Some(Arc::from("custom.stun.example:3478"))) + .stun_server(Some(Arc::from("custom.stun.example"))) .build() .await .expect("network should build with custom stun server"); assert_eq!( dhttp_network.network().quic().stun_server().as_deref(), - Some("custom.stun.example:3478") + Some("custom.stun.example") ); } @@ -267,7 +267,7 @@ mod tests { .iface_manager(iface_manager.clone()) .io_factory(io_factory.clone()) .stun_resolver(stun_resolver.clone()) - .stun_server(Some(Arc::from("builder.stun.example:3478"))) + .stun_server(Some(Arc::from("builder.stun.example"))) .quic_router(quic_router.clone()) .locations(locations.clone()) .build() @@ -278,16 +278,13 @@ mod tests { assert!(Arc::ptr_eq(&quic.iface_manager(), &iface_manager)); assert!(Arc::ptr_eq(&quic.io_factory(), &io_factory)); assert!(Arc::ptr_eq(&quic.stun_resolver(), &stun_resolver)); - assert_eq!( - quic.stun_server().as_deref(), - Some("builder.stun.example:3478") - ); + assert_eq!(quic.stun_server().as_deref(), Some("builder.stun.example")); assert!(Arc::ptr_eq(&quic.quic_router(), &quic_router)); assert!(Arc::ptr_eq(&quic.locations(), &locations)); } #[tokio::test] - async fn builder_derives_stun_resolver_from_custom_resolver() { + async fn builder_derives_stun_resolver_from_custom_resolver_without_literal_port() { use futures::StreamExt; let calls = Arc::new(AtomicUsize::new(0)); @@ -304,7 +301,7 @@ mod tests { .network() .quic() .stun_resolver() - .lookup("stun.example.test:3478") + .lookup("stun.example.test") .await .expect("custom resolver should resolve STUN server"); From 3c0c7ff680c212d888a41cff9eb9819cba6857f2 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 24 Jun 2026 18:46:29 +0800 Subject: [PATCH 4/9] feat(network): consume typed local endpoint hub --- Cargo.toml | 2 +- dhttp/src/network.rs | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 223aeeb..7a63de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ ddns = { package = "dyns", version = "0.4.0", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.4.0", features = [ +h3x = { version = "0.4.1", features = [ "dquic", ] } dhttp = { path = "dhttp", version = "0.2.0" } diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index bedc6ab..9c628b2 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -9,7 +9,9 @@ use crate::ddns::{ use crate::dquic::{ Network, binds::BindPattern, - net::{Devices, InterfaceManager, Locations, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY}, + net::{ + Devices, InterfaceManager, LocalEndpoints, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY, + }, }; pub(crate) type ArcResolvers = Arc; @@ -104,7 +106,7 @@ impl DhttpNetwork { >, #[builder(default = Arc::new(DEFAULT_IO_FACTORY))] io_factory: Arc, #[builder(default = Arc::new(QuicRouter::new()))] quic_router: Arc, - #[builder(default = Arc::new(Locations::new()))] locations: Arc, + #[builder(default = Arc::new(LocalEndpoints::new()))] local_endpoints: Arc, ) -> Result { let stun_server = stun_server.unwrap_or_else(|| Some(Arc::::from(crate::endpoint::STUN_SERVER))); @@ -117,7 +119,7 @@ impl DhttpNetwork { .iface_manager(iface_manager) .io_factory(io_factory) .quic_router(quic_router) - .locations(locations) + .local_endpoints(local_endpoints) .build(); return Ok(Self { network, @@ -135,7 +137,7 @@ impl DhttpNetwork { .iface_manager(iface_manager) .io_factory(io_factory) .quic_router(quic_router) - .locations(locations) + .local_endpoints(local_endpoints) .build() }, &dns_plan, @@ -261,7 +263,7 @@ mod tests { let stun_resolver: Arc = Arc::new(crate::dquic::resolver::handy::SystemResolver); let quic_router = Arc::new(crate::dquic::net::QuicRouter::new()); - let locations = Arc::new(crate::dquic::net::Locations::new()); + let local_endpoints = Arc::new(crate::dquic::net::LocalEndpoints::new()); let dhttp_network = DhttpNetwork::builder() .iface_manager(iface_manager.clone()) @@ -269,7 +271,7 @@ mod tests { .stun_resolver(stun_resolver.clone()) .stun_server(Some(Arc::from("builder.stun.example"))) .quic_router(quic_router.clone()) - .locations(locations.clone()) + .local_endpoints(local_endpoints.clone()) .build() .await .expect("network should build with forwarded options"); @@ -280,7 +282,7 @@ mod tests { assert!(Arc::ptr_eq(&quic.stun_resolver(), &stun_resolver)); assert_eq!(quic.stun_server().as_deref(), Some("builder.stun.example")); assert!(Arc::ptr_eq(&quic.quic_router(), &quic_router)); - assert!(Arc::ptr_eq(&quic.locations(), &locations)); + assert!(Arc::ptr_eq(&quic.local_endpoints(), &local_endpoints)); } #[tokio::test] From 091f83763e59668e960fd9db728b1eec8bfd740c Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 25 Jun 2026 17:44:26 +0800 Subject: [PATCH 5/9] chore: prepare dhttp 0.3.0 release --- .github/workflows/publish-crates.yml | 13 +++++++++++-- Cargo.toml | 8 ++++---- access/Cargo.toml | 2 +- api/package-lock.json | 4 ++-- api/package.json | 2 +- home/Cargo.toml | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index f63d84f..82f172b 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -25,9 +25,18 @@ jobs: - name: Install Rust stable toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Check formatting + run: cargo fmt -- --check + + - name: Run clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: Test workspace run: cargo test --workspace --all-features --all-targets + - name: Validate workspace publish graph + run: cargo publish --workspace --exclude dhttp-api --dry-run + - name: Authenticate to crates.io if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') uses: rust-lang/crates-io-auth-action@v1 @@ -126,10 +135,10 @@ jobs: fi if [[ "$mode" == "dry-run" ]]; then - publish_args=(cargo publish --dry-run --locked) + publish_args=(cargo publish --dry-run) echo "dry-run publish packages: ${packages_to_publish[*]}" else - publish_args=(cargo publish --locked) + publish_args=(cargo publish) echo "publish packages: ${packages_to_publish[*]}" fi diff --git a/Cargo.toml b/Cargo.toml index 7a63de7..a855058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["dhttp", "identity", "home", "api", "access"] [workspace.package] -version = "0.2.0" +version = "0.3.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/genmeta/dhttp" @@ -40,7 +40,7 @@ chrono = { version = "0.4", features = ["serde"] } # release dependencies resolve through crates.io in the formal release graph. dhttp-identity = "0.2.0" dhttp-home = { path = "home", version = "0.2.0" } -ddns = { package = "dyns", version = "0.4.0", features = [ +ddns = { package = "dyns", version = "0.5.0", features = [ "resolvers", "publishers", "h3", @@ -48,8 +48,8 @@ ddns = { package = "dyns", version = "0.4.0", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.4.1", features = [ +h3x = { version = "0.5.0", features = [ "dquic", ] } -dhttp = { path = "dhttp", version = "0.2.0" } +dhttp = { path = "dhttp", version = "0.3.0" } dhttp-access = { path = "access", version = "0.2.0" } diff --git a/access/Cargo.toml b/access/Cargo.toml index db608d2..76ff073 100644 --- a/access/Cargo.toml +++ b/access/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-access" description = "Identity-aware access control primitives for DHttp" -version.workspace = true +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/api/package-lock.json b/api/package-lock.json index 285f365..8811917 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genmeta/dhttp", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genmeta/dhttp", - "version": "0.2.0", + "version": "0.3.0", "devDependencies": { "@napi-rs/cli": "^3.3.5" } diff --git a/api/package.json b/api/package.json index ca72753..cca3b77 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@genmeta/dhttp", - "version": "0.2.0", + "version": "0.3.0", "description": "The True Internet", "license": "Apache-2.0", "homepage": "https://dhttp.net/", diff --git a/home/Cargo.toml b/home/Cargo.toml index 05024c0..700152a 100644 --- a/home/Cargo.toml +++ b/home/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-home" description = "Local identity home and profile management for DHttp" -version.workspace = true +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true From 0e1e60705a9684aaf59db37834fe25ff88a50f68 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 26 Jun 2026 07:42:25 +0800 Subject: [PATCH 6/9] ci: validate only publishable crate candidates --- .github/workflows/publish-crates.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 82f172b..5324945 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -34,9 +34,6 @@ jobs: - name: Test workspace run: cargo test --workspace --all-features --all-targets - - name: Validate workspace publish graph - run: cargo publish --workspace --exclude dhttp-api --dry-run - - name: Authenticate to crates.io if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') uses: rust-lang/crates-io-auth-action@v1 From 42411387c6474fa052bb1a81c4cc900d95547932 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 26 Jun 2026 07:50:28 +0800 Subject: [PATCH 7/9] fix(home): gate unix global default by target --- home/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/home/src/lib.rs b/home/src/lib.rs index c9cef2c..063b330 100644 --- a/home/src/lib.rs +++ b/home/src/lib.rs @@ -10,6 +10,7 @@ use snafu::Snafu; const USER_HOME_ENV: &str = "DHTTP_HOME"; const GLOBAL_HOME_ENV: &str = "DHTTP_GLOBAL_HOME"; +#[cfg(any(target_os = "linux", target_os = "macos"))] const DEFAULT_UNIX_GLOBAL_HOME: &str = "/etc/dhttp"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] From f2eb44d9225a75e1ae80c94a04b736f115d2d09b Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 26 Jun 2026 14:19:47 +0800 Subject: [PATCH 8/9] chore(home): prepare dhttp-home 0.3.0 release --- Cargo.toml | 2 +- home/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a855058..5a58595 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ chrono = { version = "0.4", features = ["serde"] } # Keep same-repository workspace members as path dependencies. Cross-repo # release dependencies resolve through crates.io in the formal release graph. dhttp-identity = "0.2.0" -dhttp-home = { path = "home", version = "0.2.0" } +dhttp-home = { path = "home", version = "0.3.0" } ddns = { package = "dyns", version = "0.5.0", features = [ "resolvers", "publishers", diff --git a/home/Cargo.toml b/home/Cargo.toml index 700152a..2816e42 100644 --- a/home/Cargo.toml +++ b/home/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-home" description = "Local identity home and profile management for DHttp" -version = "0.2.0" +version = "0.3.0" edition.workspace = true license.workspace = true repository.workspace = true From 64cd960119d34457031e3f963d266e2fd01f00f0 Mon Sep 17 00:00:00 2001 From: eareimu Date: Fri, 26 Jun 2026 14:28:13 +0800 Subject: [PATCH 9/9] refactor(home): remove legacy home loading api --- access/src/db/identity.rs | 2 +- access/src/db/mod.rs | 5 ++--- api/src/home.rs | 3 +-- dhttp/src/endpoint.rs | 5 ++--- home/src/lib.rs | 31 +------------------------------ 5 files changed, 7 insertions(+), 39 deletions(-) diff --git a/access/src/db/identity.rs b/access/src/db/identity.rs index ebe7f84..d6394ec 100644 --- a/access/src/db/identity.rs +++ b/access/src/db/identity.rs @@ -1,2 +1,2 @@ -pub use dhttp_home::{DhttpHome, LocateDhttpHomeError}; +pub use dhttp_home::{DhttpHome, LoadDhttpHomeError}; pub use dhttp_identity::name::{DhttpName as Name, InvalidDhttpName as InvalidName}; diff --git a/access/src/db/mod.rs b/access/src/db/mod.rs index 0eacf11..1b39b29 100644 --- a/access/src/db/mod.rs +++ b/access/src/db/mod.rs @@ -28,7 +28,7 @@ pub const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000; pub enum AccessDbError { #[snafu(display("failed to locate DHTTP_HOME"))] LocateDhttpHome { - source: identity::LocateDhttpHomeError, + source: identity::LoadDhttpHomeError, }, #[snafu(display("access store does not exist at `{}`", path.display()))] @@ -51,9 +51,8 @@ pub enum AccessDbError { InitializeDatabase { source: sea_orm::DbErr }, } -#[allow(deprecated)] pub fn load_dhttp_home() -> Result { - DhttpHome::load_from_environment().context(LocateDhttpHomeSnafu) + DhttpHome::load(dhttp_home::HomeScope::User).context(LocateDhttpHomeSnafu) } pub fn access_db_path(home: &DhttpHome, identity: identity::Name<'_>) -> PathBuf { diff --git a/api/src/home.rs b/api/src/home.rs index 1a44fd0..24701f5 100644 --- a/api/src/home.rs +++ b/api/src/home.rs @@ -14,8 +14,7 @@ pub struct IdentityProfile(dhttp::home::identity::IdentityProfile); impl DhttpHome { pub fn load() -> Result { - #[allow(deprecated)] - dhttp::home::DhttpHome::load_from_environment() + dhttp::home::DhttpHome::load(dhttp::home::HomeScope::User) .map(Self) .map_err(|error| DhttpError::from_error("home.load", error)) } diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 9945639..e8cf4e7 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -229,7 +229,7 @@ where InvalidName { source: E }, #[snafu(display("failed to locate dhttp home"))] NoHome { - source: crate::home::LocateDhttpHomeError, + source: crate::home::LoadDhttpHomeError, }, #[snafu(display("failed to resolve identity profile"))] ResolveIdentityProfile { @@ -394,8 +394,7 @@ impl Endpoint { let name = name .try_into() .context(load_endpoint_error::InvalidNameSnafu)?; - #[allow(deprecated)] - let home = crate::home::DhttpHome::load_from_environment() + let home = crate::home::DhttpHome::load(crate::home::HomeScope::User) .context(load_endpoint_error::NoHomeSnafu)?; let profile = home diff --git a/home/src/lib.rs b/home/src/lib.rs index 063b330..a17b37b 100644 --- a/home/src/lib.rs +++ b/home/src/lib.rs @@ -4,9 +4,7 @@ mod bootstrap; use std::path::{Path, PathBuf}; -#[cfg(any(unix, windows))] -use snafu::OptionExt; -use snafu::Snafu; +use snafu::{OptionExt, Snafu}; const USER_HOME_ENV: &str = "DHTTP_HOME"; const GLOBAL_HOME_ENV: &str = "DHTTP_GLOBAL_HOME"; @@ -29,18 +27,6 @@ pub struct DhttpHome { path: PathBuf, } -#[derive(Debug, Snafu)] -#[snafu(module)] -pub enum LocateDhttpHomeError { - #[cfg(any(unix, windows))] - #[snafu(display("cannot locate user home directory"))] - NoUserHome {}, - #[snafu(display( - "dhttp home cannot be automatically located on this platform, try setting DHTTP_HOME environment variable" - ))] - UnsupportedPlatform {}, -} - #[derive(Debug, Snafu)] #[snafu(module)] pub enum LoadDhttpHomeError { @@ -80,21 +66,6 @@ impl DhttpHome { } } - #[deprecated(note = "use DhttpHome::load(HomeScope::User) instead")] - pub fn load_from_environment() -> Result { - if let Some(path) = std::env::var_os(USER_HOME_ENV) { - return Ok(Self::new(PathBuf::from(path))); - } - - #[cfg(any(unix, windows))] - return Ok(Self::for_user_home_dir( - dirs::home_dir().context(locate_dhttp_home_error::NoUserHomeSnafu)?, - )); - - #[allow(unreachable_code)] - locate_dhttp_home_error::UnsupportedPlatformSnafu.fail() - } - pub fn as_path(&self) -> &Path { self.path.as_path() }